diff --git a/eng/tools/azure-sdk-tools/ci_tools/functions.py b/eng/tools/azure-sdk-tools/ci_tools/functions.py index c2cc9cdcb216..f3da0521c67c 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/functions.py +++ b/eng/tools/azure-sdk-tools/ci_tools/functions.py @@ -651,8 +651,11 @@ def is_package_compatible( if not package_requirement.specifier.contains(immutable_version): if should_log: logging.info( - f"Dev req dependency {package_name}'s requirement specifier of {package_requirement}" - f"is not compatible with immutable requirement {immutable_requirement}." + "Dev req dependency %s's requirement specifier of %s is not compatible with " + "immutable requirement %s.", + package_name, + package_requirement, + immutable_requirement, ) return False @@ -697,70 +700,6 @@ def get_total_coverage( return report -def resolve_compatible_package(package_name: str, immutable_requirements: List[Requirement]) -> Optional[str]: - """ - This function attempts to resolve a compatible package version for whatever set of immutable_requirements that - the package must be compatible with. - - It should only be utilized when a package is found to be incompatible with the immutable_requirements. - It will attempt to resolve the incompatibility by walking backwards through different versions of - until a compatible version is found that works with the immutable_requirements. - """ - - pypi = PyPIClient() - immovable_pkgs = {req.name: req for req in immutable_requirements} - - # Let's use a real use-case to walk through this function. We're going to use the azure-ai-language-conversations - # package as an example. - - # immovable_pkgs = the selected mindependency for azure-ai-language-conversations - # -> "azure-core==1.28.0", - # -> "isodate==0.6.1", - # -> "typing-extensions==4.0.1", - # we have the following dev_reqs for azure-ai-language-conversations - # -> ../azure-sdk-tools - # -> ../azure-identity - # -> ../azure-core - - # as we walk each of the dev reqs, we check for compatibility with the immovable_packages. - # (this happens in is_package_compatible) if the dev req is incompatible, we need to resolve it. - # THIS function is what resolves it! - - # since we already know that package_name is incompatible with the immovable_pkgs, we need to walk backwards - # through the versions of package_name checking to ensure that each version is compatible with the immovable_pkgs. - # if we find one that is, we will return a new requirement string for that package which will replace this dev_req line. - for pkg in immovable_pkgs: - required_package = immovable_pkgs[pkg].name - try: - required_pkg_version = next(iter(immovable_pkgs[pkg].specifier)).version - except StopIteration: - required_pkg_version = None - - versions = pypi.get_ordered_versions(package_name, True) - versions.reverse() - - # only allow prerelease versions if the dev_req we're targeting is also prerelease - if required_pkg_version: - if not Version(required_pkg_version).is_prerelease: - versions = [v for v in versions if not v.is_prerelease] - - for version in versions: - version_release = pypi.project_release(package_name, version).get("info", {}).get("requires_dist", []) - - if version_release: - requirements_for_dev_req = [Requirement(r) for r in version_release] - - compatible = is_package_compatible( - required_package, requirements_for_dev_req, immutable_requirements, should_log=False - ) - if compatible: - # we have found a compatible version. We can return this as the new requirement line for the dev_req file. - return f"{package_name}=={version}" - - # no changes necessary - return None - - def handle_incompatible_minimum_dev_reqs( setup_path: str, filtered_requirement_list: List[str], packages_for_install: List[Requirement] ) -> List[str]: @@ -785,7 +724,6 @@ def handle_incompatible_minimum_dev_reqs( if cleansed_dev_requirement_line: dev_req_package = None - dev_req_version = None requirements_for_dev_req = [] # this is a locally built wheel file, ise pkginfo to get the metadata @@ -799,7 +737,6 @@ def handle_incompatible_minimum_dev_reqs( local_package_metadata = pkginfo.get_metadata(cleansed_dev_requirement_line) if local_package_metadata: dev_req_package = local_package_metadata.name - dev_req_version = local_package_metadata.version requirements_for_dev_req = [Requirement(r) for r in local_package_metadata.requires_dist] else: logging.error( @@ -821,7 +758,6 @@ def handle_incompatible_minimum_dev_reqs( if local_package: dev_req_package = local_package.name - dev_req_version = local_package.version requirements_for_dev_req = [Requirement(r) for r in local_package.requires] else: logging.error( @@ -844,13 +780,19 @@ def handle_incompatible_minimum_dev_reqs( # we understand how to parse it, so we should handle it if dev_req_package: if not is_package_compatible(dev_req_package, requirements_for_dev_req, packages_for_install): - new_req = resolve_compatible_package(dev_req_package, packages_for_install) - - if new_req: - cleansed_reqs.append(new_req) + available_versions = PyPIClient().get_ordered_versions(dev_req_package, True) + if available_versions: + logging.info( + "Resolving incompatible local dev requirement %s from the configured index " + "against immutable requirements %s.", + dev_req_package, + packages_for_install, + ) + cleansed_reqs.append(dev_req_package) else: logging.error( - f'Found incompatible dev requirement {dev_req_package}, but unable to locate a compatible version. Not modifying the line: "{dev_requirement_line}".' + f"Found incompatible dev requirement {dev_req_package}, but it is not published on the " + f'configured index. Not modifying the line: "{dev_requirement_line}".' ) cleansed_reqs.append(cleansed_dev_requirement_line) else: diff --git a/eng/tools/azure-sdk-tools/ci_tools/scenario/dependency_resolution.py b/eng/tools/azure-sdk-tools/ci_tools/scenario/dependency_resolution.py index 10c2951ada61..b463434a6b63 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/scenario/dependency_resolution.py +++ b/eng/tools/azure-sdk-tools/ci_tools/scenario/dependency_resolution.py @@ -148,7 +148,7 @@ def install_dependent_packages( with open(pkgs_file_path, "w", encoding="utf-8") as pkgs_file: for package in released_packages: pkgs_file.write(package + "\n") - logger.info("Created file %s to track azure packages found on PyPI", pkgs_file_path) + logger.info("Created file %s to track packages selected from the configured index", pkgs_file_path) def check_pkg_against_overrides(pkg_specifier: str) -> List[str]: @@ -239,7 +239,7 @@ def process_requirement(req: str, dependency_type: str, orig_pkg_name: str) -> s client = PyPIClient() versions = [str(v) for v in client.get_ordered_versions(pkg_name, True)] - logger.info("Versions available on PyPI for %s: %s", pkg_name, versions) + logger.info("Versions available from the configured index for %s: %s", pkg_name, versions) # prepass filter before choosing a latest or minimum, eliminate prerelease versions if they are not allowed based on the specifier if not allows_prereleases: @@ -263,7 +263,7 @@ def process_requirement(req: str, dependency_type: str, orig_pkg_name: str) -> s ) return pkg_name + "==" + version - logger.error("No version is found on PyPI for package %s that matches specifier %s", pkg_name, spec) + logger.error("No version is found on the configured index for package %s that matches specifier %s", pkg_name, spec) return "" diff --git a/eng/tools/azure-sdk-tools/pypi_tools/azdo.py b/eng/tools/azure-sdk-tools/pypi_tools/azdo.py index 8a97569c3b78..f190f483a946 100644 --- a/eng/tools/azure-sdk-tools/pypi_tools/azdo.py +++ b/eng/tools/azure-sdk-tools/pypi_tools/azdo.py @@ -1,12 +1,16 @@ import base64 +from html.parser import HTMLParser import json import logging import os import re +import sys from dataclasses import dataclass from typing import Any, Dict, List, Optional -from urllib.parse import urlparse +from urllib.parse import unquote, urlparse +from packaging.specifiers import InvalidSpecifier, SpecifierSet +from packaging.utils import InvalidSdistFilename, InvalidWheelFilename, parse_sdist_filename, parse_wheel_filename from packaging.version import Version, InvalidVersion, parse from urllib3 import PoolManager, Retry @@ -15,6 +19,18 @@ def pep503_normalize(name: str) -> str: return re.sub(r"[-_.]+", "-", name).lower() +class _SimpleIndexParser(HTMLParser): + """Collect package-file links and their PEP 503 metadata.""" + + def __init__(self): + super().__init__() + self.links: List[Dict[str, Optional[str]]] = [] + + def handle_starttag(self, tag: str, attrs: List[tuple[str, Optional[str]]]) -> None: + if tag == "a": + self.links.append(dict(attrs)) + + @dataclass(frozen=True) class AzureArtifactsFeedConfig: organization: str @@ -167,6 +183,65 @@ def get_ordered_versions(self, package_name: str, include_deleted: bool = False) out.sort() return out + @staticmethod + def _version_from_link(href: str) -> Optional[Version]: + """Extract a normalized version from a wheel or source archive link.""" + filename = os.path.basename(urlparse(href).path) + filename = unquote(filename) + try: + if filename.endswith(".whl"): + return parse_wheel_filename(filename)[1] + return parse_sdist_filename(filename)[1] + except (InvalidSdistFilename, InvalidWheelFilename): + return None + + def get_python_compatible_versions(self, package_name: str) -> List[Version]: + """Return feed versions with at least one file compatible with this interpreter. + + Azure Artifacts' package REST response lists versions but does not include + ``Requires-Python``. The PEP 503 simple index exposes that constraint on + each distribution link, allowing minimum-dependency selection to skip + releases that cannot run on the current interpreter. + + :param str package_name: The package whose versions should be listed. + :return: Sorted versions compatible with the running Python interpreter. + :rtype: list[packaging.version.Version] + """ + package = pep503_normalize(package_name) + url = f"{self._pkgs_base_url}/{self._path_prefix()}" f"/_packaging/{self._cfg.feed}/pypi/simple/{package}/" + response = self._http.request("GET", url, headers={"Accept": "text/html", **self._auth_header()}) + parser = _SimpleIndexParser() + parser.feed(response.data.decode("utf-8")) + + compatible_versions = set() + current_python = Version(".".join(map(str, sys.version_info[:3]))) + for link in parser.links: + href = link.get("href") + if not href or "data-yanked" in link: + continue + + # Compatibility is file-specific: one release can publish several + # distributions with different Python constraints. + requires_python = link.get("data-requires-python") + if requires_python: + try: + if current_python not in SpecifierSet(requires_python): + continue + except InvalidSpecifier: + logging.warning( + "Invalid python_requires %r for package %s", + requires_python, + package_name, + ) + continue + + version = self._version_from_link(href) + if version is not None: + # Several compatible wheels can represent the same release. + compatible_versions.add(version) + + return sorted(compatible_versions) + def _head_ok(self, url: str) -> bool: """Return True if a HEAD request to *url* resolves successfully (2xx).""" headers = self._auth_header() diff --git a/eng/tools/azure-sdk-tools/pypi_tools/pypi.py b/eng/tools/azure-sdk-tools/pypi_tools/pypi.py index 5af8eda8d717..d06d13e568a3 100644 --- a/eng/tools/azure-sdk-tools/pypi_tools/pypi.py +++ b/eng/tools/azure-sdk-tools/pypi_tools/pypi.py @@ -45,27 +45,16 @@ def __init__(self, host="https://pypi.org", force_pypi=False): ca_certs=os.getenv("REQUESTS_CA_BUNDLE", None), ) - def _pypi_http(self): - """Lazy PoolManager for pypi.org fallback when on AzDO backend.""" - if not hasattr(self, "_pypi_http_pool"): - self._pypi_http_pool = PoolManager( - retries=Retry(total=3, raise_on_status=True), - ca_certs=os.getenv("REQUESTS_CA_BUNDLE", None), - ) - return self._pypi_http_pool - def _pypi_json_request(self, path): - """GET from pypi.org JSON API, using the active backend's http pool if on pypi, else fallback.""" - if self._backend == "pypi": - url = "{host}{path}".format(host=self._host, path=path) - response = self._http.request("get", url) - else: - url = "https://pypi.org{path}".format(path=path) - response = self._pypi_http().request("get", url) + """GET from the configured public PyPI JSON endpoint.""" + if self._backend != "pypi": + raise NotImplementedError("PyPI JSON requests are unavailable against Azure Artifacts") + url = "{host}{path}".format(host=self._host, path=path) + response = self._http.request("get", url) return json.loads(response.data.decode("utf-8")) # ------------------------------------------------------------------ - # PyPI JSON endpoints (fall back to pypi.org when on AzDO backend) + # PyPI JSON endpoints # ------------------------------------------------------------------ def project(self, package_name): @@ -73,42 +62,52 @@ def project(self, package_name): raise NotImplementedError("project() is only available against pypi.org") return self._pypi_json_request("/pypi/{}/json".format(package_name)) - def project_release(self, package_name, version): - return self._pypi_json_request("/pypi/{}/{}/json".format(package_name, version)) - # ------------------------------------------------------------------ # Shared interface # ------------------------------------------------------------------ - def filter_packages_for_compatibility(self, package_name, version_set): + def filter_packages_for_compatibility(self, package_name, version_set, project=None): if self._backend != "pypi": raise NotImplementedError( "filter_packages_for_compatibility() requires pypi.org (needs requires_python metadata)" ) from packaging.specifiers import InvalidSpecifier, SpecifierSet + project = project or self.project(package_name) + releases = project.get("releases", {}) + current_python = parse(".".join(map(str, sys.version_info[:3]))) results: List[Version] = [] for version in version_set: - requires_python = self.project_release(package_name, version)["info"]["requires_python"] - if requires_python: + files = releases.get(str(version), []) + compatible = False + for release_file in files: + if release_file.get("yanked", False): + continue + requires_python = release_file.get("requires_python") + if not requires_python: + compatible = True + break try: - if parse(".".join(map(str, sys.version_info[:3]))) in SpecifierSet(requires_python): - results.append(version) + if current_python in SpecifierSet(requires_python): + compatible = True + break except InvalidSpecifier: - logging.warn(f"Invalid python_requires {requires_python!r} for package {package_name}=={version}") + logging.warning( + "Invalid python_requires %r for package %s==%s", + requires_python, + package_name, + version, + ) continue - else: + if compatible: results.append(version) return results def get_ordered_versions(self, package_name, filter_by_compatibility=False) -> List[Version]: if self._backend == "azdo": - versions = self._azdo.get_ordered_versions(package_name) if filter_by_compatibility: - logging.warning( - "filter_by_compatibility is not supported against Azure Artifacts; returning unfiltered versions" - ) - return versions + return self._azdo.get_python_compatible_versions(package_name) + return self._azdo.get_ordered_versions(package_name) project = self.project(package_name) versions: List[Version] = [] @@ -123,7 +122,7 @@ def get_ordered_versions(self, package_name, filter_by_compatibility=False) -> L versions.sort() if filter_by_compatibility: - return self.filter_packages_for_compatibility(package_name, versions) + return self.filter_packages_for_compatibility(package_name, versions, project) return versions diff --git a/eng/tools/azure-sdk-tools/tests/test_conflict_resolution.py b/eng/tools/azure-sdk-tools/tests/test_conflict_resolution.py index 4e505804d157..1a1e8f732e0c 100644 --- a/eng/tools/azure-sdk-tools/tests/test_conflict_resolution.py +++ b/eng/tools/azure-sdk-tools/tests/test_conflict_resolution.py @@ -1,11 +1,12 @@ -import pytest - +from types import SimpleNamespace from unittest.mock import patch from tempfile import TemporaryDirectory -from ci_tools.functions import resolve_compatible_package, is_package_compatible -from typing import Optional, List -from packaging.version import Version + +import pytest from packaging.requirements import Requirement +from packaging.version import Version + +from ci_tools.functions import handle_incompatible_minimum_dev_reqs, is_package_compatible @pytest.mark.parametrize( @@ -20,18 +21,82 @@ def test_incompatible_specifier(fake_package_input_requirements, immutable_requi assert result == expected_result -def test_identity_resolution(): - result = resolve_compatible_package( - "azure-identity", - [Requirement("azure-core>=1.28.0"), Requirement("isodate>=0.6.1"), Requirement("typing-extensions>=4.0.1")], +@pytest.fixture +def incompatible_local_package(): + return SimpleNamespace( + name="azure-identity", + version="1.26.0", + requires=["azure-core>=1.31.0"], ) - assert result == "azure-identity==1.16.0" -def test_resolution_no_requirement(): - result = resolve_compatible_package( - "azure-identity", - [Requirement("azure-core")], +def test_compatible_relative_requirement_stays_relative(): + local_package = SimpleNamespace(name="azure-identity", version="1.26.0", requires=["azure-core>=1.30.0"]) + with patch("ci_tools.functions.ParsedSetup.from_path", return_value=local_package): + result = handle_incompatible_minimum_dev_reqs( + "/repo/package", + ["../identity/azure-identity\n"], + [Requirement("azure-core==1.30.0")], + ) + + assert result == ["../identity/azure-identity"] + + +def test_incompatible_relative_requirement_uses_configured_index(incompatible_local_package): + with patch("ci_tools.functions.ParsedSetup.from_path", return_value=incompatible_local_package), patch( + "ci_tools.functions.PyPIClient.get_ordered_versions", + return_value=[Version("1.17.1")], + ) as get_versions: + result = handle_incompatible_minimum_dev_reqs( + "/repo/package", + ["../identity/azure-identity\n"], + [Requirement("azure-core==1.30.0")], + ) + + assert result == ["azure-identity"] + get_versions.assert_called_once_with("azure-identity", True) + + +def test_incompatible_unpublished_relative_requirement_stays_relative(incompatible_local_package): + with patch("ci_tools.functions.ParsedSetup.from_path", return_value=incompatible_local_package), patch( + "ci_tools.functions.PyPIClient.get_ordered_versions", + return_value=[], + ): + result = handle_incompatible_minimum_dev_reqs( + "/repo/package", + ["../identity/azure-identity\n"], + [Requirement("azure-core==1.30.0")], + ) + + assert result == ["../identity/azure-identity"] + + +def test_incompatible_local_wheel_uses_configured_index(tmp_path): + wheel_path = tmp_path / "azure_identity-1.26.0-py3-none-any.whl" + wheel_path.touch() + metadata = SimpleNamespace( + name="azure-identity", + version="1.26.0", + requires_dist=["azure-core>=1.31.0"], ) - assert result is not None - assert result.startswith("azure-identity==1.") + with patch("pkginfo.get_metadata", return_value=metadata), patch( + "ci_tools.functions.PyPIClient.get_ordered_versions", + return_value=[Version("1.17.1")], + ): + result = handle_incompatible_minimum_dev_reqs( + "/repo/package", + [str(wheel_path)], + [Requirement("azure-core==1.30.0")], + ) + + assert result == ["azure-identity"] + + +def test_standard_requirement_is_unchanged(): + result = handle_incompatible_minimum_dev_reqs( + "/repo/package", + ["pytest>=8\n"], + [Requirement("azure-core==1.30.0")], + ) + + assert result == ["pytest>=8\n"] diff --git a/eng/tools/azure-sdk-tools/tests/test_dependency_resolution.py b/eng/tools/azure-sdk-tools/tests/test_dependency_resolution.py new file mode 100644 index 000000000000..e0f11d6587ae --- /dev/null +++ b/eng/tools/azure-sdk-tools/tests/test_dependency_resolution.py @@ -0,0 +1,87 @@ +import subprocess +from unittest.mock import patch + +import pytest +from packaging.version import Version + +from ci_tools.scenario.dependency_resolution import install_packages, process_requirement + + +def test_minimum_selects_oldest_compatible_index_version(): + with patch( + "ci_tools.scenario.dependency_resolution.PyPIClient.get_ordered_versions", + return_value=[Version("1.0.0"), Version("1.1.0"), Version("2.0.0")], + ) as get_versions: + result = process_requirement("example-dependency>=1.1.0", "Minimum", "example-package") + + assert result == "example-dependency==1.1.0" + get_versions.assert_called_once_with("example-dependency", True) + + +def test_latest_selects_newest_compatible_index_version(): + with patch( + "ci_tools.scenario.dependency_resolution.PyPIClient.get_ordered_versions", + return_value=[Version("1.0.0"), Version("1.1.0"), Version("2.0.0")], + ): + result = process_requirement("example-dependency>=1.1.0", "Latest", "example-package") + + assert result == "example-dependency==2.0.0" + + +def test_prerelease_is_excluded_unless_requirement_allows_it(): + versions = [Version("1.0.0"), Version("2.0.0b1")] + with patch( + "ci_tools.scenario.dependency_resolution.PyPIClient.get_ordered_versions", + return_value=versions, + ): + stable_result = process_requirement("example-dependency>=1.0.0", "Latest", "example-package") + preview_result = process_requirement("example-dependency>=1.0.0b1", "Latest", "example-package") + + assert stable_result == "example-dependency==1.0.0" + assert preview_result == "example-dependency==2.0.0b1" + + +def test_install_passes_exact_pins_and_requirement_file_in_one_uv_command(): + with patch( + "ci_tools.scenario.dependency_resolution.get_pip_command", + return_value=["uv", "pip"], + ), patch("ci_tools.scenario.dependency_resolution.subprocess.check_call") as check_call: + install_packages( + ["azure-core==1.30.0"], + "/tmp/new_dev_requirements.txt", + "/tmp/python", + cwd="/repo/package", + ) + + check_call.assert_called_once_with( + [ + "uv", + "pip", + "install", + "--python", + "/tmp/python", + "azure-core==1.30.0", + "-r", + "/tmp/new_dev_requirements.txt", + ], + cwd="/repo/package", + ) + + +def test_unsatisfiable_install_propagates_resolver_failure(): + failure = subprocess.CalledProcessError(1, ["uv", "pip", "install"]) + with patch( + "ci_tools.scenario.dependency_resolution.get_pip_command", + return_value=["uv", "pip"], + ), patch( + "ci_tools.scenario.dependency_resolution.subprocess.check_call", + side_effect=failure, + ): + with pytest.raises(subprocess.CalledProcessError) as exc_info: + install_packages( + ["azure-core==1.30.0"], + "/tmp/new_dev_requirements.txt", + "/tmp/python", + ) + + assert exc_info.value is failure diff --git a/eng/tools/azure-sdk-tools/tests/test_pypi_client.py b/eng/tools/azure-sdk-tools/tests/test_pypi_client.py index 5d2e678d7b52..939a7d9a3ae8 100644 --- a/eng/tools/azure-sdk-tools/tests/test_pypi_client.py +++ b/eng/tools/azure-sdk-tools/tests/test_pypi_client.py @@ -1,6 +1,6 @@ from pypi_tools.pypi import PyPIClient, retrieve_versions_from_pypi from pypi_tools.azdo import AzureArtifactsClient, AzureArtifactsFeedConfig -from unittest.mock import patch +from unittest.mock import MagicMock, patch import os import pytest from packaging.version import Version @@ -239,24 +239,6 @@ def test_azdo_download_uri_returns_none_when_no_candidate_resolves(self): assert client.get_download_uri("example-pkg", "1.0.0") is None -# --------------------------------------------------------------------------- -# project_release — works on both backends (AzDO falls back to pypi.org) -# --------------------------------------------------------------------------- - - -class TestProjectRelease: - """Covers functions.py:888 usage of project_release() for requires_dist.""" - - @SKIP_IN_CI - def test_project_release_returns_version_info(self, client): - result = client.project_release(WELL_KNOWN_PACKAGE, WELL_KNOWN_VERSION) - - assert result["info"]["name"] == WELL_KNOWN_PACKAGE - assert result["info"]["release_url"] == f"https://pypi.org/project/{WELL_KNOWN_PACKAGE}/{WELL_KNOWN_VERSION}/" - # requires_dist is what the mindep resolver reads - assert "requires_dist" in result["info"] - - # --------------------------------------------------------------------------- # PyPI-only tests (project / filter_packages_for_compatibility) # --------------------------------------------------------------------------- @@ -278,14 +260,58 @@ def test_project_returns_info_and_releases(self): assert "1.25.1" in result["releases"] assert WELL_KNOWN_VERSION in result["releases"] - @SKIP_IN_CI - @patch("pypi_tools.pypi.sys") - def test_filter_packages_for_compatibility(self, mock_sys): - mock_sys.version_info = (2, 7, 0) + def test_filter_packages_for_compatibility_uses_release_file_metadata(self): client = _make_client(PYPI_HOST) - filtered = client.get_ordered_versions(WELL_KNOWN_PACKAGE, True) - unfiltered = client.get_ordered_versions(WELL_KNOWN_PACKAGE, False) - assert len(filtered) < len(unfiltered) + project_data = { + "releases": { + "1.0.0": [{"requires_python": ">=3.8", "yanked": False}], + "2.0.0": [{"requires_python": ">=99", "yanked": False}], + "3.0.0": [{"requires_python": None, "yanked": False}], + "4.0.0": [{"requires_python": "not-a-specifier", "yanked": False}], + "5.0.0": [{"requires_python": ">=3.8", "yanked": True}], + } + } + + with patch.object(PyPIClient, "project", return_value=project_data): + filtered = client.get_ordered_versions(WELL_KNOWN_PACKAGE, True) + + assert filtered == [Version("1.0.0"), Version("3.0.0")] + + def test_azdo_compatibility_filter_uses_simple_index(self): + client = _make_client(AZDO_FEED_URL) + with patch.object( + client._azdo, + "get_python_compatible_versions", + return_value=[Version("1.0.0")], + ) as get_compatible: + versions = client.get_ordered_versions("example-pkg", True) + + assert versions == [Version("1.0.0")] + get_compatible.assert_called_once_with("example-pkg") + + def test_azdo_backend_does_not_call_pypi_json(self): + client = _make_client(AZDO_FEED_URL) + with pytest.raises(NotImplementedError): + client._pypi_json_request("/pypi/example/json") + + +class TestAzureArtifactsCompatibility: + def test_simple_index_filters_requires_python_yanked_and_invalid_files(self): + client = AzureArtifactsClient(AzureArtifactsFeedConfig("org", "project", "feed")) + response = MagicMock() + response.data = b""" + + one + two + three + four + invalid + + """ + with patch.object(client._http, "request", return_value=response): + versions = client.get_python_compatible_versions("example-pkg") + + assert versions == [Version("1.0.0"), Version("3.0.0")] # --------------------------------------------------------------------------- diff --git a/sdk/cosmos/cosmos-emulator-internal-matrix.json b/sdk/cosmos/cosmos-emulator-internal-matrix.json index b870e761454b..7e6a930eff8b 100644 --- a/sdk/cosmos/cosmos-emulator-internal-matrix.json +++ b/sdk/cosmos/cosmos-emulator-internal-matrix.json @@ -75,31 +75,31 @@ "PythonVersion": "3.10", "CoverageArg": "--disablecov", "TestSamples": "false", - "ChecksOverride": "latestdependency" + "ChecksOverride": "latestdependency,mindependency" }, "Emulator Tests Python 3.11 Dependency Checks": { "PythonVersion": "3.11", "CoverageArg": "--disablecov", "TestSamples": "false", - "ChecksOverride": "latestdependency" + "ChecksOverride": "latestdependency,mindependency" }, "Emulator Tests Python 3.12 Dependency Checks": { "PythonVersion": "3.12", "CoverageArg": "--disablecov", "TestSamples": "false", - "ChecksOverride": "latestdependency" + "ChecksOverride": "latestdependency,mindependency" }, "Emulator Tests Python 3.13 Dependency Checks": { "PythonVersion": "3.13", "CoverageArg": "--disablecov", "TestSamples": "false", - "ChecksOverride": "latestdependency" + "ChecksOverride": "latestdependency,mindependency" }, "Emulator Tests Python 3.14 Dependency Checks": { "PythonVersion": "3.14", "CoverageArg": "--disablecov", "TestSamples": "false", - "ChecksOverride": "latestdependency" + "ChecksOverride": "latestdependency,mindependency" } } } diff --git a/sdk/cosmos/cosmos-emulator-public-matrix.json b/sdk/cosmos/cosmos-emulator-public-matrix.json index 1816bdcf2b0f..22689564ad7c 100644 --- a/sdk/cosmos/cosmos-emulator-public-matrix.json +++ b/sdk/cosmos/cosmos-emulator-public-matrix.json @@ -44,6 +44,31 @@ "CoverageArg": "--disablecov", "TestSamples": "false", "ChecksOverride": "whl,sdist" + }, + + "Emulator Tests Python 3.10 Dependency Checks": { + "PythonVersion": "3.10", + "CoverageArg": "--disablecov", + "TestSamples": "false", + "ChecksOverride": "mindependency" + }, + "Emulator Tests Python 3.11 Dependency Checks": { + "PythonVersion": "3.11", + "CoverageArg": "--disablecov", + "TestSamples": "false", + "ChecksOverride": "mindependency" + }, + "Emulator Tests Python 3.12 Dependency Checks": { + "PythonVersion": "3.12", + "CoverageArg": "--disablecov", + "TestSamples": "false", + "ChecksOverride": "mindependency" + }, + "Emulator Tests Python 3.13 Dependency Checks": { + "PythonVersion": "3.13", + "CoverageArg": "--disablecov", + "TestSamples": "false", + "ChecksOverride": "mindependency" } } }