Coverage for tests / test_Config.py: 15%
438 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:53 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:53 +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/>.
28import io
29import itertools
30import os
31import pickle
32import re
33import tempfile
34import unittest
35from types import SimpleNamespace
37try:
38 import yaml
39except ImportError:
40 yaml = None
42import lsst.pex.config as pexConfig
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
51GLOBAL_REGISTRY = {}
54class Simple(pexConfig.Config):
55 """A simple config used for testing."""
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"))
78GLOBAL_REGISTRY["AAA"] = Simple
81class InnerConfig(pexConfig.Config):
82 """Inner config used for testing."""
84 f = pexConfig.Field("Inner.f", float, default=0.0, check=lambda x: x >= 0, optional=False)
87GLOBAL_REGISTRY["BBB"] = InnerConfig
90class OuterConfig(InnerConfig, pexConfig.Config):
91 """Outer config used for testing."""
93 i = pexConfig.ConfigField("Outer.i", InnerConfig)
95 def __init__(self):
96 pexConfig.Config.__init__(self)
97 self.i.f = 5.0
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")
105class Complex(pexConfig.Config):
106 """A complex config for testing."""
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)
115class Deprecation(pexConfig.Config):
116 """A test config with a deprecated field."""
118 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!")
121class ConfigTest(unittest.TestCase):
122 """Tests of basic Config functionality."""
124 def setUp(self):
125 self.simple = Simple()
126 self.inner = InnerConfig()
127 self.outer = OuterConfig()
128 self.comp = Complex()
129 self.deprecation = Deprecation()
131 def tearDown(self):
132 del self.simple
133 del self.inner
134 del self.outer
135 del self.comp
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)
142 # verify that forward references work correctly
143 testField = pexConfig.Field["float"](doc="test")
144 self.assertEqual(testField.dtype, float)
146 # verify that Field rejects multiple types
147 with self.assertRaises(ValueError):
148 pexConfig.Field[str, int](doc="test") # type: ignore
150 # verify that Field raises in conflict with dtype:
151 with self.assertRaises(ValueError):
152 pexConfig.Field[str](doc="test", dtype=int)
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)
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)
168 self.assertEqual(self.deprecation._fields["old"].doc, "Something. Deprecated: not used!")
170 self.assertEqual(self.outer.i.f, 5.0)
171 self.assertEqual(self.outer.f, 0.0)
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)
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)
184 self.assertIn(self.deprecation._fields["old"].deprecated, str(w.warnings[-1].message))
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())
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)
202 with self.assertRaises(ValueError):
203 pexConfig.RangeField("", int, default=3, min=3, max=4)
205 with self.assertRaises(ValueError):
206 pexConfig.DictField("", str, str, default={"key": "value"})
208 with self.assertRaises(ValueError):
209 pexConfig.ListField("", int, default=[1, 2, 3])
211 with self.assertRaises(ValueError):
212 pexConfig.ConfigField("", InnerConfig)
214 with self.assertRaises(ValueError):
215 pexConfig.ConfigChoiceField("", typemap=GLOBAL_REGISTRY, default="AAA")
217 def testValidate(self):
218 self.simple.validate()
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()
225 with self.assertRaises(pexConfig.FieldValidationError):
226 self.simple.d["failKey"] = "failValue"
227 self.simple.validate()
229 self.outer.i = InnerConfig
230 self.assertRaises(ValueError, self.outer.validate)
231 self.outer.i = InnerConfig()
232 self.assertRaises(ValueError, self.outer.validate)
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()
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 )
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 )
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 ):
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 )
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 )
330 if shouldRaise:
331 self.assertRaises(pexConfig.FieldValidationError, Cfg1)
332 self.assertRaises(pexConfig.FieldValidationError, Cfg2)
333 else:
334 Cfg1()
335 Cfg2()
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)
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
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
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)
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
386 def testDuplicateRegistryNames(self):
387 self.comp.r["AAA"].f = 5.0
388 self.assertEqual(self.comp.p["AAA"].f, 3.0)
390 def testInheritance(self):
391 class AAA(pexConfig.Config):
392 a = pexConfig.Field("AAA.a", int, default=4)
394 class BBB(AAA):
395 b = pexConfig.Field("BBB.b", int, default=3)
397 class CCC(BBB):
398 c = pexConfig.Field("CCC.c", int, default=2)
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)
406 # test conflicting multiple inheritance
407 class DDD(pexConfig.Config):
408 a = pexConfig.Field("DDD.a", float, default=0.0)
410 class EEE(DDD, AAA):
411 pass
413 e = EEE()
414 self.assertEqual(e._fields["a"].dtype, float)
415 self.assertIn("a", e.toDict())
416 self.assertEqual(e.a, 0.0)
418 class FFF(AAA, DDD):
419 pass
421 f = FFF()
422 self.assertEqual(f._fields["a"].dtype, int)
423 self.assertIn("a", f.toDict())
424 self.assertEqual(f.a, 4)
426 # test inheritance from non Config objects
427 class GGG:
428 a = pexConfig.Field("AAA.a", float, default=10.0)
430 class HHH(GGG, AAA):
431 pass
433 h = HHH()
434 self.assertEqual(h._fields["a"].dtype, float)
435 self.assertIn("a", h.toDict())
436 self.assertEqual(h.a, 10.0)
438 # test partial Field redefinition
440 class III(AAA):
441 pass
443 III.a.default = 5
445 self.assertEqual(III.a.default, 5)
446 self.assertEqual(AAA.a.default, 4)
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))
457 ps = pexConfig.makePropertySet(self.comp)
458 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
460 def testFreeze(self):
461 self.comp.freeze()
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)
468 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
469 self.comp.c.f = 5.0
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)
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))
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)
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)
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)
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)
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)
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)
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 = []
541 def outFunc(msg):
542 outList.append(msg)
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)
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))
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
591 outList.clear()
592 self.assertFalse(simple4.compare(comp2, output=outFunc))
593 self.assertIn("test_Config.Simple != test_Config.Complex", outList[-1])
595 outList.clear()
596 self.assertFalse(pexConfig.compareConfigs("s", simple4, None, output=outFunc))
597 self.assertIn("!= None", outList[-1])
599 outList.clear()
600 self.assertFalse(pexConfig.compareConfigs("s", None, simple4, output=outFunc))
601 self.assertIn("None !=", outList[-1])
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"))
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")
618 def testNames(self):
619 """Check that the names() method returns valid keys.
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))
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)
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)
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")
698if __name__ == "__main__": 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true
699 unittest.main()