Skip to content
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

Class fields feature #45

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyfields/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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',
Expand Down
80 changes: 71 additions & 9 deletions pyfields/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 24 additions & 1 deletion pyfields/tests/test_so.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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"