|
15 | 15 | import tests.collection_chains as chain_module |
16 | 16 | import tests.collection_deprecate as proxy_module |
17 | 17 | import tests.collection_misconfigured as sample_module |
18 | | -from deprecate import deprecated, validate_deprecation_expiry |
| 18 | +from deprecate import ( |
| 19 | + DeprecatedCallableInfo, |
| 20 | + deprecated, |
| 21 | + find_deprecated_callables, |
| 22 | + validate_deprecated_callable, |
| 23 | + validate_deprecation_expiry, |
| 24 | +) |
19 | 25 | from deprecate._types import DeprecationConfig |
20 | 26 | from deprecate.audit import ( |
21 | 27 | ChainType, |
@@ -443,6 +449,24 @@ def test_detects_chain_with_composed_arg_mappings(self, chain_issues: list) -> N |
443 | 449 | assert info.chain_type is ChainType.TARGET |
444 | 450 | assert info.deprecated_info.args_mapping == {"predictions": "preds", "labels": "truth"} |
445 | 451 |
|
| 452 | + def test_detects_three_hop_target_chain(self, chain_issues: list) -> None: |
| 453 | + """Chain depth > 2 (A → B → C → final, all deprecated) is detected at the outermost hop. |
| 454 | +
|
| 455 | + The detection logic inspects only the immediate target (it does not recursively walk the |
| 456 | + chain), so ``caller_three_hop_sum`` reports ``ChainType.TARGET`` because its target |
| 457 | + ``caller_sum_via_depr_sum`` is itself deprecated. Each intermediate hop is reported |
| 458 | + independently when scanned — this test pins the outermost hop only, validating that |
| 459 | + depth ≥ 3 does not break chain detection on the leading wrapper. |
| 460 | +
|
| 461 | + """ |
| 462 | + by_name = {i.function: i for i in chain_issues} |
| 463 | + assert "caller_three_hop_sum" in by_name, "Three-hop chain fixture must be discovered by the audit" |
| 464 | + assert by_name["caller_three_hop_sum"].chain_type is ChainType.TARGET |
| 465 | + # Every intermediate hop is independently a deprecation chain — the audit reports |
| 466 | + # all three (outer, middle, and inner-via-decorated_sum already covered) when |
| 467 | + # scanning the module. |
| 468 | + assert "caller_sum_via_depr_sum" in by_name, "Middle hop must also be flagged as TARGET chain" |
| 469 | + |
446 | 470 | @pytest.mark.parametrize( |
447 | 471 | "fn_pattern", |
448 | 472 | [ |
@@ -844,3 +868,79 @@ def test_return_format(self) -> None: |
844 | 868 | assert all(isinstance(msg, str) for msg in expired) |
845 | 869 | for msg in expired: |
846 | 870 | assert "Callable" in msg or "scheduled" in msg |
| 871 | + |
| 872 | + |
| 873 | +class TestBackwardCompatShims: |
| 874 | + """Coverage for the three v0.6-era public aliases preserved in :mod:`deprecate.audit`. |
| 875 | +
|
| 876 | + The three shims are themselves wrapped with :func:`~deprecate.deprecation.deprecated` / |
| 877 | + :func:`~deprecate.proxy.deprecated_class` (``deprecated_in="0.6"``, ``remove_in="1.0"``), |
| 878 | + so calling them must (a) emit a :class:`FutureWarning` (the project default warning |
| 879 | + category) and (b) forward to the new-name implementation transparently. |
| 880 | +
|
| 881 | + Only the shim's own deprecation warning is asserted. The underlying audit helpers |
| 882 | + (:func:`~deprecate.audit.validate_deprecation_wrapper`, :func:`~deprecate.audit.find_deprecation_wrappers`) |
| 883 | + read ``__deprecated__`` metadata without invoking the deprecated callable, so no additional |
| 884 | + warning originates from the fixture functions themselves. |
| 885 | +
|
| 886 | + """ |
| 887 | + |
| 888 | + def test_validate_deprecated_callable_emits_warning_and_forwards(self) -> None: |
| 889 | + """``validate_deprecated_callable`` warns and returns the same result as the new name.""" |
| 890 | + with warnings.catch_warnings(record=True) as recorded: |
| 891 | + warnings.simplefilter("always") |
| 892 | + result_old = validate_deprecated_callable(proxy_module.decorated_pow_self) |
| 893 | + |
| 894 | + # The shim's own deprecation warning must appear in the record; identify it by message. |
| 895 | + shim_warns = [w for w in recorded if "validate_deprecated_callable" in str(w.message)] |
| 896 | + assert shim_warns, "Calling the shim must emit a FutureWarning about its own deprecation" |
| 897 | + assert shim_warns[0].category is FutureWarning |
| 898 | + |
| 899 | + # Result must equal the underlying new-name implementation called on the same input. |
| 900 | + result_new = validate_deprecation_wrapper(proxy_module.decorated_pow_self) |
| 901 | + assert result_old == result_new |
| 902 | + |
| 903 | + def test_find_deprecated_callables_emits_warning_and_forwards(self) -> None: |
| 904 | + """``find_deprecated_callables`` warns and returns the same list as ``find_deprecation_wrappers``.""" |
| 905 | + with warnings.catch_warnings(record=True) as recorded: |
| 906 | + warnings.simplefilter("always") |
| 907 | + result_old = find_deprecated_callables(proxy_module, recursive=False) |
| 908 | + |
| 909 | + shim_warns = [w for w in recorded if "find_deprecated_callables" in str(w.message)] |
| 910 | + assert shim_warns, "Calling the shim must emit a FutureWarning about its own deprecation" |
| 911 | + assert shim_warns[0].category is FutureWarning |
| 912 | + |
| 913 | + result_new = find_deprecation_wrappers(proxy_module, recursive=False) |
| 914 | + assert {r.function for r in result_old} == {r.function for r in result_new} |
| 915 | + |
| 916 | + def test_deprecated_callable_info_alias_resolves(self) -> None: |
| 917 | + """``DeprecatedCallableInfo`` is a :func:`deprecated_class` proxy aliasing ``DeprecationWrapperInfo``. |
| 918 | +
|
| 919 | + A real :class:`DeprecationWrapperInfo` instance (obtained from |
| 920 | + :func:`find_deprecation_wrappers`) must satisfy ``isinstance(_, DeprecatedCallableInfo)`` — |
| 921 | + the proxy implements ``__instancecheck__`` to delegate to the target class. |
| 922 | +
|
| 923 | + """ |
| 924 | + results = find_deprecation_wrappers(proxy_module, recursive=False) |
| 925 | + assert results, "Fixture module must contain at least one deprecated wrapper" |
| 926 | + # Filter to deterministic instance — first item from a non-empty list. |
| 927 | + info = results[0] |
| 928 | + assert isinstance(info, DeprecationWrapperInfo) |
| 929 | + assert isinstance(info, DeprecatedCallableInfo) |
| 930 | + |
| 931 | + def test_deprecated_callable_info_instantiation_warns(self) -> None: |
| 932 | + """Calling ``DeprecatedCallableInfo(...)`` directly emits a ``FutureWarning`` about its own deprecation.""" |
| 933 | + # pytest introspection (inspect.hasattr '__wrapped__') may consume num_warns=1 before this |
| 934 | + # test body runs; reset the proxy's warn counter so the call below always fires the warning. |
| 935 | + DeprecatedCallableInfo._cfg.warned = 0 # type: ignore[attr-defined] |
| 936 | + with warnings.catch_warnings(record=True) as recorded: |
| 937 | + warnings.simplefilter("always") |
| 938 | + info = DeprecatedCallableInfo( # type: ignore[call-arg] |
| 939 | + module="tests", |
| 940 | + function="f", |
| 941 | + deprecated_info=DeprecationConfig(deprecated_in="1.0", remove_in="2.0"), |
| 942 | + ) |
| 943 | + shim_warns = [w for w in recorded if "DeprecatedCallableInfo" in str(w.message)] |
| 944 | + assert shim_warns, "Calling DeprecatedCallableInfo(...) must emit FutureWarning about its own deprecation" |
| 945 | + assert shim_warns[0].category is FutureWarning |
| 946 | + assert isinstance(info, DeprecationWrapperInfo) |
0 commit comments