Skip to content

v0.10.0: Property accessors & class attribute mapping

Choose a tag to compare

@Borda Borda released this 22 Jun 08:56
· 30 commits to main since this release

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