|
27 | 27 | SUPPORT_LAZY_IMPORT, |
28 | 28 | SETATTR_TARGET, |
29 | 29 | LazyModule, |
| 30 | + _check_SETATTR_TARGET_value, |
30 | 31 | force_load, |
31 | 32 | is_lazy, |
32 | 33 | lazy, |
@@ -626,5 +627,151 @@ def test_configuration_via_core_round_trips(self): |
626 | 627 | delattr(math, "SCRATCH2") |
627 | 628 |
|
628 | 629 |
|
| 630 | +# License MIT <aiwonderland> in <2026> |
| 631 | +class TestCheckSetattrTargetValue(unittest.TestCase): |
| 632 | + """Tests for ``_check_SETATTR_TARGET_value``. |
| 633 | +
|
| 634 | + The validator is the single source of truth for what counts as a |
| 635 | + legal ``SETATTR_TARGET`` value. It must: |
| 636 | +
|
| 637 | + * return the validated value (acting as a normaliser); |
| 638 | + * accept both ``"module"`` and ``"proxy"``; |
| 639 | + * reject unknown strings with ``ValueError``; |
| 640 | + * reject non-string arguments with ``TypeError``; |
| 641 | + * treat ``value=None`` as a request to re-check the current |
| 642 | + module-level ``SETATTR_TARGET``. |
| 643 | + """ |
| 644 | + |
| 645 | + def setUp(self): |
| 646 | + # Save and restore the global so an exception-raising test |
| 647 | + # does not leave the package in a broken state for the |
| 648 | + # tests that follow. |
| 649 | + self._previous_mode = core.SETATTR_TARGET |
| 650 | + |
| 651 | + def tearDown(self): |
| 652 | + core.SETATTR_TARGET = self._previous_mode |
| 653 | + if core.SETATTR_TARGET not in ("module", "proxy"): |
| 654 | + core.SETATTR_TARGET = "module" |
| 655 | + |
| 656 | + # ------------------------------------------------------------------ |
| 657 | + # Happy path |
| 658 | + # ------------------------------------------------------------------ |
| 659 | + def test_default_module_value_passes(self): |
| 660 | + # The shipped default must validate without raising. We |
| 661 | + # call without an argument so the function checks the |
| 662 | + # current ``SETATTR_TARGET``. |
| 663 | + result = _check_SETATTR_TARGET_value() |
| 664 | + self.assertEqual(result, SETATTR_TARGET) |
| 665 | + self.assertEqual(result, "module") |
| 666 | + |
| 667 | + def test_explicit_module_string(self): |
| 668 | + result = _check_SETATTR_TARGET_value("module") |
| 669 | + self.assertEqual(result, "module") |
| 670 | + |
| 671 | + def test_explicit_proxy_string(self): |
| 672 | + result = _check_SETATTR_TARGET_value("proxy") |
| 673 | + self.assertEqual(result, "proxy") |
| 674 | + |
| 675 | + def test_none_argument_validates_current_value(self): |
| 676 | + # ``None`` is documented as ``"check the current value"``, |
| 677 | + # not as "missing argument". This is a behaviour the tests |
| 678 | + # lock down so a future refactor cannot quietly change it. |
| 679 | + core.SETATTR_TARGET = "proxy" |
| 680 | + result = _check_SETATTR_TARGET_value(None) |
| 681 | + self.assertEqual(result, "proxy") |
| 682 | + core.SETATTR_TARGET = "module" |
| 683 | + result = _check_SETATTR_TARGET_value(None) |
| 684 | + self.assertEqual(result, "module") |
| 685 | + |
| 686 | + def test_returns_valid_value_for_use_as_normaliser(self): |
| 687 | + # The return value must equal the input when it is valid, |
| 688 | + # which lets callers use the function as a one-stop |
| 689 | + # ``str -> Literal[...]`` converter. |
| 690 | + self.assertEqual(_check_SETATTR_TARGET_value("module"), "module") |
| 691 | + self.assertEqual(_check_SETATTR_TARGET_value("proxy"), "proxy") |
| 692 | + |
| 693 | + # ------------------------------------------------------------------ |
| 694 | + # Value errors |
| 695 | + # ------------------------------------------------------------------ |
| 696 | + def test_unknown_string_raises_value_error(self): |
| 697 | + for bad in ("MODULE", "Module", "Module ", "", "modules", "prxy"): |
| 698 | + with self.subTest(value=bad): |
| 699 | + with self.assertRaises(ValueError): |
| 700 | + _check_SETATTR_TARGET_value(bad) |
| 701 | + |
| 702 | + def test_value_error_message_lists_accepted_values(self): |
| 703 | + # The message is part of the API surface: callers (and |
| 704 | + # automated tooling) rely on it being informative without |
| 705 | + # having to consult the source. |
| 706 | + try: |
| 707 | + _check_SETATTR_TARGET_value("nope") |
| 708 | + except ValueError as exc: |
| 709 | + message = str(exc) |
| 710 | + else: |
| 711 | + self.fail("ValueError not raised") |
| 712 | + # Both accepted values must appear in the message. |
| 713 | + self.assertIn("'module'", message) |
| 714 | + self.assertIn("'proxy'", message) |
| 715 | + # And the offending value must be echoed back so the |
| 716 | + # user can see what they sent. |
| 717 | + self.assertIn("'nope'", message) |
| 718 | + |
| 719 | + def test_value_error_with_none_checks_current(self): |
| 720 | + # If the *current* ``SETATTR_TARGET`` has been corrupted |
| 721 | + # before the test runs, the validator must still catch it. |
| 722 | + core.SETATTR_TARGET = "garbage" |
| 723 | + with self.assertRaises(ValueError): |
| 724 | + _check_SETATTR_TARGET_value(None) |
| 725 | + |
| 726 | + # ------------------------------------------------------------------ |
| 727 | + # Type errors |
| 728 | + # ------------------------------------------------------------------ |
| 729 | + def test_non_string_raises_type_error(self): |
| 730 | + for bad in (0, 1, 1.5, True, None, [], {}, (), b"module", object()): |
| 731 | + with self.subTest(value=bad, type=type(bad).__name__): |
| 732 | + # ``None`` is special-cased (means "check current |
| 733 | + # value") and should NOT raise ``TypeError``. Skip |
| 734 | + # it explicitly here. |
| 735 | + if bad is None: |
| 736 | + continue |
| 737 | + with self.assertRaises(TypeError): |
| 738 | + _check_SETATTR_TARGET_value(bad) |
| 739 | + |
| 740 | + def test_type_error_message_mentions_str(self): |
| 741 | + try: |
| 742 | + _check_SETATTR_TARGET_value(42) |
| 743 | + except TypeError as exc: |
| 744 | + message = str(exc) |
| 745 | + else: |
| 746 | + self.fail("TypeError not raised") |
| 747 | + # The error must point at ``str`` as the expected type so |
| 748 | + # users immediately know what to pass instead. |
| 749 | + self.assertIn("str", message) |
| 750 | + |
| 751 | + def test_subclass_of_str_is_accepted(self): |
| 752 | + # ``str`` subclasses are valid: this is standard Python |
| 753 | + # duck-typing and avoids surprising users who build their |
| 754 | + # own string-like types. |
| 755 | + class MyStr(str): |
| 756 | + pass |
| 757 | + self.assertEqual(_check_SETATTR_TARGET_value(MyStr("module")), "module") |
| 758 | + self.assertEqual(_check_SETATTR_TARGET_value(MyStr("proxy")), "proxy") |
| 759 | + |
| 760 | + # ------------------------------------------------------------------ |
| 761 | + # Integration with the package import-time check |
| 762 | + # ------------------------------------------------------------------ |
| 763 | + def test_package_loaded_successfully(self): |
| 764 | + # ``__init__.py`` calls ``_check_SETATTR_TARGET_value()`` |
| 765 | + # once at import time. The mere fact that we have reached |
| 766 | + # this test class proves the call succeeded; if the |
| 767 | + # default value were invalid the import would have raised |
| 768 | + # before any test could run. |
| 769 | + # |
| 770 | + # We additionally assert the global is still in a valid |
| 771 | + # state after all the previous tests have potentially |
| 772 | + # poked at it. |
| 773 | + _check_SETATTR_TARGET_value() # must not raise |
| 774 | + |
| 775 | + |
629 | 776 | if __name__ == "__main__": |
630 | 777 | unittest.main(verbosity=2) |
0 commit comments