Skip to content

v0.11.0: Operator forwarding, subclassing support, and proxy identity fixes

Latest

Choose a tag to compare

@Borda Borda released this 17 Jul 09:35
· 8 commits to main since this release

pyDeprecate 0.11.0 makes deprecated object proxies act like real drop-in replacements instead of leaky wrappers. Operators and protocol methods — arithmetic, iteration, context managers, async support — now forward to the wrapped object. Proxies survive copy/deepcopy/pickle (they used to blow up with RecursionError), and you can now subclass a deprecated_class alias directly instead of hitting a TypeError. Two identity fixes make isinstance behave the way you'd expect, and validate_deprecation_expiry() now catches expired class members by default. No breaking changes.

✨ Spotlights

Operators and protocol methods now forward to the wrapped object

Arithmetic, comparisons, context managers, iteration, numeric conversion, os.fspath, format, and async protocols all delegate to the wrapped object now instead of raising TypeError.

from deprecate import deprecated_instance

DEFAULTS = deprecated_instance({"lr": 0.001}, deprecated_in="1.2", remove_in="2.0", read_only=True)
print(DEFAULTS["lr"])  # 0.001 — warns once

Subclassing a deprecated class alias now works

class Child(OldName) on a deprecated_class alias used to raise a confusing metaclass error. It now resolves cleanly to the active replacement class.

validate_deprecation_expiry() now scans class members by default

The include_members default flipped from False to True, matching find_deprecation_wrappers(). If your CI expiry check was missing expired methods, constructors, or properties, it'll catch them now — pass include_members=False if you want the old, narrower scope back.

Proxy identity fixes: __class__, isinstance/issubclass

A deprecated proxy's __class__ now reports the wrapped object's real type, so isinstance checks against it work as expected; type(proxy) still tells you it's a proxy. Passing an instance proxy as the second argument to isinstance/issubclass now raises TypeError instead of silently returning False.

Proxies support copy, deepcopy, and pickle

These used to crash with RecursionError. deprecated_instance proxies over plain objects are now fully picklable; deprecated_class alias proxies may still raise PicklingError — use copy.deepcopy for those instead.

🔄 Migration guide

No breaking changes in this release. Nothing was removed or renamed.

Three behavior changes are worth a quick look. None require action for typical use — the last two only matter if your code depends on the specific old behavior:

  • validate_deprecation_expiry() now scans class members by default — pass include_members=False to restore the old scope.
  • A deprecated proxy's __class__ now reports the wrapped type. If you're detecting a proxy by checking obj.__class__ is _DeprecatedProxy, switch to type(obj) is _DeprecatedProxy instead.
  • isinstance/issubclass with an instance proxy as the second argument now raises TypeError instead of silently returning False.

📝 Notable changes

🚀 Added

  • Deprecated proxies forward operators and protocol methods to the wrapped object — arithmetic, comparison/ordering, context managers, iteration, numeric conversion, os.fspath, format, and async protocols now delegate instead of raising TypeError; binary operators still return NotImplemented where Python expects it, and in-place operators (+=) rebind to the plain result. (#214)
  • Subclassing a deprecated class alias now works — the alias resolves to the active class and warns, staying silent when the proxy only remaps attributes or arguments. (#214)

🌱 Changed

  • Call forwarding is about 2.4× faster — no behavior change, just less overhead on every call. (#214)
  • validate_deprecation_expiry() default include_members flipped from False to True — your CI expiry gate now scans class members by default; pass include_members=False to restore the old scope. (#210)
  • A deprecated proxy's __class__ now reports the wrapped object's type — this gives you real isinstance transparency for JSON encoders, validators, functools.singledispatch, and similar; type(proxy) still tells you it's a proxy. (#210)
  • Proxy identity operations (repr, str, ==, hash) now route through the active object — previously they used the deprecated source while attribute/item/call access used the active target, which was inconsistent. (#216)
  • isinstance/issubclass with an instance proxy as the second argument now raise TypeError — previously this returned False silently, which could hide bugs. Class-alias proxies are unaffected. (#216)
  • Batch expiry checks now warn on an unparsable remove_in instead of skipping it silently. (#216)

🔧 Fixed

  • Inspecting a deprecated proxy — hasattr, copy/deepcopy, reading dunder attributes — no longer counts against your warning limit. (#210)
  • Functions with positional-only parameters no longer raise TypeError on every call in notify-only and self-remapping modes (both sync and async). (#210)
  • Extra *args are now forwarded to callable targets instead of silently dropped. (#210)
  • Positional-only forwarding no longer misbinds arguments when an earlier parameter is missing; the same fix applies to deprecated_class(args_mapping=...). (#210)
  • CLI: unknown or misspelled flags now exit non-zero instead of being silently ignored. (#210)
  • CLI: version auto-detection no longer walks up into an unrelated project's pyproject.toml. (#210)
  • Recursive audits (find_deprecation_wrappers(recursive=True)) now survive submodules that raise errors other than ImportError on import. (#210)
  • Deprecated proxies now support copy.copy, copy.deepcopy, and pickle. (#212)
  • The warning count (num_warns) is now tracked safely when multiple threads call a deprecated function for the first time at once. (#214)
  • Forwarding between static methods on different classes no longer raises a spurious TypeError. (#214)
  • Instantiating a deprecated class that only remaps attributes is now silent — only the deprecated-attribute access itself warns. (#214)
  • Audit scans no longer double-count re-exported wrappers. (#215)
  • Audit report formatting no longer triggers chained proxies (which caused spurious warnings, drained the warning budget, and fabricated a module path). (#215)
  • CLI all/status no longer fail on plain directories that lack an __init__.py. (#215)
  • CLI version auto-detection now resolves distributions whose name differs from the import name. (#215)
  • A bare @deprecated (missing parens) now raises a clear TypeError when the first argument isn't callable. (#216)
  • A custom template_mgs with a bare %-conversion is now rejected with ValueError at decoration time instead of failing later. (#216)
  • args_mapping, args_extra, and attrs_mapping are now copied defensively when you apply the decorator, so later mutation of the originals can't leak through. (#216)
  • The cross-class forwarding guard now also catches @property/@classmethod/@staticmethod-decorated methods. (#216)
  • A warning stream that raises TypeError internally is no longer invoked twice. (#216)
  • Argument validation against a **kwargs/*args target gives you the curated error message again. (#216)
  • Audits now surface deprecated private (_-prefixed) and dunder members instead of skipping them. (#216)
  • Audits now detect a proxy pointing at itself and report it as a no-op instead of as an effective deprecation. (#216)
  • Recursive audits now tolerate foreign objects whose attribute access raises something other than AttributeError. (#216)
  • Version strings with PEP 440 local segments (e.g. 1.2.3+cuda) now parse correctly instead of losing that segment. (#216)
  • CLI: no longer crashes when the terminal doesn't report a text encoding. (#216)
  • CLI: exceptions without a message now still exit with a non-blank line on stderr. (#216)

Full changelog: v0.10.1...v0.11.0