Coverage for pyfields/tests/test_core.py: 91%

442 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# 

3# Copyright (c) Schneider Electric Industries, 2019. All right reserved. 

4 

5# Authors: Sylvain Marie <sylvain.marie@se.com> 

6# 

7import pickle 

8import sys 

9from collections import OrderedDict 

10 

11import pytest 

12 

13from valid8 import ValidationError, ValidationFailure 

14from valid8.base import InvalidValue 

15from valid8.validation_lib import non_empty, Empty 

16 

17from pyfields import field, MandatoryFieldInitError, UnsupportedOnNativeFieldError, \ 

18 copy_value, copy_field, Converter, Field, ConversionError, ReadOnlyFieldError, FieldTypeError, make_init 

19 

20 

21@pytest.mark.parametrize('write_before_reading', [False, True], ids="write_before_reading={}".format) 

22@pytest.mark.parametrize('type_', ['default_factory', 'default', 'mandatory'], ids="type_={}".format) 

23@pytest.mark.parametrize('read_only', [False, True], ids="read_only={}".format) 

24def test_field(write_before_reading, type_, read_only): 

25 """Checks that field works as expected""" 

26 

27 if type_ == 'default_factory': 

28 class Tweety(object): 

29 afraid = field(default_factory=lambda obj: False, read_only=read_only) 

30 elif type_ == 'default': 

31 class Tweety(object): 

32 afraid = field(default=False, read_only=read_only) 

33 elif type_ == 'mandatory': 33 ↛ 37line 33 didn't jump to line 37, because the condition on line 33 was never false

34 class Tweety(object): 

35 afraid = field(read_only=read_only) 

36 else: 

37 raise ValueError() 

38 

39 # instantiate 

40 t = Tweety() 

41 

42 written = False 

43 

44 # (1) write 

45 if write_before_reading: 

46 t.afraid = True 

47 written = True 

48 

49 # (2) read 

50 if not write_before_reading and type_ == 'mandatory': 

51 # mandatory value not already overridden 

52 with pytest.raises(MandatoryFieldInitError): 

53 print(t.afraid) 

54 else: 

55 # either default value (False) or already-written value (True) 

56 assert t.afraid is write_before_reading 

57 written = True # because reading a non-mandatory field sets it to default 

58 

59 # (3) write (possibly again) and check 

60 if not (read_only and written): 

61 t.afraid = True 

62 assert t.afraid is True 

63 if not read_only: 

64 t.afraid = False 

65 assert t.afraid is False 

66 written = True 

67 

68 # if read only, check exception on second write 

69 if read_only: 

70 # make sure that now the value has been 

71 assert written 

72 

73 with pytest.raises(ReadOnlyFieldError) as exc_info: 

74 t.afraid = False 

75 qualname = Tweety.afraid.qualname 

76 assert str(exc_info.value) == "Read-only field '%s' has already been " \ 

77 "initialized on instance %s and cannot be modified anymore." % (qualname, t) 

78 

79 

80def test_slots(): 

81 """tests that fields are replaced with descriptor fields automatically when used on a class with `__slots__`""" 

82 class WithSlots(object): 

83 __slots__ = ('_a',) 

84 a = field() 

85 

86 if sys.version_info < (3, 0): 86 ↛ 88line 86 didn't jump to line 88, because the condition on line 86 was never true

87 # qualname does not exist, we use str(cls) 

88 a_fixed_name = "pyfields.tests.test_core.WithSlots.a" 

89 else: 

90 a_fixed_name = "test_slots.<locals>.WithSlots.a" 

91 

92 a_unknown_name = "<unknown_cls>.None" 

93 

94 if sys.version_info >= (3, 6): 94 ↛ 99line 94 didn't jump to line 99, because the condition on line 94 was never false

95 # change is done immediately 

96 assert repr(WithSlots.__dict__['a']) == "<DescriptorField: %s>" % a_fixed_name 

97 else: 

98 # change will be done after first access 

99 assert repr(WithSlots.__dict__['a']) == "<NativeField: %s>" % a_unknown_name 

100 

101 w = WithSlots() 

102 

103 if sys.version_info < (3, 6): 103 ↛ 105line 103 didn't jump to line 105, because the condition on line 103 was never true

104 # Really not ideal you have to do something 

105 try: 

106 w.a 

107 except: 

108 pass 

109 

110 w.a = 1 

111 assert w.a == 1 

112 

113 assert repr(WithSlots.a) == "<DescriptorField: %s>" % a_fixed_name 

114 

115 

116def test_slots2(): 

117 class WithSlots(object): 

118 __slots__ = ('__dict__',) 

119 a = field() 

120 

121 if sys.version_info >= (3, 6): 121 ↛ 124line 121 didn't jump to line 124, because the condition on line 121 was never false

122 a_name = "test_slots2.<locals>.WithSlots.a" 

123 else: 

124 a_name = "<unknown_cls>.None" 

125 

126 assert repr(WithSlots.__dict__['a']) == "<NativeField: %s>" % a_name 

127 

128 

129def test_default_factory(): 

130 """""" 

131 class Foo(object): 

132 a = field(default_factory=copy_value([])) 

133 b = field(default_factory=copy_field('z')) 

134 c = field() 

135 

136 @c.default_factory 

137 def c_default(self): 

138 return self.a + ['yes'] 

139 

140 z = field() 

141 

142 f = Foo() 

143 g = Foo() 

144 assert f.a == [] 

145 assert g.a == [] 

146 g.a.append(1) 

147 assert g.a == [1] 

148 assert f.a == [] 

149 # we can not initialize b since it requires a copy of uninitialized z 

150 with pytest.raises(MandatoryFieldInitError) as exc_info: 

151 print(f.b) 

152 assert str(exc_info.value).startswith("Mandatory field 'z' has not been initialized yet") 

153 # if we initialize z we can now safely make a copy 

154 f.z = 12 

155 assert f.b == 12 

156 f.z += 1 

157 assert f.z == 13 

158 assert f.b == 12 

159 

160 assert g.c == [1, 'yes'] 

161 

162 

163def test_type(): 

164 """ Tests that when `type_hint` is provided and `validate_type` is explicitly set, it works as expected """ 

165 

166 class Foo(object): 

167 f = field(type_hint=str, check_type=True) 

168 

169 o = Foo() 

170 o.f = 'hello' 

171 with pytest.raises(FieldTypeError) as exc_info: 

172 o.f = 1 

173 

174 if sys.version_info < (3, 0): 174 ↛ 175line 174 didn't jump to line 175, because the condition on line 174 was never true

175 qualname = 'pyfields.tests.test_core.Foo.f' 

176 else: 

177 qualname = 'test_type.<locals>.Foo.f' 

178 assert str(exc_info.value) == "Invalid value type provided for '%s'. " \ 

179 "Value should be of type %s. " \ 

180 "Instead, received a 'int': 1" % (qualname, str) 

181 

182 

183def test_type_multiple_tuple(): 

184 """ Tests that when `type_hint` is provided and `validate_type` is explicitly set, it works as expected """ 

185 

186 class Foo(object): 

187 f = field(type_hint=(str, int), check_type=True) 

188 

189 o = Foo() 

190 o.f = 'hello' 

191 o.f = 1 

192 with pytest.raises(FieldTypeError) as exc_info: 

193 o.f = 1.1 

194 

195 # msg = Value type should be one of ('str', 'int') 

196 msg = "Value type should be one of (%s, %s)" % (str, int) 

197 if sys.version_info < (3, 0): 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true

198 qualname = 'pyfields.tests.test_core.Foo.f' 

199 else: 

200 qualname = 'test_type_multiple_tuple.<locals>.Foo.f' 

201 assert str(exc_info.value) == "Invalid value type provided for '%s'. " \ 

202 "%s. " \ 

203 "Instead, received a 'float': 1.1" % (qualname, msg) 

204 

205 

206try: 

207 from typing import Optional 

208 typing_present = True 

209except ImportError: 

210 typing_present = False 

211 

212 

213@pytest.mark.skipif(not typing_present, reason="typing module is not present") 

214def test_type_multiple_typing(): 

215 """ Tests that when `type_hint` is provided and `validate_type` is explicitly set, it works as expected """ 

216 

217 from typing import Union 

218 

219 class Foo(object): 

220 f = field(type_hint=Union[int, str], check_type=True) 

221 

222 o = Foo() 

223 o.f = 'hello' 

224 o.f = 1 

225 with pytest.raises(FieldTypeError) as exc_info: 

226 o.f = 1.1 

227 

228 if sys.version_info < (3, 0): 228 ↛ 229line 228 didn't jump to line 229, because the condition on line 228 was never true

229 qualname = 'pyfields.tests.test_core.Foo.f' 

230 else: 

231 qualname = 'test_type_multiple_typing.<locals>.Foo.f' 

232 assert str(exc_info.value) == "Invalid value type provided for '%s'. " \ 

233 "Value should be of type typing.Union[int, str]. " \ 

234 "Instead, received a 'float': 1.1" % qualname 

235 

236 

237@pytest.mark.skipif(sys.version_info < (3, 6), reason="class member annotations are not allowed before python 3.6") 

238def test_type_from_pep484_annotations(): 

239 # import the class to use 

240 from ._test_py36 import _test_class_annotations 

241 Foo = _test_class_annotations() 

242 

243 # create an instance 

244 foo = Foo() 

245 

246 # test that the field that is non-native has type checking active 

247 foo.field_with_validate_type = 2 

248 with pytest.raises(TypeError) as exc_info: 

249 foo.field_with_validate_type = 'hello' 

250 assert str(exc_info.value).startswith("Invalid value type provided for ") 

251 

252 # by default the type is not checked 

253 foo.field_with_defaults = 'hello' 

254 

255 

256@pytest.mark.parametrize("case_nb", [1, 2, 3, 4, 5], ids="case_nb={}".format) 

257def test_field_validators(case_nb): 

258 """ tests that `validators` functionality works correctly with several flavours of definition.""" 

259 

260 # class EmptyError(ValidationError): 

261 # help_msg = "h should be non empty" 

262 

263 class EmptyFailure(ValidationFailure, ValueError): 

264 """ Custom ValidationFailure raised by non_empty """ 

265 help_msg = 'len(x) > 0 does not hold for x={wrong_value}' 

266 

267 class Foo2(object): 

268 # one single validator 

269 f = field(default="hey", type_hint=str, validators=non_empty) 

270 

271 # one single validator in a list 

272 g = field(type_hint=str, validators=[non_empty]) 

273 

274 # one single validator accepting three arguments (obj, field, val) 

275 gg = field(type_hint=str, validators=lambda obj, field, val: obj.f in val) 

276 

277 # several validators in a dict. keys and values can contain elements of definition in any order 

278 h = field(type_hint=str, validators=OrderedDict([("h should be non empty", (non_empty, EmptyFailure)), 

279 ("h should contain field f", (lambda obj, val: obj.f in val)), 

280 ("h should contain 'a'", (lambda val: 'a' in val))])) 

281 

282 if sys.version_info < (3, 0): 282 ↛ 284line 282 didn't jump to line 284, because the condition on line 282 was never true

283 # qualname does not exist, we use str(cls) 

284 c_name = "pyfields.tests.test_core.Foo2" 

285 else: 

286 c_name = "test_field_validators.<locals>.Foo2" 

287 

288 # the object that we'll use 

289 o = Foo2() 

290 

291 if case_nb == 1: 

292 o.f = 'hey' 

293 with pytest.raises(ValidationError) as exc_info: 

294 o.f = '' 

295 str(exc_info.value) 

296 assert isinstance(exc_info.value.failure, Empty) 

297 assert str(exc_info.value) == "Error validating [%s.f='']. " \ 

298 "Empty: len(x) > 0 does not hold for x=. Wrong value: ''." % c_name 

299 

300 elif case_nb == 2: 

301 o.g = 'hey' 

302 with pytest.raises(ValidationError) as exc_info: 

303 o.g = '' 

304 str(exc_info.value) 

305 assert isinstance(exc_info.value.failure, Empty) 

306 assert str(exc_info.value) == "Error validating [%s.g='']. " \ 

307 "Empty: len(x) > 0 does not hold for x=. Wrong value: ''." % c_name 

308 

309 elif case_nb == 3: 

310 o.gg = 'heyho' 

311 with pytest.raises(ValidationError) as exc_info: 

312 o.gg = 'ho' # does not contain field f ('hey') 

313 str(exc_info.value) 

314 assert isinstance(exc_info.value.failure, InvalidValue) 

315 assert exc_info.value.failure.validation_outcome is False 

316 assert str(exc_info.value) == "Error validating [%s.gg=ho]. " \ 

317 "InvalidValue: Function [<lambda>] returned [False] for value 'ho'." % c_name 

318 

319 elif case_nb in (4, 5): 319 ↛ exitline 319 didn't return from function 'test_field_validators', because the condition on line 319 was never false

320 if case_nb == 4: 

321 # override the definition for Foo2.h 

322 # several validators in a list. Tuples should start with the function 

323 Foo2.h = field(name='h', type_hint=str, validators=[(non_empty, "h should be non empty", EmptyFailure), 

324 non_empty, 

325 (lambda obj, val: obj.f in val, "h should contain field f"), 

326 (lambda val: 'a' in val, "h should contain 'a'"), 

327 (non_empty, EmptyFailure), 

328 ]) 

329 

330 # o.h should be a non-empty string containing 'a' and containing o.f 

331 with pytest.raises(ValidationError) as exc_info: 

332 o.h = '' # empty 

333 str(exc_info.value) 

334 assert isinstance(exc_info.value.failure.__cause__, EmptyFailure) 

335 assert str(exc_info.value.failure.__cause__) == "h should be non empty. " \ 

336 "Function [non_empty] raised " \ 

337 "Empty: len(x) > 0 does not hold for x=. Wrong value: ''." 

338 

339 with pytest.raises(ValidationError) as exc_info: 

340 o.h = 'hey' # does not contain 'a' 

341 assert isinstance(exc_info.value.failure.__cause__, InvalidValue) 

342 assert exc_info.value.failure.__cause__.validation_outcome is False 

343 assert str(exc_info.value.failure.__cause__) == "h should contain 'a'. " \ 

344 "Function [<lambda>] returned [False] for value 'hey'." 

345 

346 with pytest.raises(ValidationError) as exc_info: 

347 o.h = 'a' # does not contain field f ('hey') 

348 assert isinstance(exc_info.value.failure.__cause__, InvalidValue) 

349 assert exc_info.value.failure.__cause__.validation_outcome is False 

350 assert str(exc_info.value.failure.__cause__) == "h should contain field f. " \ 

351 "Function [<lambda>] returned [False] for value 'a'." 

352 o.h = 'hey ya' 

353 

354 

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

356def test_field_validators_decorator(explicit): 

357 """Tests that the @<field>.decorator works correctly""" 

358 

359 if explicit: 

360 native = False 

361 else: 

362 native = None 

363 if sys.version_info < (3, 6): 363 ↛ 364line 363 didn't jump to line 364, because the condition on line 363 was never true

364 with pytest.raises(UnsupportedOnNativeFieldError): 

365 class Foo(object): 

366 f = field(native=native) 

367 @f.validator 

368 def validate_f(self, val): 

369 return val % 3 == 0 

370 return 

371 

372 class Foo(object): 

373 f = field(native=native) 

374 

375 @f.validator 

376 def f_should_be_a_multiple_of_3(self, f_val): 

377 return f_val % 3 == 0 

378 

379 @f.validator(help_msg="not a large enough value") 

380 def f_should_be_larger_than_g(self, f_val): 

381 return f_val > self.g 

382 

383 f_field = Foo.f 

384 assert len(f_field.root_validator.base_validation_funcs) == 2 

385 foo = Foo() 

386 foo.g = 0 

387 with pytest.raises(ValidationError) as exc_info: 

388 foo.f = 2 

389 # assert str(exc_info.value) == "Error validating [%s=2]. " \ 

390 # "InvalidValue: Function [f_should_be_a_multiple_of_3] returned [False] for value 2." \ 

391 # % Foo.f.qualname 

392 assert str(exc_info.value) == "Error validating [%s=2]. At least one validation function failed for value 2. " \ 

393 "Successes: ['f_should_be_larger_than_g'] / " \ 

394 "Failures: {'f_should_be_a_multiple_of_3': 'Returned False.'}." \ 

395 % Foo.f.qualname 

396 foo.f = 3 

397 foo.g = 3 

398 with pytest.raises(ValidationError) as exc_info: 

399 foo.f = 3 

400 assert str(exc_info.value) == "Error validating [%s=3]. At least one validation function failed for value 3. " \ 

401 "Successes: ['f_should_be_a_multiple_of_3'] / " \ 

402 "Failures: {'f_should_be_larger_than_g': " \ 

403 "'InvalidValue: not a large enough value. Returned False.'}." \ 

404 % Foo.f.qualname 

405 

406 

407def test_validator_not_compliant_with_native_field(): 

408 """tests that `native=True` can not be set when a validator is provided""" 

409 with pytest.raises(UnsupportedOnNativeFieldError): 

410 class Foo(object): 

411 f = field(validators=lambda x: True, native=True) 411 ↛ exitline 411 didn't run the lambda on line 411

412 

413 

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

415def test_field_converters_decorator(explicit): 

416 """Tests that the @<field>.converter works correctly""" 

417 

418 if explicit: 

419 native = False 

420 else: 

421 native = None 

422 if sys.version_info < (3, 6): 422 ↛ 423line 422 didn't jump to line 423, because the condition on line 422 was never true

423 with pytest.raises(UnsupportedOnNativeFieldError): 

424 class Foo(object): 

425 f = field(native=native) 

426 @f.converter 

427 def validate_f(self, val): 

428 return val % 3 == 0 

429 return 

430 

431 class Foo(object): 

432 f = field(native=native) 

433 

434 @f.converter(accepts=str) 

435 def f_from_str(self, f_val): 

436 # make sure the filter has worked 

437 assert isinstance(f_val, str) 

438 return int(f_val) 

439 

440 @f.converter 

441 def f_from_anything(self, f_val): 

442 if isinstance(f_val, int): 

443 # of course we would not do that in real life but this is a test that exceptions are supported 

444 raise Exception("no need to convert! already an int") 

445 return int(f_val) + 1 

446 

447 f_field = Foo.f 

448 assert len(f_field.converters) == 2 

449 foo = Foo() 

450 foo.f = 0 # uses no converter at all 

451 assert foo.f == 0 

452 foo.f = '2' # uses the first converter 

453 assert foo.f == 2 

454 foo.f = 2.1 # uses the second converter 

455 assert foo.f == 3 

456 

457 

458def test_converter_not_compliant_with_native_field(): 

459 """tests that `native=True` can not be set when a validator is provided""" 

460 with pytest.raises(UnsupportedOnNativeFieldError): 

461 class Foo(object): 

462 f = field(converters=lambda x: x, native=True) 462 ↛ exitline 462 didn't run the lambda on line 462

463 

464 

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

466@pytest.mark.parametrize("nbargs", [1, 2, 3], ids="nbargs={}".format) 

467@pytest.mark.parametrize("format", ['single_converter', 'single_fun', 

468 '(v_fun, c_fun)', '(v_type, c_fun)', '(joker, c_fun)', '(None, c_fun)', 

469 '{v_fun: c_fun}', '{v_type: c_fun}', '{joker: c_fun}', '{None: c_fun}'], 

470 ids="format={}".format) 

471def test_converters(format, nbargs, validator_return_none): 

472 """Various tests about converters definition format""" 

473 

474 from mini_lambda import x 

475 

476 if nbargs == 1: 

477 def parse_nb(x): 

478 return int(x) 

479 

480 def valid_str(x): 

481 if validator_return_none: 

482 return None if isinstance(x, str) else False 

483 return isinstance(x, str) 

484 elif nbargs == 2: 

485 def parse_nb(obj, x): 

486 assert obj.__class__.__name__ == 'Foo' 

487 return int(x) 

488 

489 def valid_str(obj, x): 

490 assert obj.__class__.__name__ == 'Foo' 

491 if validator_return_none: 

492 return None if isinstance(x, str) else False 

493 return isinstance(x, str) 

494 elif nbargs == 3: 494 ↛ 507line 494 didn't jump to line 507, because the condition on line 494 was never false

495 def parse_nb(obj, field, x): 

496 assert obj.__class__.__name__ == 'Foo' 

497 assert isinstance(field, Field) 

498 return int(x) 

499 

500 def valid_str(obj, field, x): 

501 assert obj.__class__.__name__ == 'Foo' 

502 assert isinstance(field, Field) 

503 if validator_return_none: 

504 return None if isinstance(x, str) else False 

505 return isinstance(x, str) 

506 else: 

507 raise ValueError(nbargs) 

508 

509 if format == 'single_converter': 

510 class ParseNb(Converter): 

511 def convert(self, obj, field, x): 

512 if nbargs == 1: 

513 return parse_nb(x) 

514 elif nbargs == 2: 

515 return parse_nb(obj, x) 

516 elif nbargs == 3: 516 ↛ exitline 516 didn't return from function 'convert', because the condition on line 516 was never false

517 return parse_nb(obj, field, x) 

518 convs = ParseNb() 

519 accepts_int = True 

520 c_name = "ParseNb" 

521 

522 elif format == 'single_fun': 

523 convs = parse_nb 

524 accepts_int = True 

525 c_name = 'parse_nb' 

526 

527 elif format == '(v_fun, c_fun)': 

528 convs = (valid_str, parse_nb) 

529 accepts_int = False 

530 c_error_details = "Acceptance test: REJECTED (returned False)" 

531 c_name = 'parse_nb' 

532 

533 elif format == '(v_type, c_fun)': 

534 convs = (str, parse_nb) 

535 accepts_int = False 

536 c_error_details = "Acceptance test: ERROR [HasWrongType] Value should be an instance of %s. " \ 

537 "Wrong value: 1." % str 

538 c_name = 'parse_nb' 

539 

540 elif format == '(joker, c_fun)': 

541 convs = ('*', parse_nb) 

542 accepts_int = True 

543 c_name = 'parse_nb' 

544 

545 elif format == '(None, c_fun)': 

546 convs = (None, parse_nb) 

547 accepts_int = True 

548 c_name = 'parse_nb' 

549 

550 elif format == '{v_fun: c_fun}': 

551 convs = {valid_str: parse_nb} 

552 accepts_int = False 

553 c_error_details = "Acceptance test: REJECTED (returned False)" 

554 c_name = 'parse_nb' 

555 

556 elif format == '{v_type: c_fun}': 

557 convs = {str: parse_nb} 

558 accepts_int = False 

559 c_error_details = "Acceptance test: ERROR [HasWrongType] Value should be an instance of %s. " \ 

560 "Wrong value: 1." % str 

561 c_name = 'parse_nb' 

562 

563 elif format == '{joker: c_fun}': 

564 convs = {'*': parse_nb} 

565 accepts_int = True 

566 c_name = 'parse_nb' 

567 

568 elif format == '{None: c_fun}': 568 ↛ 574line 568 didn't jump to line 574, because the condition on line 568 was never false

569 convs = {None: parse_nb} 

570 accepts_int = True 

571 c_name = 'parse_nb' 

572 

573 else: 

574 raise ValueError(format) 

575 

576 class Foo(object): 

577 f = field(converters=convs, validators=[x % 3 == 0]) 

578 

579 o = Foo() 

580 f_field = Foo.f 

581 f_converters = f_field.converters 

582 assert len(f_converters) == 1 and isinstance(f_converters[0], Converter) 

583 o.f = 3 

584 o.f = '6' 

585 assert o.f == 6 

586 with pytest.raises(ValueError) as exc_info: 

587 o.f = '5' 

588 if sys.version_info < (3, 0): 588 ↛ 589line 588 didn't jump to line 589, because the condition on line 588 was never true

589 qualname = "pyfields.tests.test_core.Foo.f" # qualname does not exist, we use str(cls) 

590 else: 

591 qualname = "test_converters.<locals>.Foo.f" 

592 assert str(exc_info.value) == "Error validating [%s=5]. " \ 

593 "InvalidValue: Function [x %% 3 == 0] returned [False] for value 5." % qualname 

594 

595 if accepts_int: 

596 if nbargs == 1: 

597 converted_value, details = f_field.trace_convert(1) 

598 else: 

599 # we have to provide the object, as it is used in our converter 

600 converted_value, details = f_field.trace_convert(1, obj=o) 

601 

602 assert converted_value == 1 

603 assert str(details) == """Value 1 successfully converted to 1 using converter '%s', after the following attempts: 

604 - Converter '%s': Acceptance test: SUCCESS (returned None). Conversion: SUCCESS -> 1 

605""" % (c_name, c_name) 

606 else: 

607 with pytest.raises(ConversionError) as exc_info: 

608 if nbargs == 1: 

609 converted_value, details = f_field.trace_convert(1) 

610 else: 

611 # we have to provide the object, as it is used in our converter 

612 converted_value, details = f_field.trace_convert(1, obj=o) 

613 assert str(exc_info.value) == """Unable to convert value 1. Results: 

614 - Converter '%s': %s 

615""" % (c_name, c_error_details) 

616 

617 

618def test_inheritance(): 

619 """Makes sure that fields from parent classes are automatically fixed on old python versions. 

620 See https://github.com/smarie/python-pyfields/issues/41 

621 """ 

622 

623 class A(object): 

624 a = field(default='hello') 

625 

626 class B(A): 

627 pass 

628 

629 b = B() 

630 assert b.a == 'hello' # first access should have fixed the field name 

631 

632 # make sure that for all python versions (especially 2 and 3.5) the name is now ok. 

633 assert A.__dict__['a'].name == 'a' 

634 

635 

636class Foo(object): 

637 a = field(type_hint=int, default=0, check_type=True) 

638 b = field(type_hint=int, validators={'is positive': lambda x: x > 0}) 

639 c = field(default_factory=copy_field(a)) 

640 __init__ = make_init() 

641 

642 

643def test_pickle(): 

644 """ Tests that pickle actually works """ 

645 

646 f = Foo(b=1) 

647 serialized = pickle.dumps(f) 

648 g = pickle.loads(serialized) 

649 assert vars(g) == vars(f) 

650 

651 

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

653@pytest.mark.parametrize("default_flavor", ["simple", "copy_value", "factory_function"], ids="use_factory={}".format) 

654def test_default_validated(default_flavor, check_type): 

655 """ Tests that the default value of a DescriptorField is validated """ 

656 

657 # --- How the default value is created 

658 def make_def_kwargs(): 

659 if default_flavor == "simple": 

660 def_kwargs = dict(default=0) 

661 elif default_flavor == "copy_value": 

662 def_kwargs = dict(default_factory=copy_value(0)) 

663 elif default_flavor == "factory_function": 663 ↛ 669line 663 didn't jump to line 669, because the condition on line 663 was never false

664 def custom_factory(obj): 

665 # important note: this could be something dependent 

666 return 0 

667 def_kwargs = dict(default_factory=custom_factory) 

668 else: 

669 raise ValueError(default_flavor) 

670 return def_kwargs 

671 

672 def_kwargs = make_def_kwargs() 

673 

674 # --- validation can be a type check or a validator 

675 if check_type: 

676 validator = None 

677 else: 

678 # a validator that validates the same thing than the type hint 

679 def validator(x): 

680 return isinstance(x, str) 

681 

682 # nominal: the converter is used correctly on the default value so this is ok 

683 class Foo(object): 

684 bar = field(type_hint=str, check_type=check_type, validators=validator, converters=str, **def_kwargs) 

685 

686 # default value check 

687 bar_field = Foo.__dict__['bar'] 

688 if default_flavor == "simple": 

689 assert bar_field.default == 0 

690 assert bar_field._default_is_safe is False 

691 elif default_flavor == "copy_value": 

692 assert bar_field.default.get_copied_value() == 0 

693 assert bar_field._default_is_safe is False 

694 elif default_flavor == "factory_function": 694 ↛ 698line 694 didn't jump to line 698, because the condition on line 694 was never false

695 assert bar_field._default_is_safe is None 

696 

697 # instance creation and default value access 

698 f = Foo() 

699 assert f.bar == '0' 

700 

701 # we can check that the default value is modified 

702 if default_flavor == "simple": 

703 assert bar_field.default == '0' 

704 assert bar_field._default_is_safe is True 

705 elif default_flavor == "copy_value": 

706 assert bar_field.default.get_copied_value() == '0' 

707 assert bar_field._default_is_safe is True 

708 elif default_flavor == "factory_function": 708 ↛ 712line 708 didn't jump to line 712, because the condition on line 708 was never false

709 assert bar_field._default_is_safe is None 

710 

711 # make sure it works fine several times :) 

712 del f.bar 

713 assert f.bar == '0' 

714 g = Foo() 

715 assert g.bar == '0' 

716 

717 # no converter: does not work, the default value is not valid 

718 def_kwargs = make_def_kwargs() 

719 class Foo(object): 

720 bar = field(type_hint=str, check_type=check_type, validators=validator, **def_kwargs) 

721 

722 f = Foo() 

723 with pytest.raises(FieldTypeError if check_type else ValidationError): 

724 f.bar