Coverage for pyfields/core.py: 85%
357 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-06 16:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-06 16:35 +0000
1# Authors: Sylvain MARIE <sylvain.marie@se.com>
2# + All contributors to <https://github.com/smarie/python-pyfields>
3#
4# License: 3-clause BSD, <https://github.com/smarie/python-pyfields/blob/master/LICENSE>
5import sys
6from enum import Enum
7from textwrap import dedent
8from inspect import getmro
10try:
11 from inspect import signature, Parameter
12except ImportError:
13 # noinspection PyUnresolvedReferences,PyPackageRequirements
14 from funcsigs import signature, Parameter # noqa
16from valid8 import ValidationFailure, is_pep484_nonable
18from pyfields.typing_utils import assert_is_of_type, FieldTypeError, get_type_hints
19from pyfields.validate_n_convert import FieldValidator, make_converters_list, trace_convert
21try: # python 3.5+
22 # noinspection PyUnresolvedReferences
23 from typing import Callable, Type, Any, Union, Iterable, Tuple, TypeVar
24 _NoneType = type(None)
25 use_type_hints = sys.version_info > (3, 0)
26 if use_type_hints: 26 ↛ 36line 26 didn't jump to line 36, because the condition on line 26 was never false
27 T = TypeVar('T')
28 # noinspection PyUnresolvedReferences
29 from pyfields.validate_n_convert import ValidatorDef, Validators, Converters, ConverterFuncDefinition,\
30 DetailedConversionResults, ValidationFuncOrLambda, ValidType
32except ImportError:
33 use_type_hints = False
36USE_ADVANCED_TYPE_CHECKER = assert_is_of_type is not None
39PY36 = sys.version_info >= (3, 6)
40PY2 = sys.version_info < (3, 0)
41# PY35 = sys.version_info >= (3, 5)
43try:
44 object.__qualname__
45except AttributeError:
46 # old python without __qualname__
47 import re
48 RE_CLASS_NAME = re.compile("<class '(.*)'>")
50 def qualname(cls):
51 cls_str = str(cls)
52 match = RE_CLASS_NAME.match(cls_str)
53 if match:
54 return match.groups()[0]
55 else:
56 return cls_str
59class FieldError(Exception):
60 """
61 Base class for exceptions related to fields
62 """
63 pass
66class MandatoryFieldInitError(FieldError):
67 """
68 Raised by `field` when a mandatory field is read without being set first.
69 """
70 __slots__ = 'field_name', 'obj'
72 def __init__(self, field_name, obj):
73 self.field_name = field_name
74 self.obj = obj
76 def __str__(self):
77 return "Mandatory field '%s' has not been initialized yet on instance %s." % (self.field_name, self.obj)
80class ReadOnlyFieldError(FieldError):
81 """
82 Raised by descriptor field when a read-only attribute is accessed for writing
83 """
84 __slots__ = 'field_name', 'obj'
86 def __init__(self, field_name, obj):
87 self.field_name = field_name
88 self.obj = obj
90 def __str__(self):
91 return "Read-only field '%s' has already been initialized on instance %s and cannot be modified anymore." \
92 % (self.field_name, self.obj)
95class Symbols(Enum):
96 """
97 A few symbols used in `fields` for signatures
99 note: we used to use the great `sentinel` package to create these symbols one by one, but since we have
100 now quite a number of symbols, it seemed overkill to create one anonymous class for each.
102 Still, I am not sure if this made a perf difference actually.
103 """
104 GUESS = 0
105 UNKNOWN = 1
106 EMPTY = 2 # type: Any
107 USE_FACTORY = 3
108 _unset = 4
109 DELAYED = 5
111 def __repr__(self):
112 """ More compact representation for signatures readability"""
113 return self.name
116# GUESS = sentinel.create('guess')
117GUESS = Symbols.GUESS
119# UNKNOWN = sentinel.create('unknown')
120UNKNOWN = Symbols.UNKNOWN
122# EMPTY = sentinel.create('empty')
123EMPTY = Symbols.EMPTY
124DELAYED = Symbols.DELAYED
126# USE_FACTORY = sentinel.create('use_factory')
127USE_FACTORY = Symbols.USE_FACTORY
129# _unset = sentinel.create('_unset')
130_unset = Symbols._unset
133if not PY36: 133 ↛ 135line 133 didn't jump to line 135, because the condition on line 133 was never true
134 # a thread-safe lock for the global instance counter
135 from threading import Lock
136 threadLock = Lock()
139class Field(object):
140 """
141 Base class for fields
142 """
143 __slots__ = ('__weakref__', 'is_mandatory', 'default', 'is_default_factory', 'name', 'type_hint', 'nonable', 'doc',
144 'owner_cls', 'pending_validators', 'pending_converters')
145 if not PY36: 145 ↛ 148line 145 didn't jump to line 148, because the condition on line 145 was never true
146 # we need to count the instances created, so as to be able to track their order in classes
147 # indeed in python < 3.6, class members are not sorted by order of appearance.
148 __slots__ += ('__fieldinstcount__', )
149 __field_global_inst_counter__ = 0
151 def __init__(self,
152 default=EMPTY, # type: T
153 default_factory=None, # type: Callable[[], T]
154 type_hint=EMPTY, # type: Any
155 nonable=UNKNOWN, # type: Union[bool, Symbols]
156 doc=None, # type: str
157 name=None # type: str
158 ):
159 """See help(field) for details"""
161 if not PY36: 161 ↛ 162line 161 didn't jump to line 162, because the condition on line 161 was never true
162 with threadLock:
163 # remember the instance creation number, and increment the counter
164 self.__fieldinstcount__ = Field.__field_global_inst_counter__
165 Field.__field_global_inst_counter__ += 1
167 # default
168 if default_factory is not None:
169 self.is_mandatory = False
170 if default is not EMPTY: 170 ↛ 171line 170 didn't jump to line 171, because the condition on line 170 was never true
171 raise ValueError("Only one of `default` and `default_factory` should be provided")
172 else:
173 self.default = default_factory
174 self.is_default_factory = True
175 else:
176 self.is_mandatory = default is EMPTY
177 self.default = default
178 self.is_default_factory = False
180 # name
181 self.name = name
182 self.owner_cls = None
184 # doc
185 self.doc = dedent(doc) if doc is not None else None
187 # type hints
188 if type_hint is not EMPTY and type_hint is not None:
189 self.type_hint = type_hint
190 else:
191 self.type_hint = EMPTY
193 # nonable
194 if nonable is GUESS:
195 if self.default is None:
196 self.nonable = True
197 elif type_hint is not EMPTY and type_hint is not None:
198 if is_pep484_nonable(type_hint):
199 self.nonable = True
200 else:
201 self.nonable = UNKNOWN
202 else:
203 # set as unknown until type hint is set (in set_as_cls_member)
204 self.nonable = UNKNOWN
205 else:
206 self.nonable = nonable
208 # pending validators and converters
209 self.pending_validators = None
210 self.pending_converters = None
212 def set_as_cls_member(self,
213 owner_cls,
214 name,
215 owner_cls_type_hints=None,
216 type_hint=None
217 ):
218 """
219 Updates a field with all information available concerning how it is attached to the class.
221 - its owner class
222 - the name under which it is known in that class
223 - the type hints (python 3.6)
225 In python 3.6+ this is called directly at class creation time through the `__set_name__` callback.
227 In older python versions this is called whenever we have the opportunity :(, through `collect_fields`,
228 `fix_fields` and `fix_field`. We currently use the following strategies in python 2 and 3.5-:
230 - When users create a init method, `collect_fields` will be called when the init method is first accessed
231 - When users GET a native field, or GET or SET a descriptor field, `fix_field` will be called.
233 :param owner_cls:
234 :param name:
235 :param owner_cls_type_hints:
236 :param type_hint: you can provide the type hint directly
237 :return:
238 """
239 # set the owner class
240 self.owner_cls = owner_cls
242 if PY2 and isinstance(self, DescriptorField) and not issubclass(owner_cls, object): 242 ↛ 243line 242 didn't jump to line 243, because the condition on line 242 was never true
243 raise ValueError("descriptor fields can not be used on old-style classes under python 2.")
245 # check if the name provided as argument differ from the one provided
246 if self.name is not None:
247 if self.name != name: 247 ↛ 248line 247 didn't jump to line 248, because the condition on line 247 was never true
248 raise ValueError("field name '%s' in class '%s' does not correspond to explicitly declared name '%s' "
249 "in field constructor" % (name, owner_cls, self.name))
250 # already set correctly
251 else:
252 # set it
253 self.name = name
255 # if not already manually overridden, get the type hints if there are some in the owner class annotations
256 if self.type_hint is EMPTY or self.type_hint is DELAYED:
257 # first reconciliate both ways to get the hint
258 if owner_cls_type_hints is not None:
259 if type_hint is not None: 259 ↛ 260line 259 didn't jump to line 260, because the condition on line 259 was never true
260 raise ValueError("Provide either owner_cls_type_hints or type_hint, not both")
261 type_hint = owner_cls_type_hints.get(name)
263 # then use it
264 if type_hint is not None:
265 # only use type hint if not empty
266 self.type_hint = type_hint
267 # update the 'nonable' status - only if not already explicitly set.
268 # note: if this is UNKNOWN, we already saw that self.default is not None. No need to check again.
269 if self.nonable is UNKNOWN:
270 if is_pep484_nonable(type_hint):
271 self.nonable = True
272 else:
273 self.nonable = UNKNOWN
275 # detect a validator or a converter on a native field
276 if self.pending_validators is not None or self.pending_converters is not None:
277 # create a descriptor field to replace this native field
278 new_field = DescriptorField.create_from_field(self, validators=self.pending_validators,
279 converters=self.pending_converters)
280 # register it on the class in place of self
281 setattr(self.owner_cls, self.name, new_field)
283 # detect classes with slots
284 elif not isinstance(self, DescriptorField) and '__slots__' in vars(owner_cls) \
285 and '__dict__' not in owner_cls.__slots__:
286 # create a descriptor field to replace of this native field
287 new_field = DescriptorField.create_from_field(self)
288 # register it on the class in place of self
289 setattr(owner_cls, name, new_field)
291 def __set_name__(self,
292 owner, # type: Type[Any]
293 name # type: str
294 ):
295 if owner is not None: 295 ↛ exitline 295 didn't return from function '__set_name__', because the condition on line 295 was never false
296 # fill all the information about how it is attached to the class
297 # resolve type hint strings and get "optional" type hint automatically
298 # note: we need to pass an appropriate local namespace so that forward refs work.
299 # this seems like a bug in `get_type_hints` ?
300 try:
301 cls_type_hints = get_type_hints(owner)
302 except NameError:
303 # probably an issue of forward reference, or PEP563 is activated. Delay checking for later
304 self.set_as_cls_member(owner, name, type_hint=DELAYED)
305 else:
306 # nominal usage
307 self.set_as_cls_member(owner, name, owner_cls_type_hints=cls_type_hints)
309 @property
310 def qualname(self):
311 # type: (...) -> str
313 if self.owner_cls is not None:
314 try:
315 owner_qualname = self.owner_cls.__qualname__
316 except AttributeError:
317 # python 2: no __qualname__
318 owner_qualname = qualname(self.owner_cls)
319 else:
320 owner_qualname = "<unknown_cls>"
322 return "%s.%s" % (owner_qualname, self.name)
324 def __repr__(self):
325 return "<%s: %s>" % (self.__class__.__name__, self.qualname)
327 def default_factory(self, f):
328 """
329 Decorator to register the decorated function as the default factory of a field. Any previously registered
330 default factory will be overridden.
332 The decorated function should accept a single argument `(obj/self)`, and should return a value to use as the
333 default.
335 >>> import sys, pytest
336 >>> if sys.version_info < (3, 6): pytest.skip("doctest skipped for python < 3.6")
337 ...
338 >>> class Pocket:
339 ... items = field()
340 ...
341 ... @items.default_factory
342 ... def default_items(self):
343 ... print("generating default value for %s" % self)
344 ... return []
345 ...
346 >>> p = Pocket()
347 >>> p.items
348 generating default value for <pyfields.core.Pocket object ...
349 []
351 """
352 self.default = f
353 self.is_default_factory = True
354 self.is_mandatory = False
355 return f
357 def validator(self,
358 help_msg=None, # type: str
359 failure_type=None # type: Type[ValidationFailure]
360 ):
361 """
362 A decorator to add a validator to a field.
364 >>> import sys, pytest
365 >>> if sys.version_info < (3, 6): pytest.skip('skipped on python <3.6')
366 ...
367 >>> class Foo(object):
368 ... m = field()
369 ... @m.validator
370 ... def m_is_positive(self, m_value):
371 ... return m_value > 0
372 ...
373 >>> o = Foo()
374 >>> o.m = 0 # doctest: +NORMALIZE_WHITESPACE
375 Traceback (most recent call last):
376 ...
377 valid8.entry_points.ValidationError[ValueError]: Error validating [Foo.m=0]. InvalidValue:
378 Function [m_is_positive] returned [False] for value 0.
380 The decorated function should have a signature of `(val)`, `(obj/self, val)`, or `(obj/self, field, val)`. It
381 should return `True` or `None` in case of success.
383 You can use several of these decorators on the same function so as to share implementation across multiple
384 fields:
386 >>> class Foo(object):
387 ... m = field()
388 ... m2 = field()
389 ...
390 ... @m.validator
391 ... @m2.validator
392 ... def is_positive(self, field, value):
393 ... print("validating %s" % field.qualname)
394 ... return value > 0
395 ...
396 >>> o = Foo()
397 >>> o.m2 = 12
398 validating Foo.m2
399 >>> o.m = 0 # doctest: +NORMALIZE_WHITESPACE
400 Traceback (most recent call last):
401 ...
402 valid8.entry_points.ValidationError[ValueError]: Error validating [Foo.m=0]. InvalidValue:
403 Function [is_positive] returned [False] for value 0.
405 :param help_msg:
406 :param failure_type:
407 :return:
408 """
409 if help_msg is not None and callable(help_msg) and failure_type is None:
410 # used without parenthesis @<field>.validator: validation_callable := help_msg
411 self.add_validator(help_msg)
412 return help_msg
413 else:
414 # used with parenthesis @<field>.validator(...): return a decorator
415 def decorate_f(f):
416 # create the validator definition
417 if help_msg is None: 417 ↛ 418line 417 didn't jump to line 418, because the condition on line 417 was never true
418 if failure_type is None:
419 validator = f
420 else:
421 validator = (f, failure_type)
422 else:
423 if failure_type is None: 423 ↛ 426line 423 didn't jump to line 426, because the condition on line 423 was never false
424 validator = (f, help_msg)
425 else:
426 validator = (f, help_msg, failure_type)
427 self.add_validator(validator)
428 return f
430 return decorate_f
432 def add_validator(self,
433 validator # type: ValidatorDef
434 ):
435 """
436 Adds a validator to the set of validators on that field.
437 This is the implementation for native fields
439 :param validator:
440 :return:
441 """
442 if self.owner_cls is not None: 442 ↛ 444line 442 didn't jump to line 444, because the condition on line 442 was never true
443 # create a descriptor field instead of this native field
444 new_field = DescriptorField.create_from_field(self, validators=(validator, ))
446 # register it on the class
447 setattr(self.owner_cls, self.name, new_field)
448 else:
449 if not PY36: 449 ↛ 450line 449 didn't jump to line 450, because the condition on line 449 was never true
450 raise UnsupportedOnNativeFieldError(
451 "defining validators is not supported on native fields in python < 3.6."
452 " Please set `native=False` on field '%s' to enable this feature."
453 % (self,))
455 # mark as pending
456 if self.pending_validators is None:
457 self.pending_validators = [validator]
458 else:
459 self.pending_validators.append(validator)
461 def converter(self,
462 _decorated_fun=None, # type: _NoneType
463 accepts=None, # type: Union[ValidationFuncOrLambda, ValidType]
464 ):
465 """
466 A decorator to add a validator to a field.
468 >>> import sys, pytest
469 >>> if sys.version_info < (3, 6): pytest.skip('skipped on python <3.6')
470 ...
471 >>> class Foo(object):
472 ... m = field()
473 ... @m.converter
474 ... def m_from_anything(self, m_value):
475 ... return int(m_value)
476 ...
477 >>> o = Foo()
478 >>> o.m = '0'
479 >>> o.m
480 0
482 The decorated function should have a signature of `(val)`, `(obj/self, val)`, or `(obj/self, field, val)`. It
483 should return a converted value in case of success.
485 You can explicitly declare which values are accepted by the converter, by providing an `accepts` argument.
486 It may either contain a `<validation_callable>`, an `<accepted_type>` or a wildcard (`'*'` or `None`). Passing
487 a wildcard is equivalent to calling the decorator without parenthesis as seen above.
488 WARNING: this argument needs to be provided as keyword for the converter to work properly.
490 You can use several of these decorators on the same function so as to share implementation across multiple
491 fields:
493 >>> class Foo(object):
494 ... m = field(type_hint=int, check_type=True)
495 ... m2 = field(type_hint=int, check_type=True)
496 ...
497 ... @m.converter(accepts=str)
498 ... @m2.converter
499 ... def from_anything(self, field, value):
500 ... print("converting a value for %s" % field.qualname)
501 ... return int(value)
502 ...
503 >>> o = Foo()
504 >>> o.m2 = '12'
505 converting a value for Foo.m2
506 >>> o.m2 = 1.5
507 converting a value for Foo.m2
508 >>> o.m = 1.5 # doctest: +NORMALIZE_WHITESPACE
509 Traceback (most recent call last):
510 ...
511 pyfields.typing_utils.FieldTypeError: Invalid value type provided for 'Foo.m'. Value should be of type
512 <class 'int'>. Instead, received a 'float': 1.5
514 :param _decorated_fun: internal, the decorated function. Do not fill this argument!
515 :param accepts: a `<validation_callable>`, an `<accepted_type>` or a wildcard (`'*'` or `None`) defining on
516 which values this converter will have a chance to succeed. Default is `None`.
517 :return:
518 """
519 if accepts is None and _decorated_fun is not None:
520 # used without parenthesis @<field>.converter:
521 self.add_converter(_decorated_fun)
522 return _decorated_fun
523 else:
524 # used with parenthesis @<field>.converter(...): return a decorator
525 def decorate_f(f):
526 # create the converter definition
527 self.add_converter((accepts, f))
528 return f
530 return decorate_f
532 def add_converter(self,
533 converter_def # type: ConverterFuncDefinition
534 ):
535 """
536 Adds a converter to the set of converters on that field.
537 This is the implementation for native fields.
539 :param converter_def:
540 :return:
541 """
542 if self.owner_cls is not None: 542 ↛ 544line 542 didn't jump to line 544, because the condition on line 542 was never true
543 # create a descriptor field instead of this native field
544 new_field = DescriptorField.create_from_field(self, converters=(converter_def, ))
546 # register it on the class as a replacement for this native field
547 setattr(self.owner_cls, self.name, new_field)
548 else:
549 if not PY36: 549 ↛ 550line 549 didn't jump to line 550, because the condition on line 549 was never true
550 raise UnsupportedOnNativeFieldError(
551 "defining converters is not supported on native fields in python < 3.6."
552 " Please set `native=False` on field '%s' to enable this feature."
553 % (self,))
555 # mark as pending
556 if self.pending_converters is None:
557 self.pending_converters = [converter_def]
558 else:
559 self.pending_converters.append(converter_def)
561 def trace_convert(self, value, obj=None):
562 # type: (...) -> Tuple[Any, DetailedConversionResults]
563 """
564 Can be used to debug conversion problems.
565 Instead of just returning the converted value, it also returns conversion details.
567 Note that this method does not set the field value, it simply returns the conversion results.
568 In case no converter is able to convert the provided value, a `ConversionError` is raised.
570 :param obj:
571 :param value:
572 :return: a tuple (converted_value, details).
573 """
574 raise UnsupportedOnNativeFieldError("Native fields do not have converters.")
577def field(type_hint=None, # type: Union[Type[T], Iterable[Type[T]]]
578 nonable=GUESS, # type: Union[bool, Type[GUESS]]
579 check_type=False, # type: bool
580 default=EMPTY, # type: T
581 default_factory=None, # type: Callable[[], T]
582 validators=None, # type: Validators
583 converters=None, # type: Converters
584 read_only=False, # type: bool
585 doc=None, # type: str
586 name=None, # type: str
587 native=None # type: bool
588 ):
589 # type: (...) -> Union[T, Field]
590 """
591 Returns a class-level attribute definition. It allows developers to define an attribute without writing an
592 `__init__` method. Typically useful for mixin classes.
594 Laziness
595 --------
596 The field will be lazily-defined, so if you create an instance of the class, the field will not have any value
597 until it is first read or written.
599 Optional/Mandatory
600 ------------------
601 By default fields are mandatory, which means that you must set them before reading them (otherwise a
602 `MandatoryFieldInitError` will be raised). You can define an optional field by providing a `default` value.
603 This value will not be copied but used "as is" on all instances, following python's classical pattern for default
604 values. If you wish to run specific code to instantiate the default value, you may provide a `default_factory`
605 callable instead. That callable should have no mandatory argument and should return the default value. Alternately
606 you can use the `@<field>.default_factory` decorator.
608 Read-only
609 ---------
610 TODO
612 Typing
613 ------
614 Type hints for fields can be provided using the standard python typing mechanisms (type comments for python < 3.6
615 and class member type hints for python >= 3.6). Types declared this way will not be checked at runtime, they are
616 just hints for the IDE. You can also specify a `type_hint` explicitly to override the type hints gathered from the
617 other means indicated above. It supports both a single type or an iterable of alternate types (e.g. `(int, str)`).
618 The corresponding type hint is automatically declared by `field` so your IDE will know about it. Specifying a
619 `type_hint` explicitly is mostly useful if you are running python < 3.6 and wish to use type validation, see below.
621 By default `check_type` is `False`. This means that the above mentioned `type_hint` is just a hint. If you set
622 `check_type=True` the type declared in the type hint will be validated, and a `FieldTypeError` will be raised if
623 provided values are invalid. Important: if you are running python < 3.6 you have to set the type hint explicitly
624 using `type_hint` if you wish to set `check_type=True`, otherwise you will get an exception. Indeed type comments
625 can not be collected by the code.
627 Type hints relying on the `typing` module (PEP484) are correctly checked using whatever 3d party type checking
628 library is available (`typeguard` is first looked for, then `pytypes` as a fallback). If none of these providers
629 are available, a fallback implementation is provided, basically flattening `Union`s and replacing `TypeVar`s before
630 doing `is_instance`. It is not guaranteed to support all `typing` subtleties.
632 Validation
633 ----------
634 TODO
636 Nonable
637 -------
638 TODO
639 see also: https://stackoverflow.com/a/57390124/7262247
641 Conversion
642 ----------
643 TODO
645 Documentation
646 -------------
647 A docstring can be provided for code readability.
649 Example
650 -------
652 >>> import sys, pytest
653 >>> if sys.version_info < (3, 6): pytest.skip('skipped on python <3.6')
654 ...
655 >>> class Foo(object):
656 ... od = field(default='bar', doc="This is an optional field with a default value")
657 ... odf = field(default_factory=lambda obj: [], doc="This is an optional with a default value factory")
658 ... m = field(doc="This is a mandatory field")
659 ... mt: int = field(check_type=True, doc="This is a type-checked mandatory field")
660 ...
661 >>> o = Foo()
662 >>> o.od # read access with default value
663 'bar'
664 >>> o.odf # read access with default value factory
665 []
666 >>> o.odf = 12 # write access
667 >>> o.odf
668 12
669 >>> o.m # read access for mandatory attr without init
670 Traceback (most recent call last):
671 ...
672 pyfields.core.MandatoryFieldInitError: Mandatory field 'm' has not been initialized yet on instance...
673 >>> o.m = True
674 >>> o.m # read access for mandatory attr after init
675 True
676 >>> del o.m # all attributes can be deleted, same behaviour than new object
677 >>> o.m
678 Traceback (most recent call last):
679 ...
680 pyfields.core.MandatoryFieldInitError: Mandatory field 'm' has not been initialized yet on instance...
681 >>> o.mt = 1
682 >>> o.mt = '1'
683 Traceback (most recent call last):
684 ...
685 pyfields.typing_utils.FieldTypeError: Invalid value type ...
687 Limitations
688 -----------
689 Old-style classes are not supported: in python 2, don't forget to inherit from `object`.
691 Performance overhead
692 --------------------
693 `field` has two different ways to create your fields. One named `NativeField` is faster but does not permit type
694 checking, validation, or converters; besides it does not work with classes using `__slots__`. It is used by default
695 everytime where it is possible, except if you use one of the above mentioned features. In that case a
696 `DescriptorField` will transparently be created. You can force a `DescriptorField` to be created by setting
697 `native=False`.
699 The `NativeField` class implements the "non-data" descriptor protocol. So the first time the attribute is read, a
700 small python method call extra cost is paid. *But* afterwards the attribute is replaced with a native attribute
701 inside the object `__dict__`, so subsequent calls use native access without overhead.
702 This was inspired by
703 [werkzeug's @cached_property](https://tedboy.github.io/flask/generated/generated/werkzeug.cached_property.html).
705 Inspired by
706 -----------
707 This method was inspired by
709 - @lazy_attribute (sagemath)
710 - @cached_property (werkzeug) and https://stackoverflow.com/questions/24704147/python-what-is-a-lazy-property
711 - https://stackoverflow.com/q/42023852/7262247
712 - attrs / dataclasses
714 :param type_hint: an optional explicit type hint for the field, to override the type hint defined by PEP484
715 especially on old python versions because type comments can not be captured. Both a single type or an iterable
716 of alternate types (e.g. `(int, str)`) are supported. By default the type hint is just a
717 hint and does not contribute to validation. To enable type validation, set `check_type` to `True`.
718 :param nonable: a boolean that can be used to explicitly declare that a field can contain `None`. When this is set
719 to an explicit `True` or `False` value, usual type checking and validation (*if any*) are not anymore executed
720 on `None` values. Instead ; if this is `True`, type checking and validation will be *deactivated* when the field
721 is set to `None` so as to always accept the value. If this is `False`, an `None`error will be raised when `None`
722 is set on the field.
723 When this is left as `GUESS` (default), the behaviour is "automatic". This means that
724 - if the field (a) is optional with default value `None` or (b) has type hint `typing.Optional[]`, the
725 behaviour will be the same as with `nonable=True`.
726 - otherwise, the value will be the same as `nonable=UNKNOWN` and no special behaviour is put in place. `None`
727 values will be treated as any other value. This can be particularly handy if a field accepts `None` ONLY IF
728 another field is set to a special value. This can be done in a custom validator.
729 :param check_type: by default (`check_type=False`), the type of a field, provided using PEP484 type hints or
730 an explicit `type_hint`, is not validated when you assign a new value to it. You can activate type validation
731 by setting `check_type=True`. In that case the field will become a descriptor field.
732 :param default: a default value for the field. Providing a `default` makes the field "optional". `default` value
733 is not copied on new instances, if you wish a new copy to be created you should provide a `default_factory`
734 instead. Only one of `default` or `default_factory` should be provided.
735 :param default_factory: a factory that will be called (without arguments) to get the default value for that
736 field, everytime one is needed. Providing a `default_factory` makes the field "optional". Only one of `default`
737 or `default_factory` should be provided.
738 :param validators: a validation function definition, sequence of validation function definitions, or dict-like of
739 validation function definitions. See `valid8` "simple syntax" for details
740 :param converters: a sequence of (<type_def>, <converter>) pairs or a dict-like of such pairs. `<type_def>` should
741 either be a type, a tuple of types, or the '*' string indicating "any other case".
742 :param read_only: a boolean (default `False`) stating if a field can be modified after initial value has been
743 provided.
744 :param doc: documentation for the field. This is mostly for class readability purposes for now.
745 :param name: in python < 3.6 this is mandatory if you do not use any other decorator or constructor creation on the
746 class (such as `make_init`). If provided, it should be the same name than the one used used in the class field
747 definition (i.e. you should define the field as `<name> = field(name=<name>)`).
748 :param native: a boolean that can be turned to `False` to force a field to be a descriptor field, or to `True` to
749 force it to be a native field. Native fields are faster but can not support type and value validation
750 nor conversions or callbacks. `None` (default) automatically sets `native=True` if no `validators` nor
751 `check_type=True` nor `converters` are provided ; and `native=False` otherwise. In general you should not
752 set this option manually except for experiments.
753 :return:
754 """
755 # Should we create a Native or a Descriptor field ?
756 if native is None:
757 # default: choose automatically according to user-provided options
758 create_descriptor = check_type or (validators is not None) or (converters is not None) or read_only
759 else:
760 # explicit user choice
761 if native:
762 # explicit `native=True`.
763 if check_type or (validators is not None) or (converters is not None) or read_only:
764 raise UnsupportedOnNativeFieldError("`native=False` can not be set "
765 "if a `validators` or `converters` is provided "
766 "or if `check_type` or `read_only` is `True`")
767 else:
768 create_descriptor = False
769 else:
770 # explicit `native=False`. Force-use a descriptor
771 create_descriptor = True
773 # Create the correct type of field
774 if create_descriptor:
775 return DescriptorField(type_hint=type_hint, nonable=nonable, default=default, default_factory=default_factory,
776 check_type=check_type, validators=validators, converters=converters,
777 read_only=read_only, doc=doc, name=name)
778 else:
779 return NativeField(type_hint=type_hint, nonable=nonable, default=default, default_factory=default_factory,
780 doc=doc, name=name)
783class UnsupportedOnNativeFieldError(FieldError):
784 """
785 Exception raised whenever someone tries to perform an operation that is not supported on a "native" field.
786 """
787 pass
790class ClassFieldAccessError(FieldError):
791 """
792 Error raised when you use <cls>.<field>. This is currently put in place because otherwise the
793 type hints in pycharm get messed up. See below.
794 """
795 __slots__ = 'field',
797 # noinspection PyShadowingNames
798 def __init__(self, field):
799 self.field = field
801 def __str__(self):
802 return "Accessing a `field` from the class is not yet supported. You can use %s.__dict__['%s'] as a " \
803 "workaround. See https://github.com/smarie/python-pyfields/issues/12" \
804 % (self.field.owner_cls.__name__, self.field.name)
807class NativeField(Field):
808 """
809 A field that is replaced with a native python attribute on first read or write access.
810 Faster but provides not much flexibility (no validator, no type check, no converter)
811 """
812 __slots__ = ()
814 def __get__(self, obj, obj_type):
815 # type: (...) -> T
817 # do this first, because a field might be referenced from its class the first time it will be used
818 # for example if in `make_init` we use a field defined in another class, that was not yet accessed on instance.
819 if self.name is None or self.type_hint is DELAYED: 819 ↛ 821line 819 didn't jump to line 821, because the condition on line 819 was never true
820 # __set_name__ was not called yet. lazy-fix the name and type hints
821 fix_field(obj_type, self)
823 if obj is None:
824 # class-level call: https://youtrack.jetbrains.com/issue/PY-38151 is solved, we can now return self
825 return self
827 # Check if the field is already set in the object __dict__
828 value = obj.__dict__.get(self.name, _unset)
830 if value is _unset: 830 ↛ 848line 830 didn't jump to line 848, because the condition on line 830 was never false
831 # mandatory field: raise an error
832 if self.is_mandatory:
833 raise MandatoryFieldInitError(self.name, obj)
835 # optional: get default
836 if self.is_default_factory:
837 value = self.default(obj)
838 else:
839 value = self.default
841 # nominal initialization on first read: we set the attribute in the object __dict__
842 # so that next reads will be pure native field access
843 obj.__dict__[self.name] = value
845 # else:
846 # this was probably a manual call of __get__ (or a concurrent call of the first access)
848 return value
850 # not needed apparently
851 # def __delete__(self, obj):
852 # try:
853 # del obj.__dict__[self.name]
854 # except KeyError:
855 # # silently ignore: the field has not been set on that object yet,
856 # # and we wont delete the class `field` anyway...
857 # pass
860class NoneError(TypeError, ValueError, FieldError):
861 """
862 Error raised when `None` is set on an explicitly not-nonable field. It is both a `TypeError` and a `ValueError`.
863 """
864 __slots__ = ('field', )
866 def __init__(self, field):
867 super(NoneError, self).__init__()
868 self.field = field
870 def __str__(self):
871 return "Received invalid value `None` for '%s'. This field is explicitely declared as non-nonable."\
872 % (self.field.qualname, )
875# default value policies
876_NO = None
877_NO_BUT_CAN_CACHE_FIRST_RESULT = False
878_YES = True
881class DescriptorField(Field):
882 """
883 General-purpose implementation for fields that require type-checking or validation or converter
884 """
885 __slots__ = 'root_validator', 'check_type', 'converters', 'read_only', '_default_is_safe'
887 @classmethod
888 def create_from_field(cls,
889 other_field, # type: Field
890 validators=None, # type: Iterable[ValidatorDef]
891 converters=None # type: Iterable[ConverterFuncDefinition]
892 ):
893 # type: (...) -> DescriptorField
894 """
895 Creates a descriptor field by copying the information from the given other field, typically a native field
897 :param other_field:
898 :param validators: validators to add to the field definition
899 :param converters: converters to add to the field definition
900 :return:
901 """
902 if other_field.is_default_factory: 902 ↛ 903line 902 didn't jump to line 903, because the condition on line 902 was never true
903 default = EMPTY
904 default_factory = other_field.default
905 else:
906 default_factory = None
907 default = other_field.default
909 new_field = DescriptorField(type_hint=other_field.type_hint, default=default, default_factory=default_factory,
910 doc=other_field.doc, name=other_field.name, validators=validators,
911 converters=converters)
913 # copy the owner class info too
914 new_field.owner_cls = other_field.owner_cls
915 return new_field
917 def __init__(self,
918 type_hint=None, # type: Type[T]
919 nonable=UNKNOWN, # type: Union[bool, Symbols]
920 default=EMPTY, # type: T
921 default_factory=None, # type: Callable[[], T]
922 check_type=False, # type: bool
923 validators=None, # type: Validators
924 converters=None, # type: Converters
925 read_only=False, # type: bool
926 doc=None, # type: str
927 name=None # type: str
928 ):
929 """See help(field) for details"""
930 super(DescriptorField, self).__init__(type_hint=type_hint, nonable=nonable,
931 default=default, default_factory=default_factory, doc=doc, name=name)
933 # type validation
934 self.check_type = check_type
936 # validators
937 if validators is not None:
938 self.root_validator = FieldValidator(self, validators)
939 else:
940 self.root_validator = None
942 # converters
943 if converters is not None:
944 self.converters = list(make_converters_list(converters))
945 else:
946 self.converters = None
948 # read-only
949 self.read_only = read_only
951 # self._default_is_safe is used to know if we should validate/convert the default value before use
952 # - None means "always". This is the case when there is a default factory we can't modify
953 # - False means "once", and then True means "not anymore" (after first validation). This is the case
954 # when we can modify the default value so that we can replace it with the possibly converted one
955 if default is not EMPTY:
956 # a fixed default value is here, we'll validate it once and for all
957 self._default_is_safe = _NO_BUT_CAN_CACHE_FIRST_RESULT
958 elif default_factory is not None:
959 # noinspection PyBroadException
960 try:
961 # is this the `copy_value` factory ?
962 default_factory.clone_with_new_val
963 except Exception:
964 # No: the factory can be anything else
965 self._default_is_safe = _NO
966 else:
967 # Yes: we can replace the value that it uses on first
968 self._default_is_safe = _NO_BUT_CAN_CACHE_FIRST_RESULT
969 else:
970 # no default at all
971 self._default_is_safe = _NO
973 def add_validator(self,
974 validator # type: ValidatorDef
975 ):
976 """
977 Add a validation function to the set of validation functions.
979 :param validator:
980 :return:
981 """
982 if self.root_validator is None:
983 self.root_validator = FieldValidator(self, validator)
984 else:
985 self.root_validator.add_validator(validator)
987 def add_converter(self,
988 converter_def # type: ConverterFuncDefinition
989 ):
990 converters = make_converters_list(converter_def)
991 if self.converters is None:
992 # use the new list
993 self.converters = list(converters)
994 else:
995 # concatenate the lists
996 self.converters += converters
998 def __get__(self, obj, obj_type):
999 # type: (...) -> T
1001 # do this first, because a field might be referenced from its class the first time it will be used
1002 # for example if in `make_init` we use a field defined in another class, that was not yet accessed on instance.
1003 if self.name is None or self.type_hint is DELAYED: 1003 ↛ 1005line 1003 didn't jump to line 1005, because the condition on line 1003 was never true
1004 # __set_name__ was not called yet. lazy-fix the name and type hints
1005 fix_field(obj_type, self)
1007 if obj is None:
1008 # class-level call: https://youtrack.jetbrains.com/issue/PY-38151 is solved, we can now return self
1009 return self
1011 private_name = '_' + self.name
1013 # Check if the field is already set in the object
1014 value = getattr(obj, private_name, _unset)
1016 if value is _unset:
1017 # mandatory field: raise an error
1018 if self.is_mandatory:
1019 raise MandatoryFieldInitError(self.name, obj)
1021 # optional: get default
1022 if self.is_default_factory:
1023 value = self.default(obj)
1024 else:
1025 value = self.default
1027 # nominal initialization on first read: we set the attribute in the object
1028 if self._default_is_safe is _YES:
1029 # no need to validate/convert the default value, fast track (use the private name directly)
1030 setattr(obj, private_name, value)
1031 else:
1032 # we need conversion and validation - go through the setter (same as using the public name)
1033 possibly_converted_value = self.__set__(obj, value, _return=True)
1035 if self._default_is_safe is _NO_BUT_CAN_CACHE_FIRST_RESULT:
1036 # there is a possibility to remember the new default and skip this next time
1038 # If there was a conversion, use the converted value as the new default
1039 if possibly_converted_value is not value:
1040 if self.is_default_factory:
1041 # Modify the `copy_value` factory
1042 self.default = self.default.clone_with_new_val(possibly_converted_value)
1043 else:
1044 # Modify the value
1045 self.default = possibly_converted_value
1046 # else:
1047 # # no conversion: we can continue to use the same default value, it is valid
1048 # pass
1050 # mark the default as safe now, so that this is skipped next time
1051 self._default_is_safe = _YES
1053 return possibly_converted_value
1055 return value
1057 def trace_convert(self, value, obj=None):
1058 """Overrides the method in `Field` to provide a valid implementation."""
1059 return trace_convert(field=self, value=value, obj=obj)
1061 def __set__(self,
1062 obj,
1063 value, # type: T
1064 _return=False # type: bool
1065 ):
1067 # do this first, because a field might be referenced from its class the first time it will be used
1068 # for example if in `make_init` we use a field defined in another class, that was not yet accessed on instance.
1069 if self.name is None or self.type_hint is DELAYED:
1070 # __set_name__ was not called yet. lazy-fix the name and type hints
1071 fix_field(obj.__class__, self)
1073 # if obj is None:
1074 # # class-level call: this never happens
1075 # # https://youtrack.jetbrains.com/issue/PY-38151 is solved, but what do we wish to do here actually ?
1076 # raise ClassFieldAccessError(self)
1078 if self.converters is not None:
1079 # this is an inlined version of `trace_convert` with no capture of details
1080 for converter in self.converters:
1081 # noinspection PyBroadException
1082 try:
1083 # does the converter accept this input ?
1084 accepted = converter.accepts(obj, self, value)
1085 except Exception: # noqa
1086 # ignore all exceptions from converters
1087 continue
1088 else:
1089 if accepted is None or accepted: 1089 ↛ 1101line 1089 didn't jump to line 1101, because the condition on line 1089 was never false
1090 # if so, let's try to convert
1091 try:
1092 converted_value = converter.convert(obj, self, value)
1093 except Exception: # noqa
1094 # ignore all exceptions from converters
1095 continue
1096 else:
1097 # successful conversion: use the converted value
1098 value = converted_value
1099 break
1100 else:
1101 continue
1103 # speedup for vars used several time
1104 t = self.type_hint
1105 nonable = self.nonable
1106 private_name = "_" + self.name
1108 # read-only check
1109 if self.read_only:
1110 # Check if the field is already set in the object
1111 _v = getattr(obj, private_name, _unset)
1112 if _v is not _unset:
1113 raise ReadOnlyFieldError(self.qualname, obj)
1115 # type checker and validators
1116 if value is not None or nonable is UNKNOWN:
1117 # check the type
1118 if self.check_type:
1119 if t is EMPTY: 1119 ↛ 1120line 1119 didn't jump to line 1120, because the condition on line 1119 was never true
1120 raise ValueError("`check_type` is enabled on field '%s' but no type hint is available. Please "
1121 "provide type hints or set `field.check_type` to `False`. Note that python code is"
1122 " not able to read type comments so if you wish to be compliant with python < 3.6 "
1123 "you'll have to set the type hint explicitly in `field.type_hint` instead")
1125 if USE_ADVANCED_TYPE_CHECKER: 1125 ↛ 1129line 1125 didn't jump to line 1129, because the condition on line 1125 was never false
1126 # take into account all the subtleties from `typing` module by relying on 3d party providers.
1127 assert_is_of_type(self, value, t)
1129 elif not isinstance(value, t):
1130 raise FieldTypeError(self, value, t)
1132 # run the validators
1133 if self.root_validator is not None:
1134 self.root_validator.assert_valid(obj, value)
1136 elif not nonable:
1137 # value is None and field is not nonable: raise an error
1138 # note: the root validator might not even exist, so do not reuse valid8 none rejecter here
1139 raise NoneError(self)
1140 # else:
1141 # # value is None and field is nonable: nothing to do
1142 # pass
1144 # set the new value
1145 setattr(obj, private_name, value)
1147 # return it for the callers that need it
1148 if _return:
1149 return value
1151 def __delete__(self, obj):
1152 # private_name = "_" + self.name
1153 delattr(obj, "_" + self.name)
1156# noinspection PyShadowingNames
1157def fix_field(cls, # type: Type[Any]
1158 field, # type: Field
1159 include_inherited=True, # type: bool
1160 fix_type_hints=PY36 # type: bool
1161 ):
1162 """
1163 Fixes the given field name and type hint on the given class
1165 :param cls:
1166 :param field:
1167 :param include_inherited: should the field be looked for in parent classes following the mro. Default = True
1168 :param fix_type_hints:
1169 :return:
1170 """
1171 if fix_type_hints: 1171 ↛ 1174line 1171 didn't jump to line 1174, because the condition on line 1171 was never false
1172 cls_type_hints = get_type_hints(cls)
1173 else:
1174 cls_type_hints = None
1176 where_cls = getmro(cls) if include_inherited else (cls, )
1178 found = False
1179 for _cls in where_cls: 1179 ↛ 1191line 1179 didn't jump to line 1191, because the loop on line 1179 didn't complete
1180 for member_name, member in vars(_cls).items(): 1180 ↛ 1188line 1180 didn't jump to line 1188, because the loop on line 1180 didn't complete
1181 # if not member_name.startswith('__'): not stated in the doc: too dangerous to have such implicit filter
1182 if member is field:
1183 # do the same than in __set_name__
1184 field.set_as_cls_member(_cls, member_name, owner_cls_type_hints=cls_type_hints)
1185 # found: no need to look further
1186 found = True
1187 break
1188 if found: 1188 ↛ 1179line 1188 didn't jump to line 1179, because the condition on line 1188 was never false
1189 break
1190 else:
1191 raise ValueError("field %s was not found on class %s%s"
1192 % (field, cls, 'or its ancestors' if include_inherited else ''))