From b9f073d6cdb5ae94a76499313cdc35d1b71a1637 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 24 Sep 2021 14:09:34 +0200 Subject: [PATCH 01/24] Add the deprecated decorator --- openfisca_core/commons/__init__.py | 1 + openfisca_core/commons/decorators.py | 76 +++++++++++++++++++ openfisca_core/commons/tests/__init__.py | 0 .../commons/tests/test_decorators.py | 20 +++++ setup.cfg | 2 +- 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 openfisca_core/commons/decorators.py create mode 100644 openfisca_core/commons/tests/__init__.py create mode 100644 openfisca_core/commons/tests/test_decorators.py diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index db41ed1874..42c1ada223 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -23,6 +23,7 @@ from .dummy import Dummy # noqa: F401 +from .decorators import deprecated # noqa: F401 from .formulas import apply_thresholds, concat, switch # noqa: F401 from .misc import empty_clone, stringify_array # noqa: F401 from .rates import average_rate, marginal_rate # noqa: F401 diff --git a/openfisca_core/commons/decorators.py b/openfisca_core/commons/decorators.py new file mode 100644 index 0000000000..2041a7f4f7 --- /dev/null +++ b/openfisca_core/commons/decorators.py @@ -0,0 +1,76 @@ +import functools +import warnings +import typing +from typing import Any, Callable, TypeVar + +T = Callable[..., Any] +F = TypeVar("F", bound = T) + + +class deprecated: + """Allows (soft) deprecating a functionality of OpenFisca. + + Attributes: + since (:obj:`str`): Since when the functionality is deprecated. + expires (:obj:`str`): When will it be removed forever? + + Args: + since: Since when the functionality is deprecated. + expires: When will it be removed forever? + + Examples: + >>> @deprecated(since = "35.5.0", expires = "in the future") + ... def obsolete(): + ... return "I'm obsolete!" + + >>> repr(obsolete) + '' + + >>> str(obsolete) + '' + + .. versionadded:: 35.6.0 + + """ + + since: str + expires: str + + def __init__(self, since: str, expires: str) -> None: + self.since = since + self.expires = expires + + def __call__(self, function: F) -> F: + """Wraps a function to return another one, decorated. + + Args: + function: The function or method to decorate. + + Returns: + :obj:`callable`: The decorated function. + + Examples: + >>> def obsolete(): + ... return "I'm obsolete!" + + >>> decorator = deprecated( + ... since = "35.5.0", + ... expires = "in the future", + ... ) + + >>> decorator(obsolete) + + + """ + + def wrapper(*args: Any, **kwds: Any) -> Any: + message = [ + f"{function.__qualname__} has been deprecated since", + f"version {self.since}, and will be removed in", + f"{self.expires}.", + ] + warnings.warn(" ".join(message), DeprecationWarning) + return function(*args, **kwds) + + functools.update_wrapper(wrapper, function) + return typing.cast(F, wrapper) diff --git a/openfisca_core/commons/tests/__init__.py b/openfisca_core/commons/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/commons/tests/test_decorators.py b/openfisca_core/commons/tests/test_decorators.py new file mode 100644 index 0000000000..04c5ce3d91 --- /dev/null +++ b/openfisca_core/commons/tests/test_decorators.py @@ -0,0 +1,20 @@ +import re + +import pytest + +from openfisca_core.commons import deprecated + + +def test_deprecated(): + """The decorated function throws a deprecation warning when used.""" + + since = "yesterday" + expires = "doomsday" + match = re.compile(f"^.*{since}.*{expires}.*$") + + @deprecated(since, expires) + def function(a: int, b: float) -> float: + return a + b + + with pytest.warns(DeprecationWarning, match = match): + assert function(1, 2.) == 3. diff --git a/setup.cfg b/setup.cfg index 4f98591eeb..9ed6e11c9e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ hang-closing = true ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true rst-roles = any, class, exc, meth, obj -rst-directives = attribute +rst-directives = attribute, versionadded [tool:pytest] addopts = --showlocals --doctest-modules --disable-pytest-warnings From 1117f07a214dfbea660d4ad6c8d13f6463c22cf9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 24 Sep 2021 14:12:35 +0200 Subject: [PATCH 02/24] Use decorator with already deprecated Dummy --- openfisca_core/commons/dummy.py | 22 ++++++++++++++-------- openfisca_core/commons/tests/test_dummy.py | 10 ++++++++++ setup.cfg | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 openfisca_core/commons/tests/test_dummy.py diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 4136a0d429..7cd1951931 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -1,13 +1,19 @@ -import warnings +from .decorators import deprecated class Dummy: - """A class that does nothing.""" + """A class that did nothing. + Examples: + >>> Dummy() + None: - message = [ - "The 'Dummy' class has been deprecated since version 34.7.0,", - "and will be removed in the future.", - ] - warnings.warn(" ".join(message), DeprecationWarning) - pass + ... diff --git a/openfisca_core/commons/tests/test_dummy.py b/openfisca_core/commons/tests/test_dummy.py new file mode 100644 index 0000000000..d4ecec3842 --- /dev/null +++ b/openfisca_core/commons/tests/test_dummy.py @@ -0,0 +1,10 @@ +import pytest + +from openfisca_core.commons import Dummy + + +def test_dummy_deprecation(): + """Dummy throws a deprecation warning when instantiated.""" + + with pytest.warns(DeprecationWarning): + assert Dummy() diff --git a/setup.cfg b/setup.cfg index 9ed6e11c9e..3fb18ecb66 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ hang-closing = true ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true rst-roles = any, class, exc, meth, obj -rst-directives = attribute, versionadded +rst-directives = attribute, deprecated, versionadded [tool:pytest] addopts = --showlocals --doctest-modules --disable-pytest-warnings From bc248daca33971c1fdca128307b97eaae9d5dc53 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 24 Sep 2021 18:58:47 +0200 Subject: [PATCH 03/24] Add script to list deprecations --- openfisca_core/scripts/find_deprecations.py | 58 +++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 openfisca_core/scripts/find_deprecations.py diff --git a/openfisca_core/scripts/find_deprecations.py b/openfisca_core/scripts/find_deprecations.py new file mode 100644 index 0000000000..6a6724a966 --- /dev/null +++ b/openfisca_core/scripts/find_deprecations.py @@ -0,0 +1,58 @@ +import ast +import pkg_resources +import pathlib +import textwrap +import subprocess + +CURRENT_VERSION = pkg_resources.get_distribution("openfisca_core").version + + +class FindDeprecated(ast.NodeVisitor): + + def __init__(self): + result = subprocess.run( + ["git", "ls-files", "*.py"], + stdout = subprocess.PIPE, + ) + + self.files = result.stdout.decode("utf-8").split() + self.nodes = [self.node(file) for file in self.files] + + def __call__(self): + for count, node in enumerate(self.nodes): + self.count = count + self.visit(node) + + def visit_FunctionDef(self, node): + for decorator in node.decorator_list: + if "deprecated" in ast.dump(decorator): + file = self.files[self.count] + path = pathlib.Path(file).resolve() + module = f"{path.parts[-2]}.{path.stem}" + lineno = node.lineno + 1 + since, expires = decorator.keywords + + # breakpoint() + + message = [ + f"[{module}.{node.name}:{lineno}]", + f"Deprecated since: {since.value.s}.", + f"Expiration status: {expires.value.s}", + f"(current: {CURRENT_VERSION}).", + ] + + print(" ".join(message)) + + def node(self, file): + try: + with open(file, "rb") as f: + source = textwrap.dedent(f.read().decode("utf-8")) + node = ast.parse(source, file, "exec") + return node + + except IsADirectoryError: + pass + + +find = FindDeprecated() +find() From d60372291dbfe6f735c7f8b10daa7104b921facb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 24 Sep 2021 19:20:30 +0200 Subject: [PATCH 04/24] Add task to Makefile --- Makefile | 2 +- .../scripts/{find_deprecations.py => find_deprecated.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename openfisca_core/scripts/{find_deprecations.py => find_deprecated.py} (100%) diff --git a/Makefile b/Makefile index 2691268681..b5c73a5ff8 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ print_pass = echo $$(tput setaf 2)[✓]$$(tput sgr0) $$(tput setaf 8)$1$$(tput s ## Similar to `print_work`, but this will read the comments above a task, and ## print them to the user at the start of each task. The `$1` is a function ## argument. -print_help = sed -n "/^$1/ { x ; p ; } ; s/\#\#/$(print_work)/ ; s/\./…/ ; x" ${MAKEFILE_LIST} +print_help = sed -n "/^$1/ { x ; p ; } ; s/\#\#/\r$(print_work)/ ; s/\./…/ ; x" ${MAKEFILE_LIST} ## Same as `make`. .DEFAULT_GOAL := all diff --git a/openfisca_core/scripts/find_deprecations.py b/openfisca_core/scripts/find_deprecated.py similarity index 100% rename from openfisca_core/scripts/find_deprecations.py rename to openfisca_core/scripts/find_deprecated.py From 71753960f8d99871a630ad22a3c545dff95b3d0c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 24 Sep 2021 19:36:27 +0200 Subject: [PATCH 05/24] Remove leftover --- openfisca_core/scripts/find_deprecated.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openfisca_core/scripts/find_deprecated.py b/openfisca_core/scripts/find_deprecated.py index 6a6724a966..bbfd53d488 100644 --- a/openfisca_core/scripts/find_deprecated.py +++ b/openfisca_core/scripts/find_deprecated.py @@ -32,8 +32,6 @@ def visit_FunctionDef(self, node): lineno = node.lineno + 1 since, expires = decorator.keywords - # breakpoint() - message = [ f"[{module}.{node.name}:{lineno}]", f"Deprecated since: {since.value.s}.", From 29c8de6ecbfd0ed4b69f2f997ca30c1a75819411 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 10:52:20 +0200 Subject: [PATCH 06/24] Move to publish submake --- openfisca_make/publish.mk | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 openfisca_make/publish.mk diff --git a/openfisca_make/publish.mk b/openfisca_make/publish.mk new file mode 100644 index 0000000000..23014dc15c --- /dev/null +++ b/openfisca_make/publish.mk @@ -0,0 +1,4 @@ +## Check for features marked as deprecated. +check-deprecated: + @$(call help,$@:) + @python openfisca_core/scripts/find_deprecated.py From b32161d5caac327f4c1c221b29d53e332adde104 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 11:08:11 +0200 Subject: [PATCH 07/24] Add test to find deprecated --- openfisca_core/scripts/__init__.py | 2 ++ .../scripts/tests/test_find_deprecated.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 openfisca_core/scripts/tests/test_find_deprecated.py diff --git a/openfisca_core/scripts/__init__.py b/openfisca_core/scripts/__init__.py index 9e0a3b67bc..a5480c3e82 100644 --- a/openfisca_core/scripts/__init__.py +++ b/openfisca_core/scripts/__init__.py @@ -6,6 +6,8 @@ import pkgutil from os import linesep +from .find_deprecated import FindDeprecated # noqa: F401 + log = logging.getLogger(__name__) diff --git a/openfisca_core/scripts/tests/test_find_deprecated.py b/openfisca_core/scripts/tests/test_find_deprecated.py new file mode 100644 index 0000000000..c26c989e79 --- /dev/null +++ b/openfisca_core/scripts/tests/test_find_deprecated.py @@ -0,0 +1,14 @@ +from openfisca_core.commons import deprecated +from openfisca_core.scripts import FindDeprecated + + +@deprecated(since = "yesterday", expires = "tomorrow") +def function(a: int, b: float) -> float: + return a + b + + +def test_find_deprecated(capsys): + """caca.""" + + FindDeprecated()() + assert "[tests.test_find_deprecated.function:6]" in capsys.readouterr().out From 77626a5a483d655363ae64fe7a7237f2f40265b3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 13:23:35 +0200 Subject: [PATCH 08/24] Raise an error when a deprecation expired --- openfisca_core/commons/decorators.py | 49 ++++++++++++-- .../commons/tests/test_decorators.py | 19 ++++-- openfisca_core/scripts/find_deprecated.py | 67 +++++++++++++------ .../scripts/tests/test_find_deprecated.py | 31 +++++++-- 4 files changed, 130 insertions(+), 36 deletions(-) diff --git a/openfisca_core/commons/decorators.py b/openfisca_core/commons/decorators.py index 2041a7f4f7..91b317ceb6 100644 --- a/openfisca_core/commons/decorators.py +++ b/openfisca_core/commons/decorators.py @@ -1,7 +1,7 @@ import functools import warnings import typing -from typing import Any, Callable, TypeVar +from typing import Any, Callable, Sequence, TypeVar T = Callable[..., Any] F = TypeVar("F", bound = T) @@ -11,12 +11,22 @@ class deprecated: """Allows (soft) deprecating a functionality of OpenFisca. Attributes: - since (:obj:`str`): Since when the functionality is deprecated. - expires (:obj:`str`): When will it be removed forever? + since: + Since when the functionality is deprecated. + expires: + When will it be removed forever? Args: - since: Since when the functionality is deprecated. - expires: When will it be removed forever? + since: + Since when the functionality is deprecated. + expires: + When will it be removed forever? Note that this value, if set to a + valid semantic version, it has to be a major one. + + Raises: + ValueError: + When :attr:`expires` is set to a version, but not to a major one. + Examples: >>> @deprecated(since = "35.5.0", expires = "in the future") @@ -36,9 +46,9 @@ class deprecated: since: str expires: str - def __init__(self, since: str, expires: str) -> None: + def __init__(self, *, since: str, expires: str) -> None: self.since = since - self.expires = expires + self.expires = self._parse(expires) def __call__(self, function: F) -> F: """Wraps a function to return another one, decorated. @@ -64,13 +74,38 @@ def __call__(self, function: F) -> F: """ def wrapper(*args: Any, **kwds: Any) -> Any: + message: Sequence[str] message = [ f"{function.__qualname__} has been deprecated since", f"version {self.since}, and will be removed in", f"{self.expires}.", ] + warnings.warn(" ".join(message), DeprecationWarning) return function(*args, **kwds) functools.update_wrapper(wrapper, function) return typing.cast(F, wrapper) + + @staticmethod + def _parse(expires: str) -> str: + minor: str + patch: str + message: Sequence[str] + + if expires.find(".") == -1: + return expires + + _, minor, patch, *_ = expires.split(".") + + if minor != "0" or patch != "0": + message = [ + "Deprecations can only expire on major releases.", + f"Or, {expires} is not a major one.", + "To learn more about semantic versioning:", + "https://semver.org/" + ] + + raise ValueError(" ".join(message)) + + return expires diff --git a/openfisca_core/commons/tests/test_decorators.py b/openfisca_core/commons/tests/test_decorators.py index 04c5ce3d91..d49f809656 100644 --- a/openfisca_core/commons/tests/test_decorators.py +++ b/openfisca_core/commons/tests/test_decorators.py @@ -9,12 +9,23 @@ def test_deprecated(): """The decorated function throws a deprecation warning when used.""" since = "yesterday" - expires = "doomsday" - match = re.compile(f"^.*{since}.*{expires}.*$") + expires = "tomorrow" + message = re.compile(f"^.*{since}.*{expires}.*$") - @deprecated(since, expires) + @deprecated(since = since, expires = expires) def function(a: int, b: float) -> float: return a + b - with pytest.warns(DeprecationWarning, match = match): + with pytest.warns(DeprecationWarning, match = message): assert function(1, 2.) == 3. + + +def test_deprecated_when_illegal(): + """Raises an error when the deprecation expiration is not a major.""" + + since = "yesterday" + expires = "1.2.3" + message = "Deprecations can only expire on major releases" + + with pytest.raises(ValueError, match = message): + deprecated(since = since, expires = expires) diff --git a/openfisca_core/scripts/find_deprecated.py b/openfisca_core/scripts/find_deprecated.py index bbfd53d488..221dc13772 100644 --- a/openfisca_core/scripts/find_deprecated.py +++ b/openfisca_core/scripts/find_deprecated.py @@ -1,56 +1,81 @@ import ast -import pkg_resources +import os import pathlib -import textwrap +import pkg_resources import subprocess +import sys +import textwrap +from typing import Sequence -CURRENT_VERSION = pkg_resources.get_distribution("openfisca_core").version +VERSION: str +VERSION = pkg_resources.get_distribution("openfisca_core").version class FindDeprecated(ast.NodeVisitor): - def __init__(self): + count: int + exit: int = os.EX_OK + files: Sequence[str] + nodes: Sequence[ast.Module] + version: str + + def __init__(self, version: str = VERSION) -> None: result = subprocess.run( ["git", "ls-files", "*.py"], stdout = subprocess.PIPE, ) self.files = result.stdout.decode("utf-8").split() - self.nodes = [self.node(file) for file in self.files] + self.nodes = [self._node(file) for file in self.files] + self.version = version - def __call__(self): + def __call__(self) -> None: for count, node in enumerate(self.nodes): self.count = count self.visit(node) - def visit_FunctionDef(self, node): + def visit_FunctionDef(self, node) -> None: + expires: ast.Str + file: str + lineno: int + message: Sequence[str] + module: str + path: pathlib.Path + since: ast.Str + for decorator in node.decorator_list: if "deprecated" in ast.dump(decorator): file = self.files[self.count] path = pathlib.Path(file).resolve() module = f"{path.parts[-2]}.{path.stem}" lineno = node.lineno + 1 - since, expires = decorator.keywords + since = decorator.keywords[0].value + expires = decorator.keywords[1].value + + if self._isthis(expires.s): + self.exit = 1 message = [ f"[{module}.{node.name}:{lineno}]", - f"Deprecated since: {since.value.s}.", - f"Expiration status: {expires.value.s}", - f"(current: {CURRENT_VERSION}).", + f"Deprecated since: {since.s}.", + f"Expiration status: {expires.s}", + f"(current: {self.version}).", ] print(" ".join(message)) - def node(self, file): - try: - with open(file, "rb") as f: - source = textwrap.dedent(f.read().decode("utf-8")) - node = ast.parse(source, file, "exec") - return node + def _isthis(self, version: str) -> bool: + return self.version == version + + def _node(self, file: str) -> ast.Module: + source: str - except IsADirectoryError: - pass + with open(file, "r") as f: + source = textwrap.dedent(f.read()) + return ast.parse(source, file, "exec") -find = FindDeprecated() -find() +if __name__ == "__main__": + find = FindDeprecated() + find() + sys.exit(find.exit) diff --git a/openfisca_core/scripts/tests/test_find_deprecated.py b/openfisca_core/scripts/tests/test_find_deprecated.py index c26c989e79..1924b9468d 100644 --- a/openfisca_core/scripts/tests/test_find_deprecated.py +++ b/openfisca_core/scripts/tests/test_find_deprecated.py @@ -1,14 +1,37 @@ +import os +import sys + +import pytest + from openfisca_core.commons import deprecated from openfisca_core.scripts import FindDeprecated -@deprecated(since = "yesterday", expires = "tomorrow") +@deprecated(since = "yesterday", expires = "1.0.0") def function(a: int, b: float) -> float: return a + b def test_find_deprecated(capsys): - """caca.""" + """Prints out the features marked as deprecated.""" + + find = FindDeprecated() + find() + + with pytest.raises(SystemExit) as exit: + sys.exit(find.exit) + + assert exit.value.code == os.EX_OK + assert "tests.test_find_deprecated.function:11" in capsys.readouterr().out + + +def test_find_deprecated_when_expired(capsys): + """Raises an error when at least one deprecation has expired.""" + + find = FindDeprecated("1.0.0") + find() + + with pytest.raises(SystemExit) as exit: + sys.exit(find.exit) - FindDeprecated()() - assert "[tests.test_find_deprecated.function:6]" in capsys.readouterr().out + assert exit.value.code != os.EX_OK From 4741a8947126bf34b9b88d0092016156b5d1704b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 15:03:06 +0200 Subject: [PATCH 09/24] Test deprecation in isolation --- openfisca_core/scripts/find_deprecated.py | 20 ++++++--- .../scripts/tests/test_find_deprecated.py | 41 +++++++++++++++---- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/openfisca_core/scripts/find_deprecated.py b/openfisca_core/scripts/find_deprecated.py index 221dc13772..393223f2fe 100644 --- a/openfisca_core/scripts/find_deprecated.py +++ b/openfisca_core/scripts/find_deprecated.py @@ -7,6 +7,14 @@ import textwrap from typing import Sequence +FILES: Sequence[str] +FILES = \ + subprocess \ + .run(["git", "ls-files", "*.py"], stdout = subprocess.PIPE) \ + .stdout \ + .decode("utf-8") \ + .split() + VERSION: str VERSION = pkg_resources.get_distribution("openfisca_core").version @@ -19,13 +27,13 @@ class FindDeprecated(ast.NodeVisitor): nodes: Sequence[ast.Module] version: str - def __init__(self, version: str = VERSION) -> None: - result = subprocess.run( - ["git", "ls-files", "*.py"], - stdout = subprocess.PIPE, - ) + def __init__( + self, + files: Sequence[str] = FILES, + version: str = VERSION, + ) -> None: - self.files = result.stdout.decode("utf-8").split() + self.files = files self.nodes = [self._node(file) for file in self.files] self.version = version diff --git a/openfisca_core/scripts/tests/test_find_deprecated.py b/openfisca_core/scripts/tests/test_find_deprecated.py index 1924b9468d..efb2789bf9 100644 --- a/openfisca_core/scripts/tests/test_find_deprecated.py +++ b/openfisca_core/scripts/tests/test_find_deprecated.py @@ -1,35 +1,58 @@ import os import sys +import tempfile import pytest -from openfisca_core.commons import deprecated from openfisca_core.scripts import FindDeprecated -@deprecated(since = "yesterday", expires = "1.0.0") -def function(a: int, b: float) -> float: - return a + b +class Module: + """Some module with an expired function.""" + + def __init__(self, expires = "never"): + self.module = [ + b"from openfisca_core.commons import deprecated", + b"", + b"", + f"@deprecated(since = 'today', expires = '{expires}')".encode(), + b"def function() -> None:", + b" ..." + ] + + def __enter__(self): + self.file = tempfile.NamedTemporaryFile() + self.name = ".".join(self.file.name.split("/")[-2:]) + self.file.write(b"\n".join(self.module)) + self.file.seek(0) + return self.file, self.name + + def __exit__(self, *__): + self.file.close() def test_find_deprecated(capsys): """Prints out the features marked as deprecated.""" - find = FindDeprecated() - find() + with Module() as (file, name): + find = FindDeprecated([file.name]) + find() with pytest.raises(SystemExit) as exit: sys.exit(find.exit) assert exit.value.code == os.EX_OK - assert "tests.test_find_deprecated.function:11" in capsys.readouterr().out + assert f"[{name}.function:5]" in capsys.readouterr().out def test_find_deprecated_when_expired(capsys): """Raises an error when at least one deprecation has expired.""" - find = FindDeprecated("1.0.0") - find() + version = "1.0.0" + + with Module(version) as (file, _): + find = FindDeprecated([file.name], version) + find() with pytest.raises(SystemExit) as exit: sys.exit(find.exit) From 3f8c52354e74be6bbe3648abd4a890f983e7b1ea Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 16:20:24 +0200 Subject: [PATCH 10/24] Move check to the tasks package --- openfisca_core/scripts/__init__.py | 2 -- openfisca_make/publish.mk | 4 ---- openfisca_tasks/__init__.py | 1 + openfisca_tasks/__main__.py | 8 ++++++++ .../_check_deprecated.py | 9 +-------- openfisca_tasks/publish.mk | 6 ++++++ .../tests/test_check_deprecated.py | 14 +++++++------- setup.cfg | 2 +- 8 files changed, 24 insertions(+), 22 deletions(-) delete mode 100644 openfisca_make/publish.mk create mode 100644 openfisca_tasks/__init__.py create mode 100644 openfisca_tasks/__main__.py rename openfisca_core/scripts/find_deprecated.py => openfisca_tasks/_check_deprecated.py (93%) rename openfisca_core/scripts/tests/test_find_deprecated.py => openfisca_tasks/tests/test_check_deprecated.py (83%) diff --git a/openfisca_core/scripts/__init__.py b/openfisca_core/scripts/__init__.py index a5480c3e82..9e0a3b67bc 100644 --- a/openfisca_core/scripts/__init__.py +++ b/openfisca_core/scripts/__init__.py @@ -6,8 +6,6 @@ import pkgutil from os import linesep -from .find_deprecated import FindDeprecated # noqa: F401 - log = logging.getLogger(__name__) diff --git a/openfisca_make/publish.mk b/openfisca_make/publish.mk deleted file mode 100644 index 23014dc15c..0000000000 --- a/openfisca_make/publish.mk +++ /dev/null @@ -1,4 +0,0 @@ -## Check for features marked as deprecated. -check-deprecated: - @$(call help,$@:) - @python openfisca_core/scripts/find_deprecated.py diff --git a/openfisca_tasks/__init__.py b/openfisca_tasks/__init__.py new file mode 100644 index 0000000000..f203d211dc --- /dev/null +++ b/openfisca_tasks/__init__.py @@ -0,0 +1 @@ +from ._check_deprecated import CheckDeprecated # noqa: F401 diff --git a/openfisca_tasks/__main__.py b/openfisca_tasks/__main__.py new file mode 100644 index 0000000000..5f1075a634 --- /dev/null +++ b/openfisca_tasks/__main__.py @@ -0,0 +1,8 @@ +import sys + +import openfisca_tasks as tasks + +if __name__ == "__main__": + task = tasks.__getattribute__(sys.argv[1])() + task() + sys.exit(task.exit) diff --git a/openfisca_core/scripts/find_deprecated.py b/openfisca_tasks/_check_deprecated.py similarity index 93% rename from openfisca_core/scripts/find_deprecated.py rename to openfisca_tasks/_check_deprecated.py index 393223f2fe..08187669b6 100644 --- a/openfisca_core/scripts/find_deprecated.py +++ b/openfisca_tasks/_check_deprecated.py @@ -3,7 +3,6 @@ import pathlib import pkg_resources import subprocess -import sys import textwrap from typing import Sequence @@ -19,7 +18,7 @@ VERSION = pkg_resources.get_distribution("openfisca_core").version -class FindDeprecated(ast.NodeVisitor): +class CheckDeprecated(ast.NodeVisitor): count: int exit: int = os.EX_OK @@ -81,9 +80,3 @@ def _node(self, file: str) -> ast.Module: with open(file, "r") as f: source = textwrap.dedent(f.read()) return ast.parse(source, file, "exec") - - -if __name__ == "__main__": - find = FindDeprecated() - find() - sys.exit(find.exit) diff --git a/openfisca_tasks/publish.mk b/openfisca_tasks/publish.mk index 2f34599fd9..b6d5016a6c 100644 --- a/openfisca_tasks/publish.mk +++ b/openfisca_tasks/publish.mk @@ -5,3 +5,9 @@ build: setup.py @$(call print_help,$@:) @python $? bdist_wheel @find dist -name "*.whl" -exec pip install --force-reinstall {}[dev] \; + +## Check for features marked as deprecated. +check-deprecated: + @$(call print_help,$@:) + @python -m openfisca_tasks CheckDeprecated + @$(call print_pass,$@:) diff --git a/openfisca_core/scripts/tests/test_find_deprecated.py b/openfisca_tasks/tests/test_check_deprecated.py similarity index 83% rename from openfisca_core/scripts/tests/test_find_deprecated.py rename to openfisca_tasks/tests/test_check_deprecated.py index efb2789bf9..cf77c0bf72 100644 --- a/openfisca_core/scripts/tests/test_find_deprecated.py +++ b/openfisca_tasks/tests/test_check_deprecated.py @@ -4,7 +4,7 @@ import pytest -from openfisca_core.scripts import FindDeprecated +from openfisca_tasks import CheckDeprecated class Module: @@ -35,11 +35,11 @@ def test_find_deprecated(capsys): """Prints out the features marked as deprecated.""" with Module() as (file, name): - find = FindDeprecated([file.name]) - find() + checker = CheckDeprecated([file.name]) + checker() with pytest.raises(SystemExit) as exit: - sys.exit(find.exit) + sys.exit(checker.exit) assert exit.value.code == os.EX_OK assert f"[{name}.function:5]" in capsys.readouterr().out @@ -51,10 +51,10 @@ def test_find_deprecated_when_expired(capsys): version = "1.0.0" with Module(version) as (file, _): - find = FindDeprecated([file.name], version) - find() + checker = CheckDeprecated([file.name], version) + checker() with pytest.raises(SystemExit) as exit: - sys.exit(find.exit) + sys.exit(checker.exit) assert exit.value.code != os.EX_OK diff --git a/setup.cfg b/setup.cfg index 3fb18ecb66..f3dc4d9fc7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ rst-directives = attribute, deprecated, versionadded [tool:pytest] addopts = --showlocals --doctest-modules --disable-pytest-warnings -testpaths = tests +testpaths = openfisca_tasks tests python_files = **/*.py [mypy] From 049f2e883bc066d8e8a418ed29e2523b0dd48893 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 16:21:34 +0200 Subject: [PATCH 11/24] Deprecate Dummy --- openfisca_core/commons/dummy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 7cd1951931..1b8e979d12 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -14,6 +14,6 @@ class Dummy: """ - @deprecated(since = "34.7.0", expires = "in the future") + @deprecated(since = "34.7.0", expires = "36.0.0") def __init__(self) -> None: ... From 9651bfa4963ba7c7b323dc3f729864f5ae2b38bb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 16:24:24 +0200 Subject: [PATCH 12/24] Add the attr rst directive --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f3dc4d9fc7..4c789e1123 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ hang-closing = true ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true -rst-roles = any, class, exc, meth, obj +rst-roles = any, attr, class, exc, meth, obj rst-directives = attribute, deprecated, versionadded [tool:pytest] From a39b8c7099bae36e679887782479eacc735a2976 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 16:31:23 +0200 Subject: [PATCH 13/24] Do not use print --- openfisca_tasks/_check_deprecated.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openfisca_tasks/_check_deprecated.py b/openfisca_tasks/_check_deprecated.py index 08187669b6..81f1b1197b 100644 --- a/openfisca_tasks/_check_deprecated.py +++ b/openfisca_tasks/_check_deprecated.py @@ -3,6 +3,7 @@ import pathlib import pkg_resources import subprocess +import sys import textwrap from typing import Sequence @@ -15,7 +16,10 @@ .split() VERSION: str -VERSION = pkg_resources.get_distribution("openfisca_core").version +VERSION = \ + pkg_resources \ + .get_distribution("openfisca_core") \ + .version class CheckDeprecated(ast.NodeVisitor): @@ -69,7 +73,7 @@ def visit_FunctionDef(self, node) -> None: f"(current: {self.version}).", ] - print(" ".join(message)) + sys.stdout.write(f"{' '.join(message)}\n") def _isthis(self, version: str) -> bool: return self.version == version From 42444f8dec338473f7ee690bee33838a5e8d159f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 16:36:45 +0200 Subject: [PATCH 14/24] Add typing_extensions --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 396435263d..88c8409915 100644 --- a/setup.py +++ b/setup.py @@ -28,10 +28,11 @@ 'flake8-bugbear >= 19.3.0, < 20.0.0', 'flake8-print >= 3.1.0, < 4.0.0', 'flake8-rst-docstrings < 1.0.0', - 'pytest-cov >= 2.6.1, < 3.0.0', 'mypy >= 0.701, < 0.800', 'openfisca-country-template >= 3.10.0, < 4.0.0', - 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0' + 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', + 'pytest-cov >= 2.6.1, < 3.0.0', + 'typing-extensions == 3.10.0.2', ] + api_requirements setup( From dbbc29fd81f337a43771038644085d5700b4605b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 16:43:07 +0200 Subject: [PATCH 15/24] Add the task protocol --- openfisca_tasks/__main__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openfisca_tasks/__main__.py b/openfisca_tasks/__main__.py index 5f1075a634..0df94de173 100644 --- a/openfisca_tasks/__main__.py +++ b/openfisca_tasks/__main__.py @@ -1,8 +1,21 @@ +import abc import sys +from typing_extensions import Protocol + import openfisca_tasks as tasks + +class HasExit(Protocol): + exit: int + + @abc.abstractmethod + def __call__(self) -> None: + ... + + if __name__ == "__main__": + task: HasExit task = tasks.__getattribute__(sys.argv[1])() task() sys.exit(task.exit) From 345cedcefa970845777f25ac965c2933fbefd137 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 17:31:31 +0200 Subject: [PATCH 16/24] Type-check the deprecation check --- openfisca_tasks/_check_deprecated.py | 29 ++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/openfisca_tasks/_check_deprecated.py b/openfisca_tasks/_check_deprecated.py index 81f1b1197b..6184865fa0 100644 --- a/openfisca_tasks/_check_deprecated.py +++ b/openfisca_tasks/_check_deprecated.py @@ -45,31 +45,40 @@ def __call__(self) -> None: self.count = count self.visit(node) - def visit_FunctionDef(self, node) -> None: - expires: ast.Str + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + keywords: Sequence[str] file: str + path: pathlib.Path + module: str lineno: int + since: str + expires: str message: Sequence[str] - module: str - path: pathlib.Path - since: ast.Str for decorator in node.decorator_list: + if not isinstance(decorator, ast.Call): + break + if "deprecated" in ast.dump(decorator): + keywords = [ + kwd.value.s + for kwd in decorator.keywords + if isinstance(kwd.value, ast.Str) + ] + file = self.files[self.count] path = pathlib.Path(file).resolve() module = f"{path.parts[-2]}.{path.stem}" lineno = node.lineno + 1 - since = decorator.keywords[0].value - expires = decorator.keywords[1].value + since, expires = keywords - if self._isthis(expires.s): + if self._isthis(expires): self.exit = 1 message = [ f"[{module}.{node.name}:{lineno}]", - f"Deprecated since: {since.s}.", - f"Expiration status: {expires.s}", + f"Deprecated since: {since}.", + f"Expiration status: {expires}", f"(current: {self.version}).", ] From 09a636f96c4cba7f1b0c2fe94bd6614ce3e3f4e2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 18:03:52 +0200 Subject: [PATCH 17/24] Document the deprecation check --- openfisca_core/commons/decorators.py | 2 +- openfisca_core/commons/dummy.py | 3 +- openfisca_tasks/_check_deprecated.py | 79 ++++++++++++++++++++++++++-- setup.cfg | 2 +- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/openfisca_core/commons/decorators.py b/openfisca_core/commons/decorators.py index 91b317ceb6..b4e7891dbe 100644 --- a/openfisca_core/commons/decorators.py +++ b/openfisca_core/commons/decorators.py @@ -39,7 +39,7 @@ class deprecated: >>> str(obsolete) '' - .. versionadded:: 35.6.0 + .. versionadded:: 36.0.0 """ diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 1b8e979d12..732ed49a65 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -9,8 +9,7 @@ class Dummy: None: - self.files = files self.nodes = [self._node(file) for file in self.files] self.version = version def __call__(self) -> None: + # We use ``count`` to link each ``node`` with the corresponding + # ``file``. for count, node in enumerate(self.nodes): self.count = count self.visit(node) def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + """Defines the ``visit()`` function to inspect the ``node``. + + Args: + node: The :mod:`ast` node to inspect. + + Attributes: + keywords: + The decorator's keywords, see :mod:`.decorators`. + file: + The path of a file containing a module. + path: + The resolved ``file`` path. + module: + The name of the module. + lineno: + The line number of each ``node``. + since: + The ``since`` keyword's value. + expires: + The ``expires`` keyword's value. + message: + The message we will print to the user. + + .. versionadded:: 36.0.0 + + """ + keywords: Sequence[str] file: str path: pathlib.Path @@ -55,23 +108,39 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: expires: str message: Sequence[str] + # We look for the corresponding ``file``. + file = self.files[self.count] + + # We find the absolute path of the the file. + path = pathlib.Path(file).resolve() + + # We build the module name with the name of the parent path, a + # folder, and the name of the file, without the extension. + module = f"{path.parts[-2]}.{path.stem}" + + # We assume the function is defined just one line after the + # decorator. + lineno = node.lineno + 1 + for decorator in node.decorator_list: + # We cast the ``decorator`` to ``callable``. if not isinstance(decorator, ast.Call): break + # We only print out the deprecated functions. if "deprecated" in ast.dump(decorator): + # We cast each keyword to ``str``. keywords = [ kwd.value.s for kwd in decorator.keywords if isinstance(kwd.value, ast.Str) ] - file = self.files[self.count] - path = pathlib.Path(file).resolve() - module = f"{path.parts[-2]}.{path.stem}" - lineno = node.lineno + 1 + # Finally we assign each keyword to a variable. since, expires = keywords + # If there is at least one expired deprecation, the handler + # will exit with an error. if self._isthis(expires): self.exit = 1 diff --git a/setup.cfg b/setup.cfg index 4c789e1123..1e3f3e6621 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ hang-closing = true ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true -rst-roles = any, attr, class, exc, meth, obj +rst-roles = any, attr, class, exc, meth, mod, obj rst-directives = attribute, deprecated, versionadded [tool:pytest] From 5c633108b5c82aef27fd1660ffc8ee595e5629b8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 18:08:25 +0200 Subject: [PATCH 18/24] Add task to circleci --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index ea7148b39b..63f27ef112 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -108,6 +108,10 @@ jobs: git fetch .circleci/is-version-number-acceptable.sh + - run: + name: Check expired deprecations + command: make check-deprecated + submit_coverage: docker: - image: python:3.7 From c96d04ddf8fca9291e061a65fac57dc701028dd1 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 18:45:22 +0200 Subject: [PATCH 19/24] Use literals for exit codes --- openfisca_tasks/_check_deprecated.py | 66 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/openfisca_tasks/_check_deprecated.py b/openfisca_tasks/_check_deprecated.py index 189d7a881d..d33a940215 100644 --- a/openfisca_tasks/_check_deprecated.py +++ b/openfisca_tasks/_check_deprecated.py @@ -1,5 +1,4 @@ import ast -import os import pathlib import pkg_resources import subprocess @@ -7,6 +6,11 @@ import textwrap from typing import Sequence +from typing_extensions import Literal + +EXIT_OK: Literal[0] = 0 +EXIT_KO: Literal[1] = 1 + FILES: Sequence[str] FILES = \ subprocess \ @@ -50,7 +54,7 @@ class CheckDeprecated(ast.NodeVisitor): """ count: int - exit: int = os.EX_OK + exit: Literal[0, 1] = EXIT_OK files: Sequence[str] nodes: Sequence[ast.Module] version: str @@ -111,7 +115,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # We look for the corresponding ``file``. file = self.files[self.count] - # We find the absolute path of the the file. + # We find the absolute path of the file. path = pathlib.Path(file).resolve() # We build the module name with the name of the parent path, a @@ -125,33 +129,39 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: for decorator in node.decorator_list: # We cast the ``decorator`` to ``callable``. if not isinstance(decorator, ast.Call): - break + continue # We only print out the deprecated functions. - if "deprecated" in ast.dump(decorator): - # We cast each keyword to ``str``. - keywords = [ - kwd.value.s - for kwd in decorator.keywords - if isinstance(kwd.value, ast.Str) - ] - - # Finally we assign each keyword to a variable. - since, expires = keywords - - # If there is at least one expired deprecation, the handler - # will exit with an error. - if self._isthis(expires): - self.exit = 1 - - message = [ - f"[{module}.{node.name}:{lineno}]", - f"Deprecated since: {since}.", - f"Expiration status: {expires}", - f"(current: {self.version}).", - ] - - sys.stdout.write(f"{' '.join(message)}\n") + if "deprecated" not in ast.dump(decorator): + continue + + # We cast each keyword to ``str``. + keywords = [ + kwd.value.s + for kwd in decorator.keywords + if isinstance(kwd.value, ast.Str) + ] + + # Finally we assign each keyword to a variable. + since, expires = keywords + + message = [ + f"[{module}.{node.name}:{lineno}]", + f"Deprecated since: {since}.", + f"Expiration status: {expires}", + f"(current: {self.version}).", + ] + + sys.stdout.write(f"{' '.join(message)}\n") + + # If the exit code has already been modified, we wont set it again. + if self.exit == EXIT_KO: + continue + + # If there is at least one expired deprecation, the handler + # will exit with an error. + if self._isthis(expires): + self.exit = EXIT_KO def _isthis(self, version: str) -> bool: return self.version == version From f7a1c7f2a5ca107224b794285eb9efa9a74a0f0e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 20:48:42 +0200 Subject: [PATCH 20/24] Add progress bar --- openfisca_tasks/__main__.py | 12 +++++ openfisca_tasks/_check_deprecated.py | 44 +++++++++++++++---- .../tests/test_check_deprecated.py | 3 +- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/openfisca_tasks/__main__.py b/openfisca_tasks/__main__.py index 0df94de173..31f291ebe0 100644 --- a/openfisca_tasks/__main__.py +++ b/openfisca_tasks/__main__.py @@ -13,6 +13,18 @@ class HasExit(Protocol): def __call__(self) -> None: ... + @abc.abstractmethod + def __init_progress__(self) -> None: + ... + + @abc.abstractmethod + def __push_progress__(self) -> None: + ... + + @abc.abstractmethod + def __wipe_progress__(self) -> None: + ... + if __name__ == "__main__": task: HasExit diff --git a/openfisca_tasks/_check_deprecated.py b/openfisca_tasks/_check_deprecated.py index d33a940215..8460e7b233 100644 --- a/openfisca_tasks/_check_deprecated.py +++ b/openfisca_tasks/_check_deprecated.py @@ -8,8 +8,11 @@ from typing_extensions import Literal -EXIT_OK: Literal[0] = 0 -EXIT_KO: Literal[1] = 1 +EXIT_OK: Literal[0] +EXIT_OK = 0 + +EXIT_KO: Literal[1] +EXIT_KO = 1 FILES: Sequence[str] FILES = \ @@ -67,6 +70,7 @@ def __init__( self.files = files self.nodes = [self._node(file) for file in self.files] self.version = version + self.__init_progress__() def __call__(self) -> None: # We use ``count`` to link each ``node`` with the corresponding @@ -74,6 +78,9 @@ def __call__(self) -> None: for count, node in enumerate(self.nodes): self.count = count self.visit(node) + self.__push_progress__() + + self.__wipe_progress__() def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """Defines the ``visit()`` function to inspect the ``node``. @@ -146,22 +153,21 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: since, expires = keywords message = [ - f"[{module}.{node.name}:{lineno}]", + f"[i] {module}.{node.name}:{lineno} =>", f"Deprecated since: {since}.", f"Expiration status: {expires}", f"(current: {self.version}).", ] - sys.stdout.write(f"{' '.join(message)}\n") - - # If the exit code has already been modified, we wont set it again. - if self.exit == EXIT_KO: - continue + sys.stdout.write(f"{' '.join(message)}") # If there is at least one expired deprecation, the handler # will exit with an error. if self._isthis(expires): self.exit = EXIT_KO + sys.stdout.write("\r[!]") + + sys.stdout.write("\n") def _isthis(self, version: str) -> bool: return self.version == version @@ -172,3 +178,25 @@ def _node(self, file: str) -> ast.Module: with open(file, "r") as f: source = textwrap.dedent(f.read()) return ast.parse(source, file, "exec") + + def __init_progress__(self) -> None: + sys.stdout.write(f"[/] 0% |{'·' * 50}|\r") + + def __push_progress__(self) -> None: + doner: int + space: str + + doner = (self.count + 1) * 100 // len(self.nodes) + space = "" + space += [' ', ''][doner >= 100] + space += [' ', ''][doner >= 10] + + sys.stdout.write( + f"[/] {doner}% {space}|" + f"{'█' * (doner // 2)}" + f"{'·' * (50 - doner // 2)}" + "|\r" + ) + + def __wipe_progress__(self) -> None: + sys.stdout.write(f"{' ' * 100}\r") diff --git a/openfisca_tasks/tests/test_check_deprecated.py b/openfisca_tasks/tests/test_check_deprecated.py index cf77c0bf72..9c004890f2 100644 --- a/openfisca_tasks/tests/test_check_deprecated.py +++ b/openfisca_tasks/tests/test_check_deprecated.py @@ -42,7 +42,7 @@ def test_find_deprecated(capsys): sys.exit(checker.exit) assert exit.value.code == os.EX_OK - assert f"[{name}.function:5]" in capsys.readouterr().out + assert f"[i] {name}.function:5" in capsys.readouterr().out def test_find_deprecated_when_expired(capsys): @@ -58,3 +58,4 @@ def test_find_deprecated_when_expired(capsys): sys.exit(checker.exit) assert exit.value.code != os.EX_OK + assert "[!]" in capsys.readouterr().out From 84d3d5a6141bcf35242d3a31ac67c9fe4887eb14 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 30 Sep 2021 19:22:13 +0200 Subject: [PATCH 21/24] Colorise --- openfisca_tasks/_check_deprecated.py | 36 ++++++++++++++++++++++------ setup.py | 1 + 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/openfisca_tasks/_check_deprecated.py b/openfisca_tasks/_check_deprecated.py index 8460e7b233..9ff1dc865b 100644 --- a/openfisca_tasks/_check_deprecated.py +++ b/openfisca_tasks/_check_deprecated.py @@ -6,6 +6,7 @@ import textwrap from typing import Sequence +import termcolor from typing_extensions import Literal EXIT_OK: Literal[0] @@ -14,6 +15,24 @@ EXIT_KO: Literal[1] EXIT_KO = 1 +WORK: str +WORK = termcolor.colored("[/]", "cyan") + +WARN: str +WARN = termcolor.colored("[i]", "yellow") + +FAIL: str +FAIL = termcolor.colored("[!]", "red") + +BAR: str +BAR = termcolor.colored("|", "green") + +ETA: str +ETA = termcolor.colored("✓", "green") + +ACC: str +ACC = termcolor.colored("·", "green") + FILES: Sequence[str] FILES = \ subprocess \ @@ -153,7 +172,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: since, expires = keywords message = [ - f"[i] {module}.{node.name}:{lineno} =>", + f"{WARN} {module}.{node.name}:{lineno} =>", f"Deprecated since: {since}.", f"Expiration status: {expires}", f"(current: {self.version}).", @@ -165,7 +184,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # will exit with an error. if self._isthis(expires): self.exit = EXIT_KO - sys.stdout.write("\r[!]") + sys.stdout.write(f"\r{FAIL}") sys.stdout.write("\n") @@ -180,7 +199,7 @@ def _node(self, file: str) -> ast.Module: return ast.parse(source, file, "exec") def __init_progress__(self) -> None: - sys.stdout.write(f"[/] 0% |{'·' * 50}|\r") + sys.stdout.write(f"{WORK} 0% {BAR}{ACC * 50}{BAR}\r") def __push_progress__(self) -> None: doner: int @@ -192,11 +211,14 @@ def __push_progress__(self) -> None: space += [' ', ''][doner >= 10] sys.stdout.write( - f"[/] {doner}% {space}|" - f"{'█' * (doner // 2)}" - f"{'·' * (50 - doner // 2)}" - "|\r" + f"{WORK} {doner}% {space}{BAR}" + f"{ETA * (doner // 2)}" + f"{ACC * (50 - doner // 2)}" + f"{BAR}\r" ) + import time + time.sleep(.1) + def __wipe_progress__(self) -> None: sys.stdout.write(f"{' ' * 100}\r") diff --git a/setup.py b/setup.py index 88c8409915..e12bba3c03 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ 'openfisca-country-template >= 3.10.0, < 4.0.0', 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', 'pytest-cov >= 2.6.1, < 3.0.0', + 'termcolor == 1.1.0', 'typing-extensions == 3.10.0.2', ] + api_requirements From 16dde3ca290ecbabbe042a0ffcc156b62ca6b248 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 1 Oct 2021 12:28:36 +0200 Subject: [PATCH 22/24] Split check/progress --- openfisca_tasks/__main__.py | 32 ++++--------- openfisca_tasks/_check_deprecated.py | 68 +++++++------------------- openfisca_tasks/_progress_bar.py | 71 ++++++++++++++++++++++++++++ openfisca_tasks/_protocols.py | 36 ++++++++++++++ 4 files changed, 132 insertions(+), 75 deletions(-) create mode 100644 openfisca_tasks/_progress_bar.py create mode 100644 openfisca_tasks/_protocols.py diff --git a/openfisca_tasks/__main__.py b/openfisca_tasks/__main__.py index 31f291ebe0..87d7879a22 100644 --- a/openfisca_tasks/__main__.py +++ b/openfisca_tasks/__main__.py @@ -1,33 +1,19 @@ -import abc -import sys +from __future__ import annotations -from typing_extensions import Protocol +import sys import openfisca_tasks as tasks - -class HasExit(Protocol): - exit: int - - @abc.abstractmethod - def __call__(self) -> None: - ... - - @abc.abstractmethod - def __init_progress__(self) -> None: - ... - - @abc.abstractmethod - def __push_progress__(self) -> None: - ... - - @abc.abstractmethod - def __wipe_progress__(self) -> None: - ... +from ._progress_bar import ProgressBar +from ._protocols import HasExit, SupportsProgress if __name__ == "__main__": task: HasExit task = tasks.__getattribute__(sys.argv[1])() - task() + + progress: SupportsProgress + progress = ProgressBar() + + task(progress) sys.exit(task.exit) diff --git a/openfisca_tasks/_check_deprecated.py b/openfisca_tasks/_check_deprecated.py index 9ff1dc865b..5e31edc711 100644 --- a/openfisca_tasks/_check_deprecated.py +++ b/openfisca_tasks/_check_deprecated.py @@ -6,33 +6,16 @@ import textwrap from typing import Sequence -import termcolor from typing_extensions import Literal +from ._protocols import SupportsProgress + EXIT_OK: Literal[0] EXIT_OK = 0 EXIT_KO: Literal[1] EXIT_KO = 1 -WORK: str -WORK = termcolor.colored("[/]", "cyan") - -WARN: str -WARN = termcolor.colored("[i]", "yellow") - -FAIL: str -FAIL = termcolor.colored("[!]", "red") - -BAR: str -BAR = termcolor.colored("|", "green") - -ETA: str -ETA = termcolor.colored("✓", "green") - -ACC: str -ACC = termcolor.colored("·", "green") - FILES: Sequence[str] FILES = \ subprocess \ @@ -76,9 +59,11 @@ class CheckDeprecated(ast.NodeVisitor): """ count: int - exit: Literal[0, 1] = EXIT_OK + exit: Literal[0, 1] files: Sequence[str] nodes: Sequence[ast.Module] + progress: SupportsProgress + total: int version: str def __init__( @@ -86,20 +71,24 @@ def __init__( files: Sequence[str] = FILES, version: str = VERSION, ) -> None: + self.exit = EXIT_OK self.files = files self.nodes = [self._node(file) for file in self.files] + self.total = len(self.nodes) self.version = version - self.__init_progress__() - def __call__(self) -> None: + def __call__(self, progress: SupportsProgress) -> None: + self.progress = progress + self.progress.init() + # We use ``count`` to link each ``node`` with the corresponding # ``file``. for count, node in enumerate(self.nodes): self.count = count self.visit(node) - self.__push_progress__() + self.progress.push(self.count, self.total) - self.__wipe_progress__() + self.progress.wipe() def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """Defines the ``visit()`` function to inspect the ``node``. @@ -172,19 +161,19 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: since, expires = keywords message = [ - f"{WARN} {module}.{node.name}:{lineno} =>", + f"{module}.{node.name}:{lineno} =>", f"Deprecated since: {since}.", f"Expiration status: {expires}", f"(current: {self.version}).", ] - sys.stdout.write(f"{' '.join(message)}") + self.progress.warn(f"{' '.join(message)}") # If there is at least one expired deprecation, the handler # will exit with an error. if self._isthis(expires): self.exit = EXIT_KO - sys.stdout.write(f"\r{FAIL}") + self.progress.fail() sys.stdout.write("\n") @@ -197,28 +186,3 @@ def _node(self, file: str) -> ast.Module: with open(file, "r") as f: source = textwrap.dedent(f.read()) return ast.parse(source, file, "exec") - - def __init_progress__(self) -> None: - sys.stdout.write(f"{WORK} 0% {BAR}{ACC * 50}{BAR}\r") - - def __push_progress__(self) -> None: - doner: int - space: str - - doner = (self.count + 1) * 100 // len(self.nodes) - space = "" - space += [' ', ''][doner >= 100] - space += [' ', ''][doner >= 10] - - sys.stdout.write( - f"{WORK} {doner}% {space}{BAR}" - f"{ETA * (doner // 2)}" - f"{ACC * (50 - doner // 2)}" - f"{BAR}\r" - ) - - import time - time.sleep(.1) - - def __wipe_progress__(self) -> None: - sys.stdout.write(f"{' ' * 100}\r") diff --git a/openfisca_tasks/_progress_bar.py b/openfisca_tasks/_progress_bar.py new file mode 100644 index 0000000000..43cb55c45c --- /dev/null +++ b/openfisca_tasks/_progress_bar.py @@ -0,0 +1,71 @@ +from typing import Sequence + +import sys +import termcolor + + +WORK_ICON: str +WORK_ICON = termcolor.colored("[/]", "cyan") + +WARN_ICON: str +WARN_ICON = termcolor.colored("[i]", "yellow") + +FAIL_ICON: str +FAIL_ICON = termcolor.colored("[!]", "red") + +BAR_ICON: str +BAR_ICON = termcolor.colored("|", "green") + +ACC_ICON: str +ACC_ICON = termcolor.colored("✓", "green") + +ETA_ICON: str +ETA_ICON = termcolor.colored("·", "green") + +BAR_SIZE: int +BAR_SIZE = 50 + + +class ProgressBar: + + def init(self) -> None: + sys.stdout.write(self._init_message()) + + def push(self, count: int, total: int) -> None: + done: int + + done = (count + 1) * 100 // total + + sys.stdout.write(self._push_message(done)) + + def warn(self, message: str) -> None: + sys.stdout.write(f"{WARN_ICON} {message}") + + def fail(self) -> None: + sys.stdout.write(f"\r{FAIL_ICON}") + + def wipe(self) -> None: + sys.stdout.write(self._wipe_message()) + + def _init_message(self) -> str: + return f"{WORK_ICON} 0% {BAR_ICON}{ETA_ICON * BAR_SIZE}{BAR_ICON}\r" + + def _push_message(self, done: int) -> str: + message: Sequence[str] + spaces: str + + spaces = "" + spaces += [' ', ''][done >= BAR_SIZE * 2] + spaces += [' ', ''][done >= BAR_SIZE // 5] + + message = [ + f"{WORK_ICON} {done}% {spaces}{BAR_ICON}" + f"{ACC_ICON * (done // 2)}" + f"{ETA_ICON * (BAR_SIZE - done // 2)}" + f"{BAR_ICON}\r" + ] + + return "".join(message) + + def _wipe_message(self) -> str: + return f"{' ' * BAR_SIZE * 2}\r" diff --git a/openfisca_tasks/_protocols.py b/openfisca_tasks/_protocols.py new file mode 100644 index 0000000000..8b307e2550 --- /dev/null +++ b/openfisca_tasks/_protocols.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import abc + +from typing_extensions import Protocol + + +class HasExit(Protocol): + exit: int + + @abc.abstractmethod + def __call__(self, __progress: SupportsProgress) -> None: + ... + + +class SupportsProgress(Protocol): + + @abc.abstractmethod + def init(self) -> None: + ... + + @abc.abstractmethod + def push(self, __count: int, __total: int) -> None: + ... + + @abc.abstractmethod + def warn(self, __message: str) -> None: + ... + + @abc.abstractmethod + def fail(self) -> None: + ... + + @abc.abstractmethod + def wipe(self) -> None: + ... From 6f44c2988fc7736bf13718549d6b5890428296bf Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 1 Oct 2021 13:13:04 +0200 Subject: [PATCH 23/24] Fix check deprecated test --- openfisca_tasks/tests/__init__.py | 0 .../tests/test_check_deprecated.py | 47 ++++++++++++++++--- setup.cfg | 3 ++ 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 openfisca_tasks/tests/__init__.py diff --git a/openfisca_tasks/tests/__init__.py b/openfisca_tasks/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_tasks/tests/test_check_deprecated.py b/openfisca_tasks/tests/test_check_deprecated.py index 9c004890f2..5395b4ab6c 100644 --- a/openfisca_tasks/tests/test_check_deprecated.py +++ b/openfisca_tasks/tests/test_check_deprecated.py @@ -1,11 +1,15 @@ +import inspect import os import sys import tempfile +from typing import NamedTuple import pytest from openfisca_tasks import CheckDeprecated +from .._protocols import SupportsProgress + class Module: """Some module with an expired function.""" @@ -31,31 +35,62 @@ def __exit__(self, *__): self.file.close() -def test_find_deprecated(capsys): +@pytest.fixture +def progress(): + + def name(): + return inspect.stack()[1][3] + + class Call(NamedTuple): + name: str + args: str = None + + class ProgressBar(SupportsProgress): + + def init(self): + self.called = [] + + def push(self, __count, __total): + ... + + def warn(self, message): + self.called.append(Call(name(), message)) + + def fail(self): + self.called.append(Call(name())) + + def wipe(self): + ... + + return ProgressBar() + + +def test_find_deprecated(progress): """Prints out the features marked as deprecated.""" with Module() as (file, name): checker = CheckDeprecated([file.name]) - checker() + checker(progress) with pytest.raises(SystemExit) as exit: sys.exit(checker.exit) assert exit.value.code == os.EX_OK - assert f"[i] {name}.function:5" in capsys.readouterr().out + assert progress.called[-1].name == "warn" + assert f"{name}.function:5" in progress.called[-1].args -def test_find_deprecated_when_expired(capsys): +def test_find_deprecated_when_expired(progress): """Raises an error when at least one deprecation has expired.""" version = "1.0.0" with Module(version) as (file, _): checker = CheckDeprecated([file.name], version) - checker() + checker(progress) with pytest.raises(SystemExit) as exit: sys.exit(checker.exit) assert exit.value.code != os.EX_OK - assert "[!]" in capsys.readouterr().out + assert progress.called[-1].name == "fail" diff --git a/setup.cfg b/setup.cfg index 1e3f3e6621..0698e4f6c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,5 +19,8 @@ python_files = **/*.py [mypy] ignore_missing_imports = True +[mypy-openfisca_tasks.tests.*] +ignore_errors = True + [mypy-openfisca_core.scripts.*] ignore_errors = True From 3b4c7d0d6ea1718d7abdc5c3733cf8694ac775b4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 25 Sep 2021 18:12:43 +0200 Subject: [PATCH 24/24] Bump major to 36.0.0 --- CHANGELOG.md | 19 +++++++++++++++++++ setup.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec66f40e3c..0454cb6d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +# 36.0.0 [#1044](https://github.com/openfisca/openfisca-core/pull/1044) + +Follows discussion on #1033 + +#### New features + +- Introduce `make check-deprecated` + - Allows for listing the features marked as deprecated. + +Example: + +``` +$ make check-deprecated + +[⚙] Check for features marked as deprecated... +[!] commons.dummy.__init__:17 => Deprecated since: 34.7.0. Expiration status: 36.0.0 (current: 36.0.0). +[/] 18% |█████████·········································| +``` + ### 35.5.1 [#1046](https://github.com/openfisca/openfisca-core/pull/1046) #### Non-technical changes diff --git a/setup.py b/setup.py index e12bba3c03..9ff7670317 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ setup( name = 'OpenFisca-Core', - version = '35.5.1', + version = '36.0.0', author = 'OpenFisca Team', author_email = 'contact@openfisca.org', classifiers = [