Skip to content

v0.9.0: Generators, async & audit status tables

Choose a tag to compare

@Borda Borda released this 05 Jun 19:50

🎉 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.md

Sample 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-zero

v0.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 first next(). All three TargetMode variants work transparently. (#176)
  • async def coroutine wrapper support for @deprecated. Wrapper is async def, inspect.iscoroutinefunction(wrapper) returns True, warning fires when the coroutine is awaited. All three TargetMode variants work with async sources and async targets. (#180)
  • Async generator function support for @deprecated. No more decoration-time UserWarning; wrapper is a sync callable that fires the warning eagerly and returns an async-generator object. All three TargetMode variants work. (#181)
  • Order-agnostic @classmethod / @staticmethod. Both orders produce classmethod(deprecated_wrapper); decorator order no longer matters. (#178)
  • Stacked @deprecatedARGS_REMAP + NOTIFY combination. Rename arguments first, deprecate the whole function later. Six other stacking shapes emit UserWarning at decoration time and will become TypeError in v1.0. (#172)
  • Markdown deprecation tables — generate_deprecation_table() + pydeprecate status CLI subcommand. New DeprecationStatus and TableStyle public enums. New --style and --output CLI flags. Integrated into pydeprecate all. Auto-detects package version from pyproject.toml or installed metadata. (#133)
  • ChainType enum now exported as public API. Previously returned by validate_deprecation_chains(); now listed in deprecate.__all__. (#133)
  • Audit discovery extended to class descriptors. find_deprecation_wrappers() now inspects classmethod and staticmethod descriptors on class members. (#178)

⚠️ Breaking Changes

  • CLI flag renamed: --skip_errors--exit-zero across all four subcommands (check, expiry, chains, all). No transitional release — flag was new in v0.8.0. Canonical spelling is --exit-zero; --exit_zero also accepted by the CLI framework. (#187)
  • Programmatic cmd_check / cmd_expiry / cmd_chains / cmd_all keyword renamed from skip_errors to exit_zero. (#187)

🌱 Changed

  • Misconfigured stacking combinations warn at decoration time. Six previously-undefined @deprecated stacking shapes now emit UserWarning at decoration time naming the specific shape. Scheduled to become TypeError in v1.0. (#172)
  • Audit chain classification — ARGS_REMAP + NOTIFY is now classified as STACKED rather than TARGET. 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() gains include_members parameter for scanning descriptor members; validate_deprecation_expiry() default unchanged at include_members=False to preserve existing scan scope. (#133)

🔧 Fixed

  • stacklevel attribution on Python 3.12+. inspect.signature(warnings.warn) raises ValueError on 3.12+ (the C builtin lacks an introspectable signature), which caused every @deprecated warning to point to deprecation.py instead of the caller. Replaced the inspect.signature probe with a try/except at call site. (#176)
  • find_deprecation_wrappers() no longer aborts on PEP 702 typing_extensions.deprecated objects. Scanner was matching PEP 702 wrappers (whose __deprecated__ is a string) and raising ValueError. (#178)
  • Short-circuit forwarding for *args sources 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