diff --git a/pyfields/__init__.py b/pyfields/__init__.py index 6f426d1..90c1a2c 100644 --- a/pyfields/__init__.py +++ b/pyfields/__init__.py @@ -1,5 +1,5 @@ from .typing_utils import FieldTypeError -from .core import field, Field, FieldError, MandatoryFieldInitError, UnsupportedOnNativeFieldError, \ +from .core import field, classfield, Field, FieldError, MandatoryFieldInitError, UnsupportedOnNativeFieldError, \ ReadOnlyFieldError from .validate_n_convert import Converter, ConversionError, DetailedConversionResults, trace_convert from .init_makers import inject_fields, make_init, init_fields @@ -19,7 +19,7 @@ # submodules 'core', 'validate_n_convert', 'init_makers', 'helpers', # symbols - 'field', 'Field', 'FieldError', 'MandatoryFieldInitError', 'UnsupportedOnNativeFieldError', + 'field', 'classfield', 'Field', 'FieldError', 'MandatoryFieldInitError', 'UnsupportedOnNativeFieldError', 'ReadOnlyFieldError', 'FieldTypeError', 'Converter', 'ConversionError', 'DetailedConversionResults', 'trace_convert', 'inject_fields', 'make_init', 'init_fields', diff --git a/pyfields/core.py b/pyfields/core.py index 33b212d..b462698 100644 --- a/pyfields/core.py +++ b/pyfields/core.py @@ -671,6 +671,45 @@ class (such as `make_init`). If provided, it should be the same name than the on :return: """ # Should we create a Native or a Descriptor field ? + create_descriptor = _descriptor_needed(check_type, validators, converters, read_only, native) + + # Create the correct type of field + if create_descriptor: + return DescriptorField(type_hint=type_hint, default=default, default_factory=default_factory, + check_type=check_type, validators=validators, converters=converters, + read_only=read_only, doc=doc, name=name) + else: + return NativeField(type_hint=type_hint, default=default, default_factory=default_factory, + doc=doc, name=name) + + +def classfield(type_hint=None, # type: Union[Type[T], Iterable[Type[T]]] + check_type=False, # type: bool + default=EMPTY, # type: T + default_factory=None, # type: Callable[[], T] + validators=None, # type: Validators + converters=None, # type: Converters + read_only=False, # type: bool + doc=None, # type: str + name=None, # type: str + native=None # type: bool + ): + # type: (...) -> Union[T, Field] + # Should we create a Native or a Descriptor field ? + create_descriptor = _descriptor_needed(check_type, validators, converters, read_only, native) + + # Create the correct type of field + if create_descriptor: + return DescriptorClassField(type_hint=type_hint, default=default, default_factory=default_factory, + check_type=check_type, validators=validators, converters=converters, + read_only=read_only, doc=doc, name=name) + else: + return NativeClassField(type_hint=type_hint, default=default, default_factory=default_factory, + doc=doc, name=name) + + +def _descriptor_needed(check_type, validators, converters, read_only, native): + """ Should we create a Native or a Descriptor field ? """ if native is None: # default: choose automatically according to user-provided options create_descriptor = check_type or (validators is not None) or (converters is not None) or read_only @@ -687,15 +726,7 @@ class (such as `make_init`). If provided, it should be the same name than the on else: # explicit `native=False`. Force-use a descriptor create_descriptor = True - - # Create the correct type of field - if create_descriptor: - return DescriptorField(type_hint=type_hint, default=default, default_factory=default_factory, - check_type=check_type, validators=validators, converters=converters, - read_only=read_only, doc=doc, name=name) - else: - return NativeField(type_hint=type_hint, default=default, default_factory=default_factory, - doc=doc, name=name) + return create_descriptor class UnsupportedOnNativeFieldError(FieldError): @@ -779,6 +810,18 @@ def __get__(self, obj, obj_type): # pass +class NativeClassField(NativeField): + """ + A field that is replaced with a native python attribute on first read or write access. + Faster but provides not much flexibility (no validator, no type check, no converter) + """ + __slots__ = () + + def __get__(self, obj, obj_type): + # same than super but it acts on the object type + return super(NativeClassField, self).__get__(obj_type, obj_type) + + class DescriptorField(Field): """ General-purpose implementation for fields that require type-checking or validation or converter @@ -993,6 +1036,25 @@ def __delete__(self, obj): delattr(obj, "_" + self.name) +class DescriptorClassField(DescriptorField): + """ + A field that is replaced with a native python attribute on first read or write access. + Faster but provides not much flexibility (no validator, no type check, no converter) + """ + __slots__ = () + + def __get__(self, obj, obj_type): + # same than super but it acts on the object type + return super(DescriptorClassField, self).__get__(obj_type, obj_type) + + def __set__(self, + obj, + value # type: T + ): + # same than super but it acts on the object type + return super(DescriptorClassField, self).__set__(obj.__class__, value) + + def collect_all_fields(cls, include_inherited=True, remove_duplicates=True, diff --git a/pyfields/tests/test_so.py b/pyfields/tests/test_so.py index 2126d31..bb72764 100644 --- a/pyfields/tests/test_so.py +++ b/pyfields/tests/test_so.py @@ -4,7 +4,8 @@ import pytest -from pyfields import ReadOnlyFieldError +from pyfields import ReadOnlyFieldError, MandatoryFieldInitError, FieldTypeError +from pyfields.core import DescriptorClassField from valid8 import ValidationError @@ -206,3 +207,25 @@ class User(object): qualname = User.__dict__['username'].qualname assert str(exc_info.value) == "Read-only field '%s' has already been initialized on instance %s and cannot be " \ "modified anymore." % (qualname, u) + + +def test_so8_classfields(): + """ checks answer at xxx (todo: not capable of doing this yet) """ + + from pyfields import classfield + + class A(object): + s = classfield(type_hint=int, check_type=True) + + class ClassFromA(A): + pass + + s_field = A.__dict__['s'] + assert isinstance(s_field, DescriptorClassField) + + for c in (A, ClassFromA): + with pytest.raises(MandatoryFieldInitError): + print(c.s) + + with pytest.raises(FieldTypeError): + c.s = "hello"