diff --git a/docs/conceptual-guides/deprecation.md b/docs/conceptual-guides/deprecation.md new file mode 100644 index 000000000..7c181b255 --- /dev/null +++ b/docs/conceptual-guides/deprecation.md @@ -0,0 +1,86 @@ +--- +myst: + html_meta: + "description": "Understanding deprecation in Plone - rationale, philosophy, and use cases" + "property=og:description": "Understanding deprecation in Plone - rationale, philosophy, and use cases" + "property=og:title": "Deprecation" + "keywords": "deprecation, Plone, Python, Node.js, React, philosophy, rationale, use cases" +--- + +(conceptual-deprecation-label)= + +# Deprecation + +This chapter describes the rationale and philosophy of deprecations in Plone. +It is meant as a guide for how to think about deprecations in Plone core packages. + +```{seealso} +For implementation details and code examples, see {doc}`/developer-guide/deprecation`. +``` + + +(why-deprecation-label)= + +## Why deprecation + +Developers may need to get rid of old code, unify to a consistent API style, fix typos in names, move code or templates around, resolve technical debt, address security issues, or adapt to changes in external dependencies. + +When refactoring code, it's often necessary to move modules, functions, classes, and methods. +It's critical not to break third party code imports from the old place. +It's also important that usage of old functions or methods must work for a while to allow developers to migrate or update their code. + +Deprecated methods are usually removed with the next major release of Plone. +Plone follows the [semantic versioning guideline](https://semver.org). + + +## Help programmers without annoyance + +Developers should use code deprecations to support the consumers of the code, that is, their fellow Plone developers. +From the consumer's point of view, Plone core code is an API. +Any change may annoy them, but they feel better when deprecation warnings tell them how to adapt their code to the changes. + +Deprecations must always log at the level of warning. + +Deprecations should always answer the following questions. + +- Why is the code gone from the old place? +- What should the developer do instead? + +A short message is enough, such as the following examples. + +- "Replaced by new API `xyz`, found at `abc.cde`". +- "Moved to `xyz`, because of `abc`". +- "Name had a typo, new name is `xyz`". + +All logging must be done only once, in other words, on the first usage or import. +It must not flood the logs. + + +## Use cases + +The following use cases describe when to deprecate. + +Rename +: Developers may want to rename classes, methods, functions, or global or class variables to get a more consistent API or because of a typo. + Renaming alone is not enough to deprecate code. + Always provide a deprecated version that logs a verbose deprecation warning with information for where to import from in the future. + +Move objects +: For reasons described in {ref}`why-deprecation-label`, developers may need to move code around. + When imported from the old place, it logs a verbose deprecation warning with information of where to import from in the future. + +Deprecation of a whole Python or npm package +: A whole {ref}`Python package ` or [npm package](https://www.npmjs.com/) may be moved to a new location. + + - All imports still work. + - Log deprecation warnings on first import. + - The ZCML still exists, but is empty or includes the ZCML from the new place, if there's no auto import for `meta.zcml`. + +Deprecation of a whole released or installable package +: Plone developers provide a major release with no "real" code, but only backward compatible imports of the public API. + This should be done the way described above for a whole package. + The README should clearly state why it was moved and where to find the code now. + +Deprecation of a GenericSetup profile +: These may have been renamed for consistency or are superfluous after an update. + Code does not need to break to support this. diff --git a/docs/conceptual-guides/index.md b/docs/conceptual-guides/index.md index fb98fce31..dcd44b630 100644 --- a/docs/conceptual-guides/index.md +++ b/docs/conceptual-guides/index.md @@ -16,8 +16,9 @@ This part of the documentation provides explanation of concepts to deepen and br :maxdepth: 2 choose-user-interface -compare-buildout-pip distributions +compare-buildout-pip +deprecation package-management package-dependencies make-backend-build diff --git a/docs/developer-guide/deprecation.md b/docs/developer-guide/deprecation.md new file mode 100644 index 000000000..0296ded16 --- /dev/null +++ b/docs/developer-guide/deprecation.md @@ -0,0 +1,263 @@ +--- +myst: + html_meta: + "description": "How to implement deprecations in Plone, including Python, ZCML and templates." + "property=og:description": "How to implement deprecations in Plone, including Python, ZCML and templates." + "property=og:title": "Implement deprecations" + "keywords": "deprecation, zcml, template, jbot, Plone, Python" +--- + +(developer-deprecation-label)= + +# Deprecate code + +This chapter describes how to enable deprecation warnings and best practices for deprecating code in Plone, Zope, and Python. + +```{seealso} +For background on deprecation philosophy and use cases, see {doc}`/conceptual-guides/deprecation`. +``` + + +## Enable deprecation warnings + +This section describes how to enable deprecation warnings in Zope, Python, and tests. + + +### Zope + +Zope configures logging and warnings, so the steps as described below in {ref}`deprecation-warning-python-label` aren't needed. + +Using `plone.recipe.zope2instance`, add the option `deprecation-warnings = on` to the buildout's `[instance]` section. + +```cfg +[buildout] +parts = instance + +[instance] +recipe = plone.recipe.zope2instance +# … +deprecation-warnings = on +# … +``` + +This adds the following line to the {file}`zope.conf` file. + +```cfg +debug-mode on +``` + +Without the recipe, this can be set manually as well. +In {file}`zope.conf`, define custom filters for warnings, such as the following example. + +```xml + + action always + category exceptions.DeprecationWarning + +``` + + +(deprecation-warning-python-label)= + +### Python + +Enable warnings + +: Warnings are written to `stderr` by default, but `DeprecationWarning` output is surpressed by default. + + Output can be enabled by starting the Python interpreter with the {ref}`-W[all|module|once] ` argument. + + It's possible to enable output in code, too. + + ```python + import warnings + warnings.simplefilter("module") + ``` + +Configure logging + +: Once output is enabled, it's possible to use {func}`python:logging.captureWarnings` to redirect warnings to the logger. + + ```python + import logging + logging.captureWarnings(True) + ``` + +### Running tests + +In Plone, test deprecation warnings are not shown by default. +The {file}`zope.conf` setting is not taken into account. + +To enable deprecation warnings, use the `-W` command. + +Given you're using a modern buildout with a virtual environment as recommended, the command would be the following + +```shell +./bin/python -W module ./bin/test +``` + + +## Deprecation best practices + +It's recommended to follow these best practices when deprecating code. + + +### Vanilla deprecation messages + +Python offers a built-in exception {exc}`DeprecationWarning` which can be issued using the standard library's {mod}`warnings` module. + +Its basic usage is the following example. + +```python +import warnings +warnings.warn("deprecated", DeprecationWarning) +``` + + +### Move an entire module + +Given a package {file}`old.pkg` with a module {file}`foo.py`, to move it to a package {file}`new.pkg` as {file}`bar.py`, go through the following steps. + +[`zope.deprecation` Moving modules](https://zopedeprecation.readthedocs.io/en/latest/api.html#moving-modules) offers a helper. + +1. Move the {file}`foo.py` as {file}`bar.py` to the {file}`new.pkg`. +1. At the old place, create a new {file}`foo.py`, and add to it the following lines of code. + + ```python + from zope.deprecation import moved + moved("new.pkg.bar", "Version 2.0") + ``` + +1. Now you can still import the namespace from `bar` at the old place, but get a deprecation warning. + + ```console + DeprecationWarning: old.pkg.foo has moved to new.pkg.bar. + Import of old.pkg.foo will become unsupported in Version 2.0 + ``` + + +### Move an entire package + +To move an entire package, the process is exactly the same as moving a module, but instead, create a file for each module in the package. + + +### Deprecate methods and properties + +Use the `@deprecate` decorator from [`zope.deprecation` Deprecating methods and properties](https://zopedeprecation.readthedocs.io/en/latest/api.html#deprecating-methods-and-properties) to deprecate methods in a module. + +```python +from zope.deprecation import deprecate + +@deprecate("Old method is no longer supported, use new_method instead.") +def old_method(): + return "some value" +``` + +The `@deprecated` wrapper method deprecates properties. + +```python +from zope.deprecation import deprecated + +foo = None +foo = deprecated(foo, "foo is no more, use bar instead") +``` + + +### Move functions and classes + +This example describes how to move some classes or functions from a Python file at {file}`old/foo/bar.py` to {file}`new/baz/baaz.py`. +Here, `zope.deferredimport` offers a deprecation helper. +It also avoids circular imports at initialization time. + +```python +import zope.deferredimport +zope.deferredimport.initialize() + +zope.deferredimport.deprecated( + "Import from new.baz.baaz instead", + SomeOldClass="new.baz:baaz.SomeMovedClass", + some_old_function="new.baz:baaz.some_moved_function", +) + +def some_function_which_is_not_touched_at_all(): + pass +``` + +### Deprecate a GenericSetup profile + +In GenericSetup, the `post_handler` attribute in ZCML can be used to call a function after the profile was applied. +Use this feature to issue a warning. + +First, register the same profile twice, under both the new name and old. + +```xml + + + +``` + +Then in {file}`setuphandlers.py`, add a function. + +```python +import warnings + +def deprecate_profile_some_confusing_name(tool): + warnings.warn( + 'The profile with id "some_confusing_name" was renamed to "default".', + DeprecationWarning + ) +``` + +### Deprecate a template position + +Sometimes you need to move templates to new locations. +Since add-ons often use [`z3c.jbot`](https://github.com/zopefoundation/z3c.jbot) to override templates by their position, you'll need to point them to the new position as well as make sure that the override still works with the old position. + +To deprecate a package, follow these steps. + +1. In the old package folder's {file}`__init__.py`, add a dictionary `jbot_deprecations` that maps the old template locations to their new counterparts. + + ```python + jbot_deprecations = { + "plone.locking.browser.info.pt": "plone.app.layout.viewlets.info.pt" + } + ``` + +1. Add this deprecation snippet to the package {file}`configure.zcml` file. + + ```{code-block} xml + :emphasize-lines: 7-14 + :linenos: + + + + + + + + ``` + +If a `z3c.jbot` version that supports deprecation is found, trying to override the template with the old location will trigger a deprecation warning that will instruct the user to rename its override file. diff --git a/docs/developer-guide/index.md b/docs/developer-guide/index.md index 940f95007..22eb2c572 100644 --- a/docs/developer-guide/index.md +++ b/docs/developer-guide/index.md @@ -25,4 +25,5 @@ develop-volto-add-ons-index create-a-distribution standardize-python-project-configuration native-namespace +deprecation ```