├── tests ├── __init__.py ├── deprecated_params │ ├── __init__.py │ ├── test_demo_pow2.py │ ├── test_demo_versions.py │ ├── test_demo_area.py │ └── test_demo_paragraph.py ├── test.py ├── test_sphinx_metaclass.py ├── test_deprecated_metaclass.py ├── test_sphinx_adapter.py ├── test_deprecated_class.py ├── test_sphinx_class.py ├── test_deprecated.py └── test_sphinx.py ├── docs ├── source │ ├── contributing.rst │ ├── license.rst │ ├── sphinx │ │ ├── use_calc_mean_deco.py │ │ ├── calc_mean.py │ │ ├── calc_mean_deco.py │ │ └── sphinx_demo.py │ ├── _static │ │ ├── title-page.jpg │ │ ├── deprecated-label.png │ │ └── rusty-tools-background.jpeg │ ├── changelog.rst │ ├── tutorial │ │ ├── v4 │ │ │ ├── using_liberty.py │ │ │ └── liberty.py │ │ ├── v1 │ │ │ ├── using_liberty.py │ │ │ └── liberty.py │ │ ├── v2 │ │ │ ├── using_liberty.py │ │ │ └── liberty.py │ │ ├── v3 │ │ │ ├── using_liberty.py │ │ │ └── liberty.py │ │ ├── v0 │ │ │ └── liberty.py │ │ └── warning_ctrl │ │ │ ├── filter_action_demo.py │ │ │ ├── filter_warnings_demo.py │ │ │ ├── extra_stacklevel_demo.py │ │ │ └── warning_classes_demo.py │ ├── api.rst │ ├── index.rst │ ├── introduction.rst │ ├── sphinx_deco.rst │ ├── installation.rst │ ├── conf.py │ ├── tutorial.rst │ └── white_paper.rst ├── cover │ └── paper_10.jpeg ├── blurb.rst └── requirements.txt ├── setup.cfg ├── .editorconfig ├── MANIFEST.in ├── pyproject.toml ├── deprecated ├── __init__.py ├── params.py ├── classic.py └── sphinx.py ├── .packit.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── workflows │ ├── python-package.yml │ └── codeql-analysis.yml └── CODE_OF_CONDUCT.md ├── .bumpversion.cfg ├── Makefile ├── .readthedocs.yaml ├── LICENSE.rst ├── python-deprecated.spec ├── CHANGELOG.rst ├── tox.ini ├── README.md ├── CHANGELOG-1.1.rst ├── .gitignore ├── CONTRIBUTING.rst ├── setup.py └── CHANGELOG-1.2.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/deprecated_params/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | 4 | .. include:: ../../LICENSE.rst 5 | -------------------------------------------------------------------------------- /docs/source/sphinx/use_calc_mean_deco.py: -------------------------------------------------------------------------------- 1 | from calc_mean_deco import mean 2 | 3 | print(mean.__doc__) 4 | -------------------------------------------------------------------------------- /docs/cover/paper_10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurent-laporte-pro/deprecated/HEAD/docs/cover/paper_10.jpeg -------------------------------------------------------------------------------- /docs/source/_static/title-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurent-laporte-pro/deprecated/HEAD/docs/source/_static/title-page.jpg -------------------------------------------------------------------------------- /docs/source/_static/deprecated-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurent-laporte-pro/deprecated/HEAD/docs/source/_static/deprecated-label.png -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGELOG.rst 2 | .. include:: ../../CHANGELOG-1.2.rst 3 | .. include:: ../../CHANGELOG-1.1.rst 4 | -------------------------------------------------------------------------------- /docs/source/_static/rusty-tools-background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurent-laporte-pro/deprecated/HEAD/docs/source/_static/rusty-tools-background.jpeg -------------------------------------------------------------------------------- /docs/source/tutorial/v4/using_liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import liberty 3 | 4 | obj = liberty.Liberty("Salutation") 5 | obj.print_value() 6 | obj.print_value() 7 | -------------------------------------------------------------------------------- /docs/source/tutorial/v1/using_liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import liberty 3 | 4 | liberty.print_value("hello") 5 | liberty.print_value("hello again") 6 | liberty.better_print("Hi Tom!") 7 | -------------------------------------------------------------------------------- /docs/source/tutorial/v2/using_liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import liberty 3 | 4 | liberty.print_value("hello") 5 | liberty.print_value("hello again") 6 | liberty.better_print("Hi Tom!") 7 | -------------------------------------------------------------------------------- /docs/source/tutorial/v3/using_liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import liberty 3 | 4 | obj = liberty.Liberty("Greeting") 5 | obj.print_value() 6 | obj.print_value() 7 | obj.better_print() 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [tool:pytest] 5 | python_files = test*.py 6 | 7 | [aliases] 8 | release = egg_info -D -b '' sdist bdist_wheel 9 | 10 | [build_sphinx] 11 | source_dir = docs/source 12 | build_dir = dist/docs 13 | -------------------------------------------------------------------------------- /docs/source/tutorial/v0/liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Liberty library is free """ 3 | 4 | import pprint 5 | 6 | 7 | def print_value(value): 8 | """ 9 | Print the value 10 | 11 | :param value: The value to print 12 | """ 13 | pprint.pprint(value) 14 | -------------------------------------------------------------------------------- /docs/source/tutorial/warning_ctrl/filter_action_demo.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from deprecated import deprecated 3 | 4 | 5 | @deprecated(reason="do not call it", action="error") 6 | def foo(): 7 | print("foo") 8 | 9 | 10 | if __name__ == '__main__': 11 | warnings.simplefilter("ignore") 12 | foo() 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{yml,yaml}] 13 | indent_size = 2 14 | 15 | [*.{bat,cmd,ps1}] 16 | end_of_line = crlf 17 | -------------------------------------------------------------------------------- /docs/source/tutorial/warning_ctrl/filter_warnings_demo.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from deprecated import deprecated 3 | 4 | 5 | @deprecated(version='1.2.1', reason="deprecated function") 6 | def fun(): 7 | print("fun") 8 | 9 | 10 | if __name__ == '__main__': 11 | warnings.simplefilter("ignore", category=DeprecationWarning) 12 | fun() 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft deprecated 3 | graft tests 4 | graft .github 5 | 6 | include .bumpversion.cfg 7 | include .coveragerc 8 | include .editorconfig 9 | 10 | include *.rst 11 | include *.spec 12 | include *.yaml 13 | include *.yml 14 | include Makefile 15 | include tox.ini 16 | 17 | global-exclude *.py[cod] __pycache__ *.so *.dylib .DS_Store 18 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API 4 | === 5 | 6 | This part of the documentation covers all the interfaces of the Deprecated Library. 7 | 8 | .. automodule:: deprecated 9 | :members: 10 | 11 | .. automodule:: deprecated.classic 12 | :members: 13 | 14 | .. automodule:: deprecated.params 15 | :members: 16 | 17 | .. automodule:: deprecated.sphinx 18 | :members: 19 | -------------------------------------------------------------------------------- /docs/source/sphinx/calc_mean.py: -------------------------------------------------------------------------------- 1 | def mean(values): 2 | """ 3 | Compute the arithmetic mean (“average”) of values. 4 | 5 | :type values: typing.List[float] 6 | :param values: List of floats 7 | :return: Mean of values. 8 | 9 | .. deprecated:: 2.5.0 10 | Since Python 3.4, you can use the standard function :func:`statistics.mean`. 11 | """ 12 | return sum(values) / len(values) 13 | -------------------------------------------------------------------------------- /docs/source/tutorial/v4/liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Liberty library is free """ 3 | 4 | import pprint 5 | 6 | from deprecated import deprecated 7 | 8 | 9 | @deprecated("This class is not perfect") 10 | class Liberty(object): 11 | def __init__(self, value): 12 | self.value = value 13 | 14 | def print_value(self): 15 | """ Print the value """ 16 | pprint.pprint(self.value) 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | skip-string-normalization = true 4 | target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312', "py313", "py314"] 5 | include = '\.pyi?$' 6 | 7 | [tool.isort] 8 | line_length = 120 9 | force_single_line = true 10 | 11 | [[tool.uv.index]] 12 | name = "testpypi" 13 | url = "https://test.pypi.org/simple/" 14 | publish-url = "https://test.pypi.org/legacy/" 15 | explicit = true 16 | -------------------------------------------------------------------------------- /deprecated/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Deprecated Library 4 | ================== 5 | 6 | Python ``@deprecated`` decorator to deprecate old python classes, functions or methods. 7 | 8 | """ 9 | 10 | __version__ = "1.3.1" 11 | __author__ = u"Laurent LAPORTE " 12 | __date__ = "2025-10-30" 13 | __credits__ = "(c) Laurent LAPORTE" 14 | 15 | from deprecated.classic import deprecated 16 | from deprecated.params import deprecated_params 17 | -------------------------------------------------------------------------------- /docs/source/sphinx/calc_mean_deco.py: -------------------------------------------------------------------------------- 1 | from deprecated.sphinx import deprecated 2 | 3 | 4 | @deprecated( 5 | reason="""Since Python 3.4, you can use the standard function :func:`statistics.mean`.""", 6 | version="2.5.0", 7 | ) 8 | def mean(values): 9 | """ 10 | Compute the arithmetic mean (“average”) of values. 11 | 12 | :type values: typing.List[float] 13 | :param values: List of floats 14 | :return: Mean of values. 15 | """ 16 | return sum(values) / len(values) 17 | -------------------------------------------------------------------------------- /.packit.yml: -------------------------------------------------------------------------------- 1 | files_to_sync: 2 | - python-deprecated.spec 3 | - .packit.yml 4 | upstream_package_name: Deprecated 5 | upstream_tag_template: v{version} 6 | downstream_package_name: python-deprecated 7 | jobs: 8 | - job: propose_downstream 9 | trigger: release 10 | metadata: 11 | dist_git_branches: 12 | - fedora-all 13 | - job: copr_build 14 | trigger: pull_request 15 | metadata: 16 | targets: 17 | - fedora-all 18 | 19 | srpm_build_deps: 20 | - python3-pip 21 | - python3-setuptools_scm 22 | 23 | -------------------------------------------------------------------------------- /docs/blurb.rst: -------------------------------------------------------------------------------- 1 | If you need to mark a function, a class or a method as deprecated, you can use the @deprecated decorator. 2 | This documentation explains how to install and use the Deprecated Library. 3 | It includes a detailed tutorial and simple use cases. 4 | You will also have all the keys to contribute to the source code development. 5 | 6 | Why should you pay for this documentation? This a way to donate to the Deprecated Library project. 7 | Your gratitude and financial help will be motivating to continue the project’s development. 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Describe what this patch does to fix the issue. 2 | 3 | Link to any relevant issues or pull requests. 4 | 5 | 17 | -------------------------------------------------------------------------------- /docs/source/tutorial/v1/liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Liberty library is free """ 3 | 4 | import pprint 5 | 6 | from deprecated import deprecated 7 | 8 | 9 | @deprecated 10 | def print_value(value): 11 | """ 12 | Print the value 13 | 14 | :param value: The value to print 15 | """ 16 | pprint.pprint(value) 17 | 18 | 19 | def better_print(value, printer=None): 20 | """ 21 | Print the value using a *printer*. 22 | 23 | :param value: The value to print 24 | :param printer: Callable used to print the value, by default: :func:`pprint.pprint` 25 | """ 26 | printer = printer or pprint.pprint 27 | printer(value) 28 | -------------------------------------------------------------------------------- /docs/source/tutorial/warning_ctrl/extra_stacklevel_demo.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from deprecated import deprecated 4 | 5 | 6 | @deprecated(version='1.0', extra_stacklevel=1) 7 | class MyObject(object): 8 | def __init__(self, name): 9 | self.name = name 10 | 11 | def __str__(self): 12 | return "object: {name}".format(name=self.name) 13 | 14 | 15 | def create_object(name): 16 | return MyObject(name) 17 | 18 | 19 | if __name__ == '__main__': 20 | warnings.filterwarnings("default", category=DeprecationWarning) 21 | # warn here: 22 | print(create_object("orange")) 23 | # and also here: 24 | print(create_object("banane")) 25 | -------------------------------------------------------------------------------- /docs/source/tutorial/v2/liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Liberty library is free """ 3 | 4 | import pprint 5 | 6 | from deprecated import deprecated 7 | 8 | 9 | @deprecated("This function is rotten, use 'better_print' instead") 10 | def print_value(value): 11 | """ 12 | Print the value 13 | 14 | :param value: The value to print 15 | """ 16 | pprint.pprint(value) 17 | 18 | 19 | def better_print(value, printer=None): 20 | """ 21 | Print the value using a *printer*. 22 | 23 | :param value: The value to print 24 | :param printer: Callable used to print the value, by default: :func:`pprint.pprint` 25 | """ 26 | printer = printer or pprint.pprint 27 | printer(value) 28 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Project dependencies (editable mode) 2 | -e . 3 | 4 | # Sphinx to build the documentation 5 | Sphinx ~= 8.1.3 6 | 7 | # Pinned versions of dependencies to ensure compatibility with Sphinx 8 | alabaster >=0.7.14, <1.1.0 9 | babel >=2.13, <2.17.0 10 | docutils >=0.20, <0.22 11 | imagesize >=1.3, <1.5.0 12 | Jinja2 >=3.1, <3.2.0 13 | packaging >=23.0, <24.3 14 | Pygments >=2.17, <2.20.0 15 | requests >=2.30.0, <2.33.0 16 | snowballstemmer >=2.2, <2.3.0 17 | sphinxcontrib-applehelp >=1.0.7, <2.1.0 18 | sphinxcontrib-devhelp >=1.0.6, <2.1.0 19 | sphinxcontrib-htmlhelp >=2.0.6, <2.2.0 20 | sphinxcontrib-jsmath >=1.0.1, <1.1.0 21 | sphinxcontrib-qthelp >=1.0.6, <2.1.0 22 | sphinxcontrib-serializinghtml >=1.1.9, <2.1.0 23 | -------------------------------------------------------------------------------- /docs/source/tutorial/v3/liberty.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Liberty library is free """ 3 | 4 | import pprint 5 | 6 | from deprecated import deprecated 7 | 8 | 9 | class Liberty(object): 10 | def __init__(self, value): 11 | self.value = value 12 | 13 | @deprecated("This method is rotten, use 'better_print' instead") 14 | def print_value(self): 15 | """ Print the value """ 16 | pprint.pprint(self.value) 17 | 18 | def better_print(self, printer=None): 19 | """ 20 | Print the value using a *printer*. 21 | 22 | :param printer: Callable used to print the value, by default: :func:`pprint.pprint` 23 | """ 24 | printer = printer or pprint.pprint 25 | printer(self.value) 26 | -------------------------------------------------------------------------------- /docs/source/tutorial/warning_ctrl/warning_classes_demo.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from deprecated import deprecated 4 | 5 | 6 | class MyDeprecationWarning(DeprecationWarning): 7 | """ My DeprecationWarning """ 8 | 9 | 10 | class DeprecatedIn26(MyDeprecationWarning): 11 | """ deprecated in 2.6 """ 12 | 13 | 14 | class DeprecatedIn30(MyDeprecationWarning): 15 | """ deprecated in 3.0 """ 16 | 17 | 18 | @deprecated(category=DeprecatedIn26, reason="deprecated function") 19 | def foo(): 20 | print("foo") 21 | 22 | 23 | @deprecated(category=DeprecatedIn30, reason="deprecated function") 24 | def bar(): 25 | print("bar") 26 | 27 | 28 | if __name__ == '__main__': 29 | warnings.filterwarnings("ignore", category=DeprecatedIn30) 30 | foo() 31 | bar() 32 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.3.1 3 | commit = True 4 | tag = False 5 | message = Prepare next version {new_version} (unreleased) 6 | 7 | [bumpversion:file:setup.py] 8 | search = version="{current_version}" 9 | replace = version="{new_version}" 10 | 11 | [bumpversion:file:deprecated/__init__.py] 12 | search = __version__ = "{current_version}" 13 | replace = __version__ = "{new_version}" 14 | 15 | [bumpversion:file:docs/source/conf.py] 16 | search = release = "{current_version}" 17 | replace = release = "{new_version}" 18 | 19 | [bumpversion:file:python-deprecated.spec] 20 | search = Version: {current_version} 21 | replace = Version: {new_version} 22 | 23 | [bumpversion:file:docs/source/_static/rusty-tools-background.svg] 24 | search = id="deprecated-version">v{current_version} 25 | replace = id="deprecated-version">v{new_version} 26 | 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all install-dev test coverage cov test-all tox release-minor release-patch upload-minor upload-patch clean-pyc 2 | 3 | all: test 4 | 5 | install-dev: 6 | pip install -q -e .[dev] 7 | 8 | test: clean-pyc install-dev 9 | pytest tests/ 10 | 11 | coverage: clean-pyc install-dev 12 | pytest --cov-report term-missing --cov-report html --cov=deprecated tests/ 13 | 14 | cov: coverage 15 | 16 | test-all: install-dev 17 | tox 18 | 19 | tox: test-all 20 | 21 | release-minor: 22 | bumpversion minor 23 | python setup.py release 24 | 25 | release-patch: 26 | bumpversion patch 27 | python setup.py release 28 | 29 | upload-minor: release-minor 30 | python setup.py upload 31 | git push origin --tags 32 | 33 | upload-patch: release-patch 34 | python setup.py upload 35 | git push origin --tags 36 | 37 | clean-pyc: 38 | find . -name '*.pyc' -exec rm -f {} + 39 | find . -name '*.pyo' -exec rm -f {} + 40 | find . -name '*~' -exec rm -f {} + 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **This issue tracker is a tool to address bugs in Deprecated itself. 2 | Please use Stack Overflow for general questions about using Deprecated 3 | or issues not related to Deprecated Library.** 4 | 5 | If you'd like to report a bug in Deprecated, fill out the template below. Provide 6 | any extra information that may be useful/related to your problem. 7 | Ideally, create an [Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve), 8 | which helps us understand the problem and helps check that it is not caused by something in your code. 9 | 10 | --- 11 | 12 | ### Expected Behavior 13 | 14 | Tell us what should happen. 15 | 16 | ```python 17 | # Paste a minimal example that causes the problem. 18 | ``` 19 | 20 | ### Actual Behavior 21 | 22 | Tell us what happens instead. 23 | 24 | ```pytb 25 | Paste the full traceback if there was an exception. 26 | ``` 27 | 28 | ### Environment 29 | 30 | * Python version: 31 | * Deprecated version: 32 | -------------------------------------------------------------------------------- /docs/source/sphinx/sphinx_demo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from deprecated.sphinx import deprecated 3 | from deprecated.sphinx import versionadded 4 | from deprecated.sphinx import versionchanged 5 | 6 | 7 | @deprecated( 8 | reason=""" 9 | This is deprecated, really. So you need to use another function. 10 | But I don\'t know which one. 11 | 12 | - The first, 13 | - The second. 14 | 15 | Just guess! 16 | """, 17 | version='0.3.0', 18 | ) 19 | @versionchanged( 20 | reason='Well, I add a new feature in this function. ' 21 | 'It is very useful as you can see in the example below, so try it. ' 22 | 'This is a very very very very very long sentence.', 23 | version='0.2.0', 24 | ) 25 | @versionadded(reason='Here is my new function.', version='0.1.0') 26 | def successor(n): 27 | """ 28 | Calculate the successor of a number. 29 | 30 | :param n: a number 31 | :return: number + 1 32 | """ 33 | return n + 1 34 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import pkg_resources 3 | 4 | import deprecated 5 | 6 | 7 | def test_deprecated_has_docstring(): 8 | # The deprecated package must have a docstring 9 | assert deprecated.__doc__ is not None 10 | assert "Deprecated Library" in deprecated.__doc__ 11 | 12 | 13 | def test_deprecated_has_version(): 14 | # The deprecated package must have a valid version number 15 | assert deprecated.__version__ is not None 16 | version = pkg_resources.parse_version(deprecated.__version__) 17 | 18 | # .. note:: 19 | # 20 | # The classes ``SetuptoolsVersion`` and ``SetuptoolsLegacyVersion`` 21 | # are removed since setuptools >= 39. 22 | # They are replaced by ``Version`` and ``LegacyVersion`` respectively. 23 | # 24 | # To check if the version is good, we now use the solution explained here: 25 | # https://github.com/pypa/setuptools/issues/1299 26 | 27 | assert 'Legacy' not in version.__class__.__name__ 28 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/source/conf.py 16 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 17 | # builder: "dirhtml" 18 | # Fail on all warnings to avoid broken references 19 | # fail_on_warning: true 20 | 21 | # Optionally build your docs in additional formats such as PDF and ePub 22 | # formats: 23 | # - pdf 24 | # - epub 25 | 26 | # Optional but recommended, declare the Python requirements required 27 | # to build your documentation 28 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 29 | python: 30 | install: 31 | - requirements: docs/requirements.txt 32 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Laurent LAPORTE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | pull_request: 8 | branches: ['master', 'main', 'develop', 'release/**', 'hotfix/**'] 9 | 10 | jobs: 11 | pytest: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: [ ubuntu-latest, macos-latest, windows-latest ] 16 | python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] 17 | 18 | runs-on: ${{ matrix.platform }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install pytest setuptools 30 | python -m pip install -e . 31 | - name: Test with pytest 32 | run: | 33 | pytest tests/ 34 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Deprecated 2 | ===================== 3 | 4 | .. image:: _static/rusty-tools-background.svg 5 | :alt: Deprecated: When once-stable features are removed in upcoming releases 6 | 7 | Welcome to Deprecated’s Documentation. 8 | This documentation is divided into different parts. 9 | I recommend that you get started with :ref:`installation` and then head over to the :ref:`tutorial`. 10 | If you'd rather dive into the internals of the Deprecated Library, check out the :ref:`api` documentation. 11 | 12 | 13 | User's Guide 14 | ------------ 15 | 16 | This part of the documentation, which is mostly prose, begins with some 17 | background information about Deprecated, then focuses on step-by-step 18 | instructions for using Deprecated. 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | 23 | installation 24 | introduction 25 | tutorial 26 | sphinx_deco 27 | white_paper 28 | 29 | 30 | API Reference 31 | ------------- 32 | 33 | If you are looking for information on a specific function, class or 34 | method, this part of the documentation is for you. 35 | 36 | .. toctree:: 37 | :maxdepth: 2 38 | 39 | api 40 | 41 | 42 | Additional Notes 43 | ---------------- 44 | 45 | Legal information and changelog are here for the interested. 46 | 47 | .. toctree:: 48 | :maxdepth: 2 49 | 50 | changelog 51 | license 52 | contributing 53 | -------------------------------------------------------------------------------- /python-deprecated.spec: -------------------------------------------------------------------------------- 1 | %global srcname Deprecated 2 | %global pkgname deprecated 3 | 4 | Name: python-%{pkgname} 5 | Version: 1.3.1 6 | Release: 1%{?dist} 7 | Summary: Python decorator to deprecate old python classes, functions or methods 8 | License: MIT 9 | URL: https://github.com/laurent-laporte-pro/%{pkgname} 10 | Source0: %{pypi_source} 11 | BuildArch: noarch 12 | 13 | %description 14 | Python @deprecated decorator to deprecate old python classes, 15 | functions or methods. 16 | 17 | %package -n python3-%{pkgname} 18 | Summary: %{summary} 19 | BuildRequires: python3-devel 20 | BuildRequires: python3-setuptools 21 | %{?python_provide:%python_provide python3-%{pkgname}} 22 | 23 | %description -n python3-%{pkgname} 24 | Python @deprecated decorator to deprecate old python classes, 25 | functions or methods. 26 | 27 | %prep 28 | %autosetup -n %{srcname}-%{version} 29 | rm -rf %{pkgname}.egg-info 30 | 31 | %build 32 | %py3_build 33 | 34 | %install 35 | %py3_install 36 | 37 | %files -n python3-%{pkgname} 38 | %license LICENSE.rst 39 | %doc README.md 40 | %{python3_sitelib}/%{pkgname}/ 41 | %{python3_sitelib}/%{srcname}-*.egg-info/ 42 | 43 | 44 | %changelog 45 | * Fri Jul 26 2019 Petr Hracek - 1.2.6-2 46 | - Fix python3_sitelib issue 47 | 48 | * Fri Jul 26 2019 Petr Hracek - 1.2.6-1 49 | - Initial package 50 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Changelog 1.3.x 3 | =============== 4 | 5 | All notable changes to this project will be documented in this file. 6 | 7 | The format is based on `Keep a Changelog `_ 8 | and this project adheres to `Semantic Versioning `_. 9 | 10 | 11 | v1.3.1 (2025-10-30) 12 | =================== 13 | 14 | Patch release: Packaging fix 15 | 16 | Fixed 17 | ----- 18 | 19 | - Restore missing source distribution (``.tar.gz``) that was not included in v1.3.0. 20 | 21 | 22 | v1.3.0 (2025-10-29) 23 | =================== 24 | 25 | .. note:: 26 | 27 | This release was **yanked** on PyPI due to a missing source distribution (``.tar.gz``). 28 | See issue #94: https://github.com/laurent-laporte-pro/deprecated/issues/94 29 | It has been replaced by version 1.3.1. 30 | 31 | Minor release: Parameters deprecation 32 | 33 | Added 34 | ----- 35 | 36 | - Add compatibility tests and adjustments for Wrapt v2.0. See PR #88 (musicinmybrain). 37 | 38 | - Add experimental `@deprecated_params` decorator to mark function parameters as deprecated at call-time; emits warnings when deprecated parameters are used with optional messages and configurable warning categories. See PR #93. 39 | 40 | Documentation 41 | ------------- 42 | 43 | - Update the Wrapt compatibility matrix to include Python 3.13 and 3.14. See PR #91 44 | 45 | Changed 46 | ------- 47 | 48 | - Limit test coverage collection to the dedicated ``coverage`` tox environment to avoid collecting coverage across all test environments and reduce cross-environment coverage noise. See PR #92. 49 | -------------------------------------------------------------------------------- /tests/deprecated_params/test_demo_pow2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This example shows a function with an unused optional parameter. A warning 4 | message should be emitted if `z` is used (as a positional or keyword parameter). 5 | """ 6 | 7 | import sys 8 | import warnings 9 | 10 | import pytest 11 | 12 | from deprecated.params import deprecated_params 13 | 14 | PY38 = sys.version_info[0:2] >= (3, 8) 15 | 16 | if PY38: 17 | # Positional-Only Arguments are only available for Python >= 3 18 | # On other version, this code raises a SyntaxError exception. 19 | exec ( 20 | """ 21 | @deprecated_params("z") 22 | def pow2(x, y, z=None, /): 23 | return x ** y 24 | """ 25 | ) 26 | 27 | else: 28 | 29 | @deprecated_params("z") 30 | def pow2(x, y, z=None): 31 | return x ** y 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "args, kwargs, expected", 36 | [ 37 | pytest.param((5, 6), {}, [], id="'z' not used: no warnings"), 38 | pytest.param( 39 | (5, 6, 8), 40 | {}, 41 | ["'z' parameter is deprecated"], 42 | id="'z' used in positional params", 43 | ), 44 | pytest.param( 45 | (5, 6), 46 | {"z": 8}, 47 | ["'z' parameter is deprecated"], 48 | id="'z' used in keyword params", 49 | marks=pytest.mark.skipif(PY38, reason="'z' parameter is positional only"), 50 | ), 51 | ], 52 | ) 53 | def test_pow2(args, kwargs, expected): 54 | with warnings.catch_warnings(record=True) as warns: 55 | warnings.simplefilter("always") 56 | pow2(*args, **kwargs) 57 | actual = [str(warn.message) for warn in warns] 58 | assert actual == expected 59 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox configuration file 2 | # ====================== 3 | # 4 | # Tox (https://tox.readthedocs.io/) is a tool for running tests 5 | # in multiple virtualenvs. This configuration file will run the 6 | # test suite on all supported python versions. To use it, "pip install tox" 7 | # and then run "tox" from this directory. 8 | 9 | [tox] 10 | # PyPy configuration (on Linux/OSX): 11 | # - /usr/local/bin/pypy3 -> /opt/pypy3.6-v7.3.0-osx64/bin/pypy3 12 | envlist = 13 | py{37,38}-wrapt{1.10,1.11,1.12,1.13,1.14} 14 | py{39,310}-wrapt{1.10,1.11,1.12,1.13,1.14,1.15,1.16,1.17,2.0} 15 | py{311}-wrapt{1.14,1.15,1.16,1.17,2.0} 16 | py{312}-wrapt{1.16,1.17,2.0} 17 | py{313,314}-wrapt{1.17,2.0} 18 | pypy3 19 | coverage 20 | docs 21 | 22 | [testenv] 23 | commands = pytest tests/ 24 | deps = 25 | py{37,38,39,310,311,312,313,314,py3}: PyTest 26 | wrapt1.10: wrapt ~= 1.10.0 27 | wrapt1.11: wrapt ~= 1.11.0 28 | wrapt1.12: wrapt ~= 1.12.0 29 | wrapt1.13: wrapt ~= 1.13.0 30 | wrapt1.14: wrapt ~= 1.14.0 31 | wrapt1.15: wrapt ~= 1.15.0 32 | wrapt1.16: wrapt ~= 1.16.0 33 | wrapt1.17: wrapt ~= 1.17.0 34 | wrapt2.0: wrapt ~= 2.0.0 35 | setuptools; python_version>="3.12" 36 | 37 | [testenv:coverage] 38 | basepython = python 39 | commands = 40 | pytest --cov-report=term-missing --cov=deprecated --cov-report=html tests/ 41 | deps = 42 | coverage 43 | pytest 44 | pytest-cov 45 | setuptools; python_version>="3.12" 46 | 47 | [testenv:docs] 48 | basepython = python 49 | deps = 50 | -r docs/requirements.txt 51 | commands = 52 | sphinx-build -b html -d {envtmpdir}/doctrees docs/source/ {envtmpdir}/html 53 | sphinx-build -b epub -d {envtmpdir}/doctrees docs/source/ {envtmpdir}/epub 54 | -------------------------------------------------------------------------------- /tests/deprecated_params/test_demo_versions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This example shows a function with an unused optional parameter. A warning 4 | message should be emitted if `z` is used (as a positional or keyword parameter). 5 | """ 6 | import warnings 7 | 8 | from deprecated.params import deprecated_params 9 | 10 | 11 | class V2DeprecationWarning(DeprecationWarning): 12 | pass 13 | 14 | 15 | # noinspection PyUnusedLocal 16 | @deprecated_params( 17 | { 18 | "epsilon": "epsilon is deprecated in version v2", 19 | "start": "start is removed in version v2", 20 | }, 21 | category=V2DeprecationWarning, 22 | ) 23 | @deprecated_params("epsilon", reason="epsilon is deprecated in version v1.1") 24 | def integrate(f, a, b, n=0, epsilon=0.0, start=None): 25 | epsilon = epsilon or (b - a) / n 26 | n = n or int((b - a) / epsilon) 27 | return sum((f(a + (i * epsilon)) + f(a + (i * epsilon) + epsilon)) * epsilon / 2 for i in range(n)) 28 | 29 | 30 | def test_only_one_warning_for_each_parameter(): 31 | """ 32 | This unit test checks that only one warning message is emitted for each deprecated parameter. 33 | 34 | However, we notice that the current implementation generates two warning messages for the `epsilon` parameter. 35 | We should therefore improve the implementation to avoid this. 36 | """ 37 | with warnings.catch_warnings(record=True) as warns: 38 | warnings.simplefilter("always") 39 | integrate(lambda x: x**2, 0, 2, epsilon=0.0012, start=123) 40 | actual = [{"message": str(w.message), "category": w.category} for w in warns] 41 | assert actual == [ 42 | {"category": V2DeprecationWarning, "message": "epsilon is deprecated in version v2"}, 43 | {"category": V2DeprecationWarning, "message": "start is removed in version v2"}, 44 | {"category": DeprecationWarning, "message": "epsilon is deprecated in version v1.1"}, 45 | ] 46 | -------------------------------------------------------------------------------- /tests/deprecated_params/test_demo_area.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This example shows how to implement a function that accepts two positional 4 | arguments or two keyword arguments. A warning message should be emitted 5 | if `x` and `y` are used instead of `width` and `height`. 6 | """ 7 | import warnings 8 | 9 | import pytest 10 | 11 | from deprecated.params import deprecated_params 12 | 13 | 14 | @deprecated_params( 15 | { 16 | "x": "use `width` instead or `x`", 17 | "y": "use `height` instead or `y`", 18 | }, 19 | ) 20 | def area(*args, **kwargs): 21 | def _area_impl(width, height): 22 | return width * height 23 | 24 | if args: 25 | # positional arguments (no checking) 26 | return _area_impl(*args) 27 | elif set(kwargs) == {"width", "height"}: 28 | # nominal case: no warning 29 | return _area_impl(kwargs["width"], kwargs["height"]) 30 | elif set(kwargs) == {"x", "y"}: 31 | # old case: deprecation warning 32 | return _area_impl(kwargs["x"], kwargs["y"]) 33 | else: 34 | raise TypeError("invalid arguments") 35 | 36 | 37 | @pytest.mark.parametrize( 38 | "args, kwargs, expected", 39 | [ 40 | pytest.param((4, 6), {}, [], id="positional arguments: no warning"), 41 | pytest.param((), {"width": 3, "height": 6}, [], id="correct keyword arguments"), 42 | pytest.param( 43 | (), 44 | {"x": 2, "y": 7}, 45 | ['use `width` instead or `x`', 'use `height` instead or `y`'], 46 | id="wrong keyword arguments", 47 | ), 48 | pytest.param( 49 | (), 50 | {"x": 2, "height": 7}, 51 | [], 52 | id="invalid arguments is raised", 53 | marks=pytest.mark.xfail(raises=TypeError, strict=True), 54 | ), 55 | ], 56 | ) 57 | def test_area(args, kwargs, expected): 58 | with warnings.catch_warnings(record=True) as warns: 59 | warnings.simplefilter("always") 60 | area(*args, **kwargs) 61 | actual = [str(warn.message) for warn in warns] 62 | assert actual == expected 63 | -------------------------------------------------------------------------------- /tests/deprecated_params/test_demo_paragraph.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This example shows a function with a keyword-only parameter. A warning 4 | message should be emitted if `color` is used (as a positional or keyword parameter). 5 | """ 6 | import sys 7 | import warnings 8 | import xml.sax.saxutils 9 | 10 | import pytest 11 | 12 | from deprecated.params import deprecated_params 13 | 14 | PY3 = sys.version_info[0] == 3 15 | 16 | if PY3: 17 | # Keyword-Only Arguments are only available for Python >= 3 18 | # On Python 2.7, this code raises a SyntaxError exception. 19 | exec( 20 | ''' 21 | @deprecated_params("color", reason="you should use 'styles' instead of 'color'") 22 | def paragraph(text, *, color=None, styles=None): 23 | """Create a styled HTML paragraphe.""" 24 | styles = styles or {} 25 | if color: 26 | styles['color'] = color 27 | html_styles = " ".join("{k}: {v};".format(k=k, v=v) for k, v in styles.items()) 28 | html_text = xml.sax.saxutils.escape(text) 29 | fmt = '

{html_text}

' 30 | return fmt.format(html_styles=html_styles, html_text=html_text) 31 | ''' 32 | ) 33 | 34 | else: 35 | 36 | @deprecated_params("color", reason="you should use 'styles' instead of 'color'") 37 | def paragraph(text, color=None, styles=None): 38 | """Create a styled HTML paragraphe.""" 39 | styles = styles or {} 40 | if color: 41 | styles['color'] = color 42 | html_styles = " ".join("{k}: {v};".format(k=k, v=v) for k, v in styles.items()) 43 | html_text = xml.sax.saxutils.escape(text) 44 | fmt = '

{html_text}

' 45 | return fmt.format(html_styles=html_styles, html_text=html_text) 46 | 47 | 48 | @pytest.mark.parametrize( 49 | "args, kwargs, expected", 50 | [ 51 | pytest.param(("Hello",), {}, [], id="'color' not used: no warnings"), 52 | pytest.param(("Hello",), {'styles': {'color': 'blue'}}, [], id="regular usage: no warnings"), 53 | pytest.param( 54 | ("Hello",), 55 | {'color': 'blue'}, 56 | ["you should use 'styles' instead of 'color'"], 57 | id="'color' used in keyword-argument", 58 | ), 59 | ], 60 | ) 61 | def test_paragraph(args, kwargs, expected): 62 | with warnings.catch_warnings(record=True) as warns: 63 | warnings.simplefilter("always") 64 | paragraph(*args, **kwargs) 65 | actual = [str(warn.message) for warn in warns] 66 | assert actual == expected 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated Decorator 2 | 3 | Python ``@deprecated`` decorator to deprecate old python classes, functions or methods. 4 | 5 | 6 | [![license](https://img.shields.io/badge/license-MIT-blue?logo=opensourceinitiative&logoColor=white)](https://raw.githubusercontent.com/laurent-laporte-pro/deprecated/master/LICENSE.rst) 7 | [![GitHub release](https://img.shields.io/github/v/release/laurent-laporte-pro/deprecated?logo=github&logoColor=white)](https://github.com/laurent-laporte-pro/deprecated/releases/latest) 8 | [![PyPI](https://img.shields.io/pypi/v/deprecated?logo=pypi&logoColor=white)](https://pypi.org/project/Deprecated/) 9 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/laurent-laporte-pro/deprecated/python-package.yml?logo=github&logoColor=white)](https://github.com/laurent-laporte-pro/deprecated/actions/workflows/python-package.yml) 10 | [![Coveralls branch](https://img.shields.io/coverallsCoverage/github/laurent-laporte-pro/deprecated?logo=coveralls&logoColor=white)](https://coveralls.io/github/laurent-laporte-pro/deprecated?branch=master) 11 | [![Read the Docs (version)](https://img.shields.io/readthedocs/deprecated/latest?logo=readthedocs&logoColor=white) 12 | ](http://deprecated.readthedocs.io/en/latest/?badge=latest) 13 | 14 | ## Installation 15 | 16 | ```shell 17 | pip install Deprecated 18 | ``` 19 | 20 | ## Usage 21 | 22 | To use this, decorate your deprecated function with **@deprecated** decorator: 23 | 24 | ```python 25 | from deprecated import deprecated 26 | 27 | 28 | @deprecated 29 | def some_old_function(x, y): 30 | return x + y 31 | ``` 32 | 33 | You can also decorate a class or a method: 34 | 35 | ```python 36 | from deprecated import deprecated 37 | 38 | 39 | class SomeClass(object): 40 | @deprecated 41 | def some_old_method(self, x, y): 42 | return x + y 43 | 44 | 45 | @deprecated 46 | class SomeOldClass(object): 47 | pass 48 | ``` 49 | 50 | You can give a "reason" message to help the developer to choose another function/class: 51 | 52 | ```python 53 | from deprecated import deprecated 54 | 55 | 56 | @deprecated(reason="use another function") 57 | def some_old_function(x, y): 58 | return x + y 59 | ``` 60 | 61 | ## Authors 62 | 63 | The authors of this library are: 64 | [Marcos CARDOSO](https://github.com/vrcmarcos), and 65 | [Laurent LAPORTE](https://github.com/laurent-laporte-pro). 66 | 67 | The original code was made in [this StackOverflow post](https://stackoverflow.com/questions/2536307) by 68 | [Leandro REGUEIRO](https://stackoverflow.com/users/1336250/leandro-regueiro), 69 | [Patrizio BERTONI](https://stackoverflow.com/users/1315480/patrizio-bertoni), and 70 | [Eric WIESER](https://stackoverflow.com/users/102441/eric). 71 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '22 8 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /deprecated/params.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Parameters deprecation 4 | ====================== 5 | 6 | .. _Tantale's Blog: https://tantale.github.io/ 7 | .. _Deprecated Parameters: https://tantale.github.io/articles/deprecated_params/ 8 | 9 | This module introduces a :class:`deprecated_params` decorator to specify that one (or more) 10 | parameter(s) are deprecated: when the user executes a function with a deprecated parameter, 11 | he will see a warning message in the console. 12 | 13 | The decorator is customizable, the user can specify the deprecated parameter names 14 | and associate to each of them a message providing the reason of the deprecation. 15 | As with the :func:`~deprecated.classic.deprecated` decorator, the user can specify 16 | a version number (using the *version* parameter) and also define the warning message category 17 | (a subclass of :class:`Warning`) and when to display the messages (using the *action* parameter). 18 | 19 | The complete study concerning the implementation of this decorator is available on the `Tantale's blog`_, 20 | on the `Deprecated Parameters`_ page. 21 | """ 22 | import collections 23 | import functools 24 | import warnings 25 | 26 | try: 27 | # noinspection PyPackageRequirements 28 | import inspect2 as inspect 29 | except ImportError: 30 | import inspect 31 | 32 | 33 | class DeprecatedParams(object): 34 | """ 35 | Decorator used to decorate a function which at least one 36 | of the parameters is deprecated. 37 | """ 38 | 39 | def __init__(self, param, reason="", category=DeprecationWarning): 40 | self.messages = {} # type: dict[str, str] 41 | self.category = category 42 | self.populate_messages(param, reason=reason) 43 | 44 | def populate_messages(self, param, reason=""): 45 | if isinstance(param, dict): 46 | self.messages.update(param) 47 | elif isinstance(param, str): 48 | fmt = "'{param}' parameter is deprecated" 49 | reason = reason or fmt.format(param=param) 50 | self.messages[param] = reason 51 | else: 52 | raise TypeError(param) 53 | 54 | def check_params(self, signature, *args, **kwargs): 55 | binding = signature.bind(*args, **kwargs) 56 | bound = collections.OrderedDict(binding.arguments, **binding.kwargs) 57 | return [param for param in bound if param in self.messages] 58 | 59 | def warn_messages(self, messages): 60 | # type: (list[str]) -> None 61 | for message in messages: 62 | warnings.warn(message, category=self.category, stacklevel=3) 63 | 64 | def __call__(self, f): 65 | # type: (callable) -> callable 66 | signature = inspect.signature(f) 67 | 68 | @functools.wraps(f) 69 | def wrapper(*args, **kwargs): 70 | invalid_params = self.check_params(signature, *args, **kwargs) 71 | self.warn_messages([self.messages[param] for param in invalid_params]) 72 | return f(*args, **kwargs) 73 | 74 | return wrapper 75 | 76 | 77 | #: Decorator used to decorate a function which at least one 78 | #: of the parameters is deprecated. 79 | deprecated_params = DeprecatedParams 80 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [Laurent LAPORTE](mailto:laurent.laporte.pro@gmail.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /tests/test_sphinx_metaclass.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import warnings 3 | 4 | import deprecated.sphinx 5 | 6 | 7 | def with_metaclass(meta, *bases): 8 | """Create a base class with a metaclass.""" 9 | 10 | # This requires a bit of explanation: the basic idea is to make a dummy 11 | # metaclass for one level of class instantiation that replaces itself with 12 | # the actual metaclass. 13 | class metaclass(type): 14 | def __new__(cls, name, this_bases, d): 15 | return meta(name, bases, d) 16 | 17 | @classmethod 18 | def __prepare__(cls, name, this_bases): 19 | return meta.__prepare__(name, bases) 20 | 21 | return type.__new__(metaclass, 'temporary_class', (), {}) 22 | 23 | 24 | def test_with_init(): 25 | @deprecated.classic.deprecated 26 | class MyClass(object): 27 | def __init__(self, a, b=5): 28 | self.a = a 29 | self.b = b 30 | 31 | with warnings.catch_warnings(record=True) as warns: 32 | warnings.simplefilter("always") 33 | obj = MyClass("five") 34 | 35 | assert len(warns) == 1 36 | 37 | assert obj.a == "five" 38 | assert obj.b == 5 39 | 40 | 41 | def test_with_new(): 42 | @deprecated.classic.deprecated 43 | class MyClass(object): 44 | def __new__(cls, a, b=5): 45 | obj = super(MyClass, cls).__new__(cls) 46 | obj.c = 3.14 47 | return obj 48 | 49 | def __init__(self, a, b=5): 50 | self.a = a 51 | self.b = b 52 | 53 | with warnings.catch_warnings(record=True) as warns: 54 | warnings.simplefilter("always") 55 | obj = MyClass("five") 56 | 57 | assert len(warns) == 1 58 | 59 | assert obj.a == "five" 60 | assert obj.b == 5 61 | assert obj.c == 3.14 62 | 63 | 64 | def test_with_metaclass(): 65 | class Meta(type): 66 | def __call__(cls, *args, **kwargs): 67 | obj = super(Meta, cls).__call__(*args, **kwargs) 68 | obj.c = 3.14 69 | return obj 70 | 71 | @deprecated.classic.deprecated 72 | class MyClass(with_metaclass(Meta)): 73 | def __init__(self, a, b=5): 74 | self.a = a 75 | self.b = b 76 | 77 | with warnings.catch_warnings(record=True) as warns: 78 | warnings.simplefilter("always") 79 | obj = MyClass("five") 80 | 81 | assert len(warns) == 1 82 | 83 | assert obj.a == "five" 84 | assert obj.b == 5 85 | assert obj.c == 3.14 86 | 87 | 88 | def test_with_singleton_metaclass(): 89 | class Singleton(type): 90 | _instances = {} 91 | 92 | def __call__(cls, *args, **kwargs): 93 | if cls not in cls._instances: 94 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 95 | return cls._instances[cls] 96 | 97 | @deprecated.classic.deprecated 98 | class MyClass(with_metaclass(Singleton)): 99 | def __init__(self, a, b=5): 100 | self.a = a 101 | self.b = b 102 | 103 | with warnings.catch_warnings(record=True) as warns: 104 | warnings.simplefilter("always") 105 | obj1 = MyClass("five") 106 | obj2 = MyClass("six", b=6) 107 | 108 | # __new__ is called only once: 109 | # the instance is constructed only once, 110 | # so we have only one warning. 111 | assert len(warns) == 1 112 | 113 | assert obj1.a == "five" 114 | assert obj1.b == 5 115 | assert obj2 is obj1 116 | -------------------------------------------------------------------------------- /tests/test_deprecated_metaclass.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import warnings 3 | 4 | import deprecated.classic 5 | 6 | 7 | def with_metaclass(meta, *bases): 8 | """Create a base class with a metaclass.""" 9 | 10 | # This requires a bit of explanation: the basic idea is to make a dummy 11 | # metaclass for one level of class instantiation that replaces itself with 12 | # the actual metaclass. 13 | class metaclass(type): 14 | def __new__(cls, name, this_bases, d): 15 | return meta(name, bases, d) 16 | 17 | @classmethod 18 | def __prepare__(cls, name, this_bases): 19 | return meta.__prepare__(name, bases) 20 | 21 | return type.__new__(metaclass, 'temporary_class', (), {}) 22 | 23 | 24 | def test_with_init(): 25 | @deprecated.classic.deprecated 26 | class MyClass(object): 27 | def __init__(self, a, b=5): 28 | self.a = a 29 | self.b = b 30 | 31 | with warnings.catch_warnings(record=True) as warns: 32 | warnings.simplefilter("always") 33 | obj = MyClass("five") 34 | 35 | assert len(warns) == 1 36 | 37 | assert obj.a == "five" 38 | assert obj.b == 5 39 | 40 | 41 | def test_with_new(): 42 | @deprecated.classic.deprecated 43 | class MyClass(object): 44 | def __new__(cls, a, b=5): 45 | obj = super(MyClass, cls).__new__(cls) 46 | obj.c = 3.14 47 | return obj 48 | 49 | def __init__(self, a, b=5): 50 | self.a = a 51 | self.b = b 52 | 53 | with warnings.catch_warnings(record=True) as warns: 54 | warnings.simplefilter("always") 55 | obj = MyClass("five") 56 | 57 | assert len(warns) == 1 58 | 59 | assert obj.a == "five" 60 | assert obj.b == 5 61 | assert obj.c == 3.14 62 | 63 | 64 | def test_with_metaclass(): 65 | class Meta(type): 66 | def __call__(cls, *args, **kwargs): 67 | obj = super(Meta, cls).__call__(*args, **kwargs) 68 | obj.c = 3.14 69 | return obj 70 | 71 | @deprecated.classic.deprecated 72 | class MyClass(with_metaclass(Meta)): 73 | def __init__(self, a, b=5): 74 | self.a = a 75 | self.b = b 76 | 77 | with warnings.catch_warnings(record=True) as warns: 78 | warnings.simplefilter("always") 79 | obj = MyClass("five") 80 | 81 | assert len(warns) == 1 82 | 83 | assert obj.a == "five" 84 | assert obj.b == 5 85 | assert obj.c == 3.14 86 | 87 | 88 | def test_with_singleton_metaclass(): 89 | class Singleton(type): 90 | _instances = {} 91 | 92 | def __call__(cls, *args, **kwargs): 93 | if cls not in cls._instances: 94 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 95 | return cls._instances[cls] 96 | 97 | @deprecated.classic.deprecated 98 | class MyClass(with_metaclass(Singleton)): 99 | def __init__(self, a, b=5): 100 | self.a = a 101 | self.b = b 102 | 103 | with warnings.catch_warnings(record=True) as warns: 104 | warnings.simplefilter("always") 105 | obj1 = MyClass("five") 106 | obj2 = MyClass("six", b=6) 107 | 108 | # __new__ is called only once: 109 | # the instance is constructed only once, 110 | # so we have only one warning. 111 | assert len(warns) == 1 112 | 113 | assert obj1.a == "five" 114 | assert obj1.b == 5 115 | assert obj2 is obj1 116 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | What "Deprecated" Means 5 | ----------------------- 6 | 7 | .. _Deprecated Library: https://pypi.org/project/Deprecated/ 8 | 9 | A function or class is deprecated when it is considered as it is no longer important. It is so unimportant, in fact, that you should no longer use it, since it has been superseded and may cease to exist in the future. 10 | 11 | As a module, a class or function evolves, its API (Application Programming Interface) inevitably changes: functions are renamed for consistency, new and better methods are added, and attributes change. But such changes introduce a problem. You need to keep the old API around until developers make the transition to the new one, but you don't want them to continue programming to the old API. 12 | 13 | The ability to deprecate a class or a function solves the problem. Python Standard Library does not provide a way to express deprecation easily. The Python `Deprecated Library`_ is here to fulfill this lack. 14 | 15 | When to Deprecate 16 | ----------------- 17 | 18 | When you design an API, carefully consider whether it supersedes an old API. If it does, and you wish to encourage developers (users of the API) to migrate to the new API, then deprecate the old API. Valid reasons to deprecate an API include: 19 | 20 | - It is insecure, buggy, or highly inefficient; 21 | - It is going away in a future release, 22 | - It encourages bad coding practices. 23 | 24 | Deprecation is a reasonable choice in all these cases because it preserves "backward compatibility" while encouraging developers to change to the new API. Also, the deprecation comments help developers decide when to move to the new API, and so should briefly mention the technical reasons for deprecation. 25 | 26 | How to Deprecate 27 | ---------------- 28 | 29 | .. _Python warning control: https://docs.python.org/3/library/warnings.html 30 | 31 | The Python Deprecated Library provides a ``@deprecated`` decorator to deprecate a class, method or function. 32 | 33 | Using the decorator causes the Python interpreter to emit a warning at runtime, when an class instance is constructed, or a function is called. The warning is emitted using the `Python warning control`_. Warning messages are normally written to ``sys.stderr``, but their disposition can be changed flexibly, from ignoring all warnings to turning them into exceptions. 34 | 35 | You are strongly recommended to use the ``@deprecated`` decorator with appropriate comments explaining how to use the new API. This ensures developers will have a workable migration path from the old API to the new API. 36 | 37 | **Example:** 38 | 39 | .. code-block:: python 40 | 41 | from deprecated import deprecated 42 | 43 | 44 | @deprecated(version='1.2.0', reason="You should use another function") 45 | def some_old_function(x, y): 46 | return x + y 47 | 48 | 49 | class SomeClass(object): 50 | @deprecated(version='1.3.0', reason="This method is deprecated") 51 | def some_old_method(self, x, y): 52 | return x + y 53 | 54 | 55 | some_old_function(12, 34) 56 | obj = SomeClass() 57 | obj.some_old_method(5, 8) 58 | 59 | When you run this Python script, you will get something like this in the console: 60 | 61 | .. code-block:: bash 62 | 63 | $ pip install Deprecated 64 | $ python hello.py 65 | hello.py:15: DeprecationWarning: Call to deprecated function (or staticmethod) some_old_function. (You should use another function) -- Deprecated since version 1.2.0. 66 | some_old_function(12, 34) 67 | hello.py:17: DeprecationWarning: Call to deprecated method some_old_method. (This method is deprecated) -- Deprecated since version 1.3.0. 68 | obj.some_old_method(5, 8) 69 | -------------------------------------------------------------------------------- /tests/test_sphinx_adapter.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import textwrap 3 | 4 | import pytest 5 | 6 | from deprecated.sphinx import SphinxAdapter 7 | from deprecated.sphinx import deprecated 8 | from deprecated.sphinx import versionadded 9 | from deprecated.sphinx import versionchanged 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "line_length, expected", 14 | [ 15 | ( 16 | 50, 17 | textwrap.dedent( 18 | """ 19 | Description of foo 20 | 21 | :return: nothing 22 | 23 | .. {directive}:: 1.2.3 24 | foo has changed in this version 25 | 26 | bar bar bar bar bar bar bar bar bar bar bar 27 | bar bar bar bar bar bar bar bar bar bar bar 28 | bar 29 | """ 30 | ), 31 | ), 32 | ( 33 | 0, 34 | textwrap.dedent( 35 | """ 36 | Description of foo 37 | 38 | :return: nothing 39 | 40 | .. {directive}:: 1.2.3 41 | foo has changed in this version 42 | 43 | bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar 44 | """ 45 | ), 46 | ), 47 | ], 48 | ids=["wrapped", "long"], 49 | ) 50 | @pytest.mark.parametrize("directive", ["versionchanged", "versionadded", "deprecated"]) 51 | def test_sphinx_adapter(directive, line_length, expected): 52 | lines = [ 53 | "foo has changed in this version", 54 | "", # newline 55 | "bar " * 23, # long line 56 | "", # trailing newline 57 | ] 58 | reason = "\n".join(lines) 59 | adapter = SphinxAdapter(directive, reason=reason, version="1.2.3", line_length=line_length) 60 | 61 | def foo(): 62 | """ 63 | Description of foo 64 | 65 | :return: nothing 66 | """ 67 | 68 | wrapped = adapter.__call__(foo) 69 | expected = expected.format(directive=directive) 70 | assert wrapped.__doc__ == expected 71 | 72 | 73 | @pytest.mark.parametrize("directive", ["versionchanged", "versionadded", "deprecated"]) 74 | def test_sphinx_adapter__empty_docstring(directive): 75 | lines = [ 76 | "foo has changed in this version", 77 | "", # newline 78 | "bar " * 23, # long line 79 | "", # trailing newline 80 | ] 81 | reason = "\n".join(lines) 82 | adapter = SphinxAdapter(directive, reason=reason, version="1.2.3", line_length=50) 83 | 84 | def foo(): 85 | pass 86 | 87 | wrapped = adapter.__call__(foo) 88 | expected = textwrap.dedent( 89 | """ 90 | .. {directive}:: 1.2.3 91 | foo has changed in this version 92 | 93 | bar bar bar bar bar bar bar bar bar bar bar 94 | bar bar bar bar bar bar bar bar bar bar bar 95 | bar 96 | """ 97 | ) 98 | expected = expected.format(directive=directive) 99 | assert wrapped.__doc__ == expected 100 | 101 | 102 | @pytest.mark.parametrize( 103 | "decorator_factory, directive", 104 | [ 105 | (versionadded, "versionadded"), 106 | (versionchanged, "versionchanged"), 107 | (deprecated, "deprecated"), 108 | ], 109 | ) 110 | def test_decorator_accept_line_length(decorator_factory, directive): 111 | reason = "bar " * 30 112 | decorator = decorator_factory(reason=reason, version="1.2.3", line_length=50) 113 | 114 | def foo(): 115 | pass 116 | 117 | foo = decorator(foo) 118 | 119 | expected = textwrap.dedent( 120 | """ 121 | .. {directive}:: 1.2.3 122 | bar bar bar bar bar bar bar bar bar bar bar 123 | bar bar bar bar bar bar bar bar bar bar bar 124 | bar bar bar bar bar bar bar bar 125 | """ 126 | ) 127 | expected = expected.format(directive=directive) 128 | assert foo.__doc__ == expected 129 | -------------------------------------------------------------------------------- /CHANGELOG-1.1.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Changelog 1.1.x and 1.0.x 3 | ========================= 4 | 5 | All notable changes for the 1.1.x and 1.0.x releases. 6 | 7 | The format is based on `Keep a Changelog `_ 8 | and this project adheres to `Semantic Versioning `_. 9 | 10 | v1.1.5 (2019-02-28) 11 | =================== 12 | 13 | Bug fix release 14 | 15 | Fix 16 | --- 17 | 18 | - Fix #6: Use :func:`inspect.isroutine` to check if the wrapped object is a user-defined or built-in function or method. 19 | 20 | Other 21 | ----- 22 | 23 | - Upgrade Tox configuration to add support for Python 3.7. 24 | Also, fix PyTest version for Python 2.7 and 3.4 (limited support). 25 | Remove dependency 'requests[security]': useless to build documentation. 26 | 27 | - Upgrade project configuration (``setup.py``) to add support for Python 3.7. 28 | 29 | 30 | v1.1.4 (2018-11-03) 31 | =================== 32 | 33 | Bug fix release 34 | 35 | Fix 36 | --- 37 | 38 | - Fix #4: Correct the function :func:`~deprecated.deprecated`: 39 | Don't pass arguments to :meth:`object.__new__` (other than *cls*). 40 | 41 | Other 42 | ----- 43 | 44 | - Change the configuration for TravisCI and AppVeyor: 45 | drop configuration for Python **2.6** and **3.3**. 46 | add configuration for Python **3.7**. 47 | 48 | .. note:: 49 | 50 | Deprecated is no more tested with Python **2.6** and **3.3**. 51 | Those Python versions are EOL for some time now and incur incompatibilities 52 | with Continuous Integration tools like TravisCI and AppVeyor. 53 | However, this library should still work perfectly... 54 | 55 | 56 | v1.1.3 (2018-09-03) 57 | =================== 58 | 59 | Bug fix release 60 | 61 | Fix 62 | --- 63 | 64 | - Fix #2: a deprecated class is a class (not a function). Any subclass of a deprecated class is also deprecated. 65 | 66 | 67 | v1.1.2 (2018-08-27) 68 | =================== 69 | 70 | Bug fix release 71 | 72 | Fix 73 | --- 74 | 75 | - Add a ``MANIFEST.in`` file to package additional files like "LICENSE.rst" in the source distribution. 76 | 77 | 78 | v1.1.1 (2018-04-02) 79 | =================== 80 | 81 | Bug fix release 82 | 83 | Fix 84 | --- 85 | 86 | - Minor correction in ``CONTRIBUTING.rst`` for Sphinx builds: add the ``-d`` option to put apart the ``doctrees`` 87 | from the generated documentation and avoid warnings with epub generator. 88 | - Fix in documentation configuration: remove hyphens in ``epub_identifier`` (ISBN number has no hyphens). 89 | - Fix in Tox configuration: set the versions interval of each dependency. 90 | 91 | Other 92 | ----- 93 | 94 | - Change in documentation: improve sentence phrasing in the Tutorial. 95 | - Restore the epub title to "Python Deprecated Library v1.1 Documentation" (required for Lulu.com). 96 | 97 | 98 | v1.1.0 (2017-11-06) 99 | =================== 100 | 101 | Minor release 102 | 103 | Added 104 | ----- 105 | 106 | - Change in :func:`deprecated.deprecated` decorator: you can give a "reason" message 107 | to help the developer choose another class, function or method. 108 | - Add support for Universal Wheel (Python versions 2.6, 2.7, 3.3, 3.4, 3.5, 3.6 and PyPy). 109 | - Add missing ``__doc__`` and ``__version__`` attributes to :mod:`deprecated` module. 110 | - Add an extensive documentation of Deprecated Library. 111 | 112 | Other 113 | ----- 114 | 115 | - Improve `Travis `_ configuration file (compatibility from Python 2.6 to 3.7-dev, and PyPy). 116 | - Add `AppVeyor `_ configuration file. 117 | - Add `Tox `_ configuration file. 118 | - Add `BumpVersion `_ configuration file. 119 | - Improve project settings: add a long description for the project. 120 | Set the **license** and the **development status** in the classifiers property. 121 | - Add the :file:`CONTRIBUTING.rst` file: "How to contribute to Deprecated Library". 122 | 123 | 124 | v1.0.0 (2016-08-30) 125 | =================== 126 | 127 | Major release 128 | 129 | Added 130 | ----- 131 | 132 | - **deprecated**: Created **@deprecated** decorator 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 111 | .pdm.toml 112 | .pdm-python 113 | .pdm-build/ 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | .idea/ 164 | 165 | ### macOS template 166 | # General 167 | .DS_Store 168 | .AppleDouble 169 | .LSOverride 170 | 171 | # Icon must end with two \r 172 | Icon? 173 | ![iI]con[_a-zA-Z0-9] 174 | 175 | # Thumbnails 176 | ._* 177 | 178 | # Files that might appear in the root of a volume 179 | .DocumentRevisions-V100 180 | .fseventsd 181 | .Spotlight-V100 182 | .TemporaryItems 183 | .Trashes 184 | .VolumeIcon.icns 185 | .com.apple.timemachine.donotpresent 186 | 187 | # Directories potentially created on remote AFP share 188 | .AppleDB 189 | .AppleDesktop 190 | Network Trash Folder 191 | Temporary Items 192 | .apdisk 193 | 194 | -------------------------------------------------------------------------------- /tests/test_deprecated_class.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import print_function 3 | 4 | import inspect 5 | import io 6 | import warnings 7 | 8 | import deprecated.classic 9 | 10 | 11 | def test_simple_class_deprecation(): 12 | # stream is used to store the deprecation message for testing 13 | stream = io.StringIO() 14 | 15 | # To deprecate a class, it is better to emit a message when ``__new__`` is called. 16 | # The simplest way is to override the ``__new__``method. 17 | class MyBaseClass(object): 18 | def __new__(cls, *args, **kwargs): 19 | print(u"I am deprecated!", file=stream) 20 | return super(MyBaseClass, cls).__new__(cls, *args, **kwargs) 21 | 22 | # Of course, the subclass will be deprecated too 23 | class MySubClass(MyBaseClass): 24 | pass 25 | 26 | obj = MySubClass() 27 | assert isinstance(obj, MyBaseClass) 28 | assert inspect.isclass(MyBaseClass) 29 | assert stream.getvalue().strip() == u"I am deprecated!" 30 | 31 | 32 | def test_class_deprecation_using_wrapper(): 33 | # stream is used to store the deprecation message for testing 34 | stream = io.StringIO() 35 | 36 | class MyBaseClass(object): 37 | pass 38 | 39 | # To deprecated the class, we use a wrapper function which emits 40 | # the deprecation message and calls ``__new__```. 41 | 42 | original_new = MyBaseClass.__new__ 43 | 44 | def wrapped_new(unused, *args, **kwargs): 45 | print(u"I am deprecated!", file=stream) 46 | return original_new(*args, **kwargs) 47 | 48 | # Like ``__new__``, this wrapper is a class method. 49 | # It is used to patch the original ``__new__``method. 50 | MyBaseClass.__new__ = classmethod(wrapped_new) 51 | 52 | class MySubClass(MyBaseClass): 53 | pass 54 | 55 | obj = MySubClass() 56 | assert isinstance(obj, MyBaseClass) 57 | assert inspect.isclass(MyBaseClass) 58 | assert stream.getvalue().strip() == u"I am deprecated!" 59 | 60 | 61 | def test_class_deprecation_using_a_simple_decorator(): 62 | # stream is used to store the deprecation message for testing 63 | stream = io.StringIO() 64 | 65 | # To deprecated the class, we use a simple decorator 66 | # which patches the original ``__new__`` method. 67 | 68 | def simple_decorator(wrapped_cls): 69 | old_new = wrapped_cls.__new__ 70 | 71 | def wrapped_new(unused, *args, **kwargs): 72 | print(u"I am deprecated!", file=stream) 73 | return old_new(*args, **kwargs) 74 | 75 | wrapped_cls.__new__ = classmethod(wrapped_new) 76 | return wrapped_cls 77 | 78 | @simple_decorator 79 | class MyBaseClass(object): 80 | pass 81 | 82 | class MySubClass(MyBaseClass): 83 | pass 84 | 85 | obj = MySubClass() 86 | assert isinstance(obj, MyBaseClass) 87 | assert inspect.isclass(MyBaseClass) 88 | assert stream.getvalue().strip() == u"I am deprecated!" 89 | 90 | 91 | def test_class_deprecation_using_deprecated_decorator(): 92 | @deprecated.classic.deprecated 93 | class MyBaseClass(object): 94 | pass 95 | 96 | class MySubClass(MyBaseClass): 97 | pass 98 | 99 | with warnings.catch_warnings(record=True) as warns: 100 | warnings.simplefilter("always") 101 | obj = MySubClass() 102 | 103 | assert len(warns) == 1 104 | assert isinstance(obj, MyBaseClass) 105 | assert inspect.isclass(MyBaseClass) 106 | assert issubclass(MySubClass, MyBaseClass) 107 | 108 | 109 | def test_class_respect_global_filter(): 110 | @deprecated.classic.deprecated 111 | class MyBaseClass(object): 112 | pass 113 | 114 | with warnings.catch_warnings(record=True) as warns: 115 | warnings.simplefilter("once") 116 | obj = MyBaseClass() 117 | obj = MyBaseClass() 118 | 119 | assert len(warns) == 1 120 | 121 | 122 | def test_subclass_deprecation_using_deprecated_decorator(): 123 | @deprecated.classic.deprecated 124 | class MyBaseClass(object): 125 | pass 126 | 127 | @deprecated.classic.deprecated 128 | class MySubClass(MyBaseClass): 129 | pass 130 | 131 | with warnings.catch_warnings(record=True) as warns: 132 | warnings.simplefilter("always") 133 | obj = MySubClass() 134 | 135 | assert len(warns) == 2 136 | assert isinstance(obj, MyBaseClass) 137 | assert inspect.isclass(MyBaseClass) 138 | assert issubclass(MySubClass, MyBaseClass) 139 | 140 | 141 | def test_simple_class_deprecation_with_args(): 142 | @deprecated.classic.deprecated('kwargs class') 143 | class MyClass(object): 144 | def __init__(self, arg): 145 | super(MyClass, self).__init__() 146 | self.args = arg 147 | 148 | MyClass(5) 149 | with warnings.catch_warnings(record=True) as warns: 150 | warnings.simplefilter("always") 151 | obj = MyClass(5) 152 | 153 | assert len(warns) == 1 154 | assert isinstance(obj, MyClass) 155 | assert inspect.isclass(MyClass) 156 | -------------------------------------------------------------------------------- /tests/test_sphinx_class.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import print_function 3 | 4 | import inspect 5 | import io 6 | import sys 7 | import warnings 8 | 9 | import pytest 10 | 11 | import deprecated.sphinx 12 | 13 | 14 | def test_class_deprecation_using_a_simple_decorator(): 15 | # stream is used to store the deprecation message for testing 16 | stream = io.StringIO() 17 | 18 | # To deprecated the class, we use a simple decorator 19 | # which patches the original ``__new__`` method. 20 | 21 | def simple_decorator(wrapped_cls): 22 | old_new = wrapped_cls.__new__ 23 | 24 | def wrapped_new(unused, *args, **kwargs): 25 | print(u"I am deprecated!", file=stream) 26 | return old_new(*args, **kwargs) 27 | 28 | wrapped_cls.__new__ = classmethod(wrapped_new) 29 | return wrapped_cls 30 | 31 | @simple_decorator 32 | class MyBaseClass(object): 33 | pass 34 | 35 | class MySubClass(MyBaseClass): 36 | pass 37 | 38 | obj = MySubClass() 39 | assert isinstance(obj, MyBaseClass) 40 | assert inspect.isclass(MyBaseClass) 41 | assert stream.getvalue().strip() == u"I am deprecated!" 42 | 43 | 44 | @pytest.mark.skipif( 45 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 46 | ) 47 | def test_class_deprecation_using_deprecated_decorator(): 48 | @deprecated.sphinx.deprecated(version="7.8.9") 49 | class MyBaseClass(object): 50 | pass 51 | 52 | class MySubClass(MyBaseClass): 53 | pass 54 | 55 | with warnings.catch_warnings(record=True) as warns: 56 | warnings.simplefilter("always") 57 | obj = MySubClass() 58 | 59 | assert len(warns) == 1 60 | assert isinstance(obj, MyBaseClass) 61 | assert inspect.isclass(MyBaseClass) 62 | assert issubclass(MySubClass, MyBaseClass) 63 | 64 | 65 | @pytest.mark.skipif( 66 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 67 | ) 68 | def test_subclass_deprecation_using_deprecated_decorator(): 69 | @deprecated.sphinx.deprecated(version="7.8.9") 70 | class MyBaseClass(object): 71 | pass 72 | 73 | @deprecated.sphinx.deprecated(version="7.8.9") 74 | class MySubClass(MyBaseClass): 75 | pass 76 | 77 | with warnings.catch_warnings(record=True) as warns: 78 | warnings.simplefilter("always") 79 | obj = MySubClass() 80 | 81 | assert len(warns) == 2 82 | assert isinstance(obj, MyBaseClass) 83 | assert inspect.isclass(MyBaseClass) 84 | assert issubclass(MySubClass, MyBaseClass) 85 | 86 | 87 | @pytest.mark.skipif( 88 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 89 | ) 90 | def test_isinstance_versionadded(): 91 | # https://github.com/laurent-laporte-pro/deprecated/issues/48 92 | @deprecated.sphinx.versionadded(version="X.Y", reason="some reason") 93 | class VersionAddedCls: 94 | pass 95 | 96 | @deprecated.sphinx.versionadded(version="X.Y", reason="some reason") 97 | class VersionAddedChildCls(VersionAddedCls): 98 | pass 99 | 100 | instance = VersionAddedChildCls() 101 | assert isinstance(instance, VersionAddedChildCls) 102 | assert isinstance(instance, VersionAddedCls) 103 | 104 | 105 | @pytest.mark.skipif( 106 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 107 | ) 108 | def test_isinstance_versionchanged(): 109 | @deprecated.sphinx.versionchanged(version="X.Y", reason="some reason") 110 | class VersionChangedCls: 111 | pass 112 | 113 | @deprecated.sphinx.versionchanged(version="X.Y", reason="some reason") 114 | class VersionChangedChildCls(VersionChangedCls): 115 | pass 116 | 117 | instance = VersionChangedChildCls() 118 | assert isinstance(instance, VersionChangedChildCls) 119 | assert isinstance(instance, VersionChangedCls) 120 | 121 | 122 | @pytest.mark.skipif( 123 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 124 | ) 125 | def test_isinstance_deprecated(): 126 | @deprecated.sphinx.deprecated(version="X.Y", reason="some reason") 127 | class DeprecatedCls: 128 | pass 129 | 130 | @deprecated.sphinx.deprecated(version="Y.Z", reason="some reason") 131 | class DeprecatedChildCls(DeprecatedCls): 132 | pass 133 | 134 | instance = DeprecatedChildCls() 135 | assert isinstance(instance, DeprecatedChildCls) 136 | assert isinstance(instance, DeprecatedCls) 137 | 138 | 139 | @pytest.mark.skipif( 140 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 141 | ) 142 | def test_isinstance_versionadded_versionchanged(): 143 | @deprecated.sphinx.versionadded(version="X.Y") 144 | @deprecated.sphinx.versionchanged(version="X.Y.Z") 145 | class AddedChangedCls: 146 | pass 147 | 148 | instance = AddedChangedCls() 149 | assert isinstance(instance, AddedChangedCls) 150 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | How to contribute to Deprecated Library 2 | ======================================= 3 | 4 | Thank you for considering contributing to Deprecated! 5 | 6 | Support questions 7 | ----------------- 8 | 9 | Please, don't use the issue tracker for this. Use one of the following 10 | resources for questions about your own code: 11 | 12 | * Ask on `Stack Overflow`_. Search with Google first using: 13 | ``site:stackoverflow.com deprecated decorator {search term, exception message, etc.}`` 14 | 15 | .. _Stack Overflow: https://stackoverflow.com/search?q=python+deprecated+decorator 16 | 17 | Reporting issues 18 | ---------------- 19 | 20 | - Describe what you expected to happen. 21 | - If possible, include a `minimal, complete, and verifiable example`_ to help 22 | us identify the issue. This also helps check that the issue is not with your 23 | own code. 24 | - Describe what actually happened. Include the full traceback if there was an 25 | exception. 26 | - List your Python, Deprecated versions. If possible, check if this 27 | issue is already fixed in the repository. 28 | 29 | .. _minimal, complete, and verifiable example: https://stackoverflow.com/help/mcve 30 | 31 | Submitting patches 32 | ------------------ 33 | 34 | - Include tests if your patch is supposed to solve a bug, and explain 35 | clearly under which circumstances the bug happens. Make sure the test fails 36 | without your patch. 37 | - Try to follow `PEP8`_, but you may ignore the line length limit if following 38 | it would make the code uglier. 39 | 40 | First time setup 41 | ~~~~~~~~~~~~~~~~ 42 | 43 | - Download and install the `latest version of git`_. 44 | - Configure git with your `username`_ and `email`_:: 45 | 46 | git config --global user.name 'your name' 47 | git config --global user.email 'your email' 48 | 49 | - Make sure you have a `GitHub account`_. 50 | - Fork Deprecated to your GitHub account by clicking the `Fork`_ button. 51 | - `Clone`_ your GitHub fork locally:: 52 | 53 | git clone https://github.com/{username}/deprecated.git 54 | cd deprecated 55 | 56 | - Add the main repository as a remote to update later:: 57 | 58 | git remote add upstream https://github.com/laurent-laporte-pro/deprecated.git 59 | git fetch upstream 60 | 61 | - Create a virtualenv:: 62 | 63 | python3 -m venv env 64 | . env/bin/activate 65 | # or "env\Scripts\activate" on Windows 66 | 67 | - Install Deprecated in editable mode with development dependencies:: 68 | 69 | pip install -e ".[dev]" 70 | 71 | .. _GitHub account: https://github.com/join 72 | .. _latest version of git: https://git-scm.com/downloads 73 | .. _username: https://help.github.com/articles/setting-your-username-in-git/ 74 | .. _email: https://help.github.com/articles/setting-your-commit-email-address-in-git/ 75 | .. _Fork: https://github.com/laurent-laporte-pro/deprecated#fork-destination-box 76 | .. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork 77 | 78 | Start coding 79 | ~~~~~~~~~~~~ 80 | 81 | - Create a branch to identify the issue you would like to work on (e.g. 82 | ``2287-dry-test-suite``) 83 | - Using your favorite editor, make your changes, `committing as you go`_. 84 | - Try to follow `PEP8`_, but you may ignore the line length limit if following 85 | it would make the code uglier. 86 | - Include tests that cover any code changes you make. Make sure the test fails 87 | without your patch. `Running the tests`_. 88 | - Push your commits to GitHub and `create a pull request`_. 89 | - Celebrate 🎉 90 | 91 | .. _committing as you go: http://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes 92 | .. _PEP8: https://pep8.org/ 93 | .. _create a pull request: https://help.github.com/articles/creating-a-pull-request/ 94 | 95 | Running the tests 96 | ~~~~~~~~~~~~~~~~~ 97 | 98 | Run the basic test suite with:: 99 | 100 | pytest tests/ 101 | 102 | This only runs the tests for the current environment. Whether this is relevant 103 | depends on which part of Deprecated you're working on. Travis-CI will run the full 104 | suite when you submit your pull request. 105 | 106 | The full test suite takes a long time to run because it tests multiple 107 | combinations of Python and dependencies. You need to have Python 2.7, 108 | 3.4, 3.5, 3.6, PyPy 2.7 and 3.6 installed to run all of the environments (notice 109 | that Python **2.6** and **3.3** are no more supported). Then run:: 110 | 111 | tox 112 | 113 | Running test coverage 114 | ~~~~~~~~~~~~~~~~~~~~~ 115 | 116 | Generating a report of lines that do not have test coverage can indicate 117 | where to start contributing. Run ``pytest`` using ``coverage`` and generate a 118 | report on the terminal and as an interactive HTML document:: 119 | 120 | pytest --cov-report term-missing --cov-report html --cov=deprecated tests/ 121 | # then open htmlcov/index.html 122 | 123 | Read more about `coverage `_. 124 | 125 | Running the full test suite with ``tox`` will combine the coverage reports 126 | from all runs. 127 | 128 | ``make`` targets 129 | ~~~~~~~~~~~~~~~~ 130 | 131 | Deprecated provides a ``Makefile`` with various shortcuts. They will ensure that 132 | all dependencies are installed. 133 | 134 | - ``make test`` runs the basic test suite with ``pytest`` 135 | - ``make cov`` runs the basic test suite with ``coverage`` 136 | - ``make test-all`` runs the full test suite with ``tox`` 137 | 138 | Generating the documentation 139 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 140 | 141 | The documentation is automatically generated with ReadTheDocs for each git push on master. 142 | You can also generate it manually using Sphinx. 143 | 144 | To generate the HTML documentation, run:: 145 | 146 | sphinx-build -b html -d dist/docs/doctrees docs/source/ dist/docs/html/ 147 | 148 | 149 | To generate the epub v2 documentation, run:: 150 | 151 | sphinx-build -b epub -d dist/docs/doctrees docs/source/ dist/docs/epub/ 152 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | u""" 4 | Deprecated Library 5 | ------------------ 6 | 7 | Deprecated is Easy to Use 8 | ````````````````````````` 9 | 10 | If you need to mark a function or a method as deprecated, 11 | you can use the ``@deprecated`` decorator: 12 | 13 | Save in a hello.py: 14 | 15 | .. code:: python 16 | 17 | from deprecated import deprecated 18 | 19 | 20 | @deprecated(version='1.2.1', reason="You should use another function") 21 | def some_old_function(x, y): 22 | return x + y 23 | 24 | 25 | class SomeClass(object): 26 | @deprecated(version='1.3.0', reason="This method is deprecated") 27 | def some_old_method(self, x, y): 28 | return x + y 29 | 30 | 31 | some_old_function(12, 34) 32 | obj = SomeClass() 33 | obj.some_old_method(5, 8) 34 | 35 | 36 | And Easy to Setup 37 | ````````````````` 38 | 39 | And run it: 40 | 41 | .. code:: bash 42 | 43 | $ pip install Deprecated 44 | $ python hello.py 45 | hello.py:15: DeprecationWarning: Call to deprecated function (or staticmethod) some_old_function. 46 | (You should use another function) -- Deprecated since version 1.2.0. 47 | some_old_function(12, 34) 48 | hello.py:17: DeprecationWarning: Call to deprecated method some_old_method. 49 | (This method is deprecated) -- Deprecated since version 1.3.0. 50 | obj.some_old_method(5, 8) 51 | 52 | 53 | You can document your code 54 | `````````````````````````` 55 | 56 | Have you ever wonder how to document that some functions, classes, methods, etc. are deprecated? 57 | This is now possible with the integrated Sphinx directives: 58 | 59 | For instance, in hello_sphinx.py: 60 | 61 | .. code:: python 62 | 63 | from deprecated.sphinx import deprecated 64 | from deprecated.sphinx import versionadded 65 | from deprecated.sphinx import versionchanged 66 | 67 | 68 | @versionadded(version='1.0', reason="This function is new") 69 | def function_one(): 70 | '''This is the function one''' 71 | 72 | 73 | @versionchanged(version='1.0', reason="This function is modified") 74 | def function_two(): 75 | '''This is the function two''' 76 | 77 | 78 | @deprecated(version='1.0', reason="This function will be removed soon") 79 | def function_three(): 80 | '''This is the function three''' 81 | 82 | 83 | function_one() 84 | function_two() 85 | function_three() # warns 86 | 87 | help(function_one) 88 | help(function_two) 89 | help(function_three) 90 | 91 | 92 | The result it immediate 93 | ``````````````````````` 94 | 95 | Run it: 96 | 97 | .. code:: bash 98 | 99 | $ python hello_sphinx.py 100 | 101 | hello_sphinx.py:23: DeprecationWarning: Call to deprecated function (or staticmethod) function_three. 102 | (This function will be removed soon) -- Deprecated since version 1.0. 103 | function_three() # warns 104 | 105 | Help on function function_one in module __main__: 106 | 107 | function_one() 108 | This is the function one 109 | 110 | .. versionadded:: 1.0 111 | This function is new 112 | 113 | Help on function function_two in module __main__: 114 | 115 | function_two() 116 | This is the function two 117 | 118 | .. versionchanged:: 1.0 119 | This function is modified 120 | 121 | Help on function function_three in module __main__: 122 | 123 | function_three() 124 | This is the function three 125 | 126 | .. deprecated:: 1.0 127 | This function will be removed soon 128 | 129 | 130 | Links 131 | ````` 132 | 133 | * `Python package index (PyPi) `_ 134 | * `GitHub website `_ 135 | * `Read The Docs `_ 136 | * `EBook on Lulu.com `_ 137 | * `StackOverFlow Q&A `_ 138 | * `Development version 139 | `_ 140 | 141 | """ 142 | 143 | from setuptools import setup 144 | 145 | setup( 146 | name="Deprecated", 147 | version="1.3.1", 148 | url="https://github.com/laurent-laporte-pro/deprecated", 149 | project_urls={ 150 | "Documentation": "https://deprecated.readthedocs.io/en/latest/", 151 | "Source": "https://github.com/laurent-laporte-pro/deprecated", 152 | "Bug Tracker": "https://github.com/laurent-laporte-pro/deprecated/issues", 153 | }, 154 | license="MIT", 155 | author="Laurent LAPORTE", # since v1.1.0 156 | author_email="laurent.laporte.pro@gmail.com", 157 | description="Python @deprecated decorator to deprecate old python classes, functions or methods.", 158 | long_description=__doc__, 159 | long_description_content_type="text/x-rst", 160 | keywords="deprecate,deprecated,deprecation,warning,warn,decorator", 161 | packages=["deprecated"], 162 | install_requires=["wrapt < 3, >= 1.10", "inspect2; python_version < '3'"], 163 | zip_safe=False, 164 | include_package_data=True, 165 | platforms="any", 166 | classifiers=[ 167 | "Development Status :: 5 - Production/Stable", 168 | "Environment :: Web Environment", 169 | "Intended Audience :: Developers", 170 | "License :: OSI Approved :: MIT License", 171 | "Operating System :: OS Independent", 172 | "Programming Language :: Python", 173 | "Programming Language :: Python :: 2", 174 | "Programming Language :: Python :: 2.7", 175 | "Programming Language :: Python :: 3", 176 | "Programming Language :: Python :: 3.4", 177 | "Programming Language :: Python :: 3.5", 178 | "Programming Language :: Python :: 3.6", 179 | "Programming Language :: Python :: 3.7", 180 | "Programming Language :: Python :: 3.8", 181 | "Programming Language :: Python :: 3.9", 182 | "Programming Language :: Python :: 3.10", 183 | "Programming Language :: Python :: 3.11", 184 | "Programming Language :: Python :: 3.12", 185 | "Programming Language :: Python :: 3.13", 186 | "Programming Language :: Python :: 3.14", 187 | "Topic :: Software Development :: Libraries :: Python Modules", 188 | ], 189 | extras_require={ 190 | "dev": [ 191 | "tox", 192 | "PyTest", 193 | "PyTest-Cov", 194 | "bump2version < 1", 195 | "setuptools; python_version>='3.12'", 196 | ] 197 | }, 198 | python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", 199 | ) 200 | -------------------------------------------------------------------------------- /docs/source/sphinx_deco.rst: -------------------------------------------------------------------------------- 1 | .. _Sphinx: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html 2 | .. _Sphinx directives: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html 3 | .. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html 4 | .. _Markdown: https://www.sphinx-doc.org/en/master/usage/markdown.html 5 | .. _docstring: https://docs.python.org/3/glossary.html#term-docstring 6 | .. _autodoc: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html 7 | 8 | .. _sphinx_deco: 9 | 10 | The "Sphinx" decorators 11 | ======================= 12 | 13 | Overview 14 | -------- 15 | 16 | Developers use the Deprecated library to decorate deprecated functions or classes. This is very practical, 17 | but you know that this library does more: you can also document your source code! How? 18 | It's very simple: instead of using the "classic" decorator, you can use one of the "Sphinx" decorators. 19 | 20 | The "Sphinx" decorators have the same function as the "classic" decorator but also allow you to add 21 | `Sphinx directives`_ in your functions or classes documentation (inside the docstring_). 22 | 23 | .. attention:: 24 | 25 | In Python 3.3 and previous versions, the docstring of a class is immutable [#f1]_, this means that you cannot 26 | use the "Sphinx" decorators. Naturally, this limitation does not exist in Python 3.4 and above. 27 | 28 | What is a Sphinx directive? 29 | --------------------------- 30 | 31 | Sphinx_ is a tool that makes it easy to create intelligent and beautiful documentation. 32 | This tool uses the reStructuredText_ (or Markdown_) syntax to generate the documentation in different formats, 33 | the most common being HTML. Developers generally use this syntax to document the source code of their applications. 34 | 35 | Sphinx_ offers several directives allowing to introduce a text block with a predefined role. 36 | Among all the directives, the ones that interest us are those related to the functions (or classes) 37 | life cycle, namely: ``versionadded``, ``versionchanged`` and ``deprecated``. 38 | 39 | In the following example, the *mean()* function can be documented as follows: 40 | 41 | .. literalinclude:: sphinx/calc_mean.py 42 | 43 | Therefore, the "Sphinx" decorators allow you to add a Sphinx directive to your functions 44 | or classes documentation. In the case of the ``deprecated`` directive, it obviously allows you to emit a 45 | :exc:`DeprecationWarning` warning. 46 | 47 | Using the "Sphinx" decorators 48 | ----------------------------- 49 | 50 | The previous example can be writen using a "Sphinx" decorator: 51 | 52 | .. literalinclude:: sphinx/calc_mean_deco.py 53 | 54 | You can see the generated documentation with this simple call: 55 | 56 | .. literalinclude:: sphinx/use_calc_mean_deco.py 57 | 58 | The documentation of the *mean()* function looks like this: 59 | 60 | .. code-block:: rst 61 | 62 | Compute the arithmetic mean (“average”) of values. 63 | 64 | :type values: typing.List[float] 65 | :param values: List of floats 66 | :return: Mean of values. 67 | 68 | .. deprecated:: 2.5.0 69 | Since Python 3.4, you can use the standard function 70 | :func:`statistics.mean`. 71 | 72 | More elaborate example 73 | ---------------------- 74 | 75 | The Deprecated library offers you 3 decorators: 76 | 77 | - :func:`~deprecated.sphinx.deprecated`: insert a ``deprecated`` directive in docstring, and emit a warning on each call. 78 | - :func:`~deprecated.sphinx.versionadded`: insert a ``versionadded`` directive in docstring, don't emit warning. 79 | - :func:`~deprecated.sphinx.versionchanged`: insert a ``versionchanged`` directive in docstring, don't emit warning. 80 | 81 | The decorators can be combined to reflect the life cycle of a function: 82 | 83 | - When it is added in your API, with the ``@versionadded`` decorator, 84 | - When it has an important change, with the ``@versionchanged`` decorator, 85 | - When it is deprecated, with the ``@deprecated`` decorator. 86 | 87 | The example bellow illustrate this life cycle: 88 | 89 | .. literalinclude:: sphinx/sphinx_demo.py 90 | 91 | To see the result, you can use the builtin function :func:`help` to display a formatted help message 92 | of the *successor()* function. It is something like this: 93 | 94 | .. code-block:: text 95 | 96 | Help on function successor in module __main__: 97 | 98 | successor(n) 99 | Calculate the successor of a number. 100 | 101 | :param n: a number 102 | :return: number + 1 103 | 104 | 105 | .. versionadded:: 0.1.0 106 | Here is my new function. 107 | 108 | 109 | .. versionchanged:: 0.2.0 110 | Well, I add a new feature in this function. It is very useful as 111 | you can see in the example below, so try it. This is a very very 112 | very very very long sentence. 113 | 114 | 115 | .. deprecated:: 0.3.0 116 | This is deprecated, really. So you need to use another function. 117 | But I don't know which one. 118 | 119 | - The first, 120 | - The second. 121 | 122 | Just guess! 123 | 124 | .. note:: Decorators must be writen in reverse order: recent first, older last. 125 | 126 | Building the documentation 127 | -------------------------- 128 | 129 | The easiest way to build your API documentation is to use the autodoc_ plugin. 130 | The directives like ``automodule``, ``autoclass``, ``autofunction`` scan your source code 131 | and generate the documentation from your docstrings. 132 | 133 | Usually, the first thing that we need to do is indicate where the Python package that contains your 134 | source code is in relation to the ``conf.py`` file. 135 | 136 | But here, that will not work! The reason is that your modules must be imported during build: 137 | the Deprecated decorators must be interpreted. 138 | 139 | So, to build the API documentation of your project with Sphinx_ you need to setup a virtualenv, 140 | and install Sphinx, external themes and/or plugins and also your project. 141 | Nowadays, this is the right way to do it. 142 | 143 | For instance, you can configure a documentation building task in your ``tox.ini`` file, for instance: 144 | 145 | .. code-block:: ini 146 | 147 | [testenv:docs] 148 | basepython = python 149 | deps = 150 | sphinx 151 | commands = 152 | sphinx-build -b html -d {envtmpdir}/doctrees docs/source/ {envtmpdir}/html 153 | 154 | .. hint:: 155 | 156 | You can see a sample implementation of Sphinx directives in the demo project 157 | `Deprecated-Demo.Sphinx `_. 158 | 159 | .. rubric:: Footnotes 160 | 161 | .. [#f1] See Issue 12773: `classes should have mutable docstrings `_. 162 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | Python Version 7 | -------------- 8 | 9 | Our project supports Python 2.7 (for historical reasons), Python 3.4 and newer versions, as well as PyPy 2.7 and 10 | PyPy 3.6 and newer. We recommend using the latest version of Python 3 whenever possible. 11 | 12 | Dependencies 13 | ------------ 14 | 15 | This library uses the `Wrapt`_ library as a basis to construct 16 | function wrappers and decorator functions. 17 | 18 | .. _Wrapt: http://wrapt.readthedocs.io/en/latest/ 19 | 20 | The table below shows the compatibility matrix between Python versions and the `wrapt` versions that have been 21 | tested to date. Recent versions are listed first. 22 | 23 | .. list-table:: Compatibility matrix (tested versions) 24 | :header-rows: 1 25 | :widths: 25 9 9 9 9 9 9 9 9 9 26 | 27 | * - Python / wrapt 28 | - 2.0 29 | - 1.17 30 | - 1.16 31 | - 1.15 32 | - 1.14 33 | - 1.13 34 | - 1.12 35 | - 1.11 36 | - 1.10 37 | * - py3.14 38 | - ✓ 39 | - ✓ 40 | - ✗ 41 | - ✗ 42 | - ✗ 43 | - ✗ 44 | - ✗ 45 | - ✗ 46 | - ✗ 47 | * - py3.13 48 | - ✓ 49 | - ✓ 50 | - ✗ 51 | - ✗ 52 | - ✗ 53 | - ✗ 54 | - ✗ 55 | - ✗ 56 | - ✗ 57 | * - py3.12 58 | - ✓ 59 | - ✓ 60 | - ✓ 61 | - ✗ 62 | - ✗ 63 | - ✗ 64 | - ✗ 65 | - ✗ 66 | - ✗ 67 | * - py3.11 68 | - ✓ 69 | - ✓ 70 | - ✓ 71 | - ✓ 72 | - ✓ 73 | - ✗ 74 | - ✗ 75 | - ✗ 76 | - ✗ 77 | * - py3.10 78 | - ✓ 79 | - ✓ 80 | - ✓ 81 | - ✓ 82 | - ✓ 83 | - ✓ 84 | - ✓ 85 | - ✓ 86 | - ✓ 87 | * - py3.9 88 | - ✓ 89 | - ✓ 90 | - ✓ 91 | - ✓ 92 | - ✓ 93 | - ✓ 94 | - ✓ 95 | - ✓ 96 | - ✓ 97 | * - py3.8 98 | - ? 99 | - ? 100 | - ? 101 | - ? 102 | - ✓ 103 | - ✓ 104 | - ✓ 105 | - ✓ 106 | - ✓ 107 | * - py3.7 108 | - ? 109 | - ? 110 | - ? 111 | - ? 112 | - ✓ 113 | - ✓ 114 | - ✓ 115 | - ✓ 116 | - ✓ 117 | 118 | Legend: ✓ = tested and compatible ; ✗ = incompatible, ? = untested but expected to work 119 | 120 | 121 | Development dependencies 122 | ~~~~~~~~~~~~~~~~~~~~~~~~ 123 | 124 | These distributions will not be installed automatically. 125 | You need to install them explicitly with `pip install -e .[dev]`. 126 | 127 | * `pytest`_ is a framework which makes it easy to write small tests, 128 | yet scales to support complex functional testing for applications and libraries… 129 | * `pytest-cov`_ is a `pytest`_ plugin used to produce coverage reports. 130 | * `tox`_ aims to automate and standardize testing in Python. 131 | It is part of a larger vision of easing the packaging, testing and release process of Python software… 132 | * `bump2version`_ is a small command line tool to simplify releasing software 133 | by updating all version strings in your source code by the correct increment. 134 | Also creates commits and tags… 135 | * `sphinx`_ is a tool that makes it easy to create intelligent and beautiful documentation. 136 | 137 | .. _pytest: https://docs.pytest.org/en/latest/ 138 | .. _pytest-cov: http://pytest-cov.readthedocs.io/en/latest/ 139 | .. _tox: https://tox.readthedocs.io/en/latest/ 140 | .. _bump2version: https://github.com/c4urself/bump2version 141 | .. _sphinx: http://www.sphinx-doc.org/en/stable/index.html 142 | 143 | 144 | Virtual environments 145 | -------------------- 146 | 147 | Use a virtual environment to manage the dependencies for your project, both in 148 | development and in production. 149 | 150 | What problem does a virtual environment solve? The more Python projects you 151 | have, the more likely it is that you need to work with different versions of 152 | Python libraries, or even Python itself. Newer versions of libraries for one 153 | project can break compatibility in another project. 154 | 155 | Virtual environments are independent groups of Python libraries, one for each 156 | project. Packages installed for one project will not affect other projects or 157 | the operating system's packages. 158 | 159 | Python 3 comes bundled with the :mod:`venv` module to create virtual 160 | environments. If you're using a modern version of Python, you can continue on 161 | to the next section. 162 | 163 | If you're using Python 2, see :ref:`install-install-virtualenv` first. 164 | 165 | .. _install-create-env: 166 | 167 | Create an environment 168 | ~~~~~~~~~~~~~~~~~~~~~ 169 | 170 | Create a project folder and a :file:`venv` folder within: 171 | 172 | .. code-block:: sh 173 | 174 | mkdir myproject 175 | cd myproject 176 | python3 -m venv venv 177 | 178 | On Windows: 179 | 180 | .. code-block:: bat 181 | 182 | py -3 -m venv venv 183 | 184 | If you needed to install virtualenv because you are on an older version of 185 | Python, use the following command instead: 186 | 187 | .. code-block:: sh 188 | 189 | virtualenv venv 190 | 191 | On Windows: 192 | 193 | .. code-block:: bat 194 | 195 | \Python27\Scripts\virtualenv.exe venv 196 | 197 | Activate the environment 198 | ~~~~~~~~~~~~~~~~~~~~~~~~ 199 | 200 | Before you work on your project, activate the corresponding environment: 201 | 202 | .. code-block:: sh 203 | 204 | . venv/bin/activate 205 | 206 | On Windows: 207 | 208 | .. code-block:: bat 209 | 210 | venv\Scripts\activate 211 | 212 | Your shell prompt will change to show the name of the activated environment. 213 | 214 | Install Deprecated 215 | ------------------------- 216 | 217 | Within the activated environment, use the following command to install Deprecated: 218 | 219 | .. code-block:: sh 220 | 221 | pip install Deprecated 222 | 223 | Living on the edge 224 | ~~~~~~~~~~~~~~~~~~ 225 | 226 | If you want to work with the latest Deprecated code before it's released, install or 227 | update the code from the master branch: 228 | 229 | .. code-block:: sh 230 | 231 | pip install -U https://github.com/laurent-laporte-pro/deprecated/archive/master.tar.gz 232 | 233 | .. _install-install-virtualenv: 234 | 235 | Install virtualenv 236 | ------------------ 237 | 238 | If you are using Python 2, the venv module is not available. Instead, 239 | install `virtualenv`_. 240 | 241 | On Linux, virtualenv is provided by your package manager: 242 | 243 | .. code-block:: sh 244 | 245 | # Debian, Ubuntu 246 | sudo apt-get install python-virtualenv 247 | 248 | # CentOS, Fedora 249 | sudo yum install python-virtualenv 250 | 251 | # Arch 252 | sudo pacman -S python-virtualenv 253 | 254 | If you are on Mac OS X or Windows, download `get-pip.py`_, then: 255 | 256 | .. code-block:: sh 257 | 258 | sudo python2 Downloads/get-pip.py 259 | sudo python2 -m pip install virtualenv 260 | 261 | On Windows, as an administrator: 262 | 263 | .. code-block:: bat 264 | 265 | \Python27\python.exe Downloads\get-pip.py 266 | \Python27\python.exe -m pip install virtualenv 267 | 268 | Now you can continue to :ref:`install-create-env`. 269 | 270 | .. _virtualenv: https://virtualenv.pypa.io/ 271 | .. _get-pip.py: https://bootstrap.pypa.io/get-pip.py 272 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Deprecated Library Documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Jul 19 22:23:11 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.doctest', 37 | 'sphinx.ext.intersphinx', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.githubpages', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # 48 | # source_suffix = ['.rst', '.md'] 49 | source_suffix = '.rst' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'Deprecated' 56 | copyright = '2017, Marcos CARDOSO & Laurent LAPORTE' 57 | author = 'Marcos CARDOSO & Laurent LAPORTE' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The full version, including alpha/beta/rc tags. 64 | release = "1.3.1" 65 | # The short X.Y version. 66 | version = release.rpartition('.')[0] 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = 'en' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | # This patterns also effect to html_static_path and html_extra_path 78 | exclude_patterns = [] 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = 'sphinx' 82 | 83 | # If true, `todo` and `todoList` produce output, else they produce nothing. 84 | todo_include_todos = False 85 | 86 | # -- Options for HTML output ---------------------------------------------- 87 | 88 | # The theme to use for HTML and HTML Help pages. See the documentation for 89 | # a list of builtin themes. 90 | # 91 | html_theme = 'alabaster' 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | # html_theme_options = {} 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | html_static_path = ['_static'] 103 | 104 | # Custom sidebar templates, must be a dictionary that maps document names 105 | # to template names. 106 | # 107 | # This is required for the alabaster theme 108 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 109 | html_sidebars = { 110 | '**': [ 111 | 'about.html', 112 | 'navigation.html', 113 | 'relations.html', # needs 'show_related': True theme option to display 114 | 'searchbox.html', 115 | 'donate.html', 116 | ] 117 | } 118 | 119 | # -- Options for HTMLHelp output ------------------------------------------ 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = 'Deprecated-Doc' 123 | 124 | # -- Options for LaTeX output --------------------------------------------- 125 | 126 | latex_elements = { 127 | # The paper size ('letterpaper' or 'a4paper'). 128 | # 129 | # 'papersize': 'letterpaper', 130 | 131 | # The font size ('10pt', '11pt' or '12pt'). 132 | # 133 | # 'pointsize': '10pt', 134 | 135 | # Additional stuff for the LaTeX preamble. 136 | # 137 | # 'preamble': '', 138 | 139 | # Latex figure (float) alignment 140 | # 141 | # 'figure_align': 'htbp', 142 | } 143 | 144 | # Grouping the document tree into LaTeX files. List of tuples 145 | # (source start file, target name, title, 146 | # author, documentclass [howto, manual, or own class]). 147 | latex_documents = [ 148 | (master_doc, 'Deprecated.tex', 'Deprecated Documentation', 'Marcos CARDOSO and Laurent LAPORTE', 'manual') 149 | ] 150 | 151 | # -- Options for manual page output --------------------------------------- 152 | 153 | # One entry per manual page. List of tuples 154 | # (source start file, name, description, authors, manual section). 155 | man_pages = [(master_doc, 'deprecated', 'Deprecated Documentation', [author], 1)] 156 | 157 | # -- Options for Texinfo output ------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | ( 164 | master_doc, 165 | 'Deprecated', 166 | 'Deprecated Documentation', 167 | author, 168 | 'Deprecated', 169 | 'Python @deprecated decorator to deprecate old python classes, functions or methods.', 170 | 'Miscellaneous', 171 | ) 172 | ] 173 | 174 | # Example configuration for intersphinx: refer to the Python standard library. 175 | intersphinx_mapping = { 176 | 'python': ('https://docs.python.org/3/', None), 177 | 'wrapt': ('https://wrapt.readthedocs.io/en/latest/', None), 178 | 'flask': ('http://flask.pocoo.org/docs/1.0/', None), 179 | 'django': ('https://docs.djangoproject.com/en/2.1/', 'https://docs.djangoproject.com/en/2.1/_objects/'), 180 | } 181 | 182 | # -- Options for EPub output ------------------------------------------- 183 | 184 | epub_basename = project 185 | epub_theme = 'epub' 186 | epub_theme_options = { 187 | # relbar1: If this is true, the relbar1 block is inserted in the epub output, otherwise it is omitted. 188 | 'relbar1': False, 189 | # footer: If this is true, the footer block is inserted in the epub output, otherwise it is omitted. 190 | 'footer': False, 191 | } 192 | epub_title = "Python Deprecated Library v1.2 Documentation" 193 | epub_description = "Python @deprecated decorator to deprecate old python classes, functions or methods." 194 | epub_author = author 195 | epub_contributor = "Original idea from Leandro REGUEIRO, Patrizio BERTONI, Eric WIESER" 196 | epub_language = language or 'en' 197 | epub_publisher = "www.lulu.com" 198 | epub_copyright = copyright 199 | epub_identifier = "9780244627768" 200 | epub_scheme = 'ISBN' 201 | epub_uid = "BookId" # dacd6b24-3909-4358-8527-359be2e25777 202 | epub_cover = ('_static/title-page.jpg', '') 203 | -------------------------------------------------------------------------------- /tests/test_deprecated.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | import sys 4 | import warnings 5 | 6 | import pytest 7 | 8 | import deprecated.classic 9 | 10 | 11 | class MyDeprecationWarning(DeprecationWarning): 12 | pass 13 | 14 | 15 | class WrongStackLevelWarning(DeprecationWarning): 16 | pass 17 | 18 | 19 | _PARAMS = [ 20 | None, 21 | ((), {}), 22 | (('Good reason',), {}), 23 | ((), {'reason': 'Good reason'}), 24 | ((), {'version': '1.2.3'}), 25 | ((), {'action': 'once'}), 26 | ((), {'category': MyDeprecationWarning}), 27 | ((), {'extra_stacklevel': 1, 'category': WrongStackLevelWarning}), 28 | ] 29 | 30 | 31 | @pytest.fixture(scope="module", params=_PARAMS) 32 | def classic_deprecated_function(request): 33 | if request.param is None: 34 | 35 | @deprecated.classic.deprecated 36 | def foo1(): 37 | pass 38 | 39 | return foo1 40 | else: 41 | args, kwargs = request.param 42 | 43 | @deprecated.classic.deprecated(*args, **kwargs) 44 | def foo1(): 45 | pass 46 | 47 | return foo1 48 | 49 | 50 | @pytest.fixture(scope="module", params=_PARAMS) 51 | def classic_deprecated_class(request): 52 | if request.param is None: 53 | 54 | @deprecated.classic.deprecated 55 | class Foo2(object): 56 | pass 57 | 58 | return Foo2 59 | else: 60 | args, kwargs = request.param 61 | 62 | @deprecated.classic.deprecated(*args, **kwargs) 63 | class Foo2(object): 64 | pass 65 | 66 | return Foo2 67 | 68 | 69 | @pytest.fixture(scope="module", params=_PARAMS) 70 | def classic_deprecated_method(request): 71 | if request.param is None: 72 | 73 | class Foo3(object): 74 | @deprecated.classic.deprecated 75 | def foo3(self): 76 | pass 77 | 78 | return Foo3 79 | else: 80 | args, kwargs = request.param 81 | 82 | class Foo3(object): 83 | @deprecated.classic.deprecated(*args, **kwargs) 84 | def foo3(self): 85 | pass 86 | 87 | return Foo3 88 | 89 | 90 | @pytest.fixture(scope="module", params=_PARAMS) 91 | def classic_deprecated_static_method(request): 92 | if request.param is None: 93 | 94 | class Foo4(object): 95 | @staticmethod 96 | @deprecated.classic.deprecated 97 | def foo4(): 98 | pass 99 | 100 | return Foo4.foo4 101 | else: 102 | args, kwargs = request.param 103 | 104 | class Foo4(object): 105 | @staticmethod 106 | @deprecated.classic.deprecated(*args, **kwargs) 107 | def foo4(): 108 | pass 109 | 110 | return Foo4.foo4 111 | 112 | 113 | @pytest.fixture(scope="module", params=_PARAMS) 114 | def classic_deprecated_class_method(request): 115 | if request.param is None: 116 | 117 | class Foo5(object): 118 | @classmethod 119 | @deprecated.classic.deprecated 120 | def foo5(cls): 121 | pass 122 | 123 | return Foo5 124 | else: 125 | args, kwargs = request.param 126 | 127 | class Foo5(object): 128 | @classmethod 129 | @deprecated.classic.deprecated(*args, **kwargs) 130 | def foo5(cls): 131 | pass 132 | 133 | return Foo5 134 | 135 | 136 | # noinspection PyShadowingNames 137 | def test_classic_deprecated_function__warns(classic_deprecated_function): 138 | with warnings.catch_warnings(record=True) as warns: 139 | warnings.simplefilter("always") 140 | classic_deprecated_function() 141 | assert len(warns) == 1 142 | warn = warns[0] 143 | assert issubclass(warn.category, DeprecationWarning) 144 | assert "deprecated function (or staticmethod)" in str(warn.message) 145 | assert warn.filename == __file__ or warn.category is WrongStackLevelWarning, 'Incorrect warning stackLevel' 146 | 147 | 148 | # noinspection PyShadowingNames 149 | def test_classic_deprecated_class__warns(classic_deprecated_class): 150 | with warnings.catch_warnings(record=True) as warns: 151 | warnings.simplefilter("always") 152 | classic_deprecated_class() 153 | assert len(warns) == 1 154 | warn = warns[0] 155 | assert issubclass(warn.category, DeprecationWarning) 156 | assert "deprecated class" in str(warn.message) 157 | assert warn.filename == __file__ or warn.category is WrongStackLevelWarning, 'Incorrect warning stackLevel' 158 | 159 | 160 | # noinspection PyShadowingNames 161 | def test_classic_deprecated_method__warns(classic_deprecated_method): 162 | with warnings.catch_warnings(record=True) as warns: 163 | warnings.simplefilter("always") 164 | obj = classic_deprecated_method() 165 | obj.foo3() 166 | assert len(warns) == 1 167 | warn = warns[0] 168 | assert issubclass(warn.category, DeprecationWarning) 169 | assert "deprecated method" in str(warn.message) 170 | assert warn.filename == __file__ or warn.category is WrongStackLevelWarning, 'Incorrect warning stackLevel' 171 | 172 | 173 | # noinspection PyShadowingNames 174 | def test_classic_deprecated_static_method__warns(classic_deprecated_static_method): 175 | with warnings.catch_warnings(record=True) as warns: 176 | warnings.simplefilter("always") 177 | classic_deprecated_static_method() 178 | assert len(warns) == 1 179 | warn = warns[0] 180 | assert issubclass(warn.category, DeprecationWarning) 181 | assert "deprecated function (or staticmethod)" in str(warn.message) 182 | assert warn.filename == __file__ or warn.category is WrongStackLevelWarning, 'Incorrect warning stackLevel' 183 | 184 | 185 | # noinspection PyShadowingNames 186 | def test_classic_deprecated_class_method__warns(classic_deprecated_class_method): 187 | with warnings.catch_warnings(record=True) as warns: 188 | warnings.simplefilter("always") 189 | cls = classic_deprecated_class_method() 190 | cls.foo5() 191 | assert len(warns) == 1 192 | warn = warns[0] 193 | assert issubclass(warn.category, DeprecationWarning) 194 | if (3, 9) <= sys.version_info < (3, 13): 195 | assert "deprecated class method" in str(warn.message) 196 | else: 197 | assert "deprecated function (or staticmethod)" in str(warn.message) 198 | assert warn.filename == __file__ or warn.category is WrongStackLevelWarning, 'Incorrect warning stackLevel' 199 | 200 | 201 | def test_should_raise_type_error(): 202 | try: 203 | deprecated.classic.deprecated(5) 204 | assert False, "TypeError not raised" 205 | except TypeError: 206 | pass 207 | 208 | 209 | def test_warning_msg_has_reason(): 210 | reason = "Good reason" 211 | 212 | @deprecated.classic.deprecated(reason=reason) 213 | def foo(): 214 | pass 215 | 216 | with warnings.catch_warnings(record=True) as warns: 217 | foo() 218 | warn = warns[0] 219 | assert reason in str(warn.message) 220 | 221 | 222 | def test_warning_msg_has_version(): 223 | version = "1.2.3" 224 | 225 | @deprecated.classic.deprecated(version=version) 226 | def foo(): 227 | pass 228 | 229 | with warnings.catch_warnings(record=True) as warns: 230 | foo() 231 | warn = warns[0] 232 | assert version in str(warn.message) 233 | 234 | 235 | def test_warning_is_ignored(): 236 | @deprecated.classic.deprecated(action='ignore') 237 | def foo(): 238 | pass 239 | 240 | with warnings.catch_warnings(record=True) as warns: 241 | foo() 242 | assert len(warns) == 0 243 | 244 | 245 | def test_specific_warning_cls_is_used(): 246 | @deprecated.classic.deprecated(category=MyDeprecationWarning) 247 | def foo(): 248 | pass 249 | 250 | with warnings.catch_warnings(record=True) as warns: 251 | foo() 252 | warn = warns[0] 253 | assert issubclass(warn.category, MyDeprecationWarning) 254 | 255 | 256 | def test_respect_global_filter(): 257 | @deprecated.classic.deprecated(version='1.2.1', reason="deprecated function") 258 | def fun(): 259 | print("fun") 260 | 261 | warnings.simplefilter("once", category=DeprecationWarning) 262 | 263 | with warnings.catch_warnings(record=True) as warns: 264 | fun() 265 | fun() 266 | assert len(warns) == 1 267 | 268 | 269 | def test_default_stacklevel(): 270 | """ 271 | The objective of this unit test is to ensure that the triggered warning message, 272 | when invoking the 'use_foo' function, correctly indicates the line where the 273 | deprecated 'foo' function is called. 274 | """ 275 | 276 | @deprecated.classic.deprecated 277 | def foo(): 278 | pass 279 | 280 | def use_foo(): 281 | foo() 282 | 283 | with warnings.catch_warnings(record=True) as warns: 284 | warnings.simplefilter("always") 285 | use_foo() 286 | 287 | # Check that the warning path matches the module path 288 | warn = warns[0] 289 | assert warn.filename == __file__ 290 | 291 | # Check that the line number points to the first line inside 'use_foo' 292 | use_foo_lineno = inspect.getsourcelines(use_foo)[1] 293 | assert warn.lineno == use_foo_lineno + 1 294 | 295 | 296 | def test_extra_stacklevel(): 297 | """ 298 | The unit test utilizes an 'extra_stacklevel' of 1 to ensure that the warning message 299 | accurately identifies the caller of the deprecated function. It verifies that when 300 | the 'use_foo' function is called, the warning message correctly indicates the line 301 | where the call to 'use_foo' is made. 302 | """ 303 | 304 | @deprecated.classic.deprecated(extra_stacklevel=1) 305 | def foo(): 306 | pass 307 | 308 | def use_foo(): 309 | foo() 310 | 311 | def demo(): 312 | use_foo() 313 | 314 | with warnings.catch_warnings(record=True) as warns: 315 | warnings.simplefilter("always") 316 | demo() 317 | 318 | # Check that the warning path matches the module path 319 | warn = warns[0] 320 | assert warn.filename == __file__ 321 | 322 | # Check that the line number points to the first line inside 'demo' 323 | demo_lineno = inspect.getsourcelines(demo)[1] 324 | assert warn.lineno == demo_lineno + 1 325 | -------------------------------------------------------------------------------- /deprecated/classic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Classic deprecation warning 4 | =========================== 5 | 6 | Classic ``@deprecated`` decorator to deprecate old python classes, functions or methods. 7 | 8 | .. _The Warnings Filter: https://docs.python.org/3/library/warnings.html#the-warnings-filter 9 | """ 10 | import functools 11 | import inspect 12 | import platform 13 | import warnings 14 | 15 | import wrapt 16 | 17 | try: 18 | # If the C extension for wrapt was compiled and wrapt/_wrappers.pyd exists, then the 19 | # stack level that should be passed to warnings.warn should be 2. However, if using 20 | # a pure python wrapt, an extra stacklevel is required. 21 | import wrapt._wrappers 22 | 23 | _routine_stacklevel = 2 24 | _class_stacklevel = 2 25 | except ImportError: # pragma: no cover 26 | _routine_stacklevel = 3 27 | if platform.python_implementation() == "PyPy": 28 | _class_stacklevel = 2 29 | else: 30 | _class_stacklevel = 3 31 | 32 | string_types = (type(b''), type(u'')) 33 | 34 | 35 | class ClassicAdapter(wrapt.AdapterFactory): 36 | """ 37 | Classic adapter -- *for advanced usage only* 38 | 39 | This adapter is used to get the deprecation message according to the wrapped object type: 40 | class, function, standard method, static method, or class method. 41 | 42 | This is the base class of the :class:`~deprecated.sphinx.SphinxAdapter` class 43 | which is used to update the wrapped object docstring. 44 | 45 | You can also inherit this class to change the deprecation message. 46 | 47 | In the following example, we change the message into "The ... is deprecated.": 48 | 49 | .. code-block:: python 50 | 51 | import inspect 52 | 53 | from deprecated.classic import ClassicAdapter 54 | from deprecated.classic import deprecated 55 | 56 | 57 | class MyClassicAdapter(ClassicAdapter): 58 | def get_deprecated_msg(self, wrapped, instance): 59 | if instance is None: 60 | if inspect.isclass(wrapped): 61 | fmt = "The class {name} is deprecated." 62 | else: 63 | fmt = "The function {name} is deprecated." 64 | else: 65 | if inspect.isclass(instance): 66 | fmt = "The class method {name} is deprecated." 67 | else: 68 | fmt = "The method {name} is deprecated." 69 | if self.reason: 70 | fmt += " ({reason})" 71 | if self.version: 72 | fmt += " -- Deprecated since version {version}." 73 | return fmt.format(name=wrapped.__name__, 74 | reason=self.reason or "", 75 | version=self.version or "") 76 | 77 | Then, you can use your ``MyClassicAdapter`` class like this in your source code: 78 | 79 | .. code-block:: python 80 | 81 | @deprecated(reason="use another function", adapter_cls=MyClassicAdapter) 82 | def some_old_function(x, y): 83 | return x + y 84 | """ 85 | 86 | def __init__(self, reason="", version="", action=None, category=DeprecationWarning, extra_stacklevel=0): 87 | """ 88 | Construct a wrapper adapter. 89 | 90 | :type reason: str 91 | :param reason: 92 | Reason message which documents the deprecation in your library (can be omitted). 93 | 94 | :type version: str 95 | :param version: 96 | Version of your project which deprecates this feature. 97 | If you follow the `Semantic Versioning `_, 98 | the version number has the format "MAJOR.MINOR.PATCH". 99 | 100 | :type action: Literal["default", "error", "ignore", "always", "module", "once"] 101 | :param action: 102 | A warning filter used to activate or not the deprecation warning. 103 | Can be one of "error", "ignore", "always", "default", "module", or "once". 104 | If ``None`` or empty, the global filtering mechanism is used. 105 | See: `The Warnings Filter`_ in the Python documentation. 106 | 107 | :type category: Type[Warning] 108 | :param category: 109 | The warning category to use for the deprecation warning. 110 | By default, the category class is :class:`~DeprecationWarning`, 111 | you can inherit this class to define your own deprecation warning category. 112 | 113 | :type extra_stacklevel: int 114 | :param extra_stacklevel: 115 | Number of additional stack levels to consider instrumentation rather than user code. 116 | With the default value of 0, the warning refers to where the class was instantiated 117 | or the function was called. 118 | 119 | .. versionchanged:: 1.2.15 120 | Add the *extra_stacklevel* parameter. 121 | """ 122 | self.reason = reason or "" 123 | self.version = version or "" 124 | self.action = action 125 | self.category = category 126 | self.extra_stacklevel = extra_stacklevel 127 | super(ClassicAdapter, self).__init__() 128 | 129 | def get_deprecated_msg(self, wrapped, instance): 130 | """ 131 | Get the deprecation warning message for the user. 132 | 133 | :param wrapped: Wrapped class or function. 134 | 135 | :param instance: The object to which the wrapped function was bound when it was called. 136 | 137 | :return: The warning message. 138 | """ 139 | if instance is None: 140 | if inspect.isclass(wrapped): 141 | fmt = "Call to deprecated class {name}." 142 | else: 143 | fmt = "Call to deprecated function (or staticmethod) {name}." 144 | else: 145 | if inspect.isclass(instance): 146 | fmt = "Call to deprecated class method {name}." 147 | else: 148 | fmt = "Call to deprecated method {name}." 149 | if self.reason: 150 | fmt += " ({reason})" 151 | if self.version: 152 | fmt += " -- Deprecated since version {version}." 153 | return fmt.format(name=wrapped.__name__, reason=self.reason or "", version=self.version or "") 154 | 155 | def __call__(self, wrapped): 156 | """ 157 | Decorate your class or function. 158 | 159 | :param wrapped: Wrapped class or function. 160 | 161 | :return: the decorated class or function. 162 | 163 | .. versionchanged:: 1.2.4 164 | Don't pass arguments to :meth:`object.__new__` (other than *cls*). 165 | 166 | .. versionchanged:: 1.2.8 167 | The warning filter is not set if the *action* parameter is ``None`` or empty. 168 | """ 169 | if inspect.isclass(wrapped): 170 | old_new1 = wrapped.__new__ 171 | 172 | def wrapped_cls(cls, *args, **kwargs): 173 | msg = self.get_deprecated_msg(wrapped, None) 174 | stacklevel = _class_stacklevel + self.extra_stacklevel 175 | if self.action: 176 | with warnings.catch_warnings(): 177 | warnings.simplefilter(self.action, self.category) 178 | warnings.warn(msg, category=self.category, stacklevel=stacklevel) 179 | else: 180 | warnings.warn(msg, category=self.category, stacklevel=stacklevel) 181 | if old_new1 is object.__new__: 182 | return old_new1(cls) 183 | # actually, we don't know the real signature of *old_new1* 184 | return old_new1(cls, *args, **kwargs) 185 | 186 | wrapped.__new__ = staticmethod(wrapped_cls) 187 | 188 | elif inspect.isroutine(wrapped): 189 | @wrapt.decorator 190 | def wrapper_function(wrapped_, instance_, args_, kwargs_): 191 | msg = self.get_deprecated_msg(wrapped_, instance_) 192 | stacklevel = _routine_stacklevel + self.extra_stacklevel 193 | if self.action: 194 | with warnings.catch_warnings(): 195 | warnings.simplefilter(self.action, self.category) 196 | warnings.warn(msg, category=self.category, stacklevel=stacklevel) 197 | else: 198 | warnings.warn(msg, category=self.category, stacklevel=stacklevel) 199 | return wrapped_(*args_, **kwargs_) 200 | 201 | return wrapper_function(wrapped) 202 | 203 | else: # pragma: no cover 204 | raise TypeError(repr(type(wrapped))) 205 | 206 | return wrapped 207 | 208 | 209 | def deprecated(*args, **kwargs): 210 | """ 211 | This is a decorator which can be used to mark functions 212 | as deprecated. It will result in a warning being emitted 213 | when the function is used. 214 | 215 | **Classic usage:** 216 | 217 | To use this, decorate your deprecated function with **@deprecated** decorator: 218 | 219 | .. code-block:: python 220 | 221 | from deprecated import deprecated 222 | 223 | 224 | @deprecated 225 | def some_old_function(x, y): 226 | return x + y 227 | 228 | You can also decorate a class or a method: 229 | 230 | .. code-block:: python 231 | 232 | from deprecated import deprecated 233 | 234 | 235 | class SomeClass(object): 236 | @deprecated 237 | def some_old_method(self, x, y): 238 | return x + y 239 | 240 | 241 | @deprecated 242 | class SomeOldClass(object): 243 | pass 244 | 245 | You can give a *reason* message to help the developer to choose another function/class, 246 | and a *version* number to specify the starting version number of the deprecation. 247 | 248 | .. code-block:: python 249 | 250 | from deprecated import deprecated 251 | 252 | 253 | @deprecated(reason="use another function", version='1.2.0') 254 | def some_old_function(x, y): 255 | return x + y 256 | 257 | The *category* keyword argument allow you to specify the deprecation warning class of your choice. 258 | By default, :exc:`DeprecationWarning` is used, but you can choose :exc:`FutureWarning`, 259 | :exc:`PendingDeprecationWarning` or a custom subclass. 260 | 261 | .. code-block:: python 262 | 263 | from deprecated import deprecated 264 | 265 | 266 | @deprecated(category=PendingDeprecationWarning) 267 | def some_old_function(x, y): 268 | return x + y 269 | 270 | The *action* keyword argument allow you to locally change the warning filtering. 271 | *action* can be one of "error", "ignore", "always", "default", "module", or "once". 272 | If ``None``, empty or missing, the global filtering mechanism is used. 273 | See: `The Warnings Filter`_ in the Python documentation. 274 | 275 | .. code-block:: python 276 | 277 | from deprecated import deprecated 278 | 279 | 280 | @deprecated(action="error") 281 | def some_old_function(x, y): 282 | return x + y 283 | 284 | The *extra_stacklevel* keyword argument allows you to specify additional stack levels 285 | to consider instrumentation rather than user code. With the default value of 0, the 286 | warning refers to where the class was instantiated or the function was called. 287 | """ 288 | if args and isinstance(args[0], string_types): 289 | kwargs['reason'] = args[0] 290 | args = args[1:] 291 | 292 | if args and not callable(args[0]): 293 | raise TypeError(repr(type(args[0]))) 294 | 295 | if args: 296 | adapter_cls = kwargs.pop('adapter_cls', ClassicAdapter) 297 | adapter = adapter_cls(**kwargs) 298 | wrapped = args[0] 299 | return adapter(wrapped) 300 | 301 | return functools.partial(deprecated, **kwargs) 302 | -------------------------------------------------------------------------------- /CHANGELOG-1.2.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Changelog 1.2.x 3 | =============== 4 | 5 | All notable changes for the 1.2.x releases. 6 | 7 | The format is based on `Keep a Changelog `_ 8 | and this project adheres to `Semantic Versioning `_. 9 | 10 | .. note:: 11 | 12 | The library **"Python-Deprecated"** was renamed **"Deprecated"**, simply! 13 | This project is more consistent because now, the name of the library is the same as the name of the Python package. 14 | 15 | - In your ``setup.py``, you can replace the "Python-Deprecated" dependency with "Deprecated". 16 | - In your source code, nothing has changed, you will always use ``import deprecated``, as before. 17 | - I decided to keep the same version number because there is really no change in the source code 18 | (only in comment or documentation). 19 | 20 | v1.2.18 (2024-01-25) 21 | ==================== 22 | 23 | This version does not bring any change in the source code, but fixes the build anomaly on Fedora (Packit). 24 | 25 | The package must be published on PyPi using `twine `_ to correctly deal with the license file. 26 | 27 | 28 | v1.2.17 (2024-01-25) 29 | ==================== 30 | 31 | Bug fix release 32 | 33 | 34 | v1.2.16 (2025-01-24) 35 | ==================== 36 | 37 | Bug fix release 38 | 39 | Fix 40 | --- 41 | 42 | - Fix #78: Fix configuration for Packit 1.0.0 43 | 44 | - Fix #79: Fix the configuration for the intersphinx mapping in the Sphinx documentation. 45 | See `How to link to other documentation projects with Intersphinx `_. 46 | 47 | Other 48 | ----- 49 | 50 | - Drop support for Python older than 3.7 in GitHub Actions. 51 | 52 | 53 | v1.2.15 (2024-11-15) 54 | ==================== 55 | 56 | Bug fix release 57 | 58 | Fix 59 | --- 60 | 61 | - Resolve Python 2.7 support issue introduced in v1.2.14 in ``sphinx.py``. 62 | 63 | - Fix #69: Add ``extra_stacklevel`` argument for interoperating with other wrapper functions (refer to #68 for a concrete use case). 64 | 65 | - Fix #73: Update class method deprecation warnings for Python 3.13. 66 | 67 | - Fix #75: Update GitHub workflows and fix development dependencies for Python 3.12. 68 | 69 | Other 70 | ----- 71 | 72 | - Fix #66: discontinue TravisCI and AppVeyor due to end of free support. 73 | 74 | 75 | v1.2.14 (2023-05-27) 76 | ==================== 77 | 78 | Bug fix release 79 | 80 | Fix 81 | --- 82 | 83 | - Fix #60: return a correctly dedented docstring when long docstring are using the D212 or D213 format. 84 | 85 | Other 86 | ----- 87 | 88 | - Add support for Python 3.11. 89 | 90 | - Drop support for Python older than 3.7 in build systems like pytest and tox, 91 | while ensuring the library remains production-compatible. 92 | 93 | - Update GitHub workflow to run in recent Python versions. 94 | 95 | 96 | v1.2.13 (2021-09-05) 97 | ==================== 98 | 99 | Bug fix release 100 | 101 | Fix 102 | --- 103 | 104 | - Fix #45: Change the signature of the :func:`~deprecated.sphinx.deprecated` decorator to reflect 105 | the valid use cases. 106 | 107 | - Fix #48: Fix ``versionadded`` and ``versionchanged`` decorators: do not return a decorator factory, 108 | but a Wrapt adapter. 109 | 110 | Other 111 | ----- 112 | 113 | - Fix configuration for AppVeyor: simplify the test scripts and set the version format to match the current version. 114 | 115 | - Change configuration for Tox: 116 | 117 | + change the requirements for ``pip`` to "pip >= 9.0.3, < 21" (Python 2.7, 3.4 and 3.5). 118 | + install ``typing`` when building on Python 3.4 (required by Pytest->Attrs). 119 | + run unit tests on Wrapt 1.13 (release candidate). 120 | 121 | - Migrating project to `travis-ci.com `_. 122 | 123 | 124 | v1.2.12 (2021-03-13) 125 | ==================== 126 | 127 | Bug fix release 128 | 129 | Fix 130 | --- 131 | 132 | - Avoid "Explicit markup ends without a blank line" when the decorated function has no docstring. 133 | 134 | - Fix #40: 'version' argument is required in Sphinx directives. 135 | 136 | - Fix #41: :mod:`deprecated.sphinx`: strip Sphinx cross-referencing syntax from warning message. 137 | 138 | 139 | Other 140 | ----- 141 | 142 | - Change in Tox and Travis CI configurations: enable unit testing on Python 3.10. 143 | 144 | 145 | v1.2.11 (2021-01-17) 146 | ==================== 147 | 148 | Bug fix release 149 | 150 | Fix 151 | --- 152 | 153 | - Fix packit configuration: use ``upstream_tag_template: v{version}``. 154 | 155 | - Fix #33: Change the class :class:`~deprecated.sphinx.SphinxAdapter`: 156 | add the ``line_length`` keyword argument to the constructor to specify the max line length of the directive text. 157 | Sphinx decorators also accept the ``line_length`` argument. 158 | 159 | - Fix #34: ``versionadded`` and ``versionchanged`` decorators don't emit ``DeprecationWarning`` 160 | anymore on decorated classes. 161 | 162 | 163 | Other 164 | ----- 165 | 166 | - Change the Tox configuration to run tests on Python 2.7, Python 3.4 and above (and PyPy 2.7 & 3.6). 167 | 168 | - Update the classifiers in ``setup.py``. 169 | 170 | - Replace ``bumpversion`` by `bump2version `_ in ``setup.py`` and documentation. 171 | 172 | - Update configuration for Black and iSort. 173 | 174 | - Fix the development requirement versions in ``setup.py`` for Python 2.7 EOL. 175 | 176 | 177 | v1.2.10 (2020-05-13) 178 | ==================== 179 | 180 | Bug fix release 181 | 182 | Fix 183 | --- 184 | 185 | - Fix #25: ``@deprecated`` respects global warning filters with actions other than "ignore" and "always" on Python 3. 186 | 187 | Other 188 | ----- 189 | 190 | - Change the configuration for TravisCI to build on pypy and pypy3. 191 | 192 | - Change the configuration for TravisCI and AppVeyor: drop configuration for Python **3.4** and add **3.8**. 193 | 194 | 195 | v1.2.9 (2020-04-10) 196 | =================== 197 | 198 | Bug fix release 199 | 200 | Fix 201 | --- 202 | 203 | - Fix #20: Set the :func:`warnings.warn` stacklevel to 2 if the Python implementation is `PyPy `_. 204 | 205 | - Fix packit configuration: use ``dist-git-branch: fedora-all``. 206 | 207 | Other 208 | ----- 209 | 210 | - Change the Tox configuration to run tests on PyPy v2.7 and 3.6. 211 | 212 | 213 | v1.2.8 (2020-04-05) 214 | =================== 215 | 216 | Bug fix release 217 | 218 | Fix 219 | --- 220 | 221 | - Fix #15: The ``@deprecated`` decorator doesn't set a warning filter if the *action* keyword argument is 222 | not provided or ``None``. In consequences, the warning messages are only emitted if the global filter allow it. 223 | For more information, see `The Warning Filter `_ 224 | in the Python documentation. 225 | 226 | - Fix #13: Warning displays the correct filename and line number when decorating a class if wrapt 227 | does not have the compiled c extension. 228 | 229 | Documentation 230 | ------------- 231 | 232 | - The :ref:`api` documentation and the :ref:`tutorial` is improved to explain how to use 233 | custom warning categories and local filtering (warning filtering at function call). 234 | 235 | - Fix #17: Customize the sidebar to add links to the documentation to the source in GitHub and to the Bug tracker. 236 | Add a logo in the sidebar and change the logo in the main page to see the library version. 237 | 238 | - Add a detailed documentation about :ref:`sphinx_deco`. 239 | 240 | 241 | Other 242 | ----- 243 | 244 | - Change the Tox configuration to test the library with Wrapt 1.12.x. 245 | 246 | 247 | v1.2.7 (2019-11-11) 248 | =================== 249 | 250 | Bug fix release 251 | 252 | Fix 253 | --- 254 | 255 | - Fix #13: Warning displays the correct filename and line number when decorating a function if wrapt 256 | does not have the compiled c extension. 257 | 258 | Other 259 | ----- 260 | 261 | - Support packit for Pull Request tests and sync to Fedora (thanks to Petr Hráček). 262 | Supported since v1.2.6. 263 | 264 | - Add `Black `_ configuration file. 265 | 266 | 267 | v1.2.6 (2019-07-06) 268 | =================== 269 | 270 | Bug fix release 271 | 272 | Fix 273 | --- 274 | 275 | - Fix #9: Change the project's configuration: reinforce the constraint to the Wrapt requirement. 276 | 277 | Other 278 | ----- 279 | 280 | - Upgrade project configuration (``setup.py``) to add the *project_urls* property: 281 | Documentation, Source and Bug Tracker URLs. 282 | 283 | - Change the Tox configuration to test the library against different Wrapt versions. 284 | 285 | - Fix an issue with the AppVeyor build: upgrade setuptools version in ``appveyor.yml``, 286 | change the Tox configuration: set ``py27,py34,py35: pip >= 9.0.3, < 19.2``. 287 | 288 | 289 | v1.2.5 (2019-02-28) 290 | =================== 291 | 292 | Bug fix release 293 | 294 | Fix 295 | --- 296 | 297 | - Fix #6: Use :func:`inspect.isroutine` to check if the wrapped object is a user-defined or built-in function or method. 298 | 299 | Other 300 | ----- 301 | 302 | - Upgrade Tox configuration to add support for Python 3.7. 303 | Also, fix PyTest version for Python 2.7 and 3.4 (limited support). 304 | Remove dependency 'requests[security]': useless to build documentation. 305 | 306 | - Upgrade project configuration (``setup.py``) to add support for Python 3.7. 307 | 308 | 309 | v1.2.4 (2018-11-03) 310 | =================== 311 | 312 | Bug fix release 313 | 314 | Fix 315 | --- 316 | 317 | - Fix #4: Correct the class :class:`~deprecated.classic.ClassicAdapter`: 318 | Don't pass arguments to :meth:`object.__new__` (other than *cls*). 319 | 320 | Other 321 | ----- 322 | 323 | - Add missing docstring to the classes :class:`~deprecated.classic.ClassicAdapter` 324 | and :class:`~deprecated.sphinx.SphinxAdapter`. 325 | 326 | - Change the configuration for TravisCI and AppVeyor: 327 | drop configuration for Python **2.6** and **3.3**. 328 | add configuration for Python **3.7** (if available). 329 | 330 | .. note:: 331 | 332 | Deprecated is no more tested with Python **2.6** and **3.3**. 333 | Those Python versions are EOL for some time now and incur incompatibilities 334 | with Continuous Integration tools like TravisCI and AppVeyor. 335 | However, this library should still work perfectly... 336 | 337 | 338 | v1.2.3 (2018-09-12) 339 | =================== 340 | 341 | Bug fix release 342 | 343 | Fix 344 | --- 345 | 346 | - Fix #3: ``deprecated.sphinx`` decorators don't update the docstring. 347 | 348 | 349 | v1.2.2 (2018-09-04) 350 | =================== 351 | 352 | Bug fix release 353 | 354 | Fix 355 | --- 356 | 357 | - Fix #2: a deprecated class is a class (not a function). Any subclass of a deprecated class is also deprecated. 358 | 359 | - Minor fix: add missing documentation in :mod:`deprecated.sphinx` module. 360 | 361 | 362 | v1.2.1 (2018-08-27) 363 | =================== 364 | 365 | Bug fix release 366 | 367 | Fix 368 | --- 369 | 370 | - Add a ``MANIFEST.in`` file to package additional files like "LICENSE.rst" in the source distribution. 371 | 372 | 373 | v1.2.0 (2018-04-02) 374 | =================== 375 | 376 | Minor release 377 | 378 | Added 379 | ----- 380 | 381 | - Add decorators for Sphinx directive integration: ``versionadded``, ``versionchanged``, ``deprecated``. 382 | That way, the developer can document the changes. 383 | 384 | Changed 385 | ------- 386 | 387 | - Add the ``version`` parameter to the ``@deprecated`` decorator: 388 | used to specify the starting version number of the deprecation. 389 | - Add a way to choose a ``DeprecationWarning`` subclass. 390 | 391 | Removed 392 | ------- 393 | 394 | - Deprecated no longer supports Python **2.6** and **3.3**. Those Python versions 395 | are EOL for some time now and incur maintenance and compatibility costs on 396 | the Deprecated core team, and following up with the rest of the community we 397 | decided that they will no longer be supported starting on this version. Users 398 | which still require those versions should pin Deprecated to ``< 1.2``. 399 | -------------------------------------------------------------------------------- /deprecated/sphinx.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Sphinx directive integration 4 | ============================ 5 | 6 | We usually need to document the life-cycle of functions and classes: 7 | when they are created, modified or deprecated. 8 | 9 | To do that, `Sphinx `_ has a set 10 | of `Paragraph-level markups `_: 11 | 12 | - ``versionadded``: to document the version of the project which added the described feature to the library, 13 | - ``versionchanged``: to document changes of a feature, 14 | - ``deprecated``: to document a deprecated feature. 15 | 16 | The purpose of this module is to defined decorators which adds this Sphinx directives 17 | to the docstring of your function and classes. 18 | 19 | Of course, the ``@deprecated`` decorator will emit a deprecation warning 20 | when the function/method is called or the class is constructed. 21 | """ 22 | import re 23 | import textwrap 24 | 25 | from deprecated.classic import ClassicAdapter 26 | from deprecated.classic import deprecated as _classic_deprecated 27 | 28 | 29 | class SphinxAdapter(ClassicAdapter): 30 | """ 31 | Sphinx adapter -- *for advanced usage only* 32 | 33 | This adapter override the :class:`~deprecated.classic.ClassicAdapter` 34 | in order to add the Sphinx directives to the end of the function/class docstring. 35 | Such a directive is a `Paragraph-level markup `_ 36 | 37 | - The directive can be one of "versionadded", "versionchanged" or "deprecated". 38 | - The version number is added if provided. 39 | - The reason message is obviously added in the directive block if not empty. 40 | """ 41 | 42 | def __init__( 43 | self, 44 | directive, 45 | reason="", 46 | version="", 47 | action=None, 48 | category=DeprecationWarning, 49 | extra_stacklevel=0, 50 | line_length=70, 51 | ): 52 | """ 53 | Construct a wrapper adapter. 54 | 55 | :type directive: str 56 | :param directive: 57 | Sphinx directive: can be one of "versionadded", "versionchanged" or "deprecated". 58 | 59 | :type reason: str 60 | :param reason: 61 | Reason message which documents the deprecation in your library (can be omitted). 62 | 63 | :type version: str 64 | :param version: 65 | Version of your project which deprecates this feature. 66 | If you follow the `Semantic Versioning `_, 67 | the version number has the format "MAJOR.MINOR.PATCH". 68 | 69 | :type action: Literal["default", "error", "ignore", "always", "module", "once"] 70 | :param action: 71 | A warning filter used to activate or not the deprecation warning. 72 | Can be one of "error", "ignore", "always", "default", "module", or "once". 73 | If ``None`` or empty, the global filtering mechanism is used. 74 | See: `The Warnings Filter`_ in the Python documentation. 75 | 76 | :type category: Type[Warning] 77 | :param category: 78 | The warning category to use for the deprecation warning. 79 | By default, the category class is :class:`~DeprecationWarning`, 80 | you can inherit this class to define your own deprecation warning category. 81 | 82 | :type extra_stacklevel: int 83 | :param extra_stacklevel: 84 | Number of additional stack levels to consider instrumentation rather than user code. 85 | With the default value of 0, the warning refers to where the class was instantiated 86 | or the function was called. 87 | 88 | :type line_length: int 89 | :param line_length: 90 | Max line length of the directive text. If non nul, a long text is wrapped in several lines. 91 | 92 | .. versionchanged:: 1.2.15 93 | Add the *extra_stacklevel* parameter. 94 | """ 95 | if not version: 96 | # https://github.com/laurent-laporte-pro/deprecated/issues/40 97 | raise ValueError("'version' argument is required in Sphinx directives") 98 | self.directive = directive 99 | self.line_length = line_length 100 | super(SphinxAdapter, self).__init__( 101 | reason=reason, version=version, action=action, category=category, extra_stacklevel=extra_stacklevel 102 | ) 103 | 104 | def __call__(self, wrapped): 105 | """ 106 | Add the Sphinx directive to your class or function. 107 | 108 | :param wrapped: Wrapped class or function. 109 | 110 | :return: the decorated class or function. 111 | """ 112 | # -- build the directive division 113 | fmt = ".. {directive}:: {version}" if self.version else ".. {directive}::" 114 | div_lines = [fmt.format(directive=self.directive, version=self.version)] 115 | width = self.line_length - 3 if self.line_length > 3 else 2**16 116 | reason = textwrap.dedent(self.reason).strip() 117 | for paragraph in reason.splitlines(): 118 | if paragraph: 119 | div_lines.extend( 120 | textwrap.fill( 121 | paragraph, 122 | width=width, 123 | initial_indent=" ", 124 | subsequent_indent=" ", 125 | ).splitlines() 126 | ) 127 | else: 128 | div_lines.append("") 129 | 130 | # -- get the docstring, normalize the trailing newlines 131 | # keep a consistent behaviour if the docstring starts with newline or directly on the first one 132 | docstring = wrapped.__doc__ or "" 133 | lines = docstring.splitlines(True) or [""] 134 | docstring = textwrap.dedent("".join(lines[1:])) if len(lines) > 1 else "" 135 | docstring = lines[0] + docstring 136 | if docstring: 137 | # An empty line must separate the original docstring and the directive. 138 | docstring = re.sub(r"\n+$", "", docstring, flags=re.DOTALL) + "\n\n" 139 | else: 140 | # Avoid "Explicit markup ends without a blank line" when the decorated function has no docstring 141 | docstring = "\n" 142 | 143 | # -- append the directive division to the docstring 144 | docstring += "".join("{}\n".format(line) for line in div_lines) 145 | 146 | wrapped.__doc__ = docstring 147 | if self.directive in {"versionadded", "versionchanged"}: 148 | return wrapped 149 | return super(SphinxAdapter, self).__call__(wrapped) 150 | 151 | def get_deprecated_msg(self, wrapped, instance): 152 | """ 153 | Get the deprecation warning message (without Sphinx cross-referencing syntax) for the user. 154 | 155 | :param wrapped: Wrapped class or function. 156 | 157 | :param instance: The object to which the wrapped function was bound when it was called. 158 | 159 | :return: The warning message. 160 | 161 | .. versionadded:: 1.2.12 162 | Strip Sphinx cross-referencing syntax from warning message. 163 | 164 | """ 165 | msg = super(SphinxAdapter, self).get_deprecated_msg(wrapped, instance) 166 | # Strip Sphinx cross-reference syntax (like ":function:", ":py:func:" and ":py:meth:") 167 | # Possible values are ":role:`foo`", ":domain:role:`foo`" 168 | # where ``role`` and ``domain`` should match "[a-zA-Z]+" 169 | msg = re.sub(r"(?: : [a-zA-Z]+ )? : [a-zA-Z]+ : (`[^`]*`)", r"\1", msg, flags=re.X) 170 | return msg 171 | 172 | 173 | def versionadded(reason="", version="", line_length=70): 174 | """ 175 | This decorator can be used to insert a "versionadded" directive 176 | in your function/class docstring in order to document the 177 | version of the project which adds this new functionality in your library. 178 | 179 | :param str reason: 180 | Reason message which documents the addition in your library (can be omitted). 181 | 182 | :param str version: 183 | Version of your project which adds this feature. 184 | If you follow the `Semantic Versioning `_, 185 | the version number has the format "MAJOR.MINOR.PATCH", and, 186 | in the case of a new functionality, the "PATCH" component should be "0". 187 | 188 | :type line_length: int 189 | :param line_length: 190 | Max line length of the directive text. If non nul, a long text is wrapped in several lines. 191 | 192 | :return: the decorated function. 193 | """ 194 | adapter = SphinxAdapter( 195 | 'versionadded', 196 | reason=reason, 197 | version=version, 198 | line_length=line_length, 199 | ) 200 | return adapter 201 | 202 | 203 | def versionchanged(reason="", version="", line_length=70): 204 | """ 205 | This decorator can be used to insert a "versionchanged" directive 206 | in your function/class docstring in order to document the 207 | version of the project which modifies this functionality in your library. 208 | 209 | :param str reason: 210 | Reason message which documents the modification in your library (can be omitted). 211 | 212 | :param str version: 213 | Version of your project which modifies this feature. 214 | If you follow the `Semantic Versioning `_, 215 | the version number has the format "MAJOR.MINOR.PATCH". 216 | 217 | :type line_length: int 218 | :param line_length: 219 | Max line length of the directive text. If non nul, a long text is wrapped in several lines. 220 | 221 | :return: the decorated function. 222 | """ 223 | adapter = SphinxAdapter( 224 | 'versionchanged', 225 | reason=reason, 226 | version=version, 227 | line_length=line_length, 228 | ) 229 | return adapter 230 | 231 | 232 | def deprecated(reason="", version="", line_length=70, **kwargs): 233 | """ 234 | This decorator can be used to insert a "deprecated" directive 235 | in your function/class docstring in order to document the 236 | version of the project which deprecates this functionality in your library. 237 | 238 | :param str reason: 239 | Reason message which documents the deprecation in your library (can be omitted). 240 | 241 | :param str version: 242 | Version of your project which deprecates this feature. 243 | If you follow the `Semantic Versioning `_, 244 | the version number has the format "MAJOR.MINOR.PATCH". 245 | 246 | :type line_length: int 247 | :param line_length: 248 | Max line length of the directive text. If non nul, a long text is wrapped in several lines. 249 | 250 | Keyword arguments can be: 251 | 252 | - "action": 253 | A warning filter used to activate or not the deprecation warning. 254 | Can be one of "error", "ignore", "always", "default", "module", or "once". 255 | If ``None``, empty or missing, the global filtering mechanism is used. 256 | 257 | - "category": 258 | The warning category to use for the deprecation warning. 259 | By default, the category class is :class:`~DeprecationWarning`, 260 | you can inherit this class to define your own deprecation warning category. 261 | 262 | - "extra_stacklevel": 263 | Number of additional stack levels to consider instrumentation rather than user code. 264 | With the default value of 0, the warning refers to where the class was instantiated 265 | or the function was called. 266 | 267 | 268 | :return: a decorator used to deprecate a function. 269 | 270 | .. versionchanged:: 1.2.13 271 | Change the signature of the decorator to reflect the valid use cases. 272 | 273 | .. versionchanged:: 1.2.15 274 | Add the *extra_stacklevel* parameter. 275 | """ 276 | directive = kwargs.pop('directive', 'deprecated') 277 | adapter_cls = kwargs.pop('adapter_cls', SphinxAdapter) 278 | kwargs["reason"] = reason 279 | kwargs["version"] = version 280 | kwargs["line_length"] = line_length 281 | return _classic_deprecated(directive=directive, adapter_cls=adapter_cls, **kwargs) 282 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial: 2 | 3 | Tutorial 4 | ======== 5 | 6 | In this tutorial, we will use the Deprecated Library to mark pieces of codes as deprecated. 7 | We will also see what's happened when a user tries to call deprecated codes. 8 | 9 | Deprecated function 10 | ------------------- 11 | 12 | First, we have this little library composed of a single module: ``liberty.py``: 13 | 14 | .. literalinclude:: tutorial/v0/liberty.py 15 | 16 | You decided to write a more powerful function called ``better_print()`` 17 | which will become a replacement of ``print_value()``. 18 | And you decided that the later function is deprecated. 19 | 20 | To mark the ``print_value()`` as deprecated, you can use the :meth:`~deprecated` decorator: 21 | 22 | .. literalinclude:: tutorial/v1/liberty.py 23 | 24 | If the user tries to use the deprecated functions, he will have a warning for each call: 25 | 26 | .. literalinclude:: tutorial/v1/using_liberty.py 27 | 28 | .. code-block:: sh 29 | 30 | $ python use_liberty.py 31 | 32 | using_liberty.py:4: DeprecationWarning: Call to deprecated function (or staticmethod) print_value. 33 | liberty.print_value("hello") 34 | 'hello' 35 | using_liberty.py:5: DeprecationWarning: Call to deprecated function (or staticmethod) print_value. 36 | liberty.print_value("hello again") 37 | 'hello again' 38 | 'Hi Tom!' 39 | 40 | As you can see, the deprecation warning is displayed like a stack trace. 41 | You have the source code path, the line number and the called function. 42 | This is very useful for debugging. 43 | But, this doesn't help the developer to choose a alternative: which function could he use instead? 44 | 45 | To help the developer, you can add a "reason" message. For instance: 46 | 47 | .. literalinclude:: tutorial/v2/liberty.py 48 | 49 | When the user calls the deprecated functions, he will have a more useful message: 50 | 51 | .. code-block:: sh 52 | 53 | $ python use_liberty.py 54 | 55 | using_liberty.py:4: DeprecationWarning: Call to deprecated function (or staticmethod) print_value. (This function is rotten, use 'better_print' instead) 56 | liberty.print_value("hello") 57 | 'hello' 58 | using_liberty.py:5: DeprecationWarning: Call to deprecated function (or staticmethod) print_value. (This function is rotten, use 'better_print' instead) 59 | liberty.print_value("hello again") 60 | 'hello again' 61 | 'Hi Tom!' 62 | 63 | 64 | Deprecated method 65 | ----------------- 66 | 67 | Decorating a deprecated method works like decorating a function. 68 | 69 | .. literalinclude:: tutorial/v3/liberty.py 70 | 71 | When the user calls the deprecated methods, like this: 72 | 73 | .. literalinclude:: tutorial/v3/using_liberty.py 74 | 75 | He will have: 76 | 77 | .. code-block:: sh 78 | 79 | $ python use_liberty.py 80 | 81 | using_liberty.py:5: DeprecationWarning: Call to deprecated method print_value. (This method is rotten, use 'better_print' instead) 82 | obj.print_value() 83 | 'Greeting' 84 | using_liberty.py:6: DeprecationWarning: Call to deprecated method print_value. (This method is rotten, use 'better_print' instead) 85 | obj.print_value() 86 | 'Greeting' 87 | 'Greeting' 88 | 89 | .. note:: The call is done to the same object, so we have 3 times 'Greeting'. 90 | 91 | 92 | Deprecated class 93 | ---------------- 94 | 95 | You can also decide that a class is deprecated. 96 | 97 | For instance: 98 | 99 | .. literalinclude:: tutorial/v4/liberty.py 100 | 101 | When the user use the deprecated class like this: 102 | 103 | .. literalinclude:: tutorial/v4/using_liberty.py 104 | 105 | He will have a warning at object instantiation. 106 | Once the object is initialised, no more warning are emitted. 107 | 108 | .. code-block:: sh 109 | 110 | $ python use_liberty.py 111 | 112 | using_liberty.py:4: DeprecationWarning: Call to deprecated class Liberty. (This class is not perfect) 113 | obj = liberty.Liberty("Salutation") 114 | 'Salutation' 115 | 'Salutation' 116 | 117 | If a deprecated class is used, then a warning message is emitted during class instantiation. 118 | In other word, deprecating a class is the same as deprecating it's ``__new__`` class method. 119 | 120 | As a reminder, the magic method ``__new__`` will be called when instance is being created. 121 | Using this method you can customize the instance creation. 122 | the :func:`~deprecated.deprecated` decorator patches the ``__new__`` method in order to 123 | emmit the warning message before instance creation. 124 | 125 | 126 | Deprecated parameters 127 | --------------------- 128 | 129 | It is also possible to mark one or more parameters of a function as deprecated using the 130 | :func:`deprecated.params.deprecated_params` decorator. 131 | 132 | Example: 133 | 134 | .. code-block:: python 135 | 136 | import warnings 137 | from deprecated.params import deprecated_params 138 | 139 | class V2DeprecationWarning(DeprecationWarning): 140 | pass 141 | 142 | # noinspection PyUnusedLocal 143 | @deprecated_params( 144 | { 145 | "epsilon": "epsilon is deprecated in version v2", 146 | "start": "start is removed in version v2", 147 | }, 148 | category=V2DeprecationWarning, 149 | ) 150 | @deprecated_params("epsilon", reason="epsilon is deprecated in version v1.1") 151 | def integrate(f, a, b, n=0, epsilon=0.0, start=None): 152 | epsilon = epsilon or (b - a) / n 153 | n = n or int((b - a) / epsilon) 154 | return sum((f(a + (i * epsilon)) + f(a + (i * epsilon) + epsilon)) * epsilon / 2 for i in range(n)) 155 | 156 | When the function is called, parameters marked as deprecated will emit deprecation 157 | warnings (using the provided category and message). This allows you to inform users 158 | about alternatives or the versions in which parameters were changed or removed. 159 | 160 | .. code-block:: sh 161 | 162 | $ python use_deprecated_params.py 163 | 164 | use_deprecated_params.py:48: V2DeprecationWarning: epsilon is deprecated in version v2 165 | integrate(lambda x: x**2, 0, 2, epsilon=0.0012, start=123) 166 | use_deprecated_params.py:48: V2DeprecationWarning: start is removed in version v2 167 | integrate(lambda x: x**2, 0, 2, epsilon=0.0012, start=123) 168 | 169 | 170 | Controlling warnings 171 | -------------------- 172 | 173 | .. _Python warning control: https://docs.python.org/3/library/warnings.html 174 | .. _-W: https://docs.python.org/3/using/cmdline.html#cmdoption-W 175 | .. _PYTHONWARNINGS: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS 176 | 177 | Warnings are emitted using the `Python warning control`_. By default, Python installs several warning filters, 178 | which can be overridden by the `-W`_ command-line option, the `PYTHONWARNINGS`_ environment variable and 179 | calls to :func:`warnings.filterwarnings`. The warnings filter controls whether warnings are ignored, displayed, 180 | or turned into errors (raising an exception). 181 | 182 | For instance: 183 | 184 | .. literalinclude:: tutorial/warning_ctrl/filter_warnings_demo.py 185 | 186 | When the user runs this script, the deprecation warnings are ignored in the main program, 187 | so no warning message are emitted: 188 | 189 | .. code-block:: sh 190 | 191 | $ python filter_warnings_demo.py 192 | 193 | fun 194 | 195 | 196 | Deprecation warning classes 197 | --------------------------- 198 | 199 | The :func:`deprecated.classic.deprecated` and :func:`deprecated.sphinx.deprecated` functions 200 | are using the :exc:`DeprecationWarning` category but you can customize them by using your own category 201 | (or hierarchy of categories). 202 | 203 | * *category* classes which you can use (among other) are: 204 | 205 | +----------------------------------+-----------------------------------------------+ 206 | | Class | Description | 207 | +==================================+===============================================+ 208 | | :exc:`DeprecationWarning` | Base category for warnings about deprecated | 209 | | | features when those warnings are intended for | 210 | | | other Python developers (ignored by default, | 211 | | | unless triggered by code in ``__main__``). | 212 | +----------------------------------+-----------------------------------------------+ 213 | | :exc:`FutureWarning` | Base category for warnings about deprecated | 214 | | | features when those warnings are intended for | 215 | | | end users of applications that are written in | 216 | | | Python. | 217 | +----------------------------------+-----------------------------------------------+ 218 | | :exc:`PendingDeprecationWarning` | Base category for warnings about features | 219 | | | that will be deprecated in the future | 220 | | | (ignored by default). | 221 | +----------------------------------+-----------------------------------------------+ 222 | 223 | You can define your own deprecation warning hierarchy based on the standard deprecation classes. 224 | 225 | For instance: 226 | 227 | .. literalinclude:: tutorial/warning_ctrl/warning_classes_demo.py 228 | 229 | When the user runs this script, the deprecation warnings for the 3.0 version are ignored: 230 | 231 | .. code-block:: sh 232 | 233 | $ python warning_classes_demo.py 234 | 235 | foo 236 | bar 237 | warning_classes_demo.py:30: DeprecatedIn26: Call to deprecated function (or staticmethod) foo. (deprecated function) 238 | foo() 239 | 240 | Filtering warnings locally 241 | -------------------------- 242 | 243 | The :func:`deprecated.classic.deprecated` and :func:`deprecated.sphinx.deprecated` functions 244 | can change the warning filtering locally (at function calls). 245 | 246 | * *action* is one of the following strings: 247 | 248 | +---------------+----------------------------------------------+ 249 | | Value | Disposition | 250 | +===============+==============================================+ 251 | | ``"default"`` | print the first occurrence of matching | 252 | | | warnings for each location (module + | 253 | | | line number) where the warning is issued | 254 | +---------------+----------------------------------------------+ 255 | | ``"error"`` | turn matching warnings into exceptions | 256 | +---------------+----------------------------------------------+ 257 | | ``"ignore"`` | never print matching warnings | 258 | +---------------+----------------------------------------------+ 259 | | ``"always"`` | always print matching warnings | 260 | +---------------+----------------------------------------------+ 261 | | ``"module"`` | print the first occurrence of matching | 262 | | | warnings for each module where the warning | 263 | | | is issued (regardless of line number) | 264 | +---------------+----------------------------------------------+ 265 | | ``"once"`` | print only the first occurrence of matching | 266 | | | warnings, regardless of location | 267 | +---------------+----------------------------------------------+ 268 | 269 | You can define the *action* keyword parameter to override the filtering warnings locally. 270 | 271 | For instance: 272 | 273 | .. literalinclude:: tutorial/warning_ctrl/filter_action_demo.py 274 | 275 | In this example, even if the global filter is set to "ignore", a call to the ``foo()`` 276 | function will raise an exception because the *action* is set to "error". 277 | 278 | .. code-block:: sh 279 | 280 | $ python filter_action_demo.py 281 | 282 | Traceback (most recent call last): 283 | File "filter_action_demo.py", line 12, in 284 | foo() 285 | File "path/to/deprecated/classic.py", line 274, in wrapper_function 286 | warnings.warn(msg, category=category, stacklevel=_stacklevel) 287 | DeprecationWarning: Call to deprecated function (or staticmethod) foo. (do not call it) 288 | 289 | Modifying the deprecated code reference 290 | --------------------------------------- 291 | 292 | By default, when a deprecated function or class is called, the warning message indicates the location of the caller. 293 | 294 | The ``extra_stacklevel`` parameter allows customizing the stack level reference in the deprecation warning message. 295 | 296 | This parameter is particularly useful in scenarios where you have a factory or utility function that creates deprecated 297 | objects or performs deprecated operations. By specifying an ``extra_stacklevel`` value, you can control the stack level 298 | at which the deprecation warning is emitted, making it appear as if the calling function is the deprecated one, 299 | rather than the actual deprecated entity. 300 | 301 | For example, if you have a factory function ``create_object()`` that creates deprecated objects, you can use 302 | the ``extra_stacklevel`` parameter to emit the deprecation warning at the calling location. This provides clearer and 303 | more actionable deprecation messages, allowing developers to identify and update the code that invokes the deprecated 304 | functionality. 305 | 306 | For instance: 307 | 308 | .. literalinclude:: tutorial/warning_ctrl/extra_stacklevel_demo.py 309 | 310 | Please note that the ``extra_stacklevel`` value should be an integer indicating the number of stack levels to skip 311 | when emitting the deprecation warning. 312 | -------------------------------------------------------------------------------- /tests/test_sphinx.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import print_function 3 | 4 | import re 5 | import sys 6 | import textwrap 7 | import warnings 8 | 9 | import pytest 10 | 11 | import deprecated.sphinx 12 | 13 | 14 | @pytest.fixture( 15 | scope="module", 16 | params=[ 17 | None, 18 | """This function adds *x* and *y*.""", 19 | """ 20 | This function adds *x* and *y*. 21 | 22 | :param x: number *x* 23 | :param y: number *y* 24 | :return: sum = *x* + *y* 25 | """, 26 | """This function adds *x* and *y*. 27 | 28 | :param x: number *x* 29 | :param y: number *y* 30 | :return: sum = *x* + *y* 31 | """, 32 | ], 33 | ids=["no_docstring", "short_docstring", "D213_long_docstring", "D212_long_docstring"], 34 | ) 35 | def docstring(request): 36 | return request.param 37 | 38 | 39 | @pytest.fixture(scope="module", params=['versionadded', 'versionchanged', 'deprecated']) 40 | def directive(request): 41 | return request.param 42 | 43 | 44 | # noinspection PyShadowingNames 45 | @pytest.mark.parametrize( 46 | "reason, version, expected", 47 | [ 48 | ( 49 | 'A good reason', 50 | '1.2.0', 51 | textwrap.dedent( 52 | """\ 53 | .. {directive}:: {version} 54 | {reason} 55 | """ 56 | ), 57 | ), 58 | ( 59 | None, 60 | '1.2.0', 61 | textwrap.dedent( 62 | """\ 63 | .. {directive}:: {version} 64 | """ 65 | ), 66 | ), 67 | ], 68 | ids=["reason&version", "version"], 69 | ) 70 | def test_has_sphinx_docstring(docstring, directive, reason, version, expected): 71 | # The function: 72 | def foo(x, y): 73 | return x + y 74 | 75 | # with docstring: 76 | foo.__doc__ = docstring 77 | 78 | # is decorated with: 79 | decorator_factory = getattr(deprecated.sphinx, directive) 80 | decorator = decorator_factory(reason=reason, version=version) 81 | foo = decorator(foo) 82 | 83 | # The function must contains this Sphinx docstring: 84 | expected = expected.format(directive=directive, version=version, reason=reason) 85 | 86 | current = textwrap.dedent(foo.__doc__) 87 | assert current.endswith(expected) 88 | 89 | current = current.replace(expected, '') 90 | if docstring: 91 | # An empty line must separate the original docstring and the directive. 92 | assert re.search("\n[ ]*\n$", current, flags=re.DOTALL) 93 | else: 94 | # Avoid "Explicit markup ends without a blank line" when the decorated function has no docstring 95 | assert current == "\n" 96 | 97 | with warnings.catch_warnings(record=True) as warns: 98 | foo(1, 2) 99 | 100 | if directive in {'versionadded', 'versionchanged'}: 101 | # don't emit DeprecationWarning 102 | assert len(warns) == 0 103 | else: 104 | # emit DeprecationWarning 105 | assert len(warns) == 1 106 | 107 | 108 | # noinspection PyShadowingNames 109 | @pytest.mark.skipif( 110 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 111 | ) 112 | @pytest.mark.parametrize( 113 | "reason, version, expected", 114 | [ 115 | ( 116 | 'A good reason', 117 | '1.2.0', 118 | textwrap.dedent( 119 | """\ 120 | .. {directive}:: {version} 121 | {reason} 122 | """ 123 | ), 124 | ), 125 | ( 126 | None, 127 | '1.2.0', 128 | textwrap.dedent( 129 | """\ 130 | .. {directive}:: {version} 131 | """ 132 | ), 133 | ), 134 | ], 135 | ids=["reason&version", "version"], 136 | ) 137 | def test_cls_has_sphinx_docstring(docstring, directive, reason, version, expected): 138 | # The class: 139 | class Foo(object): 140 | pass 141 | 142 | # with docstring: 143 | Foo.__doc__ = docstring 144 | 145 | # is decorated with: 146 | decorator_factory = getattr(deprecated.sphinx, directive) 147 | decorator = decorator_factory(reason=reason, version=version) 148 | Foo = decorator(Foo) 149 | 150 | # The class must contain this Sphinx docstring: 151 | expected = expected.format(directive=directive, version=version, reason=reason) 152 | 153 | current = textwrap.dedent(Foo.__doc__) 154 | assert current.endswith(expected) 155 | 156 | current = current.replace(expected, '') 157 | if docstring: 158 | # An empty line must separate the original docstring and the directive. 159 | assert re.search("\n[ ]*\n$", current, flags=re.DOTALL) 160 | else: 161 | # Avoid "Explicit markup ends without a blank line" when the decorated function has no docstring 162 | assert current == "\n" 163 | 164 | with warnings.catch_warnings(record=True) as warns: 165 | Foo() 166 | 167 | if directive in {'versionadded', 'versionchanged'}: 168 | # don't emit DeprecationWarning 169 | assert len(warns) == 0 170 | else: 171 | # emit DeprecationWarning 172 | assert len(warns) == 1 173 | 174 | 175 | class MyDeprecationWarning(DeprecationWarning): 176 | pass 177 | 178 | 179 | _PARAMS = [ 180 | {'version': '1.2.3'}, 181 | {'version': '1.2.3', 'reason': 'Good reason'}, 182 | {'version': '1.2.3', 'action': 'once'}, 183 | {'version': '1.2.3', 'category': MyDeprecationWarning}, 184 | ] 185 | 186 | 187 | @pytest.fixture(scope="module", params=_PARAMS) 188 | def sphinx_deprecated_function(request): 189 | kwargs = request.param 190 | 191 | @deprecated.sphinx.deprecated(**kwargs) 192 | def foo1(): 193 | pass 194 | 195 | return foo1 196 | 197 | 198 | @pytest.fixture(scope="module", params=_PARAMS) 199 | def sphinx_deprecated_class(request): 200 | kwargs = request.param 201 | 202 | @deprecated.sphinx.deprecated(**kwargs) 203 | class Foo2(object): 204 | pass 205 | 206 | return Foo2 207 | 208 | 209 | @pytest.fixture(scope="module", params=_PARAMS) 210 | def sphinx_deprecated_method(request): 211 | kwargs = request.param 212 | 213 | class Foo3(object): 214 | @deprecated.sphinx.deprecated(**kwargs) 215 | def foo3(self): 216 | pass 217 | 218 | return Foo3 219 | 220 | 221 | @pytest.fixture(scope="module", params=_PARAMS) 222 | def sphinx_deprecated_static_method(request): 223 | kwargs = request.param 224 | 225 | class Foo4(object): 226 | @staticmethod 227 | @deprecated.sphinx.deprecated(**kwargs) 228 | def foo4(): 229 | pass 230 | 231 | return Foo4.foo4 232 | 233 | 234 | @pytest.fixture(scope="module", params=_PARAMS) 235 | def sphinx_deprecated_class_method(request): 236 | kwargs = request.param 237 | 238 | class Foo5(object): 239 | @classmethod 240 | @deprecated.sphinx.deprecated(**kwargs) 241 | def foo5(cls): 242 | pass 243 | 244 | return Foo5 245 | 246 | 247 | # noinspection PyShadowingNames 248 | def test_sphinx_deprecated_function__warns(sphinx_deprecated_function): 249 | with warnings.catch_warnings(record=True) as warns: 250 | warnings.simplefilter("always") 251 | sphinx_deprecated_function() 252 | assert len(warns) == 1 253 | warn = warns[0] 254 | assert issubclass(warn.category, DeprecationWarning) 255 | assert "deprecated function (or staticmethod)" in str(warn.message) 256 | 257 | 258 | # noinspection PyShadowingNames 259 | @pytest.mark.skipif( 260 | sys.version_info < (3, 3), reason="Classes should have mutable docstrings -- resolved in python 3.3" 261 | ) 262 | def test_sphinx_deprecated_class__warns(sphinx_deprecated_class): 263 | with warnings.catch_warnings(record=True) as warns: 264 | warnings.simplefilter("always") 265 | sphinx_deprecated_class() 266 | assert len(warns) == 1 267 | warn = warns[0] 268 | assert issubclass(warn.category, DeprecationWarning) 269 | assert "deprecated class" in str(warn.message) 270 | 271 | 272 | # noinspection PyShadowingNames 273 | def test_sphinx_deprecated_method__warns(sphinx_deprecated_method): 274 | with warnings.catch_warnings(record=True) as warns: 275 | warnings.simplefilter("always") 276 | obj = sphinx_deprecated_method() 277 | obj.foo3() 278 | assert len(warns) == 1 279 | warn = warns[0] 280 | assert issubclass(warn.category, DeprecationWarning) 281 | assert "deprecated method" in str(warn.message) 282 | 283 | 284 | # noinspection PyShadowingNames 285 | def test_sphinx_deprecated_static_method__warns(sphinx_deprecated_static_method): 286 | with warnings.catch_warnings(record=True) as warns: 287 | warnings.simplefilter("always") 288 | sphinx_deprecated_static_method() 289 | assert len(warns) == 1 290 | warn = warns[0] 291 | assert issubclass(warn.category, DeprecationWarning) 292 | assert "deprecated function (or staticmethod)" in str(warn.message) 293 | 294 | 295 | # noinspection PyShadowingNames 296 | def test_sphinx_deprecated_class_method__warns(sphinx_deprecated_class_method): 297 | with warnings.catch_warnings(record=True) as warns: 298 | warnings.simplefilter("always") 299 | cls = sphinx_deprecated_class_method() 300 | cls.foo5() 301 | assert len(warns) == 1 302 | warn = warns[0] 303 | assert issubclass(warn.category, DeprecationWarning) 304 | if (3, 9) <= sys.version_info < (3, 13): 305 | assert "deprecated class method" in str(warn.message) 306 | else: 307 | assert "deprecated function (or staticmethod)" in str(warn.message) 308 | 309 | 310 | def test_should_raise_type_error(): 311 | try: 312 | @deprecated.sphinx.deprecated(version="4.5.6", reason=5) 313 | def foo(): 314 | pass 315 | 316 | assert False, "TypeError not raised" 317 | except TypeError: 318 | pass 319 | 320 | 321 | def test_warning_msg_has_reason(): 322 | reason = "Good reason" 323 | 324 | @deprecated.sphinx.deprecated(version="4.5.6", reason=reason) 325 | def foo(): 326 | pass 327 | 328 | with warnings.catch_warnings(record=True) as warns: 329 | foo() 330 | warn = warns[0] 331 | assert reason in str(warn.message) 332 | 333 | 334 | def test_warning_msg_has_version(): 335 | version = "1.2.3" 336 | 337 | @deprecated.sphinx.deprecated(version=version) 338 | def foo(): 339 | pass 340 | 341 | with warnings.catch_warnings(record=True) as warns: 342 | foo() 343 | warn = warns[0] 344 | assert version in str(warn.message) 345 | 346 | 347 | def test_warning_is_ignored(): 348 | @deprecated.sphinx.deprecated(version="4.5.6", action='ignore') 349 | def foo(): 350 | pass 351 | 352 | with warnings.catch_warnings(record=True) as warns: 353 | foo() 354 | assert len(warns) == 0 355 | 356 | 357 | def test_specific_warning_cls_is_used(): 358 | @deprecated.sphinx.deprecated(version="4.5.6", category=MyDeprecationWarning) 359 | def foo(): 360 | pass 361 | 362 | with warnings.catch_warnings(record=True) as warns: 363 | foo() 364 | warn = warns[0] 365 | assert issubclass(warn.category, MyDeprecationWarning) 366 | 367 | 368 | def test_can_catch_warnings(): 369 | with warnings.catch_warnings(record=True) as warns: 370 | warnings.simplefilter("always") 371 | warnings.warn("A message in a bottle", category=DeprecationWarning, stacklevel=2) 372 | assert len(warns) == 1 373 | 374 | 375 | @pytest.mark.parametrize( 376 | ["reason", "expected"], 377 | [ 378 | ("Use :function:`bar` instead", "Use `bar` instead"), 379 | ("Use :py:func:`bar` instead", "Use `bar` instead"), 380 | ], 381 | ) 382 | def test_sphinx_syntax_trimming(reason, expected): 383 | @deprecated.sphinx.deprecated(version="4.5.6", reason=reason) 384 | def foo(): 385 | pass 386 | 387 | with warnings.catch_warnings(record=True) as warns: 388 | foo() 389 | warn = warns[0] 390 | assert expected in str(warn.message) 391 | 392 | 393 | # noinspection SpellCheckingInspection 394 | @pytest.mark.parametrize( 395 | "reason, expected", 396 | [ 397 | # classic examples using the default domain (Python) 398 | ("Use :func:`bar` instead", "Use `bar` instead"), 399 | ("Use :function:`bar` instead", "Use `bar` instead"), 400 | ("Use :class:`Baz` instead", "Use `Baz` instead"), 401 | ("Use :exc:`Baz` instead", "Use `Baz` instead"), 402 | ("Use :exception:`Baz` instead", "Use `Baz` instead"), 403 | ("Use :meth:`Baz.bar` instead", "Use `Baz.bar` instead"), 404 | ("Use :method:`Baz.bar` instead", "Use `Baz.bar` instead"), 405 | # other examples using a domain : 406 | ("Use :py:func:`bar` instead", "Use `bar` instead"), 407 | ("Use :cpp:func:`bar` instead", "Use `bar` instead"), 408 | ("Use :js:func:`bar` instead", "Use `bar` instead"), 409 | # the reference can have special characters: 410 | ("Use :func:`~pkg.mod.bar` instead", "Use `~pkg.mod.bar` instead"), 411 | # edge cases: 412 | ("Use :r:`` instead", "Use `` instead"), 413 | ("Use :d:r:`` instead", "Use `` instead"), 414 | ("Use :r:`foo` instead", "Use `foo` instead"), 415 | ("Use :d:r:`foo` instead", "Use `foo` instead"), 416 | ("Use r:`bad` instead", "Use r:`bad` instead"), 417 | ("Use ::`bad` instead", "Use ::`bad` instead"), 418 | ("Use :::`bad` instead", "Use :::`bad` instead"), 419 | ], 420 | ) 421 | def test_get_deprecated_msg(reason, expected): 422 | adapter = deprecated.sphinx.SphinxAdapter("deprecated", reason=reason, version="1") 423 | actual = adapter.get_deprecated_msg(lambda: None, None) 424 | assert expected in actual 425 | -------------------------------------------------------------------------------- /docs/source/white_paper.rst: -------------------------------------------------------------------------------- 1 | .. _white_paper: 2 | 3 | White Paper 4 | =========== 5 | 6 | This white paper shows some examples of how function deprecation is implemented in the Python Standard Library and Famous Open Source libraries. 7 | 8 | You will see which kind of deprecation you can find in such libraries, and how it is documented in the user manuel. 9 | 10 | .. _Python Standard Library: 11 | 12 | The Python Standard Library 13 | --------------------------- 14 | 15 | :Library: Python_ 16 | :GitHub: `python/cpython `_. 17 | :Version: v3.8.dev 18 | 19 | An example of function deprecation can be found in the :mod:`urllib` module (:file:`Lib/urllib/parse.py`): 20 | 21 | .. code-block:: python 22 | 23 | def to_bytes(url): 24 | warnings.warn("urllib.parse.to_bytes() is deprecated as of 3.8", 25 | DeprecationWarning, stacklevel=2) 26 | return _to_bytes(url) 27 | 28 | In the Python library, a warning is emitted in the function body using the function :func:`warnings.warn`. 29 | This implementation is straightforward, it uses the category :exc:`DeprecationWarning` for warning filtering. 30 | 31 | Another example is the deprecation of the *collections* ABC, which are now moved in the :mod:`collections.abc` module. 32 | This example is available in the :mod:`collections` module (:file:`Lib/collections/__init__.py`): 33 | 34 | .. code-block:: python 35 | 36 | def __getattr__(name): 37 | if name in _collections_abc.__all__: 38 | obj = getattr(_collections_abc, name) 39 | import warnings 40 | warnings.warn("Using or importing the ABCs from 'collections' instead " 41 | "of from 'collections.abc' is deprecated, " 42 | "and in 3.8 it will stop working", 43 | DeprecationWarning, stacklevel=2) 44 | globals()[name] = obj 45 | return obj 46 | raise AttributeError(f'module {__name__!r} has no attribute {name!r}') 47 | 48 | The warning is only emitted when an ABC is accessed from the :mod:`collections` instead of :mod:`collections.abc` module. 49 | 50 | We can also see an example of keyword argument deprecation in the :class:`~collections.UserDict` class: 51 | 52 | .. code-block:: python 53 | 54 | def __init__(*args, **kwargs): 55 | if not args: 56 | raise TypeError("descriptor '__init__' of 'UserDict' object " 57 | "needs an argument") 58 | self, *args = args 59 | if len(args) > 1: 60 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 61 | if args: 62 | dict = args[0] 63 | elif 'dict' in kwargs: 64 | dict = kwargs.pop('dict') 65 | import warnings 66 | warnings.warn("Passing 'dict' as keyword argument is deprecated", 67 | DeprecationWarning, stacklevel=2) 68 | else: 69 | dict = None 70 | self.data = {} 71 | if dict is not None: 72 | self.update(dict) 73 | if len(kwargs): 74 | self.update(kwargs) 75 | 76 | Again, this implementation is straightforward: if the *dict* keyword argument is used, a warning is emitted. 77 | 78 | Python make also use of the category :exc:`PendingDeprecationWarning` for instance in the :mod:`asyncio.tasks` module 79 | (:file:`Lib/asyncio/tasks.py`): 80 | 81 | .. code-block:: python 82 | 83 | @classmethod 84 | def current_task(cls, loop=None): 85 | warnings.warn("Task.current_task() is deprecated, " 86 | "use asyncio.current_task() instead", 87 | PendingDeprecationWarning, 88 | stacklevel=2) 89 | if loop is None: 90 | loop = events.get_event_loop() 91 | return current_task(loop) 92 | 93 | The category :exc:`FutureWarning` is also used to emit a warning when the functions is broken and will be 94 | fixed in a "future" release. We can see for instance the method :meth:`~xml.etree.ElementTree.ElementTree.find` 95 | of the class :class:`~xml.etree.ElementTree.ElementTree` (:file:`Lib/xml/etree/ElementTree.py`): 96 | 97 | .. code-block:: python 98 | 99 | def find(self, path, namespaces=None): 100 | if path[:1] == "/": 101 | path = "." + path 102 | warnings.warn( 103 | "This search is broken in 1.3 and earlier, and will be " 104 | "fixed in a future version. If you rely on the current " 105 | "behaviour, change it to %r" % path, 106 | FutureWarning, stacklevel=2 107 | ) 108 | return self._root.find(path, namespaces) 109 | 110 | As a conclusion: 111 | 112 | - Python library uses :func:`warnings.warn` to emit a deprecation warning in the body of functions. 113 | - 3 categories are used: :exc:`DeprecationWarning`, :exc:`PendingDeprecationWarning` and :exc:`FutureWarning`. 114 | - The docstring doesn't show anything about deprecation. 115 | - The documentation warns about some, but not all, deprecated usages. 116 | 117 | .. _Python: https://docs.python.org/fr/3/ 118 | 119 | .. _Flask Library: 120 | 121 | The Flask Library 122 | ----------------- 123 | 124 | :Library: Flask_ 125 | :GitHub: `pallets/flask `_. 126 | :Version: v1.1.dev 127 | 128 | In the source code of Flask, we find only few deprecations: in the :mod:`~flask.app` (:file:`flask/app.py`) 129 | and in the :mod:`~flask.helpers` (:file:`flask/helpers.py`) modules. 130 | 131 | In the Flask Library, like in the `Python Standard Library`_, deprecation warnings are emitted during function calls. 132 | The implementation make use of the category :exc:`DeprecationWarning`. 133 | 134 | Unlike the `Python Standard Library`_, the docstring documents explicitly the deprecation. 135 | Flask uses Sphinx_’s `deprecated directive`_: 136 | 137 | The bellow example shows the deprecation of the :meth:`~flask.Flask.open_session` method: 138 | 139 | .. code-block:: python 140 | 141 | def open_session(self, request): 142 | """Creates or opens a new session. Default implementation stores all 143 | session data in a signed cookie. This requires that the 144 | :attr:`secret_key` is set. Instead of overriding this method 145 | we recommend replacing the :class:`session_interface`. 146 | 147 | .. deprecated: 1.0 148 | Will be removed in 1.1. Use ``session_interface.open_session`` 149 | instead. 150 | 151 | :param request: an instance of :attr:`request_class`. 152 | """ 153 | 154 | warnings.warn(DeprecationWarning( 155 | '"open_session" is deprecated and will be removed in 1.1. Use' 156 | ' "session_interface.open_session" instead.' 157 | )) 158 | return self.session_interface.open_session(self, request) 159 | 160 | .. _deprecated directive: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-deprecated 161 | .. _Sphinx: http://www.sphinx-doc.org/en/stable/index.html 162 | 163 | .. hint:: 164 | 165 | When the function :func:`warnings.warn` is called with a :exc:`DeprecationWarning` instance, 166 | the instance class is used like a warning category. 167 | 168 | The documentation also mention a :exc:`flask.exthook.ExtDeprecationWarning` (which is not found in Flask’s source code): 169 | 170 | .. code-block:: rst 171 | 172 | Extension imports 173 | ````````````````` 174 | 175 | Extension imports of the form ``flask.ext.foo`` are deprecated, you should use 176 | ``flask_foo``. 177 | 178 | The old form still works, but Flask will issue a 179 | ``flask.exthook.ExtDeprecationWarning`` for each extension you import the old 180 | way. We also provide a migration utility called `flask-ext-migrate 181 | `_ that is supposed to 182 | automatically rewrite your imports for this. 183 | 184 | As a conclusion: 185 | 186 | - Flask library uses :func:`warnings.warn` to emit a deprecation warning in the body of functions. 187 | - Only one category is used: :exc:`DeprecationWarning`. 188 | - The docstring use `Sphinx`_’s `deprecated directive`_. 189 | - The API documentation contains the deprecated usages. 190 | 191 | .. _Flask: http://flask.pocoo.org/docs/ 192 | 193 | .. _Django Library: 194 | 195 | The Django Library 196 | ------------------ 197 | 198 | :Library: Django 199 | :GitHub: `django/django `_. 200 | :Version: v3.0.dev 201 | 202 | The `Django`_ Library defines several categories for deprecation in the module :mod:`django.utils.deprecation`: 203 | 204 | - The category :exc:`~django.utils.deprecation.RemovedInDjango31Warning` which inherits 205 | from :exc:`DeprecationWarning`. 206 | - The category :exc:`~django.utils.deprecation.RemovedInDjango40Warning` which inherits 207 | from :exc:`PendingDeprecationWarning`. 208 | - The category :exc:`~django.utils.deprecation.RemovedInNextVersionWarning` which is an alias 209 | of :exc:`~django.utils.deprecation.RemovedInDjango40Warning`. 210 | 211 | The `Django`_ Library don't use :exc:`DeprecationWarning` or :exc:`PendingDeprecationWarning` directly, 212 | but always use one of this 2 classes. The category :exc:`~django.utils.deprecation.RemovedInNextVersionWarning` 213 | is only used in unit tests. 214 | 215 | There are a lot of class deprecation examples. The deprecation warning is emitted during the call 216 | of the ``__init__`` method. For instance in the class :class:`~django.contrib.postgres.forms.ranges.FloatRangeField` 217 | (:file:`django/contrib/staticfiles/storage.py`): 218 | 219 | .. code-block:: python 220 | 221 | class FloatRangeField(DecimalRangeField): 222 | base_field = forms.FloatField 223 | 224 | def __init__(self, **kwargs): 225 | warnings.warn( 226 | 'FloatRangeField is deprecated in favor of DecimalRangeField.', 227 | RemovedInDjango31Warning, stacklevel=2, 228 | ) 229 | super().__init__(**kwargs) 230 | 231 | The implementation in the Django Library is similar to the one done in the `Python Standard Library`_: 232 | deprecation warnings are emitted during function calls. 233 | The implementation use the category :exc:`~django.utils.deprecation.RemovedInDjango31Warning`. 234 | 235 | In the Django Library, we also find an example of property deprecation: 236 | The property :meth:`~django.conf.LazySettings.FILE_CHARSET` of the class :class:`django.conf.LazySettings`. 237 | The implementation of this property is: 238 | 239 | .. code-block:: python 240 | 241 | @property 242 | def FILE_CHARSET(self): 243 | stack = traceback.extract_stack() 244 | # Show a warning if the setting is used outside of Django. 245 | # Stack index: -1 this line, -2 the caller. 246 | filename, _line_number, _function_name, _text = stack[-2] 247 | if not filename.startswith(os.path.dirname(django.__file__)): 248 | warnings.warn( 249 | FILE_CHARSET_DEPRECATED_MSG, 250 | RemovedInDjango31Warning, 251 | stacklevel=2, 252 | ) 253 | return self.__getattr__('FILE_CHARSET') 254 | 255 | We also find function deprecations, mainly with the category :exc:`~django.utils.deprecation.RemovedInDjango40Warning`. 256 | For instance, the function :func:`~django.utils.encoding.smart_text` emits a deprecation warning as follow: 257 | 258 | .. code-block:: python 259 | 260 | def smart_text(s, encoding='utf-8', strings_only=False, errors='strict'): 261 | warnings.warn( 262 | 'smart_text() is deprecated in favor of smart_str().', 263 | RemovedInDjango40Warning, stacklevel=2, 264 | ) 265 | return smart_str(s, encoding, strings_only, errors) 266 | 267 | The Django Library also define a decorator :class:`~django.utils.deprecation.warn_about_renamed_method` 268 | which is used internally in the metaclass :class:`~django.utils.deprecation.RenameMethodsBase`. 269 | This metaclass is only used in unit tests to check renamed methods. 270 | 271 | As a conclusion: 272 | 273 | - The Django library uses :func:`warnings.warn` to emit a deprecation warning in the body of functions. 274 | - It uses two categories which inherits the standard categories :exc:`DeprecationWarning` 275 | and :exc:`PendingDeprecationWarning`. 276 | - The source code of the Django Library doesn't contains much docstring. 277 | The deprecation never appears in the docstring anyway. 278 | - The release notes contain information about deprecated features. 279 | 280 | .. _Django: https://docs.djangoproject.com/ 281 | 282 | The lxml Library 283 | ---------------- 284 | 285 | :Library: lxml_ 286 | :GitHub: `lxml/lxml `_. 287 | :Version: v4.3.2.dev 288 | 289 | The lxml_ Library is developed in Cython, not Python. But, it is a similar language. 290 | This library mainly use comments or docstring to mark function as deprecated. 291 | 292 | For instance, in the class :class:`lxml.xpath._XPathEvaluatorBase`(:file:`src/lxml/xpath.pxi`), 293 | the ``evaluate`` method is deprecated as follow: 294 | 295 | .. code-block:: python 296 | 297 | def evaluate(self, _eval_arg, **_variables): 298 | u"""evaluate(self, _eval_arg, **_variables) 299 | 300 | Evaluate an XPath expression. 301 | 302 | Instead of calling this method, you can also call the evaluator object 303 | itself. 304 | 305 | Variables may be provided as keyword arguments. Note that namespaces 306 | are currently not supported for variables. 307 | 308 | :deprecated: call the object, not its method. 309 | """ 310 | return self(_eval_arg, **_variables) 311 | 312 | There is only one example of usage of the function :func:`warnings.warn`: 313 | in the :class:`~lxml.etree._ElementTree` class (:file:`src/lxml/etree.pyx`): 314 | 315 | .. code-block:: python 316 | 317 | if docstring is not None and doctype is None: 318 | import warnings 319 | warnings.warn( 320 | "The 'docstring' option is deprecated. Use 'doctype' instead.", 321 | DeprecationWarning) 322 | doctype = docstring 323 | 324 | 325 | As a conclusion: 326 | 327 | - Except in one example, the lxml library doesn't use :func:`warnings.warn` to emit a deprecation warnings. 328 | - The deprecations are described in the function docstrings. 329 | - The release notes contain information about deprecated features. 330 | 331 | .. _lxml: https://lxml.de 332 | 333 | The openpyxl Library 334 | -------------------- 335 | 336 | :Library: openpyxl 337 | :Bitbucket: `openpyxl/openpyxl `_. 338 | :Version: v2.6.1.dev 339 | 340 | openpyxl_ is a Python library to read/write Excel 2010 xlsx/xlsm/xltx/xltm files. 341 | Tu warn about deprecation, this library uses a home-made ``@deprecated`` decorator. 342 | 343 | The implementation of this decorator is an adapted copy of the first version of Tantale’s ``@deprecated`` decorator. 344 | It has the enhancement to update the docstring of the decorated function. 345 | So, this is similar to the function :func:`deprecated.sphinx.deprecated`. 346 | 347 | .. code-block:: python 348 | 349 | string_types = (type(b''), type(u'')) 350 | def deprecated(reason): 351 | 352 | if isinstance(reason, string_types): 353 | 354 | def decorator(func1): 355 | 356 | if inspect.isclass(func1): 357 | fmt1 = "Call to deprecated class {name} ({reason})." 358 | else: 359 | fmt1 = "Call to deprecated function {name} ({reason})." 360 | 361 | @wraps(func1) 362 | def new_func1(*args, **kwargs): 363 | #warnings.simplefilter('default', DeprecationWarning) 364 | warnings.warn( 365 | fmt1.format(name=func1.__name__, reason=reason), 366 | category=DeprecationWarning, 367 | stacklevel=2 368 | ) 369 | return func1(*args, **kwargs) 370 | 371 | # Enhance docstring with a deprecation note 372 | deprecationNote = "\n\n.. note::\n Deprecated: " + reason 373 | if new_func1.__doc__: 374 | new_func1.__doc__ += deprecationNote 375 | else: 376 | new_func1.__doc__ = deprecationNote 377 | return new_func1 378 | 379 | return decorator 380 | 381 | elif inspect.isclass(reason) or inspect.isfunction(reason): 382 | raise TypeError("Reason for deprecation must be supplied") 383 | 384 | else: 385 | raise TypeError(repr(type(reason))) 386 | 387 | As a conclusion: 388 | 389 | - The openpyxl library uses a decorator to deprecate functions. 390 | - It uses the category :exc:`DeprecationWarning`. 391 | - The decorator update the docstring and add a ``.. note::`` directive, 392 | which is visible in the documentation. 393 | 394 | .. _openpyxl: https://openpyxl.readthedocs.io/ 395 | --------------------------------------------------------------------------------