Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

updates and improvements in tools/find_deprecated #13593

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 87 additions & 23 deletions tools/find_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""List deprecated decorators."""
from __future__ import annotations
from typing import cast, Optional
from re import findall
from pathlib import Path
from collections import OrderedDict, defaultdict
import ast
Expand Down Expand Up @@ -50,15 +51,15 @@ class DeprecationDecorator(Deprecation):

Args:
filename: where is the deprecation.
decorator_node: AST node of the decorator call.
deprecation_node: AST node of the decorator call.
func_node: AST node of the decorated call.
"""

def __init__(
self, filename: Path, decorator_node: ast.Call, func_node: ast.FunctionDef
self, filename: Path, deprecation_node: ast.Call, func_node: ast.FunctionDef
) -> None:
self.filename = filename
self.decorator_node = decorator_node
self.deprecation_node = deprecation_node
self.func_node = func_node
self._since: str | None = None
self._pending: bool | None = None
Expand All @@ -67,9 +68,11 @@ def __init__(
def since(self) -> str | None:
"""Version since the deprecation applies."""
if not self._since:
for kwarg in self.decorator_node.keywords:
for kwarg in self.deprecation_node.keywords:
if kwarg.arg == "since":
self._since = ".".join(cast(ast.Constant, kwarg.value).value.split(".")[:2])
self._since = ".".join(
str(cast(ast.Constant, kwarg.value).value).split(".")[:2]
)
return self._since

@property
Expand All @@ -79,7 +82,7 @@ def pending(self) -> bool | None:
self._pending = next(
(
kwarg.value.value
for kwarg in self.decorator_node.keywords
for kwarg in self.deprecation_node.keywords
if kwarg.arg == "pending"
),
False,
Expand All @@ -89,7 +92,7 @@ def pending(self) -> bool | None:
@property
def lineno(self) -> int:
"""Line number of the decorator."""
return self.decorator_node.lineno
return self.deprecation_node.lineno

@property
def target(self) -> str:
Expand All @@ -107,16 +110,17 @@ class DeprecationCall(Deprecation):

def __init__(self, filename: Path, decorator_call: ast.Call) -> None:
self.filename = filename
self.decorator_node = decorator_call
self.deprecation_node = decorator_call
self.lineno = decorator_call.lineno
self.pending: bool | None = None
self._target: str | None = None
self._since: str | None = None

@property
def target(self) -> str | None:
"""what's deprecated."""
if not self._target:
arg = self.decorator_node.args.__getitem__(0)
arg = self.deprecation_node.args.__getitem__(0)
if isinstance(arg, ast.Attribute):
self._target = f"{arg.value.id}.{arg.attr}"
if isinstance(arg, ast.Name):
Expand All @@ -127,12 +131,43 @@ def target(self) -> str | None:
def since(self) -> str | None:
"""Version since the deprecation applies."""
if not self._since:
for kwarg in self.decorator_node.func.keywords:
for kwarg in self.deprecation_node.func.keywords:
if kwarg.arg == "since":
self._since = ".".join(cast(ast.Constant, kwarg.value).value.split(".")[:2])
return self._since


class DeprecationWarn(DeprecationDecorator):
"""
Deprecation via manual warning

Args:
filename: where is the deprecation.
deprecation_node: AST node of the decorator call.
func_node: AST node of the decorated call.
"""

@property
def since(self) -> str | None:
if not self._since:
candidates = []
for arg in self.deprecation_node.args:
if isinstance(arg, ast.Constant):
candidates += [v.strip(".") for v in findall(r"\s+([\d.]+)", arg.value)]
self._since = (min(candidates, default=0) or "n/a") + "?"
return self._since

@property
def pending(self) -> bool | None:
"""If it is a pending deprecation."""
if self._pending is None:
self._pending = False
for arg in self.deprecation_node.args:
if hasattr(arg, "id") and arg.id == "PendingDeprecationWarning":
self._pending = True
return self._pending


class DecoratorVisitor(ast.NodeVisitor):
"""
Node visitor for finding deprecation decorator
Expand All @@ -153,6 +188,19 @@ def is_deprecation_decorator(node: ast.expr) -> bool:
and node.func.id.startswith("deprecate_")
)

@staticmethod
def is_deprecation_warning(node: ast.expr) -> bool:
"""Check if a node is a deprecation warning"""
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "warn"
):
for arg in node.args:
if hasattr(arg, "id") and "DeprecationWarning" in arg.id:
return True
return False

@staticmethod
def is_deprecation_call(node: ast.expr) -> bool:
"""Check if a node is a deprecation call"""
Expand All @@ -164,11 +212,14 @@ def is_deprecation_call(node: ast.expr) -> bool:

def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # pylint: disable=invalid-name
"""Visitor for function declarations"""
self.deprecations += [
DeprecationDecorator(self.filename, cast(ast.Call, d_node), node)
for d_node in node.decorator_list
if DecoratorVisitor.is_deprecation_decorator(d_node)
]
for d_node in node.decorator_list:
if DecoratorVisitor.is_deprecation_decorator(d_node):
self.deprecations.append(
DeprecationDecorator(self.filename, cast(ast.Call, d_node), node)
)
for stmt in ast.walk(node):
if DecoratorVisitor.is_deprecation_warning(stmt):
self.deprecations.append(DeprecationWarn(self.filename, stmt, node))
ast.NodeVisitor.generic_visit(self, node)

def visit_Call(self, node: ast.Call) -> None: # pylint: disable=invalid-name
Expand Down Expand Up @@ -210,7 +261,7 @@ def group_by(self, attribute_idx: str) -> None:
grouped = defaultdict(list)
for obj in self.deprecations:
grouped[getattr(obj, attribute_idx)].append(obj)
for key in sorted(grouped.keys()):
for key in sorted(grouped.keys(), key=str):
self.grouped[key] = grouped[key]

@staticmethod
Expand All @@ -223,17 +274,17 @@ def find_deprecations(file_name: Path) -> list[Deprecation]:
return decorator_visitor.deprecations


def print_main(directory: str, pending: str) -> None:
def print_main(directory: str, pending: str, format_: str) -> None:
# pylint: disable=invalid-name
"""Prints output"""
collection = DeprecationCollection(Path(directory))
collection.group_by("since")

DATA_JSON = LAST_TIME_MINOR = DETAILS = None
try:
DATA_JSON = requests.get("https://pypi.org/pypi/qiskit-terra/json", timeout=5).json()
DATA_JSON = requests.get("https://pypi.org/pypi/qiskit/json", timeout=5).json()
except requests.exceptions.ConnectionError:
print("https://pypi.org/pypi/qiskit-terra/json timeout...", file=sys.stderr)
print("https://pypi.org/pypi/qiskit/json timeout...", file=sys.stderr)

if DATA_JSON:
LAST_MINOR = ".".join(DATA_JSON["info"]["version"].split(".")[:2])
Expand All @@ -251,17 +302,23 @@ def print_main(directory: str, pending: str) -> None:
diff_days = (LAST_TIME_MINOR - release_minor_datetime).days
DETAILS = f"Released in {release_minor_date}"
if diff_days:
DETAILS += f" (wrt last minor release, {round(diff_days / 30.4)} month old)"
DETAILS += f" ({round(diff_days / 30.4)} month since the last minor release)"
except KeyError:
DETAILS = "Future release"
DETAILS = "Future release?"
lines = []
for deprecation in deprecations:
if pending == "exclude" and deprecation.pending:
continue
if pending == "only" and not deprecation.pending:
continue
pending_arg = " - PENDING" if deprecation.pending else ""
lines.append(f" - {deprecation.location_str} ({deprecation.target}){pending_arg}")
if format_ == "console":
lines.append(f" - {deprecation.location_str} ({deprecation.target}){pending_arg}")
if format_ == "md":
lines.append(f" - `{deprecation.location_str}` (`{deprecation.target}`)")
if format_ == "md":
since_version = f"**{since_version or 'n/a'}**"
DETAILS = ""
if lines:
print(f"\n{since_version}: {DETAILS}")
print("\n".join(lines))
Expand All @@ -283,9 +340,16 @@ def create_parser() -> argparse.ArgumentParser:
default="exclude",
help="show pending deprecations",
)
parser.add_argument(
"-f",
"--format",
choices=["console", "md"],
default="console",
help="format the output",
)
return parser


if __name__ == "__main__":
args = create_parser().parse_args()
print_main(args.directory, args.pending)
print_main(args.directory, args.pending, args.format)
Loading