Skip to content

Commit

Permalink
Added deprecation decorator (#419)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenthoms authored Apr 27, 2021
1 parent 0be4989 commit e42cc2d
Show file tree
Hide file tree
Showing 14 changed files with 550 additions and 43 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [UNRELEASED] - YYYY-MM-DD

### Added
- [#419](https://github.com/equinor/webviz-config/pull/419) - Plugins and their
arguments can now be marked as deprecated by using the newly implemented
deprecation framework that, in addition to console FutureWarnings,
automatically shows deprecation messages to the end user in the app and in the documentation.
Adjusted contributing guide accordingly.
- [#318](https://github.com/equinor/webviz-config/pull/318) - `webviz-config`
now facilitates automatically including necessary plugin projects as dependencies
in generated Docker setup. Private repositories are also supported, however the
Expand All @@ -20,6 +25,8 @@ output, such that Webviz themes can optionally reduce CSS scope to the output fr
the `Markdown` plugin.

### Changed
- [#419](https://github.com/equinor/webviz-config/pull/419) - Changed `plugin_metadata`
and `plugin_project_metadata` in `webviz_config.plugins` to uppercase.
- [#409](https://github.com/equinor/webviz-config/pull/409) - Relax Python test
dependency constraints.
- [#318](https://github.com/equinor/webviz-config/pull/318) - Ad-hoc plugins not
Expand Down
51 changes: 50 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,4 +606,53 @@ explicitly state git pointer/reference (thereby not use the one derived from
`GIT_POINTER_NAME_PLUGIN_PROJECT`.

For private repositories, a GitHub SSH deploy key will need to be provided to the Docker
build process (see instructions in `README` created with the portable application).
build process (see instructions in `README` created with the portable application).

## Deprecate plugins or arguments

Plugins can be marked as deprecated by using the `@deprecated_plugin(short_message, long_message)` decorator.

```python
from webviz_config.deprecation_decorators import deprecated_plugin


@deprecated_plugin("This message is shown to the end user in the app.", "This message is shown in the documentation of the plugin.")
class MyPlugin(WebvizPluginABC):
...
```

Plugin arguments can be marked as deprecated by using the `@deprecated_plugin_arguments(check={})` decorator in front of the `__init__` function.
Arguments can either be marked as deprecated in any case (see `MyPluginExample1`) or their values can be checked within a function (see `MyPluginExample2`) which returns a tuple containing a short string shown to the end user in the app and a long string shown in the plugin's documentation.

```python
from typing import Optional, Tuple
from webviz_config.deprecation_decorators import deprecated_plugin_arguments


class MyPluginExample1(WebvizPluginABC):
...
@deprecated_plugin_arguments(
{
"arg3": (
"Short message shown to the end user both in the app and documentation.",
(
"This can be a long message, which is shown only in the documentation, explaining "
"e.g. why it is deprecated and which plugin should be used instead."
)
)
}
)
def __init__(self, arg1: str, arg2: int, arg3: Optional[int] = None):
...

class MyPluginExample2(WebvizPluginABC):
...
@deprecated_plugin_arguments(check_deprecation)
def __init__(self, arg1: str, arg2: int, arg3: Optional[int] = None):
...

def check_deprecation(arg1: int, arg3: int) -> Optional[Tuple[str, str]]:
if arg3 == arg1:
return ("This message is shown to the end user in the app.", "This message is shown in the documentation of the plugin.")
return None
```
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ def get_long_description() -> str:
"pyyaml>=5.1",
"requests>=2.20",
"tqdm>=4.8",
"dataclasses>=0.8; python_version<'3.7'",
"importlib-metadata>=1.7; python_version<'3.8'",
"typing-extensions>=3.7; python_version<'3.8'",
"webviz-core-components>=0.1.0",
"webviz-core-components>=0.4.0",
],
extras_require={
"tests": TESTS_REQUIRES,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_plugin_metadata.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import webviz_config
from webviz_config.plugins import plugin_project_metadata
from webviz_config.plugins import PLUGIN_PROJECT_METADATA


def test_webviz_config_metadata():

metadata = plugin_project_metadata["webviz-config"]
metadata = PLUGIN_PROJECT_METADATA["webviz-config"]

assert metadata["dist_version"] == webviz_config.__version__
assert metadata["documentation_url"] == "https://equinor.github.io/webviz-config"
Expand Down
83 changes: 79 additions & 4 deletions webviz_config/_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import sys
import pathlib
import inspect
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Any
import warnings

import yaml

import webviz_config.plugins
from .utils import terminal_colors
from .utils._get_webviz_plugins import _get_webviz_plugins
from . import _deprecation_store as _ds

SPECIAL_ARGS = ["self", "app", "webviz_settings", "_call_signature"]

Expand All @@ -19,7 +21,7 @@ def _call_signature(
config_folder: pathlib.Path,
contact_person: Optional[dict] = None,
) -> tuple:
# pylint: disable=too-many-branches,too-many-statements
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
"""Takes as input the name of a plugin together with user given arguments
(originating from the configuration file). Returns the equivalent Python code wrt.
initiating an instance of that plugin (with the given arguments).
Expand Down Expand Up @@ -112,6 +114,76 @@ def _call_signature(
except TypeError:
pass

kwargs_including_defaults = kwargs
deprecation_warnings = []

deprecated_plugin = _ds.DEPRECATION_STORE.get_stored_plugin_deprecation(
getattr(webviz_config.plugins, plugin_name)
)
if deprecated_plugin:
deprecation_warnings.append(deprecated_plugin.short_message)

deprecations = _ds.DEPRECATION_STORE.get_stored_plugin_argument_deprecations(
getattr(webviz_config.plugins, plugin_name).__init__
)

signature = inspect.signature(getattr(webviz_config.plugins, plugin_name).__init__)
for key, value in signature.parameters.items():
if value.default is not inspect.Parameter.empty and key not in kwargs.keys():
kwargs_including_defaults[key] = value.default

for deprecation in deprecations:
if isinstance(deprecation, _ds.DeprecatedArgument):
if deprecation.argument_name in kwargs_including_defaults.keys():
deprecation_warnings.append(deprecation.short_message)
warnings.warn(
"""Deprecated Argument: {} with value '{}' in method {} in module {}
------------------------
{}
===
{}
""".format(
deprecation.argument_name,
kwargs_including_defaults[deprecation.argument_name],
deprecation.method_name,
getattr(deprecation.method_reference, "__module__"),
deprecation.short_message,
deprecation.long_message,
),
FutureWarning,
)
elif isinstance(deprecation, _ds.DeprecatedArgumentCheck):
mapped_args: Dict[str, Any] = {}
for arg in deprecation.argument_names:
for name, value in kwargs_including_defaults.items():
if arg == name:
mapped_args[arg] = value
break

result = deprecation.callback(**mapped_args) # type: ignore
if result:
deprecation_warnings.append(result[0])
warnings.warn(
"""Deprecated Argument(s): {} with value '{}' in method {} in module {}
------------------------
{}
===
{}
""".format(
deprecation.argument_names,
[
value
for key, value in kwargs_including_defaults.items()
if key in deprecation.argument_names
],
deprecation.method_name,
getattr(deprecation.method_reference, "__module__"),
result[0],
result[1],
),
FutureWarning,
)

special_args = ""
if "app" in argspec.args:
special_args += "app=app, "
Expand All @@ -120,7 +192,10 @@ def _call_signature(

return (
f"{plugin_name}({special_args}**{kwargs})",
f"plugin_layout(contact_person={contact_person})",
(
f"plugin_layout(contact_person={contact_person}"
f", deprecation_warnings={deprecation_warnings})"
),
)


Expand Down Expand Up @@ -291,7 +366,7 @@ def clean_configuration(self) -> None:
self._assets.update(getattr(webviz_config.plugins, plugin_name).ASSETS)
self._plugin_metadata[
plugin_name
] = webviz_config.plugins.plugin_metadata[plugin_name]
] = webviz_config.plugins.PLUGIN_METADATA[plugin_name]

@property
def configuration(self) -> dict:
Expand Down
68 changes: 68 additions & 0 deletions webviz_config/_deprecation_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Any, List, Optional, Dict, Callable, Union
from dataclasses import dataclass


@dataclass(frozen=True)
class DeprecatedPlugin:
class_reference: Any
short_message: str
long_message: str


@dataclass(frozen=True)
class DeprecatedArgument:
method_reference: Any
method_name: str
argument_name: str
argument_value: str
short_message: str
long_message: str


@dataclass(frozen=True)
class DeprecatedArgumentCheck:
method_reference: Any
method_name: str
argument_names: List[str]
callback: Callable
callback_code: str


class DeprecationStore:
def __init__(self) -> None:
self.stored_plugin_deprecations: Dict[Any, DeprecatedPlugin] = {}
self.stored_plugin_argument_deprecations: List[
Union[DeprecatedArgument, DeprecatedArgumentCheck]
] = []

def register_deprecated_plugin(self, deprecated_plugin: DeprecatedPlugin) -> None:
"""This function is automatically called by the decorator
@deprecated_plugin, registering the plugin it decorates.
"""
self.stored_plugin_deprecations[
deprecated_plugin.class_reference
] = deprecated_plugin

def register_deprecated_plugin_argument(
self,
deprecated_plugin_argument: Union[DeprecatedArgument, DeprecatedArgumentCheck],
) -> None:
"""This function is automatically called by the decorator
@deprecated_plugin_arguments, registering the __init__ function it decorates.
"""
self.stored_plugin_argument_deprecations.append(deprecated_plugin_argument)

def get_stored_plugin_deprecation(self, plugin: Any) -> Optional[DeprecatedPlugin]:
return self.stored_plugin_deprecations.get(plugin)

def get_stored_plugin_argument_deprecations(
self, method: Callable
) -> List[Union[DeprecatedArgument, DeprecatedArgumentCheck]]:
return [
stored
for stored in self.stored_plugin_argument_deprecations
if stored.method_reference == method
]


DEPRECATION_STORE = DeprecationStore()
6 changes: 3 additions & 3 deletions webviz_config/_dockerize/_create_docker_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import jinja2
import requests

from ..plugins import plugin_project_metadata
from ..plugins import PLUGIN_PROJECT_METADATA
from ._pip_git_url import pip_git_url


Expand All @@ -31,13 +31,13 @@ def create_docker_setup(
template = template_environment.get_template("Dockerfile.jinja2")

distributions = {
metadata["dist_name"]: plugin_project_metadata[metadata["dist_name"]]
metadata["dist_name"]: PLUGIN_PROJECT_METADATA[metadata["dist_name"]]
for metadata in plugin_metadata.values()
}

# Regardless of a standard webviz-config plugin is included in user's
# configuration file, we still need to install the plugin framework webviz-config:
distributions["webviz-config"] = plugin_project_metadata["webviz-config"]
distributions["webviz-config"] = PLUGIN_PROJECT_METADATA["webviz-config"]

requirements = get_python_requirements(distributions)
requirements.append("gunicorn")
Expand Down
Loading

0 comments on commit e42cc2d

Please sign in to comment.