Coverage for tests / test_Config.py: 15%

438 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 08:43 +0000

1# This file is part of pex_config. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28import io 

29import itertools 

30import os 

31import pickle 

32import re 

33import tempfile 

34import unittest 

35from types import SimpleNamespace 

36 

37try: 

38 import yaml 

39except ImportError: 

40 yaml = None 

41 

42import lsst.pex.config as pexConfig 

43 

44# Some tests depend on daf_base. 

45# Skip them if it is not found. 

46try: 

47 import lsst.daf.base as dafBase 

48except ImportError: 

49 dafBase = None 

50 

51GLOBAL_REGISTRY = {} 

52 

53 

54class Simple(pexConfig.Config): 

55 """A simple config used for testing.""" 

56 

57 i = pexConfig.Field("integer test", int, optional=True) 

58 f = pexConfig.Field("float test", float, default=3.0) 

59 b = pexConfig.Field("boolean test", bool, default=False, optional=False) 

60 c = pexConfig.ChoiceField( 

61 "choice test", str, default="Hello", allowed={"Hello": "First choice", "World": "second choice"} 

62 ) 

63 r = pexConfig.RangeField("Range test", float, default=3.0, optional=False, min=3.0, inclusiveMin=True) 

64 ll = pexConfig.ListField( 

65 "list test", 

66 int, 

67 default=[1, 2, 3], 

68 maxLength=5, 

69 itemCheck=lambda x: x is not None and x > 0, 

70 optional=True, 

71 ) 

72 d = pexConfig.DictField( 

73 "dict test", str, str, default={"key": "value"}, itemCheck=lambda x: x.startswith("v") 

74 ) 

75 n = pexConfig.Field("nan test", float, default=float("NAN")) 

76 

77 

78GLOBAL_REGISTRY["AAA"] = Simple 

79 

80 

81class InnerConfig(pexConfig.Config): 

82 """Inner config used for testing.""" 

83 

84 f = pexConfig.Field("Inner.f", float, default=0.0, check=lambda x: x >= 0, optional=False) 

85 

86 

87GLOBAL_REGISTRY["BBB"] = InnerConfig 

88 

89 

90class OuterConfig(InnerConfig, pexConfig.Config): 

91 """Outer config used for testing.""" 

92 

93 i = pexConfig.ConfigField("Outer.i", InnerConfig) 

94 

95 def __init__(self): 

96 pexConfig.Config.__init__(self) 

97 self.i.f = 5.0 

98 

99 def validate(self): 

100 pexConfig.Config.validate(self) 

101 if self.i.f < 5: 

102 raise ValueError("validation failed, outer.i.f must be greater than 5") 

103 

104 

105class Complex(pexConfig.Config): 

106 """A complex config for testing.""" 

107 

108 c = pexConfig.ConfigField("an inner config", InnerConfig) 

109 r = pexConfig.ConfigChoiceField( 

110 "a registry field", typemap=GLOBAL_REGISTRY, default="AAA", optional=False 

111 ) 

112 p = pexConfig.ConfigChoiceField("another registry", typemap=GLOBAL_REGISTRY, default="BBB", optional=True) 

113 

114 

115class Deprecation(pexConfig.Config): 

116 """A test config with a deprecated field.""" 

117 

118 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!") 

119 

120 

121class ConfigTest(unittest.TestCase): 

122 """Tests of basic Config functionality.""" 

123 

124 def setUp(self): 

125 self.simple = Simple() 

126 self.inner = InnerConfig() 

127 self.outer = OuterConfig() 

128 self.comp = Complex() 

129 self.deprecation = Deprecation() 

130 

131 def tearDown(self): 

132 del self.simple 

133 del self.inner 

134 del self.outer 

135 del self.comp 

136 

137 def testFieldTypeAnnotationRuntime(self): 

138 # test parsing type annotation for runtime dtype 

139 testField = pexConfig.Field[str](doc="test") 

140 self.assertEqual(testField.dtype, str) 

141 

142 # verify that forward references work correctly 

143 testField = pexConfig.Field["float"](doc="test") 

144 self.assertEqual(testField.dtype, float) 

145 

146 # verify that Field rejects multiple types 

147 with self.assertRaises(ValueError): 

148 pexConfig.Field[str, int](doc="test") # type: ignore 

149 

150 # verify that Field raises in conflict with dtype: 

151 with self.assertRaises(ValueError): 

152 pexConfig.Field[str](doc="test", dtype=int) 

153 

154 # verify that Field does not raise if dtype agrees 

155 testField = pexConfig.Field[int](doc="test", dtype=int) 

156 self.assertEqual(testField.dtype, int) 

157 

158 def testInit(self): 

159 self.assertIsNone(self.simple.i) 

160 self.assertEqual(self.simple.f, 3.0) 

161 self.assertFalse(self.simple.b) 

162 self.assertEqual(self.simple.c, "Hello") 

163 self.assertEqual(list(self.simple.ll), [1, 2, 3]) 

164 self.assertEqual(self.simple.d["key"], "value") 

165 self.assertEqual(self.inner.f, 0.0) 

166 self.assertEqual(self.deprecation.old, 10) 

167 

168 self.assertEqual(self.deprecation._fields["old"].doc, "Something. Deprecated: not used!") 

169 

170 self.assertEqual(self.outer.i.f, 5.0) 

171 self.assertEqual(self.outer.f, 0.0) 

172 

173 self.assertEqual(self.comp.c.f, 0.0) 

174 self.assertEqual(self.comp.r.name, "AAA") 

175 self.assertEqual(self.comp.r.active.f, 3.0) 

176 self.assertEqual(self.comp.r["BBB"].f, 0.0) 

177 

178 def testDeprecationWarning(self): 

179 """Test that a deprecated field emits a warning when it is set.""" 

180 with self.assertWarns(FutureWarning) as w: 

181 self.deprecation.old = 5 

182 self.assertEqual(self.deprecation.old, 5) 

183 

184 self.assertIn(self.deprecation._fields["old"].deprecated, str(w.warnings[-1].message)) 

185 

186 def testDeprecationOutput(self): 

187 """Test that a deprecated field is not written out unless it is set.""" 

188 stream = io.StringIO() 

189 self.deprecation.saveToStream(stream) 

190 self.assertNotIn("config.old", stream.getvalue()) 

191 with self.assertWarns(FutureWarning): 

192 self.deprecation.old = 5 

193 stream = io.StringIO() 

194 self.deprecation.saveToStream(stream) 

195 self.assertIn("config.old=5\n", stream.getvalue()) 

196 

197 def testDocstring(self): 

198 """Test that the docstring is not allowed to be empty.""" 

199 with self.assertRaises(ValueError): 

200 pexConfig.Field("", int, default=1) 

201 

202 with self.assertRaises(ValueError): 

203 pexConfig.RangeField("", int, default=3, min=3, max=4) 

204 

205 with self.assertRaises(ValueError): 

206 pexConfig.DictField("", str, str, default={"key": "value"}) 

207 

208 with self.assertRaises(ValueError): 

209 pexConfig.ListField("", int, default=[1, 2, 3]) 

210 

211 with self.assertRaises(ValueError): 

212 pexConfig.ConfigField("", InnerConfig) 

213 

214 with self.assertRaises(ValueError): 

215 pexConfig.ConfigChoiceField("", typemap=GLOBAL_REGISTRY, default="AAA") 

216 

217 def testValidate(self): 

218 self.simple.validate() 

219 

220 self.inner.validate() 

221 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5) 

222 self.outer.i.f = 10.0 

223 self.outer.validate() 

224 

225 with self.assertRaises(pexConfig.FieldValidationError): 

226 self.simple.d["failKey"] = "failValue" 

227 self.simple.validate() 

228 

229 self.outer.i = InnerConfig 

230 self.assertRaises(ValueError, self.outer.validate) 

231 self.outer.i = InnerConfig() 

232 self.assertRaises(ValueError, self.outer.validate) 

233 

234 self.comp.validate() 

235 self.comp.r = None 

236 self.assertRaises(ValueError, self.comp.validate) 

237 self.comp.r = "BBB" 

238 self.comp.validate() 

239 

240 def testRangeFieldConstructor(self): 

241 """Test RangeField constructor's checking of min, max.""" 

242 val = 3 

243 self.assertRaises(ValueError, pexConfig.RangeField, "test", int, default=val, min=val, max=val - 1) 

244 self.assertRaises( 

245 ValueError, pexConfig.RangeField, "test", float, default=val, min=val, max=val - 1e-15 

246 ) 

247 for inclusiveMin, inclusiveMax in itertools.product((False, True), (False, True)): 

248 if inclusiveMin and inclusiveMax: 

249 # should not raise 

250 class Cfg1(pexConfig.Config): 

251 r1 = pexConfig.RangeField( 

252 doc="test", 

253 dtype=int, 

254 default=val, 

255 min=val, 

256 max=val, 

257 inclusiveMin=inclusiveMin, 

258 inclusiveMax=inclusiveMax, 

259 ) 

260 r2 = pexConfig.RangeField( 

261 doc="test", 

262 dtype=float, 

263 default=val, 

264 min=val, 

265 max=val, 

266 inclusiveMin=inclusiveMin, 

267 inclusiveMax=inclusiveMax, 

268 ) 

269 

270 Cfg1() 

271 else: 

272 # raise while constructing the RangeField (hence cannot make 

273 # it part of a Config) 

274 self.assertRaises( 

275 ValueError, 

276 pexConfig.RangeField, 

277 doc="test", 

278 dtype=int, 

279 default=val, 

280 min=val, 

281 max=val, 

282 inclusiveMin=inclusiveMin, 

283 inclusiveMax=inclusiveMax, 

284 ) 

285 self.assertRaises( 

286 ValueError, 

287 pexConfig.RangeField, 

288 doc="test", 

289 dtype=float, 

290 default=val, 

291 min=val, 

292 max=val, 

293 inclusiveMin=inclusiveMin, 

294 inclusiveMax=inclusiveMax, 

295 ) 

296 

297 def testRangeFieldDefault(self): 

298 """Test RangeField's checking of the default value.""" 

299 minVal = 3 

300 maxVal = 4 

301 for val, inclusiveMin, inclusiveMax, shouldRaise in ( 

302 (minVal, False, True, True), 

303 (minVal, True, True, False), 

304 (maxVal, True, False, True), 

305 (maxVal, True, True, False), 

306 ): 

307 

308 class Cfg1(pexConfig.Config): 

309 r = pexConfig.RangeField( 

310 doc="test", 

311 dtype=int, 

312 default=val, 

313 min=minVal, 

314 max=maxVal, 

315 inclusiveMin=inclusiveMin, 

316 inclusiveMax=inclusiveMax, 

317 ) 

318 

319 class Cfg2(pexConfig.Config): 

320 r2 = pexConfig.RangeField( 

321 doc="test", 

322 dtype=float, 

323 default=val, 

324 min=minVal, 

325 max=maxVal, 

326 inclusiveMin=inclusiveMin, 

327 inclusiveMax=inclusiveMax, 

328 ) 

329 

330 if shouldRaise: 

331 self.assertRaises(pexConfig.FieldValidationError, Cfg1) 

332 self.assertRaises(pexConfig.FieldValidationError, Cfg2) 

333 else: 

334 Cfg1() 

335 Cfg2() 

336 

337 def testSave(self): 

338 self.comp.r = "BBB" 

339 self.comp.p = "AAA" 

340 self.comp.c.f = 5.0 

341 with tempfile.TemporaryDirectory(prefix="config-save-test", ignore_cleanup_errors=True) as tmpdir: 

342 roundtrip_path = os.path.join(tmpdir, "roundtrip.test") 

343 self.comp.save(roundtrip_path) 

344 

345 roundTrip = Complex() 

346 roundTrip.load(roundtrip_path) 

347 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

348 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

349 del roundTrip 

350 

351 # test saving to an open file 

352 roundtrip_path = os.path.join(tmpdir, "roundtrip_open.test") 

353 with open(roundtrip_path, "w") as outfile: 

354 self.comp.saveToStream(outfile) 

355 roundTrip = Complex() 

356 with open(roundtrip_path) as infile: 

357 roundTrip.loadFromStream(infile) 

358 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

359 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

360 del roundTrip 

361 

362 # Test an override of the default variable name. 

363 roundtrip_path = os.path.join(tmpdir, "roundtrip_def.test") 

364 with open(roundtrip_path, "w") as outfile: 

365 self.comp.saveToStream(outfile, root="root") 

366 roundTrip = Complex() 

367 with self.assertRaises(NameError): 

368 roundTrip.load(roundtrip_path) 

369 roundTrip.load(roundtrip_path, root="root") 

370 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

371 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

372 

373 # test saving to a string. 

374 saved_string = self.comp.saveToString() 

375 saved_string += "config.c.f = parameters.value" 

376 namespace = SimpleNamespace(value=7) 

377 extraLocals = {"parameters": namespace} 

378 roundTrip = Complex() 

379 roundTrip.loadFromString(saved_string, extraLocals=extraLocals) 

380 self.assertEqual(namespace.value, roundTrip.c.f) 

381 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

382 with self.assertRaises(ValueError): 

383 roundTrip.loadFromString(saved_string, root="config", extraLocals={"config": 6}) 

384 del roundTrip 

385 

386 def testDuplicateRegistryNames(self): 

387 self.comp.r["AAA"].f = 5.0 

388 self.assertEqual(self.comp.p["AAA"].f, 3.0) 

389 

390 def testInheritance(self): 

391 class AAA(pexConfig.Config): 

392 a = pexConfig.Field("AAA.a", int, default=4) 

393 

394 class BBB(AAA): 

395 b = pexConfig.Field("BBB.b", int, default=3) 

396 

397 class CCC(BBB): 

398 c = pexConfig.Field("CCC.c", int, default=2) 

399 

400 # test multi-level inheritance 

401 c = CCC() 

402 self.assertIn("a", c.toDict()) 

403 self.assertEqual(c._fields["a"].dtype, int) 

404 self.assertEqual(c.a, 4) 

405 

406 # test conflicting multiple inheritance 

407 class DDD(pexConfig.Config): 

408 a = pexConfig.Field("DDD.a", float, default=0.0) 

409 

410 class EEE(DDD, AAA): 

411 pass 

412 

413 e = EEE() 

414 self.assertEqual(e._fields["a"].dtype, float) 

415 self.assertIn("a", e.toDict()) 

416 self.assertEqual(e.a, 0.0) 

417 

418 class FFF(AAA, DDD): 

419 pass 

420 

421 f = FFF() 

422 self.assertEqual(f._fields["a"].dtype, int) 

423 self.assertIn("a", f.toDict()) 

424 self.assertEqual(f.a, 4) 

425 

426 # test inheritance from non Config objects 

427 class GGG: 

428 a = pexConfig.Field("AAA.a", float, default=10.0) 

429 

430 class HHH(GGG, AAA): 

431 pass 

432 

433 h = HHH() 

434 self.assertEqual(h._fields["a"].dtype, float) 

435 self.assertIn("a", h.toDict()) 

436 self.assertEqual(h.a, 10.0) 

437 

438 # test partial Field redefinition 

439 

440 class III(AAA): 

441 pass 

442 

443 III.a.default = 5 

444 

445 self.assertEqual(III.a.default, 5) 

446 self.assertEqual(AAA.a.default, 4) 

447 

448 @unittest.skipIf(dafBase is None, "lsst.daf.base is required") 

449 def testConvertPropertySet(self): 

450 ps = pexConfig.makePropertySet(self.simple) 

451 self.assertFalse(ps.exists("i")) 

452 self.assertEqual(ps.getScalar("f"), self.simple.f) 

453 self.assertEqual(ps.getScalar("b"), self.simple.b) 

454 self.assertEqual(ps.getScalar("c"), self.simple.c) 

455 self.assertEqual(list(ps.getArray("ll")), list(self.simple.ll)) 

456 

457 ps = pexConfig.makePropertySet(self.comp) 

458 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f) 

459 

460 def testFreeze(self): 

461 self.comp.freeze() 

462 

463 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.c, "f", 10.0) 

464 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "r", "AAA") 

465 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "p", "AAA") 

466 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.p["AAA"], "f", 5.0) 

467 

468 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere): 

469 self.comp.c.f = 5.0 

470 

471 # Generate a Config through loading 

472 stream = io.StringIO() 

473 stream.write(str(importStatement)) 

474 self.comp.saveToStream(stream) 

475 roundtrip = Complex() 

476 roundtrip.loadFromStream(stream.getvalue()) 

477 self.assertEqual(self.comp.c.f, roundtrip.c.f) 

478 

479 # Check the save stream 

480 stream = io.StringIO() 

481 roundtrip.saveToStream(stream) 

482 self.assertEqual(self.comp.c.f, roundtrip.c.f) 

483 streamStr = stream.getvalue() 

484 if shouldBeThere: 

485 self.assertTrue(re.search(searchString, streamStr)) 

486 else: 

487 self.assertFalse(re.search(searchString, streamStr)) 

488 

489 def testImports(self): 

490 # A module not used by anything else, but which exists 

491 importing = "import lsst.pex.config._doNotImportMe\n" 

492 self.checkImportRoundTrip(importing, importing, True) 

493 

494 def testBadImports(self): 

495 dummy = "somethingThatDoesntExist" 

496 importing = f""" 

497try: 

498 import {dummy} 

499except ImportError: 

500 pass 

501""" 

502 self.checkImportRoundTrip(importing, dummy, False) 

503 

504 def testPickle(self): 

505 self.simple.f = 5 

506 simple = pickle.loads(pickle.dumps(self.simple)) 

507 self.assertIsInstance(simple, Simple) 

508 self.assertEqual(self.simple.f, simple.f) 

509 

510 self.comp.c.f = 5 

511 comp = pickle.loads(pickle.dumps(self.comp)) 

512 self.assertIsInstance(comp, Complex) 

513 self.assertEqual(self.comp.c.f, comp.c.f) 

514 

515 @unittest.skipIf(yaml is None, "Test requires pyyaml") 

516 def testYaml(self): 

517 self.simple.f = 5 

518 simple = yaml.safe_load(yaml.dump(self.simple)) 

519 self.assertIsInstance(simple, Simple) 

520 self.assertEqual(self.simple.f, simple.f) 

521 

522 self.comp.c.f = 5 

523 # Use a different loader to check that it also works 

524 comp = yaml.load(yaml.dump(self.comp), Loader=yaml.FullLoader) 

525 self.assertIsInstance(comp, Complex) 

526 self.assertEqual(self.comp.c.f, comp.c.f) 

527 

528 def testCompare(self): 

529 comp2 = Complex() 

530 inner2 = InnerConfig() 

531 simple2 = Simple() 

532 self.assertTrue(self.comp.compare(comp2)) 

533 self.assertTrue(comp2.compare(self.comp)) 

534 self.assertTrue(self.comp.c.compare(inner2)) 

535 self.assertTrue(self.simple.compare(simple2)) 

536 self.assertTrue(simple2.compare(self.simple)) 

537 self.assertEqual(self.simple, simple2) 

538 self.assertEqual(simple2, self.simple) 

539 outList = [] 

540 

541 def outFunc(msg): 

542 outList.append(msg) 

543 

544 simple2.b = True 

545 simple2.ll.append(4) 

546 simple2.d["foo"] = "var" 

547 self.assertFalse(self.simple.compare(simple2, shortcut=True, output=outFunc)) 

548 self.assertEqual(len(outList), 1) 

549 del outList[:] 

550 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc)) 

551 output = "\n".join(outList) 

552 self.assertIn("b: ", output) 

553 self.assertIn("ll (len): ", output) 

554 self.assertIn("d (keys): ", output) 

555 del outList[:] 

556 self.simple.d["foo"] = "vast" 

557 self.simple.ll.append(5) 

558 self.simple.b = True 

559 self.simple.f += 1e8 

560 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc)) 

561 output = "\n".join(outList) 

562 self.assertIn("f: ", output) 

563 self.assertIn("ll[3]: ", output) 

564 self.assertIn("d['foo']: ", output) 

565 del outList[:] 

566 comp2.r["BBB"].f = 1.0 # changing the non-selected item shouldn't break equality 

567 self.assertTrue(self.comp.compare(comp2)) 

568 comp2.r["AAA"].i = 56 # changing the selected item should break equality 

569 comp2.c.f = 1.0 

570 self.assertFalse(self.comp.compare(comp2, shortcut=False, output=outFunc)) 

571 output = "\n".join(outList) 

572 self.assertIn("c.f: ", output) 

573 self.assertIn("r['AAA']", output) 

574 self.assertNotIn("r['BBB']", output) 

575 

576 # Before DM-16561, this incorrectly returned `True`. 

577 self.assertFalse(self.inner.compare(self.outer)) 

578 # Before DM-16561, this raised. 

579 self.assertFalse(self.outer.compare(self.inner)) 

580 

581 outList.clear() 

582 simple3 = Simple() 

583 simple3.i = 2 

584 simple4 = Simple() 

585 self.assertFalse(simple4.compare(simple3, output=outFunc)) 

586 self.assertEqual(outList[-1], "i: None != 2") 

587 self.assertFalse(simple3.compare(simple4, output=outFunc)) 

588 self.assertEqual(outList[-1], "i: 2 != None") 

589 simple3.i = None 

590 

591 outList.clear() 

592 self.assertFalse(simple4.compare(comp2, output=outFunc)) 

593 self.assertIn("test_Config.Simple != test_Config.Complex", outList[-1]) 

594 

595 outList.clear() 

596 self.assertFalse(pexConfig.compareConfigs("s", simple4, None, output=outFunc)) 

597 self.assertIn("!= None", outList[-1]) 

598 

599 outList.clear() 

600 self.assertFalse(pexConfig.compareConfigs("s", None, simple4, output=outFunc)) 

601 self.assertIn("None !=", outList[-1]) 

602 

603 outList.clear() 

604 simple3.ll = None 

605 self.assertFalse(simple4.compare(simple3, output=outFunc)) 

606 self.assertIn("ll", outList[-1]) 

607 outList.clear() 

608 self.assertFalse(simple3.compare(simple4, output=outFunc)) 

609 self.assertTrue(outList[-1].startswith("ll: None")) 

610 

611 def testLoadError(self): 

612 """Check that loading allows errors in the file being loaded to 

613 propagate. 

614 """ 

615 self.assertRaises(SyntaxError, self.simple.loadFromStream, "bork bork bork") 

616 self.assertRaises(NameError, self.simple.loadFromStream, "config.f = bork") 

617 

618 def testNames(self): 

619 """Check that the names() method returns valid keys. 

620 

621 Also check that we have the right number of keys, and as they are 

622 all known to be valid we know that we got them all. 

623 """ 

624 names = self.simple.names() 

625 self.assertEqual(len(names), 8) 

626 for name in names: 

627 self.assertTrue(hasattr(self.simple, name)) 

628 

629 def testIteration(self): 

630 self.assertIn("ll", self.simple) 

631 self.assertIn("ll", self.simple.keys()) 

632 self.assertIn("Hello", self.simple.values()) 

633 self.assertEqual(len(self.simple.values()), 8) 

634 

635 for k, v, (k1, v1) in zip(self.simple.keys(), self.simple.values(), self.simple.items(), strict=True): 

636 self.assertEqual(k, k1) 

637 if k == "n": 

638 self.assertNotEqual(v, v1) 

639 else: 

640 self.assertEqual(v, v1) 

641 

642 def test_copy(self): 

643 """Test the copy method.""" 

644 self.comp.freeze() 

645 copy1 = self.comp.copy() 

646 copy1.c.f = 6.0 

647 self.assertEqual(copy1.c.f, 6.0) 

648 self.assertEqual(self.comp.c.f, 0.0) 

649 copy1.r["AAA"].i = 1 

650 self.assertEqual(copy1.r["AAA"].i, 1) 

651 self.assertIsNone(self.comp.r["AAA"].i) 

652 copy1.r["AAA"].f = 2.0 

653 self.assertEqual(copy1.r["AAA"].f, 2.0) 

654 self.assertEqual(self.comp.r["AAA"].f, 3.0) 

655 copy1.r["AAA"].c = "World" 

656 self.assertEqual(copy1.r["AAA"].c, "World") 

657 self.assertEqual(self.comp.r["AAA"].c, "Hello") 

658 copy1.r["AAA"].r = 4.0 

659 self.assertEqual(copy1.r["AAA"].r, 4.0) 

660 self.assertEqual(self.comp.r["AAA"].r, 3.0) 

661 copy1.r["AAA"].ll.append(4) 

662 self.assertEqual(copy1.r["AAA"].ll, [1, 2, 3, 4]) 

663 self.assertEqual(self.comp.r["AAA"].ll, [1, 2, 3]) 

664 copy1.r["AAA"].d["key2"] = "value2" 

665 self.assertEqual(copy1.r["AAA"].d, {"key": "value", "key2": "value2"}) 

666 self.assertEqual(self.comp.r["AAA"].d, {"key": "value"}) 

667 copy1.r.name = "BBB" 

668 self.assertEqual(copy1.r.name, "BBB") 

669 self.assertEqual(self.comp.r.name, "AAA") 

670 copy1.p.name = None 

671 self.assertIsNone(copy1.p.name) 

672 self.assertEqual(self.comp.p.name, "BBB") 

673 # Copy again to avoid shortcuts for default nested objects. 

674 copy1.freeze() 

675 copy2 = copy1.copy() 

676 copy2.c.f = 7.0 

677 self.assertEqual(copy2.c.f, 7.0) 

678 self.assertEqual(copy1.c.f, 6.0) 

679 self.assertEqual(self.comp.c.f, 0.0) 

680 copy2.r["AAA"].ll.append(5) 

681 self.assertEqual(copy2.r["AAA"].ll, [1, 2, 3, 4, 5]) 

682 self.assertEqual(copy1.r["AAA"].ll, [1, 2, 3, 4]) 

683 self.assertEqual(self.comp.r["AAA"].ll, [1, 2, 3]) 

684 del copy2.r["AAA"].d["key"] 

685 self.assertEqual(copy2.r["AAA"].d, {"key2": "value2"}) 

686 self.assertEqual(copy1.r["AAA"].d, {"key": "value", "key2": "value2"}) 

687 self.assertEqual(self.comp.r["AAA"].d, {"key": "value"}) 

688 copy2.r.name = "AAA" 

689 self.assertEqual(copy2.r.name, "AAA") 

690 self.assertEqual(copy1.r.name, "BBB") 

691 self.assertEqual(self.comp.r.name, "AAA") 

692 copy2.p.name = "AAA" 

693 self.assertEqual(copy2.p.name, "AAA") 

694 self.assertIsNone(copy1.p.name) 

695 self.assertEqual(self.comp.p.name, "BBB") 

696 

697 

698if __name__ == "__main__": 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true

699 unittest.main()