diff --git a/.github/scripts/get_min_versions.py b/.github/scripts/get_min_versions.py index f91dd00ee06d9..0838a6e7e3957 100644 --- a/.github/scripts/get_min_versions.py +++ b/.github/scripts/get_min_versions.py @@ -7,12 +7,17 @@ # for python 3.10 and below, which doesnt have stdlib tomllib import tomli as tomllib -from packaging.version import parse as parse_version from packaging.specifiers import SpecifierSet from packaging.version import Version + +import requests +from packaging.version import parse +from typing import List + import re + MIN_VERSION_LIBS = [ "langchain-core", "langchain-community", @@ -31,29 +36,61 @@ ] -def get_min_version(version: str) -> str: - # base regex for x.x.x with cases for rc/post/etc - # valid strings: https://peps.python.org/pep-0440/#public-version-identifiers - vstring = r"\d+(?:\.\d+){0,2}(?:(?:a|b|rc|\.post|\.dev)\d+)?" - # case ^x.x.x - _match = re.match(f"^\\^({vstring})$", version) - if _match: - return _match.group(1) +def get_pypi_versions(package_name: str) -> List[str]: + """ + Fetch all available versions for a package from PyPI. + + Args: + package_name (str): Name of the package + + Returns: + List[str]: List of all available versions - # case >=x.x.x,=({vstring}),<({vstring})$", version) - if _match: - _min = _match.group(1) - _max = _match.group(2) - assert parse_version(_min) < parse_version(_max) - return _min + Raises: + requests.exceptions.RequestException: If PyPI API request fails + KeyError: If package not found or response format unexpected + """ + pypi_url = f"https://pypi.org/pypi/{package_name}/json" + response = requests.get(pypi_url) + response.raise_for_status() + return list(response.json()["releases"].keys()) - # case x.x.x - _match = re.match(f"^({vstring})$", version) - if _match: - return _match.group(1) - raise ValueError(f"Unrecognized version format: {version}") +def get_minimum_version(package_name: str, spec_string: str) -> Optional[str]: + """ + Find the minimum published version that satisfies the given constraints. + + Args: + package_name (str): Name of the package + spec_string (str): Version specification string (e.g., ">=0.2.43,<0.4.0,!=0.3.0") + + Returns: + Optional[str]: Minimum compatible version or None if no compatible version found + """ + # rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string) + spec_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", spec_string) + # rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1 (can be anywhere in constraint string) + for y in range(1, 10): + spec_string = re.sub(rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}", spec_string) + # rewrite occurrences of ^x.y.z to >=x.y.z,={x}.\1.\2,<{x+1}", spec_string + ) + + spec_set = SpecifierSet(spec_string) + all_versions = get_pypi_versions(package_name) + + valid_versions = [] + for version_str in all_versions: + try: + version = parse(version_str) + if spec_set.contains(version): + valid_versions.append(version) + except ValueError: + continue + + return str(min(valid_versions)) if valid_versions else None def get_min_version_from_toml( @@ -96,7 +133,7 @@ def get_min_version_from_toml( ][0]["version"] # Use parse_version to get the minimum supported version from version_string - min_version = get_min_version(version_string) + min_version = get_minimum_version(lib, version_string) # Store the minimum version in the min_versions dictionary min_versions[lib] = min_version @@ -112,6 +149,20 @@ def check_python_version(version_string, constraint_string): :param constraint_string: A string representing the package's Python version constraints (e.g. ">=3.6, <4.0"). :return: True if the version matches the constraints, False otherwise. """ + + # rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string) + constraint_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", constraint_string) + # rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1.0 (can be anywhere in constraint string) + for y in range(1, 10): + constraint_string = re.sub( + rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}.0", constraint_string + ) + # rewrite occurrences of ^x.y.z to >=x.y.z,={x}.0.\1,<{x+1}.0.0", constraint_string + ) + try: version = Version(version_string) constraints = SpecifierSet(constraint_string) diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index 574cdaa037b0f..7eaa9410beb06 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -247,7 +247,7 @@ jobs: working-directory: ${{ inputs.working-directory }} id: min-version run: | - poetry run pip install packaging + poetry run pip install packaging requests python_version="$(poetry run python --version | awk '{print $2}')" min_versions="$(poetry run python $GITHUB_WORKSPACE/.github/scripts/get_min_versions.py pyproject.toml release $python_version)" echo "min-versions=$min_versions" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 9d01508367e0d..0e03bbb16aece 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -47,7 +47,7 @@ jobs: id: min-version shell: bash run: | - poetry run pip install packaging tomli + poetry run pip install packaging tomli requests python_version="$(poetry run python --version | awk '{print $2}')" min_versions="$(poetry run python $GITHUB_WORKSPACE/.github/scripts/get_min_versions.py pyproject.toml pull_request $python_version)" echo "min-versions=$min_versions" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/check_diffs.yml b/.github/workflows/check_diffs.yml index b5729611645c6..5caa9c9348b12 100644 --- a/.github/workflows/check_diffs.yml +++ b/.github/workflows/check_diffs.yml @@ -31,7 +31,7 @@ jobs: uses: Ana06/get-changed-files@v2.2.0 - id: set-matrix run: | - python -m pip install packaging + python -m pip install packaging requests python .github/scripts/check_diff.py ${{ steps.files.outputs.all }} >> $GITHUB_OUTPUT outputs: lint: ${{ steps.set-matrix.outputs.lint }}