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), which caused every@deprecatedwarning to point todeprecation.pyinstead of the caller. Replaced theinspect.signatureprobe with a try/except at call site. (#176)find_deprecation_wrappers()no longer aborts on PEP 702typing_extensions.deprecatedobjects. Scanner was matching PEP 702 wrappers (whose__deprecated__is a string) and raisingValueError. (#178)- Short-circuit forwarding for
*argssources now preserves extra positional arguments. Previously, extra positional arguments past the named parameters were silently dropped. (#180)
Full changelog: v0.8.0...v0.9.0