{"id":15600581,"url":"https://github.com/borda/pydeprecate","last_synced_at":"2026-03-14T02:48:21.665Z","repository":{"id":41842078,"uuid":"348507513","full_name":"Borda/pyDeprecate","owner":"Borda","description":"Simple tooling for marking deprecated functions or classes and re-routing to the new successors' instance.","archived":false,"fork":false,"pushed_at":"2025-04-10T19:34:37.000Z","size":137,"stargazers_count":51,"open_issues_count":2,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-16T15:06:45.937Z","etag":null,"topics":["compatibility","deprecated","python","redirection","routing"],"latest_commit_sha":null,"homepage":"https://borda.github.io/pyDeprecate/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Borda.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-03-16T22:17:15.000Z","updated_at":"2025-04-10T19:34:41.000Z","dependencies_parsed_at":"2022-08-11T19:10:12.323Z","dependency_job_id":"e8b17ea1-c117-44c9-86ae-b9e5d43e4d76","html_url":"https://github.com/Borda/pyDeprecate","commit_stats":{"total_commits":108,"total_committers":10,"mean_commits":10.8,"dds":0.4629629629629629,"last_synced_commit":"3298268bf4086428a97240f3a5a0baaa527da3dc"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Borda%2FpyDeprecate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Borda%2FpyDeprecate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Borda%2FpyDeprecate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Borda%2FpyDeprecate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Borda","download_url":"https://codeload.github.com/Borda/pyDeprecate/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254553959,"owners_count":22090417,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compatibility","deprecated","python","redirection","routing"],"created_at":"2024-10-03T02:04:35.806Z","updated_at":"2026-03-14T02:48:21.651Z","avatar_url":"https://github.com/Borda.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pyDeprecate\n\n**Simple tooling for marking deprecated functions or classes and re-routing to their successors.**\n\n\u003e **Summary**: pyDeprecate is a lightweight Python library for managing function and class deprecations with zero dependencies. It provides automatic call forwarding to replacement functions, argument mapping between old and new APIs, and configurable warning controls to prevent log spam. Perfect for library maintainers evolving APIs while maintaining backward compatibility.\n\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyDeprecate)](https://pypi.org/project/pyDeprecate/)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/Borda/pyDeprecate/blob/main/LICENSE)\n[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FBorda%2FpyDeprecate.svg?type=shield\u0026issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FBorda%2FpyDeprecate?ref=badge_shield\u0026issueType=license)\n\n[![PyPI Status](https://badge.fury.io/py/pyDeprecate.svg)](https://badge.fury.io/py/pyDeprecate)\n[![PyPI Status](https://pepy.tech/badge/pyDeprecate)](https://pepy.tech/project/pyDeprecate)\n[![Conda](https://img.shields.io/conda/v/conda-forge/pyDeprecate?label=conda\u0026color=success)](https://anaconda.org/conda-forge/pyDeprecate)\n![Conda](https://img.shields.io/conda/dn/conda-forge/pyDeprecate?color=blue)\n[![CodeFactor](https://www.codefactor.io/repository/github/borda/pydeprecate/badge)](https://www.codefactor.io/repository/github/borda/pydeprecate)\n\n[![CI testing](https://github.com/Borda/pyDeprecate/actions/workflows/ci_testing.yml/badge.svg?branch=main\u0026event=push)](https://github.com/Borda/pyDeprecate/actions/workflows/ci_testing.yml)\n[![Install pkg](https://github.com/Borda/pyDeprecate/actions/workflows/ci_install-pkg.yml/badge.svg?branch=main\u0026event=push)](https://github.com/Borda/pyDeprecate/actions/workflows/ci_install-pkg.yml)\n[![codecov](https://codecov.io/gh/Borda/pyDeprecate/branch/main/graph/badge.svg?token=BG7RQ86UJA)](https://codecov.io/gh/Borda/pyDeprecate)\n[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Borda/pyDeprecate/main.svg)](https://results.pre-commit.ci/latest/github/Borda/pyDeprecate/main)\n\n______________________________________________________________________\n\n## 📋 Table of Contents\n\n- [📖 Overview](#-overview)\n- [✨ Features](#-features)\n  - [Comparison with Other Tools](#-comparison-with-other-tools)\n- [💾 Installation](#-installation)\n- [🚀 Quick Start](#-quick-start)\n- [📚 Use-cases and Applications](#-use-cases-and-applications)\n  - [Simple function forwarding](#-simple-function-forwarding)\n  - [Advanced target argument mapping](#-advanced-target-argument-mapping)\n  - [Deprecation warning only](#-deprecation-warning-only)\n  - [Self argument mapping](#-self-argument-mapping)\n  - [Multiple deprecation levels](#-multiple-deprecation-levels)\n  - [Conditional skip](#-conditional-skip)\n  - [Class deprecation](#-class-deprecation)\n  - [Deprecating constants and instances](#-deprecating-constants-and-instances)\n  - [Deprecating Enums and dataclasses](#-deprecating-enums-and-dataclasses)\n  - [Automatic docstring updates](#-automatic-docstring-updates)\n- [🔇 Understanding the void() Helper](#-understanding-the-void-helper)\n- [🔍 Audit](#-audit)\n  - [Validating Wrapper Configuration](#-validating-wrapper-configuration)\n  - [Enforcing Deprecation Removal Deadlines](#-enforcing-deprecation-removal-deadlines)\n  - [Detecting Deprecation Chains](#-detecting-deprecation-chains)\n- [🧪 Testing Deprecated Code](#-testing-deprecated-code)\n- [🔧 Troubleshooting](#-troubleshooting)\n- [🤝 Contributing](#-contributing)\n\n## 📖 Overview\n\nThe common use-case is moving your functions across a codebase or outsourcing some functionalities to new packages.\nFor most of these cases, you want to maintain some compatibility, so you cannot simply remove the past function. You also want to warn users for some time that the functionality they have been using has moved and is now deprecated in favor of another function (which should be used instead) and will soon be removed completely.\n\nAnother good aspect is not overwhelming users with too many warnings, so per function/class, this warning is raised only N times in the preferred stream (warning, logger, etc.).\n\n## ✨ Features\n\n- ⚠️ Deprecation warnings are shown once per function by default (prevents log spam)\n- 🔄 Arguments are automatically mapped to the target function\n- 🚫 The deprecated function body is never executed when using `target`\n- ⚡ Minimal runtime overhead with zero dependencies (Python standard library only)\n- 🛠️ Supports deprecating functions, methods, and classes\n- 📝 Optionally, docstrings can be updated automatically to reflect deprecation\n- 🔍 Preserves original function signature, annotations and metadata for introspection\n- ⚙️ Configurable warning message template and output stream (logging, warnings, custom callable)\n- 🎯 Fine‑grained control: per‑argument deprecation/mapping and conditional `skip_if` behavior\n- 🧪 Includes testing helpers (e.g., `assert_no_warnings`, formerly `no_warning_call`) for deterministic tests\n- 🔗 Compatible with methods, class constructors and cross‑module moves\n\n### 📊 Comparison with Other Tools\n\n\u003e 💬 _How does pyDeprecate compare to other Python deprecation solutions?_\n\nWhile `pyDeprecate` focuses on comprehensive forwarding and argument mapping, other tools might fit different needs:\n\n- [`warnings.warn`](https://docs.python.org/3/library/warnings.html) (stdlib): The standard library's built-in function, perfect for simple cases requiring no dependencies.\n- [`deprecation`](https://pypi.org/project/deprecation/) (Lib): A widely used library by Brian Curtin, excellent for version-based deprecations.\n- [`Deprecated`](https://pypi.org/project/Deprecated/) (wrapt): A robust decorator-based library by Laurent Laporte with `wrapt` integration.\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eKey Advantages \u0026 Feature Breakdown\u003c/strong\u003e\u003c/summary\u003e\n\n- **Simple Warnings**: Emits standard Python warnings, compatible with default error handling tools.\n- **Auto-Forward Calls**: Automatically redirects calls to the new function, ensuring the deprecated code is *never* executed.\n- **Argument Mapping**: Seamlessly translates old API arguments to new ones, handling complex renames and restructuring.\n- **Argument Deprecation**: Warns when specific arguments are used, even if the function itself isn't deprecated.\n- **Docstring Updates**: Automatically appends deprecation notices to the function's docstring.\n- **Version Tracking**: Clearly specifies `deprecated_in` and `remove_in` versions for better lifecycle management.\n- **Prevent Log Spam**: Prevents log spam by showing warnings only once per function (or N times) by default.\n- **Zero Extra Depend.**: Lightweight and easy to install, relying solely on the Python standard library.\n- **Custom Streams**: Route warnings to `logging`, standard `warnings`, or any custom callable to fit your monitoring stack.\n- **Testing Helpers**: Built-in tools like `assert_no_warnings()` ensure your deprecations are testable and deterministic.\n\n\u003c/details\u003e\n\n| Feature                  | `pyDeprecate` | `warnings.warn` (stdlib) | `deprecation` (Lib) | `Deprecated` (wrapt) |\n| ------------------------ | :-----------: | :----------------------: | :-----------------: | :------------------: |\n| **Simple Warnings**      |      ✅       |            ✅            |         ✅          |          ✅          |\n| **Auto-Forward Calls**   |      ✅       |            ❌            |         ❌          |          ❌          |\n| **Argument Mapping**     |      ✅       |            ❌            |         ❌          |          ❌          |\n| **Argument Deprecation** |      ✅       |            ✍️            |         ❌          |          ❌          |\n| **Docstring Updates**    |      ✅       |            ❌            |         ✅          |          ✅          |\n| **Version Tracking**     |      ✅       |            ✍️            |         ✅          |          ✅          |\n| **Prevent Log Spam**     |      ✅       |            ✍️            |         ❌          |          ❌          |\n| **Zero Extra Depend.**   |      ✅       |            ✅            |         ❌          |          ❌          |\n| **Custom Streams**       |      ✅       |            ✅            |         ❌          |          ❌          |\n| **Testing Helpers**      |      ✅       |            ❌            |         ❌          |          ❌          |\n\n✍️ = possible but requires manual implementation\n\n\u003e [!NOTE]\n\u003e This comparison is compiled to the best of our knowledge and we're happy to make any justified corrections. If you spot an inaccuracy, please [open an issue](https://github.com/Borda/pyDeprecate/issues) or submit a PR.\n\n## 💾 Installation\n\nRequires **Python 3.9 or later**.\n\nSimple installation from PyPI:\n\n```bash\npip install pyDeprecate\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eOther installations\u003c/summary\u003e\n\nSimply install with pip from source:\n\n```bash\npip install https://github.com/Borda/pyDeprecate/archive/main.zip\n```\n\n\u003c/details\u003e\n\n## 🚀 Quick Start\n\nHere's the simplest way to get started with deprecating a function:\n\n```python\nfrom deprecate import deprecated\n\n\n# NEW/FUTURE API — renamed to be more explicit about what it computes\ndef compute_sum(a: int = 0, b: int = 3) -\u003e int:\n    return a + b\n\n\n# DEPRECATED API — `addition` was the original name before the rename\n@deprecated(target=compute_sum, deprecated_in=\"1.0\", remove_in=\"2.0\")\ndef addition(a: int, b: int = 5) -\u003e int:\n    pass  # body is not needed — calls are forwarded to compute_sum\n\n\n# Using the original name still works but shows a warning\nresult = addition(1, 2)  # Returns 3\n# Warning: The `addition` was deprecated since v1.0 in favor of `__main__.compute_sum`.\n#          It will be removed in v2.0.\n```\n\nThat's it! All calls to `addition()` are automatically forwarded to `compute_sum()` with a deprecation warning.\n\n## 📚 Use-cases and Applications\n\nThe functionality is kept simple and all defaults should be reasonable, but you can still do extra customization such as:\n\n- 💬 define user warning message and preferred stream\n- 🔀 extended argument mapping to target function/method\n- 🎯 define deprecation logic for self arguments\n- 📊 specify warning count per:\n  - called function (for func deprecation)\n  - used arguments (for argument deprecation)\n- ⚙️ define conditional skip (e.g. depending on some package version)\n\nIn particular the target values (cases):\n\n- _None_ - raise only warning message (ignore all argument mapping)\n- _True_ - deprecate some argument of itself (argument mapping should be specified)\n- _Callable_ - forward call to new methods (optionally also argument mapping or extras)\n\n\u003e [!NOTE]\n\u003e `@deprecated` is designed for functions and methods. To deprecate a class, Enum, or dataclass, use `@deprecated_class()` instead (see [Deprecating Enums and dataclasses](#-deprecating-enums-and-dataclasses)).\n\n### ➡ Simple function forwarding\n\nIt is very straightforward: you forward your function call to a new function and all arguments are mapped:\n\n```python\n# NEW/FUTURE API — renamed to be more explicit about what it computes\ndef compute(a: int = 0, b: int = 3) -\u003e int:\n    \"\"\"New function anywhere in the codebase or even other package.\"\"\"\n    return a + b\n\n\n# ---------------------------\n\nfrom deprecate import deprecated\n\n\n# What this module looked like before the rename:\n# def calculate(a: int, b: int = 5) -\u003e int:\n#     return a + b\n\n\n# DEPRECATED API — `calculate` was the original name before the rename\n@deprecated(target=compute, deprecated_in=\"0.1\", remove_in=\"0.5\")\ndef calculate(a: int, b: int = 5) -\u003e int:\n    \"\"\"\n    My deprecated function which now has an empty body\n     as all calls are routed to the new function.\n    \"\"\"\n    pass  # or you can just place docstring as one above\n\n\n# calling this function will raise a deprecation warning:\n#   The `calculate` was deprecated since v0.1 in favor of `__main__.compute`.\n#   It will be removed in v0.5.\nprint(calculate(1, 2))\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003eprint(calculate(1, 2))\u003c/code\u003e\u003c/summary\u003e\n\n```\n3\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWrapper form: applying \u003ccode\u003e@deprecated\u003c/code\u003e without the decorator syntax\u003c/summary\u003e\n\nWhen the deprecated name already exists as a callable (for example, imported from another package), you can apply `deprecated()` directly without redefining the function:\n\n```python\nfrom deprecate import deprecated\n\n\n# NEW/FUTURE API — in real usage this would be imported from another module\ndef compute_sum(a: int, b: int = 0) -\u003e int:\n    return a + b\n\n\n# LEGACY — already-existing callable that is being deprecated\ndef addition(a: int, b: int = 0) -\u003e int:\n    return a + b\n\n\n# DEPRECATED API — `calculate` was the original name in this package;\n# wrap it without redefining a function body\ncalculate = deprecated(\n    target=compute_sum,\n    deprecated_in=\"0.5\",\n    remove_in=\"1.0\",\n)(addition)\n```\n\nThis is an equivalent to the `@deprecated(...)` decorator form but applied to an already-existing callable — useful when the deprecated function lives in a dependency you don't control.\n\n\u003c/details\u003e\n\n### 🔀 Advanced target argument mapping\n\nAnother more complex example is using argument mapping is:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample: mapping deprecated args to \u003ccode\u003esklearn.metrics.accuracy_score\u003c/code\u003e\u003c/summary\u003e\n\n```python\nimport logging\nfrom sklearn.metrics import accuracy_score\nfrom deprecate import deprecated, void\n\n\n@deprecated(\n    # use standard sklearn accuracy implementation\n    target=accuracy_score,\n    # custom warning stream\n    stream=logging.warning,\n    # number of warnings per lifetime (with -1 for always)\n    num_warns=5,\n    # custom message template\n    template_mgs=\"`%(source_name)s` was deprecated, use `%(target_path)s`\",\n    # as target args are different, define mapping from source to target func\n    args_mapping={\"preds\": \"y_pred\", \"target\": \"y_true\", \"blabla\": None},\n)\ndef depr_accuracy(preds: list, target: list, blabla: float) -\u003e float:\n    \"\"\"My deprecated function which is mapping to sklearn accuracy.\"\"\"\n    # to stop complain your IDE about unused argument you can use void/empty function\n    return void(preds, target, blabla)\n\n\n# calling this function will raise a deprecation warning:\n#   WARNING:root:`depr_accuracy` was deprecated, use `sklearn.metrics.accuracy_score`\nprint(depr_accuracy([1, 0, 1, 2], [0, 1, 1, 2], 1.23))\n```\n\nsample output:\n\n```\n0.5\n```\n\n\u003c/details\u003e\n\n### ⚠ Deprecation warning only\n\nBase use-case with no forwarding and just raising a warning:\n\n```python\nfrom deprecate import deprecated\n\n\n@deprecated(target=None, deprecated_in=\"0.1\", remove_in=\"0.5\")\ndef my_sum(a: int, b: int = 5) -\u003e int:\n    \"\"\"My deprecated function which still has to have implementation.\"\"\"\n    return a + b\n\n\n# calling this function will raise a deprecation warning:\n#   The `my_sum` was deprecated since v0.1. It will be removed in v0.5.\nprint(my_sum(1, 2))\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003eprint(my_sum(1, 2))\u003c/code\u003e\u003c/summary\u003e\n\n```\n3\n```\n\n\u003c/details\u003e\n\n\u003e [!NOTE]\n\u003e When using `target=None`, the deprecated function's implementation must be preserved and will be executed. The deprecation decorator only adds a warning without forwarding.\n\n### 🔄 Self argument mapping\n\nWe also support deprecation and argument mapping for the function itself:\n\n```python\nfrom deprecate import deprecated\n\n\n@deprecated(\n    # define as deprecation some self argument - mapping\n    target=True,\n    args_mapping={\"coef\": \"new_coef\"},\n    # common version info\n    deprecated_in=\"0.2\",\n    remove_in=\"0.4\",\n)\ndef any_pow(base: float, coef: float = 0, new_coef: float = 0) -\u003e float:\n    \"\"\"My function with deprecated argument `coef` mapped to `new_coef`.\"\"\"\n    return base**new_coef\n\n\n# calling this function will raise a deprecation warning:\n#   The `any_pow` uses deprecated arguments: `coef` -\u003e `new_coef`.\n#   They were deprecated since v0.2 and will be removed in v0.4.\nprint(any_pow(2, 3))\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003eprint(any_pow(2, 3))\u003c/code\u003e\u003c/summary\u003e\n\n```\n8\n```\n\n\u003c/details\u003e\n\n### 🔗 Multiple deprecation levels\n\nEventually you can set multiple deprecation levels via chaining deprecation arguments as each could be deprecated in another version:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample: chaining two argument deprecations across different versions\u003c/summary\u003e\n\n```python\nfrom deprecate import deprecated\n\n\n@deprecated(\n    True,\n    deprecated_in=\"0.3\",\n    remove_in=\"0.6\",\n    args_mapping=dict(c1=\"nc1\"),\n    template_mgs=\"Depr: v%(deprecated_in)s rm v%(remove_in)s for args: %(argument_map)s.\",\n)\n@deprecated(\n    True,\n    deprecated_in=\"0.4\",\n    remove_in=\"0.7\",\n    args_mapping=dict(nc1=\"nc2\"),\n    template_mgs=\"Depr: v%(deprecated_in)s rm v%(remove_in)s for args: %(argument_map)s.\",\n)\ndef any_pow(base, c1: float = 0, nc1: float = 0, nc2: float = 2) -\u003e float:\n    return base**nc2\n\n\n# calling this function will raise deprecation warnings:\n#   FutureWarning('Depr: v0.3 rm v0.6 for args: `c1` -\u003e `nc1`.')\n#   FutureWarning('Depr: v0.4 rm v0.7 for args: `nc1` -\u003e `nc2`.')\nprint(any_pow(2, 3))\n```\n\ncode output:\n\n```\n8\n```\n\n\u003c/details\u003e\n\n### ⚙ Conditional skip\n\nConditional skip of which can be used for mapping between different target functions depending on additional input such as package version\n\n\u003cdetails\u003e\n\u003csummary\u003eExample: \u003ccode\u003eskip_if\u003c/code\u003e based on a runtime condition\u003c/summary\u003e\n\n```python\nfrom deprecate import deprecated\n\nFAKE_VERSION = 1\n\n\ndef version_greater_1():\n    return FAKE_VERSION \u003e 1\n\n\n@deprecated(True, \"0.3\", \"0.6\", args_mapping=dict(c1=\"nc1\"), skip_if=version_greater_1)\ndef skip_pow(base, c1: float = 1, nc1: float = 1) -\u003e float:\n    return base ** (c1 - nc1)\n\n\n# calling this function will raise a deprecation warning\nprint(skip_pow(2, 3))\n\n# change the fake versions\nFAKE_VERSION = 2\n\n# will not raise any warning\nprint(skip_pow(2, 3))\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003eskip_pow\u003c/code\u003e before and after version change\u003c/summary\u003e\n\n```\n0.25\n4\n```\n\n\u003c/details\u003e\n\nThis can be beneficial with multiple deprecation levels shown above...\n\n### 🏗 Class deprecation\n\nTwo common patterns: deprecating a single method (rename within the same class) or deprecating the whole class by forwarding `__init__` to a successor.\n\n\u003cdetails\u003e\n\u003csummary\u003eExample: renaming a method within the same class\u003c/summary\u003e\n\n```python\nfrom deprecate import deprecated, void\n\n\nclass MyService:\n    # NEW/FUTURE API — renamed from run() for clarity\n    def execute(self, x: int) -\u003e int:\n        \"\"\"Current method.\"\"\"\n        return x * 2\n\n    # DEPRECATED API — `run` was the original name before the rename\n    @deprecated(target=execute, deprecated_in=\"1.0\", remove_in=\"2.0\")\n    def run(self, x: int) -\u003e int:\n        \"\"\"Deprecated — renamed to execute().\"\"\"\n        return void(x)\n\n\nsvc = MyService()\n# calling this method will raise a deprecation warning:\n#   The `run` was deprecated since v1.0 in favor of `__main__.execute`.\n#   It will be removed in v2.0.\nprint(svc.run(5))\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003esvc.run(5)\u003c/code\u003e\u003c/summary\u003e\n\n```\n10\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExample: forwarding \u003ccode\u003e__init__\u003c/code\u003e to a successor class\u003c/summary\u003e\n\n```python\n# NEW/FUTURE API — renamed to be more descriptive\nclass HttpClient:\n    \"\"\"My new class anywhere in the codebase or other package.\"\"\"\n\n    def __init__(self, c: float, d: str = \"abc\"):\n        self.my_c = c\n        self.my_d = d\n\n\n# ---------------------------\n\nfrom deprecate import deprecated, void\n\n\n# DEPRECATED API — `Client` was the original name before it was renamed to HttpClient\nclass Client(HttpClient):\n    \"\"\"\n    The deprecated class should be inherited from the successor class\n     to hold all methods and properties.\n    \"\"\"\n\n    @deprecated(target=HttpClient, deprecated_in=\"0.2\", remove_in=\"0.4\")\n    def __init__(self, c: int, d: str = \"efg\"):\n        \"\"\"\n        You place the decorator around __init__ as you want\n         to warn user just at the time of creating object.\n\n        Decorating __init__ warns at instantiation time and optionally\n        forwards to another class. For deprecating the class itself\n        (name change, Enum, dataclass), use @deprecated_class() instead.\n        \"\"\"\n        void(c, d)\n\n\n# calling this function will raise a deprecation warning:\n#   The `Client` was deprecated since v0.2 in favor of `__main__.HttpClient`.\n#   It will be removed in v0.4.\ninst = Client(7)\nprint(inst.my_c)  # returns: 7\nprint(inst.my_d)  # returns: \"efg\"\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003eClient\u003c/code\u003e instance attributes\u003c/summary\u003e\n\n```\n7\nefg\n```\n\n\u003c/details\u003e\n\n### 📦 Deprecating constants and instances\n\nUse `deprecated_instance` to wrap objects accessed via attribute/item/call operations (for example, dicts,\nlists, or custom objects) with transparent deprecation warnings. Primitive protocol methods (such as numeric\narithmetic on `float` or concatenation on `str`) are not proxied. For primitive constants like floats or\nstrings, prefer wrapping them in a container (such as a dict or configuration object) or updating call sites\ndirectly, since arithmetic and other primitive protocol operations are not intercepted by the wrapper. The\n`name` parameter is optional; when omitted it defaults to the type name of the wrapped object.\n\n```python\nfrom deprecate import deprecated_instance\n\n# NEW/FUTURE API — renamed to be more explicit about its scope\nTRAINING_CONFIG = {\"lr\": 0.001, \"batch_size\": 32, \"epochs\": 10}\n\n# What it looked like before the rename:\n# DEFAULTS = {\"lr\": 0.001, \"batch_size\": 32, \"epochs\": 10}\n\n# DEPRECATED API — `DEFAULTS` was the original name; read-only so\n# callers cannot mutate shared state through the deprecated alias\nDEFAULTS = deprecated_instance(\n    TRAINING_CONFIG,\n    deprecated_in=\"1.2\",\n    remove_in=\"2.0\",\n    read_only=True,\n)\n\n# Reading still works but emits a FutureWarning once:\n#   The `dict` was deprecated since v1.2. It will be removed in v2.0.\nprint(DEFAULTS[\"lr\"])  # 0.001\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003eprint(DEFAULTS[\"lr\"])\u003c/code\u003e\u003c/summary\u003e\n\n```\n0.001\n```\n\n\u003c/details\u003e\n\n### 🗂 Deprecating Enums and dataclasses\n\n\u003cdetails\u003e\n\u003csummary\u003eExample: \u003ccode\u003edeprecated_class()\u003c/code\u003e for Enum and dataclass\u003c/summary\u003e\n\n`deprecated_class()` wraps an entire Enum or dataclass in a transparent proxy that warns on every\naccess and forwards attribute, item, and call operations to the replacement class.\nUse `args_mapping` to rename or drop kwargs when the deprecated class is called.\n\n\u003e [!NOTE]\n\u003e Type checks with `isinstance()` and `issubclass()` work transparently with `deprecated_class()` proxies and do not emit deprecation warnings, as these are structural checks rather than actual usage of the deprecated API.\n\n```python\nfrom enum import Enum\nfrom dataclasses import dataclass\nfrom deprecate import deprecated_class\n\n# mypackage/theme.py — what it looked like before the rename:\n#\n# class Color(Enum):\n#     RED = 1\n#     BLUE = 2\n\n\n# NEW/FUTURE API — renamed to be more descriptive\nclass ThemeColor(Enum):\n    RED = 1\n    BLUE = 2\n\n\n# DEPRECATED API — `Color` was the original name; no class body needed,\n# the proxy forwards all access to ThemeColor\nColor = deprecated_class(target=ThemeColor, deprecated_in=\"1.0\", remove_in=\"2.0\")(ThemeColor)\n\n# All access is forwarded to ThemeColor — a FutureWarning is emitted once:\n#   The `Color` was deprecated since v1.0. It will be removed in v2.0.\nprint(Color.RED is ThemeColor.RED)  # True\nprint(Color(1) is ThemeColor.RED)  # True\nprint(Color[\"RED\"] is ThemeColor.RED)  # True\n\n\n# Precision migration story:\n# - PointV1 used integer pixel coordinates.\n# - PointV2 supports float coordinates for sub-pixel precision and smoother transforms.\n\n\n# NEW/FUTURE API — extended to float precision\n@dataclass\nclass PointV2:\n    x: float\n    y: float\n\n\n# DEPRECATED API — PointV1 was the original integer-coordinate implementation\n@deprecated_class(target=PointV2, deprecated_in=\"1.8\", remove_in=\"2.0\")\n@dataclass\nclass PointV1:\n    x: int\n    y: int\n\n\n# Existing callers using integer coordinates still work and are forwarded to PointV2:\np_old = PointV1(3, 4)\nprint(isinstance(p_old, PointV2))\nprint((p_old.x, p_old.y))\n\n# New callers can use higher precision directly:\np_new = PointV2(3.25, 4.75)\nprint((p_new.x, p_new.y))\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: \u003ccode\u003eColor\u003c/code\u003e forwarding and \u003ccode\u003ePointV1\u003c/code\u003e precision migration\u003c/summary\u003e\n\n```\nTrue\nTrue\nTrue\nTrue\n(3, 4)\n(3.25, 4.75)\n```\n\n\u003c/details\u003e\n\n### 📝 Automatic docstring updates\n\nYou can automatically append deprecation information to your function's docstring:\n\n\u003cdetails\u003e\n\u003csummary\u003eExample: \u003ccode\u003eupdate_docstring=True\u003c/code\u003e appends a Sphinx deprecation notice\u003c/summary\u003e\n\n```python\n# NEW/FUTURE API — renamed to be more explicit about what it does\ndef transform(x: int) -\u003e int:\n    \"\"\"New implementation of the function.\"\"\"\n    return x * 2\n\n\n# ---------------------------\n\nfrom deprecate import deprecated\n\n\n# DEPRECATED API — `process` was the original name before the rename\n@deprecated(\n    target=transform,\n    deprecated_in=\"1.0\",\n    remove_in=\"2.0\",\n    update_docstring=True,  # Enable automatic docstring updates\n)\ndef process(x: int) -\u003e int:\n    \"\"\"Transforms the input value.\n\n    Args:\n        x: Input value\n\n    Returns:\n        Result of computation\n    \"\"\"\n    pass\n\n\n# The docstring now includes deprecation information\nprint(process.__doc__)\n# Output includes:\n# .. deprecated:: 1.0\n#    Will be removed in 2.0.\n#    Use `__main__.transform` instead.\n```\n\n\u003c/details\u003e\n\nThis is particularly useful for generating API documentation with tools like Sphinx, where the deprecation notice will appear in the generated docs.\n\n![Documentation Sample](assets/docs-sample.png)\n\n## 🔇 Understanding the `void()` Helper\n\nWhen using `@deprecated` with a `target` function, the deprecated function's body is never executed—all calls are automatically forwarded. However, your IDE might complain about \"unused parameters\". The `void()` helper function silences these warnings:\n\n```python\ndef new_add(a: int, b: int) -\u003e int:\n    return a + b\n\n\n# ---------------------------\n\nfrom deprecate import deprecated, void\n\n\n@deprecated(target=new_add, deprecated_in=\"1.0\", remove_in=\"2.0\")\ndef old_add(a: int, b: int) -\u003e int:\n    return void(a, b)  # Tells IDE: \"Yes, I know these parameters aren't used\"\n    # This line is never reached - call is forwarded to new_add\n\n\n# Alternative: You can also use pass or just a docstring\n@deprecated(target=new_add, deprecated_in=\"1.0\", remove_in=\"2.0\")\ndef old_add_v2(a: int, b: int) -\u003e int:\n    \"\"\"Just a docstring works too.\"\"\"\n    pass  # This also works\n```\n\n\u003e [!TIP]\n\u003e `void()` is purely for IDE convenience and has no runtime effect. It simply returns `None` after accepting any arguments.\n\n## 🔍 Audit\n\nDeprecations are only as good as the hygiene around them. The `deprecate.audit` module provides utilities for verifying that deprecated wrappers are correctly configured, that removal deadlines are actually enforced, and that chains of deprecated-to-deprecated calls don't silently pile up. These tools are designed to run in CI pipelines and test suites, catching problems before they reach users.\n\n\u003e [!NOTE]\n\u003e **Renamed in v0.6**: `find_deprecated_callables` → `find_deprecation_wrappers`, `validate_deprecated_callable` → `validate_deprecation_wrapper`, `DeprecatedCallableInfo` → `DeprecationWrapperInfo`. The old names are still exported for backwards compatibility but will be removed in v1.0.\n\n### Validating Wrapper Configuration\n\nDuring development, you may want to verify that your deprecated wrappers are configured correctly. pyDeprecate provides two utilities for this: `validate_deprecation_wrapper()` for inspecting a single function, and `find_deprecation_wrappers()` for scanning an entire package.\n\nThe `DeprecationWrapperInfo` dataclass contains:\n\n- `module`: Module name where the function is defined (empty for direct validation)\n- `function`: Function name\n- `deprecated_info`: The `__deprecated__` attribute dict from the decorator\n- `invalid_args`: List of args_mapping keys that don't exist in the function signature\n- `empty_mapping`: True if args_mapping is None or empty (no argument remapping)\n- `identity_mapping`: List of args where key equals value (e.g., `{'arg': 'arg'}` - no effect)\n- `self_reference`: True if target points to the same function (self-reference)\n- `no_effect`: True if wrapper has zero impact (self-reference, empty mapping, or all identity)\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eValidating a Single Function\u003c/b\u003e\u003c/summary\u003e\n\nThe `validate_deprecation_wrapper()` utility extracts the configuration from the function's `__deprecated__` attribute and returns a `DeprecationWrapperInfo` dataclass that helps you identify configurations that would make your deprecation wrapper have zero impact:\n\n```python\nfrom deprecate import validate_deprecation_wrapper, deprecated, DeprecationWrapperInfo\n\n\n# Define your deprecated function\n@deprecated(target=True, args_mapping={\"old_arg\": \"new_arg\"}, deprecated_in=\"1.0\")\ndef my_func(old_arg: int = 0, new_arg: int = 0) -\u003e int:\n    return new_arg\n\n\n# Validate the configuration - automatically extracts `args_mapping` and target from the decorator\nresult = validate_deprecation_wrapper(my_func)\n\n\n# DeprecationWrapperInfo(\n#   function='my_func',\n#   invalid_args=[],\n#   empty_mapping=False,\n#   identity_mapping=[],\n#   self_reference=False,\n#   no_effect=False\n# )\n\n\n# Example: Function with invalid args_mapping\n@deprecated(target=True, args_mapping={\"nonexistent\": \"new_arg\"}, deprecated_in=\"1.0\")\ndef bad_func(real_arg: int = 0) -\u003e int:\n    return real_arg\n\n\nresult = validate_deprecation_wrapper(bad_func)\n# result.invalid_args == ['nonexistent']\nprint(result)\n\n\n# Example: Function with empty mapping (no effect)\n@deprecated(target=True, args_mapping={}, deprecated_in=\"1.0\")\ndef empty_func(arg: int = 0) -\u003e int:\n    return arg\n\n\nresult = validate_deprecation_wrapper(empty_func)\n# result.empty_mapping == True, result.no_effect == True\nprint(result)\n\n# Quick check if wrapper has any effect\nif result.no_effect:\n    print(\"Warning: This wrapper configuration has zero impact!\")\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eScanning a Package for Deprecated Wrappers\u003c/b\u003e\u003c/summary\u003e\n\nThe `find_deprecation_wrappers()` utility scans an entire package or module and returns a list of `DeprecationWrapperInfo` dataclasses:\n\n```python\nfrom deprecate import find_deprecation_wrappers\n\n# For testing purposes, we use the test module; normally you would import your own package\nfrom tests import collection_deprecate as my_package\n\n# Scan an entire package for deprecated wrappers\nresults = find_deprecation_wrappers(my_package)\n\n# Or scan using a string module path\nresults = find_deprecation_wrappers(\"tests.collection_deprecate\")\n\n# Check results - each item is a DeprecationWrapperInfo dataclass\nfor r in results:\n    print(f\"{r.module}.{r.function}: no_effect={r.no_effect}\")\n    if r.no_effect:\n        print(f\"  Warning: This wrapper has zero impact!\")\n        print(f\"  invalid_args: {r.invalid_args}, identity_mapping: {r.identity_mapping}\")\n\n# Filter to only ineffective wrappers\nineffective = [r for r in results if r.no_effect]\nif ineffective:\n    print(f\"Found {len(ineffective)} deprecated wrappers with zero impact!\")\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eGenerating Reports by Issue Type\u003c/b\u003e\u003c/summary\u003e\n\nGroup validation results by issue type for better reporting:\n\n```python\nfrom deprecate import find_deprecation_wrappers\n\n# For testing purposes, we use the test module; normally you would import your own package\nfrom tests import collection_deprecate as my_package\n\nresults = find_deprecation_wrappers(my_package)\n\n# Group by issue type (using dataclass attribute access)\nwrong_args = [r for r in results if r.invalid_args]\nidentity_mappings = [r for r in results if r.identity_mapping]\nself_refs = [r for r in results if r.self_reference]\n\nprint(f\"=== Deprecation Validation Report ===\")\nprint(f\"Wrong arguments: {len(wrong_args)}\")\nprint(f\"Identity mappings: {len(identity_mappings)}\")\nprint(f\"Self-references: {len(self_refs)}\")\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eCI/pytest Integration\u003c/b\u003e\u003c/summary\u003e\n\nUse in pytest to validate your package's deprecation wrappers:\n\n```python\nimport warnings\n\nimport pytest\nfrom deprecate import find_deprecation_wrappers\n\n# For testing purposes, we use the test module; normally you would import your own package\nfrom tests import collection_deprecate as my_package\n\n\ndef test_deprecated_wrappers_are_valid():\n    \"\"\"Validate all deprecated wrappers have proper configuration.\"\"\"\n    results = find_deprecation_wrappers(my_package)\n\n    # Collect issues — wrong arg names are errors, identity mappings are worth a warning\n    wrong_args = [r for r in results if r.invalid_args]\n    identity_mappings = [r for r in results if r.identity_mapping]\n\n    # Raise errors for wrong arguments (critical issues)\n    if wrong_args:\n        for r in wrong_args:\n            print(f\"ERROR: {r.module}.{r.function} has invalid args: {r.invalid_args}\")\n        pytest.fail(f\"Found {len(wrong_args)} deprecated wrappers with invalid arguments\")\n\n    # Warn for identity mappings (less severe)\n    for r in identity_mappings:\n        pytest.warns(UserWarning, match=f\"{r.function} has identity mapping\")\n```\n\n\u003c/details\u003e\n\n### ⏰ Enforcing Deprecation Removal Deadlines\n\nWhen you deprecate code with a `remove_in` version, you're making a commitment to remove that code when that version is reached. However, it's easy to forget to actually remove the code—leading to \"zombie code\" that lingers past its scheduled removal.\n\npyDeprecate provides enforcement utilities to detect and prevent zombie code in your CI/CD pipeline:\n\nThe `validate_deprecation_expiry()` utility scans an entire module or package for expired deprecations:\n\n\u003cdetails\u003e\n\u003csummary\u003eExample: scanning a package for expired removal deadlines\u003c/summary\u003e\n\n```python\nfrom deprecate import validate_deprecation_expiry\n\n# For testing purposes, we use the test module; normally you would import your own package\nfrom tests import collection_deprecate as my_package\n\n# Scan your package for expired deprecations - using early-version that won't have expirations\nexpired = validate_deprecation_expiry(my_package, \"0.2\")\nprint(f\"Found {len(expired)} expired\")  # Returns a list of error messages (empty list = no expired)\n\n# Example with expired deprecations found (using later-version)\nexpired = validate_deprecation_expiry(my_package, \"0.5\")\nprint(f\"Found {len(expired)} expired\")\n\n# Auto-detect version from package metadata (mocked for demo)\nfrom unittest.mock import patch\n\nwith patch(\"importlib.metadata.version\", return_value=\"0.3\"):\n    expired = validate_deprecation_expiry(my_package)  # Automatically detects version\n    print(f\"Found {len(expired)} expired\")\n\n# Control recursion\nexpired = validate_deprecation_expiry(my_package, \"0.1\", recursive=False)  # Only scan top-level module\nprint(f\"Found {len(expired)} expired\")\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: expired count per scanned version\u003c/summary\u003e\n\n```\nFound 14 expired\nFound 28 expired\nFound 17 expired\nFound 0 expired\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eCI/pytest Integration for Expiry Enforcement\u003c/b\u003e\u003c/summary\u003e\n\nIntegrate expiry checks into your test suite to catch zombie code automatically:\n\n```python\nimport pytest\nfrom deprecate import validate_deprecation_expiry\n\n# For testing purposes, we use the test module; normally you would import your own package\nfrom tests import collection_deprecate as my_package\n\n\ndef test_no_zombie_deprecations():\n    \"\"\"Ensure all deprecated code is removed when it reaches its deadline.\"\"\"\n    # Use your package's actual version - for this example we use a test version\n    current_version = \"0.5\"  # Replace with: from mypackage import __version__\n\n    expired = validate_deprecation_expiry(my_package, current_version)\n\n    if expired:\n        error_msg = \"Found deprecated code past its removal deadline:\\n\"\n        for msg in expired:\n            error_msg += f\"  - {msg}\\n\"\n        pytest.fail(error_msg)\n\n\n# Alternative: Use a fixture to run on every test session\n# For testing purposes, we use the test module; normally you would import your own package\n@pytest.fixture(scope=\"session\", autouse=True)\ndef enforce_deprecation_deadlines():\n    \"\"\"Automatically check for zombie code before running any tests.\"\"\"\n    from tests import collection_deprecate as my_package\n\n    current_version = \"0.5\"  # Replace with: from mypackage import __version__\n    expired = validate_deprecation_expiry(my_package, current_version)\n    if expired:\n        raise AssertionError(\n            f\"Cannot run tests: {len(expired)} deprecated callables past removal deadline. \"\n            f\"Remove these functions first: {expired}\"\n        )\n```\n\n\u003c/details\u003e\n\n\u003e [!TIP]\n\u003e\n\u003e - Callables without `remove_in` are skipped (warnings-only deprecations are allowed)\n\u003e - Invalid version formats in `remove_in` are silently skipped\n\u003e - PEP 440 versioning is used for comparison (e.g., \"2.0.0\" \u003e \"1.9.5\")\n\u003e - Pre-release versions are handled correctly (e.g., \"1.5.0a1\" \u003c \"1.5.0\")\n\n### 🔗 Detecting Deprecation Chains\n\nWhen refactoring code, it's easy to create \"lazy\" deprecated wrappers that call other deprecated functions instead of calling the new target directly. This creates deprecation chains that defeat the purpose of deprecation.\n\nThe `validate_deprecation_chains()` utility scans a module or package for deprecated functions whose `target` is itself a deprecated callable. Such chains are wasteful: the outer wrapper should point directly to the final (non-deprecated) implementation. Detection is purely metadata-based — no source-code inspection.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eExample: Detecting Both Chain Types\u003c/b\u003e\u003c/summary\u003e\n\n```python\nfrom deprecate import deprecated, validate_deprecation_wrapper, void\n\n\ndef new_power(base: float, exponent: float = 2) -\u003e float:\n    return base**exponent\n\n\n# deprecated forwarder — targets new_power directly\n@deprecated(target=new_power, deprecated_in=\"1.0\", remove_in=\"2.0\")\ndef power_v2(base: float, exponent: float = 2) -\u003e float:\n    void(base, exponent)\n\n\n# self-deprecation — renames old arg \"exp\" -\u003e \"exponent\" within the same function\n@deprecated(True, deprecated_in=\"1.0\", remove_in=\"2.0\", args_mapping={\"exp\": \"exponent\"})\ndef legacy_power(base: float, exp: float = 2, exponent: float = 2) -\u003e float:\n    return base**exponent\n\n\n# BAD: targets power_v2 (another deprecated forwarder) — ChainType.TARGET\n# SOLUTION: point directly to new_power\n@deprecated(target=power_v2, deprecated_in=\"1.5\", remove_in=\"2.5\")\ndef caller_target_chain(base: float, exponent: float = 2) -\u003e float:  # ❌\n    return void(base, exponent)\n\n\n# BAD: targets legacy_power (target=True with arg renaming) — ChainType.STACKED\n# Mappings chain: \"power\" -\u003e \"exp\" -\u003e \"exponent\" — must be composed.\n# SOLUTION: target=new_power, args_mapping={\"power\": \"exponent\"}\n@deprecated(target=legacy_power, deprecated_in=\"1.5\", remove_in=\"2.5\", args_mapping={\"power\": \"exp\"})\ndef caller_stacked_chain(base: float, power: float = 2) -\u003e float:  # ❌\n    return void(base, power)\n\n\n# GOOD: targets final implementation directly with composed mapping\n@deprecated(target=new_power, deprecated_in=\"1.5\", remove_in=\"2.5\", args_mapping={\"power\": \"exponent\"})\ndef caller_direct(base: float, power: float = 2) -\u003e float:  # ✅\n    return void(base, power)\n\n\nfor func in (caller_target_chain, caller_stacked_chain, caller_direct):\n    info = validate_deprecation_wrapper(func)\n    print(f\"{func.__name__}: {info.chain_type}\")\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eOutput: chain types\u003c/summary\u003e\n\n```\ncaller_target_chain: ChainType.TARGET\ncaller_stacked_chain: ChainType.STACKED\ncaller_direct: None\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eCI/pytest Integration for Chain Detection\u003c/b\u003e\u003c/summary\u003e\n\nIntegrate chain detection into your test suite to prevent deprecated-to-deprecated forwarding:\n\n```python\nimport pytest\nfrom deprecate import validate_deprecation_chains\n\n# normally you would import your own package\nfrom tests import collection_chains as my_package\n\n\ndef test_no_deprecation_chains():\n    \"\"\"Ensure no deprecated function targets another deprecated function.\"\"\"\n    issues = validate_deprecation_chains(my_package)\n\n    if issues:\n        lines = [\n            f\"  - {i.function}: target '{getattr(i.deprecated_info['target'], '__name__', repr(i.deprecated_info['target']))}' is deprecated\"\n            for i in issues\n        ]\n        pytest.fail(\"Found deprecation chains:\\n\" + \"\\n\".join(lines))\n\n\n# Alternative: session-scoped auto-use fixture\n@pytest.fixture(scope=\"session\", autouse=True)\ndef enforce_no_deprecation_chains():\n    from tests import collection_chains as my_package\n\n    issues = validate_deprecation_chains(my_package)\n    if issues:\n        raise AssertionError(f\"Found {len(issues)} deprecation chain(s). Fix before running tests.\")\n```\n\n\u003c/details\u003e\n\n\u003e [!TIP]\n\u003e\n\u003e - The function scans all deprecated functions found by `find_deprecation_wrappers()`\n\u003e - Returns `list[DeprecationWrapperInfo]` — each entry has `chain_type` set to a `ChainType` enum value\n\u003e - `ChainType.TARGET` — target is a deprecated callable that forwards to another function; fix by pointing directly to the final target\n\u003e - `ChainType.STACKED` — arg mappings chain through multiple hops and must be composed; two sub-cases:\n\u003e   - Callable target is itself `@deprecated(True, args_mapping=...)` (self-renaming) — mappings compose across hops\n\u003e   - Stacked `@deprecated(True, args_mapping=...)` on the same function — merge into one decorator with combined `args_mapping`\n\u003e - Use `recursive=False` to scan only the top-level module\n\n## 🧪 Testing Deprecated Code\n\npyDeprecate provides utilities to help you test deprecated code properly:\n\n```python\nfrom deprecate import deprecated, assert_no_warnings, void\nimport pytest\n\n\ndef new_func(x: int) -\u003e int:\n    return x * 2\n\n\n@deprecated(target=new_func, deprecated_in=\"1.0\", remove_in=\"2.0\")\ndef old_func(x: int) -\u003e int:\n    pass\n\n\n@deprecated(target=new_func, deprecated_in=\"1.0\", remove_in=\"2.0\")\ndef old_func2(x: int) -\u003e int:\n    return void(x)\n\n\ndef test_deprecated_function_shows_warning():\n    \"\"\"Verify the deprecation warning is shown.\"\"\"\n    with pytest.warns(FutureWarning, match=\"old_func.*deprecated\"):\n        result = old_func(42)\n    assert result == 84\n\n\ndef test_new_function_no_warning():\n    \"\"\"Verify new function doesn't trigger warnings.\"\"\"\n    with assert_no_warnings(FutureWarning):\n        result = new_func(42)\n    assert result == 84\n\n\ndef test_no_warning_after_first_call():\n    \"\"\"By default, warnings are shown only once per function.\"\"\"\n    # First call shows warning\n    with pytest.warns(FutureWarning):\n        old_func2(1)\n\n    # Subsequent calls don't show warning (by default num_warns=1)\n    with assert_no_warnings(FutureWarning):\n        old_func2(2)\n\n\n# call the tests for CI demonstration/validation\ntest_deprecated_function_shows_warning()\ntest_new_function_no_warning()\ntest_no_warning_after_first_call()\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eAdvanced: Control warning frequency\u003c/summary\u003e\n\n```python\n# Minimal replacement implementation used in examples\ndef new_func(x: int) -\u003e int:\n    return x * 2\n\n\n# ---------------------------\n\nfrom deprecate import deprecated\n\n\n# Show warning every time (useful for critical deprecations)\n@deprecated(target=new_func, deprecated_in=\"1.0\", remove_in=\"2.0\", num_warns=-1)\ndef old_func_always_warn(x: int) -\u003e int:\n    pass\n\n\n# Show warning N times total\n@deprecated(target=new_func, deprecated_in=\"1.0\", remove_in=\"2.0\", num_warns=5)\ndef old_func_warn_n_times(x: int) -\u003e int:\n    pass\n```\n\n\u003c/details\u003e\n\n## 🔧 Troubleshooting\n\n### ❗ TypeError: `Cannot apply @deprecated to class`\n\n**Problem:** `TypeError: Cannot apply @deprecated to class 'MyClass'. For class-level deprecation use @deprecated_class() from deprecate.proxy.`\n\n**Cause:** You tried to apply `@deprecated` directly to a class. The `@deprecated` decorator is designed for functions and methods only.\n\n\u003cdetails\u003e\n\u003csummary\u003eSolution\u003c/summary\u003e\n\nUse `@deprecated_class()` for class-level deprecation:\n\n```python\nfrom deprecate import deprecated_class\nfrom enum import Enum\n\n\n# Correct: use @deprecated_class for classes\n@deprecated_class(target=None, deprecated_in=\"1.0\", remove_in=\"2.0\")\nclass MyClass:\n    pass\n\n\n@deprecated_class(target=None, deprecated_in=\"1.0\", remove_in=\"2.0\")\nclass MyEnum(Enum):\n    A = 1\n    B = 2\n\n\n# Alternative: decorate __init__ to warn at instantiation while keeping the class name\nfrom deprecate import deprecated\n\n\nclass MyClass:\n    @deprecated(target=None, deprecated_in=\"1.0\", remove_in=\"2.0\")\n    def __init__(self, x: int) -\u003e None:\n        self.x = x  # body still executes; warning fires on every new MyClass(...)\n```\n\n\u003c/details\u003e\n\n### ❗ TypeError: `Failed mapping`\n\n**Problem:** `TypeError: Failed mapping of 'my_func', arguments missing in target source: ['old_arg']`\n\n**Cause:** Your deprecated function has arguments that the target function doesn't accept.\n\n\u003cdetails\u003e\n\u003csummary\u003eSolutions\u003c/summary\u003e\n\n1. **Skip the argument** (if it's no longer needed):\n\n   ```python\n   # define a target that ignores the extra arg\n   def new_func(required_arg: int, **kwargs) -\u003e int:\n       return required_arg * 2\n\n\n   # ---------------------------\n\n   from deprecate import deprecated\n\n\n   # None means skip this argument\n   @deprecated(target=new_func, args_mapping={\"old_arg\": None})\n   def old_func(old_arg: int, new_arg: int) -\u003e int:\n       pass\n   ```\n\n2. **Rename the argument** (if target uses different name):\n\n   ```python\n   def new_func(new_name: int) -\u003e int:\n       return new_name * 2\n\n\n   # ---------------------------\n\n   from deprecate import deprecated\n\n\n   # Map old to new\n   @deprecated(target=new_func, args_mapping={\"old_name\": \"new_name\"})\n   def old_func(old_name: int) -\u003e int:\n       pass\n   ```\n\n3. **Use target=True for self-deprecation** (deprecate argument of same function):\n\n   ```python\n   from deprecate import deprecated\n\n\n   # Deprecate within same function\n   @deprecated(target=True, args_mapping={\"old_arg\": \"new_arg\"})\n   def my_func(old_arg: int = 0, new_arg: int = 0) -\u003e int:\n       return new_arg * 2\n   ```\n\n\u003c/details\u003e\n\n### ❗ TypeError: `User function 'should_ship' shall return bool`\n\n**Problem:** `TypeError: User function 'should_ship' shall return bool, but got: \u003ctype\u003e`\n\n**Cause:** When using `skip_if` with a callable, the function must return a boolean value.\n\n\u003cdetails\u003e\n\u003csummary\u003eSolution\u003c/summary\u003e\n\n```python\n# Minimal replacement function for examples\ndef new_func() -\u003e str:\n    return \"Hi!\"\n\n\n# ---------------------------\n\nfrom deprecate import deprecated\n\n\n# Correct: function returns bool\ndef should_skip() -\u003e bool:\n    return False  # replace with your condition\n\n\n@deprecated(target=new_func, skip_if=should_skip)\ndef old_func1():\n    pass\n\n\n# Also correct: use a lambda\n@deprecated(target=new_func, skip_if=lambda: False)\ndef old_func2():\n    pass\n```\n\n\u003c/details\u003e\n\n### ⚠️ Warning Not Showing\n\n**Problem:** You don't see the deprecation warning.\n\n**Cause:** By default, warnings are shown **only once per function** (`num_warns=1`) to prevent log spam.\nFor per-argument deprecation (when using `args_mapping` with `target=True`), each deprecated argument\nhas its own warning counter, meaning warnings for different arguments are tracked independently.\n\n\u003cdetails\u003e\n\u003csummary\u003eSolutions\u003c/summary\u003e\n\n```python\n# Minimal replacement function for examples\ndef new_func(x: int) -\u003e int:\n    return x * 2\n\n\n# ---------------------------\n\nfrom deprecate import deprecated\n\n\n# Show warning every time\n@deprecated(target=new_func, num_warns=-1)  # -1 means unlimited\ndef old_func_always_warn():\n    pass\n\n\n# Show warning N times total\n@deprecated(target=new_func, num_warns=5)  # Show 5 times\ndef old_func_warn_n_times():\n    pass\n```\n\n\u003c/details\u003e\n\n### 📦 Deprecation Not Working Across Modules\n\nIf you're moving functions to a different module or package, show the pattern rather than importing a non-existent package in the docs.\n\nThe warning will correctly show the full path for real imports when used in your package.\n\n## 🤝 Contributing\n\nHave you faced this issue in the past or are you facing it now? Do you have good ideas for improvement? All contributions are welcome!\n\nPlease read our [Contributing Guide](.github/CONTRIBUTING.md) for details on how to contribute, and our [Code of Conduct](.github/CODE_OF_CONDUCT.md) for community guidelines.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborda%2Fpydeprecate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fborda%2Fpydeprecate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborda%2Fpydeprecate/lists"}