├── .coveragerc ├── .gitignore ├── .gitreview ├── .mailmap ├── .pre-commit-config.yaml ├── .stestr.conf ├── .zuul.yaml ├── CONTRIBUTING.rst ├── HACKING.rst ├── LICENSE ├── README.rst ├── debtcollector ├── __init__.py ├── _utils.py ├── fixtures │ ├── __init__.py │ └── disable.py ├── moves.py ├── removals.py ├── renames.py ├── tests │ ├── __init__.py │ ├── base.py │ └── test_deprecation.py └── updating.py ├── doc ├── requirements.txt └── source │ ├── conf.py │ ├── contributor │ └── index.rst │ ├── index.rst │ ├── install │ └── index.rst │ ├── reference │ └── index.rst │ └── user │ ├── history.rst │ ├── index.rst │ └── usage.rst ├── releasenotes ├── notes │ ├── add-reno-996dd44974d53238.yaml │ ├── drop-python-2-7-73d3113c69d724d6.yaml │ └── remove-py38-301fc55249d7333e.yaml └── source │ ├── _static │ └── .placeholder │ ├── _templates │ └── .placeholder │ ├── conf.py │ ├── index.rst │ ├── ocata.rst │ ├── pike.rst │ ├── queens.rst │ ├── rocky.rst │ ├── stein.rst │ ├── train.rst │ ├── unreleased.rst │ ├── ussuri.rst │ └── victoria.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = debtcollector 4 | omit = debtcollector/tests/* 5 | 6 | [report] 7 | ignore_errors = True 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | .stestr/ 28 | .venv 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Complexity 39 | output/*.html 40 | output/*/index.html 41 | 42 | # Sphinx 43 | doc/build 44 | 45 | # pbr generates these 46 | AUTHORS 47 | ChangeLog 48 | 49 | # Editors 50 | *~ 51 | .*.swp 52 | .*sw? 53 | 54 | # IPython Files 55 | .ipynb_checkpoints/ 56 | *.ipynb 57 | 58 | # reno build 59 | releasenotes/build 60 | RELEASENOTES.rst 61 | releasenotes/notes/reno.cache 62 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/debtcollector.git 5 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # Format is: 2 | # 3 | # -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | # Replaces or checks mixed line ending 7 | - id: mixed-line-ending 8 | args: ['--fix', 'lf'] 9 | exclude: '.*\.(svg)$' 10 | # Forbid files which have a UTF-8 byte-order marker 11 | - id: check-byte-order-marker 12 | # Checks that non-binary executables have a proper shebang 13 | - id: check-executables-have-shebangs 14 | # Check for files that contain merge conflict strings. 15 | - id: check-merge-conflict 16 | # Check for debugger imports and py37+ breakpoint() 17 | # calls in python source 18 | - id: debug-statements 19 | - id: check-yaml 20 | files: .*\.(yaml|yml)$ 21 | - repo: https://opendev.org/openstack/hacking 22 | rev: 7.0.0 23 | hooks: 24 | - id: hacking 25 | additional_dependencies: [] 26 | - repo: https://github.com/PyCQA/doc8 27 | rev: v1.1.2 28 | hooks: 29 | - id: doc8 30 | files: doc/source/.*\.rst$ 31 | - repo: https://github.com/asottile/pyupgrade 32 | rev: v3.18.0 33 | hooks: 34 | - id: pyupgrade 35 | args: [--py3-only] 36 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./debtcollector/tests 3 | top_dir=./ 4 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - project: 2 | templates: 3 | - check-requirements 4 | - lib-forward-testing-python3 5 | - openstack-python3-jobs 6 | - periodic-stable-jobs 7 | - publish-openstack-docs-pti 8 | - release-notes-jobs-python3 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you would like to contribute to the development of OpenStack, 2 | you must follow the steps in this page: 3 | https://docs.openstack.org/infra/manual/developers.html 4 | 5 | Once those steps have been completed, changes to OpenStack 6 | should be submitted for review via the Gerrit tool, following 7 | the workflow documented at: 8 | https://docs.openstack.org/infra/manual/developers.html#development-workflow 9 | 10 | Pull requests submitted through GitHub will be ignored. 11 | 12 | Bugs should be filed on Launchpad, not GitHub: 13 | https://bugs.launchpad.net/debtcollector 14 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | debtcollector Style Commandments 2 | ================================ 3 | 4 | Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Team and repository tags 3 | ======================== 4 | 5 | .. image:: https://governance.openstack.org/tc/badges/debtcollector.svg 6 | :target: https://governance.openstack.org/tc/reference/tags/index.html 7 | 8 | .. Change things from this point on 9 | 10 | Debtcollector 11 | ============= 12 | 13 | .. image:: https://img.shields.io/pypi/v/debtcollector.svg 14 | :target: https://pypi.org/project/debtcollector/ 15 | :alt: Latest Version 16 | 17 | A collection of Python deprecation patterns and strategies that help you 18 | collect your technical debt in a non-destructive manner. The goal of this 19 | library is to provide well documented developer facing deprecation 20 | patterns that start of with a basic set and can expand into a larger 21 | set of patterns as time goes on. The desired output of these patterns 22 | is to apply the warnings module to emit DeprecationWarning or PendingDeprecationWarning 23 | or similar derivative to developers using libraries (or potentially 24 | applications) about future deprecations. 25 | 26 | 27 | * Free software: Apache license 28 | * Documentation: https://docs.openstack.org/debtcollector/latest 29 | * Source: https://opendev.org/openstack/debtcollector 30 | * Bugs: https://bugs.launchpad.net/debtcollector 31 | * Release Notes: https://docs.openstack.org/releasenotes/debtcollector 32 | -------------------------------------------------------------------------------- /debtcollector/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import importlib.metadata 14 | 15 | from debtcollector import _utils 16 | 17 | __version__ = importlib.metadata.version('debtcollector') 18 | 19 | 20 | def deprecate(prefix, postfix=None, message=None, 21 | version=None, removal_version=None, 22 | stacklevel=3, category=DeprecationWarning): 23 | """Helper to deprecate some thing using generated message format. 24 | 25 | :param prefix: prefix string used as the prefix of the output message 26 | :param postfix: postfix string used as the postfix of the output message 27 | :param message: message string used as ending contents of the deprecate 28 | message 29 | :param version: version string (represents the version this 30 | deprecation was created in) 31 | :param removal_version: version string (represents the version this 32 | deprecation will be removed in); a string of '?' 33 | will denote this will be removed in some future 34 | unknown version 35 | :param stacklevel: stacklevel used in the :func:`warnings.warn` function 36 | to locate where the users code is in the 37 | :func:`warnings.warn` call 38 | :param category: the :mod:`warnings` category to use, defaults to 39 | :py:class:`DeprecationWarning` if not provided 40 | """ 41 | out_message = _utils.generate_message(prefix, postfix=postfix, 42 | version=version, message=message, 43 | removal_version=removal_version) 44 | _utils.deprecation(out_message, stacklevel=stacklevel, 45 | category=category) 46 | -------------------------------------------------------------------------------- /debtcollector/_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import functools 16 | import inspect 17 | import warnings 18 | 19 | # See https://docs.python.org/3/library/builtins.html 20 | _BUILTIN_MODULES = ('builtins', 'exceptions') 21 | _enabled = True 22 | 23 | 24 | def deprecation(message, stacklevel=None, category=None): 25 | """Warns about some type of deprecation that has been (or will be) made. 26 | 27 | This helper function makes it easier to interact with the warnings module 28 | by standardizing the arguments that the warning function receives so that 29 | it is easier to use. 30 | 31 | This should be used to emit warnings to users (users can easily turn these 32 | warnings off/on, see https://docs.python.org/2/library/warnings.html 33 | as they see fit so that the messages do not fill up the users logs with 34 | warnings that they do not wish to see in production) about functions, 35 | methods, attributes or other code that is deprecated and will be removed 36 | in a future release (this is done using these warnings to avoid breaking 37 | existing users of those functions, methods, code; which a library should 38 | avoid doing by always giving at *least* N + 1 release for users to address 39 | the deprecation warnings). 40 | """ 41 | if not _enabled: 42 | return 43 | if category is None: 44 | category = DeprecationWarning 45 | if stacklevel is None: 46 | warnings.warn(message, category=category) 47 | else: 48 | warnings.warn(message, category=category, stacklevel=stacklevel) 49 | 50 | 51 | def get_qualified_name(obj): 52 | # Prefer the py3.x name (if we can get at it...) 53 | try: 54 | return (True, obj.__qualname__) 55 | except AttributeError: 56 | return (False, obj.__name__) 57 | 58 | 59 | def generate_message(prefix, postfix=None, message=None, 60 | version=None, removal_version=None): 61 | """Helper to generate a common message 'style' for deprecation helpers.""" 62 | message_components = [prefix] 63 | if version: 64 | message_components.append(" in version '%s'" % version) 65 | if removal_version: 66 | if removal_version == "?": 67 | message_components.append(" and will be removed in a future" 68 | " version") 69 | else: 70 | message_components.append(" and will be removed in version '%s'" 71 | % removal_version) 72 | if postfix: 73 | message_components.append(postfix) 74 | if message: 75 | message_components.append(": %s" % message) 76 | return ''.join(message_components) 77 | 78 | 79 | def get_assigned(decorator): 80 | """Helper to fix/workaround https://bugs.python.org/issue3445""" 81 | return functools.WRAPPER_ASSIGNMENTS 82 | 83 | 84 | def get_class_name(obj, fully_qualified=True): 85 | """Get class name for object. 86 | 87 | If object is a type, fully qualified name of the type is returned. 88 | Else, fully qualified name of the type of the object is returned. 89 | For builtin types, just name is returned. 90 | """ 91 | if not isinstance(obj, type): 92 | obj = type(obj) 93 | try: 94 | built_in = obj.__module__ in _BUILTIN_MODULES 95 | except AttributeError: 96 | pass 97 | else: 98 | if built_in: 99 | return obj.__name__ 100 | 101 | if fully_qualified and hasattr(obj, '__module__'): 102 | return '{}.{}'.format(obj.__module__, obj.__name__) 103 | else: 104 | return obj.__name__ 105 | 106 | 107 | def get_method_self(method): 108 | """Gets the ``self`` object attached to this method (or none).""" 109 | if not inspect.ismethod(method): 110 | return None 111 | try: 112 | return getattr(method, '__self__') 113 | except AttributeError: 114 | return None 115 | 116 | 117 | def get_callable_name(function): 118 | """Generate a name from callable. 119 | 120 | Tries to do the best to guess fully qualified callable name. 121 | """ 122 | method_self = get_method_self(function) 123 | if method_self is not None: 124 | # This is a bound method. 125 | if isinstance(method_self, type): 126 | # This is a bound class method. 127 | im_class = method_self 128 | else: 129 | im_class = type(method_self) 130 | try: 131 | parts = (im_class.__module__, function.__qualname__) 132 | except AttributeError: 133 | parts = (im_class.__module__, im_class.__name__, function.__name__) 134 | elif inspect.ismethod(function) or inspect.isfunction(function): 135 | # This could be a function, a static method, a unbound method... 136 | try: 137 | parts = (function.__module__, function.__qualname__) 138 | except AttributeError: 139 | if hasattr(function, 'im_class'): 140 | # This is a unbound method, which exists only in python 2.x 141 | im_class = function.im_class 142 | parts = (im_class.__module__, 143 | im_class.__name__, function.__name__) 144 | else: 145 | parts = (function.__module__, function.__name__) 146 | else: 147 | im_class = type(function) 148 | if im_class is type: 149 | im_class = function 150 | try: 151 | parts = (im_class.__module__, im_class.__qualname__) 152 | except AttributeError: 153 | parts = (im_class.__module__, im_class.__name__) 154 | # When running under sphinx it appears this can be none? if so just 155 | # don't include it... 156 | mod, rest = (parts[0], parts[1:]) 157 | if not mod: 158 | return '.'.join(rest) 159 | else: 160 | return '.'.join(parts) 161 | -------------------------------------------------------------------------------- /debtcollector/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/debtcollector/395e8c52b66c2b051127b62e1964190542bc4cb3/debtcollector/fixtures/__init__.py -------------------------------------------------------------------------------- /debtcollector/fixtures/disable.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import fixtures 16 | 17 | from debtcollector import _utils 18 | 19 | 20 | class DisableFixture(fixtures.Fixture): 21 | """Fixture that disables debtcollector triggered warnings. 22 | 23 | This does **not** disable warnings calls emitted by other libraries. 24 | 25 | This can be used like:: 26 | 27 | from debtcollector.fixtures import disable 28 | 29 | with disable.DisableFixture(): 30 | 31 | """ 32 | 33 | def _setUp(self): 34 | self.addCleanup(setattr, _utils, "_enabled", True) 35 | _utils._enabled = False 36 | -------------------------------------------------------------------------------- /debtcollector/moves.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import functools 16 | import inspect 17 | 18 | import wrapt 19 | 20 | from debtcollector import _utils 21 | 22 | _KIND_MOVED_PREFIX_TPL = "%s '%s' has moved to '%s'" 23 | _CLASS_MOVED_PREFIX_TPL = "Class '%s' has moved to '%s'" 24 | _MOVED_CALLABLE_POSTFIX = "()" 25 | _FUNC_MOVED_PREFIX_TPL = "Function '%s' has moved to '%s'" 26 | 27 | 28 | def _moved_decorator(kind, new_attribute_name, message=None, 29 | version=None, removal_version=None, stacklevel=3, 30 | attr_postfix=None, category=None): 31 | """Decorates a method/property that was moved to another location.""" 32 | 33 | def decorator(f): 34 | fully_qualified, old_attribute_name = _utils.get_qualified_name(f) 35 | if attr_postfix: 36 | old_attribute_name += attr_postfix 37 | 38 | @wrapt.decorator 39 | def wrapper(wrapped, instance, args, kwargs): 40 | base_name = _utils.get_class_name(wrapped, fully_qualified=False) 41 | if fully_qualified: 42 | old_name = old_attribute_name 43 | else: 44 | old_name = ".".join((base_name, old_attribute_name)) 45 | new_name = ".".join((base_name, new_attribute_name)) 46 | prefix = _KIND_MOVED_PREFIX_TPL % (kind, old_name, new_name) 47 | out_message = _utils.generate_message( 48 | prefix, message=message, 49 | version=version, removal_version=removal_version) 50 | _utils.deprecation(out_message, stacklevel=stacklevel, 51 | category=category) 52 | return wrapped(*args, **kwargs) 53 | 54 | return wrapper(f) 55 | 56 | return decorator 57 | 58 | 59 | def moved_function(new_func, old_func_name, old_module_name, 60 | message=None, version=None, removal_version=None, 61 | stacklevel=3, category=None): 62 | """Deprecates a function that was moved to another location. 63 | 64 | This generates a wrapper around ``new_func`` that will emit a deprecation 65 | warning when called. The warning message will include the new location 66 | to obtain the function from. 67 | """ 68 | new_func_full_name = _utils.get_callable_name(new_func) 69 | new_func_full_name += _MOVED_CALLABLE_POSTFIX 70 | old_func_full_name = ".".join([old_module_name, old_func_name]) 71 | old_func_full_name += _MOVED_CALLABLE_POSTFIX 72 | prefix = _FUNC_MOVED_PREFIX_TPL % (old_func_full_name, new_func_full_name) 73 | out_message = _utils.generate_message(prefix, 74 | message=message, version=version, 75 | removal_version=removal_version) 76 | 77 | @functools.wraps(new_func, assigned=_utils.get_assigned(new_func)) 78 | def old_new_func(*args, **kwargs): 79 | _utils.deprecation(out_message, stacklevel=stacklevel, 80 | category=category) 81 | return new_func(*args, **kwargs) 82 | 83 | old_new_func.__name__ = old_func_name 84 | old_new_func.__module__ = old_module_name 85 | return old_new_func 86 | 87 | 88 | class moved_read_only_property: 89 | """Descriptor for read-only properties moved to another location. 90 | 91 | This works like the ``@property`` descriptor but can be used instead to 92 | provide the same functionality and also interact with the :mod:`warnings` 93 | module to warn when a property is accessed, so that users of those 94 | properties can know that a previously read-only property at a prior 95 | location/name has moved to another location/name. 96 | 97 | :param old_name: old attribute location/name 98 | :param new_name: new attribute location/name 99 | :param version: version string (represents the version this deprecation 100 | was created in) 101 | :param removal_version: version string (represents the version this 102 | deprecation will be removed in); a string 103 | of '?' will denote this will be removed in 104 | some future unknown version 105 | :param stacklevel: stacklevel used in the :func:`warnings.warn` function 106 | to locate where the users code is when reporting the 107 | deprecation call (the default being 3) 108 | :param category: the :mod:`warnings` category to use, defaults to 109 | :py:class:`DeprecationWarning` if not provided 110 | """ 111 | 112 | def __init__(self, old_name, new_name, 113 | version=None, removal_version=None, 114 | stacklevel=3, category=None): 115 | self._old_name = old_name 116 | self._new_name = new_name 117 | self._message = _utils.generate_message( 118 | "Read-only property '%s' has moved" 119 | " to '%s'" % (self._old_name, self._new_name), 120 | version=version, removal_version=removal_version) 121 | self._stacklevel = stacklevel 122 | self._category = category 123 | 124 | def __get__(self, instance, owner): 125 | _utils.deprecation(self._message, 126 | stacklevel=self._stacklevel, 127 | category=self._category) 128 | # This handles the descriptor being applied on a 129 | # instance or a class and makes both work correctly... 130 | if instance is not None: 131 | real_owner = instance 132 | else: 133 | real_owner = owner 134 | return getattr(real_owner, self._new_name) 135 | 136 | 137 | def moved_method(new_method_name, message=None, 138 | version=None, removal_version=None, stacklevel=3, 139 | category=None): 140 | """Decorates an *instance* method that was moved to another location.""" 141 | if not new_method_name.endswith(_MOVED_CALLABLE_POSTFIX): 142 | new_method_name += _MOVED_CALLABLE_POSTFIX 143 | return _moved_decorator('Method', new_method_name, message=message, 144 | version=version, removal_version=removal_version, 145 | stacklevel=stacklevel, 146 | attr_postfix=_MOVED_CALLABLE_POSTFIX, 147 | category=category) 148 | 149 | 150 | def moved_property(new_attribute_name, message=None, 151 | version=None, removal_version=None, stacklevel=3, 152 | category=None): 153 | """Decorates an *instance* property that was moved to another location.""" 154 | return _moved_decorator('Property', new_attribute_name, message=message, 155 | version=version, removal_version=removal_version, 156 | stacklevel=stacklevel, category=category) 157 | 158 | 159 | def moved_class(new_class, old_class_name, old_module_name, 160 | message=None, version=None, removal_version=None, 161 | stacklevel=3, category=None): 162 | """Deprecates a class that was moved to another location. 163 | 164 | This creates a 'new-old' type that can be used for a 165 | deprecation period that can be inherited from. This will emit warnings 166 | when the old locations class is initialized, telling where the new and 167 | improved location for the old class now is. 168 | """ 169 | 170 | if not inspect.isclass(new_class): 171 | _qual, type_name = _utils.get_qualified_name(type(new_class)) 172 | raise TypeError("Unexpected class type '%s' (expected" 173 | " class type only)" % type_name) 174 | 175 | old_name = ".".join((old_module_name, old_class_name)) 176 | new_name = _utils.get_class_name(new_class) 177 | prefix = _CLASS_MOVED_PREFIX_TPL % (old_name, new_name) 178 | out_message = _utils.generate_message( 179 | prefix, message=message, version=version, 180 | removal_version=removal_version) 181 | 182 | def decorator(f): 183 | 184 | @functools.wraps(f, assigned=_utils.get_assigned(f)) 185 | def wrapper(self, *args, **kwargs): 186 | _utils.deprecation(out_message, stacklevel=stacklevel, 187 | category=category) 188 | return f(self, *args, **kwargs) 189 | 190 | return wrapper 191 | 192 | old_class = type(old_class_name, (new_class,), {}) 193 | old_class.__module__ = old_module_name 194 | old_class.__init__ = decorator(old_class.__init__) 195 | return old_class 196 | -------------------------------------------------------------------------------- /debtcollector/removals.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import functools 16 | import inspect 17 | 18 | import wrapt 19 | 20 | from debtcollector import _utils 21 | 22 | 23 | def _get_qualified_name(obj): 24 | return _utils.get_qualified_name(obj)[1] 25 | 26 | 27 | def _fetch_first_result(fget, fset, fdel, apply_func, value_not_found=None): 28 | """Fetch first non-none/empty result of applying ``apply_func``.""" 29 | for f in filter(None, (fget, fset, fdel)): 30 | result = apply_func(f) 31 | if result: 32 | return result 33 | return value_not_found 34 | 35 | 36 | class removed_property: 37 | """Property descriptor that deprecates a property. 38 | 39 | This works like the ``@property`` descriptor but can be used instead to 40 | provide the same functionality and also interact with the :mod:`warnings` 41 | module to warn when a property is accessed, set and/or deleted. 42 | 43 | :param message: string used as ending contents of the deprecate message 44 | :param version: version string (represents the version this deprecation 45 | was created in) 46 | :param removal_version: version string (represents the version this 47 | deprecation will be removed in); a string 48 | of '?' will denote this will be removed in 49 | some future unknown version 50 | :param stacklevel: stacklevel used in the :func:`warnings.warn` function 51 | to locate where the users code is when reporting the 52 | deprecation call (the default being 3) 53 | :param category: the :mod:`warnings` category to use, defaults to 54 | :py:class:`DeprecationWarning` if not provided 55 | """ 56 | 57 | # Message templates that will be turned into real messages as needed. 58 | _PROPERTY_GONE_TPLS = { 59 | 'set': "Setting the '%s' property is deprecated", 60 | 'get': "Reading the '%s' property is deprecated", 61 | 'delete': "Deleting the '%s' property is deprecated", 62 | } 63 | 64 | def __init__(self, fget=None, fset=None, fdel=None, doc=None, 65 | stacklevel=3, category=DeprecationWarning, 66 | version=None, removal_version=None, message=None): 67 | self.fset = fset 68 | self.fget = fget 69 | self.fdel = fdel 70 | self.stacklevel = stacklevel 71 | self.category = category 72 | self.version = version 73 | self.removal_version = removal_version 74 | self.message = message 75 | if doc is None and inspect.isfunction(fget): 76 | doc = getattr(fget, '__doc__', None) 77 | self._message_cache = {} 78 | self.__doc__ = doc 79 | 80 | def _fetch_message_from_cache(self, kind): 81 | try: 82 | out_message = self._message_cache[kind] 83 | except KeyError: 84 | prefix_tpl = self._PROPERTY_GONE_TPLS[kind] 85 | prefix = prefix_tpl % _fetch_first_result( 86 | self.fget, self.fset, self.fdel, _get_qualified_name, 87 | value_not_found="???") 88 | out_message = _utils.generate_message( 89 | prefix, message=self.message, version=self.version, 90 | removal_version=self.removal_version) 91 | self._message_cache[kind] = out_message 92 | return out_message 93 | 94 | def __call__(self, fget, **kwargs): 95 | self.fget = fget 96 | self.message = kwargs.get('message', self.message) 97 | self.version = kwargs.get('version', self.version) 98 | self.removal_version = kwargs.get('removal_version', 99 | self.removal_version) 100 | self.stacklevel = kwargs.get('stacklevel', self.stacklevel) 101 | self.category = kwargs.get('category', self.category) 102 | self.__doc__ = kwargs.get('doc', 103 | getattr(fget, '__doc__', self.__doc__)) 104 | # Regenerate all the messages... 105 | self._message_cache.clear() 106 | return self 107 | 108 | def __delete__(self, obj): 109 | if self.fdel is None: 110 | raise AttributeError("can't delete attribute") 111 | out_message = self._fetch_message_from_cache('delete') 112 | _utils.deprecation(out_message, stacklevel=self.stacklevel, 113 | category=self.category) 114 | self.fdel(obj) 115 | 116 | def __set__(self, obj, value): 117 | if self.fset is None: 118 | raise AttributeError("can't set attribute") 119 | out_message = self._fetch_message_from_cache('set') 120 | _utils.deprecation(out_message, stacklevel=self.stacklevel, 121 | category=self.category) 122 | self.fset(obj, value) 123 | 124 | def __get__(self, obj, value): 125 | if obj is None: 126 | return self 127 | if self.fget is None: 128 | raise AttributeError("unreadable attribute") 129 | out_message = self._fetch_message_from_cache('get') 130 | _utils.deprecation(out_message, stacklevel=self.stacklevel, 131 | category=self.category) 132 | return self.fget(obj) 133 | 134 | def getter(self, fget): 135 | o = type(self)(fget, self.fset, self.fdel, self.__doc__) 136 | o.message = self.message 137 | o.version = self.version 138 | o.stacklevel = self.stacklevel 139 | o.removal_version = self.removal_version 140 | o.category = self.category 141 | return o 142 | 143 | def setter(self, fset): 144 | o = type(self)(self.fget, fset, self.fdel, self.__doc__) 145 | o.message = self.message 146 | o.version = self.version 147 | o.stacklevel = self.stacklevel 148 | o.removal_version = self.removal_version 149 | o.category = self.category 150 | return o 151 | 152 | def deleter(self, fdel): 153 | o = type(self)(self.fget, self.fset, fdel, self.__doc__) 154 | o.message = self.message 155 | o.version = self.version 156 | o.stacklevel = self.stacklevel 157 | o.removal_version = self.removal_version 158 | o.category = self.category 159 | return o 160 | 161 | 162 | def remove(f=None, message=None, version=None, removal_version=None, 163 | stacklevel=3, category=None): 164 | """Decorates a function, method, or class to emit a deprecation warning 165 | 166 | Due to limitations of the wrapt library (and python) itself, if this 167 | is applied to subclasses of metaclasses then it likely will not work 168 | as expected. More information can be found at bug #1520397 to see if 169 | this situation affects your usage of this *universal* decorator, for 170 | this specific scenario please use :py:func:`.removed_class` instead. 171 | 172 | :param str message: A message to include in the deprecation warning 173 | :param str version: Specify what version the removed function is present in 174 | :param str removal_version: What version the function will be removed. If 175 | '?' is used this implies an undefined future 176 | version 177 | :param int stacklevel: How many entries deep in the call stack before 178 | ignoring 179 | :param type category: warnings message category (this defaults to 180 | ``DeprecationWarning`` when none is provided) 181 | """ 182 | if f is None: 183 | return functools.partial(remove, message=message, 184 | version=version, 185 | removal_version=removal_version, 186 | stacklevel=stacklevel, 187 | category=category) 188 | 189 | @wrapt.decorator 190 | def wrapper(f, instance, args, kwargs): 191 | qualified, f_name = _utils.get_qualified_name(f) 192 | if qualified: 193 | if inspect.isclass(f): 194 | prefix_pre = "Using class" 195 | thing_post = '' 196 | else: 197 | prefix_pre = "Using function/method" 198 | thing_post = '()' 199 | if not qualified: 200 | prefix_pre = "Using function/method" 201 | base_name = None 202 | if instance is None: 203 | # Decorator was used on a class 204 | if inspect.isclass(f): 205 | prefix_pre = "Using class" 206 | thing_post = '' 207 | module_name = _get_qualified_name(inspect.getmodule(f)) 208 | if module_name == '__main__': 209 | f_name = _utils.get_class_name( 210 | f, fully_qualified=False) 211 | else: 212 | f_name = _utils.get_class_name( 213 | f, fully_qualified=True) 214 | # Decorator was a used on a function 215 | else: 216 | thing_post = '()' 217 | module_name = _get_qualified_name(inspect.getmodule(f)) 218 | if module_name != '__main__': 219 | f_name = _utils.get_callable_name(f) 220 | # Decorator was used on a classmethod or instancemethod 221 | else: 222 | thing_post = '()' 223 | base_name = _utils.get_class_name(instance, 224 | fully_qualified=False) 225 | if base_name: 226 | thing_name = ".".join([base_name, f_name]) 227 | else: 228 | thing_name = f_name 229 | else: 230 | thing_name = f_name 231 | if thing_post: 232 | thing_name += thing_post 233 | prefix = prefix_pre + " '%s' is deprecated" % (thing_name) 234 | out_message = _utils.generate_message( 235 | prefix, 236 | version=version, 237 | removal_version=removal_version, 238 | message=message) 239 | _utils.deprecation(out_message, 240 | stacklevel=stacklevel, category=category) 241 | return f(*args, **kwargs) 242 | return wrapper(f) 243 | 244 | 245 | def removed_kwarg(old_name, message=None, 246 | version=None, removal_version=None, stacklevel=3, 247 | category=None): 248 | """Decorates a kwarg accepting function to deprecate a removed kwarg.""" 249 | 250 | prefix = "Using the '%s' argument is deprecated" % old_name 251 | out_message = _utils.generate_message( 252 | prefix, postfix=None, message=message, version=version, 253 | removal_version=removal_version) 254 | 255 | @wrapt.decorator 256 | def wrapper(f, instance, args, kwargs): 257 | if old_name in kwargs: 258 | _utils.deprecation(out_message, 259 | stacklevel=stacklevel, category=category) 260 | return f(*args, **kwargs) 261 | 262 | return wrapper 263 | 264 | 265 | def removed_class(cls_name, replacement=None, message=None, 266 | version=None, removal_version=None, stacklevel=3, 267 | category=None): 268 | """Decorates a class to denote that it will be removed at some point.""" 269 | 270 | def _wrap_it(old_init, out_message): 271 | 272 | @functools.wraps(old_init, assigned=_utils.get_assigned(old_init)) 273 | def new_init(self, *args, **kwargs): 274 | _utils.deprecation(out_message, stacklevel=stacklevel, 275 | category=category) 276 | return old_init(self, *args, **kwargs) 277 | 278 | return new_init 279 | 280 | def _check_it(cls): 281 | if not inspect.isclass(cls): 282 | _qual, type_name = _utils.get_qualified_name(type(cls)) 283 | raise TypeError("Unexpected class type '%s' (expected" 284 | " class type only)" % type_name) 285 | 286 | def _cls_decorator(cls): 287 | _check_it(cls) 288 | out_message = _utils.generate_message( 289 | "Using class '%s' (either directly or via inheritance)" 290 | " is deprecated" % cls_name, postfix=None, message=message, 291 | version=version, removal_version=removal_version) 292 | cls.__init__ = _wrap_it(cls.__init__, out_message) 293 | return cls 294 | 295 | return _cls_decorator 296 | 297 | 298 | def removed_module(module, replacement=None, message=None, 299 | version=None, removal_version=None, stacklevel=3, 300 | category=None): 301 | """Helper to be called inside a module to emit a deprecation warning 302 | 303 | :param str replacment: A location (or information about) of any potential 304 | replacement for the removed module (if applicable) 305 | :param str message: A message to include in the deprecation warning 306 | :param str version: Specify what version the removed module is present in 307 | :param str removal_version: What version the module will be removed. If 308 | '?' is used this implies an undefined future 309 | version 310 | :param int stacklevel: How many entries deep in the call stack before 311 | ignoring 312 | :param type category: warnings message category (this defaults to 313 | ``DeprecationWarning`` when none is provided) 314 | """ 315 | if inspect.ismodule(module): 316 | module_name = _get_qualified_name(module) 317 | elif isinstance(module, str): 318 | module_name = module 319 | else: 320 | _qual, type_name = _utils.get_qualified_name(type(module)) 321 | raise TypeError("Unexpected module type '%s' (expected string or" 322 | " module type only)" % type_name) 323 | prefix = "The '%s' module usage is deprecated" % module_name 324 | if replacement: 325 | postfix = ", please use %s instead" % replacement 326 | else: 327 | postfix = None 328 | out_message = _utils.generate_message(prefix, 329 | postfix=postfix, message=message, 330 | version=version, 331 | removal_version=removal_version) 332 | _utils.deprecation(out_message, 333 | stacklevel=stacklevel, category=category) 334 | -------------------------------------------------------------------------------- /debtcollector/renames.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import wrapt 16 | 17 | from debtcollector import _utils 18 | 19 | _KWARG_RENAMED_POSTFIX_TPL = ", please use the '%s' argument instead" 20 | _KWARG_RENAMED_PREFIX_TPL = "Using the '%s' argument is deprecated" 21 | 22 | 23 | def renamed_kwarg(old_name, new_name, message=None, 24 | version=None, removal_version=None, stacklevel=3, 25 | category=None, replace=False): 26 | """Decorates a kwarg accepting function to deprecate a renamed kwarg.""" 27 | 28 | prefix = _KWARG_RENAMED_PREFIX_TPL % old_name 29 | postfix = _KWARG_RENAMED_POSTFIX_TPL % new_name 30 | out_message = _utils.generate_message( 31 | prefix, postfix=postfix, message=message, version=version, 32 | removal_version=removal_version) 33 | 34 | @wrapt.decorator 35 | def decorator(wrapped, instance, args, kwargs): 36 | if old_name in kwargs: 37 | _utils.deprecation(out_message, 38 | stacklevel=stacklevel, category=category) 39 | if replace: 40 | kwargs.setdefault(new_name, kwargs.pop(old_name)) 41 | return wrapped(*args, **kwargs) 42 | 43 | return decorator 44 | -------------------------------------------------------------------------------- /debtcollector/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/debtcollector/395e8c52b66c2b051127b62e1964190542bc4cb3/debtcollector/tests/__init__.py -------------------------------------------------------------------------------- /debtcollector/tests/base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010-2011 OpenStack Foundation 2 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import unittest 17 | 18 | 19 | class TestCase(unittest.TestCase): 20 | 21 | """Test case base class for all unit tests.""" 22 | -------------------------------------------------------------------------------- /debtcollector/tests/test_deprecation.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import inspect 16 | import warnings 17 | 18 | import debtcollector 19 | from debtcollector.fixtures import disable 20 | from debtcollector import moves 21 | from debtcollector import removals 22 | from debtcollector import renames 23 | from debtcollector.tests import base as test_base 24 | from debtcollector import updating 25 | 26 | 27 | @renames.renamed_kwarg('blip', 'blop') 28 | def blip_blop(blip=1, blop=1): 29 | return (blip, blop) 30 | 31 | 32 | def blip_blop_unwrapped(blip=1, blop=1): 33 | return (blip, blop) 34 | 35 | 36 | @renames.renamed_kwarg('blip', 'blop', category=PendingDeprecationWarning) 37 | def blip_blop_2(blip=1, blop=1): 38 | return (blip, blop) 39 | 40 | 41 | @renames.renamed_kwarg('blip', 'blop', replace=True) 42 | def blip_blop_3(blop=1): 43 | return blop 44 | 45 | 46 | @updating.updated_kwarg_default_value('type', 'cat', 'feline') 47 | def blip_blop_blip(type='cat'): 48 | return "The %s meowed quietly" % type 49 | 50 | 51 | def blip_blop_blip_unwrapped(type='cat'): 52 | return "The %s meowed quietly" % type 53 | 54 | 55 | class WoofWoof: 56 | @property 57 | def bark(self): 58 | return 'woof' 59 | 60 | @property 61 | @moves.moved_property('bark') 62 | def burk(self): 63 | return self.bark 64 | 65 | @property 66 | @moves.moved_property('bark', category=PendingDeprecationWarning) 67 | def berk(self): 68 | return self.bark 69 | 70 | @removals.removed_kwarg('resp', message="Please use 'response' instead") 71 | @classmethod 72 | def factory(cls, resp=None, response=None): 73 | return 'super-duper' 74 | 75 | 76 | class KittyKat: 77 | 78 | @moves.moved_method('supermeow') 79 | def meow(self, volume=11): 80 | return self.supermeow(volume) 81 | 82 | @moves.moved_method('supermeow', category=PendingDeprecationWarning) 83 | def maow(self, volume=11): 84 | return self.supermeow(volume) 85 | 86 | def supermeow(self, volume=11): 87 | return 'supermeow' 88 | 89 | 90 | class Giraffe: 91 | color = 'orange' 92 | colour = moves.moved_read_only_property('colour', 'color') 93 | 94 | @property 95 | def height(self): 96 | return 2 97 | 98 | heightt = moves.moved_read_only_property('heightt', 'height') 99 | 100 | 101 | class NewHotness: 102 | def hot(self): 103 | return 'cold' 104 | 105 | 106 | @removals.remove() 107 | def crimson_lightning(fake_input=None): 108 | return fake_input 109 | 110 | 111 | def crimson_lightning_unwrapped(fake_input=None): 112 | return fake_input 113 | 114 | 115 | @removals.remove(category=PendingDeprecationWarning) 116 | def crimson_lightning_to_remove(fake_input=None): 117 | return fake_input 118 | 119 | 120 | @removals.remove() 121 | def red_comet(): 122 | return True 123 | 124 | 125 | @removals.remove(category=PendingDeprecationWarning) 126 | def blue_comet(): 127 | return True 128 | 129 | 130 | def yellow_sun(): 131 | """Yellow.""" 132 | return True 133 | 134 | 135 | yellowish_sun = moves.moved_function(yellow_sun, 'yellowish_sun', __name__) 136 | 137 | 138 | @removals.remove() 139 | class EFSF: 140 | pass 141 | 142 | 143 | @removals.remove(category=PendingDeprecationWarning) 144 | class EFSF_2: 145 | pass 146 | 147 | 148 | @removals.removed_class("StarLord") 149 | class StarLord: 150 | def __init__(self): 151 | self.name = "star" 152 | 153 | 154 | class StarLordJr(StarLord): 155 | def __init__(self, name): 156 | super().__init__() 157 | self.name = name 158 | 159 | 160 | class ThingB: 161 | @removals.remove() 162 | def black_tristars(self): 163 | pass 164 | 165 | @removals.removed_property 166 | def green_tristars(self): 167 | return 'green' 168 | 169 | @green_tristars.setter 170 | def green_tristars(self, value): 171 | pass 172 | 173 | @green_tristars.deleter 174 | def green_tristars(self): 175 | pass 176 | 177 | @removals.removed_property(message="stop using me") 178 | def green_blue_tristars(self): 179 | return 'green-blue' 180 | 181 | @removals.remove(category=PendingDeprecationWarning) 182 | def blue_tristars(self): 183 | pass 184 | 185 | @removals.remove() 186 | @classmethod 187 | def white_wolf(cls): 188 | pass 189 | 190 | @removals.remove(category=PendingDeprecationWarning) 191 | @classmethod 192 | def yellow_wolf(cls): 193 | pass 194 | 195 | @removals.remove() 196 | @staticmethod 197 | def blue_giant(): 198 | pass 199 | 200 | @removals.remove(category=PendingDeprecationWarning) 201 | @staticmethod 202 | def green_giant(): 203 | pass 204 | 205 | 206 | OldHotness = moves.moved_class(NewHotness, 'OldHotness', __name__) 207 | 208 | OldHotness2 = moves.moved_class(NewHotness, 'OldHotness', __name__, 209 | category=PendingDeprecationWarning) 210 | 211 | 212 | class DeprecateAnythingTest(test_base.TestCase): 213 | def test_generation(self): 214 | with warnings.catch_warnings(record=True) as capture: 215 | warnings.simplefilter("always") 216 | debtcollector.deprecate("Its broken") 217 | debtcollector.deprecate("Its really broken") 218 | self.assertEqual(2, len(capture)) 219 | 220 | 221 | class MovedInheritableClassTest(test_base.TestCase): 222 | def test_broken_type_class(self): 223 | self.assertRaises(TypeError, moves.moved_class, 'b', __name__) 224 | 225 | def test_basics(self): 226 | old = OldHotness() 227 | self.assertIsInstance(old, NewHotness) 228 | self.assertEqual('cold', old.hot()) 229 | 230 | def test_warnings_emitted_creation(self): 231 | with warnings.catch_warnings(record=True) as capture: 232 | warnings.simplefilter("always") 233 | OldHotness() 234 | self.assertEqual(1, len(capture)) 235 | w = capture[0] 236 | self.assertEqual(DeprecationWarning, w.category) 237 | 238 | def test_warnings_emitted_creation_pending(self): 239 | with warnings.catch_warnings(record=True) as capture: 240 | warnings.simplefilter("always") 241 | OldHotness2() 242 | self.assertEqual(1, len(capture)) 243 | w = capture[0] 244 | self.assertEqual(PendingDeprecationWarning, w.category) 245 | 246 | def test_existing_refer_subclass(self): 247 | 248 | class MyOldThing(OldHotness): 249 | pass 250 | 251 | with warnings.catch_warnings(record=True) as capture: 252 | warnings.simplefilter("always") 253 | MyOldThing() 254 | 255 | self.assertEqual(1, len(capture)) 256 | w = capture[0] 257 | self.assertEqual(DeprecationWarning, w.category) 258 | 259 | 260 | class MovedPropertyTest(test_base.TestCase): 261 | def test_basics(self): 262 | dog = WoofWoof() 263 | self.assertEqual('woof', dog.burk) 264 | self.assertEqual('woof', dog.bark) 265 | 266 | def test_readonly_move(self): 267 | with warnings.catch_warnings(record=True) as capture: 268 | warnings.simplefilter("always") 269 | self.assertEqual('orange', Giraffe.colour) 270 | g = Giraffe() 271 | self.assertEqual(2, g.heightt) 272 | self.assertEqual(2, len(capture)) 273 | 274 | def test_warnings_emitted(self): 275 | dog = WoofWoof() 276 | with warnings.catch_warnings(record=True) as capture: 277 | warnings.simplefilter("always") 278 | self.assertEqual('woof', dog.burk) 279 | self.assertEqual(1, len(capture)) 280 | w = capture[0] 281 | self.assertEqual(DeprecationWarning, w.category) 282 | 283 | def test_warnings_emitted_pending(self): 284 | dog = WoofWoof() 285 | with warnings.catch_warnings(record=True) as capture: 286 | warnings.simplefilter("always") 287 | self.assertEqual('woof', dog.berk) 288 | self.assertEqual(1, len(capture)) 289 | w = capture[0] 290 | self.assertEqual(PendingDeprecationWarning, w.category) 291 | 292 | def test_warnings_not_emitted(self): 293 | dog = WoofWoof() 294 | with warnings.catch_warnings(record=True) as capture: 295 | warnings.simplefilter("always") 296 | self.assertEqual('woof', dog.bark) 297 | self.assertEqual(0, len(capture)) 298 | 299 | 300 | class DisabledTest(test_base.TestCase): 301 | def test_basics(self): 302 | dog = WoofWoof() 303 | c = KittyKat() 304 | with warnings.catch_warnings(record=True) as capture: 305 | warnings.simplefilter("always") 306 | with disable.DisableFixture(): 307 | self.assertTrue(yellowish_sun()) 308 | self.assertEqual('woof', dog.berk) 309 | self.assertEqual('supermeow', c.meow()) 310 | self.assertEqual(0, len(capture)) 311 | 312 | 313 | class MovedFunctionTest(test_base.TestCase): 314 | def test_basics(self): 315 | self.assertTrue(yellowish_sun()) 316 | self.assertTrue(yellow_sun()) 317 | self.assertEqual("Yellow.", yellowish_sun.__doc__) 318 | 319 | def test_warnings_emitted(self): 320 | with warnings.catch_warnings(record=True) as capture: 321 | warnings.simplefilter("always") 322 | self.assertTrue(yellowish_sun()) 323 | self.assertEqual(1, len(capture)) 324 | w = capture[0] 325 | self.assertEqual(DeprecationWarning, w.category) 326 | 327 | 328 | class MovedMethodTest(test_base.TestCase): 329 | def test_basics(self): 330 | c = KittyKat() 331 | self.assertEqual('supermeow', c.meow()) 332 | self.assertEqual('supermeow', c.supermeow()) 333 | 334 | def test_warnings_emitted(self): 335 | c = KittyKat() 336 | with warnings.catch_warnings(record=True) as capture: 337 | warnings.simplefilter("always") 338 | self.assertEqual('supermeow', c.meow()) 339 | self.assertEqual(1, len(capture)) 340 | w = capture[0] 341 | self.assertEqual(DeprecationWarning, w.category) 342 | 343 | def test_warnings_emitted_pending(self): 344 | c = KittyKat() 345 | with warnings.catch_warnings(record=True) as capture: 346 | warnings.simplefilter("always") 347 | self.assertEqual('supermeow', c.maow()) 348 | self.assertEqual(1, len(capture)) 349 | w = capture[0] 350 | self.assertEqual(PendingDeprecationWarning, w.category) 351 | 352 | def test_warnings_not_emitted(self): 353 | c = KittyKat() 354 | with warnings.catch_warnings(record=True) as capture: 355 | warnings.simplefilter("always") 356 | self.assertEqual('supermeow', c.supermeow()) 357 | self.assertEqual(0, len(capture)) 358 | 359 | def test_keeps_argspec(self): 360 | self.assertEqual(inspect.getfullargspec(KittyKat.supermeow), 361 | inspect.getfullargspec(KittyKat.meow)) 362 | 363 | 364 | class RenamedKwargTest(test_base.TestCase): 365 | def test_basics(self): 366 | self.assertEqual((1, 1), blip_blop()) 367 | self.assertEqual((2, 1), blip_blop(blip=2)) 368 | self.assertEqual((1, 2), blip_blop(blop=2)) 369 | self.assertEqual((2, 2), blip_blop(blip=2, blop=2)) 370 | self.assertEqual(2, blip_blop_3(blip=2)) 371 | self.assertEqual(2, blip_blop_3(blop=2)) 372 | 373 | def test_warnings_emitted(self): 374 | with warnings.catch_warnings(record=True) as capture: 375 | warnings.simplefilter("always") 376 | self.assertEqual((2, 1), blip_blop(blip=2)) 377 | self.assertEqual(1, len(capture)) 378 | w = capture[0] 379 | self.assertEqual(DeprecationWarning, w.category) 380 | with warnings.catch_warnings(record=True) as capture: 381 | warnings.simplefilter("always") 382 | self.assertEqual(2, blip_blop_3(blip=2)) 383 | self.assertEqual(1, len(capture)) 384 | w = capture[0] 385 | self.assertEqual(DeprecationWarning, w.category) 386 | 387 | def test_warnings_emitted_classmethod(self): 388 | with warnings.catch_warnings(record=True) as capture: 389 | warnings.simplefilter("always") 390 | WoofWoof.factory(resp="hi") 391 | self.assertEqual(1, len(capture)) 392 | w = capture[0] 393 | self.assertEqual(DeprecationWarning, w.category) 394 | with warnings.catch_warnings(record=True) as capture: 395 | warnings.simplefilter("always") 396 | WoofWoof.factory(response="hi") 397 | self.assertEqual(0, len(capture)) 398 | 399 | def test_warnings_emitted_pending(self): 400 | with warnings.catch_warnings(record=True) as capture: 401 | warnings.simplefilter("always") 402 | self.assertEqual((2, 1), blip_blop_2(blip=2)) 403 | self.assertEqual(1, len(capture)) 404 | w = capture[0] 405 | self.assertEqual(PendingDeprecationWarning, w.category) 406 | 407 | def test_warnings_not_emitted(self): 408 | with warnings.catch_warnings(record=True) as capture: 409 | warnings.simplefilter("always") 410 | self.assertEqual((1, 2), blip_blop(blop=2)) 411 | self.assertEqual(0, len(capture)) 412 | with warnings.catch_warnings(record=True) as capture: 413 | warnings.simplefilter("always") 414 | self.assertEqual(2, blip_blop_3(blop=2)) 415 | self.assertEqual(0, len(capture)) 416 | 417 | def test_argspec(self): 418 | # The decorated function keeps its argspec. 419 | self.assertEqual(inspect.getfullargspec(blip_blop_unwrapped), 420 | inspect.getfullargspec(blip_blop)) 421 | 422 | 423 | class UpdatedArgsTest(test_base.TestCase): 424 | def test_basic(self): 425 | with warnings.catch_warnings(record=True) as capture: 426 | warnings.simplefilter("always") 427 | self.assertEqual('The cat meowed quietly', blip_blop_blip()) 428 | self.assertEqual(1, len(capture)) 429 | w = capture[0] 430 | self.assertEqual(FutureWarning, w.category) 431 | 432 | def test_kwarg_set(self): 433 | with warnings.catch_warnings(record=True) as capture: 434 | warnings.simplefilter("always") 435 | self.assertEqual( 436 | 'The kitten meowed quietly', 437 | blip_blop_blip(type='kitten')) 438 | self.assertEqual(0, len(capture)) 439 | 440 | def test_argspec_preserved(self): 441 | self.assertEqual(inspect.getfullargspec(blip_blop_blip_unwrapped), 442 | inspect.getfullargspec(blip_blop_blip)) 443 | 444 | 445 | class RemovalTests(test_base.TestCase): 446 | def test_function_args(self): 447 | self.assertEqual(666, crimson_lightning(666)) 448 | 449 | def test_function_noargs(self): 450 | self.assertTrue(red_comet()) 451 | 452 | def test_function_keeps_argspec(self): 453 | # The decorated function keeps its argspec. 454 | self.assertEqual( 455 | inspect.getfullargspec(crimson_lightning_unwrapped), 456 | inspect.getfullargspec(crimson_lightning)) 457 | 458 | def test_deprecated_kwarg(self): 459 | 460 | @removals.removed_kwarg('b') 461 | def f(b=2): 462 | return b 463 | 464 | with warnings.catch_warnings(record=True) as capture: 465 | warnings.simplefilter("always") 466 | self.assertEqual(3, f(b=3)) 467 | self.assertEqual(1, len(capture)) 468 | w = capture[0] 469 | self.assertEqual(DeprecationWarning, w.category) 470 | 471 | with warnings.catch_warnings(record=True) as capture: 472 | warnings.simplefilter("always") 473 | self.assertEqual(2, f()) 474 | self.assertEqual(0, len(capture)) 475 | 476 | def test_removed_kwarg_keeps_argspec(self): 477 | @removals.removed_kwarg('b') 478 | def f(b=2): 479 | return b 480 | 481 | def f_unwrapped(b=2): 482 | return b 483 | 484 | self.assertEqual(inspect.getfullargspec(f_unwrapped), 485 | inspect.getfullargspec(f)) 486 | 487 | def test_pending_deprecated_kwarg(self): 488 | 489 | @removals.removed_kwarg('b', category=PendingDeprecationWarning) 490 | def f(b=2): 491 | return b 492 | 493 | with warnings.catch_warnings(record=True) as capture: 494 | warnings.simplefilter("always") 495 | self.assertEqual(3, f(b=3)) 496 | self.assertEqual(1, len(capture)) 497 | w = capture[0] 498 | self.assertEqual(PendingDeprecationWarning, w.category) 499 | 500 | with warnings.catch_warnings(record=True) as capture: 501 | warnings.simplefilter("always") 502 | self.assertEqual(2, f()) 503 | self.assertEqual(0, len(capture)) 504 | 505 | def test_warnings_emitted_property(self): 506 | with warnings.catch_warnings(record=True) as capture: 507 | warnings.simplefilter("always") 508 | o = ThingB() 509 | self.assertEqual('green', o.green_tristars) 510 | o.green_tristars = 'b' 511 | del o.green_tristars 512 | self.assertEqual(3, len(capture)) 513 | w = capture[0] 514 | self.assertEqual(DeprecationWarning, w.category) 515 | 516 | def test_warnings_emitted_property_custom_message(self): 517 | with warnings.catch_warnings(record=True) as capture: 518 | warnings.simplefilter("always") 519 | o = ThingB() 520 | self.assertEqual('green-blue', o.green_blue_tristars) 521 | self.assertEqual(1, len(capture)) 522 | w = capture[0] 523 | self.assertIn('stop using me', str(w.message)) 524 | self.assertEqual(DeprecationWarning, w.category) 525 | 526 | def test_warnings_emitted_function_args(self): 527 | with warnings.catch_warnings(record=True) as capture: 528 | warnings.simplefilter("always") 529 | self.assertEqual(666, crimson_lightning(666)) 530 | self.assertEqual(1, len(capture)) 531 | w = capture[0] 532 | self.assertEqual(DeprecationWarning, w.category) 533 | 534 | def test_pending_warnings_emitted_function_args(self): 535 | with warnings.catch_warnings(record=True) as capture: 536 | warnings.simplefilter("always") 537 | self.assertEqual(666, crimson_lightning_to_remove(666)) 538 | self.assertEqual(1, len(capture)) 539 | w = capture[0] 540 | self.assertEqual(PendingDeprecationWarning, w.category) 541 | 542 | def test_warnings_emitted_function_noargs(self): 543 | with warnings.catch_warnings(record=True) as capture: 544 | warnings.simplefilter("always") 545 | self.assertTrue(red_comet()) 546 | self.assertEqual(1, len(capture)) 547 | w = capture[0] 548 | self.assertEqual(DeprecationWarning, w.category) 549 | 550 | def test_pending_warnings_emitted_function_noargs(self): 551 | with warnings.catch_warnings(record=True) as capture: 552 | warnings.simplefilter("always") 553 | self.assertTrue(blue_comet()) 554 | self.assertEqual(1, len(capture)) 555 | w = capture[0] 556 | self.assertEqual(PendingDeprecationWarning, w.category) 557 | 558 | def test_warnings_emitted_class(self): 559 | with warnings.catch_warnings(record=True) as capture: 560 | warnings.simplefilter("always") 561 | EFSF() 562 | self.assertEqual(1, len(capture)) 563 | w = capture[0] 564 | self.assertEqual(DeprecationWarning, w.category) 565 | 566 | def test_pending_warnings_emitted_class(self): 567 | with warnings.catch_warnings(record=True) as capture: 568 | warnings.simplefilter("always") 569 | EFSF_2() 570 | self.assertEqual(1, len(capture)) 571 | w = capture[0] 572 | self.assertEqual(PendingDeprecationWarning, w.category) 573 | 574 | def test_pending_warnings_emitted_class_direct(self): 575 | with warnings.catch_warnings(record=True) as capture: 576 | warnings.simplefilter("always") 577 | s = StarLord() 578 | self.assertEqual(1, len(capture)) 579 | w = capture[0] 580 | self.assertEqual(DeprecationWarning, w.category) 581 | self.assertEqual("star", s.name) 582 | 583 | def test_pending_warnings_emitted_class_inherit(self): 584 | with warnings.catch_warnings(record=True) as capture: 585 | warnings.simplefilter("always") 586 | s = StarLordJr("star_jr") 587 | self.assertEqual(1, len(capture)) 588 | w = capture[0] 589 | self.assertEqual(DeprecationWarning, w.category) 590 | self.assertEqual("star_jr", s.name) 591 | 592 | def test_warnings_emitted_instancemethod(self): 593 | zeon = ThingB() 594 | with warnings.catch_warnings(record=True) as capture: 595 | warnings.simplefilter("always") 596 | zeon.black_tristars() 597 | self.assertEqual(1, len(capture)) 598 | w = capture[0] 599 | self.assertEqual(DeprecationWarning, w.category) 600 | 601 | def test_pending_warnings_emitted_instancemethod(self): 602 | zeon = ThingB() 603 | with warnings.catch_warnings(record=True) as capture: 604 | warnings.simplefilter("always") 605 | zeon.blue_tristars() 606 | self.assertEqual(1, len(capture)) 607 | w = capture[0] 608 | self.assertEqual(PendingDeprecationWarning, w.category) 609 | 610 | def test_pending_warnings_emitted_classmethod(self): 611 | zeon = ThingB() 612 | with warnings.catch_warnings(record=True) as capture: 613 | warnings.simplefilter("always") 614 | zeon.yellow_wolf() 615 | self.assertEqual(1, len(capture)) 616 | w = capture[0] 617 | self.assertEqual(PendingDeprecationWarning, w.category) 618 | 619 | def test_warnings_emitted_classmethod(self): 620 | zeon = ThingB() 621 | with warnings.catch_warnings(record=True) as capture: 622 | warnings.simplefilter("always") 623 | zeon.white_wolf() 624 | self.assertEqual(1, len(capture)) 625 | w = capture[0] 626 | self.assertEqual(DeprecationWarning, w.category) 627 | 628 | def test_warnings_emitted_staticmethod(self): 629 | zeon = ThingB() 630 | with warnings.catch_warnings(record=True) as capture: 631 | warnings.simplefilter("always") 632 | zeon.blue_giant() 633 | self.assertEqual(1, len(capture)) 634 | w = capture[0] 635 | self.assertEqual(DeprecationWarning, w.category) 636 | 637 | def test_pending_warnings_emitted_staticmethod(self): 638 | zeon = ThingB() 639 | with warnings.catch_warnings(record=True) as capture: 640 | warnings.simplefilter("always") 641 | zeon.green_giant() 642 | self.assertEqual(1, len(capture)) 643 | w = capture[0] 644 | self.assertEqual(PendingDeprecationWarning, w.category) 645 | 646 | def test_removed_module(self): 647 | with warnings.catch_warnings(record=True) as capture: 648 | warnings.simplefilter("always") 649 | removals.removed_module(__name__) 650 | self.assertEqual(1, len(capture)) 651 | w = capture[0] 652 | self.assertEqual(DeprecationWarning, w.category) 653 | 654 | def test_pending_removed_module(self): 655 | with warnings.catch_warnings(record=True) as capture: 656 | warnings.simplefilter("always") 657 | removals.removed_module(__name__, 658 | category=PendingDeprecationWarning) 659 | self.assertEqual(1, len(capture)) 660 | w = capture[0] 661 | self.assertEqual(PendingDeprecationWarning, w.category) 662 | 663 | def test_removed_module_bad_type(self): 664 | self.assertRaises(TypeError, removals.removed_module, 2) 665 | -------------------------------------------------------------------------------- /debtcollector/updating.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import wrapt 16 | 17 | from inspect import signature 18 | 19 | from debtcollector import _utils 20 | 21 | _KWARG_UPDATED_POSTFIX_TPL = (', please update the code to explicitly set %s ' 22 | 'as the value') 23 | _KWARG_UPDATED_PREFIX_TPL = ('The %s argument is changing its default value ' 24 | 'to %s') 25 | 26 | 27 | def updated_kwarg_default_value(name, old_value, new_value, message=None, 28 | version=None, stacklevel=3, 29 | category=FutureWarning): 30 | 31 | """Decorates a kwarg accepting function to change the default value""" 32 | 33 | prefix = _KWARG_UPDATED_PREFIX_TPL % (name, new_value) 34 | postfix = _KWARG_UPDATED_POSTFIX_TPL % old_value 35 | out_message = _utils.generate_message( 36 | prefix, postfix=postfix, message=message, version=version) 37 | 38 | def decorator(f): 39 | sig = signature(f) 40 | varnames = list(sig.parameters.keys()) 41 | 42 | @wrapt.decorator 43 | def wrapper(wrapped, instance, args, kwargs): 44 | explicit_params = set( 45 | varnames[:len(args)] + list(kwargs.keys()) 46 | ) 47 | allparams = set(varnames) 48 | default_params = set(allparams - explicit_params) 49 | if name in default_params: 50 | _utils.deprecation(out_message, 51 | stacklevel=stacklevel, category=category) 52 | return wrapped(*args, **kwargs) 53 | 54 | return wrapper(f) 55 | 56 | return decorator 57 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=2.0.0 # BSD 2 | openstackdocstheme>=2.2.1 # Apache-2.0 3 | reno>=3.1.0 # Apache-2.0 4 | fixtures>=3.0.0 # Apache-2.0/BSD 5 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import sys 18 | 19 | sys.path.insert(0, os.path.abspath('../..')) 20 | # -- General configuration ---------------------------------------------------- 21 | 22 | # Add any Sphinx extension module names here, as strings. They can be 23 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 24 | extensions = [ 25 | 'sphinx.ext.autodoc', 26 | 'sphinx.ext.viewcode', 27 | 'sphinx.ext.doctest', 28 | 'openstackdocstheme', 29 | ] 30 | 31 | # openstackdocstheme options 32 | openstackdocs_repo_name = 'openstack/debtcollector' 33 | openstackdocs_auto_name = False 34 | openstackdocs_bug_project = 'debtcollector' 35 | openstackdocs_bug_tag = 'doc' 36 | 37 | # autodoc generation is a bit aggressive and a nuisance when doing heavy 38 | # text edit cycles. 39 | # execute "export SPHINX_DEBUG=1" in your terminal to disable 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = 'debtcollector' 49 | copyright = 'OpenStack Foundation' 50 | 51 | # If true, '()' will be appended to :func: etc. cross-reference text. 52 | add_function_parentheses = True 53 | 54 | # If true, the current module name will be prepended to all description 55 | # unit titles (such as .. function::). 56 | add_module_names = True 57 | 58 | # The name of the Pygments (syntax highlighting) style to use. 59 | pygments_style = 'native' 60 | 61 | # -- Options for HTML output -------------------------------------------------- 62 | 63 | # The theme to use for HTML and HTML Help pages. Major themes that come with 64 | # Sphinx are currently 'default' and 'sphinxdoc'. 65 | # html_theme_path = ["."] 66 | # html_theme = '_theme' 67 | html_theme = 'openstackdocs' 68 | # html_static_path = ['static'] 69 | 70 | 71 | # Output file base name for HTML help builder. 72 | htmlhelp_basename = '%sdoc' % project 73 | 74 | # Grouping the document tree into LaTeX files. List of tuples 75 | # (source start file, target name, title, author, documentclass 76 | # [howto/manual]). 77 | latex_documents = [ 78 | ('index', 79 | '%s.tex' % project, 80 | '%s Documentation' % project, 81 | 'OpenStack Foundation', 'manual'), 82 | ] 83 | 84 | # -- Options for autoddoc ---------------------------------------------------- 85 | 86 | # Keep source order 87 | autodoc_member_order = 'bysource' 88 | 89 | # Always include members 90 | autodoc_default_flags = ['members', 'show-inheritance'] 91 | -------------------------------------------------------------------------------- /doc/source/contributor/index.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | .. include:: ../../../CONTRIBUTING.rst 6 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | ========================================= 2 | Welcome to debtcollector's documentation! 3 | ========================================= 4 | 5 | A collection of Python deprecation patterns and strategies that help you 6 | collect your technical debt in a non-destructive manner. 7 | 8 | .. note:: 9 | 10 | It should be noted that even though it is designed with OpenStack 11 | integration in mind, and that is where most of its *current* 12 | integration is it aims to be generally usable and useful in any 13 | project. 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | install/index 19 | user/index 20 | reference/index 21 | contributor/index 22 | 23 | .. rubric:: Indices and tables 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /doc/source/install/index.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ pip install debtcollector 8 | 9 | Or, if you have virtualenvwrapper installed:: 10 | 11 | $ mkvirtualenv debtcollector 12 | $ pip install debtcollector 13 | -------------------------------------------------------------------------------- /doc/source/reference/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | API Reference 3 | ============= 4 | 5 | 6 | The currently documented publicly exposed API's for usage in your project 7 | are defined below. 8 | 9 | .. warning:: 10 | 11 | External usage of internal utility functions and modules should be kept 12 | to a **minimum** as they may be altered, refactored or moved to other 13 | locations **without** notice (and without the typical deprecation cycle). 14 | 15 | Helpers 16 | ------- 17 | 18 | .. automodule:: debtcollector 19 | 20 | Moves 21 | ----- 22 | 23 | .. automodule:: debtcollector.moves 24 | 25 | Renames 26 | ------- 27 | 28 | .. automodule:: debtcollector.renames 29 | 30 | Removals 31 | -------- 32 | 33 | .. automodule:: debtcollector.removals 34 | 35 | Fixtures 36 | -------- 37 | 38 | .. automodule:: debtcollector.fixtures.disable 39 | -------------------------------------------------------------------------------- /doc/source/user/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../ChangeLog 2 | -------------------------------------------------------------------------------- /doc/source/user/index.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Using debtcollector 3 | =================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | usage 9 | 10 | .. history contains a lot of sections, toctree with maxdepth 1 is used. 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | history 15 | -------------------------------------------------------------------------------- /doc/source/user/usage.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | 5 | Removing a class/classmethod/method/function 6 | -------------------------------------------- 7 | 8 | To signal to a user that a method (staticmethod, classmethod, or regular 9 | instance method) or a class or function is going to be removed at some point 10 | in the future the :py:func:`~debtcollector.removals.remove` function/decorator 11 | can be used to achieve this in a non-destructive manner. 12 | 13 | A basic example to do just this (on a method/function): 14 | 15 | .. doctest:: 16 | 17 | >>> from debtcollector import removals 18 | >>> import warnings 19 | >>> warnings.simplefilter('always') 20 | >>> class Car(object): 21 | ... @removals.remove 22 | ... def start(self): 23 | ... pass 24 | ... 25 | >>> c = Car() 26 | >>> c.start() 27 | 28 | **Expected output:** 29 | 30 | .. testoutput:: 31 | 32 | __main__:1: DeprecationWarning: Using function/method 'Car.start()' is deprecated 33 | 34 | A basic example to do just this (on a class): 35 | 36 | .. doctest:: 37 | 38 | >>> from debtcollector import removals 39 | >>> import warnings 40 | >>> warnings.simplefilter('always') 41 | >>> @removals.removed_class("Pinto") 42 | ... class Pinto(object): 43 | ... pass 44 | ... 45 | >>> p = Pinto() 46 | 47 | **Expected output:** 48 | 49 | .. testoutput:: 50 | 51 | __main__:1: DeprecationWarning: Using class 'Pinto' (either directly or via inheritance) is deprecated 52 | 53 | A basic example to do just this (on a classmethod): 54 | 55 | .. doctest:: 56 | 57 | >>> from debtcollector import removals 58 | >>> import warnings 59 | >>> warnings.simplefilter("once") 60 | >>> class OldAndBusted(object): 61 | ... @removals.remove 62 | ... @classmethod 63 | ... def fix_things(cls): 64 | ... pass 65 | ... 66 | >>> OldAndBusted.fix_things() 67 | 68 | **Expected output:** 69 | 70 | .. testoutput:: 71 | 72 | __main__:1: DeprecationWarning: Using function/method 'OldAndBusted.fix_things()' is deprecated 73 | 74 | Removing a instance property 75 | ---------------------------- 76 | 77 | Use the :py:func:`~debtcollector.removals.removed_property` decorator 78 | to signal that an attribute of a class is deprecated. 79 | 80 | A basic example to do just this: 81 | 82 | .. doctest:: 83 | 84 | >>> import warnings 85 | >>> warnings.simplefilter("once") 86 | >>> from debtcollector import removals 87 | >>> class OldAndBusted(object): 88 | ... @removals.removed_property 89 | ... def thing(self): 90 | ... return 'old-and-busted' 91 | ... @thing.setter 92 | ... def thing(self, value): 93 | ... pass 94 | ... @thing.deleter 95 | ... def thing(self): 96 | ... pass 97 | ... 98 | >>> o = OldAndBusted() 99 | >>> o.thing 100 | 'old-and-busted' 101 | >>> o.thing = '2' 102 | >>> del o.thing 103 | 104 | .. testoutput:: 105 | 106 | __main__:1: DeprecationWarning: Reading the 'thing' property is deprecated 107 | __main__:1: DeprecationWarning: Setting the 'thing' property is deprecated 108 | __main__:1: DeprecationWarning: Deleting the 'thing' property is deprecated 109 | 110 | Removing a keyword argument 111 | --------------------------- 112 | 113 | A basic example to do just this (on a classmethod): 114 | 115 | .. doctest:: 116 | 117 | >>> import warnings 118 | >>> warnings.simplefilter("once") 119 | >>> from debtcollector import removals 120 | >>> class OldAndBusted(object): 121 | ... @removals.removed_kwarg('resp', message="Please use 'response' instead") 122 | ... @classmethod 123 | ... def factory(cls, resp=None, response=None): 124 | ... response = resp or response 125 | ... return response 126 | ... 127 | >>> OldAndBusted.factory(resp='super-duper') 128 | 'super-duper' 129 | 130 | .. testoutput:: 131 | 132 | __main__:1: DeprecationWarning: Using the 'resp' argument is deprecated: Please use 'response' instead 133 | 134 | A basic example to do just this (on a ``__init__`` method): 135 | 136 | .. doctest:: 137 | 138 | >>> import warnings 139 | >>> warnings.simplefilter("once") 140 | >>> from debtcollector import removals 141 | >>> class OldAndBusted(object): 142 | ... @removals.removed_kwarg('bleep') 143 | ... def __init__(self, bleep=None): 144 | ... self.bloop = bleep 145 | ... 146 | >>> o = OldAndBusted(bleep=2) 147 | 148 | .. testoutput:: 149 | 150 | __main__:1: DeprecationWarning: Using the 'bleep' argument is deprecated 151 | 152 | Changing the default value of a keyword argument 153 | ------------------------------------------------ 154 | 155 | A basic example to do just this: 156 | 157 | .. doctest:: 158 | 159 | >>> import warnings 160 | >>> warnings.simplefilter("once") 161 | >>> from debtcollector import updating 162 | >>> class OldAndBusted(object): 163 | ... ip = '127.0.0.1' 164 | ... @updating.updated_kwarg_default_value('type', 'http', 'https') 165 | ... def url(self, type='http'): 166 | ... response = '%s://%s' % (type, self.ip) 167 | ... return response 168 | ... 169 | >>> OldAndBusted().url() 170 | 'http://127.0.0.1' 171 | 172 | .. testoutput:: 173 | 174 | __main__:1: FutureWarning: The http argument is changing its default value to https, please update the code to explicitly set http as the value 175 | 176 | 177 | A basic classmethod example. 178 | 179 | .. note:: the @classmethod decorator is before the debtcollector one 180 | 181 | .. doctest:: 182 | 183 | >>> import warnings 184 | >>> warnings.simplefilter("once") 185 | >>> from debtcollector import updating 186 | >>> class OldAndBusted(object): 187 | ... ip = '127.0.0.1' 188 | ... @classmethod 189 | ... @updating.updated_kwarg_default_value('type', 'http', 'https') 190 | ... def url(cls, type='http'): 191 | ... response = '%s://%s' % (type, cls.ip) 192 | ... return response 193 | ... 194 | >>> OldAndBusted.url() 195 | 'http://127.0.0.1' 196 | 197 | .. testoutput:: 198 | 199 | __main__:1: FutureWarning: The http argument is changing its default value to https, please update the code to explicitly set http as the value 200 | 201 | Moving a function 202 | ----------------- 203 | 204 | To change the name or location of a regular function use the 205 | :py:func:`~debtcollector.moves.moved_function` function: 206 | 207 | .. doctest:: 208 | 209 | >>> from debtcollector import moves 210 | >>> import warnings 211 | >>> warnings.simplefilter('always') 212 | >>> def new_thing(): 213 | ... return "new thing" 214 | ... 215 | >>> old_thing = moves.moved_function(new_thing, 'old_thing', __name__) 216 | >>> new_thing() 217 | 'new thing' 218 | >>> old_thing() 219 | 'new thing' 220 | 221 | **Expected output:** 222 | 223 | .. testoutput:: 224 | 225 | __main__:1: DeprecationWarning: Function '__main__.old_thing()' has moved to '__main__.new_thing()' 226 | 227 | Moving a method 228 | --------------- 229 | 230 | To move a *instance* method from an existing one to a new one 231 | the :py:func:`~debtcollector.moves.moved_method` function/decorator can be 232 | used to achieve this in a non-destructive manner. 233 | 234 | A basic example to do just this: 235 | 236 | .. doctest:: 237 | 238 | >>> from debtcollector import moves 239 | >>> import warnings 240 | >>> warnings.simplefilter('always') 241 | >>> class Cat(object): 242 | ... @moves.moved_method('meow') 243 | ... def mewow(self): 244 | ... return self.meow() 245 | ... def meow(self): 246 | ... return 'kitty' 247 | ... 248 | >>> c = Cat() 249 | >>> c.mewow() 250 | 'kitty' 251 | >>> c.meow() 252 | 'kitty' 253 | 254 | **Expected output:** 255 | 256 | .. testoutput:: 257 | 258 | __main__:1: DeprecationWarning: Method 'Cat.mewow()' has moved to 'Cat.meow()' 259 | 260 | Moving a property 261 | ----------------- 262 | 263 | To move a *instance* property from an existing one to a new one 264 | the :py:func:`~debtcollector.moves.moved_property` function/decorator can be 265 | used to achieve this in a non-destructive manner. 266 | 267 | A basic example to do just this: 268 | 269 | .. doctest:: 270 | 271 | >>> from debtcollector import moves 272 | >>> import warnings 273 | >>> warnings.simplefilter('always') 274 | >>> class Dog(object): 275 | ... @property 276 | ... @moves.moved_property('bark') 277 | ... def burk(self): 278 | ... return self.bark 279 | ... @property 280 | ... def bark(self): 281 | ... return 'woof' 282 | ... 283 | >>> d = Dog() 284 | >>> d.burk 285 | 'woof' 286 | >>> d.bark 287 | 'woof' 288 | 289 | **Expected output:** 290 | 291 | .. testoutput:: 292 | 293 | __main__:1: DeprecationWarning: Property 'Dog.burk' has moved to 'Dog.bark' 294 | 295 | Moving a class 296 | -------------- 297 | 298 | To move a *class* from an existing one to a new one 299 | the :py:func:`~debtcollector.moves.moved_class` type generator function can 300 | be used to achieve this in a non-destructive manner. 301 | 302 | A basic example to do just this: 303 | 304 | .. doctest:: 305 | 306 | >>> from debtcollector import moves 307 | >>> import warnings 308 | >>> warnings.simplefilter('always') 309 | >>> class WizBang(object): 310 | ... pass 311 | ... 312 | >>> OldWizBang = moves.moved_class(WizBang, 'OldWizBang', __name__) 313 | >>> a = OldWizBang() 314 | >>> b = WizBang() 315 | 316 | **Expected output:** 317 | 318 | .. testoutput:: 319 | 320 | __main__:1: DeprecationWarning: Class '__main__.OldWizBang' has moved to '__main__.WizBang' 321 | 322 | Renaming a keyword argument 323 | --------------------------- 324 | 325 | To notify the user when a keyword argument has been replaced with a new and 326 | improved keyword argument and the user is still using the old keyword argument 327 | the :py:func:`~debtcollector.renames.renamed_kwarg` function/decorator 328 | can be used to achieve this in a non-destructive manner. 329 | 330 | A basic example to do just this: 331 | 332 | .. doctest:: 333 | 334 | >>> from debtcollector import renames 335 | >>> import warnings 336 | >>> warnings.simplefilter('always') 337 | >>> @renames.renamed_kwarg('snizzle', 'nizzle') 338 | ... def do_the_deed(snizzle=True, nizzle=True): 339 | ... return (snizzle, nizzle) 340 | ... 341 | >>> do_the_deed() 342 | (True, True) 343 | >>> do_the_deed(snizzle=False) 344 | (False, True) 345 | >>> do_the_deed(nizzle=False) 346 | (True, False) 347 | 348 | **Expected output:** 349 | 350 | .. testoutput:: 351 | 352 | __main__:1: DeprecationWarning: Using the 'snizzle' argument is deprecated, please use the 'nizzle' argument instead 353 | 354 | Further customizing the emitted messages 355 | ---------------------------------------- 356 | 357 | It is typically useful to tell the user when a deprecation has started and 358 | when the deprecated item will be officially removed (deleted or other). To 359 | enable this all the currently provided functions this library provides 360 | take a ``message``, ``version`` and ``removal_version`` keyword arguments. 361 | These are used in forming the message that is shown to the user when they 362 | trigger the deprecated activity. 363 | 364 | A basic example to do just this: 365 | 366 | .. doctest:: 367 | 368 | >>> from debtcollector import renames 369 | >>> import warnings 370 | >>> warnings.simplefilter('always') 371 | >>> @renames.renamed_kwarg('snizzle', 'nizzle', version="0.5", removal_version="0.7") 372 | ... def do_the_deed(snizzle=True, nizzle=True): 373 | ... pass 374 | ... 375 | >>> do_the_deed(snizzle=False) 376 | 377 | **Expected output:** 378 | 379 | .. testoutput:: 380 | 381 | __main__:1: DeprecationWarning: Using the 'snizzle' argument is deprecated in version '0.5' and will be removed in version '0.7', please use the 'nizzle' argument instead 382 | 383 | If the ``removal_version`` is unknown the special character ``?`` can be used 384 | instead (to denote that the deprecated activity will be removed sometime in 385 | the future). 386 | 387 | A basic example to do just this: 388 | 389 | .. doctest:: 390 | 391 | >>> from debtcollector import renames 392 | >>> import warnings 393 | >>> warnings.simplefilter('always') 394 | >>> @renames.renamed_kwarg('snizzle', 'nizzle', version="0.5", removal_version="?") 395 | ... def do_the_deed(snizzle=True, nizzle=True): 396 | ... pass 397 | ... 398 | >>> do_the_deed(snizzle=False) 399 | 400 | **Expected output:** 401 | 402 | .. testoutput:: 403 | 404 | __main__:1: DeprecationWarning: Using the 'snizzle' argument is deprecated in version '0.5' and will be removed in a future version, please use the 'nizzle' argument instead 405 | 406 | To further customize the message (with a special postfix) the ``message`` 407 | keyword argument can be provided. 408 | 409 | A basic example to do just this: 410 | 411 | .. doctest:: 412 | 413 | >>> from debtcollector import renames 414 | >>> import warnings 415 | >>> warnings.simplefilter('always') 416 | >>> @renames.renamed_kwarg('snizzle', 'nizzle', message="Pretty please stop using it") 417 | ... def do_the_deed(snizzle=True, nizzle=True): 418 | ... pass 419 | ... 420 | >>> do_the_deed(snizzle=False) 421 | 422 | **Expected output:** 423 | 424 | .. testoutput:: 425 | 426 | __main__:1: DeprecationWarning: Using the 'snizzle' argument is deprecated, please use the 'nizzle' argument instead: Pretty please stop using it 427 | 428 | Deprecating anything else 429 | ------------------------- 430 | 431 | For use-cases which do not fit the above decorators, properties other 432 | provided functionality the final option is to use debtcollectors 433 | the :py:func:`~debtcollector.deprecate` function to make your own 434 | messages (using the message building logic that debtcollector uses itself). 435 | 436 | A basic example to do just this: 437 | 438 | .. doctest:: 439 | 440 | >>> import warnings 441 | >>> warnings.simplefilter("always") 442 | >>> import debtcollector 443 | >>> debtcollector.deprecate("This is no longer supported", version="1.0") 444 | 445 | .. testoutput:: 446 | 447 | __main__:1: DeprecationWarning: This is no longer supported in version '1.0' 448 | -------------------------------------------------------------------------------- /releasenotes/notes/add-reno-996dd44974d53238.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | other: 3 | - Introduce reno for deployer release notes. 4 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-python-2-7-73d3113c69d724d6.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 2.7 support has been dropped. The minimum version of Python now 5 | supported by debtcollector is Python 3.6. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/remove-py38-301fc55249d7333e.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Support for Python 3.8 has been removed. Now the minimum python version 5 | supported is 3.9 . 6 | -------------------------------------------------------------------------------- /releasenotes/source/_static/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/debtcollector/395e8c52b66c2b051127b62e1964190542bc4cb3/releasenotes/source/_static/.placeholder -------------------------------------------------------------------------------- /releasenotes/source/_templates/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/debtcollector/395e8c52b66c2b051127b62e1964190542bc4cb3/releasenotes/source/_templates/.placeholder -------------------------------------------------------------------------------- /releasenotes/source/conf.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # This file is execfile()d with the current directory set to its 15 | # containing dir. 16 | # 17 | # Note that not all possible configuration values are present in this 18 | # autogenerated file. 19 | # 20 | # All configuration values have a default; values that are commented out 21 | # serve to show the default. 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | # sys.path.insert(0, os.path.abspath('.')) 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'openstackdocstheme', 38 | 'reno.sphinxext', 39 | ] 40 | 41 | # openstackdocstheme options 42 | openstackdocs_repo_name = 'openstack/debtcollector' 43 | openstackdocs_auto_name = False 44 | openstackdocs_bug_project = 'debtcollector' 45 | openstackdocs_bug_tag = 'doc' 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix of source filenames. 51 | source_suffix = '.rst' 52 | 53 | # The encoding of source files. 54 | # source_encoding = 'utf-8-sig' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'debtcollector Release Notes' 61 | copyright = '2016, debtcollector Developers' 62 | 63 | # Release notes do not need a version number in the title, they 64 | # cover multiple releases. 65 | # The full version, including alpha/beta/rc tags. 66 | release = '' 67 | # The short X.Y version. 68 | version = '' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | # today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | # today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = [] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | # default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | # add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | # add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | # show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'native' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | # modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | # keep_warnings = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'openstackdocs' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | # html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | # html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | # html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | # html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | # html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | # html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | # html_extra_path = [] 148 | 149 | # If true, SmartyPants will be used to convert quotes and dashes to 150 | # typographically correct entities. 151 | # html_use_smartypants = True 152 | 153 | # Custom sidebar templates, maps document names to template names. 154 | # html_sidebars = {} 155 | 156 | # Additional templates that should be rendered to pages, maps page names to 157 | # template names. 158 | # html_additional_pages = {} 159 | 160 | # If false, no module index is generated. 161 | # html_domain_indices = True 162 | 163 | # If false, no index is generated. 164 | # html_use_index = True 165 | 166 | # If true, the index is split into individual pages for each letter. 167 | # html_split_index = False 168 | 169 | # If true, links to the reST sources are added to the pages. 170 | # html_show_sourcelink = True 171 | 172 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 173 | # html_show_sphinx = True 174 | 175 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 176 | # html_show_copyright = True 177 | 178 | # If true, an OpenSearch description file will be output, and all pages will 179 | # contain a tag referring to it. The value of this option must be the 180 | # base URL from which the finished HTML is served. 181 | # html_use_opensearch = '' 182 | 183 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 184 | # html_file_suffix = None 185 | 186 | # Output file base name for HTML help builder. 187 | htmlhelp_basename = 'debtcollectorReleaseNotesDoc' 188 | 189 | 190 | # -- Options for LaTeX output --------------------------------------------- 191 | 192 | latex_elements = { 193 | # The paper size ('letterpaper' or 'a4paper'). 194 | # 'papersize': 'letterpaper', 195 | 196 | # The font size ('10pt', '11pt' or '12pt'). 197 | # 'pointsize': '10pt', 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | # 'preamble': '', 201 | } 202 | 203 | # Grouping the document tree into LaTeX files. List of tuples 204 | # (source start file, target name, title, 205 | # author, documentclass [howto, manual, or own class]). 206 | latex_documents = [ 207 | ('index', 'debtcollectorReleaseNotes.tex', 208 | 'debtcollector Release Notes Documentation', 209 | 'debtcollector Developers', 'manual'), 210 | ] 211 | 212 | # The name of an image file (relative to this directory) to place at the top of 213 | # the title page. 214 | # latex_logo = None 215 | 216 | # For "manual" documents, if this is true, then toplevel headings are parts, 217 | # not chapters. 218 | # latex_use_parts = False 219 | 220 | # If true, show page references after internal links. 221 | # latex_show_pagerefs = False 222 | 223 | # If true, show URL addresses after external links. 224 | # latex_show_urls = False 225 | 226 | # Documents to append as an appendix to all manuals. 227 | # latex_appendices = [] 228 | 229 | # If false, no module index is generated. 230 | # latex_domain_indices = True 231 | 232 | 233 | # -- Options for manual page output --------------------------------------- 234 | 235 | # One entry per manual page. List of tuples 236 | # (source start file, name, description, authors, manual section). 237 | man_pages = [ 238 | ('index', 'debtcollectorReleaseNotes', 239 | 'debtcollector Release Notes Documentation', 240 | ['debtcollector Developers'], 1) 241 | ] 242 | 243 | # If true, show URL addresses after external links. 244 | # man_show_urls = False 245 | 246 | 247 | # -- Options for Texinfo output ------------------------------------------- 248 | 249 | # Grouping the document tree into Texinfo files. List of tuples 250 | # (source start file, target name, title, author, 251 | # dir menu entry, description, category) 252 | texinfo_documents = [ 253 | ('index', 'debtcollectorReleaseNotes', 254 | 'debtcollector Release Notes Documentation', 255 | 'debtcollector Developers', 'debtcollectorReleaseNotes', 256 | 'One line description of project.', 257 | 'Miscellaneous'), 258 | ] 259 | 260 | # Documents to append as an appendix to all manuals. 261 | # texinfo_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | # texinfo_domain_indices = True 265 | 266 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 267 | # texinfo_show_urls = 'footnote' 268 | 269 | # If true, do not generate a @detailmenu in the "Top" node's menu. 270 | # texinfo_no_detailmenu = False 271 | 272 | # -- Options for Internationalization output ------------------------------ 273 | locale_dirs = ['locale/'] 274 | -------------------------------------------------------------------------------- /releasenotes/source/index.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | debtcollector Release Notes 3 | ============================= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | unreleased 9 | victoria 10 | ussuri 11 | train 12 | stein 13 | rocky 14 | queens 15 | pike 16 | ocata 17 | -------------------------------------------------------------------------------- /releasenotes/source/ocata.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Ocata Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: origin/stable/ocata 7 | -------------------------------------------------------------------------------- /releasenotes/source/pike.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Pike Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/pike 7 | -------------------------------------------------------------------------------- /releasenotes/source/queens.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Queens Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/queens 7 | -------------------------------------------------------------------------------- /releasenotes/source/rocky.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Rocky Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/rocky 7 | -------------------------------------------------------------------------------- /releasenotes/source/stein.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Stein Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/stein 7 | -------------------------------------------------------------------------------- /releasenotes/source/train.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Train Series Release Notes 3 | ========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/train 7 | -------------------------------------------------------------------------------- /releasenotes/source/unreleased.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Unreleased Release Notes 3 | ========================== 4 | 5 | .. release-notes:: 6 | -------------------------------------------------------------------------------- /releasenotes/source/ussuri.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Ussuri Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/ussuri 7 | -------------------------------------------------------------------------------- /releasenotes/source/victoria.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Victoria Series Release Notes 3 | ============================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/victoria 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements lower bounds listed here are our best effort to keep them up to 2 | # date but we do not test them so no guarantee of having them all correct. If 3 | # you find any incorrect lower bounds, let us know or propose a fix. 4 | 5 | wrapt>=1.7.0 # BSD License 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = debtcollector 3 | summary = A collection of Python deprecation patterns and strategies that help you collect your technical debt in a non-destructive manner. 4 | description_file = 5 | README.rst 6 | author = OpenStack 7 | author_email = openstack-discuss@lists.openstack.org 8 | home_page = https://docs.openstack.org/debtcollector/latest 9 | python_requires = >=3.9 10 | classifier = 11 | Environment :: OpenStack 12 | Intended Audience :: Information Technology 13 | Intended Audience :: System Administrators 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: POSIX :: Linux 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.9 19 | Programming Language :: Python :: 3.10 20 | Programming Language :: Python :: 3.11 21 | Programming Language :: Python :: 3.12 22 | Programming Language :: Python :: 3 :: Only 23 | Programming Language :: Python :: Implementation :: CPython 24 | 25 | [files] 26 | packages = 27 | debtcollector 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT 17 | import setuptools 18 | 19 | setuptools.setup( 20 | setup_requires=['pbr>=2.0.0'], 21 | pbr=True) 22 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage>=4.0 # Apache-2.0 2 | python-subunit>=1.0.0 # Apache-2.0/BSD 3 | stestr>=2.0.0 # Apache-2.0 4 | testtools>=2.2.0 # MIT 5 | fixtures>=3.0.0 # Apache-2.0/BSD 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.2.0 3 | envlist = py3,pep8 4 | 5 | [testenv] 6 | deps = 7 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 8 | -r{toxinidir}/test-requirements.txt 9 | -r{toxinidir}/requirements.txt 10 | commands = 11 | stestr run --slowest {posargs} 12 | 13 | [testenv:debug] 14 | commands = oslo_debug_helper {posargs} 15 | 16 | [testenv:pep8] 17 | deps = 18 | pre-commit>=2.6.0 # MIT 19 | {[testenv:docs]deps} 20 | commands = 21 | pre-commit run -a 22 | sphinx-build -b doctest doc/source doc/build 23 | 24 | [testenv:venv] 25 | commands = {posargs} 26 | 27 | [testenv:cover] 28 | setenv = 29 | PYTHON=coverage run --source $project --parallel-mode 30 | commands = 31 | stestr run {posargs} 32 | coverage combine 33 | coverage html -d cover 34 | coverage xml -o cover/coverage.xml 35 | 36 | [testenv:docs] 37 | deps = 38 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 39 | -r{toxinidir}/doc/requirements.txt 40 | commands = sphinx-build -W --keep-going -b html -d doc/build/doctrees doc/source doc/build/html 41 | 42 | [flake8] 43 | # E123, E125 skipped as they are invalid PEP-8. 44 | show-source = True 45 | ignore = E123,E125 46 | exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build 47 | 48 | [testenv:releasenotes] 49 | deps = {[testenv:docs]deps} 50 | commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html 51 | 52 | --------------------------------------------------------------------------------