diff --git a/.chronus/changes/http-client-python-add-pyproject-toml-2025-6-16-18-9-43.md b/.chronus/changes/http-client-python-add-pyproject-toml-2025-6-16-18-9-43.md new file mode 100644 index 00000000000..19df2a64d99 --- /dev/null +++ b/.chronus/changes/http-client-python-add-pyproject-toml-2025-6-16-18-9-43.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-python" +--- + +Adding pyproject.toml generation and optional keep-setup-py flag diff --git a/cspell.yaml b/cspell.yaml index 68ffdaf3a0f..df8339252f4 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -199,6 +199,7 @@ words: - pyright - pyrightconfig - pytest + - pytyped - pyyaml - rcfile - reactivex @@ -218,6 +219,7 @@ words: - safeint - sdkcore - segmentof + - setuppy - serde - sfixed - shiki diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 2608be74e09..7de55c3ae5e 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -152,6 +152,7 @@ async function onEmitMain(context: EmitContext) { } if (resolvedOptions["generate-packaging-files"]) { commandArgs["package-mode"] = sdkContext.arm ? "azure-mgmt" : "azure-dataplane"; + commandArgs["keep-setup-py"] = resolvedOptions["keep-setup-py"] === true ? "true" : "false"; } if (sdkContext.arm === true) { commandArgs["azure-arm"] = "true"; diff --git a/packages/http-client-python/emitter/src/lib.ts b/packages/http-client-python/emitter/src/lib.ts index 834ab778cd4..f946f445df5 100644 --- a/packages/http-client-python/emitter/src/lib.ts +++ b/packages/http-client-python/emitter/src/lib.ts @@ -22,6 +22,7 @@ export interface PythonEmitterOptions { "package-pprint-name"?: string; "head-as-boolean"?: boolean; "use-pyodide"?: boolean; + "keep-setup-py"?: boolean; } export interface PythonSdkContext extends SdkContext { @@ -86,6 +87,12 @@ export const PythonEmitterOptionsSchema: JSONSchemaType = description: "Whether to validate the versioning of the package. Defaults to `true`. If set to `false`, we will not validate the versioning of the package.", }, + "keep-setup-py": { + type: "boolean", + nullable: true, + description: + "Whether to keep the existing `setup.py` when `generate-packaging-files` is `true`. If set to `false` and by default, `pyproject.toml` will be generated instead. To generate `setup.py`, use `basic-setup-py`.", + }, }, required: [], }; diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 27bfa787247..de69805cd2e 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -153,10 +153,17 @@ const EMITTER_OPTIONS: Record | Record None: "We are working on creating a new multiapi SDK for version tolerant and it is not available yet." ) + # If multiapi, do not generate default pyproject.toml + if self.get("multiapi"): + self["keep-setup-py"] = True + if self.get("client-side-validation") and self.get("version-tolerant"): raise ValueError("Can not generate version tolerant with --client-side-validation. ") @@ -210,6 +215,9 @@ def __init__(self, *, output_folder: Union[str, Path], **kwargs: Any) -> None: _LOGGER.warning("Loading python.json file. This behavior will be depreacted") self.options.update(python_json) + def get_output_folder(self) -> Path: + return self.output_folder + def read_file(self, path: Union[str, Path]) -> str: """Directly reading from disk""" # make path relative to output folder @@ -227,6 +235,14 @@ def write_file(self, filename: Union[str, Path], file_content: str) -> None: with open(self.output_folder / Path(filename), "w", encoding="utf-8") as fd: fd.write(file_content) + def remove_file(self, filename: Union[str, Path]) -> None: + try: + file_path = self.output_folder / Path(filename) + if file_path.is_file(): + file_path.unlink() + except FileNotFoundError: + pass + def list_file(self) -> List[str]: return [str(f.relative_to(self.output_folder)) for f in self.output_folder.glob("**/*") if f.is_file()] diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py index 4e1aad3b484..56518a6ba68 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py @@ -9,6 +9,7 @@ import re from typing import List, Any, Union from pathlib import Path +from packaging.version import parse as parse_version from jinja2 import PackageLoader, Environment, FileSystemLoader, StrictUndefined from ... import ReaderAndWriter @@ -52,10 +53,9 @@ "LICENSE.jinja2", "MANIFEST.in.jinja2", "README.md.jinja2", - "setup.py.jinja2", ] -_REGENERATE_FILES = {"setup.py", "MANIFEST.in"} +_REGENERATE_FILES = {"MANIFEST.in"} AsyncInfo = namedtuple("AsyncInfo", ["async_mode", "async_path"]) @@ -80,6 +80,15 @@ def __init__( ) -> None: super().__init__(output_folder=output_folder, **kwargs) self.code_model = code_model + self._regenerate_setup_py() + + def _regenerate_setup_py(self): + if self.code_model.options["keep-setup-py"] or self.code_model.options["basic-setup-py"]: + _PACKAGE_FILES.append("setup.py.jinja2") + _REGENERATE_FILES.add("setup.py") + else: + _PACKAGE_FILES.append("pyproject.toml.jinja2") + _REGENERATE_FILES.add("pyproject.toml") @property def has_aio_folder(self) -> bool: @@ -106,7 +115,11 @@ def keep_version_file(self) -> bool: serialized_version = match.group(1) if match else "" except (FileNotFoundError, IndexError): serialized_version = "" - return serialized_version > self.code_model.options.get("package-version", "") + try: + return parse_version(serialized_version) > parse_version(self.code_model.options.get("package-version", "")) + except Exception: # pylint: disable=broad-except + # If parsing the version fails, we assume the version file is not valid and overwrite. + return False def serialize(self) -> None: env = Environment( @@ -122,9 +135,12 @@ def serialize(self) -> None: for client_namespace, client_namespace_type in self.code_model.client_namespace_types.items(): exec_path = self.exec_path(client_namespace) if client_namespace == "": - # Write the setup file if self.code_model.options["basic-setup-py"]: + # Write the setup file self.write_file(exec_path / Path("setup.py"), general_serializer.serialize_setup_file()) + elif not self.code_model.options["keep-setup-py"]: + # remove setup.py file + self.remove_file(exec_path / Path("setup.py")) # add packaging files in root namespace (e.g. setup.py, README.md, etc.) if self.code_model.options.get("package-mode"): @@ -231,9 +247,10 @@ def _serialize_and_write_package_files(self, client_namespace: str) -> None: if self.keep_version_file and file == "setup.py" and not self.code_model.options["azure-arm"]: # don't regenerate setup.py file if the version file is more up to date for data-plane continue + file_path = self.get_output_folder() / Path(output_name) self.write_file( output_name, - serializer.serialize_package_file(template_name, **params), + serializer.serialize_package_file(template_name, file_path, **params), ) def _keep_patch_file(self, path_file: Path, env: Environment): diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index cae9ab98c92..558f39ef196 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -4,7 +4,10 @@ # license information. # -------------------------------------------------------------------------- import json -from typing import Any, List +from typing import Any, List, TYPE_CHECKING +import re +import tomli as tomllib +from packaging.version import parse as parse_version from .import_serializer import FileImportSerializer, TypingSection from ..models.imports import MsrestImportType, FileImport from ..models import ( @@ -16,6 +19,9 @@ from .client_serializer import ClientSerializer, ConfigSerializer from .base_serializer import BaseSerializer +if TYPE_CHECKING: + from pathlib import Path + VERSION_MAP = { "msrest": "0.7.1", "isodate": "0.6.1", @@ -42,8 +48,73 @@ def serialize_setup_file(self) -> str: params.update({"options": self.code_model.options}) return template.render(code_model=self.code_model, **params) - def serialize_package_file(self, template_name: str, **kwargs: Any) -> str: + def _extract_min_dependency(self, s): + # Extract the minimum version from a dependency string. + # + # Handles formats like: + # - >=1.2.3 + # - >=0.1.0b1 (beta versions) + # - >=1.2.3rc2 (release candidates) + # + # Returns the parsed version if found, otherwise version "0". + m = re.search(r"[>=]=?([\d.]+(?:[a-z]+\d+)?)", s) + return parse_version(m.group(1)) if m else parse_version("0") + + def _keep_pyproject_fields(self, file_path: "Path") -> dict: + # Load the pyproject.toml file if it exists and extract fields to keep. + result: dict = {"KEEP_FIELDS": {}} + try: + with open(file_path, "rb") as f: + loaded_pyproject_toml = tomllib.load(f) + except Exception: # pylint: disable=broad-except + # If parsing the pyproject.toml fails, we assume the it does not exist or is incorrectly formatted. + return result + + # Keep azure-sdk-build configuration + if "tool" in loaded_pyproject_toml and "azure-sdk-build" in loaded_pyproject_toml["tool"]: + result["KEEP_FIELDS"]["tool.azure-sdk-build"] = loaded_pyproject_toml["tool"]["azure-sdk-build"] + + # Process dependencies + if "project" in loaded_pyproject_toml: + # Handle main dependencies + if "dependencies" in loaded_pyproject_toml["project"]: + kept_deps = [] + for dep in loaded_pyproject_toml["project"]["dependencies"]: + dep_name = re.split(r"[<>=\[]", dep)[0].strip() + + # Check if dependency is one we track in VERSION_MAP + if dep_name in VERSION_MAP: + # For tracked dependencies, check if the version is higher than our default + default_version = parse_version(VERSION_MAP[dep_name]) + dep_version = self._extract_min_dependency(dep) + # If the version is higher than the default, update VERSION_MAP + # with higher min dependency version + if dep_version > default_version: + VERSION_MAP[dep_name] = str(dep_version) + else: + # Keep non-default dependencies + kept_deps.append(dep) + + if kept_deps: + result["KEEP_FIELDS"]["project.dependencies"] = kept_deps + + # Keep optional dependencies + if "optional-dependencies" in loaded_pyproject_toml["project"]: + result["KEEP_FIELDS"]["project.optional-dependencies"] = loaded_pyproject_toml["project"][ + "optional-dependencies" + ] + + return result + + def serialize_package_file(self, template_name: str, file_path: "Path", **kwargs: Any) -> str: template = self.env.get_template(template_name) + + # Add fields to keep from an existing pyproject.toml + if template_name == "pyproject.toml.jinja2": + params = self._keep_pyproject_fields(file_path) + else: + params = {} + package_parts = ( self.code_model.namespace.split(".")[:-1] if self.code_model.is_tsp @@ -57,17 +128,19 @@ def serialize_package_file(self, template_name: str, **kwargs: Any) -> str: dev_status = "4 - Beta" else: dev_status = "5 - Production/Stable" - params = { - "code_model": self.code_model, - "dev_status": dev_status, - "token_credential": token_credential, - "pkgutil_names": [".".join(package_parts[: i + 1]) for i in range(len(package_parts))], - "init_names": ["/".join(package_parts[: i + 1]) + "/__init__.py" for i in range(len(package_parts))], - "client_name": self.code_model.clients[0].name if self.code_model.clients else "", - "VERSION_MAP": VERSION_MAP, - "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION, - "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION, - } + params.update( + { + "code_model": self.code_model, + "dev_status": dev_status, + "token_credential": token_credential, + "pkgutil_names": [".".join(package_parts[: i + 1]) for i in range(len(package_parts))], + "init_names": ["/".join(package_parts[: i + 1]) + "/__init__.py" for i in range(len(package_parts))], + "client_name": self.code_model.clients[0].name if self.code_model.clients else "", + "VERSION_MAP": VERSION_MAP, + "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION, + "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION, + } + ) params.update({"options": self.code_model.options}) params.update(kwargs) return template.render(file_import=FileImport(self.code_model), **params) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 new file mode 100644 index 00000000000..cbbed8f211e --- /dev/null +++ b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 @@ -0,0 +1,109 @@ +{% set min_version = MIN_PYTHON_VERSION.split('.')[1] | int %} +{% set max_version = MAX_PYTHON_VERSION.split('.')[1] | int %} +{% if code_model.license_header %} +{{ code_model.license_header }} +{% endif %} + +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] # Requires 61.0.0 for dynamic version +build-backend = "setuptools.build_meta" + +[project] +name = "{{ options.get('package-name')|lower }}" +{% if options.get('package-mode') %} +authors = [ + { name = "{{ code_model.company_name }}"{% if code_model.is_azure_flavor %}, email = "azpysdkhelp@microsoft.com"{% endif %} }, +] +description = "{{ code_model.company_name }} {% if code_model.is_azure_flavor and not options.get('package-pprint-name').startswith('Azure ') %}Azure {% endif %}{{ options.get('package-pprint-name') }} Client Library for Python" +license = {text = "MIT License"} +classifiers = [ + "Development Status :: {{ dev_status }}", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + {% for version in range(min_version, max_version + 1) %} + "Programming Language :: Python :: 3.{{ version }}", + {% endfor %} + "License :: OSI Approved :: MIT License", +] +requires-python = ">={{ MIN_PYTHON_VERSION }}" +{% else %} +description = "{{ options.get('package-name') }}" +{% endif %} +{% if code_model.is_azure_flavor %} +keywords = ["azure", "azure sdk"] +{% endif %} + +dependencies = [ + {% if code_model.is_legacy %} + "msrest>={{ VERSION_MAP['msrest'] }}", + {% else %} + "isodate>={{ VERSION_MAP['isodate'] }}", + {% endif %} + {% if options.get('azure_arm') %} + "azure-mgmt-core>={{ VERSION_MAP['azure-mgmt-core'] }}", + {% elif code_model.is_azure_flavor %} + "azure-core>={{ VERSION_MAP['azure-core'] }}", + {% else %} + "corehttp[requests]>={{ VERSION_MAP['corehttp'] }}", + {% endif %} + "typing-extensions>={{ VERSION_MAP['typing-extensions'] }}", + {% if KEEP_FIELDS and KEEP_FIELDS.get('project.dependencies') %} + {% for dep in KEEP_FIELDS.get('project.dependencies') %} + "{{ dep }}", + {% endfor %} + {% endif %} +] +dynamic = [ + {% if options.get('package-mode') %}"version", {% endif %}"readme" +] +{% if not options.get('package-mode') %} +version = "{{ options.get("package-version", "unknown") }}" +{% endif %} +{% if KEEP_FIELDS and KEEP_FIELDS.get('project.optional-dependencies') %} + +[project.optional-dependencies] +{% for key, val in KEEP_FIELDS.get('project.optional-dependencies').items() %} +{{ key }} = [ + {% for dep in val %} + "{{ dep }}", + {% endfor %} +] +{% endfor %} +{% endif %} +{% if code_model.is_azure_flavor %} + +[project.urls] +repository = "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk" +{% endif %} + +[tool.setuptools.dynamic] +{% if options.get('package-mode') %} + {% if code_model.is_tsp %} +version = {attr = "{{ code_model.namespace|lower }}._version.VERSION"} + {% else %} +version = {attr = "{{ options.get('package-name')|lower|replace('-', '.') }}._version.VERSION"} + {% endif %} +{% endif %} +readme = {file = ["README.md"], content-type = "text/markdown"} +{% if options.get('package-mode') %} + +[tool.setuptools.packages.find] +exclude = [ + "tests*", + "samples*", + {% for pkgutil_name in pkgutil_names %} + "{{ pkgutil_name }}", + {% endfor %} +] + +[tool.setuptools.package-data] +pytyped = ["py.typed"] +{% endif %} +{% if KEEP_FIELDS and KEEP_FIELDS.get('tool.azure-sdk-build') %} + +[tool.azure-sdk-build] +{% for key, val in KEEP_FIELDS.get('tool.azure-sdk-build').items() %} +{{ key }} = {{ val|tojson }} +{% endfor %} +{% endif %} diff --git a/packages/http-client-python/generator/test/azure/requirements.txt b/packages/http-client-python/generator/test/azure/requirements.txt index 14fc87ceb39..1575260ff6b 100644 --- a/packages/http-client-python/generator/test/azure/requirements.txt +++ b/packages/http-client-python/generator/test/azure/requirements.txt @@ -40,6 +40,7 @@ azure-mgmt-core==1.6.0 -e ./generated/authentication-http-custom -e ./generated/authentication-oauth2 -e ./generated/authentication-union +-e ./generated/setuppy-authentication-union -e ./generated/encode-duration -e ./generated/encode-numeric -e ./generated/parameters-basic diff --git a/packages/http-client-python/generator/test/azure/tox.ini b/packages/http-client-python/generator/test/azure/tox.ini index 980a7808a91..62e9fb06e6a 100644 --- a/packages/http-client-python/generator/test/azure/tox.ini +++ b/packages/http-client-python/generator/test/azure/tox.ini @@ -20,7 +20,7 @@ commands = python ../../../eng/scripts/ci/run_pyright.py -t azure -s "generated" {posargs} # apiview - pip install apiview-stub-generator==0.3.13 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + pip install apiview-stub-generator==0.3.19 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" python ../../../eng/scripts/ci/run_apiview.py -t azure -s "generated" {posargs} [testenv:test] @@ -52,5 +52,5 @@ commands = deps= -r requirements.txt commands = - pip install apiview-stub-generator==0.3.13 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + pip install apiview-stub-generator==0.3.19 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" python ../../../eng/scripts/ci/run_apiview.py -t azure -s "generated" {posargs} diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_authentication_async.py b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_authentication_async.py index bb92d42bc4a..357eed73603 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_authentication_async.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/asynctests/test_authentication_async.py @@ -8,6 +8,7 @@ from authentication.http.custom.aio import CustomClient from authentication.oauth2.aio import OAuth2Client from authentication.union.aio import UnionClient +from setuppy.authentication.union.aio import UnionClient as SetuppyUnionClient # Utilities functions @@ -99,14 +100,16 @@ async def test_oauth2_invalid(oauth2_client, core_library): @pytest.mark.asyncio -async def test_union_keyvalid(api_key_client): - client = api_key_client(UnionClient) +@pytest.mark.parametrize("union_client_type", [UnionClient, SetuppyUnionClient]) +async def test_union_keyvalid(api_key_client, union_client_type): + client = api_key_client(union_client_type) await client.valid_key() @pytest.mark.asyncio -async def test_union_tokenvalid(oauth2_client): - client = oauth2_client(UnionClient) +@pytest.mark.parametrize("union_client_type", [UnionClient, SetuppyUnionClient]) +async def test_union_tokenvalid(oauth2_client, union_client_type): + client = oauth2_client(union_client_type) await client.valid_token(enforce_https=False) diff --git a/packages/http-client-python/generator/test/generic_mock_api_tests/test_authentication.py b/packages/http-client-python/generator/test/generic_mock_api_tests/test_authentication.py index 5c1dc39a913..f5ed841ecc2 100644 --- a/packages/http-client-python/generator/test/generic_mock_api_tests/test_authentication.py +++ b/packages/http-client-python/generator/test/generic_mock_api_tests/test_authentication.py @@ -7,6 +7,7 @@ from authentication.apikey import ApiKeyClient from authentication.oauth2 import OAuth2Client from authentication.union import UnionClient +from setuppy.authentication.union import UnionClient as SetuppyUnionClient from authentication.http.custom import CustomClient @@ -94,13 +95,15 @@ def test_oauth2_invalid(oauth2_client, core_library): assert ex.value.status_code == 403 -def test_union_keyvalid(api_key_client): - client = api_key_client(UnionClient) +@pytest.mark.parametrize("union_client_type", [UnionClient, SetuppyUnionClient]) +def test_union_keyvalid(api_key_client, union_client_type): + client = api_key_client(union_client_type) client.valid_key() -def test_union_tokenvalid(oauth2_client): - client = oauth2_client(UnionClient) +@pytest.mark.parametrize("union_client_type", [UnionClient, SetuppyUnionClient]) +def test_union_tokenvalid(oauth2_client, union_client_type): + client = oauth2_client(union_client_type) client.valid_token(enforce_https=False) diff --git a/packages/http-client-python/generator/test/unbranded/mock_api_tests/test_unbranded.py b/packages/http-client-python/generator/test/unbranded/mock_api_tests/test_unbranded.py index 1f52da5d0f7..60ce44be022 100644 --- a/packages/http-client-python/generator/test/unbranded/mock_api_tests/test_unbranded.py +++ b/packages/http-client-python/generator/test/unbranded/mock_api_tests/test_unbranded.py @@ -54,4 +54,6 @@ def test_sensitive_word(): check_folder = (Path(os.path.dirname(__file__)) / "../generated").resolve() assert [] == check_sensitive_word(check_folder, "azure") # after update spector, it shall also equal to [] - assert ["authentication-oauth2", "authentication-union"] == check_sensitive_word(check_folder, "microsoft") + assert ["authentication-oauth2", "authentication-union", "setuppy-authentication-union"] == check_sensitive_word( + check_folder, "microsoft" + ) diff --git a/packages/http-client-python/generator/test/unbranded/requirements.txt b/packages/http-client-python/generator/test/unbranded/requirements.txt index 1a2783a4b07..8ef26eadf31 100644 --- a/packages/http-client-python/generator/test/unbranded/requirements.txt +++ b/packages/http-client-python/generator/test/unbranded/requirements.txt @@ -6,6 +6,7 @@ -e ./generated/authentication-http-custom -e ./generated/authentication-oauth2 -e ./generated/authentication-union +-e ./generated/setuppy-authentication-union -e ./generated/encode-duration -e ./generated/encode-numeric -e ./generated/parameters-basic diff --git a/packages/http-client-python/generator/test/unbranded/tox.ini b/packages/http-client-python/generator/test/unbranded/tox.ini index 41aa28dad43..6e66ac7c027 100644 --- a/packages/http-client-python/generator/test/unbranded/tox.ini +++ b/packages/http-client-python/generator/test/unbranded/tox.ini @@ -20,7 +20,7 @@ commands = python ../../../eng/scripts/ci/run_pyright.py -t unbranded -s "generated" {posargs} # apiview - pip install apiview-stub-generator==0.3.13 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + pip install apiview-stub-generator==0.3.19 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" python ../../../eng/scripts/ci/run_apiview.py -t unbranded -s "generated" {posargs} [testenv:test] @@ -52,5 +52,5 @@ commands = deps= -r requirements.txt commands = - pip install apiview-stub-generator==0.3.13 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + pip install apiview-stub-generator==0.3.19 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" python ../../../eng/scripts/ci/run_apiview.py -t unbranded -s "generated" {posargs}