Releases: Borda/pyDeprecate
Release list
v0.10.1: Cycle detection, strict property, & inner-order audit flag
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
v0.10.0: Property accessors & class attribute mapping
v0.10.0 brings fine-grained attribute-level deprecation to deprecated_class via a new attrs_mapping parameter — deprecate individual attribute names (reads, writes, deletes) with per-attribute warning budgets and, for dataclasses, automatic constructor-kwarg expansion. The release also closes two coverage gaps: @deprecated @property now wraps the setter and deleter in addition to the getter, and raw staticmethod/classmethod descriptors are accepted as target without the .__func__ workaround. A correctness fix ensures calls to targets with positional-only parameters no longer raise TypeError.
✨ Spotlights / highlights
1. deprecated_class(attrs_mapping={...}) — selective attribute deprecation
Deprecate individual attribute names with per-attribute budgets. Reads, writes, and deletes each fire FutureWarning independently. None as the redirect value means warn-only (no rename). TargetMode.ATTRS_REMAP is the corresponding mode. (#191)
from deprecate import deprecated_class
@deprecated_class(attrs_mapping={"color": "colour"}, deprecated_in="2.0", remove_in="3.0")
class Palette:
colour: str = "red" # canonical name
size: int = 10 # unlisted — always silent
print(Palette.color) # warns: FutureWarning → redirected to colour
print(Palette.colour) # silent2. Dataclass attrs_mapping auto-expand
When the wrapped class is a @dataclass, one attrs_mapping entry automatically covers both attribute access and constructor kwargs — no separate args_mapping needed.
from dataclasses import dataclass
from deprecate import deprecated_class
@deprecated_class(attrs_mapping={"px": "x", "py": "y"}, deprecated_in="2.0", remove_in="3.0")
@dataclass
class OldPoint:
x: float = 0.0
y: float = 0.0
pt = OldPoint(px=1.0, py=2.0) # warns on px and py → x=1.0, y=2.0Explicit args_mapping entries always win over auto-expanded ones. (#193)
3. @deprecated @property wraps fset and fdel
Outer-order @deprecated @property now wraps all three accessors. Previously only fget fired a warning. Chain-style @value.setter / @value.deleter re-wraps automatically via the new _DeprecatedProperty subclass.
from deprecate import deprecated
class Config:
def __init__(self) -> None:
self._timeout: int = 30
@deprecated(deprecated_in="1.0", remove_in="2.0")
@property
def timeout(self) -> int:
return self._timeout
@timeout.setter
def timeout(self, value: int) -> None:
self._timeout = value
@timeout.deleter
def timeout(self) -> None:
del self._timeout
cfg = Config()
_ = cfg.timeout # warns: FutureWarning (read)
cfg.timeout = 60 # warns: FutureWarning (write)
del cfg.timeout # warns: FutureWarning (delete)Inner order (@property @deprecated) still wraps fget only. (#190)
4. Raw staticmethod/classmethod descriptors as target
Inside a class body, pass the new method directly as target=new_method — no .__func__ needed. (#192)
from deprecate import deprecated, void
class Compute:
@staticmethod
def area(radius: float) -> float:
return 3.14159 * radius**2
@staticmethod
@deprecated(target=area, deprecated_in="1.0", remove_in="2.0")
def surface(radius: float) -> float:
"""Deprecated — use area() instead."""
return void(radius)
print(Compute.surface(3.0)) # warns: FutureWarning, returns area(3.0)5. Fix: @deprecated forwards calls to POSITIONAL_ONLY targets
Previously raised TypeError at call time when target declared positional-only parameters (def fn(x, /)). Now detects at decoration time, emits UserWarning, and splits call dispatch positionally. (#194)
from deprecate import deprecated
def new_fn(value: float, /) -> float:
return value * 2
@deprecated(target=new_fn, deprecated_in="1.0", remove_in="2.0")
def old_fn(value: float) -> float: ...
result = old_fn(value=5.0) # warns: FutureWarning; calls new_fn(5.0) correctly
print(result) # 10.0⬆️ Upgrade notes
No breaking changes. All existing code continues to work.
If you use @deprecated @property with a setter or deleter and run filterwarnings=error::FutureWarning: setter and deleter writes now correctly fire FutureWarning — they were silently skipped before (bug). Tests that write to or delete a deprecated property will now raise as expected. To intentionally keep only the getter warned, switch to inner order (@property @deprecated).
📋 Notable changes
🚀 Added
deprecated_class(attrs_mapping={...})for selective attribute deprecation. Deprecated attribute names emitFutureWarningon read, write, and delete with per-attribute warning budgets.Noneas the redirect value means warn-only.TargetMode.ATTRS_REMAPis the corresponding mode. Multi-hop chains allowed; cycles raiseValueErrorat decoration time. (#191)deprecated_classstacking now supported. Two@deprecated_classdecorators on the same class work correctly —isinstance()delegates through the chain, instantiation emits at most one warning. (#193)- Dataclass
attrs_mappingauto-expand. Singleattrs_mappingentry covers both attribute access and constructor kwargs on a@dataclass. Explicitargs_mappingalways wins. (#193) validate_mapping_compatibility()audit function. Returns alldeprecated_classproxies whoseargs_mappingremaps toPOSITIONAL_ONLYconstructor parameters. Use in CI alongsidevalidate_deprecation_expiry. (#193)@deprecated @propertywrapsfsetandfdel. Outer-order decoration now covers all three accessors via the new_DeprecatedPropertysubclass. (#190)- Raw
staticmethod/classmethoddescriptors accepted astarget._normalize_targetunwraps automatically inside class bodies. (#192)
🔧 Fixed
@deprecatedcorrectly forwards calls to targets withPOSITIONAL_ONLYparameters. Detects at decoration time, emitsUserWarning, splits call dispatch positionally.args_mappingapplied before split. (#194)args_mappingprecedence: explicit new-name always wins. When caller passes both deprecated old name and new name simultaneously, new-name value wins regardless of call-site order. (#198)
🏆 Contributors
Full changelog: v0.9.0...v0.10.0
v0.9.0: Generators, async & audit status tables
🎉 pyDeprecate 0.9.0
pyDeprecate 0.9.0 adds async support, fixes descriptor ordering, and introduces Markdown audit tables — @deprecated now wraps async def coroutines, async generators, and sync generators correctly, decorator order on @classmethod and friends no longer matters, and pydeprecate status renders Markdown audit reports you can commit to your docs.
📋 Summary
v0.9.0 closes three long-standing gaps in @deprecated. Sync generators, async coroutines, and async generators are all wrapped correctly — sync generators fire the warning eagerly at call time instead of after the first next(), inspect.iscoroutinefunction(wrapper) returns True for async coroutines, and async generators no longer raise a UserWarning at decoration time.
Descriptor decorator order is no longer a footgun. Both @classmethod @deprecated and @deprecated @classmethod (and the equivalent for @staticmethod) produce a working classmethod(deprecated_wrapper) — the descriptor is unwrapped, the inner function is deprecated, and the result is re-wrapped. find_deprecation_wrappers() looks inside descriptor members too, so audit scans no longer miss wrapped classmethods or staticmethods.
A new generate_deprecation_table() function and pydeprecate status CLI subcommand render Markdown reports grouped by module and API type (function, method, classmethod, staticmethod, property, class, instance) — with compact and matrix styles, lifecycle classification via the new DeprecationStatus enum, and --output FILE for committable artifacts.
One breaking CLI change: --skip_errors was renamed to --exit-zero across all four subcommands. The flag was introduced in v0.8.0 and renamed because the old name reads as if it swallows exceptions; the new name matches ruff, pylint, and shellcheck convention and describes the behavior plainly. Update CI scripts before upgrading. v0.8.0's target=None / target=True / DeprecationWrapperInfo field deprecations are still active and are scheduled for removal in v1.0.
🚀 Spotlights
Generator function support for @deprecated
Decorating a generator function (def fn(): yield ...) now emits the deprecation warning eagerly at call time — before the first next() — consistent with regular function behavior. The generator body executes lazily as normal when iterated. All three TargetMode variants work transparently; no isgeneratorfunction check is needed at the call site.
from deprecate import deprecated
@deprecated(deprecated_in="0.9", remove_in="1.0")
def stream_legacy_rows():
for row in _legacy_source():
yield row
it = stream_legacy_rows() # FutureWarning fires eagerly here
for row in it: # iteration runs lazily, unchanged
process(row)async def coroutine and async-generator support
@deprecated now wraps async def coroutines and async def + yield async generators with no changes needed at the call site for direct use. The async-coroutine wrapper is itself async def, so inspect.iscoroutinefunction(wrapper) returns True and the warning fires when the coroutine is awaited. For async generators, the wrapper is a sync callable that fires the warning eagerly at call time and returns the async-generator object — iterate with async for. Note: inspect.isasyncgenfunction(wrapper) returns False for the async-generator case (the wrapper is sync), so frameworks that branch on that flag to decide how to call the function may need a thin async def passthrough.
import inspect
from deprecate import deprecated
@deprecated(deprecated_in="0.9", remove_in="1.0")
async def fetch_legacy(url: str) -> bytes:
return await _http_get(url)
assert inspect.iscoroutinefunction(fetch_legacy) # True
# FutureWarning fires here, at await time:
data = await fetch_legacy("https://example.com")Order-agnostic @classmethod / @staticmethod
Both @classmethod @deprecated and @deprecated @classmethod (and the equivalent for @staticmethod) produce a working classmethod(deprecated_wrapper). The descriptor is unwrapped at decoration time, the inner function is deprecated, and the result is re-wrapped. FutureWarning fires at call time in either order; no decoration-time UserWarning is emitted. find_deprecation_wrappers() now inspects descriptor members so wrapped classmethods and staticmethods show up in audit scans.
from deprecate import deprecated
class Repo:
# preferred order
@classmethod
@deprecated(target=new_load, deprecated_in="0.9", remove_in="1.0")
def load(cls, path): ...
# also works in v0.9.0
@deprecated(target=new_load, deprecated_in="0.9", remove_in="1.0")
@classmethod
def load_alt(cls, path): ...Markdown deprecation tables — generate_deprecation_table() + pydeprecate status
Two new public enums power the reports: DeprecationStatus (lifecycle classification — ACTIVE, EXPIRED, plus dev/alpha/beta/rc removal windows) and TableStyle (COMPACT / MATRIX). Tables are grouped by module and API type, auto-detect the package version from pyproject.toml or installed metadata, and write to stdout or a file via --output. pydeprecate all now includes the table automatically.
pydeprecate status src/mypackage --style compact --output DEPRECATIONS.mdSample output (COMPACT style):
| Original API | API Type | New API | Deprecated | Remove | Current Status |
|---|---|---|---|---|---|
mypackage.load |
callable | mypackage.load_v2 |
v1.0 | v2.0 | 📢 Deprecation Active |
mypackage.Client.connect |
classmethod | mypackage.Client.open |
v1.1 | v1.4 | ⏰ Removal Imminent |
mypackage.to_dict |
callable | — | v0.9 | v1.3 | 💥 Past Removal Date |
⚠️ Migration guide
One breaking change: the CLI flag --skip_errors was renamed to --exit-zero across all four subcommands (check, expiry, chains, all). Update any CI scripts before upgrading.
# before — fails on v0.9.0
pydeprecate check src/mypackage --skip_errors
# after
pydeprecate check src/mypackage --exit-zerov0.8.0 deprecations (target=None, target=True, DeprecationWrapperInfo field renames) are still active and will be removed in v1.0. See MIGRATION.md for full before/after examples.
📝 Notable changes
🚀 Added
- Generator function support for
@deprecated. Warning emitted eagerly at call time, before the firstnext(). All threeTargetModevariants work transparently. (#176) async defcoroutine wrapper support for@deprecated. Wrapper isasync def,inspect.iscoroutinefunction(wrapper)returnsTrue, warning fires when the coroutine is awaited. All threeTargetModevariants work with async sources and async targets. (#180)- Async generator function support for
@deprecated. No more decoration-timeUserWarning; wrapper is a sync callable that fires the warning eagerly and returns an async-generator object. All threeTargetModevariants work. (#181) - Order-agnostic
@classmethod/@staticmethod. Both orders produceclassmethod(deprecated_wrapper); decorator order no longer matters. (#178) - Stacked
@deprecated—ARGS_REMAP + NOTIFYcombination. Rename arguments first, deprecate the whole function later. Six other stacking shapes emitUserWarningat decoration time and will becomeTypeErrorin v1.0. (#172) - Markdown deprecation tables —
generate_deprecation_table()+pydeprecate statusCLI subcommand. NewDeprecationStatusandTableStylepublic enums. New--styleand--outputCLI flags. Integrated intopydeprecate all. Auto-detects package version frompyproject.tomlor installed metadata. (#133) ChainTypeenum now exported as public API. Previously returned byvalidate_deprecation_chains(); now listed indeprecate.__all__. (#133)- Audit discovery extended to class descriptors.
find_deprecation_wrappers()now inspectsclassmethodandstaticmethoddescriptors on class members. (#178)
⚠️ Breaking Changes
- CLI flag renamed:
--skip_errors→--exit-zeroacross all four subcommands (check,expiry,chains,all). No transitional release — flag was new in v0.8.0. Canonical spelling is--exit-zero;--exit_zeroalso accepted by the CLI framework. (#187) - Programmatic
cmd_check/cmd_expiry/cmd_chains/cmd_allkeyword renamed fromskip_errorstoexit_zero. (#187)
🌱 Changed
- Misconfigured stacking combinations warn at decoration time. Six previously-undefined
@deprecatedstacking shapes now emitUserWarningat decoration time naming the specific shape. Scheduled to becomeTypeErrorin v1.0. (#172) - Audit chain classification —
ARGS_REMAP + NOTIFYis now classified asSTACKEDrather thanTARGET. Fixes audit reports for the supported stacking shape. (#172) - CLI warning suppression narrowed to
deprecate.*warnings only. Third-party warnings emitted during a scan are no longer silenced. (#133) generate_deprecation_table()gainsinclude_membersparameter for scanning descriptor members;validate_deprecation_expiry()default unchanged atinclude_members=Falseto preserve existing scan scope. (#133)
🔧 Fixed
stacklevelattribution on Python 3.12+.inspect.signature(warnings.warn)raisesValueErroron 3.12+ (the C builtin lacks an introspectable signature), whi...
v0.8.0: Default TargetMode enum & CLI audit tools
🎉 pyDeprecate 0.8.0
pyDeprecate 0.8.0 is the TargetMode enum & CLI tooling release — deprecation intent is now typed and explicit, warn-only deprecation is the default, and a new pydeprecate CLI lets you scan any package for misconfigured wrappers without writing a single audit script.
Summary
v0.8.0 centers on TargetMode: a proper enum (TargetMode.NOTIFY, TargetMode.ARGS_REMAP) replacing the target=None / target=True boolean sentinels. The old sentinels still work — they emit FutureWarning at decoration time — so you've got a full release cycle to migrate before they're removed in v1.0.
target now defaults to TargetMode.NOTIFY, so the most common pattern — warn callers and run the original body unchanged — needs nothing more than deprecated_in and remove_in:
@deprecated(deprecated_in="1.0", remove_in="2.0")
def old_fn(): ...A new pydeprecate CLI (four subcommands) rounds out the release. Run pydeprecate check path/to/mypackage to surface misconfigured wrappers across a whole codebase in seconds.
🚀 Spotlights
TargetMode enum
from deprecate import deprecated, TargetMode
# Warn-only: source body executes unchanged
@deprecated(target=TargetMode.NOTIFY, deprecated_in="0.8", remove_in="1.0")
def old_api(): ...
# Argument-rename: warns only when old arg name is passed
@deprecated(
target=TargetMode.ARGS_REMAP,
deprecated_in="0.8",
remove_in="1.0",
args_mapping={"old_param": "new_param"},
)
def new_api(new_param): ...TargetMode is exported from deprecate and works everywhere target= is accepted: @deprecated, deprecated_class(), and deprecated_instance().
Zero-boilerplate warn-only deprecation
target is now optional — TargetMode.NOTIFY is the default. Drop the target= entirely:
@deprecated(deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...Omitting deprecated_in now surfaces a UserWarning at decoration time, not silently at call time, so misconfiguration is visible immediately.
pydeprecate CLI
pydeprecate check path/to/mypackage # validate wrapper configuration
pydeprecate expiry path/to/mypackage # find wrappers past remove_in date
pydeprecate chains path/to/mypackage # detect deprecated→deprecated chains
pydeprecate all path/to/mypackage # run all three in one passAlso available as python -m deprecate. Requires pip install 'pyDeprecate[cli]' for all subcommands (fire dependency). expiry additionally needs pip install 'pyDeprecate[audit]'. Or install both: pip install 'pyDeprecate[cli,audit]'.
Migration guide
No breaking changes. All v0.7.x code continues to run. The items below emit deprecation warnings now and will be removed in v1.0.
target=None → TargetMode.NOTIFY
# before — emits FutureWarning at decoration time
@deprecated(target=None, deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...
# after — explicit or implicit (default)
@deprecated(target=TargetMode.NOTIFY, deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...
# simplest form — NOTIFY is the default
@deprecated(deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...target=True → TargetMode.ARGS_REMAP
# before — emits FutureWarning at decoration time
@deprecated(target=True, deprecated_in="0.8", remove_in="1.0", args_mapping={"old_arg": "new_arg"})
def new_fn(new_arg): ...
# after
@deprecated(target=TargetMode.ARGS_REMAP, deprecated_in="0.8", remove_in="1.0", args_mapping={"old_arg": "new_arg"})
def new_fn(new_arg): ...DeprecationWrapperInfo field renames
| Old name | New name | Removed in |
|---|---|---|
empty_mapping |
empty_args_mapping |
v1.0 |
identity_mapping |
identity_args_mapping |
v1.0 |
# before
if info.empty_mapping or info.identity_mapping:
...
# after
if info.empty_args_mapping or info.identity_args_mapping:
...DeprecationConfig.target no longer stores raw sentinels
Code that inspects wrapper.__deprecated__.target and compares against None or True must update:
# before
assert fn.__deprecated__.target is None
# after
from deprecate import TargetMode
assert fn.__deprecated__.target is TargetMode.NOTIFYNotable changes
Added
TargetModeenum (NOTIFY,ARGS_REMAP) exported fromdeprecate— typed replacement fortarget=None/target=Truesentinels. (#150)targetdefaults toTargetMode.NOTIFYon@deprecated— warn-only deprecation now requires onlydeprecated_inandremove_in. (#162)pydeprecateCLI —check,expiry,chains,allsubcommands.template_mgsandargs_extraondeprecated_class()anddeprecated_instance()— proxy factories now at full parity with@deprecated. (#150)DeprecationWrapperInfo.empty_deprecated_in—Truewhendeprecated_inis absent; for CI pipeline use. (#166)DeprecationConfig.misconfigured—Truewhen an invalid raw target sentinel (False) was passed at decoration time; surfaced viaDeprecationWrapperInfo.misconfigured_target. (#150)num_warns=0documented — equivalent tostream=None; suppresses all warnings. (#150)- Stacked-callable-target guard —
@deprecated(target=callable_a)stacked over another callable-target wrapper now emitsUserWarningat decoration time instead of crashing withTypeErrorat call time. (#169) template_mgsvalidated at decoration time — malformed%-style placeholders raiseValueErrorimmediately. (#169)
Changed
DeprecationConfig.targetnormalized at decoration time — storesTargetModeorCallable, never rawNone/True/False. (#150)- Misconfigured
TargetModecombos warn at construction time —ARGS_REMAPwithoutargs_mapping,NOTIFYwithargs_mappingorargs_extraall emitUserWarningimmediately. (#150) - Docs site URL layout versioned — content now at
https://borda.github.io/pyDeprecate/stable/(and/<tag>/); the bare root redirects tostable/. Existing bookmarks to flat paths will break on first deploy. (#148) - CLI chains reporting —
checksubcommand reports chains as warnings;chains/allsubcommands report chains as errors. (#149)
Deprecated
target=None— useTargetMode.NOTIFY. EmitsFutureWarning. Removed in v1.0. (#150)target=True— useTargetMode.ARGS_REMAP. EmitsFutureWarning. Removed in v1.0. (#150)target=False— never valid; now emitsUserWarning, treated asTargetMode.NOTIFY. RaisesTypeErrorin v1.0. (#150)DeprecationWrapperInfo.empty_mapping→empty_args_mapping. EmitsDeprecationWarning. Removed in v1.0. (#166)DeprecationWrapperInfo.identity_mapping→identity_args_mapping. EmitsDeprecationWarning. Removed in v1.0. (#166)
Fixed
- PEP 702 stacking crash —
@deprecatedstacked under@typing.deprecatedno longer raisesAttributeErroron__deprecated__lookup. (#169) - Double
FutureWarningondeprecated_class()in NOTIFY mode. (#162) - Cross-class guard false positives — metaclass/dynamic-class qualnames and pre-applied decorators that rewrite
__qualname__no longer trigger spuriousTypeErrorat decoration time; the guard still raises for genuine cross-class forwarding. (#169) args_mappingrename no longer clobbers source default when both old and new parameter names are supplied simultaneously. (#150)
🏆 Contributors
- Onuralp SEZER (@onuralpszr) — initial CLI scaffolding (#76)
- Jiri Borovec (@Borda) —
TargetModeenum, CLI subcommands, proxy parity, cross-class guard hardening, docs site restructure
Full changelog: v0.7.0...v0.8.0
v0.7.0: Extended Docstring Tooling
🎉 pyDeprecate 0.7.0
pyDeprecate 0.7.0 is the docstring tooling release — deprecation notices now flow directly into rendered documentation. update_docstring=True is smarter (section-aware, per-argument) and MkDocs admonitions are supported alongside Sphinx. Two optional extensions wire up MKdocs and Sphinx autodoc so they pick up the injected notices automatically.
🚀 Added
MkDocs / Markdown Admonition Output.
@deprecated now accepts docstring_style="mkdocs" (alias: "markdown"). When update_docstring=True, the deprecation notice is injected as a !!! warning admonition instead of a Sphinx .. deprecated:: directive. Use docstring_style="auto" to detect the style automatically from existing docstring content. (#134)
from deprecate import deprecated
def new_load(path: str) -> dict: ...
@deprecated(target=new_load, deprecated_in="0.7", remove_in="1.0", update_docstring=True, docstring_style="mkdocs")
def load_config(path: str) -> dict:
"""Load the configuration file.
Args:
path: Path to the YAML config.
"""
# Resulting __doc__:
# !!! warning "Deprecated in 0.7"
# Will be removed in v1.0. Use `new_load` instead.
#
# Load the configuration file.
# ...Section-Aware Google / NumPy Docstring Injection.
update_docstring=True now detects Google-style (Args:, Returns:, …) and NumPy-style (Parameters, Returns + underline) section headers and inserts the deprecation notice before the first section rather than appending it at the end. (#134)
@deprecated(target=new_fn, deprecated_in="0.7", remove_in="1.0", update_docstring=True)
def old_fn(x: int) -> int:
"""Compute the result.
Args: ← notice injected above this line
x: Input value.
"""Inline Argument Deprecation in Docstrings.
When args_mapping is set and update_docstring=True, each renamed or removed argument is annotated directly in the Args: / :param section of the docstring rather than appending a general notice at the end. (#136)
@deprecated(
target=new_fn, deprecated_in="0.7", remove_in="1.0", args_mapping={"old_name": "new_name"}, update_docstring=True
)
def old_fn(old_name: str) -> str:
"""Process a value.
Args:
old_name: The value to process.
Deprecated since v0.7 — use `new_name` instead. Will be removed in v1.0.
"""Griffe Extension for mkdocstrings (beta).
Ensures that mkdocstrings renders the live injected docstring rather than the original source text. (#134)
# mkdocs.yml
plugins:
- mkdocstrings:
handlers:
python:
extensions:
- deprecate.docstring.griffe_ext:RuntimeDocstringsSphinx Autodoc Extension for Deprecated Classes (beta).
Ensures that Sphinx autodoc renders deprecated class aliases with the correct members, signature, and injected .. deprecated:: notice. (#134)
# conf.py
extensions = [
"sphinx.ext.autodoc",
"deprecate.docstring.sphinx_ext",
]Live Demo Documentation.
The project now ships live demo documentation published to GitHub Pages — an MkDocs demo, a Sphinx demo, and a portal landing page linking to both. (#134, #137)
📦 Installation
pip install --upgrade pyDeprecate🙏 Thank You
A big thank you to everyone who helped with this release:
- Jirka Borovec (@Borda) — architecture, review, and release management
Automated contributions: @copilot-swe-agent[bot], @pre-commit-ci[bot], @github-advanced-security[bot], @Copilot
Full Changelog: v0.6.0.post0...v0.7.0
v0.6.0: Deprecation Proxy for class/instances
🎉 pyDeprecate 0.6.0
pyDeprecate 0.6.0.post0 is the full proxy release — deprecated_class() and deprecated_instance() now cover the complete Python type hierarchy (Enum, dataclass, built-in types), isinstance()/issubclass() semantics work correctly on proxied classes, and the audit API is cleaner and more consistent.
🚀 Added
Deprecation Proxy for Enum, Dataclass, and Built-in Types.
deprecated_class() and deprecated_instance() now support the full Python type hierarchy via _DeprecatedProxy. Attribute access, method calls, and class behaviour all forward transparently to the underlying type.
from enum import Enum
from deprecate import deprecated_class
# NEW/FUTURE API
class Color(Enum):
RED = 1
BLUE = 2
# DEPRECATED API — transparent proxy, no class body to duplicate
OldColor = deprecated_class(Color, deprecated_in="0.6", remove_in="1.0")
OldColor.RED # FutureWarning: OldColor is deprecated, use Color
OldColor.RED.value # 1Also works for dataclass and plain built-in types. (#114)
Correct isinstance() / issubclass() Semantics on Proxies.
A deprecated_class() proxy can be used as the second argument to isinstance() or issubclass() and works exactly as expected. Without __instancecheck__/__subclasscheck__, using a proxy object (not a real type) as the right-hand argument raises TypeError. (#126)
assert isinstance(OldColor.RED, OldColor) # True
assert issubclass(Color, OldColor) # TrueNeither check emits a deprecation warning — type checks are structural and do not consume the warning budget.
🌱 Changed
@deprecated on a Class Now Delegates to deprecated_class().
Applying @deprecated directly to a class continues to work but now emits a UserWarning at decoration time and routes to deprecated_class() under the hood. This brings two concrete improvements over the v0.5.0 behaviour:
Warning-only (target=None) — now signals migration path:
from enum import Enum
from deprecate import deprecated
@deprecated(target=None, deprecated_in="0.5", remove_in="1.0")
class LMM(Enum):
FLORENCE_2 = "florence_2"
GPT_4O = "gpt_4o"Code like this continues to run. A UserWarning is now emitted at decoration time pointing to deprecated_class() as the dedicated API. (#132)
Forwarding (target=SomeClass) — fixes silent isinstance breakage:
In v0.5.0, @deprecated(NewColor, ...) class OldColor(Enum) used an internal _DeprecatedEnumWrapper that silently broke isinstance() checks — isinstance(x, OldColor) would raise TypeError or return the wrong result. The delegation to deprecated_class() replaces this with _DeprecatedProxy, which preserves isinstance/issubclass semantics correctly. (#132, #120)
# Recommended — use deprecated_class() directly for the cleanest result:
from deprecate import deprecated_class
OldColor = deprecated_class(Color, deprecated_in="0.6", remove_in="1.0")🗑️ Deprecated
Audit API Renamed for Consistency.
All three old audit names are wrapped @deprecated shims — they still work and forward to the new names, emitting a FutureWarning when called. They will be removed in v1.0.
| Old name | New name |
|---|---|
find_deprecated_callables |
find_deprecation_wrappers |
validate_deprecated_callable |
validate_deprecation_wrapper |
DeprecatedCallableInfo |
DeprecationWrapperInfo |
Before:
from deprecate import (
find_deprecated_callables,
validate_deprecated_callable,
DeprecatedCallableInfo,
)
wrappers: list[DeprecatedCallableInfo] = find_deprecated_callables(my_module)
validate_deprecated_callable(my_module.old_fn)After:
from deprecate import (
find_deprecation_wrappers,
validate_deprecation_wrapper,
DeprecationWrapperInfo,
)
wrappers: list[DeprecationWrapperInfo] = find_deprecation_wrappers(my_module)
validate_deprecation_wrapper(my_module.old_fn)Why: The old names used a deprecated_callable (past-tense adjective) pattern while the rest of the library uses deprecation_* (noun form). The rename aligns with DeprecationConfig, DeprecationWrapperInfo, and deprecated_class()/deprecated_instance(). (#125)
no_warning_call→assert_no_warnings. The test helper has been renamed to match theassertWarns/assertRaisesnaming convention from the standard library. The old name remains as a deprecated alias until v1.0.
Before:
from deprecate import no_warning_call
with no_warning_call():
call_stable_api()After:
from deprecate import assert_no_warnings
with assert_no_warnings():
call_stable_api()Why: assert_no_warnings mirrors assertWarns/assertRaises, making test intent immediately obvious at a glance. (#131)
🔧 Fixed
@deprecated with a Class target on a Non-__init__ Method Now Fails Fast.
This pattern was never documented or supported. Passing a class as target on a non-__init__ method caused @deprecated to silently instantiate the target class at call time and forward self of the wrong type into it — always a runtime bug, never a valid deprecation. The guard now raises TypeError at decoration time so the misconfiguration is caught immediately rather than at the first call. (#121)
Was silently wrong (never valid):
class OldService:
@deprecated(NewService, deprecated_in="0.5", remove_in="1.0")
def process(self): # ← TypeError raised here now
...
# Previously: instantiated NewService() and called it with self=OldService instance
# → AttributeError or worse at runtime on every callCorrect alternatives:
# 1. Rename within the same class — forward to the new method directly:
class MyService:
def process(self):
...
@deprecated(target=process, deprecated_in="0.6", remove_in="1.0")
def run(self):
...
# 2. Deprecate the whole class — use deprecated_class() on the class itself:
NewService = ...
OldService = deprecated_class(NewService, deprecated_in="0.6", remove_in="1.0")
# 3. Constructor forwarding — target=NewClass on __init__ remains valid:
class OldService(NewService):
@deprecated(NewService, deprecated_in="0.6", remove_in="1.0")
def __init__(self, x, y):
...find_deprecation_wrappers()no longer reports falseinvalid_argsfor proxy objects. The proxy__call__has a catch-all(*args, **kwargs)signature; allargs_mappingkeys were previously flagged as invalid. Signature validation is now skipped for proxy objects. (#124)
📦 Installation
pip install --upgrade pyDeprecate
# For audit features:
pip install --upgrade "pyDeprecate[audit]"🙏 Thank You
A big thank you to everyone who helped with this release:
- Jirka Borovec (@Borda) — proxy features, audit API rename, breaking-change guard, bug fixes, docs, and release management
Full Changelog: v0.5.0...v0.6.0.post0
v0.5.0: Deprecation Lifecycle Management
🎉 pyDeprecate 0.5.0
We're excited to announce pyDeprecate 0.5.0 — the deprecation lifecycle release!
This version ships a brand-new audit module that brings CI-grade tooling for keeping your deprecated APIs healthy, discoverable, and eventually removed on schedule.
✨ What's New
🔍 audit Module — Deprecation Lifecycle Management
Note
These utilities are designed for use with pyDeprecate's @deprecated decorator and inspect its __deprecated__ metadata. They are not a general-purpose deprecation auditing framework.
The audit module requires the optional [audit] extra: pip install pyDeprecate[audit]
The headline feature is a dedicated deprecate.audit module that groups all inspection and enforcement utilities into one coherent API designed to be called from pytest or your CI scripts.
import mypackage
from deprecate.audit import find_deprecated_callables
# Scan an entire package for deprecated wrappers
infos = find_deprecated_callables(mypackage)
for info in infos:
print(info.module, info.function, info.no_effect)Three complementary audit capabilities are included:
🛡️ Zero-Impact Wrapper Validation
validate_deprecated_callable and find_deprecated_callables detect wrappers that have no real effect — invalid args_mapping keys, identity mappings, missing version fields, or a target that points back to the same function.
from deprecate.audit import validate_deprecated_callable
from mypackage import old_func
info = validate_deprecated_callable(old_func)
assert not info.no_effect, f"Misconfigured wrapper: {info.invalid_args}"⏰ Expiry Enforcement
validate_deprecation_expiry scans a module or package and raises when any wrapper's remove_in version has been reached or passed — so zombie code never ships past its scheduled removal deadline.
Auto-detects the installed package version via importlib.metadata (falls back to __version__):
from deprecate.audit import validate_deprecation_expiry
import mypackage
# Raises if any deprecated wrapper is past its remove_in deadline
validate_deprecation_expiry(mypackage)
# Or pin the version explicitly
validate_deprecation_expiry(mypackage, current_version="0.5.0")🔗 Deprecation Chain Detection
validate_deprecation_chains detects wrappers whose target is itself a deprecated callable, forming chains that users traverse unnecessarily.
Two chain kinds are reported via the new ChainType enum: TARGET (forwarding chain) and STACKED (composed argument mappings).
from deprecate.audit import validate_deprecation_chains
import mypackage
chains = validate_deprecation_chains(mypackage)
assert not chains, f"Chained deprecations found: {chains}"🐛 Enum Signature Fix
@deprecated wrappers now correctly handle callables with var-positional Enum parameters in their signatures — a subtle edge case that previously caused incorrect argument forwarding (#104).
🔧 Under the Hood
- Comparison table added to README showing how
pyDeprecatestacks up against alternatives (#97) - Test suite reorganized into
tests/unittests/andtests/integration/for clearer separation of concerns (#112, #95) - mypy moved from CI step to pre-commit hook for faster local feedback (#87)
- Link checker added to CI to catch broken documentation references (#91)
- Linting, type hint, and code quality improvements throughout (#85, #86, #98)
📦 Installation
pip install --upgrade pyDeprecate
# For audit features:
pip install --upgrade "pyDeprecate[audit]"🙏 Thank You
A big thank you to everyone who contributed to this release!
Full Changelog: v0.4.0...v0.5.0
v0.4.0: Enhanced Documentation & Modernization
🎉 pyDeprecate 0.4.0
We're excited to announce pyDeprecate 0.4.0, bringing automatic documentation updates and broader Python version support to make API deprecation even smoother!
✨ What's New
📝 Automatic Documentation Updates
The headline feature: update_docs option automatically inserts deprecation notices directly into your function and class docstrings!
@deprecated(target=new_function, update_docs=True, deprecated_in="0.4", remove_in="1.0")
def old_function():
"""Does something useful."""
passNow your IDE tooltips and generated documentation will automatically show deprecation warnings, making it easier for users to discover they're using deprecated APIs without even running the code.
🐍 Expanded Python Support
Full support for modern Python versions:
- ✅ Python 3.10
- ✅ Python 3.11
- ✅ Python 3.12
- ✅ Python 3.13
Keep your deprecation strategy up-to-date with the latest Python releases!
🔧 Under the Hood
- Modernized build configuration and dependencies
- Enhanced CI/CD testing across all supported Python versions
- Improved code quality and type hints
- Better test coverage
📦 Installation
pip install --upgrade pyDeprecate🙏 Thank You
Thanks to everyone who has used pyDeprecate and provided feedback!
Full changelog: 0.3.2...0.4.0
New Contributors:
- @onuralpszr made their first contribution in #66
v0.3.2: Support containing `kwargs` in target function
Expand usage of the target function has kwargs and uses kwargs.get.
class NewCls:
def __init__(self, c: float, d: str = "abc", **kwargs):
self.my_c = c
self.my_d = d
self.my_e = kwargs.get("e", 0.2)
class PastCls(NewCls):
@deprecated(target=NewCls, deprecated_in="0.2", remove_in="0.4")
def __init__(self, c: int, d: str = "efg", **kwargs):
passThanks to @jungbaepark
v0.3.1: Fixed `void` typing
Fixed typing for void helper to by mypy compliment, see sample:
from deprecate import deprecated, void
@deprecated(...)
def depr_accuracy(preds: list, target: list, blabla: float) -> float:
# to stop complain your IDE about unused argument you can use void/empty function
return void(preds, target, blabla)