Coverage for pyfields/core.py: 85%

357 statements  

« 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 

9 

10try: 

11 from inspect import signature, Parameter 

12except ImportError: 

13 # noinspection PyUnresolvedReferences,PyPackageRequirements 

14 from funcsigs import signature, Parameter # noqa 

15 

16from valid8 import ValidationFailure, is_pep484_nonable 

17 

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 

20 

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 

31 

32except ImportError: 

33 use_type_hints = False 

34 

35 

36USE_ADVANCED_TYPE_CHECKER = assert_is_of_type is not None 

37 

38 

39PY36 = sys.version_info >= (3, 6) 

40PY2 = sys.version_info < (3, 0) 

41# PY35 = sys.version_info >= (3, 5) 

42 

43try: 

44 object.__qualname__ 

45except AttributeError: 

46 # old python without __qualname__ 

47 import re 

48 RE_CLASS_NAME = re.compile("<class '(.*)'>") 

49 

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 

57 

58 

59class FieldError(Exception): 

60 """ 

61 Base class for exceptions related to fields 

62 """ 

63 pass 

64 

65 

66class MandatoryFieldInitError(FieldError): 

67 """ 

68 Raised by `field` when a mandatory field is read without being set first. 

69 """ 

70 __slots__ = 'field_name', 'obj' 

71 

72 def __init__(self, field_name, obj): 

73 self.field_name = field_name 

74 self.obj = obj 

75 

76 def __str__(self): 

77 return "Mandatory field '%s' has not been initialized yet on instance %s." % (self.field_name, self.obj) 

78 

79 

80class ReadOnlyFieldError(FieldError): 

81 """ 

82 Raised by descriptor field when a read-only attribute is accessed for writing 

83 """ 

84 __slots__ = 'field_name', 'obj' 

85 

86 def __init__(self, field_name, obj): 

87 self.field_name = field_name 

88 self.obj = obj 

89 

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) 

93 

94 

95class Symbols(Enum): 

96 """ 

97 A few symbols used in `fields` for signatures 

98 

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. 

101 

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 

110 

111 def __repr__(self): 

112 """ More compact representation for signatures readability""" 

113 return self.name 

114 

115 

116# GUESS = sentinel.create('guess') 

117GUESS = Symbols.GUESS 

118 

119# UNKNOWN = sentinel.create('unknown') 

120UNKNOWN = Symbols.UNKNOWN 

121 

122# EMPTY = sentinel.create('empty') 

123EMPTY = Symbols.EMPTY 

124DELAYED = Symbols.DELAYED 

125 

126# USE_FACTORY = sentinel.create('use_factory') 

127USE_FACTORY = Symbols.USE_FACTORY 

128 

129# _unset = sentinel.create('_unset') 

130_unset = Symbols._unset 

131 

132 

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() 

137 

138 

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 

150 

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""" 

160 

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 

166 

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 

179 

180 # name 

181 self.name = name 

182 self.owner_cls = None 

183 

184 # doc 

185 self.doc = dedent(doc) if doc is not None else None 

186 

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 

192 

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 

207 

208 # pending validators and converters 

209 self.pending_validators = None 

210 self.pending_converters = None 

211 

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. 

220 

221 - its owner class 

222 - the name under which it is known in that class 

223 - the type hints (python 3.6) 

224 

225 In python 3.6+ this is called directly at class creation time through the `__set_name__` callback. 

226 

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-: 

229 

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. 

232 

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 

241 

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.") 

244 

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 

254 

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) 

262 

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 

274 

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) 

282 

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) 

290 

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) 

308 

309 @property 

310 def qualname(self): 

311 # type: (...) -> str 

312 

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>" 

321 

322 return "%s.%s" % (owner_qualname, self.name) 

323 

324 def __repr__(self): 

325 return "<%s: %s>" % (self.__class__.__name__, self.qualname) 

326 

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. 

331 

332 The decorated function should accept a single argument `(obj/self)`, and should return a value to use as the 

333 default. 

334 

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 [] 

350 

351 """ 

352 self.default = f 

353 self.is_default_factory = True 

354 self.is_mandatory = False 

355 return f 

356 

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. 

363 

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. 

379 

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. 

382 

383 You can use several of these decorators on the same function so as to share implementation across multiple 

384 fields: 

385 

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. 

404 

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 

429 

430 return decorate_f 

431 

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 

438 

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, )) 

445 

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,)) 

454 

455 # mark as pending 

456 if self.pending_validators is None: 

457 self.pending_validators = [validator] 

458 else: 

459 self.pending_validators.append(validator) 

460 

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. 

467 

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 

481 

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. 

484 

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. 

489 

490 You can use several of these decorators on the same function so as to share implementation across multiple 

491 fields: 

492 

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 

513 

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 

529 

530 return decorate_f 

531 

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. 

538 

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, )) 

545 

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,)) 

554 

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) 

560 

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. 

566 

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. 

569 

570 :param obj: 

571 :param value: 

572 :return: a tuple (converted_value, details). 

573 """ 

574 raise UnsupportedOnNativeFieldError("Native fields do not have converters.") 

575 

576 

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. 

593 

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. 

598 

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. 

607 

608 Read-only 

609 --------- 

610 TODO 

611 

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. 

620 

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. 

626 

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. 

631 

632 Validation 

633 ---------- 

634 TODO 

635 

636 Nonable 

637 ------- 

638 TODO 

639 see also: https://stackoverflow.com/a/57390124/7262247 

640 

641 Conversion 

642 ---------- 

643 TODO 

644 

645 Documentation 

646 ------------- 

647 A docstring can be provided for code readability. 

648 

649 Example 

650 ------- 

651 

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 ... 

686 

687 Limitations 

688 ----------- 

689 Old-style classes are not supported: in python 2, don't forget to inherit from `object`. 

690 

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`. 

698 

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). 

704 

705 Inspired by 

706 ----------- 

707 This method was inspired by 

708 

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 

713 

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 

772 

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) 

781 

782 

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 

788 

789 

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', 

796 

797 # noinspection PyShadowingNames 

798 def __init__(self, field): 

799 self.field = field 

800 

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) 

805 

806 

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__ = () 

813 

814 def __get__(self, obj, obj_type): 

815 # type: (...) -> T 

816 

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) 

822 

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 

826 

827 # Check if the field is already set in the object __dict__ 

828 value = obj.__dict__.get(self.name, _unset) 

829 

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) 

834 

835 # optional: get default 

836 if self.is_default_factory: 

837 value = self.default(obj) 

838 else: 

839 value = self.default 

840 

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 

844 

845 # else: 

846 # this was probably a manual call of __get__ (or a concurrent call of the first access) 

847 

848 return value 

849 

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 

858 

859 

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', ) 

865 

866 def __init__(self, field): 

867 super(NoneError, self).__init__() 

868 self.field = field 

869 

870 def __str__(self): 

871 return "Received invalid value `None` for '%s'. This field is explicitely declared as non-nonable."\ 

872 % (self.field.qualname, ) 

873 

874 

875# default value policies 

876_NO = None 

877_NO_BUT_CAN_CACHE_FIRST_RESULT = False 

878_YES = True 

879 

880 

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' 

886 

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 

896 

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 

908 

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) 

912 

913 # copy the owner class info too 

914 new_field.owner_cls = other_field.owner_cls 

915 return new_field 

916 

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) 

932 

933 # type validation 

934 self.check_type = check_type 

935 

936 # validators 

937 if validators is not None: 

938 self.root_validator = FieldValidator(self, validators) 

939 else: 

940 self.root_validator = None 

941 

942 # converters 

943 if converters is not None: 

944 self.converters = list(make_converters_list(converters)) 

945 else: 

946 self.converters = None 

947 

948 # read-only 

949 self.read_only = read_only 

950 

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 

972 

973 def add_validator(self, 

974 validator # type: ValidatorDef 

975 ): 

976 """ 

977 Add a validation function to the set of validation functions. 

978 

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) 

986 

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 

997 

998 def __get__(self, obj, obj_type): 

999 # type: (...) -> T 

1000 

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) 

1006 

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 

1010 

1011 private_name = '_' + self.name 

1012 

1013 # Check if the field is already set in the object 

1014 value = getattr(obj, private_name, _unset) 

1015 

1016 if value is _unset: 

1017 # mandatory field: raise an error 

1018 if self.is_mandatory: 

1019 raise MandatoryFieldInitError(self.name, obj) 

1020 

1021 # optional: get default 

1022 if self.is_default_factory: 

1023 value = self.default(obj) 

1024 else: 

1025 value = self.default 

1026 

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) 

1034 

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 

1037 

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 

1049 

1050 # mark the default as safe now, so that this is skipped next time 

1051 self._default_is_safe = _YES 

1052 

1053 return possibly_converted_value 

1054 

1055 return value 

1056 

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) 

1060 

1061 def __set__(self, 

1062 obj, 

1063 value, # type: T 

1064 _return=False # type: bool 

1065 ): 

1066 

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) 

1072 

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) 

1077 

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 

1102 

1103 # speedup for vars used several time 

1104 t = self.type_hint 

1105 nonable = self.nonable 

1106 private_name = "_" + self.name 

1107 

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) 

1114 

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") 

1124 

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) 

1128 

1129 elif not isinstance(value, t): 

1130 raise FieldTypeError(self, value, t) 

1131 

1132 # run the validators 

1133 if self.root_validator is not None: 

1134 self.root_validator.assert_valid(obj, value) 

1135 

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 

1143 

1144 # set the new value 

1145 setattr(obj, private_name, value) 

1146 

1147 # return it for the callers that need it 

1148 if _return: 

1149 return value 

1150 

1151 def __delete__(self, obj): 

1152 # private_name = "_" + self.name 

1153 delattr(obj, "_" + self.name) 

1154 

1155 

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 

1164 

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 

1175 

1176 where_cls = getmro(cls) if include_inherited else (cls, ) 

1177 

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 ''))