From 0dc4810d3fe5447c69157bc3ff512ac570d2e332 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 29 Jun 2026 14:40:59 +0800 Subject: [PATCH 1/7] new flat --pypi-version --- eng/tools/azure-sdk-tools/azpysdk/apistub.py | 62 +++++++++++++---- .../azure-sdk-tools/tests/test_apistub.py | 69 +++++++++++++++++++ 2 files changed, 118 insertions(+), 13 deletions(-) diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index 31faae7caaf9..0e2458f60817 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -69,6 +69,12 @@ def register( default=None, help="Destination directory for generated API stub files.", ) + p.add_argument( + "--pypi-version", + dest="pypi_version", + default=None, + help="Generate the stub from this released PyPI version instead of local source code.", + ) p.add_argument( "--install-deps", dest="install_deps", @@ -95,6 +101,31 @@ def ensure_apistub_dependencies(self, executable: str, package_dir: str, staging package_dir, ) + def download_pypi_wheel(self, executable: str, package_name: str, version: str, staging_directory: str) -> str: + """Download a released wheel from PyPI into the staging directory and return its path.""" + logger.info(f"Downloading {package_name}=={version} from PyPI.") + self.run_venv_command( + executable, + [ + "-m", + "pip", + "download", + f"{package_name}=={version}", + "--no-deps", + "--only-binary=:all:", + "-d", + staging_directory, + ], + cwd=staging_directory, + check=True, + ) + found_whl = find_whl(staging_directory, package_name, version) + if not found_whl: + raise FileNotFoundError( + f"No wheel found for package {package_name} version {version} after downloading from PyPI." + ) + return os.path.join(staging_directory, found_whl) + def run(self, args: argparse.Namespace) -> int: """Run the apistub check command.""" logger.info("Running apistub check...") @@ -133,23 +164,28 @@ def run(self, args: argparse.Namespace) -> int: logger.error(f"Failed to install APIView dependencies: {e}") return getattr(e, "returncode", 1) - if not os.getenv("PREBUILT_WHEEL_DIR"): - create_package_and_install( - distribution_directory=staging_directory, - target_setup=package_dir, - skip_install=True, - cache_dir=None, - work_dir=staging_directory, - force_create=False, - package_type="wheel", - pre_download_disabled=False, - python_executable=executable, - ) + pypi_version = getattr(args, "pypi_version", None) + + if pypi_version: + pkg_path = self.download_pypi_wheel(executable, package_name, pypi_version, staging_directory) + else: + if not os.getenv("PREBUILT_WHEEL_DIR"): + create_package_and_install( + distribution_directory=staging_directory, + target_setup=package_dir, + skip_install=True, + cache_dir=None, + work_dir=staging_directory, + force_create=False, + package_type="wheel", + pre_download_disabled=False, + python_executable=executable, + ) + pkg_path = get_package_wheel_path(package_dir) if install_deps: self.pip_freeze(executable) - pkg_path = get_package_wheel_path(package_dir) pkg_path = os.path.abspath(pkg_path) out_token_path = os.path.abspath(getattr(args, "dest_dir", None) or package_dir) diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index 4dd4d5bb017a..9cee3634590f 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -79,6 +79,7 @@ def _make_args( isolate=False, install_deps=False, dest_dir=None, + pypi_version=None, ): return argparse.Namespace( target=".", @@ -88,6 +89,7 @@ def _make_args( token_file=token_file, install_deps=install_deps, dest_dir=dest_dir, + pypi_version=pypi_version, ) @patch( @@ -418,3 +420,70 @@ def fake_apistub_run(exe, cmds, **kwargs): assert "--skip-pylint" not in captured_cmds[0] assert os.path.exists(os.path.join(str(tmp_path), "azure-core_python.json")) pwsh_run.assert_not_called() + + @patch( + "azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + ) + @patch("azpysdk.apistub.get_cross_language_mapping_path", return_value=None) + @patch("azpysdk.apistub.create_package_and_install") + @patch("azpysdk.apistub.install_into_venv") + @patch("azpysdk.apistub.set_envvar_defaults") + def test_pypi_version_downloads_wheel_instead_of_building( + self, _env, _install, create_package_and_install, _get_mapping, tmp_path, monkeypatch + ): + """When --pypi-version is passed, the wheel is downloaded from PyPI and local build is skipped.""" + monkeypatch.chdir(os.getcwd()) + stub = apistub() + staging = str(tmp_path / "staging") + os.makedirs(staging, exist_ok=True) + fake_parsed = MagicMock() + fake_parsed.folder = str(tmp_path) + fake_parsed.name = "azure-core" + + captured_cmds = [] + + def fake_apistub_run(exe, cmds, **kwargs): + captured_cmds.append(cmds) + out_idx = cmds.index("--out-path") + out_dir = cmds[out_idx + 1] + open(os.path.join(out_dir, "azure-core_python.json"), "w").close() + + with patch.object(stub, "get_targeted_directories", return_value=[fake_parsed]), patch.object( + stub, "get_executable", return_value=(sys.executable, staging) + ), patch.object(stub, "install_dev_reqs"), patch.object(stub, "pip_freeze"), patch.object( + stub, "ensure_apistub_dependencies" + ), patch.object( + stub, "download_pypi_wheel", return_value="/fake/azure_core-1.0.0-py3-none-any.whl" + ) as download_pypi_wheel, patch.object( + stub, "run_venv_command", side_effect=fake_apistub_run + ): + stub.run(self._make_args(token_file=True, pypi_version="1.0.0")) + + download_pypi_wheel.assert_called_once_with(sys.executable, "azure-core", "1.0.0", staging) + create_package_and_install.assert_not_called() + assert len(captured_cmds) == 1 + pkg_idx = captured_cmds[0].index("--pkg-path") + assert captured_cmds[0][pkg_idx + 1] == os.path.abspath("/fake/azure_core-1.0.0-py3-none-any.whl") + + @patch("azpysdk.apistub.find_whl", return_value="azure_core-1.0.0-py3-none-any.whl") + def test_download_pypi_wheel_runs_pip_download(self, _find_whl, tmp_path): + """download_pypi_wheel should pip download the wheel and return its path.""" + stub = apistub() + staging = str(tmp_path) + + with patch.object(stub, "run_venv_command") as run_venv_command: + result = stub.download_pypi_wheel(sys.executable, "azure-core", "1.0.0", staging) + + run_venv_command.assert_called_once() + cmds = run_venv_command.call_args.args[1] + assert cmds[0:4] == ["-m", "pip", "download", "azure-core==1.0.0"] + assert "--no-deps" in cmds + assert result == os.path.join(staging, "azure_core-1.0.0-py3-none-any.whl") + + @patch("azpysdk.apistub.find_whl", return_value=None) + def test_download_pypi_wheel_raises_when_no_wheel(self, _find_whl, tmp_path): + """download_pypi_wheel should raise FileNotFoundError when no wheel is downloaded.""" + stub = apistub() + with patch.object(stub, "run_venv_command"): + with pytest.raises(FileNotFoundError, match="No wheel found"): + stub.download_pypi_wheel(sys.executable, "azure-core", "1.0.0", str(tmp_path)) From a706ab7102539c152f8d55d2ea747e55e5e91f9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 01:52:38 +0000 Subject: [PATCH 2/7] Rename apistub PyPI flag Co-authored-by: msyyc <70930885+msyyc@users.noreply.github.com> --- eng/tools/azure-sdk-tools/azpysdk/apistub.py | 1 + .../azure-sdk-tools/tests/test_apistub.py | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index 0e2458f60817..f48a8a9c3427 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -70,6 +70,7 @@ def register( help="Destination directory for generated API stub files.", ) p.add_argument( + "--generate-from-pypi", "--pypi-version", dest="pypi_version", default=None, diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index 9cee3634590f..cef3558e0b0d 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -8,6 +8,31 @@ from azpysdk.apistub import apistub, get_package_wheel_path, get_cross_language_mapping_path + +def _build_parser(): + parser = argparse.ArgumentParser(prog="azpysdk") + subparsers = parser.add_subparsers(title="commands", dest="command") + apistub().register(subparsers) + return parser + + +class TestApistubRegistration: + def test_generate_from_pypi_flag_sets_version(self): + parser = _build_parser() + + args = parser.parse_args(["apistub", "--generate-from-pypi", "1.0.0"]) + + assert args.command == "apistub" + assert args.pypi_version == "1.0.0" + + def test_pypi_version_alias_sets_version(self): + parser = _build_parser() + + args = parser.parse_args(["apistub", "--pypi-version", "1.0.0"]) + + assert args.command == "apistub" + assert args.pypi_version == "1.0.0" + # ── get_package_wheel_path() ───────────────────────────────────────────── @@ -431,7 +456,7 @@ def fake_apistub_run(exe, cmds, **kwargs): def test_pypi_version_downloads_wheel_instead_of_building( self, _env, _install, create_package_and_install, _get_mapping, tmp_path, monkeypatch ): - """When --pypi-version is passed, the wheel is downloaded from PyPI and local build is skipped.""" + """When a PyPI version is passed, the wheel is downloaded from PyPI and local build is skipped.""" monkeypatch.chdir(os.getcwd()) stub = apistub() staging = str(tmp_path / "staging") From 3ad51397af34ae1e61e75366ec39bbeb1af909e1 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 30 Jun 2026 10:36:48 +0800 Subject: [PATCH 3/7] Remove --pypi-version alias, keep only --generate-from-pypi Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/tools/azure-sdk-tools/azpysdk/apistub.py | 1 - eng/tools/azure-sdk-tools/tests/test_apistub.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index f48a8a9c3427..4eceab2e48b7 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -71,7 +71,6 @@ def register( ) p.add_argument( "--generate-from-pypi", - "--pypi-version", dest="pypi_version", default=None, help="Generate the stub from this released PyPI version instead of local source code.", diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index cef3558e0b0d..9935c2168dbe 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -25,14 +25,6 @@ def test_generate_from_pypi_flag_sets_version(self): assert args.command == "apistub" assert args.pypi_version == "1.0.0" - def test_pypi_version_alias_sets_version(self): - parser = _build_parser() - - args = parser.parse_args(["apistub", "--pypi-version", "1.0.0"]) - - assert args.command == "apistub" - assert args.pypi_version == "1.0.0" - # ── get_package_wheel_path() ───────────────────────────────────────────── From b4b99a84cd2201ee54b1b2a6020b53c7c319abaa Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 30 Jun 2026 10:45:57 +0800 Subject: [PATCH 4/7] Rename apistub flag to --from-pypi Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/tools/azure-sdk-tools/azpysdk/apistub.py | 2 +- eng/tools/azure-sdk-tools/tests/test_apistub.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index 4eceab2e48b7..91913440884e 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -70,7 +70,7 @@ def register( help="Destination directory for generated API stub files.", ) p.add_argument( - "--generate-from-pypi", + "--from-pypi", dest="pypi_version", default=None, help="Generate the stub from this released PyPI version instead of local source code.", diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index 9935c2168dbe..b471134ca60e 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -17,10 +17,10 @@ def _build_parser(): class TestApistubRegistration: - def test_generate_from_pypi_flag_sets_version(self): + def test_from_pypi_flag_sets_version(self): parser = _build_parser() - args = parser.parse_args(["apistub", "--generate-from-pypi", "1.0.0"]) + args = parser.parse_args(["apistub", "--from-pypi", "1.0.0"]) assert args.command == "apistub" assert args.pypi_version == "1.0.0" From 73a493868be71af54ca02b079598fee43fb570f8 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 30 Jun 2026 10:48:04 +0800 Subject: [PATCH 5/7] Match apistub dest to --from-pypi flag Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/tools/azure-sdk-tools/azpysdk/apistub.py | 8 ++++---- eng/tools/azure-sdk-tools/tests/test_apistub.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index 91913440884e..42de0e1f268c 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -71,7 +71,7 @@ def register( ) p.add_argument( "--from-pypi", - dest="pypi_version", + dest="from_pypi", default=None, help="Generate the stub from this released PyPI version instead of local source code.", ) @@ -164,10 +164,10 @@ def run(self, args: argparse.Namespace) -> int: logger.error(f"Failed to install APIView dependencies: {e}") return getattr(e, "returncode", 1) - pypi_version = getattr(args, "pypi_version", None) + from_pypi = getattr(args, "from_pypi", None) - if pypi_version: - pkg_path = self.download_pypi_wheel(executable, package_name, pypi_version, staging_directory) + if from_pypi: + pkg_path = self.download_pypi_wheel(executable, package_name, from_pypi, staging_directory) else: if not os.getenv("PREBUILT_WHEEL_DIR"): create_package_and_install( diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index b471134ca60e..2ee35a6eaf5c 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -23,7 +23,7 @@ def test_from_pypi_flag_sets_version(self): args = parser.parse_args(["apistub", "--from-pypi", "1.0.0"]) assert args.command == "apistub" - assert args.pypi_version == "1.0.0" + assert args.from_pypi == "1.0.0" # ── get_package_wheel_path() ───────────────────────────────────────────── @@ -96,7 +96,7 @@ def _make_args( isolate=False, install_deps=False, dest_dir=None, - pypi_version=None, + from_pypi=None, ): return argparse.Namespace( target=".", @@ -106,7 +106,7 @@ def _make_args( token_file=token_file, install_deps=install_deps, dest_dir=dest_dir, - pypi_version=pypi_version, + from_pypi=from_pypi, ) @patch( @@ -474,7 +474,7 @@ def fake_apistub_run(exe, cmds, **kwargs): ) as download_pypi_wheel, patch.object( stub, "run_venv_command", side_effect=fake_apistub_run ): - stub.run(self._make_args(token_file=True, pypi_version="1.0.0")) + stub.run(self._make_args(token_file=True, from_pypi="1.0.0")) download_pypi_wheel.assert_called_once_with(sys.executable, "azure-core", "1.0.0", staging) create_package_and_install.assert_not_called() From 08f6726e6c98de37e00bce89cb4aa0961916b54f Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 30 Jun 2026 10:51:56 +0800 Subject: [PATCH 6/7] Use --generate-from-pypi with matching dest Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/tools/azure-sdk-tools/azpysdk/apistub.py | 10 +++++----- eng/tools/azure-sdk-tools/tests/test_apistub.py | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index 42de0e1f268c..659a8833f25c 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -70,8 +70,8 @@ def register( help="Destination directory for generated API stub files.", ) p.add_argument( - "--from-pypi", - dest="from_pypi", + "--generate-from-pypi", + dest="generate_from_pypi", default=None, help="Generate the stub from this released PyPI version instead of local source code.", ) @@ -164,10 +164,10 @@ def run(self, args: argparse.Namespace) -> int: logger.error(f"Failed to install APIView dependencies: {e}") return getattr(e, "returncode", 1) - from_pypi = getattr(args, "from_pypi", None) + generate_from_pypi = getattr(args, "generate_from_pypi", None) - if from_pypi: - pkg_path = self.download_pypi_wheel(executable, package_name, from_pypi, staging_directory) + if generate_from_pypi: + pkg_path = self.download_pypi_wheel(executable, package_name, generate_from_pypi, staging_directory) else: if not os.getenv("PREBUILT_WHEEL_DIR"): create_package_and_install( diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index 2ee35a6eaf5c..171d646f1e47 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -17,13 +17,13 @@ def _build_parser(): class TestApistubRegistration: - def test_from_pypi_flag_sets_version(self): + def test_generate_from_pypi_flag_sets_version(self): parser = _build_parser() - args = parser.parse_args(["apistub", "--from-pypi", "1.0.0"]) + args = parser.parse_args(["apistub", "--generate-from-pypi", "1.0.0"]) assert args.command == "apistub" - assert args.from_pypi == "1.0.0" + assert args.generate_from_pypi == "1.0.0" # ── get_package_wheel_path() ───────────────────────────────────────────── @@ -96,7 +96,7 @@ def _make_args( isolate=False, install_deps=False, dest_dir=None, - from_pypi=None, + generate_from_pypi=None, ): return argparse.Namespace( target=".", @@ -106,7 +106,7 @@ def _make_args( token_file=token_file, install_deps=install_deps, dest_dir=dest_dir, - from_pypi=from_pypi, + generate_from_pypi=generate_from_pypi, ) @patch( @@ -474,7 +474,7 @@ def fake_apistub_run(exe, cmds, **kwargs): ) as download_pypi_wheel, patch.object( stub, "run_venv_command", side_effect=fake_apistub_run ): - stub.run(self._make_args(token_file=True, from_pypi="1.0.0")) + stub.run(self._make_args(token_file=True, generate_from_pypi="1.0.0")) download_pypi_wheel.assert_called_once_with(sys.executable, "azure-core", "1.0.0", staging) create_package_and_install.assert_not_called() From 95f766b0a8868b22bcf0bcef5be6fddf27c5d6c5 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 30 Jun 2026 11:15:35 +0800 Subject: [PATCH 7/7] Apply black formatting to test_apistub.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/tools/azure-sdk-tools/tests/test_apistub.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index 171d646f1e47..4799383577c4 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -25,6 +25,7 @@ def test_generate_from_pypi_flag_sets_version(self): assert args.command == "apistub" assert args.generate_from_pypi == "1.0.0" + # ── get_package_wheel_path() ─────────────────────────────────────────────