Coverage for pyfields/tests/test_readme.py: 96%
290 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
1import os
2import sys
3import timeit
5import pytest
6from valid8 import ValidationError, ValidationFailure
8from pyfields import field, MandatoryFieldInitError, make_init, init_fields, ReadOnlyFieldError, NoneError, \
9 FieldTypeError, autoclass, get_fields
12def test_lazy_fields():
14 class Wall(object):
15 height = field(doc="Height of the wall in mm.") # type: int
16 color = field(default='white', doc="Color of the wall.") # type: str
18 # create an instance
19 w = Wall()
21 # the field is visible in `dir`
22 assert dir(w)[-2:] == ['color', 'height']
24 # but not yet in `vars`
25 assert vars(w) == dict()
27 # lets ask for it - default value is affected
28 print(w.color)
30 # now it is in `vars` too
31 assert vars(w) == {'color': 'white'}
33 # mandatory field
34 with pytest.raises(MandatoryFieldInitError) as exc_info:
35 print(w.height)
36 assert str(exc_info.value).startswith("Mandatory field 'height' has not been initialized yet on instance <")
38 w.height = 12
39 assert vars(w) == {'color': 'white', 'height': 12}
42@pytest.mark.parametrize("use_decorator", [False, True], ids="use_decorator={}".format)
43def test_default_factory(use_decorator):
45 class BadPocket(object):
46 items = field(default=[])
48 p = BadPocket()
49 p.items.append('thing')
50 g = BadPocket()
51 assert g.items == ['thing']
53 if use_decorator:
54 class Pocket:
55 items = field()
57 @items.default_factory
58 def default_items(self):
59 return []
60 else:
61 class Pocket(object):
62 items = field(default_factory=lambda obj: [])
64 p = Pocket()
65 g = Pocket()
66 p.items.append('thing')
67 assert p.items == ['thing']
68 assert g.items == []
71def test_readonly_field():
72 """ checks that the example in the readme is correct """
74 class User(object):
75 name = field(read_only=True)
77 u = User()
78 u.name = "john"
79 assert "name: %s" % u.name == "name: john"
80 with pytest.raises(ReadOnlyFieldError) as exc_info:
81 u.name = "john2"
82 qualname = User.__dict__['name'].qualname
83 assert str(exc_info.value) == "Read-only field '%s' has already been " \
84 "initialized on instance %s and cannot be modified anymore." % (qualname, u)
86 class User(object):
87 name = field(read_only=True, default="dummy")
89 u = User()
90 assert "name: %s" % u.name == "name: dummy"
91 with pytest.raises(ReadOnlyFieldError):
92 u.name = "john"
95@pytest.mark.parametrize("py36_style_type_hints", [False, True], ids="py36_style_type_hints={}".format)
96def test_type_validation(py36_style_type_hints):
97 if py36_style_type_hints:
98 if sys.version_info < (3, 6): 98 ↛ 99line 98 didn't jump to line 99, because the condition on line 98 was never true
99 pytest.skip()
100 Wall = None
101 else:
102 # import the test that uses python 3.6 type annotations
103 from ._test_py36 import _test_readme_type_validation
104 Wall = _test_readme_type_validation()
105 else:
106 class Wall(object):
107 height = field(type_hint=int, check_type=True, doc="Height of the wall in mm.")
108 color = field(type_hint=str, check_type=True, default='white', doc="Color of the wall.")
110 w = Wall()
111 w.height = 1
112 with pytest.raises(TypeError):
113 w.height = "1"
116@pytest.mark.parametrize("py36_style_type_hints", [False, True], ids="py36_style_type_hints={}".format)
117def test_value_validation(py36_style_type_hints):
118 colors = ('blue', 'red', 'white')
120 if py36_style_type_hints:
121 if sys.version_info < (3, 6): 121 ↛ 122line 121 didn't jump to line 122, because the condition on line 121 was never true
122 pytest.skip()
123 Wall = None
124 else:
125 # import the test that uses python 3.6 type annotations
126 from ._test_py36 import _test_readme_value_validation
127 Wall = _test_readme_value_validation(colors)
129 from mini_lambda import x
130 from valid8.validation_lib import is_in
132 class Wall(object):
133 height = field(type_hint=int,
134 validators={'should be a positive number': x > 0,
135 'should be a multiple of 100': x % 100 == 0},
136 doc="Height of the wall in mm.")
137 color = field(type_hint=str,
138 validators=is_in(colors),
139 default='white', doc="Color of the wall.")
141 w = Wall()
142 w.height = 100
143 with pytest.raises(ValidationError) as exc_info:
144 w.height = 1
145 assert "Successes: ['x > 0'] / Failures: {" \
146 "'x % 100 == 0': 'InvalidValue: should be a multiple of 100. Returned False.'" \
147 "}." in str(exc_info.value)
149 with pytest.raises(ValidationError) as exc_info:
150 w.color = 'magenta'
151 assert "NotInAllowedValues: x in ('blue', 'red', 'white') does not hold for x=magenta. Wrong value: 'magenta'." \
152 in str(exc_info.value)
155@pytest.mark.parametrize("py36_style_type_hints", [False, True], ids="py36_style_type_hints={}".format)
156def test_value_validation_advanced(py36_style_type_hints):
158 class InvalidWidth(ValidationFailure):
159 help_msg = 'should be a multiple of the height ({height})'
161 def validate_width(obj, width):
162 if width % obj.height != 0:
163 raise InvalidWidth(width, height=obj.height)
165 if py36_style_type_hints:
166 if sys.version_info < (3, 6): 166 ↛ 167line 166 didn't jump to line 167, because the condition on line 166 was never true
167 pytest.skip()
168 Wall = None
169 else:
170 # import the test that uses python 3.6 type annotations
171 from ._test_py36 import test_value_validation_advanced
172 Wall = test_value_validation_advanced(validate_width)
173 else:
174 class Wall(object):
175 height = field(type_hint=int,
176 doc="Height of the wall in mm.")
177 width = field(type_hint=str,
178 validators=validate_width,
179 doc="Width of the wall in mm.")
181 w = Wall()
182 w.height = 100
183 w.width = 200
185 with pytest.raises(ValidationError) as exc_info:
186 w.width = 201
187 assert "InvalidWidth: should be a multiple of the height (100). Wrong value: 201." in str(exc_info.value)
189try:
190 from typing import Optional
191 typing_present = True
192except ImportError:
193 typing_present = False
196@pytest.mark.skipif(not typing_present, reason="typing module is not present")
197@pytest.mark.parametrize("declaration", ['typing', 'default_value', 'explicit_nonable'], ids="declaration={}".format)
198def test_nonable_fields(declaration):
199 """Tests that nonable fields are supported and correctly handled"""
201 if declaration == 'typing':
202 from typing import Optional
204 class Foo(object):
205 a = field(type_hint=Optional[int], check_type=True)
206 b = field(type_hint=Optional[int], validators={'is positive': lambda x: x > 0}) 206 ↛ exitline 206 didn't run the lambda on line 206
207 c = field(nonable=False, check_type=True)
208 d = field(validators={'accept_all': lambda x: True})
209 e = field(nonable=False)
211 elif declaration == 'default_value':
212 class Foo(object):
213 a = field(type_hint=int, default=None, check_type=True)
214 b = field(type_hint=int, default=None, validators={'is positive': lambda x: x > 0}) 214 ↛ exitline 214 didn't run the lambda on line 214
215 c = field(nonable=False, check_type=True)
216 d = field(validators={'accept_all': lambda x: True})
217 e = field(nonable=False)
219 elif declaration == 'explicit_nonable': 219 ↛ 228line 219 didn't jump to line 228, because the condition on line 219 was never false
220 class Foo(object):
221 a = field(type_hint=int, nonable=True, check_type=True)
222 b = field(type_hint=int, nonable=True, validators={'is positive': lambda x: x > 0}) 222 ↛ exitline 222 didn't run the lambda on line 222
223 c = field(nonable=False, check_type=True)
224 d = field(validators={'accept_all': lambda x: True})
225 e = field(nonable=False)
227 else:
228 raise ValueError(declaration)
230 f = Foo()
231 f.a = None
232 f.b = None
233 with pytest.raises(NoneError):
234 f.c = None
235 f.d = None
236 f.e = None
237 assert vars(f) == {'_a': None, '_b': None, '_d': None, 'e': None}
240def test_native_descriptors():
241 """"""
242 class Foo:
243 a = field()
244 b = field(native=False)
246 a_name = "test_native_descriptors.<locals>.Foo.a" if sys.version_info >= (3, 6) else "<unknown_cls>.None"
247 b_name = "test_native_descriptors.<locals>.Foo.b" if sys.version_info >= (3, 6) else "<unknown_cls>.None"
248 assert repr(Foo.__dict__['a']) == "<NativeField: %s>" % a_name
249 assert repr(Foo.__dict__['b']) == "<DescriptorField: %s>" % b_name
251 f = Foo()
253 def set_native(): f.a = 12
255 def set_descript(): f.b = 12
257 def set_pynative(): f.c = 12
259 # make sure that the access time for native field and native are identical
260 # --get rid of the first init since it is a bit longer (replacement of the descriptor with a native field
261 set_native()
262 set_descript()
263 set_pynative()
265 # --now compare the executiong= times
266 t_native = timeit.Timer(set_native).timeit(10000000)
267 t_descript = timeit.Timer(set_descript).timeit(10000000)
268 t_pynative = timeit.Timer(set_pynative).timeit(10000000)
270 print("Average time (ns) setting the field:")
271 print("%0.2f (normal python) ; %0.2f (native field) ; %0.2f (descriptor field)"
272 % (t_pynative, t_native, t_descript))
274 ratio = t_native / t_pynative
275 print("Ratio is %.2f" % ratio)
276 assert ratio <= 1.2
279# def decompose(number):
280# """ decompose a number in scientific notation. from https://stackoverflow.com/a/45359185/7262247"""
281# (sign, digits, exponent) = Decimal(number).as_tuple()
282# fexp = len(digits) + exponent - 1
283# fman = Decimal(number).scaleb(-fexp).normalize()
284# return fman, fexp
287def test_make_init_full_defaults():
288 class Wall:
289 height = field(doc="Height of the wall in mm.") # type: int
290 color = field(default='white', doc="Color of the wall.") # type: str
291 __init__ = make_init()
293 # create an instance
294 help(Wall)
295 with pytest.raises(TypeError) as exc_info:
296 Wall()
297 assert str(exc_info.value).startswith("__init__()")
299 w = Wall(2)
300 assert vars(w) == {'color': 'white', 'height': 2}
302 w = Wall(color='blue', height=12)
303 assert vars(w) == {'color': 'blue', 'height': 12}
306def test_make_init_with_explicit_list():
307 class Wall:
308 height = field(doc="Height of the wall in mm.") # type: int
309 color = field(default='white', doc="Color of the wall.") # type: str
311 # only `height` will be in the constructor
312 __init__ = make_init(height)
314 with pytest.raises(TypeError) as exc_info:
315 Wall(1, 'blue')
316 assert str(exc_info.value).startswith("__init__()")
319def test_make_init_with_inheritance():
320 class Wall:
321 height = field(doc="Height of the wall in mm.") # type: int
322 __init__ = make_init(height)
324 class ColoredWall(Wall):
325 color = field(default='white', doc="Color of the wall.") # type: str
326 __init__ = make_init(Wall.height, color)
328 w = ColoredWall(2)
329 assert vars(w) == {'color': 'white', 'height': 2}
331 w = ColoredWall(color='blue', height=12)
332 assert vars(w) == {'color': 'blue', 'height': 12}
335def test_make_init_callback():
336 class Wall:
337 height = field(doc="Height of the wall in mm.") # type: int
338 color = field(default='white', doc="Color of the wall.") # type: str
340 def post_init(self, msg='hello'):
341 """
342 After initialization, some print message is done
343 :param msg: the message details to add
344 :return:
345 """
346 print("post init ! height=%s, color=%s, msg=%s" % (self.height, self.color, msg))
347 self.non_field_attr = msg
349 # only `height` and `foo` will be in the constructor
350 __init__ = make_init(height, post_init_fun=post_init)
352 w = Wall(1, 'hey')
353 assert vars(w) == {'color': 'white', 'height': 1, 'non_field_attr': 'hey'}
356def test_init_fields():
357 class Wall:
358 height = field(doc="Height of the wall in mm.") # type: int
359 color = field(default='white', doc="Color of the wall.") # type: str
361 @init_fields
362 def __init__(self, msg='hello'):
363 """
364 After initialization, some print message is done
365 :param msg: the message details to add
366 :return:
367 """
368 print("post init ! height=%s, color=%s, msg=%s" % (self.height, self.color, msg))
369 self.non_field_attr = msg
371 # create an instance
372 help(Wall.__init__)
373 with pytest.raises(TypeError) as exc_info:
374 Wall()
375 assert str(exc_info.value).startswith("__init__()")
377 w = Wall(2)
378 assert vars(w) == {'color': 'white', 'height': 2, 'non_field_attr': 'hello'}
380 w = Wall(msg='hey', color='blue', height=12)
381 assert vars(w) == {'color': 'blue', 'height': 12, 'non_field_attr': 'hey'}
384no_type_checker = False
385try:
386 import typeguard
387except ImportError:
388 try:
389 import pytypes
390 except ImportError:
391 no_type_checker = True
394@pytest.mark.skipif(sys.version_info < (3, 6), reason="python < 3.6 does not support class member type hints")
395@pytest.mark.skipif(no_type_checker, reason="no type checker is installed")
396def test_autofields_readme():
397 """Test for readme on autofields"""
399 from ._test_py36 import _test_autofields_readme
400 Pocket, Item, Pocket2 = _test_autofields_readme()
402 with pytest.raises(TypeError):
403 Item()
405 item1 = Item(name='1')
406 pocket1 = Pocket(size=2)
407 pocket2 = Pocket(size=2)
409 # make sure that custom constructor is not overridden by @autofields
410 pocket3 = Pocket2("world")
411 with pytest.raises(MandatoryFieldInitError):
412 pocket3.size
414 # make sure the items list is not the same in both (if we add the item to one, they do not appear in the 2d)
415 assert pocket1.size == 2
416 assert pocket1.items is not pocket2.items
417 pocket1.items.append(item1)
418 assert len(pocket2.items) == 0
421try:
422 import pytypes
423except ImportError:
424 has_pytypes = False
425else:
426 has_pytypes = True
429@pytest.mark.skipif(has_pytypes, reason="pytypes does not correctly support vtypes - "
430 "see https://github.com/Stewori/pytypes/issues/86")
431@pytest.mark.skipif(sys.version_info < (3, 6), reason="python < 3.6 does not support class member type hints")
432def test_autofields_vtypes_readme():
434 from ._test_py36 import _test_autofields_vtypes_readme
435 Rectangle = _test_autofields_vtypes_readme()
437 r = Rectangle(1, 2)
438 with pytest.raises(FieldTypeError):
439 Rectangle(1, -2)
440 with pytest.raises(FieldTypeError):
441 Rectangle('1', 2)
444def test_autoclass():
445 """ Tests the example with autoclass in the doc """
446 @autoclass
447 class Foo(object):
448 msg = field(type_hint=str)
449 age = field(default=12, type_hint=int)
451 foo = Foo(msg='hello')
453 assert [f.name for f in get_fields(Foo)] == ['msg', 'age']
455 print(foo) # automatic string representation
456 print(foo.to_dict()) # dict view
458 assert str(foo) == "Foo(msg='hello', age=12)"
459 assert str(foo.to_dict()) in ("{'msg': 'hello', 'age': 12}", "{'age': 12, 'msg': 'hello'}")
460 assert foo == Foo(msg='hello', age=12) # comparison (equality)
461 assert foo == {'msg': 'hello', 'age': 12} # comparison with dicts
464@pytest.mark.skipif(sys.version_info < (3, 6), reason="not valid for old python")
465def test_autoclass_2():
466 from ._test_py36 import _test_autoclass2
467 Foo = _test_autoclass2()
469 # assert [f.name for f in get_fields(Foo)] == ['msg', 'age', 'height']
471 foo = Foo(msg='hello')
473 assert repr(foo) == "Foo(msg='hello', age=12, height=50)" # automatic string representation
474 assert str(foo.to_dict()) # automatic dict view
476 assert foo == Foo(msg='hello', age=12, height=50) # automatic equality comparison
477 assert foo == {'msg': 'hello', 'age': 12, 'height': 50} # automatic eq comparison with dicts
480@pytest.mark.skipif(sys.version_info < (3, 6), reason="not valid for old python")
481def test_autoclass_3():
482 from ._test_py36 import _test_autoclass3
483 Foo = _test_autoclass3()
485 # assert [f.name for f in get_fields(Foo)] == ['msg', 'age', 'height']
487 foo = Foo(msg='hello')
489 with pytest.raises(AttributeError):
490 foo.to_dict() # method does not exist
492 assert repr(foo) == "Foo(msg='hello', age=12, height=50)" # automatic string representation
493 assert foo == Foo(msg='hello', age=12, height=50) # automatic equality comparison
495 # type checking ON
496 with pytest.raises(FieldTypeError):
497 foo.msg = 1