diff --git a/docs/backend/deprecation.md b/docs/backend/deprecation.md new file mode 100644 index 000000000..80a0a98e6 --- /dev/null +++ b/docs/backend/deprecation.md @@ -0,0 +1,318 @@ +--- +myst: + html_meta: + "description": "A guide how to do deprecations, including Python, ZCML and templates in Plone." + "property=og:description": "A guide how to do deprecations, including Python, ZCML and templates in Plone." + "property=og:title": "Deprecation" + "keywords": "deprecation, zcml, template, jbot" +--- + +(backend-deprecation-label)= +# Deprecation + +## Introduction + +This document describes rationales, configuration and best practices of deprecations in Plone, Zope and Python. +It is meant as a styleguide on how to apply deprecations in Plone core packages. +It also has a value as a general overview on how to deprecate in Python. + + +### Why Deprecation + +At some point we: + +- need to get rid of old code, +- want to unify API style (consistent API), +- fix typos in namings, +- move code or templates around (inside package or to another package). + +While refactoring code, moving modules, functions, classes and methods is often needed. +To not break third party code imports from the old place or usage of old functions/ methods must work for while. +Deprecated methods are usually removed with the next major release of Plone. + +Following the [semantic versioning guideline](https://semver.org) is recommended. + +### Help Programmers, No annoyance + +The developers should use code deprecations to support the consumers of the code. +From their point of view, Plone core code is an API to them. +Any change is annoying to them anyway, but they feel better if deprecation warnings are telling them what to do. + +Deprecations must always log at level *warning* and have to answers the question: + +**"Why is the code gone from the old place? What to do instead?"** + +A short message is enough., i.e.: + +- "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 has to be done once, i.e. on first usage or first import. +It must not flood the logs. + +### Use Cases + +Renaming + +: We may want to rename classes, methods, functions or global or class variables in order to get a more consistent API or because of a typo, etc. + We never just rename, we always provide a deprecated version logging a verbose deprecation warning with information where to + import from in future. + +Moving a module, class, function, etc to another place + +: For some reason, i.e. merging packages, consistent API or resolving cirular import problems, we need to move code around. + When imported from the old place it logs a verbose deprecation warning with information where to import from in future. + +Deprecation of a whole package + +: A whole [package](https://docs.python.org/3/tutorial/modules.html#packages) + + - all imports still working, logging deprecation warnings on first import + - ZCML still exists, but is empty (or includes the zcml from the new place if theres no auto import (i.e. for meta.zcml). + +Deprecation of a whole released/ installable package. + +: We will provide a last major release with no 'real' code, only backward compatible (bbb) imports of public API are provided. + This will be done the way described above for a whole package. + The README clearly states why it was moved and where to find the code now. + +Deprecation of a GenericSetup profile + +: They may got renamed for consistency or are superfluos after an update. + Code does not need to break to support this. + +## Enable Deprecation Warnings + +### Zope + +Zope does configure logging and warnings, so the steps below (under section Python) are not needed. + +Using `plone.recipe.zope2instance` add the option `deprecation-warnings = on` to the buildouts `[instance]` section. + +```ini +[buildout] +parts = instance + +[instance] +recipe = plone.recipe.zope2instance +... +deprecation-warnings = on +... +``` + +This adds this line to the `zope.conf` file: + +``` +debug-mode on +``` + +Without the recipe this can be set manually as well: +In `zope.conf` custom filters for warnings can be defined. + +```xml +... + + action always + category exceptions.DeprecationWarning + +... +``` + +### 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 [-W \[all|module|once\]](https://docs.python.org/3/using/cmdline.html#cmdoption-W) option. + + It is possible to enable output in code too: + + ```python + import warnings + warnings.simplefilter("module") + ``` + +Configure Logging + +: Once output is enabled it is possible to [redirect warnings to the logger](https://docs.python.org/3/library/logging.html#logging.captureWarnings): + + ```python + import logging + logging.captureWarnings(True) + ``` + +### Running tests + +In Plone tests deprecation warnings are not shown by default. +The `zope.conf` setting is not taken into account. + +In order to enable deprecation warnings, +the Python way with the `-W` command option must to be used. + +Given youre using a modern buildout with virtualenv as recommended, +the call looks like so: + +```bash +./bin/python -W module ./bin/test +``` + +## Deprecation Best Practice + +### Vanilla Deprecation Messages + +Python offers a built-in `DeprecationWarning` which can be issued using standard libraries `warnings` module. + +For details read the [official documentation about warnings](https://docs.python.org/3/library/warnings.html). + +In short it works like so + +```python +import warnings +warnings.warn('deprecated', DeprecationWarning) +``` + +### Moving Whole Modules + +Given a package `old.pkg` with a module `foo.py` need to be moved to a package `new.pkg` as `bar.py`. + +[zope.deprecation Moving modules](https://zopedeprecation.readthedocs.io/en/latest/api.html#moving-modules) offers a helper. + +1. Move the `foo.py` as `bar.py` to the `new.pkg`. +2. At the old place create a new `foo.py` and add to it + +```python +from zope.deprecation import moved +moved('new.pkg.bar', 'Version 2.0') +``` + +Now you can still import the namespace from `bar` at the old place, but get a deprecation warning: + +> DeprecationWarning: old.pkg.foo has moved to new.pkg.bar. +> Import of old.pkg.foo will become unsupported in Version 2.0 + +### Moving Whole Packages + +This is the same as moving a module, just create for each module a file. + +### Deprecating methods and properties + +You can 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 is for deprecating properties: + +```python +from zope.deprecation import deprecated + +foo = None +foo = deprecated(foo, 'foo is no more, use bar instead') +``` + +### Moving functions and classes + +Given we have a Python file at `old/foo/bar.py` and want to move some classes or functions to `new/baz/baaz.py`. + +Here `zope.deferredimport` offers a deprecation helper. +It also avoids circular imports on 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 +``` + +### Deprecating a GenericSetup profile + +Starting with GenericSetup 1.8.2 (part of Plone > 5.0.2) the `post_handler` attribute in ZCML can be used to call a function after the profile was applied. +We use this feature to issue a warning. + +First we register the same profile twice. Under the new name and under the old name: + +```xml + + + +``` + +And in `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 + ) +``` + +### Deprecating a template position + +Sometimes we need to move templates to new locations. Since addons often use [z3c.jbot](https://github.com/zopefoundation/z3c.jbot) to override templates by their position, we 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: + +1. In the old package folders `__init__.py` add a dictionary `jbot_deprecations` that maps the old template locations to their new counterparts, e.g.: + +```python +jbot_deprecations = { + "plone.locking.browser.info.pt": "plone.app.layout.viewlets.info.pt" +} +``` + +2. Add this deprecation snippet to the package `configure.zcml` file: + +```{code-block} xml +:emphasize-lines: 6,9-12 +: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/backend/index.md b/docs/backend/index.md index 3e7dd3278..9411e26f7 100644 --- a/docs/backend/index.md +++ b/docs/backend/index.md @@ -22,6 +22,7 @@ behaviors configuration-registry content-types/index control-panels +deprecation fields global-utils indexing