1515import re
1616import tomllib
1717from pathlib import Path
18- from typing import Optional
1918
2019import pytest
2120
@@ -63,24 +62,24 @@ def find_all_pyproject_files() -> list[Path]:
6362def parse_version_constraint (constraint : str ) -> dict :
6463 """
6564 Parse a version constraint string and extract bounds.
66-
65+
6766 Examples:
6867 ">= 0.4.0, < 0.6.0" -> {"lower": "0.4.0", "upper": "0.6.0", "upper_inclusive": False}
6968 ">= 0.4.0" -> {"lower": "0.4.0", "upper": None}
7069 """
7170 result = {"lower" : None , "upper" : None , "upper_inclusive" : False , "raw" : constraint }
72-
71+
7372 # Match upper bound patterns: < X.Y.Z or <= X.Y.Z
7473 upper_match = re .search (r'<\s*=?\s*(\d+\.\d+\.\d+)' , constraint )
7574 if upper_match :
7675 result ["upper" ] = upper_match .group (1 )
7776 result ["upper_inclusive" ] = "<=" in constraint [:upper_match .start () + 2 ]
78-
77+
7978 # Match lower bound patterns: >= X.Y.Z or > X.Y.Z
8079 lower_match = re .search (r'>=?\s*(\d+\.\d+\.\d+)' , constraint )
8180 if lower_match :
8281 result ["lower" ] = lower_match .group (1 )
83-
82+
8483 return result
8584
8685
@@ -96,7 +95,7 @@ def is_version_compatible(version: str, upper_bound: str, inclusive: bool = Fals
9695 """Check if a version is compatible with an upper bound constraint."""
9796 version_t = version_tuple (version )
9897 upper_t = version_tuple (upper_bound )
99-
98+
10099 if inclusive :
101100 return version_t <= upper_t
102101 return version_t < upper_t
@@ -105,39 +104,39 @@ def is_version_compatible(version: str, upper_bound: str, inclusive: bool = Fals
105104def get_dependencies_with_upper_bounds (pyproject_path : Path ) -> list [dict ]:
106105 """
107106 Extract dependencies that have upper bound constraints.
108-
107+
109108 Returns a list of dicts with:
110109 - package: package name
111110 - constraint: parsed constraint info
112111 - file: path to pyproject.toml
113112 """
114113 with open (pyproject_path , "rb" ) as f :
115114 data = tomllib .load (f )
116-
115+
117116 dependencies = data .get ("project" , {}).get ("dependencies" , [])
118117 results = []
119-
118+
120119 for dep in dependencies :
121120 # Parse dependency string: "package-name >= 1.0.0, < 2.0.0"
122121 match = re .match (r'^([\w\-]+)\s*(.*)$' , dep .strip ())
123122 if not match :
124123 continue
125-
124+
126125 package_name = match .group (1 )
127126 constraint_str = match .group (2 ).strip ()
128-
127+
129128 if not constraint_str :
130129 continue
131-
130+
132131 constraint = parse_version_constraint (constraint_str )
133-
132+
134133 if constraint ["upper" ]:
135134 results .append ({
136135 "package" : package_name ,
137136 "constraint" : constraint ,
138137 "file" : pyproject_path ,
139138 })
140-
139+
141140 return results
142141
143142
@@ -147,31 +146,31 @@ class TestDependencyConstraints:
147146 def test_no_restrictive_upper_bounds_on_external_packages (self ):
148147 """
149148 Ensure we don't have overly restrictive upper bounds on external packages.
150-
149+
151150 Upper bounds like `< 0.6.0` can cause issues when the external package
152151 releases a newer version (e.g., 0.7.0) that our samples depend on.
153152 This causes the resolver to silently pick older versions of our packages.
154153 """
155154 pyproject_files = find_all_pyproject_files ()
156155 issues = []
157-
156+
158157 for pyproject_path in pyproject_files :
159158 deps_with_upper = get_dependencies_with_upper_bounds (pyproject_path )
160-
159+
161160 for dep in deps_with_upper :
162161 package = dep ["package" ]
163-
162+
164163 # Check if this is an external package we should monitor
165164 if package in EXTERNAL_PACKAGES_TO_CHECK :
166165 constraint = dep ["constraint" ]
167166 relative_path = pyproject_path .relative_to (get_repo_root ())
168-
167+
169168 issues .append (
170169 f" - { relative_path } : '{ package } ' has upper bound constraint "
171170 f"'{ constraint ['raw' ]} '. This may cause resolver issues when "
172171 f"newer versions are released."
173172 )
174-
173+
175174 if issues :
176175 pytest .fail (
177176 "Found dependencies with upper bound constraints that may cause issues:\n "
@@ -184,30 +183,30 @@ def test_no_restrictive_upper_bounds_on_external_packages(self):
184183 def test_internal_package_constraints_are_flexible (self ):
185184 """
186185 Ensure internal packages don't have restrictive upper bounds on each other.
187-
186+
188187 We want internal packages to be able to evolve together without
189188 version constraint conflicts.
190189 """
191190 pyproject_files = find_all_pyproject_files ()
192191 issues = []
193-
192+
194193 for pyproject_path in pyproject_files :
195194 deps_with_upper = get_dependencies_with_upper_bounds (pyproject_path )
196-
195+
197196 for dep in deps_with_upper :
198197 package = dep ["package" ]
199-
198+
200199 # Check if this is an internal package
201200 if package in INTERNAL_PACKAGES :
202201 constraint = dep ["constraint" ]
203202 relative_path = pyproject_path .relative_to (get_repo_root ())
204-
203+
205204 issues .append (
206205 f" - { relative_path } : '{ package } ' has upper bound constraint "
207206 f"'{ constraint ['raw' ]} '. Internal packages should not have "
208207 "upper bounds on each other."
209208 )
210-
209+
211210 if issues :
212211 pytest .fail (
213212 "Found internal packages with upper bound constraints:\n "
@@ -223,12 +222,12 @@ def test_parse_version_constraint(self):
223222 assert result ["lower" ] == "0.4.0"
224223 assert result ["upper" ] == "0.6.0"
225224 assert result ["upper_inclusive" ] is False
226-
225+
227226 # Test with only lower bound
228227 result = parse_version_constraint (">= 1.0.0" )
229228 assert result ["lower" ] == "1.0.0"
230229 assert result ["upper" ] is None
231-
230+
232231 # Test with inclusive upper bound
233232 result = parse_version_constraint (">= 2.0.0, <= 3.0.0" )
234233 assert result ["lower" ] == "2.0.0"
@@ -239,12 +238,12 @@ def test_version_compatibility_check(self):
239238 """Test version compatibility checking."""
240239 # 0.7.0 is NOT compatible with < 0.6.0
241240 assert is_version_compatible ("0.7.0" , "0.6.0" , inclusive = False ) is False
242-
241+
243242 # 0.5.9 IS compatible with < 0.6.0
244243 assert is_version_compatible ("0.5.9" , "0.6.0" , inclusive = False ) is True
245-
244+
246245 # 0.6.0 IS compatible with <= 0.6.0
247246 assert is_version_compatible ("0.6.0" , "0.6.0" , inclusive = True ) is True
248-
247+
249248 # 0.6.0 is NOT compatible with < 0.6.0
250249 assert is_version_compatible ("0.6.0" , "0.6.0" , inclusive = False ) is False
0 commit comments