Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dadd9d6
Add option to run uv easily if wanted
lmazuel Aug 21, 2025
3f13f91
Merge branch 'main' into pyp_for_uv
scbedd Aug 21, 2025
a9373f2
fix tests
scbedd Aug 21, 2025
254b3a0
lotsa small changes
scbedd Aug 22, 2025
700ab3b
we are actually able to make this work now
scbedd Aug 22, 2025
8a9254a
more abstractions
scbedd Aug 22, 2025
4226d65
tiny comment adjusts
scbedd Aug 22, 2025
9ad3e75
Pulling in changes from fork (#42675)
scbedd Aug 25, 2025
f32b645
merge main
scbedd Aug 25, 2025
1f41c5b
commit doc fixes + updates to runtime code
scbedd Aug 25, 2025
64edd5f
remove the extra period
scbedd Aug 25, 2025
e172f43
Merge branch 'Azure:main' into jennypng-mypy-checks
JennyPng Aug 25, 2025
3580c2e
cleanup readme / remove breakpoint
scbedd Aug 26, 2025
c6811fe
revert troubleshooting guide
scbedd Aug 26, 2025
6892606
Merge branch 'populating-more-cli-entrypoint' of https://github.com/A…
JennyPng Aug 26, 2025
3b144ea
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-python …
JennyPng Aug 26, 2025
8fd5743
add mypy check
JennyPng Aug 26, 2025
7761a76
add mypy version
JennyPng Aug 26, 2025
f729777
add get_targeted_directories helper function
JennyPng Aug 27, 2025
8aec9a5
clean
JennyPng Aug 27, 2025
0e090be
append wildcard to target glob
JennyPng Aug 27, 2025
48cb529
revert adding /* to target
JennyPng Aug 27, 2025
41456c8
change default to **
JennyPng Aug 27, 2025
b93345d
remove comment
JennyPng Aug 27, 2025
7b0a224
update import_all
JennyPng Aug 27, 2025
f5baafa
update import_all and whl
JennyPng Aug 27, 2025
e4b447c
copilot formatting fixes
JennyPng Aug 27, 2025
70fbcf2
update exception logging
JennyPng Aug 27, 2025
4ffddd6
Merge branch 'jennypng-mypy-checks' of https://github.com/JennyPng/az…
JennyPng Aug 27, 2025
06752a4
update readme
JennyPng Aug 27, 2025
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
14 changes: 6 additions & 8 deletions eng/tools/azure-sdk-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,14 @@ class my_check(Check):
"""This is the recommended """
set_envvar_defaults()

if args.target == ".":
targeted = [os.getcwd()]
else:
target_dir = os.getcwd()
targeted = discover_targeted_packages(args.target, target_dir)
targeted = self.get_targeted_directories(args)

results: List[int] = []

for pkg in targeted:
parsed = ParsedSetup.from_path(pkg)
print(f"Processing {pkg.name} for my_check")
for parsed in targeted:
pkg_dir = parsed.folder
pkg_name = parsed.name
print(f"Processing {pkg_name} for my_check")
```

- Once the new check has been created, register it in `azpysdk/main.py` on line 58. This will likely be automated by decorator in the near future but this is out how it should be done for now.
Expand Down
32 changes: 31 additions & 1 deletion eng/tools/azure-sdk-tools/azpysdk/Check.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import abc
import os
import argparse
import traceback
from typing import Sequence, Optional, List, Any
from ci_tools.parsing import ParsedSetup
from ci_tools.functions import discover_targeted_packages

class Check(abc.ABC):
"""
Expand Down Expand Up @@ -28,4 +32,30 @@ def run(self, args: argparse.Namespace) -> int:

Subclasses can override this to perform the actual work.
"""
return 0
return 0

def get_targeted_directories(self, args: argparse.Namespace) -> List[ParsedSetup]:
"""
Get the directories that are targeted for the check.
"""
targeted: List[ParsedSetup] = []
targeted_dir = os.getcwd()

if args.target == ".":
try:
targeted.append(ParsedSetup.from_path(targeted_dir))
except Exception as e:
print("Error: Current directory does not appear to be a Python package (no setup.py or setup.cfg found). Remove '.' argument to run on child directories.")
print(f"Exception: {e}")
return []
else:
targeted_packages = discover_targeted_packages(args.target, targeted_dir)
for pkg in targeted_packages:
try:
targeted.append(ParsedSetup.from_path(pkg))
except Exception as e:
print(f"Unable to parse {pkg} as a Python package. Dumping exception detail and skipping.")
print(f"Exception: {e}")
print(traceback.format_exc())

return targeted
12 changes: 4 additions & 8 deletions eng/tools/azure-sdk-tools/azpysdk/import_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,16 @@ def run(self, args: argparse.Namespace) -> int:
"""Run the import_all check command."""
print("Running import_all check in isolated venv...")

# this is common. we should have an abstraction layer for this somehow
if args.target == ".":
targeted = [os.getcwd()]
else:
target_dir = os.getcwd()
targeted = discover_targeted_packages(args.target, target_dir)
targeted = self.get_targeted_directories(args)

# {[tox]pip_command} freeze
# python {repository_root}/eng/tox/import_all.py -t {tox_root}

outcomes: List[int] = []

for pkg in targeted:
parsed = ParsedSetup.from_path(pkg)
for parsed in targeted:
pkg = parsed.folder


staging_area = tempfile.mkdtemp()
create_package_and_install(
Expand Down
6 changes: 4 additions & 2 deletions eng/tools/azure-sdk-tools/azpysdk/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from .whl import whl
from .import_all import import_all
from .mypy import mypy

from ci_tools.scenario import install_into_venv, get_venv_python
from ci_tools.functions import get_venv_call
Expand Down Expand Up @@ -42,8 +43,8 @@ def build_parser() -> argparse.ArgumentParser:
common.add_argument(
"target",
nargs="?",
default=".",
help="Glob pattern for packages. Defaults to '.', but will match patterns below CWD if a value is provided."
default="**",
help="Glob pattern for packages. Defaults to '**', but will match patterns below CWD if a value is provided."
)
# allow --isolate to be specified after the subcommand as well
common.add_argument(
Expand All @@ -58,6 +59,7 @@ def build_parser() -> argparse.ArgumentParser:
# register our checks with the common params as their parent
whl().register(subparsers, [common])
import_all().register(subparsers, [common])
mypy().register(subparsers, [common])

return parser

Expand Down
150 changes: 150 additions & 0 deletions eng/tools/azure-sdk-tools/azpysdk/mypy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import argparse
import os
import sys
import logging
import tempfile

from typing import Optional, List
from subprocess import CalledProcessError, check_call

from .Check import Check
from ci_tools.parsing import ParsedSetup
from ci_tools.functions import discover_targeted_packages
from ci_tools.scenario.generation import create_package_and_install
from ci_tools.variables import in_ci, set_envvar_defaults
from ci_tools.environment_exclusions import (
is_check_enabled, is_typing_ignored
)

logging.getLogger().setLevel(logging.INFO)

PYTHON_VERSION = "3.9"
MYPY_VERSION = "1.14.1"

class mypy(Check):
def __init__(self) -> None:
super().__init__()

def register(self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None) -> None:
"""Register the `mypy` check. The mypy check installs mypy and runs mypy against the target package.
"""
parents = parent_parsers or []
p = subparsers.add_parser("mypy", parents=parents, help="Run the mypy check")
p.set_defaults(func=self.run)

p.add_argument(
"--next",
default=False,
help="Next version of mypy is being tested",
required=False
)

def run(self, args: argparse.Namespace) -> int:
"""Run the mypy check command."""
print("Running mypy check in isolated venv...")

set_envvar_defaults()

targeted = self.get_targeted_directories(args)

results: List[int] = []

for parsed in targeted:
package_dir = parsed.folder
package_name = parsed.name
print(f"Processing {package_name} for mypy check")

staging_area = tempfile.mkdtemp()
create_package_and_install(
distribution_directory=staging_area,
target_setup=package_dir,
skip_install=False,
cache_dir=None,
work_dir=staging_area,
force_create=False,
package_type="wheel",
pre_download_disabled=False,
)

# install mypy
try:
if (args.next):
# use latest version of mypy
check_call([sys.executable, "-m", "pip", "install", "mypy"])
else:
check_call([sys.executable, "-m", "pip", "install", f"mypy=={MYPY_VERSION}"])
except CalledProcessError as e:
print("Failed to install mypy:", e)
return e.returncode

logging.info(f"Running mypy against {package_name}")

if not args.next and in_ci():
if not is_check_enabled(package_dir, "mypy", True) or is_typing_ignored(package_name):
logging.info(
f"Package {package_name} opts-out of mypy check. See https://aka.ms/python/typing-guide for information."
)
continue

top_level_module = parsed.namespace.split(".")[0]

commands = [
sys.executable,
"-m",
"mypy",
"--python-version",
PYTHON_VERSION,
"--show-error-codes",
"--ignore-missing-imports",
]
src_code = [*commands, os.path.join(package_dir, top_level_module)]
src_code_error = None
sample_code_error = None
try:
logging.info(
f"Running mypy commands on src code: {src_code}"
)
results.append(check_call(src_code))
logging.info("Verified mypy, no issues found")
except CalledProcessError as src_error:
src_code_error = src_error
results.append(src_error.returncode)

if not args.next and in_ci() and not is_check_enabled(package_dir, "type_check_samples", True):
logging.info(
f"Package {package_name} opts-out of mypy check on samples."
)
continue
else:
# check if sample dirs exists, if not, skip sample code check
samples = os.path.exists(os.path.join(package_dir, "samples"))
generated_samples = os.path.exists(os.path.join(package_dir, "generated_samples"))
if not samples and not generated_samples:
logging.info(
f"Package {package_name} does not have a samples directory."
)
else:
sample_code = [
*commands,
"--check-untyped-defs",
"--follow-imports=silent",
os.path.join(package_dir, "samples" if samples else "generated_samples"),
]
try:
logging.info(
f"Running mypy commands on sample code: {sample_code}"
)
results.append(check_call(sample_code))
except CalledProcessError as sample_error:
sample_code_error = sample_error
results.append(sample_error.returncode)

if args.next and in_ci() and not is_typing_ignored(package_name):
from gh_tools.vnext_issue_creator import create_vnext_issue, close_vnext_issue
if src_code_error or sample_code_error:
create_vnext_issue(package_dir, "mypy")
else:
close_vnext_issue(package_name, "mypy")

return max(results) if results else 0

11 changes: 4 additions & 7 deletions eng/tools/azure-sdk-tools/azpysdk/whl.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,12 @@ def run(self, args: argparse.Namespace) -> int:

set_envvar_defaults()

if args.target == ".":
targeted = [os.getcwd()]
else:
target_dir = os.getcwd()
targeted = discover_targeted_packages(args.target, target_dir)
targeted = self.get_targeted_directories(args)

results: List[int] = []

for pkg in targeted:
parsed = ParsedSetup.from_path(pkg)
for parsed in targeted:
pkg = parsed.folder

dev_requirements = os.path.join(pkg, "dev_requirements.txt")

Expand Down
Loading