Coverage for tests/test_000_frigid/test_500_dictionaries.py: 100%

313 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-24 04:09 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Assert correct function of dictionaries. ''' 

22 

23# mypy: ignore-errors 

24# pylint: disable=attribute-defined-outside-init 

25# pylint: disable=invalid-name,magic-value-comparison 

26# pylint: disable=pointless-statement,protected-access 

27# pylint: disable=too-many-locals,too-many-statements,unnecessary-dunder-call 

28# pylint: disable=unused-argument,unused-variable 

29 

30 

31import pytest 

32 

33from itertools import product 

34from types import MappingProxyType as DictionaryProxy 

35 

36from . import ( 

37 MODULES_QNAMES, 

38 PACKAGE_NAME, 

39 cache_import_module, 

40) 

41 

42 

43THESE_MODULE_QNAMES = tuple( 

44 name for name in MODULES_QNAMES if name.endswith( '.dictionaries' ) ) 

45THESE_CLASSES_NAMES = ( 'Dictionary', 'ValidatorDictionary' ) 

46VALIDATOR_NAMES = ( 'ValidatorDictionary', ) 

47 

48base = cache_import_module( f"{PACKAGE_NAME}.__" ) 

49exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) 

50 

51 

52def select_arguments( class_name ): 

53 ''' Chooses initializer arguments depending on class. ''' 

54 if class_name in VALIDATOR_NAMES: 

55 return ( lambda k, v: isinstance( v, int ), ), { } 

56 return ( ), { } 

57 

58 

59def select_simple_arguments( class_name ): 

60 ''' Choose simple test arguments depending on class. ''' 

61 posargs = ( ( ( 'foo', 1 ), ( 'bar', 2 ) ), { 'unicorn': True } ) 

62 nomargs = DictionaryProxy( dict( orb = False ) ) 

63 if class_name in VALIDATOR_NAMES: 

64 posargs = ( ( ( 'foo', 1 ), ( 'bar', 2 ) ), { 'unicorn': 42 } ) 

65 nomargs = DictionaryProxy( dict( orb = 84 ) ) 

66 return posargs, nomargs 

67 

68 

69@pytest.mark.parametrize( 

70 'module_qname, class_name', 

71 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

72) 

73def test_100_instantiation( module_qname, class_name ): 

74 ''' Class instantiates. ''' 

75 module = cache_import_module( module_qname ) 

76 factory = getattr( module, class_name ) 

77 posargs, nomargs = select_arguments( class_name ) 

78 dct = factory( *posargs, **nomargs ) 

79 assert isinstance( dct, factory ) 

80 simple_posargs, simple_nomargs = select_simple_arguments( class_name ) 

81 dct = factory( *posargs, *simple_posargs, **simple_nomargs ) 

82 assert isinstance( dct, factory ) 

83 assert 1 == dct[ 'foo' ] 

84 assert 2 == dct[ 'bar' ] 

85 assert ( 

86 dct[ 'unicorn' ] if class_name == 'Dictionary' 

87 else dct[ 'unicorn' ] == 42 ) 

88 assert ( 

89 not dct[ 'orb' ] if class_name == 'Dictionary' 

90 else dct[ 'orb' ] == 84 ) 

91 assert ( 'bar', 'foo', 'orb', 'unicorn' ) == tuple( sorted( dct.keys( ) ) ) 

92 

93 

94@pytest.mark.parametrize( 

95 'module_qname, class_name', 

96 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES ) 

97) 

98def test_102_instantiation( module_qname, class_name ): 

99 ''' Validator class instantiates. ''' 

100 module = cache_import_module( module_qname ) 

101 factory = getattr( module, class_name ) 

102 posargs, nomargs = select_arguments( class_name ) 

103 dct = factory( *posargs, **nomargs ) 

104 assert isinstance( dct, factory ) 

105 with pytest.raises( exceptions.EntryValidityError ): 

106 dct = factory( *posargs, invalid = 'str' ) 

107 

108 

109@pytest.mark.parametrize( 

110 'module_qname, class_name', 

111 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

112) 

113def test_110_attribute_immutability( module_qname, class_name ): 

114 ''' Dictionary attributes are immutable. ''' 

115 module = cache_import_module( module_qname ) 

116 factory = getattr( module, class_name ) 

117 posargs, nomargs = select_arguments( class_name ) 

118 obj = factory( *posargs, **nomargs ) 

119 with pytest.raises( AttributeError ): 

120 obj.attr = 42 

121 

122 

123@pytest.mark.parametrize( 

124 'module_qname, class_name', 

125 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

126) 

127def test_200_dictionary_entry_immutability( module_qname, class_name ): 

128 ''' Dictionary entries are immutable. ''' 

129 module = cache_import_module( module_qname ) 

130 factory = getattr( module, class_name ) 

131 posargs, nomargs = select_arguments( class_name ) 

132 simple_posargs, simple_nomargs = select_simple_arguments( class_name ) 

133 dct = factory( *posargs, *simple_posargs, **simple_nomargs ) 

134 with pytest.raises( exceptions.EntryImmutabilityError ): 

135 del dct[ 'foo' ] 

136 with pytest.raises( exceptions.EntryImmutabilityError ): 

137 dct[ 'foo' ] = 666 

138 with pytest.raises( exceptions.EntryImmutabilityError ): 

139 dct[ 'baz' ] = 43 

140 

141 

142@pytest.mark.parametrize( 

143 'module_qname, class_name', 

144 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

145) 

146def test_160_or_combines_dictionaries( module_qname, class_name ): 

147 ''' Dictionary union produces new dictionary with combined entries. ''' 

148 module = cache_import_module( module_qname ) 

149 factory = getattr( module, class_name ) 

150 posargs, nomargs = select_arguments( class_name ) 

151 # Test union of non-overlapping dictionaries 

152 d1 = factory( *posargs, a = 1 ) 

153 d2 = factory( *posargs, b = 2 ) 

154 d3 = { 'c': 3, 'd': 4 } 

155 # Union with another immutable dictionary 

156 d4 = d1 | d2 

157 assert isinstance( d4, factory ) 

158 assert d4 == { 'a': 1, 'b': 2 } 

159 # Union with regular dict 

160 d5 = d1 | d3 

161 assert isinstance( d5, factory ) 

162 assert d5 == { 'a': 1, 'c': 3, 'd': 4 } 

163 # Reverse union with regular dict 

164 d6 = d3 | d1 

165 assert isinstance( d6, factory ) 

166 assert d6 == { 'a': 1, 'c': 3, 'd': 4 } 

167 

168 

169@pytest.mark.parametrize( 

170 'module_qname, class_name', 

171 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

172) 

173def test_161_or_rejects_invalid_operands( module_qname, class_name ): 

174 ''' Dictionary union rejects non-mapping operands. ''' 

175 module = cache_import_module( module_qname ) 

176 factory = getattr( module, class_name ) 

177 posargs, nomargs = select_arguments( class_name ) 

178 dct = factory( *posargs, **nomargs ) 

179 assert NotImplemented == dct.__or__( [ ] ) 

180 assert NotImplemented == dct.__ror__( [ ] ) 

181 

182 

183@pytest.mark.parametrize( 

184 'module_qname, class_name', 

185 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

186) 

187def test_162_or_prevents_key_conflicts( module_qname, class_name ): 

188 ''' Dictionary union raises error on key conflicts. ''' 

189 module = cache_import_module( module_qname ) 

190 factory = getattr( module, class_name ) 

191 posargs, nomargs = select_arguments( class_name ) 

192 d1 = factory( *posargs, conflict_key = 1, unique1 = 2 ) 

193 d2 = factory( *posargs, conflict_key = 3, unique2 = 4 ) 

194 d3 = { 'conflict_key': 2, 'unique3': 5 } 

195 with pytest.raises( exceptions.EntryImmutabilityError ) as excinfo: 

196 d1 | d2 

197 assert "entry for 'conflict_key'" in str( excinfo.value ) 

198 with pytest.raises( exceptions.EntryImmutabilityError ) as excinfo: 

199 d1 | d3 

200 assert "entry for 'conflict_key'" in str( excinfo.value ) 

201 with pytest.raises( exceptions.EntryImmutabilityError ) as excinfo: 

202 d3 | d1 

203 assert "entry for 'conflict_key'" in str( excinfo.value ) 

204 

205 

206@pytest.mark.parametrize( 

207 'module_qname, class_name', 

208 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

209) 

210def test_170_and_intersects_mappings( module_qname, class_name ): 

211 ''' Dictionary intersection with mapping matches key-value pairs. ''' 

212 module = cache_import_module( module_qname ) 

213 factory = getattr( module, class_name ) 

214 posargs, nomargs = select_arguments( class_name ) 

215 d1 = factory( *posargs, a = 1, b = 2, c = 3 ) 

216 d2 = factory( *posargs, a = 1, b = 3, d = 4 ) 

217 d3 = { 'a': 1, 'c': 3, 'e': 5 } 

218 d4 = d1 & d2 

219 assert isinstance( d4, factory ) 

220 assert d4 == { 'a': 1 } 

221 d5 = d1 & d3 

222 assert isinstance( d5, factory ) 

223 assert d5 == { 'a': 1, 'c': 3 } 

224 d6 = d3 & d1 

225 assert isinstance( d6, factory ) 

226 assert d6 == { 'a': 1, 'c': 3 } 

227 

228 

229@pytest.mark.parametrize( 

230 'module_qname, class_name', 

231 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

232) 

233def test_171_and_filters_by_keys( module_qname, class_name ): 

234 ''' Dictionary intersection with set filters by keys. ''' 

235 module = cache_import_module( module_qname ) 

236 factory = getattr( module, class_name ) 

237 posargs, nomargs = select_arguments( class_name ) 

238 d1 = factory( *posargs, a = 1, b = 2, c = 3 ) 

239 s1 = { 'a', 'b' } 

240 d2 = d1 & s1 

241 assert isinstance( d2, factory ) 

242 assert d2 == { 'a': 1, 'b': 2 } 

243 d3 = d1 & factory( *posargs, x = 0, a = 9, b = 8 ).keys( ) 

244 assert isinstance( d3, factory ) 

245 assert d3 == { 'a': 1, 'b': 2 } 

246 d4 = s1 & d1 

247 assert isinstance( d4, factory ) 

248 assert d4 == { 'a': 1, 'b': 2 } 

249 

250 

251@pytest.mark.parametrize( 

252 'module_qname, class_name', 

253 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

254) 

255def test_172_and_rejects_invalid_operands( module_qname, class_name ): 

256 ''' Dictionary intersection rejects invalid operands. ''' 

257 module = cache_import_module( module_qname ) 

258 factory = getattr( module, class_name ) 

259 posargs, nomargs = select_arguments( class_name ) 

260 dct = factory( *posargs, **nomargs ) 

261 assert NotImplemented == dct.__and__( [ ] ) 

262 assert NotImplemented == dct.__rand__( [ ] ) 

263 

264 

265@pytest.mark.parametrize( 

266 'module_qname, class_name', 

267 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES ) 

268) 

269def test_202_validator_dictionary_validation( module_qname, class_name ): 

270 ''' Validator dictionary validates entries during creation. ''' 

271 module = cache_import_module( module_qname ) 

272 factory = getattr( module, class_name ) 

273 posargs, nomargs = select_arguments( class_name ) 

274 with pytest.raises( exceptions.EntryValidityError ): 

275 factory( *posargs, invalid = 'str' ) 

276 

277 

278@pytest.mark.parametrize( 

279 'module_qname, class_name', 

280 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES ) 

281) 

282def test_203_validator_dictionary_generator_handling( 

283 module_qname, class_name 

284): 

285 ''' Validator dictionary properly handles generator inputs. ''' 

286 module = cache_import_module( module_qname ) 

287 factory = getattr( module, class_name ) 

288 

289 def int_validator( k, v ): 

290 return isinstance( v, int ) 

291 

292 gen = ( ( str( i ), i ) for i in range( 3 ) ) 

293 dct = factory( int_validator, gen ) 

294 assert dct == { '0': 0, '1': 1, '2': 2 } 

295 gen = ( ( f'g{i}', i ) for i in range( 2 ) ) 

296 dct = factory( 

297 int_validator, 

298 gen, 

299 { 'm1': 10, 'm2': 20 }, 

300 k1 = 100, 

301 k2 = 200, 

302 ) 

303 assert dct == { 

304 'g0': 0, 'g1': 1, 

305 'm1': 10, 'm2': 20, 

306 'k1': 100, 'k2': 200, 

307 } 

308 gen = ( ( str( i ), 'bad' if i == 1 else i ) for i in range( 3 ) ) 

309 with pytest.raises( exceptions.EntryValidityError ): 

310 factory( int_validator, gen ) 

311 

312 

313@pytest.mark.parametrize( 

314 'module_qname, class_name', 

315 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES ) 

316) 

317def test_204_validator_dictionary_operations_preserve_validation( 

318 module_qname, class_name 

319): 

320 ''' Dictionary operations maintain validation rules. ''' 

321 module = cache_import_module( module_qname ) 

322 factory = getattr( module, class_name ) 

323 

324 def int_validator( k, v ): 

325 return isinstance( v, int ) 

326 

327 d1 = factory( int_validator, a = 1, b = 2 ) 

328 d2 = factory( int_validator, c = 3, d = 4 ) # No overlapping keys with d1 

329 d3 = { 'e': 5, 'f': 6 } # No overlapping keys with d1 

330 d4 = d1 & d2 

331 assert isinstance( d4, factory ) 

332 assert d4._validator_ is int_validator 

333 assert d4 == { } # No common key-value pairs 

334 d5 = d1 & d3 

335 assert isinstance( d5, factory ) 

336 assert d5._validator_ is int_validator 

337 assert d5 == { } # No common key-value pairs 

338 d6 = d1 | d2 

339 assert isinstance( d6, factory ) 

340 assert d6._validator_ is int_validator 

341 assert d6 == { 'a': 1, 'b': 2, 'c': 3, 'd': 4 } 

342 d7 = d1.with_data( x = 10, y = 20 ) 

343 assert isinstance( d7, factory ) 

344 assert d7._validator_ is int_validator 

345 assert d7 == { 'x': 10, 'y': 20 } 

346 d8 = d1.copy( ) 

347 assert isinstance( d8, factory ) 

348 assert d8._validator_ is int_validator 

349 assert d8 == d1 

350 

351 

352 

353@pytest.mark.parametrize( 

354 'module_qname, class_name', 

355 product( THESE_MODULE_QNAMES, VALIDATOR_NAMES ) 

356) 

357def test_205_validator_dictionary_complex_validation( 

358 module_qname, class_name 

359): 

360 ''' Validator dictionary handles complex validation rules. ''' 

361 module = cache_import_module( module_qname ) 

362 factory = getattr( module, class_name ) 

363 

364 def complex_validator( k, v ): 

365 return ( 

366 isinstance( k, str ) 

367 and isinstance( v, int ) 

368 and len( k ) == v 

369 ) 

370 

371 d1 = factory( complex_validator, a = 1, bb = 2, ccc = 3 ) 

372 assert d1 == { 'a': 1, 'bb': 2, 'ccc': 3 } 

373 with pytest.raises( exceptions.EntryValidityError ): 

374 factory( complex_validator, a = 2 ) # Value doesn't match key length 

375 with pytest.raises( exceptions.EntryValidityError ): 

376 factory( complex_validator, bb = 'x' ) # Value not int 

377 with pytest.raises( exceptions.EntryValidityError ): 

378 factory( complex_validator, { 123: 3 } ) # Key not string 

379 d2 = factory( complex_validator, bb = 2, xxx = 3 ) 

380 d3 = d1 & d2 

381 assert d3 == { 'bb': 2 } 

382 d4 = d1.with_data( zz = 2 ) 

383 assert d4 == { 'zz': 2 } 

384 

385 

386@pytest.mark.parametrize( 

387 'module_qname, class_name', 

388 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

389) 

390def test_220_duplication( module_qname, class_name ): 

391 ''' Dictionary is duplicable. ''' 

392 module = cache_import_module( module_qname ) 

393 factory = getattr( module, class_name ) 

394 posargs, nomargs = select_arguments( class_name ) 

395 odct = factory( *posargs, a = 1, b = 2 ) 

396 ddct = odct.copy( ) 

397 assert odct == ddct 

398 assert odct is not ddct 

399 

400 

401@pytest.mark.parametrize( 

402 'module_qname, class_name', 

403 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

404) 

405def test_221_dictionary_iterability( module_qname, class_name ): 

406 ''' Dictionary is iterable. ''' 

407 module = cache_import_module( module_qname ) 

408 factory = getattr( module, class_name ) 

409 posargs, nomargs = select_arguments( class_name ) 

410 simple_posargs, simple_nomargs = select_simple_arguments( class_name ) 

411 dct = factory( *posargs, *simple_posargs, **simple_nomargs ) 

412 assert frozenset( dct.keys( ) ) == frozenset( iter( dct ) ) 

413 assert tuple( dct.items( ) ) == tuple( zip( dct.keys( ), dct.values( ) ) ) 

414 

415 

416@pytest.mark.parametrize( 

417 'module_qname, class_name', 

418 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

419) 

420def test_222_dictionary_measurability( module_qname, class_name ): 

421 ''' Dictionary is measurable. ''' 

422 module = cache_import_module( module_qname ) 

423 factory = getattr( module, class_name ) 

424 posargs, nomargs = select_arguments( class_name ) 

425 simple_posargs, simple_nomargs = select_simple_arguments( class_name ) 

426 dct = factory( *posargs, *simple_posargs, **simple_nomargs ) 

427 assert len( dct.keys( ) ) == len( dct ) 

428 assert len( dct.items( ) ) == len( dct ) 

429 assert len( dct.values( ) ) == len( dct ) 

430 

431 

432@pytest.mark.parametrize( 

433 'module_qname, class_name', 

434 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

435) 

436def test_225_dictionary_equality( module_qname, class_name ): 

437 ''' Dictionary is equivalent to another dictionary with same values. ''' 

438 module = cache_import_module( module_qname ) 

439 factory = getattr( module, class_name ) 

440 posargs, nomargs = select_arguments( class_name ) 

441 simple_posargs, simple_nomargs = select_simple_arguments( class_name ) 

442 dct1 = factory( *posargs, *simple_posargs, **simple_nomargs ) 

443 dct2 = dct1.copy( ) 

444 dct3 = dict( dct1 ) 

445 assert dct1 == dct2 

446 assert dct2 == dct1 

447 assert dct1 == dct3 

448 assert dct3 == dct1 

449 assert not ( dct1 == -1 ) # pylint: disable=superfluous-parens 

450 assert dct1 != -1 

451 assert dct1 != ( ) 

452 

453 

454@pytest.mark.parametrize( 

455 'module_qname, class_name', 

456 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

457) 

458def test_230_string_representation( module_qname, class_name ): 

459 ''' Dictionary has expected string representations. ''' 

460 module = cache_import_module( module_qname ) 

461 factory = getattr( module, class_name ) 

462 posargs, nomargs = select_arguments( class_name ) 

463 simple_posargs, simple_nomargs = select_simple_arguments( class_name ) 

464 dct = factory( *posargs, *simple_posargs, **simple_nomargs ) 

465 cdct = dict( dct ) 

466 assert str( cdct ) == str( dct ) 

467 assert str( cdct ) in repr( dct ) 

468 assert base.calculate_fqname( dct ) in repr( dct ) 

469 

470 

471@pytest.mark.parametrize( 

472 'module_qname, class_name', 

473 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

474) 

475def test_240_dictionary_entry_optional_retrieval( module_qname, class_name ): 

476 ''' Default value on optional access of dictionary entry. ''' 

477 module = cache_import_module( module_qname ) 

478 factory = getattr( module, class_name ) 

479 posargs, nomargs = select_arguments( class_name ) 

480 simple_posargs, simple_nomargs = select_simple_arguments( class_name ) 

481 dct = factory( *posargs, *simple_posargs, **simple_nomargs ) 

482 assert None is dct.get( 'baz' ) 

483 assert -1 == dct.get( 'baz', -1 ) 

484 assert -1 == dct.get( 'baz', default = -1 ) 

485 assert 1 == dct.get( 'foo' ) 

486 assert 1 == dct.get( 'foo', -1 ) 

487 

488 

489@pytest.mark.parametrize( 

490 'module_qname, class_name', 

491 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

492) 

493def test_250_with_data( module_qname, class_name ): 

494 ''' Dictionary creates new instance with different data. ''' 

495 module = cache_import_module( module_qname ) 

496 factory = getattr( module, class_name ) 

497 posargs, nomargs = select_arguments( class_name ) 

498 d1 = factory( *posargs, a = 1, b = 2 ) 

499 new_data = { 'c': 3, 'd': 4 } 

500 d2 = d1.with_data( new_data ) 

501 assert isinstance( d2, factory ) 

502 assert type( d1 ) is type( d2 ) 

503 assert d1 != d2 

504 assert d2 == { 'c': 3, 'd': 4 } 

505 if class_name in VALIDATOR_NAMES: 

506 with pytest.raises( exceptions.EntryValidityError ): 

507 d2 = d1.with_data( invalid = 'str' ) 

508 

509 

510@pytest.mark.parametrize( 

511 'module_qname, class_name', 

512 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

513) 

514def test_260_subclasses_abc_dictionary( module_qname, class_name ): 

515 ''' Subclasses 'collections.abc.Mapping'. ''' 

516 from collections.abc import Mapping as AbstractDictionary 

517 module = cache_import_module( module_qname ) 

518 factory = getattr( module, class_name ) 

519 assert issubclass( factory, AbstractDictionary ) 

520 

521 

522@pytest.mark.parametrize( 

523 'module_qname, class_name', 

524 product( THESE_MODULE_QNAMES, THESE_CLASSES_NAMES ) 

525) 

526def test_900_docstring_sanity( module_qname, class_name ): 

527 ''' Class has valid docstring. ''' 

528 module = cache_import_module( module_qname ) 

529 factory = getattr( module, class_name ) 

530 assert hasattr( factory, '__doc__' ) 

531 assert isinstance( factory.__doc__, str ) 

532 assert factory.__doc__