-
-
Notifications
You must be signed in to change notification settings - Fork 638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce support for per field instance default values. #20949
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,18 +3,18 @@ | |
|
||
use std::fmt::Write; | ||
|
||
use crate::externs::address::Address; | ||
use pyo3::basic::CompareOp; | ||
use pyo3::exceptions::PyValueError; | ||
use pyo3::ffi; | ||
use pyo3::intern; | ||
use pyo3::prelude::*; | ||
use pyo3::types::PyType; | ||
|
||
use crate::externs::address::Address; | ||
|
||
pub fn register(m: &PyModule) -> PyResult<()> { | ||
m.add_class::<Field>()?; | ||
m.add_class::<NoFieldValue>()?; | ||
|
||
m.add_class::<FieldDefaultValue>()?; | ||
m.add("NO_VALUE", NoFieldValue)?; | ||
|
||
Ok(()) | ||
|
@@ -35,9 +35,34 @@ impl NoFieldValue { | |
} | ||
} | ||
|
||
#[pyclass] | ||
#[derive(Clone)] | ||
struct FieldDefaultValue { | ||
value: PyObject, | ||
} | ||
|
||
#[pymethods] | ||
impl FieldDefaultValue { | ||
#[new] | ||
fn __new__(value: PyObject) -> Self { | ||
FieldDefaultValue { value } | ||
} | ||
|
||
fn __str__(self_: &PyCell<Self>) -> PyResult<String> { | ||
Ok(format!("default({})", self_.borrow().value)) | ||
} | ||
|
||
fn __repr__(self_: &PyCell<Self>) -> PyResult<String> { | ||
Ok(format!("<FieldDefaultValue({})>", self_.borrow().value)) | ||
} | ||
} | ||
|
||
#[pyclass(subclass)] | ||
#[derive(Clone)] | ||
pub struct Field { | ||
value: PyObject, | ||
// Overrides the class var default per instance when set. | ||
default: Option<PyObject>, | ||
} | ||
|
||
#[pymethods] | ||
|
@@ -59,17 +84,28 @@ impl Field { | |
let raw_value = match raw_value { | ||
Some(value) | ||
if value.extract::<NoFieldValue>(py).is_ok() | ||
&& !Self::cls_none_is_valid_value(cls)? => | ||
&& !Self::cls_none_is_valid_value(cls, py)? => | ||
{ | ||
None | ||
} | ||
rv => rv, | ||
}; | ||
let maybe_default = match raw_value { | ||
Some(ref value) => { | ||
if let Ok(default) = value.extract::<FieldDefaultValue>(py) { | ||
Some(default.value) | ||
} else { | ||
None | ||
} | ||
} | ||
_ => None, | ||
}; | ||
|
||
Ok(Self { | ||
value: cls | ||
.call_method(intern!(py, "compute_value"), (raw_value, address), None)? | ||
.into(), | ||
default: maybe_default, | ||
}) | ||
} | ||
|
||
|
@@ -112,26 +148,32 @@ impl Field { | |
py: Python, | ||
) -> PyResult<PyObject> { | ||
let default = || -> PyResult<PyObject> { | ||
if Self::cls_required(cls)? { | ||
if Self::cls_required(cls, py)? { | ||
// TODO: Should be `RequiredFieldMissingException`. | ||
Err(PyValueError::new_err(format!( | ||
"The `{}` field in target {} must be defined.", | ||
Self::cls_alias(cls)?, | ||
Self::cls_alias(cls, py)?, | ||
*address, | ||
))) | ||
} else { | ||
Self::cls_default(cls) | ||
Self::cls_default(cls, py) | ||
} | ||
}; | ||
|
||
let none_is_valid_value = Self::cls_none_is_valid_value(cls)?; | ||
let none_is_valid_value = Self::cls_none_is_valid_value(cls, py)?; | ||
match raw_value { | ||
Some(value) if none_is_valid_value && value.extract::<NoFieldValue>(py).is_ok() => { | ||
default() | ||
} | ||
None if none_is_valid_value => Ok(py.None()), | ||
None => default(), | ||
Some(value) => Ok(value), | ||
Some(value) => { | ||
if let Ok(dyn_default) = value.extract::<FieldDefaultValue>(py) { | ||
Ok(dyn_default.value) | ||
} else { | ||
Ok(value) | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -144,28 +186,28 @@ impl Field { | |
Ok(self_.get_type().hash()? & self_.borrow().value.as_ref(py).hash()?) | ||
} | ||
|
||
fn __repr__(self_: &PyCell<Self>) -> PyResult<String> { | ||
fn __repr__(self_: &PyCell<Self>, py: Python) -> PyResult<String> { | ||
let mut result = String::new(); | ||
write!( | ||
result, | ||
"{}(alias={}, value={}", | ||
self_.get_type(), | ||
Self::cls_alias(self_)?, | ||
Self::cls_alias(self_, py)?, | ||
self_.borrow().value | ||
) | ||
.unwrap(); | ||
if let Ok(default) = self_.getattr("default") { | ||
if let Ok(default) = self_.getattr(intern!(py, "default")) { | ||
write!(result, ", default={})", default).unwrap(); | ||
} else { | ||
write!(result, ")").unwrap(); | ||
} | ||
Ok(result) | ||
} | ||
|
||
fn __str__(self_: &PyCell<Self>) -> PyResult<String> { | ||
fn __str__(self_: &PyCell<Self>, py: Python) -> PyResult<String> { | ||
Ok(format!( | ||
"{}={}", | ||
Self::cls_alias(self_)?, | ||
Self::cls_alias(self_, py)?, | ||
self_.borrow().value | ||
)) | ||
} | ||
|
@@ -188,32 +230,58 @@ impl Field { | |
_ => Ok(py.NotImplemented()), | ||
} | ||
} | ||
|
||
fn __getattribute__<'a>( | ||
self_: &'a PyCell<Self>, | ||
name: String, | ||
py: Python<'a>, | ||
) -> PyResult<*mut ffi::PyObject> { | ||
if name == "default" { | ||
if let Some(default) = &self_.extract::<Self>()?.default { | ||
// Return instance default, overriding the field class var default. | ||
return Ok(default.as_ptr()); | ||
} | ||
} | ||
|
||
unsafe { | ||
// The ffi::PyObject_GenericGetAttr() call is unsafe, so we need to be in an unsafe | ||
// context to call it. | ||
let slf = self_.borrow_mut().into_ptr(); | ||
let attr = name.into_py(py).into_ptr(); | ||
let res = ffi::PyObject_GenericGetAttr(slf, attr); | ||
if res.is_null() { | ||
Err(PyErr::fetch(py)) | ||
} else { | ||
Ok(res) | ||
} | ||
} | ||
} | ||
Comment on lines
+246
to
+258
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there ought to be a better way to do this... |
||
} | ||
|
||
impl Field { | ||
fn cls_none_is_valid_value(cls: &PyAny) -> PyResult<bool> { | ||
cls.getattr("none_is_valid_value")?.extract::<bool>() | ||
fn cls_none_is_valid_value(cls: &PyAny, py: Python) -> PyResult<bool> { | ||
cls.getattr(intern!(py, "none_is_valid_value"))? | ||
.extract::<bool>() | ||
} | ||
|
||
fn cls_default(cls: &PyAny) -> PyResult<PyObject> { | ||
cls.getattr("default")?.extract() | ||
fn cls_default(cls: &PyAny, py: Python) -> PyResult<PyObject> { | ||
cls.getattr(intern!(py, "default"))?.extract() | ||
} | ||
|
||
fn cls_required(cls: &PyAny) -> PyResult<bool> { | ||
cls.getattr("required")?.extract() | ||
fn cls_required(cls: &PyAny, py: Python) -> PyResult<bool> { | ||
cls.getattr(intern!(py, "required"))?.extract() | ||
} | ||
|
||
fn cls_alias(cls: &PyAny) -> PyResult<&str> { | ||
// TODO: All of these methods should use interned attr names. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fixed ^^ while I was at it.. |
||
cls.getattr("alias")?.extract() | ||
fn cls_alias<'a>(cls: &'a PyAny, py: Python<'a>) -> PyResult<&'a str> { | ||
cls.getattr(intern!(py, "alias"))?.extract() | ||
} | ||
|
||
fn cls_removal_version(cls: &PyAny) -> PyResult<Option<&str>> { | ||
cls.getattr("removal_version")?.extract() | ||
fn cls_removal_version<'a>(cls: &'a PyAny, py: Python<'a>) -> PyResult<Option<&'a str>> { | ||
cls.getattr(intern!(py, "removal_version"))?.extract() | ||
} | ||
|
||
fn cls_removal_hint(cls: &PyAny) -> PyResult<Option<&str>> { | ||
cls.getattr("removal_hint")?.extract() | ||
fn cls_removal_hint<'a>(cls: &'a PyAny, py: Python<'a>) -> PyResult<Option<&'a str>> { | ||
cls.getattr(intern!(py, "removal_hint"))?.extract() | ||
} | ||
|
||
fn check_deprecated( | ||
|
@@ -225,24 +293,24 @@ impl Field { | |
if address.is_generated_target() { | ||
return Ok(()); | ||
} | ||
let Some(removal_version) = Self::cls_removal_version(cls)? else { | ||
let Some(removal_version) = Self::cls_removal_version(cls, py)? else { | ||
return Ok(()); | ||
}; | ||
match raw_value { | ||
Some(value) if value.extract::<NoFieldValue>(py).is_ok() => return Ok(()), | ||
_ => (), | ||
} | ||
|
||
let Some(removal_hint) = Self::cls_removal_hint(cls)? else { | ||
let Some(removal_hint) = Self::cls_removal_hint(cls, py)? else { | ||
return Err(PyValueError::new_err( | ||
"You specified `removal_version` for {cls:?}, but not the class \ | ||
property `removal_hint`.", | ||
)); | ||
}; | ||
|
||
let alias = Self::cls_alias(cls)?; | ||
let deprecated = PyModule::import(py, "pants.base.deprecated")?; | ||
deprecated.getattr("warn_or_error")?.call( | ||
let alias = Self::cls_alias(cls, py)?; | ||
let deprecated = PyModule::import(py, intern!(py, "pants.base.deprecated"))?; | ||
deprecated.getattr(intern!(py, "warn_or_error"))?.call( | ||
( | ||
removal_version, | ||
format!("the {alias} field"), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This call (along with how to handle the returned value) was taken from https://github.com/PyO3/pyo3/blob/dc4f114d6772337d8b1568c6dbb0bf35deaa84dd/src/impl_/pyclass.rs#L212