Skip to content

Releases: Borda/pyDeprecate

v0.10.1: Cycle detection, strict property, & inner-order audit flag

Choose a tag to compare

@Borda Borda released this 03 Jul 07:57

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_property flagfind_deprecation_wrappers() sets True for inner-order @property @deprecated where only fget warns; CI pipelines can filter on this field. (#201)
  • Opt-in strict property via from deprecate import property — raises TypeError at class-definition time when a getter already carries @deprecated metadata; star imports unaffected. (#201)

🔧 Fixed

  • Circular deprecation chains now raise RuntimeError at call timeContextVar re-entrancy guard replaces unbounded recursion; async cycle detection avoids false positives from concurrent tasks. (#200)
  • _StrictProperty TypeError message references correct module path — error now points to deprecate.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

Choose a tag to compare

@Borda Borda released this 22 Jun 08:56

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)  # silent

2. 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.0

Explicit 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 emit FutureWarning on read, write, and delete with per-attribute warning budgets. None as the redirect value means warn-only. TargetMode.ATTRS_REMAP is the corresponding mode. Multi-hop chains allowed; cycles raise ValueError at decoration time. (#191)
  • deprecated_class stacking now supported. Two @deprecated_class decorators on the same class work correctly — isinstance() delegates through the chain, instantiation emits at most one warning. (#193)
  • Dataclass attrs_mapping auto-expand. Single attrs_mapping entry covers both attribute access and constructor kwargs on a @dataclass. Explicit args_mapping always wins. (#193)
  • validate_mapping_compatibility() audit function. Returns all deprecated_class proxies whose args_mapping remaps to POSITIONAL_ONLY constructor parameters. Use in CI alongside validate_deprecation_expiry. (#193)
  • @deprecated @property wraps fset and fdel. Outer-order decoration now covers all three accessors via the new _DeprecatedProperty subclass. (#190)
  • Raw staticmethod/classmethod descriptors accepted as target. _normalize_target unwraps automatically inside class bodies. (#192)

🔧 Fixed

  • @deprecated correctly forwards calls to targets with POSITIONAL_ONLY parameters. Detects at decoration time, emits UserWarning, splits call dispatch positionally. args_mapping applied before split. (#194)
  • args_mapping precedence: 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

  • Jirka Borovec (@Borda) — all features and fixes in this release (#190#198)

Full changelog: v0.9.0...v0.10.0

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), whi...
Read more

v0.8.0: Default TargetMode enum & CLI audit tools

Choose a tag to compare

@Borda Borda released this 21 May 08:13

🎉 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 pass

Also 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=NoneTargetMode.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=TrueTargetMode.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.NOTIFY

Notable changes

Added

  • TargetMode enum (NOTIFY, ARGS_REMAP) exported from deprecate — typed replacement for target=None / target=True sentinels. (#150)
  • target defaults to TargetMode.NOTIFY on @deprecated — warn-only deprecation now requires only deprecated_in and remove_in. (#162)
  • pydeprecate CLIcheck, expiry, chains, all subcommands.
    • initial CLI scaffolding (#76)
    • check, expiry, chains, all subcommands (#149)
  • template_mgs and args_extra on deprecated_class() and deprecated_instance() — proxy factories now at full parity with @deprecated. (#150)
  • DeprecationWrapperInfo.empty_deprecated_inTrue when deprecated_in is absent; for CI pipeline use. (#166)
  • DeprecationConfig.misconfiguredTrue when an invalid raw target sentinel (False) was passed at decoration time; surfaced via DeprecationWrapperInfo.misconfigured_target. (#150)
  • num_warns=0 documented — equivalent to stream=None; suppresses all warnings. (#150)
  • Stacked-callable-target guard@deprecated(target=callable_a) stacked over another callable-target wrapper now emits UserWarning at decoration time instead of crashing with TypeError at call time. (#169)
  • template_mgs validated at decoration time — malformed %-style placeholders raise ValueError immediately. (#169)

Changed

  • DeprecationConfig.target normalized at decoration time — stores TargetMode or Callable, never raw None/True/False. (#150)
  • Misconfigured TargetMode combos warn at construction timeARGS_REMAP without args_mapping, NOTIFY with args_mapping or args_extra all emit UserWarning immediately. (#150)
  • Docs site URL layout versioned — content now at https://borda.github.io/pyDeprecate/stable/ (and /<tag>/); the bare root redirects to stable/. Existing bookmarks to flat paths will break on first deploy. (#148)
  • CLI chains reportingcheck subcommand reports chains as warnings; chains/all subcommands report chains as errors. (#149)

Deprecated

  • target=None — use TargetMode.NOTIFY. Emits FutureWarning. Removed in v1.0. (#150)
  • target=True — use TargetMode.ARGS_REMAP. Emits FutureWarning. Removed in v1.0. (#150)
  • target=False — never valid; now emits UserWarning, treated as TargetMode.NOTIFY. Raises TypeError in v1.0. (#150)
  • DeprecationWrapperInfo.empty_mappingempty_args_mapping. Emits DeprecationWarning. Removed in v1.0. (#166)
  • DeprecationWrapperInfo.identity_mappingidentity_args_mapping. Emits DeprecationWarning. Removed in v1.0. (#166)

Fixed

  • PEP 702 stacking crash@deprecated stacked under @typing.deprecated no longer raises AttributeError on __deprecated__ lookup. (#169)
  • Double FutureWarning on deprecated_class() in NOTIFY mode. (#162)
  • Cross-class guard false positives — metaclass/dynamic-class qualnames and pre-applied decorators that rewrite __qualname__ no longer trigger spurious TypeError at decoration time; the guard still raises for genuine cross-class forwarding. (#169)
  • args_mapping rename 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) — TargetMode enum, 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

Choose a tag to compare

@Borda Borda released this 31 Mar 17:30

🎉 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:RuntimeDocstrings

Sphinx 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

Choose a tag to compare

@Borda Borda released this 15 Mar 10:44

🎉 pyDeprecate 0.6.0

pyDeprecate 0.6.0.post0 is the full proxy releasedeprecated_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  # 1

Also 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)           # True

Neither 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_callassert_no_warnings. The test helper has been renamed to match the assertWarns/assertRaises naming 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 call

Correct 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 false invalid_args for proxy objects. The proxy __call__ has a catch-all (*args, **kwargs) signature; all args_mapping keys 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

Choose a tag to compare

@Borda Borda released this 23 Feb 22:34

🎉 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 pyDeprecate stacks up against alternatives (#97)
  • Test suite reorganized into tests/unittests/ and tests/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

Choose a tag to compare

@Borda Borda released this 03 Dec 10:39

🎉 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."""
    pass

Now 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:

v0.3.2: Support containing `kwargs` in target function

Choose a tag to compare

@Borda Borda released this 11 Jun 09:32
bf57030

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):
        pass

Thanks to @jungbaepark

v0.3.1: Fixed `void` typing

Choose a tag to compare

@Borda Borda released this 31 May 16:48

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)