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

1import os 

2import sys 

3import timeit 

4 

5import pytest 

6from valid8 import ValidationError, ValidationFailure 

7 

8from pyfields import field, MandatoryFieldInitError, make_init, init_fields, ReadOnlyFieldError, NoneError, \ 

9 FieldTypeError, autoclass, get_fields 

10 

11 

12def test_lazy_fields(): 

13 

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 

17 

18 # create an instance 

19 w = Wall() 

20 

21 # the field is visible in `dir` 

22 assert dir(w)[-2:] == ['color', 'height'] 

23 

24 # but not yet in `vars` 

25 assert vars(w) == dict() 

26 

27 # lets ask for it - default value is affected 

28 print(w.color) 

29 

30 # now it is in `vars` too 

31 assert vars(w) == {'color': 'white'} 

32 

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

37 

38 w.height = 12 

39 assert vars(w) == {'color': 'white', 'height': 12} 

40 

41 

42@pytest.mark.parametrize("use_decorator", [False, True], ids="use_decorator={}".format) 

43def test_default_factory(use_decorator): 

44 

45 class BadPocket(object): 

46 items = field(default=[]) 

47 

48 p = BadPocket() 

49 p.items.append('thing') 

50 g = BadPocket() 

51 assert g.items == ['thing'] 

52 

53 if use_decorator: 

54 class Pocket: 

55 items = field() 

56 

57 @items.default_factory 

58 def default_items(self): 

59 return [] 

60 else: 

61 class Pocket(object): 

62 items = field(default_factory=lambda obj: []) 

63 

64 p = Pocket() 

65 g = Pocket() 

66 p.items.append('thing') 

67 assert p.items == ['thing'] 

68 assert g.items == [] 

69 

70 

71def test_readonly_field(): 

72 """ checks that the example in the readme is correct """ 

73 

74 class User(object): 

75 name = field(read_only=True) 

76 

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) 

85 

86 class User(object): 

87 name = field(read_only=True, default="dummy") 

88 

89 u = User() 

90 assert "name: %s" % u.name == "name: dummy" 

91 with pytest.raises(ReadOnlyFieldError): 

92 u.name = "john" 

93 

94 

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

109 

110 w = Wall() 

111 w.height = 1 

112 with pytest.raises(TypeError): 

113 w.height = "1" 

114 

115 

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

119 

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) 

128 

129 from mini_lambda import x 

130 from valid8.validation_lib import is_in 

131 

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

140 

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) 

148 

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) 

153 

154 

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

157 

158 class InvalidWidth(ValidationFailure): 

159 help_msg = 'should be a multiple of the height ({height})' 

160 

161 def validate_width(obj, width): 

162 if width % obj.height != 0: 

163 raise InvalidWidth(width, height=obj.height) 

164 

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

180 

181 w = Wall() 

182 w.height = 100 

183 w.width = 200 

184 

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) 

188 

189try: 

190 from typing import Optional 

191 typing_present = True 

192except ImportError: 

193 typing_present = False 

194 

195 

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

200 

201 if declaration == 'typing': 

202 from typing import Optional 

203 

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) 

210 

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) 

218 

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) 

226 

227 else: 

228 raise ValueError(declaration) 

229 

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} 

238 

239 

240def test_native_descriptors(): 

241 """""" 

242 class Foo: 

243 a = field() 

244 b = field(native=False) 

245 

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 

250 

251 f = Foo() 

252 

253 def set_native(): f.a = 12 

254 

255 def set_descript(): f.b = 12 

256 

257 def set_pynative(): f.c = 12 

258 

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

264 

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) 

269 

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

273 

274 ratio = t_native / t_pynative 

275 print("Ratio is %.2f" % ratio) 

276 assert ratio <= 1.2 

277 

278 

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 

285 

286 

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

292 

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

298 

299 w = Wall(2) 

300 assert vars(w) == {'color': 'white', 'height': 2} 

301 

302 w = Wall(color='blue', height=12) 

303 assert vars(w) == {'color': 'blue', 'height': 12} 

304 

305 

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 

310 

311 # only `height` will be in the constructor 

312 __init__ = make_init(height) 

313 

314 with pytest.raises(TypeError) as exc_info: 

315 Wall(1, 'blue') 

316 assert str(exc_info.value).startswith("__init__()") 

317 

318 

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) 

323 

324 class ColoredWall(Wall): 

325 color = field(default='white', doc="Color of the wall.") # type: str 

326 __init__ = make_init(Wall.height, color) 

327 

328 w = ColoredWall(2) 

329 assert vars(w) == {'color': 'white', 'height': 2} 

330 

331 w = ColoredWall(color='blue', height=12) 

332 assert vars(w) == {'color': 'blue', 'height': 12} 

333 

334 

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 

339 

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 

348 

349 # only `height` and `foo` will be in the constructor 

350 __init__ = make_init(height, post_init_fun=post_init) 

351 

352 w = Wall(1, 'hey') 

353 assert vars(w) == {'color': 'white', 'height': 1, 'non_field_attr': 'hey'} 

354 

355 

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 

360 

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 

370 

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

376 

377 w = Wall(2) 

378 assert vars(w) == {'color': 'white', 'height': 2, 'non_field_attr': 'hello'} 

379 

380 w = Wall(msg='hey', color='blue', height=12) 

381 assert vars(w) == {'color': 'blue', 'height': 12, 'non_field_attr': 'hey'} 

382 

383 

384no_type_checker = False 

385try: 

386 import typeguard 

387except ImportError: 

388 try: 

389 import pytypes 

390 except ImportError: 

391 no_type_checker = True 

392 

393 

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

398 

399 from ._test_py36 import _test_autofields_readme 

400 Pocket, Item, Pocket2 = _test_autofields_readme() 

401 

402 with pytest.raises(TypeError): 

403 Item() 

404 

405 item1 = Item(name='1') 

406 pocket1 = Pocket(size=2) 

407 pocket2 = Pocket(size=2) 

408 

409 # make sure that custom constructor is not overridden by @autofields 

410 pocket3 = Pocket2("world") 

411 with pytest.raises(MandatoryFieldInitError): 

412 pocket3.size 

413 

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 

419 

420 

421try: 

422 import pytypes 

423except ImportError: 

424 has_pytypes = False 

425else: 

426 has_pytypes = True 

427 

428 

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

433 

434 from ._test_py36 import _test_autofields_vtypes_readme 

435 Rectangle = _test_autofields_vtypes_readme() 

436 

437 r = Rectangle(1, 2) 

438 with pytest.raises(FieldTypeError): 

439 Rectangle(1, -2) 

440 with pytest.raises(FieldTypeError): 

441 Rectangle('1', 2) 

442 

443 

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) 

450 

451 foo = Foo(msg='hello') 

452 

453 assert [f.name for f in get_fields(Foo)] == ['msg', 'age'] 

454 

455 print(foo) # automatic string representation 

456 print(foo.to_dict()) # dict view 

457 

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 

462 

463 

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

468 

469 # assert [f.name for f in get_fields(Foo)] == ['msg', 'age', 'height'] 

470 

471 foo = Foo(msg='hello') 

472 

473 assert repr(foo) == "Foo(msg='hello', age=12, height=50)" # automatic string representation 

474 assert str(foo.to_dict()) # automatic dict view 

475 

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 

478 

479 

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

484 

485 # assert [f.name for f in get_fields(Foo)] == ['msg', 'age', 'height'] 

486 

487 foo = Foo(msg='hello') 

488 

489 with pytest.raises(AttributeError): 

490 foo.to_dict() # method does not exist 

491 

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 

494 

495 # type checking ON 

496 with pytest.raises(FieldTypeError): 

497 foo.msg = 1