Skip to content

Commit 190cb09

Browse files
authored
Rewrite Verifytypes Check Without Tox (#43010)
* initial verifytypes script * fix helper function return types, and log statements * error handling * apply copilot suggestions * minor fix
1 parent b0363bb commit 190cb09

File tree

2 files changed

+178
-1
lines changed

2 files changed

+178
-1
lines changed

eng/tools/azure-sdk-tools/azpysdk/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .pyright import pyright
2525
from .next_pyright import next_pyright
2626
from .ruff import ruff
27+
from .verifytypes import verifytypes
2728

2829
from ci_tools.logging import configure_logging, logger
2930

@@ -90,7 +91,8 @@ def build_parser() -> argparse.ArgumentParser:
9091
pyright().register(subparsers, [common])
9192
next_pyright().register(subparsers, [common])
9293
ruff().register(subparsers, [common])
93-
94+
verifytypes().register(subparsers, [common])
95+
9496
return parser
9597

9698
def main(argv: Optional[Sequence[str]] = None) -> int:
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import argparse
2+
import os
3+
import tempfile
4+
import typing
5+
import sys
6+
import subprocess
7+
import json
8+
import pathlib
9+
10+
from typing import Optional, List
11+
from subprocess import CalledProcessError
12+
13+
from .Check import Check
14+
from ci_tools.functions import install_into_venv
15+
from ci_tools.variables import discover_repo_root, in_ci, set_envvar_defaults, set_envvar_defaults
16+
from ci_tools.environment_exclusions import is_check_enabled, is_typing_ignored
17+
from ci_tools.functions import get_pip_command
18+
from ci_tools.logging import logger
19+
20+
PYRIGHT_VERSION = "1.1.287"
21+
REPO_ROOT = discover_repo_root()
22+
23+
def install_from_main(setup_path: str) -> int:
24+
path = pathlib.Path(setup_path)
25+
subdirectory = path.relative_to(REPO_ROOT)
26+
cwd = os.getcwd()
27+
with tempfile.TemporaryDirectory() as temp_dir_name:
28+
os.chdir(temp_dir_name)
29+
try:
30+
subprocess.check_call(['git', 'init'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
31+
subprocess.check_call(
32+
['git', 'clone', '--no-checkout', 'https://github.com/Azure/azure-sdk-for-python.git', '--depth', '1'],
33+
stdout=subprocess.DEVNULL,
34+
stderr=subprocess.STDOUT
35+
)
36+
os.chdir("azure-sdk-for-python")
37+
subprocess.check_call(['git', 'sparse-checkout', 'init', '--cone'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
38+
subprocess.check_call(['git', 'sparse-checkout', 'set', subdirectory], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
39+
subprocess.check_call(['git', 'checkout', 'main'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
40+
41+
if not os.path.exists(os.path.join(os.getcwd(), subdirectory)):
42+
# code is not checked into main yet, nothing to compare
43+
logger.info(f"{subdirectory} is not checked into main, nothing to compare.")
44+
return 1
45+
46+
os.chdir(subdirectory)
47+
48+
command = get_pip_command() + [
49+
"install",
50+
".",
51+
"--force-reinstall"
52+
]
53+
54+
subprocess.check_call(command, stdout=subprocess.DEVNULL)
55+
finally:
56+
os.chdir(cwd) # allow temp dir to be deleted
57+
return 0
58+
59+
def get_type_complete_score(commands: typing.List[str], check_pytyped: bool = False) -> float:
60+
try:
61+
response = subprocess.run(
62+
commands,
63+
check=True,
64+
capture_output=True,
65+
)
66+
except subprocess.CalledProcessError as e:
67+
if e.returncode != 1:
68+
logger.error(
69+
f"Running verifytypes failed: {e.stderr}. See https://aka.ms/python/typing-guide for information."
70+
)
71+
return -1.0
72+
73+
report = json.loads(e.output)
74+
if check_pytyped:
75+
pytyped_present = report["typeCompleteness"].get("pyTypedPath", None)
76+
if not pytyped_present:
77+
logger.error(
78+
f"No py.typed file was found. See https://aka.ms/python/typing-guide for information."
79+
)
80+
return -1.0
81+
return report["typeCompleteness"]["completenessScore"]
82+
83+
# library scores 100%
84+
report = json.loads(response.stdout)
85+
return report["typeCompleteness"]["completenessScore"]
86+
87+
class verifytypes(Check):
88+
def __init__(self) -> None:
89+
super().__init__()
90+
91+
def register(self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None) -> None:
92+
"""Register the verifytypes check. The verifytypes check installs verifytypes and runs verifytypes against the target package.
93+
"""
94+
parents = parent_parsers or []
95+
p = subparsers.add_parser("verifytypes", parents=parents, help="Run the verifytypes check to verify type completeness of a package.")
96+
p.set_defaults(func=self.run)
97+
98+
def run(self, args: argparse.Namespace) -> int:
99+
"""Run the verifytypes check command."""
100+
logger.info("Running verifytypes check...")
101+
102+
set_envvar_defaults()
103+
targeted = self.get_targeted_directories(args)
104+
105+
results: List[int] = []
106+
107+
for parsed in targeted:
108+
package_dir = parsed.folder
109+
package_name = parsed.name
110+
module = parsed.namespace
111+
executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir)
112+
logger.info(f"Processing {package_name} for verifytypes check")
113+
114+
self.install_dev_reqs(executable, args, package_dir)
115+
116+
# install pyright
117+
try:
118+
install_into_venv(executable, [f"pyright=={PYRIGHT_VERSION}"], package_dir)
119+
except CalledProcessError as e:
120+
logger.error(f"Failed to install pyright: {e}")
121+
return e.returncode
122+
123+
if in_ci():
124+
if not is_check_enabled(package_dir, "verifytypes") or is_typing_ignored(package_name):
125+
logger.info(
126+
f"{package_name} opts-out of verifytypes check. See https://aka.ms/python/typing-guide for information."
127+
)
128+
continue
129+
130+
commands = [
131+
executable,
132+
"-m",
133+
"pyright",
134+
"--verifytypes",
135+
module,
136+
"--ignoreexternal",
137+
"--outputjson",
138+
]
139+
140+
# get type completeness score from current code
141+
score_from_current = get_type_complete_score(commands, check_pytyped=True)
142+
if (score_from_current == -1.0):
143+
results.append(1)
144+
continue
145+
146+
try:
147+
subprocess.check_call(commands[:-1])
148+
except subprocess.CalledProcessError:
149+
logger.warning("verifytypes reported issues.") # we don't fail on verifytypes, only if type completeness score worsens from main
150+
151+
if in_ci():
152+
# get type completeness score from main
153+
logger.info(
154+
"Getting the type completeness score from the code in main..."
155+
)
156+
if (install_from_main(os.path.abspath(package_dir)) > 0):
157+
continue
158+
159+
score_from_main = get_type_complete_score(commands)
160+
if (score_from_main == -1.0):
161+
results.append(1)
162+
continue
163+
164+
score_from_main_rounded = round(score_from_main * 100, 1)
165+
score_from_current_rounded = round(score_from_current * 100, 1)
166+
logger.info("\n-----Type completeness score comparison-----\n")
167+
logger.info(f"Score in main: {score_from_main_rounded}%")
168+
# Give a 5% buffer for type completeness score to decrease
169+
if score_from_current_rounded < score_from_main_rounded - 5:
170+
logger.error(
171+
f"\nERROR: The type completeness score of {package_name} has significantly decreased compared to the score in main. "
172+
f"See the above output for areas to improve. See https://aka.ms/python/typing-guide for information."
173+
)
174+
results.append(1)
175+
return max(results) if results else 0

0 commit comments

Comments
 (0)