2323PRECOMMIT_CONFIG = ".pre-commit-config.yaml"
2424PYPROJECTS = ["./pyproject.toml" , "./client/pyproject.toml" ]
2525TRACKED_DEPS = ("uv" , "ruff" , "ty" , "prek" )
26+ UV_REQUIRED_VERSION_PROJECTS = {"./pyproject.toml" }
2627
2728logger = logging .getLogger (__name__ )
2829logger .setLevel (logging .DEBUG )
@@ -52,10 +53,31 @@ def add_dep(deps: dict[str, list[dict[str, str]]], dep: str, file: str, group: s
5253 deps [pkg ].append ({"file" : file , "version" : parsed ["version" ], "group" : group })
5354
5455
55- def add_pyproject_deps (deps : dict [str , list [dict [str , str ]]], pyproject_path : str ) -> None :
56+ def uv_lower_bound (required_version : str , source : str , troubles : list [str ]) -> str | None :
57+ versions = re .findall (r"(?:^|,)\s*>=\s*(\d+(?:\.\d+){1,2})(?=\s*(?:,|$))" , required_version )
58+ if len (versions ) != 1 :
59+ troubles .append (
60+ f"{ source } : [tool.uv].required-version must have one >= lower bound, found { required_version !r} "
61+ )
62+ return None
63+ return versions [0 ]
64+
65+
66+ def add_pyproject_deps (deps : dict [str , list [dict [str , str ]]], pyproject_path : str , troubles : list [str ]) -> None :
5667 with Path (pyproject_path ).open ("rb" ) as f :
5768 pyproject : dict [str , Any ] = tomllib .load (f )
5869
70+ tool_config = pyproject .get ("tool" , {})
71+ uv_config = tool_config .get ("uv" ) if isinstance (tool_config , dict ) else None
72+ if pyproject_path in UV_REQUIRED_VERSION_PROJECTS or uv_config is not None :
73+ required_version = uv_config .get ("required-version" ) if isinstance (uv_config , dict ) else None
74+ if isinstance (required_version , str ):
75+ lower_bound = uv_lower_bound (required_version , pyproject_path , troubles )
76+ if lower_bound is not None :
77+ deps ["uv" ].append ({"file" : pyproject_path , "version" : f"=={ lower_bound } " , "group" : "required-version" })
78+ else :
79+ troubles .append (f"{ pyproject_path } : [tool.uv].required-version is missing" )
80+
5981 project = pyproject .get ("project" , {})
6082 if isinstance (project , dict ):
6183 dependencies = project .get ("dependencies" , [])
@@ -80,6 +102,8 @@ def add_pyproject_deps(deps: dict[str, list[dict[str, str]]], pyproject_path: st
80102
81103def main () -> None :
82104 deps : dict [str , list [dict [str , str ]]] = {dep : [] for dep in TRACKED_DEPS }
105+ troubles = []
106+ setup_action_calls = 0
83107
84108 for dockerfile in DOCKERFILES :
85109 dockerfile_content = Path (dockerfile ).read_text (encoding = "utf-8" )
@@ -105,20 +129,41 @@ def main() -> None:
105129 })
106130
107131 for pyproject_path in PYPROJECTS :
108- add_pyproject_deps (deps , pyproject_path )
132+ add_pyproject_deps (deps , pyproject_path , troubles )
109133
110134 for workflow_file in Path (".github/workflows" ).glob ("*.yml" ):
111- with workflow_file .open ("r" , encoding = "utf-8" ) as f :
112- workflow = yaml .safe_load (f ) or {}
113- env = workflow .get ("env" , {})
114- if "UV_VERSION" in env :
115- deps ["uv" ].append ({
116- "file" : str (workflow_file ),
117- "version" : f"=={ env ['UV_VERSION' ].lstrip ('v' )} " ,
118- "group" : "workflow" ,
119- })
135+ workflow_content = workflow_file .read_text (encoding = "utf-8" )
136+ if "astral-sh/setup-uv@" in workflow_content :
137+ troubles .append (f"{ workflow_file } : use the shared .github/actions/setup-uv action" )
138+ setup_action_calls += workflow_content .count ("./.github/actions/setup-uv" )
139+
140+ action_path = Path (".github/actions/setup-uv/action.yml" )
141+ action = yaml .safe_load (action_path .read_text (encoding = "utf-8" ))
142+ setup_steps = [
143+ step
144+ for step in action .get ("runs" , {}).get ("steps" , [])
145+ if str (step .get ("uses" , "" )).startswith ("astral-sh/setup-uv@" )
146+ ]
147+ if setup_action_calls == 0 :
148+ troubles .append (".github/workflows: no shared .github/actions/setup-uv calls found" )
149+ if len (setup_steps ) != 1 :
150+ troubles .append (f"{ action_path } : expected one astral-sh/setup-uv step" )
151+ else :
152+ setup = setup_steps [0 ].get ("with" , {})
153+ if (
154+ "version" in setup
155+ or setup .get ("version-file" ) != "pyproject.toml"
156+ or setup .get ("resolution-strategy" ) != "lowest"
157+ ):
158+ troubles .append (f"{ action_path } : install uv from pyproject.toml with resolution-strategy: lowest" )
159+ else :
160+ root = tomllib .loads (Path ("pyproject.toml" ).read_text (encoding = "utf-8" ))
161+ required = root .get ("tool" , {}).get ("uv" , {}).get ("required-version" )
162+ if isinstance (required , str ):
163+ lower_bound = uv_lower_bound (required , "pyproject.toml" , troubles )
164+ if lower_bound is not None :
165+ deps ["uv" ].append ({"file" : str (action_path ), "version" : f"=={ lower_bound } " , "group" : "setup-uv" })
120166
121- troubles = []
122167 for dep , versions in deps .items ():
123168 if not versions :
124169 troubles .append (f"{ dep } : no tracked version found" )
0 commit comments