mirror of https://github.com/Qiskit/qiskit.git
292 lines
10 KiB
Python
Executable File
292 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# This code is part of Qiskit.
|
|
#
|
|
# (C) Copyright IBM 2023
|
|
#
|
|
# This code is licensed under the Apache License, Version 2.0. You may
|
|
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
|
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
|
#
|
|
# Any modifications or derivative works of this code must retain this
|
|
# copyright notice, and modified files need to carry a notice indicating
|
|
# that they have been altered from the originals.
|
|
|
|
"""List deprecated decorators."""
|
|
from __future__ import annotations
|
|
from typing import cast, Optional
|
|
from pathlib import Path
|
|
from collections import OrderedDict, defaultdict
|
|
import ast
|
|
from datetime import datetime
|
|
import sys
|
|
import argparse
|
|
import requests
|
|
|
|
|
|
def short_path(path: Path) -> Optional[Path]:
|
|
"""shorten the full path, when possible"""
|
|
original_path = path
|
|
while path is not None:
|
|
if Path.cwd().is_relative_to(path):
|
|
return original_path.relative_to(path)
|
|
path = path.parent if path != path.parent else None
|
|
return original_path
|
|
|
|
|
|
class Deprecation:
|
|
"""
|
|
Root class for Deprecation classes.
|
|
"""
|
|
|
|
@property
|
|
def location_str(self) -> str:
|
|
"""String with the location of the deprecated decorator <filename>:<line number>"""
|
|
return f"{self.filename}:{self.lineno}"
|
|
|
|
|
|
class DeprecationDecorator(Deprecation):
|
|
"""
|
|
Deprecation decorator, representing a single deprecation decorator.
|
|
|
|
Args:
|
|
filename: where is the deprecation.
|
|
decorator_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
|
|
) -> None:
|
|
self.filename = filename
|
|
self.decorator_node = decorator_node
|
|
self.func_node = func_node
|
|
self._since: str | None = None
|
|
self._pending: bool | None = None
|
|
|
|
@property
|
|
def since(self) -> str | None:
|
|
"""Version since the deprecation applies."""
|
|
if not self._since:
|
|
for kwarg in self.decorator_node.keywords:
|
|
if kwarg.arg == "since":
|
|
self._since = ".".join(cast(ast.Constant, kwarg.value).value.split(".")[:2])
|
|
return self._since
|
|
|
|
@property
|
|
def pending(self) -> bool | None:
|
|
"""If it is a pending deprecation."""
|
|
if not self._pending:
|
|
self._pending = next(
|
|
(
|
|
kwarg.value.value
|
|
for kwarg in self.decorator_node.keywords
|
|
if kwarg.arg == "pending"
|
|
),
|
|
False,
|
|
)
|
|
return self._pending
|
|
|
|
@property
|
|
def lineno(self) -> int:
|
|
"""Line number of the decorator."""
|
|
return self.decorator_node.lineno
|
|
|
|
@property
|
|
def target(self) -> str:
|
|
"""Name of the decorated function/method."""
|
|
return self.func_node.name
|
|
|
|
|
|
class DeprecationCall(Deprecation):
|
|
"""
|
|
Deprecation call, representing a single deprecation call.
|
|
|
|
Args:
|
|
decorator_call: deprecation call.
|
|
"""
|
|
|
|
def __init__(self, filename: Path, decorator_call: ast.Call) -> None:
|
|
self.filename = filename
|
|
self.decorator_node = decorator_call
|
|
self.lineno = decorator_call.lineno
|
|
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)
|
|
if isinstance(arg, ast.Attribute):
|
|
self._target = f"{arg.value.id}.{arg.attr}"
|
|
if isinstance(arg, ast.Name):
|
|
self._target = arg.id
|
|
return self._target
|
|
|
|
@property
|
|
def since(self) -> str | None:
|
|
"""Version since the deprecation applies."""
|
|
if not self._since:
|
|
for kwarg in self.decorator_node.func.keywords:
|
|
if kwarg.arg == "since":
|
|
self._since = ".".join(cast(ast.Constant, kwarg.value).value.split(".")[:2])
|
|
return self._since
|
|
|
|
|
|
class DecoratorVisitor(ast.NodeVisitor):
|
|
"""
|
|
Node visitor for finding deprecation decorator
|
|
Args:
|
|
filename: Name of the file to analyze
|
|
"""
|
|
|
|
def __init__(self, filename: Path):
|
|
self.filename = short_path(filename)
|
|
self.deprecations: list[Deprecation] = []
|
|
|
|
@staticmethod
|
|
def is_deprecation_decorator(node: ast.expr) -> bool:
|
|
"""Check if a node is a deprecation decorator"""
|
|
return (
|
|
isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Name)
|
|
and node.func.id.startswith("deprecate_")
|
|
)
|
|
|
|
@staticmethod
|
|
def is_deprecation_call(node: ast.expr) -> bool:
|
|
"""Check if a node is a deprecation call"""
|
|
return (
|
|
isinstance(node.func, ast.Call)
|
|
and isinstance(node.func.func, ast.Name)
|
|
and node.func.func.id.startswith("deprecate_")
|
|
)
|
|
|
|
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)
|
|
]
|
|
ast.NodeVisitor.generic_visit(self, node)
|
|
|
|
def visit_Call(self, node: ast.Call) -> None: # pylint: disable=invalid-name
|
|
"""Visitor for function call"""
|
|
if DecoratorVisitor.is_deprecation_call(node):
|
|
self.deprecations.append(DeprecationCall(self.filename, node))
|
|
ast.NodeVisitor.generic_visit(self, node)
|
|
|
|
|
|
class DeprecationCollection:
|
|
"""
|
|
A collection of :class:~.Deprecation
|
|
|
|
Args:
|
|
dirname: Directory name that would be checked recursively for deprecations.
|
|
"""
|
|
|
|
def __init__(self, dirname: Path):
|
|
self.dirname = dirname
|
|
self._deprecations: list[Deprecation] | None = None
|
|
self.grouped: OrderedDict[str, list[Deprecation]] = OrderedDict()
|
|
|
|
@property
|
|
def deprecations(self) -> list[Deprecation]:
|
|
"""List of deprecation :class:~.Deprecation"""
|
|
if self._deprecations is None:
|
|
self.collect_deprecations()
|
|
return cast(list, self._deprecations)
|
|
|
|
def collect_deprecations(self) -> None:
|
|
"""Run the :class:~.DecoratorVisitor on `self.dirname` (in place)"""
|
|
self._deprecations = []
|
|
files = [self.dirname] if self.dirname.is_file() else self.dirname.rglob("*.py")
|
|
for filename in files:
|
|
self._deprecations.extend(DeprecationCollection.find_deprecations(filename))
|
|
|
|
def group_by(self, attribute_idx: str) -> None:
|
|
"""Group :class:~`.Deprecation` in self.deprecations based on the attribute attribute_idx"""
|
|
grouped = defaultdict(list)
|
|
for obj in self.deprecations:
|
|
grouped[getattr(obj, attribute_idx)].append(obj)
|
|
for key in sorted(grouped.keys()):
|
|
self.grouped[key] = grouped[key]
|
|
|
|
@staticmethod
|
|
def find_deprecations(file_name: Path) -> list[Deprecation]:
|
|
"""Runs the deprecation finder on file_name"""
|
|
code = Path(file_name).read_text()
|
|
mod = ast.parse(code, file_name)
|
|
decorator_visitor = DecoratorVisitor(file_name)
|
|
decorator_visitor.visit(mod)
|
|
return decorator_visitor.deprecations
|
|
|
|
|
|
def print_main(directory: str, pending: 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()
|
|
except requests.exceptions.ConnectionError:
|
|
print("https://pypi.org/pypi/qiskit-terra/json timeout...", file=sys.stderr)
|
|
|
|
if DATA_JSON:
|
|
LAST_MINOR = ".".join(DATA_JSON["info"]["version"].split(".")[:2])
|
|
LAST_TIME_MINOR = datetime.fromisoformat(
|
|
DATA_JSON["releases"][f"{LAST_MINOR}.0"][0]["upload_time"]
|
|
)
|
|
|
|
for since_version, deprecations in collection.grouped.items():
|
|
if DATA_JSON and LAST_TIME_MINOR:
|
|
try:
|
|
release_minor_datetime = datetime.fromisoformat(
|
|
DATA_JSON["releases"][f"{since_version}.0"][0]["upload_time"]
|
|
)
|
|
release_minor_date = release_minor_datetime.strftime("%B %d, %Y")
|
|
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)"
|
|
except KeyError:
|
|
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 lines:
|
|
print(f"\n{since_version}: {DETAILS}")
|
|
print("\n".join(lines))
|
|
|
|
|
|
def create_parser() -> argparse.ArgumentParser:
|
|
"""Creates the ArgumentParser object"""
|
|
default_directory = Path(__file__).joinpath("..", "..", "qiskit").resolve()
|
|
parser = argparse.ArgumentParser(
|
|
prog="find_deprecated",
|
|
description="Finds the deprecation decorators in a path",
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
)
|
|
parser.add_argument("-d", "--directory", default=default_directory, help="directory to search")
|
|
parser.add_argument(
|
|
"-p",
|
|
"--pending",
|
|
choices=["only", "include", "exclude"],
|
|
default="exclude",
|
|
help="show pending deprecations",
|
|
)
|
|
return parser
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = create_parser().parse_args()
|
|
print_main(args.directory, args.pending)
|