Skip to content

Commit fb0ea16

Browse files
committed
Use uv for package management
1 parent 8a6dccc commit fb0ea16

File tree

15 files changed

+2855
-3351
lines changed

15 files changed

+2855
-3351
lines changed

.github/actions/poetry_setup/action.yml

Lines changed: 0 additions & 93 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# TODO: https://docs.astral.sh/uv/guides/integration/github/#caching
2+
3+
name: uv-install
4+
description: Set up Python and uv
5+
6+
inputs:
7+
python-version:
8+
description: Python version, supporting MAJOR.MINOR only
9+
required: true
10+
11+
env:
12+
UV_VERSION: "0.8.14"
13+
14+
runs:
15+
using: composite
16+
steps:
17+
- name: Install uv and set the python version
18+
uses: astral-sh/setup-uv@v6
19+
with:
20+
version: ${{ env.UV_VERSION }}
21+
python-version: ${{ inputs.python-version }}
Lines changed: 160 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,189 @@
1+
from collections import defaultdict
12
import sys
3+
from typing import Optional
24

3-
import tomllib
4-
from packaging.version import parse as parse_version
5-
import re
6-
7-
MIN_VERSION_LIBS = ["langchain-core"]
5+
if sys.version_info >= (3, 11):
6+
import tomllib
7+
else:
8+
# for python 3.10 and below, which doesnt have stdlib tomllib
9+
import tomli as tomllib
810

11+
from packaging.requirements import Requirement
12+
from packaging.specifiers import SpecifierSet
13+
from packaging.version import Version
914

10-
def get_min_version(version: str) -> str:
11-
# case ^x.x.x
12-
_match = re.match(r"^\^(\d+(?:\.\d+){0,2})$", version)
13-
if _match:
14-
return _match.group(1)
1515

16-
# case >=x.x.x,<y.y.y
17-
_match = re.match(r"^>=(\d+(?:\.\d+){0,2}),<(\d+(?:\.\d+){0,2})$", version)
18-
if _match:
19-
_min = _match.group(1)
20-
_max = _match.group(2)
21-
assert parse_version(_min) < parse_version(_max)
22-
return _min
16+
import requests
17+
from packaging.version import parse
18+
from typing import List
2319

24-
# case x.x.x
25-
_match = re.match(r"^(\d+(?:\.\d+){0,2})$", version)
26-
if _match:
27-
return _match.group(1)
20+
import re
2821

29-
raise ValueError(f"Unrecognized version format: {version}")
3022

23+
MIN_VERSION_LIBS = ["langchain-core"]
3124

32-
def get_min_version_from_toml(toml_path: str):
25+
# some libs only get checked on release because of simultaneous changes in
26+
# multiple libs
27+
SKIP_IF_PULL_REQUEST = ["langchain-core"]
28+
29+
30+
def get_pypi_versions(package_name: str) -> List[str]:
31+
"""
32+
Fetch all available versions for a package from PyPI.
33+
34+
Args:
35+
package_name (str): Name of the package
36+
37+
Returns:
38+
List[str]: List of all available versions
39+
40+
Raises:
41+
requests.exceptions.RequestException: If PyPI API request fails
42+
KeyError: If package not found or response format unexpected
43+
"""
44+
pypi_url = f"https://pypi.org/pypi/{package_name}/json"
45+
response = requests.get(pypi_url)
46+
response.raise_for_status()
47+
return list(response.json()["releases"].keys())
48+
49+
50+
def get_minimum_version(package_name: str, spec_string: str) -> Optional[str]:
51+
"""
52+
Find the minimum published version that satisfies the given constraints.
53+
54+
Args:
55+
package_name (str): Name of the package
56+
spec_string (str): Version specification string (e.g., ">=0.2.43,<0.4.0,!=0.3.0")
57+
58+
Returns:
59+
Optional[str]: Minimum compatible version or None if no compatible version found
60+
"""
61+
# rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
62+
spec_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", spec_string)
63+
# rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1 (can be anywhere in constraint string)
64+
for y in range(1, 10):
65+
spec_string = re.sub(rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}", spec_string)
66+
# rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
67+
for x in range(1, 10):
68+
spec_string = re.sub(
69+
rf"\^{x}\.(\d+)\.(\d+)", rf">={x}.\1.\2,<{x+1}", spec_string
70+
)
71+
72+
spec_set = SpecifierSet(spec_string)
73+
all_versions = get_pypi_versions(package_name)
74+
75+
valid_versions = []
76+
for version_str in all_versions:
77+
try:
78+
version = parse(version_str)
79+
if spec_set.contains(version):
80+
valid_versions.append(version)
81+
except ValueError:
82+
continue
83+
84+
return str(min(valid_versions)) if valid_versions else None
85+
86+
87+
def _check_python_version_from_requirement(
88+
requirement: Requirement, python_version: str
89+
) -> bool:
90+
if not requirement.marker:
91+
return True
92+
else:
93+
marker_str = str(requirement.marker)
94+
if "python_version" or "python_full_version" in marker_str:
95+
python_version_str = "".join(
96+
char
97+
for char in marker_str
98+
if char.isdigit() or char in (".", "<", ">", "=", ",")
99+
)
100+
return check_python_version(python_version, python_version_str)
101+
return True
102+
103+
104+
def get_min_version_from_toml(
105+
toml_path: str,
106+
versions_for: str,
107+
python_version: str,
108+
*,
109+
include: Optional[list] = None,
110+
):
33111
# Parse the TOML file
34112
with open(toml_path, "rb") as file:
35113
toml_data = tomllib.load(file)
36114

37-
# Get the dependencies from tool.poetry.dependencies
38-
dependencies = toml_data["tool"]["poetry"]["dependencies"]
115+
dependencies = defaultdict(list)
116+
for dep in toml_data["project"]["dependencies"]:
117+
requirement = Requirement(dep)
118+
dependencies[requirement.name].append(requirement)
39119

40120
# Initialize a dictionary to store the minimum versions
41121
min_versions = {}
42122

43123
# Iterate over the libs in MIN_VERSION_LIBS
44-
for lib in MIN_VERSION_LIBS:
124+
for lib in set(MIN_VERSION_LIBS + (include or [])):
125+
if versions_for == "pull_request" and lib in SKIP_IF_PULL_REQUEST:
126+
# some libs only get checked on release because of simultaneous
127+
# changes in multiple libs
128+
continue
45129
# Check if the lib is present in the dependencies
46130
if lib in dependencies:
47-
# Get the version string
48-
version_string = dependencies[lib]
131+
if include and lib not in include:
132+
continue
133+
requirements = dependencies[lib]
134+
for requirement in requirements:
135+
if _check_python_version_from_requirement(requirement, python_version):
136+
version_string = str(requirement.specifier)
137+
break
49138

50139
# Use parse_version to get the minimum supported version from version_string
51-
min_version = get_min_version(version_string)
140+
min_version = get_minimum_version(lib, version_string)
52141

53142
# Store the minimum version in the min_versions dictionary
54143
min_versions[lib] = min_version
55144

56145
return min_versions
57146

58147

59-
# Get the TOML file path from the command line argument
60-
toml_file = sys.argv[1]
61-
62-
# Call the function to get the minimum versions
63-
min_versions = get_min_version_from_toml(toml_file)
64-
65-
print(" ".join([f"{lib}=={version}" for lib, version in min_versions.items()]))
148+
def check_python_version(version_string, constraint_string):
149+
"""
150+
Check if the given Python version matches the given constraints.
151+
152+
:param version_string: A string representing the Python version (e.g. "3.8.5").
153+
:param constraint_string: A string representing the package's Python version constraints (e.g. ">=3.6, <4.0").
154+
:return: True if the version matches the constraints, False otherwise.
155+
"""
156+
157+
# rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
158+
constraint_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", constraint_string)
159+
# rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1.0 (can be anywhere in constraint string)
160+
for y in range(1, 10):
161+
constraint_string = re.sub(
162+
rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}.0", constraint_string
163+
)
164+
# rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
165+
for x in range(1, 10):
166+
constraint_string = re.sub(
167+
rf"\^{x}\.0\.(\d+)", rf">={x}.0.\1,<{x+1}.0.0", constraint_string
168+
)
169+
170+
try:
171+
version = Version(version_string)
172+
constraints = SpecifierSet(constraint_string)
173+
return version in constraints
174+
except Exception as e:
175+
print(f"Error: {e}")
176+
return False
177+
178+
179+
if __name__ == "__main__":
180+
# Get the TOML file path from the command line argument
181+
toml_file = sys.argv[1]
182+
versions_for = sys.argv[2]
183+
python_version = sys.argv[3]
184+
assert versions_for in ["release", "pull_request"]
185+
186+
# Call the function to get the minimum versions
187+
min_versions = get_min_version_from_toml(toml_file, versions_for, python_version)
188+
189+
print(" ".join([f"{lib}=={version}" for lib, version in min_versions.items()]))

.github/workflows/_compile_integration_test.yml

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
description: "From which folder this pipeline executes"
1010

1111
env:
12-
POETRY_VERSION: "1.7.1"
12+
UV_FROZEN: "true"
1313

1414
jobs:
1515
build:
@@ -21,28 +21,23 @@ jobs:
2121
matrix:
2222
python-version:
2323
- "3.9"
24-
- "3.10"
25-
- "3.11"
26-
- "3.12"
27-
name: "poetry run pytest -m compile tests/integration_tests #${{ matrix.python-version }}"
24+
- "3.13"
25+
name: "uv run pytest -m compile tests/integration_tests"
2826
steps:
2927
- uses: actions/checkout@v4
3028

31-
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
32-
uses: "./.github/actions/poetry_setup"
29+
- name: Set up Python ${{ matrix.python-version }} + uv
30+
uses: "./.github/actions/uv_setup"
3331
with:
3432
python-version: ${{ matrix.python-version }}
35-
poetry-version: ${{ env.POETRY_VERSION }}
36-
working-directory: ${{ inputs.working-directory }}
37-
cache-key: compile-integration
3833

3934
- name: Install integration dependencies
4035
shell: bash
41-
run: poetry install --with=test_integration,test
36+
run: uv sync --group test --group test_integration
4237

4338
- name: Check integration tests compile
4439
shell: bash
45-
run: poetry run pytest -m compile tests/integration_tests
40+
run: uv run pytest -m compile tests/integration_tests
4641

4742
- name: Ensure the tests did not create any additional files
4843
shell: bash

0 commit comments

Comments
 (0)