pyDeprecate 0.10.1 closes two correctness gaps and ships one new audit diagnostic. Callable deprecation cycles now raise a clear RuntimeError instead of crashing with a RecursionError. The inner_order_property flag in audit results lets CI pipelines detect the silent setter/deleter gap from inner-order @property @deprecated stacking, and the new opt-in from deprecate import property catches that same mistake at class-body evaluation time — before any instance is created.
No migration required. All changes are additive.
✨ Spotlights
Circular deprecation chains raise RuntimeError
A target= chain that loops back (A → B → A) previously caused unbounded recursion. The decorator now detects re-entry via a ContextVar guard and raises a named error immediately. Async tasks each get an independent detection set — no false positives from concurrent coroutines.
@deprecated(deprecated_in="1.0", remove_in="2.0", target=lambda: cycle_b())
def cycle_a(): ...
@deprecated(deprecated_in="1.0", remove_in="2.0", target=cycle_a)
def cycle_b(): ...
cycle_a()
# RuntimeError: Circular deprecation cycle detected: `cycle_a` re-entered via its own target chain.inner_order_property audit flag
find_deprecation_wrappers() now sets inner_order_property=True when a plain property has a @deprecated-wrapped fget — the inner-order shape where only the getter warns; setters and deleters added via @value.setter remain silently unprotected.
results = find_deprecation_wrappers(my_module)
flagged = [r for r in results if r.inner_order_property]
# Add to CI: assert not flagged, "Inner-order @property @deprecated found"Opt-in strict property replacement
from deprecate import property shadows the builtin with _StrictProperty, which raises TypeError at class-body time when handed an already-@deprecated getter — turning the silent footgun into a loud, immediate failure.
from deprecate import deprecated
from deprecate import property # opt-in strict mode for this module
class MyModel:
@property # TypeError raised here —
@deprecated(deprecated_in="1.0", remove_in="2.0") # before any instance is created
def old_value(self): ...Star imports (from deprecate import *) are unaffected — strictness is per-module opt-in only.
📝 Notable changes
🚀 Added
DeprecationWrapperInfo.inner_order_propertyflag —find_deprecation_wrappers()setsTruefor inner-order@property @deprecatedwhere onlyfgetwarns; CI pipelines can filter on this field. (#201)- Opt-in strict
propertyviafrom deprecate import property— raisesTypeErrorat class-definition time when a getter already carries@deprecatedmetadata; star imports unaffected. (#201)
🔧 Fixed
- Circular deprecation chains now raise
RuntimeErrorat call time —ContextVarre-entrancy guard replaces unbounded recursion; async cycle detection avoids false positives from concurrent tasks. (#200) _StrictPropertyTypeErrormessage references correct module path — error now points todeprecate.deprecation._StrictProperty. (#201)
🏆 Contributors
- Jirka Borovec (@Borda) — cycle detection implementation, inner-order audit flag, strict property enforcement
Full changelog: v0.10.0...v0.10.1