Skip to content

Commit 7d4ae3a

Browse files
committed
Added deprecation framework
- Added deprecation decorators - Added deprecation store - Added deprecation to auto documentation - Added deprecation to be shown in `WebvizPluginPlaceholder` component
1 parent 258162d commit 7d4ae3a

17 files changed

+549
-65
lines changed

CONTRIBUTING.md

+50
Original file line numberDiff line numberDiff line change
@@ -636,3 +636,53 @@ $$\alpha = \frac{\beta}{\gamma}$$
636636

637637
Example of auto-built documentation for `webviz-config` can be seen
638638
[here on github](https://equinor.github.io/webviz-config/).
639+
640+
641+
## Deprecate plugins or arguments
642+
643+
Plugins can be marked as deprecated by using the `@deprecated_plugin(short_message, long_message)` decorator.
644+
645+
```python
646+
from webviz_config.webviz_deprecated import deprecated_plugin
647+
648+
649+
@deprecated_plugin("This message is shown to the end user in the app.", "This message is shown in the documentation of the plugin.")
650+
class MyPlugin(WebvizPluginABC):
651+
...
652+
```
653+
654+
Plugin arguments can be marked as deprecated by using the `@deprecated_plugin_arguments(check={})` decorator in front of the `__init__` function.
655+
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.
656+
657+
```python
658+
from typing import Optional, Tuple
659+
from webviz_config.webviz_deprecated import deprecated_plugin_arguments
660+
661+
662+
class MyPluginExample1(WebvizPluginABC):
663+
...
664+
@deprecated_plugin_arguments(
665+
{
666+
"arg3": (
667+
"Short message shown to the end user both in the app and documentation.",
668+
(
669+
"This can be a long message, which is shown only in the documentation, explaining "
670+
"e.g. why it is deprecated and which plugin should be used instead."
671+
)
672+
)
673+
}
674+
)
675+
def __init__(self, arg1: str, arg2: int, arg3: Optional[int] = None):
676+
...
677+
678+
class MyPluginExample2(WebvizPluginABC):
679+
...
680+
@deprecated_plugin_arguments(check_deprecation)
681+
def __init__(self, arg1: str, arg2: int, arg3: Optional[int] = None):
682+
...
683+
684+
def check_deprecation(arg1: int, arg3: int) -> Optional[Tuple[str, str]]:
685+
if arg3 == arg1:
686+
return ("This message is shown to the end user in the app.", "This message is shown in the documentation of the plugin.")
687+
return None
688+
```

setup.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def get_long_description() -> str:
2424

2525
TESTS_REQUIRES = [
2626
"bandit",
27-
"black",
27+
"black>=20.8b1",
2828
"jsonschema",
2929
"mock",
3030
"mypy",
@@ -72,6 +72,7 @@ def get_long_description() -> str:
7272
],
7373
},
7474
install_requires=[
75+
"astunparse>=1.6.3",
7576
"bleach>=3.1",
7677
"cryptography>=2.4",
7778
"dash>=1.16",
@@ -85,6 +86,7 @@ def get_long_description() -> str:
8586
"pyarrow>=0.16",
8687
"pyyaml>=5.1",
8788
"tqdm>=4.8",
89+
"dataclasses>=0.8; python_version<'3.7'",
8890
"importlib-metadata>=1.7; python_version<'3.8'",
8991
"typing-extensions>=3.7; python_version<'3.8'",
9092
"webviz-core-components>=0.1.0",

tests/test_plugin_metadata.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import webviz_config
2-
from webviz_config.plugins import metadata
2+
from webviz_config.plugins import METADATA
33

44

55
def test_webviz_config_metadata():
6-
meta = metadata["BannerImage"]
6+
meta = METADATA["BannerImage"]
77

88
assert meta["dist_name"] == "webviz-config"
99
assert meta["dist_version"] == webviz_config.__version__

webviz_config/_config_parser.py

+82-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
import pathlib
44
import inspect
55
import typing
6+
import warnings
67

78
import yaml
89

910
from . import plugins as standard_plugins
1011
from .utils import terminal_colors
1112
from .utils._get_webviz_plugins import _get_webviz_plugins
13+
from ._deprecation_store import (
14+
DEPRECATION_STORE,
15+
DeprecatedArgument,
16+
DeprecatedArgumentCheck,
17+
)
1218

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

@@ -19,7 +25,7 @@ def _call_signature(
1925
config_folder: pathlib.Path,
2026
contact_person: typing.Optional[dict] = None,
2127
) -> tuple:
22-
# pylint: disable=too-many-branches,too-many-statements
28+
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
2329
"""Takes as input the name of a plugin together with user given arguments
2430
(originating from the configuration file). Returns the equivalent Python code wrt.
2531
initiating an instance of that plugin (with the given arguments).
@@ -110,6 +116,76 @@ def _call_signature(
110116
except TypeError:
111117
pass
112118

119+
kwargs_including_defaults = kwargs
120+
deprecation_warnings = []
121+
122+
deprecated_plugin = DEPRECATION_STORE.get_stored_plugin_deprecation(
123+
getattr(standard_plugins, plugin_name)
124+
)
125+
if deprecated_plugin:
126+
deprecation_warnings.append(deprecated_plugin.short_message)
127+
128+
deprecations = DEPRECATION_STORE.get_stored_plugin_argument_deprecations(
129+
getattr(standard_plugins, plugin_name).__init__
130+
)
131+
132+
signature = inspect.signature(getattr(standard_plugins, plugin_name).__init__)
133+
for key, value in signature.parameters.items():
134+
if value.default is not inspect.Parameter.empty and key not in kwargs.keys():
135+
kwargs_including_defaults[key] = value.default
136+
137+
for deprecation in deprecations:
138+
if isinstance(deprecation, DeprecatedArgument):
139+
if deprecation.argument_name in kwargs_including_defaults.keys():
140+
deprecation_warnings.append(deprecation.short_message)
141+
warnings.warn(
142+
"""Deprecated Argument: {} with value '{}' in method {} in module {}
143+
------------------------
144+
{}
145+
===
146+
{}
147+
""".format(
148+
deprecation.argument_name,
149+
kwargs_including_defaults[deprecation.argument_name],
150+
deprecation.method_name,
151+
getattr(deprecation.method_reference, "__module__"),
152+
deprecation.short_message,
153+
deprecation.long_message,
154+
),
155+
FutureWarning,
156+
)
157+
elif isinstance(deprecation, DeprecatedArgumentCheck):
158+
mapped_args: typing.Dict[str, typing.Any] = {}
159+
for arg in deprecation.argument_names:
160+
for name, value in kwargs_including_defaults.items():
161+
if arg == name:
162+
mapped_args[arg] = value
163+
break
164+
165+
result = deprecation.callback(**mapped_args) # type: ignore
166+
if result:
167+
deprecation_warnings.append(result[0])
168+
warnings.warn(
169+
"""Deprecated Argument(s): {} with value '{}' in method {} in module {}
170+
------------------------
171+
{}
172+
===
173+
{}
174+
""".format(
175+
deprecation.argument_names,
176+
[
177+
value
178+
for key, value in kwargs_including_defaults.items()
179+
if key in deprecation.argument_names
180+
],
181+
deprecation.method_name,
182+
getattr(deprecation.method_reference, "__module__"),
183+
result[0],
184+
result[1],
185+
),
186+
FutureWarning,
187+
)
188+
113189
special_args = ""
114190
if "app" in argspec.args:
115191
special_args += "app=app, "
@@ -118,7 +194,10 @@ def _call_signature(
118194

119195
return (
120196
f"{plugin_name}({special_args}**{kwargs})",
121-
f"plugin_layout(contact_person={contact_person})",
197+
(
198+
f"plugin_layout(contact_person={contact_person}"
199+
f", deprecation_warnings={deprecation_warnings})"
200+
),
122201
)
123202

124203

@@ -278,9 +357,7 @@ def clean_configuration(self) -> None:
278357
)
279358

280359
plugin["_call_signature"] = _call_signature(
281-
plugin_name,
282-
kwargs,
283-
self._config_folder,
360+
plugin_name, kwargs, self._config_folder,
284361
)
285362

286363
self.assets.update(getattr(standard_plugins, plugin_name).ASSETS)

webviz_config/_deprecation_store.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import Any, List, Optional, Dict, Callable, Union
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass(frozen=True)
6+
class DeprecatedPlugin:
7+
class_reference: Any
8+
short_message: str
9+
long_message: str
10+
11+
12+
@dataclass(frozen=True)
13+
class DeprecatedArgument:
14+
method_reference: Any
15+
method_name: str
16+
argument_name: str
17+
argument_value: str
18+
short_message: str
19+
long_message: str
20+
21+
22+
@dataclass(frozen=True)
23+
class DeprecatedArgumentCheck:
24+
method_reference: Any
25+
method_name: str
26+
argument_names: List[str]
27+
callback: Callable
28+
callback_code: str
29+
30+
31+
class DeprecationStore:
32+
def __init__(self) -> None:
33+
self.stored_plugin_deprecations: Dict[Any, DeprecatedPlugin] = {}
34+
self.stored_plugin_argument_deprecations: List[
35+
Union[DeprecatedArgument, DeprecatedArgumentCheck]
36+
] = []
37+
38+
def register_deprecated_plugin(self, deprecated_plugin: DeprecatedPlugin) -> None:
39+
"""This function is automatically called by the decorator
40+
@deprecated_plugin, registering the plugin it decorates.
41+
"""
42+
self.stored_plugin_deprecations[
43+
deprecated_plugin.class_reference
44+
] = deprecated_plugin
45+
46+
def register_deprecated_plugin_argument(
47+
self,
48+
deprecated_plugin_argument: Union[DeprecatedArgument, DeprecatedArgumentCheck],
49+
) -> None:
50+
"""This function is automatically called by the decorator
51+
@deprecated_plugin_arguments, registering the __init__ function it decorates.
52+
"""
53+
self.stored_plugin_argument_deprecations.append(deprecated_plugin_argument)
54+
55+
def get_stored_plugin_deprecation(self, plugin: Any) -> Optional[DeprecatedPlugin]:
56+
return self.stored_plugin_deprecations.get(plugin)
57+
58+
def get_stored_plugin_argument_deprecations(
59+
self, method: Callable
60+
) -> List[Union[DeprecatedArgument, DeprecatedArgumentCheck]]:
61+
return [
62+
stored
63+
for stored in self.stored_plugin_argument_deprecations
64+
if stored.method_reference == method
65+
]
66+
67+
68+
DEPRECATION_STORE = DeprecationStore()

0 commit comments

Comments
 (0)