Skip to content

Commit 9b5490b

Browse files
Remove Upper Constraint for agent hosting package
1 parent 142a299 commit 9b5490b

File tree

4 files changed

+261
-11
lines changed

4 files changed

+261
-11
lines changed

libraries/microsoft-agents-a365-observability-hosting/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525
license = {text = "MIT"}
2626
keywords = ["observability", "telemetry", "tracing", "opentelemetry", "monitoring", "ai", "agents", "hosting"]
2727
dependencies = [
28-
"microsoft-agents-hosting-core >= 0.4.0, < 0.6.0",
28+
"microsoft-agents-hosting-core >= 0.4.0",
2929
"microsoft-agents-a365-observability-core >= 0.0.0",
3030
"opentelemetry-api >= 1.36.0",
3131
]

libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ classifiers = [
2424
license = {text = "MIT"}
2525
dependencies = [
2626
"microsoft-agents-a365-tooling >= 0.0.0",
27-
"microsoft-agents-hosting-core >= 0.4.0, < 0.6.0",
27+
"microsoft-agents-hosting-core >= 0.4.0",
2828
"agent-framework-azure-ai >= 1.0.0b251114",
2929
"azure-identity >= 1.12.0",
3030
"typing-extensions >= 4.0.0",
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Tests for validating dependency version constraints across all packages.
6+
7+
This test ensures that upper bound version constraints (e.g., < 0.6.0) on dependencies
8+
do not become incompatible with the latest published versions of those packages.
9+
10+
Background: We encountered an issue where microsoft-agents-a365-tooling had a constraint
11+
`microsoft-agents-hosting-core >= 0.4.0, < 0.6.0` but the samples used 0.7.0, causing
12+
the package resolver to silently pick older versions instead of the latest.
13+
"""
14+
15+
import re
16+
import tomllib
17+
from pathlib import Path
18+
from typing import Optional
19+
20+
import pytest
21+
22+
# Packages in this repo that should be checked for version constraint issues
23+
INTERNAL_PACKAGES = {
24+
"microsoft-agents-a365-tooling",
25+
"microsoft-agents-a365-tooling-extensions-openai",
26+
"microsoft-agents-a365-tooling-extensions-agentframework",
27+
"microsoft-agents-a365-tooling-extensions-semantickernel",
28+
"microsoft-agents-a365-tooling-extensions-azureaifoundry",
29+
"microsoft-agents-a365-observability-core",
30+
"microsoft-agents-a365-observability-extensions-openai",
31+
"microsoft-agents-a365-observability-extensions-agent-framework",
32+
"microsoft-agents-a365-observability-extensions-semantickernel",
33+
"microsoft-agents-a365-runtime",
34+
"microsoft-agents-a365-notifications",
35+
}
36+
37+
# Known external packages where we should be careful about upper bounds
38+
EXTERNAL_PACKAGES_TO_CHECK = {
39+
"microsoft-agents-hosting-core",
40+
"microsoft-agents-hosting-aiohttp",
41+
"microsoft-agents-authentication-msal",
42+
"microsoft-agents-activity",
43+
}
44+
45+
46+
def get_repo_root() -> Path:
47+
"""Get the root directory of the Agent365-python repository."""
48+
current = Path(__file__).resolve()
49+
# Navigate up to find the repo root (contains 'libraries' folder)
50+
for parent in current.parents:
51+
if (parent / "libraries").is_dir():
52+
return parent
53+
raise RuntimeError("Could not find repository root")
54+
55+
56+
def find_all_pyproject_files() -> list[Path]:
57+
"""Find all pyproject.toml files in the libraries directory."""
58+
repo_root = get_repo_root()
59+
libraries_dir = repo_root / "libraries"
60+
return list(libraries_dir.glob("**/pyproject.toml"))
61+
62+
63+
def parse_version_constraint(constraint: str) -> dict:
64+
"""
65+
Parse a version constraint string and extract bounds.
66+
67+
Examples:
68+
">= 0.4.0, < 0.6.0" -> {"lower": "0.4.0", "upper": "0.6.0", "upper_inclusive": False}
69+
">= 0.4.0" -> {"lower": "0.4.0", "upper": None}
70+
"""
71+
result = {"lower": None, "upper": None, "upper_inclusive": False, "raw": constraint}
72+
73+
# Match upper bound patterns: < X.Y.Z or <= X.Y.Z
74+
upper_match = re.search(r'<\s*=?\s*(\d+\.\d+\.\d+)', constraint)
75+
if upper_match:
76+
result["upper"] = upper_match.group(1)
77+
result["upper_inclusive"] = "<=" in constraint[:upper_match.start() + 2]
78+
79+
# Match lower bound patterns: >= X.Y.Z or > X.Y.Z
80+
lower_match = re.search(r'>=?\s*(\d+\.\d+\.\d+)', constraint)
81+
if lower_match:
82+
result["lower"] = lower_match.group(1)
83+
84+
return result
85+
86+
87+
def version_tuple(version: str) -> tuple:
88+
"""Convert version string to tuple for comparison."""
89+
# Handle pre-release versions like "0.2.1.dev2"
90+
base_version = version.split(".dev")[0].split("a")[0].split("b")[0].split("rc")[0]
91+
parts = base_version.split(".")
92+
return tuple(int(p) for p in parts)
93+
94+
95+
def is_version_compatible(version: str, upper_bound: str, inclusive: bool = False) -> bool:
96+
"""Check if a version is compatible with an upper bound constraint."""
97+
version_t = version_tuple(version)
98+
upper_t = version_tuple(upper_bound)
99+
100+
if inclusive:
101+
return version_t <= upper_t
102+
return version_t < upper_t
103+
104+
105+
def get_dependencies_with_upper_bounds(pyproject_path: Path) -> list[dict]:
106+
"""
107+
Extract dependencies that have upper bound constraints.
108+
109+
Returns a list of dicts with:
110+
- package: package name
111+
- constraint: parsed constraint info
112+
- file: path to pyproject.toml
113+
"""
114+
with open(pyproject_path, "rb") as f:
115+
data = tomllib.load(f)
116+
117+
dependencies = data.get("project", {}).get("dependencies", [])
118+
results = []
119+
120+
for dep in dependencies:
121+
# Parse dependency string: "package-name >= 1.0.0, < 2.0.0"
122+
match = re.match(r'^([\w\-]+)\s*(.*)$', dep.strip())
123+
if not match:
124+
continue
125+
126+
package_name = match.group(1)
127+
constraint_str = match.group(2).strip()
128+
129+
if not constraint_str:
130+
continue
131+
132+
constraint = parse_version_constraint(constraint_str)
133+
134+
if constraint["upper"]:
135+
results.append({
136+
"package": package_name,
137+
"constraint": constraint,
138+
"file": pyproject_path,
139+
})
140+
141+
return results
142+
143+
144+
class TestDependencyConstraints:
145+
"""Tests for dependency version constraints."""
146+
147+
def test_no_restrictive_upper_bounds_on_external_packages(self):
148+
"""
149+
Ensure we don't have overly restrictive upper bounds on external packages.
150+
151+
Upper bounds like `< 0.6.0` can cause issues when the external package
152+
releases a newer version (e.g., 0.7.0) that our samples depend on.
153+
This causes the resolver to silently pick older versions of our packages.
154+
"""
155+
pyproject_files = find_all_pyproject_files()
156+
issues = []
157+
158+
for pyproject_path in pyproject_files:
159+
deps_with_upper = get_dependencies_with_upper_bounds(pyproject_path)
160+
161+
for dep in deps_with_upper:
162+
package = dep["package"]
163+
164+
# Check if this is an external package we should monitor
165+
if package in EXTERNAL_PACKAGES_TO_CHECK:
166+
constraint = dep["constraint"]
167+
relative_path = pyproject_path.relative_to(get_repo_root())
168+
169+
issues.append(
170+
f" - {relative_path}: '{package}' has upper bound constraint "
171+
f"'{constraint['raw']}'. This may cause resolver issues when "
172+
f"newer versions are released."
173+
)
174+
175+
if issues:
176+
pytest.fail(
177+
"Found dependencies with upper bound constraints that may cause issues:\n"
178+
+ "\n".join(issues)
179+
+ "\n\nConsider removing upper bounds or using a more permissive constraint. "
180+
"Upper bounds on external packages can cause our packages to be downgraded "
181+
"when newer versions of the external package are released."
182+
)
183+
184+
def test_internal_package_constraints_are_flexible(self):
185+
"""
186+
Ensure internal packages don't have restrictive upper bounds on each other.
187+
188+
We want internal packages to be able to evolve together without
189+
version constraint conflicts.
190+
"""
191+
pyproject_files = find_all_pyproject_files()
192+
issues = []
193+
194+
for pyproject_path in pyproject_files:
195+
deps_with_upper = get_dependencies_with_upper_bounds(pyproject_path)
196+
197+
for dep in deps_with_upper:
198+
package = dep["package"]
199+
200+
# Check if this is an internal package
201+
if package in INTERNAL_PACKAGES:
202+
constraint = dep["constraint"]
203+
relative_path = pyproject_path.relative_to(get_repo_root())
204+
205+
issues.append(
206+
f" - {relative_path}: '{package}' has upper bound constraint "
207+
f"'{constraint['raw']}'. Internal packages should not have "
208+
"upper bounds on each other."
209+
)
210+
211+
if issues:
212+
pytest.fail(
213+
"Found internal packages with upper bound constraints:\n"
214+
+ "\n".join(issues)
215+
+ "\n\nInternal packages should use '>= X.Y.Z' without upper bounds "
216+
"to allow them to evolve together."
217+
)
218+
219+
def test_parse_version_constraint(self):
220+
"""Test the version constraint parser."""
221+
# Test with upper and lower bounds
222+
result = parse_version_constraint(">= 0.4.0, < 0.6.0")
223+
assert result["lower"] == "0.4.0"
224+
assert result["upper"] == "0.6.0"
225+
assert result["upper_inclusive"] is False
226+
227+
# Test with only lower bound
228+
result = parse_version_constraint(">= 1.0.0")
229+
assert result["lower"] == "1.0.0"
230+
assert result["upper"] is None
231+
232+
# Test with inclusive upper bound
233+
result = parse_version_constraint(">= 2.0.0, <= 3.0.0")
234+
assert result["lower"] == "2.0.0"
235+
assert result["upper"] == "3.0.0"
236+
assert result["upper_inclusive"] is True
237+
238+
def test_version_compatibility_check(self):
239+
"""Test version compatibility checking."""
240+
# 0.7.0 is NOT compatible with < 0.6.0
241+
assert is_version_compatible("0.7.0", "0.6.0", inclusive=False) is False
242+
243+
# 0.5.9 IS compatible with < 0.6.0
244+
assert is_version_compatible("0.5.9", "0.6.0", inclusive=False) is True
245+
246+
# 0.6.0 IS compatible with <= 0.6.0
247+
assert is_version_compatible("0.6.0", "0.6.0", inclusive=True) is True
248+
249+
# 0.6.0 is NOT compatible with < 0.6.0
250+
assert is_version_compatible("0.6.0", "0.6.0", inclusive=False) is False

uv.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)