From b87e2492ccb7811cb07b8fe62c7ec42fc7cc35ce Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 13 Feb 2021 15:48:55 +0000 Subject: [PATCH] ensure cythonized functions are left untouched2 (#2228) * ensure cythonized functions are left untouched2 * add change --- changes/2228-samuelcolvin.md | 1 + pydantic/main.py | 9 +++++++-- setup.cfg | 1 + tests/test_edge_cases.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 changes/2228-samuelcolvin.md diff --git a/changes/2228-samuelcolvin.md b/changes/2228-samuelcolvin.md new file mode 100644 index 0000000..05d4606 --- /dev/null +++ b/changes/2228-samuelcolvin.md @@ -0,0 +1 @@ +ensure cythonized functions are left untouched when creating models, based on #1944 by @kollmats diff --git a/pydantic/main.py b/pydantic/main.py index c00cd1f..75ae920 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -259,6 +259,11 @@ class ModelMetaclass(ABCMeta): prepare_config(config, name) + untouched_types = ANNOTATED_FIELD_UNTOUCHED_TYPES + + def is_untouched(v: Any) -> bool: + return isinstance(v, untouched_types) or v.__class__.__name__ == 'cython_function_or_method' + if (namespace.get('__module__'), namespace.get('__qualname__')) != ('pydantic.main', 'BaseModel'): annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None)) # annotation only fields need to come first in fields @@ -270,7 +275,7 @@ class ModelMetaclass(ABCMeta): value = namespace.get(ann_name, Undefined) allowed_types = get_args(ann_type) if get_origin(ann_type) is Union else (ann_type,) if ( - isinstance(value, ANNOTATED_FIELD_UNTOUCHED_TYPES) + is_untouched(value) and ann_type != PyObject and not any( lenient_issubclass(get_origin(allowed_type), Type) for allowed_type in allowed_types @@ -289,7 +294,7 @@ class ModelMetaclass(ABCMeta): untouched_types = UNTOUCHED_TYPES + config.keep_untouched for var_name, value in namespace.items(): - can_be_changed = var_name not in class_vars and not isinstance(value, untouched_types) + can_be_changed = var_name not in class_vars and not is_untouched(value) if isinstance(value, ModelPrivateAttr): if not is_valid_private_name(var_name): raise NameError( diff --git a/setup.cfg b/setup.cfg index 0154d92..93865e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,7 @@ addopts = -p no:hypothesispytest filterwarnings = error ignore::DeprecationWarning:distutils + ignore::DeprecationWarning:Cython [flake8] max-line-length = 120 diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 04d34aa..5ad7347 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -20,6 +20,11 @@ from pydantic import ( ) from pydantic.fields import Field, Schema +try: + import cython +except ImportError: + cython = None + def test_str_bytes(): class Model(BaseModel): @@ -1745,3 +1750,26 @@ def test_default_factory_validator_child(): pass assert Child(foo=['a', 'b']).foo == ['a-1', 'b-1'] + + +@pytest.mark.skipif(cython is None, reason='cython not installed') +def test_cython_function_untouched(): + Model = cython.inline( + # language=Python + """ +from pydantic import BaseModel + +class Model(BaseModel): + a = 0.0 + b = 10 + + def get_double_a(self) -> float: + return self.a + self.b + +return Model +""" + ) + model = Model(a=10.2) + assert model.a == 10.2 + assert model.b == 10 + return model.get_double_a() == 20.2