fc-data 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datasmith/__init__.py +330 -0
- datasmith/__init__.pyi +194 -0
- datasmith/agents/__init__.py +31 -0
- datasmith/agents/classifiers.py +272 -0
- datasmith/agents/codex.py +25 -0
- datasmith/agents/config.py +108 -0
- datasmith/agents/extractors.py +197 -0
- datasmith/agents/installed/README.md +52 -0
- datasmith/agents/installed/__init__.py +22 -0
- datasmith/agents/installed/base.py +240 -0
- datasmith/agents/installed/claude.py +134 -0
- datasmith/agents/installed/codex.py +91 -0
- datasmith/agents/installed/gemini.py +118 -0
- datasmith/agents/installed/none.py +27 -0
- datasmith/agents/sandbox.py +547 -0
- datasmith/agents/synthesizer.py +439 -0
- datasmith/agents/templates/AGENTS.md.j2 +150 -0
- datasmith/agents/templates/sandbox_verify.py +428 -0
- datasmith/docker/__init__.py +31 -0
- datasmith/docker/context.py +112 -0
- datasmith/docker/images.py +158 -0
- datasmith/docker/publish.py +56 -0
- datasmith/docker/templates/Dockerfile.base +26 -0
- datasmith/docker/templates/Dockerfile.pr +42 -0
- datasmith/docker/templates/Dockerfile.repo +11 -0
- datasmith/docker/templates/docker_build_base.sh +780 -0
- datasmith/docker/templates/docker_build_env.sh +309 -0
- datasmith/docker/templates/docker_build_final.sh +106 -0
- datasmith/docker/templates/docker_build_pkg.sh +99 -0
- datasmith/docker/templates/docker_build_run.sh +124 -0
- datasmith/docker/templates/entrypoint.sh +62 -0
- datasmith/docker/templates/parser.py +1405 -0
- datasmith/docker/templates/profile.sh +199 -0
- datasmith/docker/templates/pytest_runner.py +692 -0
- datasmith/docker/templates/run-tests.sh +197 -0
- datasmith/docker/verifiers.py +131 -0
- datasmith/filters.py +154 -0
- datasmith/github/__init__.py +22 -0
- datasmith/github/client.py +333 -0
- datasmith/github/hooks.py +50 -0
- datasmith/github/links.py +110 -0
- datasmith/github/models.py +206 -0
- datasmith/github/render.py +173 -0
- datasmith/github/search.py +66 -0
- datasmith/github/templates/comment.md.j2 +5 -0
- datasmith/github/templates/final.md.j2 +66 -0
- datasmith/github/templates/issues.md.j2 +21 -0
- datasmith/github/templates/repo.md.j2 +1 -0
- datasmith/preflight.py +162 -0
- datasmith/publish/__init__.py +13 -0
- datasmith/publish/huggingface.py +104 -0
- datasmith/publish/pipeline.py +60 -0
- datasmith/publish/records.py +91 -0
- datasmith/py.typed +1 -0
- datasmith/resolution/__init__.py +14 -0
- datasmith/resolution/blocklist.py +145 -0
- datasmith/resolution/cache.py +120 -0
- datasmith/resolution/constants.py +277 -0
- datasmith/resolution/dependency_resolver.py +174 -0
- datasmith/resolution/git_utils.py +378 -0
- datasmith/resolution/import_analyzer.py +66 -0
- datasmith/resolution/metadata_parser.py +412 -0
- datasmith/resolution/models.py +41 -0
- datasmith/resolution/orchestrator.py +522 -0
- datasmith/resolution/package_filters.py +312 -0
- datasmith/resolution/python_manager.py +110 -0
- datasmith/runners/__init__.py +15 -0
- datasmith/runners/base.py +112 -0
- datasmith/runners/classify_prs.py +48 -0
- datasmith/runners/render_problems.py +113 -0
- datasmith/runners/resolve_packages.py +66 -0
- datasmith/runners/scrape_commits.py +166 -0
- datasmith/runners/scrape_repos.py +44 -0
- datasmith/runners/synthesize_images.py +310 -0
- datasmith/update/__init__.py +5 -0
- datasmith/update/cli.py +169 -0
- datasmith/update/offline.py +173 -0
- datasmith/update/pipeline.py +497 -0
- datasmith/utils/__init__.py +18 -0
- datasmith/utils/core.py +67 -0
- datasmith/utils/db.py +156 -0
- datasmith/utils/tokens.py +65 -0
- fc_data-0.2.0.dist-info/METADATA +441 -0
- fc_data-0.2.0.dist-info/RECORD +87 -0
- fc_data-0.2.0.dist-info/WHEEL +4 -0
- fc_data-0.2.0.dist-info/entry_points.txt +2 -0
- fc_data-0.2.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Parsing metadata from packaging files (pyproject.toml, setup.cfg, requirements.txt, etc.)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import configparser
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import tomllib as _toml # type: ignore[import-not-found,unused-ignore]
|
|
13
|
+
except ImportError:
|
|
14
|
+
import tomli as _toml # type: ignore[no-redef,import-not-found,unused-ignore]
|
|
15
|
+
|
|
16
|
+
from git import Commit
|
|
17
|
+
|
|
18
|
+
from .constants import ENV_YML_NAMES, PYPROJECT, REQ_TXT_REGEX, SETUP_CFG, SETUP_PY
|
|
19
|
+
from .git_utils import materialize_blobs
|
|
20
|
+
from .models import Candidate, CandidateMeta
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_requirements_txt(path: Path) -> set[str]:
|
|
24
|
+
"""Parse a requirements.txt file and return a set of requirement strings."""
|
|
25
|
+
out: set[str] = set()
|
|
26
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
27
|
+
line = line.strip()
|
|
28
|
+
if not line or line.startswith("#"):
|
|
29
|
+
continue
|
|
30
|
+
out.add(line)
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_pyproject(path: Path) -> CandidateMeta:
|
|
35
|
+
"""Parse a pyproject.toml file and extract metadata."""
|
|
36
|
+
raw = _toml.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
37
|
+
data = cast(dict[str, Any], raw)
|
|
38
|
+
meta = CandidateMeta()
|
|
39
|
+
proj = data.get("project") or {}
|
|
40
|
+
if proj:
|
|
41
|
+
meta.name = proj.get("name") or meta.name
|
|
42
|
+
v = proj.get("version")
|
|
43
|
+
if isinstance(v, str):
|
|
44
|
+
meta.version = v
|
|
45
|
+
deps = proj.get("dependencies") or []
|
|
46
|
+
meta.core_deps.update([d for d in deps if isinstance(d, str)])
|
|
47
|
+
opt = proj.get("optional-dependencies") or {}
|
|
48
|
+
for k, arr in opt.items():
|
|
49
|
+
if isinstance(arr, list):
|
|
50
|
+
meta.extras[k] = {d for d in arr if isinstance(d, str)}
|
|
51
|
+
rp = proj.get("requires-python")
|
|
52
|
+
if isinstance(rp, str):
|
|
53
|
+
meta.requires_python = rp
|
|
54
|
+
|
|
55
|
+
bsys = data.get("build-system") or {}
|
|
56
|
+
breq = bsys.get("requires") or []
|
|
57
|
+
for x in breq:
|
|
58
|
+
if isinstance(x, str):
|
|
59
|
+
meta.build_requires.add(x)
|
|
60
|
+
|
|
61
|
+
return meta
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_setup_cfg(path: Path) -> CandidateMeta:
|
|
65
|
+
"""Parse a setup.cfg file and extract metadata."""
|
|
66
|
+
cfg = configparser.ConfigParser()
|
|
67
|
+
cfg.read_string(path.read_text(encoding="utf-8", errors="replace"))
|
|
68
|
+
meta = CandidateMeta()
|
|
69
|
+
if cfg.has_section("metadata"):
|
|
70
|
+
meta.name = cfg.get("metadata", "name", fallback=None) or meta.name
|
|
71
|
+
meta.version = cfg.get("metadata", "version", fallback=None) or meta.version
|
|
72
|
+
if cfg.has_section("options"):
|
|
73
|
+
if cfg.has_option("options", "install_requires"):
|
|
74
|
+
reqs = [
|
|
75
|
+
x.strip()
|
|
76
|
+
for x in cfg.get("options", "install_requires", raw=True, fallback="").splitlines()
|
|
77
|
+
if x.strip()
|
|
78
|
+
]
|
|
79
|
+
meta.core_deps.update(reqs)
|
|
80
|
+
if cfg.has_option("options", "python_requires"):
|
|
81
|
+
meta.requires_python = cfg.get("options", "python_requires", fallback=None) or meta.requires_python
|
|
82
|
+
for sec in cfg.sections():
|
|
83
|
+
if sec.startswith("options.extras_require"):
|
|
84
|
+
if sec == "options.extras_require":
|
|
85
|
+
for k, v in cfg.items(sec):
|
|
86
|
+
arr = [x.strip() for x in v.splitlines() if x.strip()]
|
|
87
|
+
meta.extras[k] = set(arr)
|
|
88
|
+
else:
|
|
89
|
+
_, _, extra = sec.partition(":")
|
|
90
|
+
arr = [x.strip() for x in cfg.get(sec, "__name__", fallback="").splitlines() if x.strip()]
|
|
91
|
+
if arr:
|
|
92
|
+
meta.extras[extra] = set(arr)
|
|
93
|
+
return meta
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_setup_py(path: Path) -> CandidateMeta: # noqa: C901
|
|
97
|
+
"""Heuristic, safe parser for setup.py (no code execution)."""
|
|
98
|
+
import ast
|
|
99
|
+
|
|
100
|
+
meta = CandidateMeta()
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
src = path.read_text(encoding="utf-8", errors="replace")
|
|
104
|
+
except Exception:
|
|
105
|
+
return meta
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
tree = ast.parse(src, filename=str(path))
|
|
109
|
+
except Exception:
|
|
110
|
+
return meta
|
|
111
|
+
|
|
112
|
+
env: dict[str, Any] = {}
|
|
113
|
+
|
|
114
|
+
def safe_eval(node: ast.AST, depth: int = 0) -> Any: # noqa: C901
|
|
115
|
+
if depth > 100:
|
|
116
|
+
raise ValueError("Too deep")
|
|
117
|
+
|
|
118
|
+
if isinstance(node, ast.Constant):
|
|
119
|
+
return node.value
|
|
120
|
+
|
|
121
|
+
if hasattr(ast, "Str") and isinstance(node, ast.Str):
|
|
122
|
+
return node.s
|
|
123
|
+
if hasattr(ast, "Num") and isinstance(node, ast.Num):
|
|
124
|
+
return node.n
|
|
125
|
+
if hasattr(ast, "NameConstant") and isinstance(node, ast.NameConstant):
|
|
126
|
+
return node.value
|
|
127
|
+
|
|
128
|
+
if isinstance(node, ast.Name):
|
|
129
|
+
if node.id in env:
|
|
130
|
+
return env[node.id]
|
|
131
|
+
raise ValueError(f"Unknown name {node.id}")
|
|
132
|
+
|
|
133
|
+
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
134
|
+
elts = []
|
|
135
|
+
for e in node.elts:
|
|
136
|
+
elts.append(safe_eval(e, depth + 1))
|
|
137
|
+
return list(elts)
|
|
138
|
+
|
|
139
|
+
if isinstance(node, ast.Dict):
|
|
140
|
+
out: dict[Any, Any] = {}
|
|
141
|
+
for k, v in zip(node.keys, node.values):
|
|
142
|
+
if k is None:
|
|
143
|
+
raise ValueError("Dict unpacking not allowed here")
|
|
144
|
+
key = safe_eval(k, depth + 1)
|
|
145
|
+
val = safe_eval(v, depth + 1)
|
|
146
|
+
out[key] = val
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
|
150
|
+
v = safe_eval(node.operand, depth + 1)
|
|
151
|
+
if isinstance(v, (int, float)) and isinstance(node.op, ast.USub):
|
|
152
|
+
return -v
|
|
153
|
+
if isinstance(v, (int, float)) and isinstance(node.op, ast.UAdd):
|
|
154
|
+
return +v
|
|
155
|
+
raise ValueError("Unsupported unary op")
|
|
156
|
+
|
|
157
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
158
|
+
left = safe_eval(node.left, depth + 1)
|
|
159
|
+
right = safe_eval(node.right, depth + 1)
|
|
160
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
161
|
+
return left + right
|
|
162
|
+
if isinstance(left, list) and isinstance(right, list):
|
|
163
|
+
return left + right
|
|
164
|
+
if isinstance(left, tuple) and isinstance(right, tuple):
|
|
165
|
+
return list(left + right)
|
|
166
|
+
raise ValueError("Unsupported addition types")
|
|
167
|
+
|
|
168
|
+
if isinstance(node, ast.Call):
|
|
169
|
+
func = node.func
|
|
170
|
+
func_name = None
|
|
171
|
+
if isinstance(func, ast.Name):
|
|
172
|
+
func_name = func.id
|
|
173
|
+
elif isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
|
174
|
+
func_name = f"{func.value.id}.{func.attr}"
|
|
175
|
+
if func_name in {"list", "tuple"} and not node.keywords and len(node.args) == 1:
|
|
176
|
+
seq = safe_eval(node.args[0], depth + 1)
|
|
177
|
+
if isinstance(seq, list):
|
|
178
|
+
return list(seq)
|
|
179
|
+
if func_name == "dict" and not node.keywords: # noqa: SIM102
|
|
180
|
+
if len(node.args) == 1:
|
|
181
|
+
seq = safe_eval(node.args[0], depth + 1)
|
|
182
|
+
out2: dict[Any, Any] = {}
|
|
183
|
+
if isinstance(seq, list):
|
|
184
|
+
for item in seq:
|
|
185
|
+
if isinstance(item, (list, tuple)) and len(item) == 2:
|
|
186
|
+
out2[item[0]] = item[1]
|
|
187
|
+
else:
|
|
188
|
+
raise ValueError("Unsupported dict constructor form")
|
|
189
|
+
return out2
|
|
190
|
+
raise ValueError("Calls are not safely evaluable")
|
|
191
|
+
|
|
192
|
+
if isinstance(node, ast.JoinedStr):
|
|
193
|
+
s: list[str] = []
|
|
194
|
+
for v in node.values:
|
|
195
|
+
if isinstance(v, ast.Str):
|
|
196
|
+
s.append(str(v.s))
|
|
197
|
+
elif isinstance(v, ast.Constant) and isinstance(v.value, str):
|
|
198
|
+
s.append(v.value)
|
|
199
|
+
else:
|
|
200
|
+
raise TypeError("Non-literal in f-string")
|
|
201
|
+
return "".join(s)
|
|
202
|
+
|
|
203
|
+
raise ValueError("Unsupported AST node")
|
|
204
|
+
|
|
205
|
+
for node in tree.body:
|
|
206
|
+
try:
|
|
207
|
+
if isinstance(node, ast.Assign):
|
|
208
|
+
val = safe_eval(node.value)
|
|
209
|
+
for target in node.targets:
|
|
210
|
+
if isinstance(target, ast.Name):
|
|
211
|
+
env[target.id] = val
|
|
212
|
+
elif isinstance(target, (ast.Tuple, ast.List)): # noqa: SIM102
|
|
213
|
+
if isinstance(val, (list, tuple)) and len(target.elts) == len(val):
|
|
214
|
+
for elt, v in zip(target.elts, val):
|
|
215
|
+
if isinstance(elt, ast.Name):
|
|
216
|
+
env[elt.id] = v
|
|
217
|
+
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.value is not None:
|
|
218
|
+
env[node.target.id] = safe_eval(node.value)
|
|
219
|
+
except Exception: # noqa: S112
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
def is_setup_call(call: ast.Call) -> bool:
|
|
223
|
+
f = call.func
|
|
224
|
+
if isinstance(f, ast.Name):
|
|
225
|
+
return f.id == "setup"
|
|
226
|
+
if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name):
|
|
227
|
+
return f.attr == "setup"
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def merge_kwargs_from_starstar(val: Any, into: dict[str, Any]) -> None:
|
|
231
|
+
if isinstance(val, str) and val in env and isinstance(env[val], dict):
|
|
232
|
+
for k, v in env[val].items():
|
|
233
|
+
into.setdefault(k, v)
|
|
234
|
+
elif isinstance(val, dict):
|
|
235
|
+
for k, v in val.items():
|
|
236
|
+
into.setdefault(k, v)
|
|
237
|
+
|
|
238
|
+
setup_kwargs: dict[str, Any] = {}
|
|
239
|
+
for ast_node in ast.walk(tree):
|
|
240
|
+
if isinstance(ast_node, ast.Call) and is_setup_call(ast_node):
|
|
241
|
+
kwargs: dict[str, Any] = {}
|
|
242
|
+
for kw in ast_node.keywords:
|
|
243
|
+
try:
|
|
244
|
+
if kw.arg is None:
|
|
245
|
+
v = safe_eval(kw.value)
|
|
246
|
+
merge_kwargs_from_starstar(v, kwargs)
|
|
247
|
+
else:
|
|
248
|
+
kwargs[kw.arg] = safe_eval(kw.value)
|
|
249
|
+
except Exception: # noqa: S112
|
|
250
|
+
continue
|
|
251
|
+
setup_kwargs = kwargs
|
|
252
|
+
|
|
253
|
+
if setup_kwargs:
|
|
254
|
+
name = setup_kwargs.get("name")
|
|
255
|
+
if isinstance(name, str):
|
|
256
|
+
meta.name = name
|
|
257
|
+
version = setup_kwargs.get("version")
|
|
258
|
+
if isinstance(version, str):
|
|
259
|
+
meta.version = version
|
|
260
|
+
pyreq = setup_kwargs.get("python_requires")
|
|
261
|
+
if isinstance(pyreq, str):
|
|
262
|
+
meta.requires_python = pyreq
|
|
263
|
+
install_requires = setup_kwargs.get("install_requires")
|
|
264
|
+
if isinstance(install_requires, (list, tuple)):
|
|
265
|
+
meta.core_deps.update([x for x in install_requires if isinstance(x, str)])
|
|
266
|
+
extras_require = setup_kwargs.get("extras_require")
|
|
267
|
+
if isinstance(extras_require, dict):
|
|
268
|
+
for k, v in extras_require.items():
|
|
269
|
+
if isinstance(k, str) and isinstance(v, (list, tuple)):
|
|
270
|
+
meta.extras[k] = {x for x in v if isinstance(x, str)}
|
|
271
|
+
setup_requires = setup_kwargs.get("setup_requires")
|
|
272
|
+
if isinstance(setup_requires, (list, tuple)):
|
|
273
|
+
meta.build_requires.update([x for x in setup_requires if isinstance(x, str)])
|
|
274
|
+
|
|
275
|
+
return meta
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def parse_conda_env_yaml(path: Path) -> set[str]: # noqa: C901
|
|
279
|
+
"""Light parser for environment.yml/.yaml."""
|
|
280
|
+
try:
|
|
281
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
282
|
+
except Exception:
|
|
283
|
+
return set()
|
|
284
|
+
out: set[str] = set()
|
|
285
|
+
in_deps = False
|
|
286
|
+
in_pip = False
|
|
287
|
+
indent_pip = None
|
|
288
|
+
for raw in lines:
|
|
289
|
+
line = raw.rstrip()
|
|
290
|
+
if not line.strip() or line.strip().startswith("#"):
|
|
291
|
+
continue
|
|
292
|
+
if line.startswith("dependencies:"):
|
|
293
|
+
in_deps, in_pip, indent_pip = True, False, None
|
|
294
|
+
continue
|
|
295
|
+
if not in_deps:
|
|
296
|
+
continue
|
|
297
|
+
if re.match(r"\s*-\s*pip\s*:\s*$", line):
|
|
298
|
+
in_pip = True
|
|
299
|
+
indent_pip = len(line) - len(line.lstrip(" "))
|
|
300
|
+
continue
|
|
301
|
+
if in_pip:
|
|
302
|
+
if (len(line) - len(line.lstrip(" "))) > (indent_pip or 0):
|
|
303
|
+
m = re.match(r"\s*-\s*([^\s].+)$", line)
|
|
304
|
+
if m:
|
|
305
|
+
out.add(m.group(1).strip())
|
|
306
|
+
continue
|
|
307
|
+
else:
|
|
308
|
+
in_pip = False
|
|
309
|
+
m = re.match(r"\s*-\s*([A-Za-z0-9_.-]+)(?:[=<>!].*)?$", line)
|
|
310
|
+
if m:
|
|
311
|
+
name = m.group(1)
|
|
312
|
+
if name.lower() not in {"python", "pip", "setuptools", "wheel"}:
|
|
313
|
+
out.add(name)
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def discover_candidates(commit: Commit) -> dict[str, Candidate]: # noqa: C901
|
|
318
|
+
"""Discover packaging roots and requirement/conda files across the repo at this commit."""
|
|
319
|
+
|
|
320
|
+
def predicate(rel: str) -> bool:
|
|
321
|
+
base = rel.rsplit("/", 1)[-1]
|
|
322
|
+
if base in (PYPROJECT, SETUP_CFG, SETUP_PY):
|
|
323
|
+
return True
|
|
324
|
+
if base in ENV_YML_NAMES:
|
|
325
|
+
return True
|
|
326
|
+
return bool(REQ_TXT_REGEX.search(rel))
|
|
327
|
+
|
|
328
|
+
blob_map = materialize_blobs(commit, predicate, out_dirname="_pkg_blobs")
|
|
329
|
+
candidates: dict[str, Candidate] = {}
|
|
330
|
+
|
|
331
|
+
def ensure_candidate(root_rel: str) -> Candidate:
|
|
332
|
+
if root_rel not in candidates:
|
|
333
|
+
candidates[root_rel] = Candidate(root_relpath=root_rel)
|
|
334
|
+
return candidates[root_rel]
|
|
335
|
+
|
|
336
|
+
for rel, local_path in blob_map.items():
|
|
337
|
+
root = str(Path(rel).parent or ".")
|
|
338
|
+
cand = ensure_candidate(root)
|
|
339
|
+
name = local_path.name
|
|
340
|
+
if name == PYPROJECT:
|
|
341
|
+
cand.pyproject_path = local_path
|
|
342
|
+
elif name == SETUP_CFG:
|
|
343
|
+
cand.setup_cfg_path = local_path
|
|
344
|
+
elif name == SETUP_PY:
|
|
345
|
+
cand.setup_py_path = local_path
|
|
346
|
+
elif REQ_TXT_REGEX.search(rel):
|
|
347
|
+
cand.req_files.append(local_path)
|
|
348
|
+
elif name in ENV_YML_NAMES:
|
|
349
|
+
cand.env_yamls.append(local_path)
|
|
350
|
+
|
|
351
|
+
return candidates
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def analyze_candidate_meta(cand: Candidate) -> CandidateMeta:
|
|
355
|
+
"""Analyze a candidate to extract metadata from its packaging files."""
|
|
356
|
+
meta = CandidateMeta()
|
|
357
|
+
if cand.pyproject_path and cand.pyproject_path.exists():
|
|
358
|
+
meta = parse_pyproject(cand.pyproject_path)
|
|
359
|
+
if cand.setup_cfg_path and cand.setup_cfg_path.exists():
|
|
360
|
+
m2 = parse_setup_cfg(cand.setup_cfg_path)
|
|
361
|
+
meta.name = meta.name or m2.name
|
|
362
|
+
meta.version = meta.version or m2.version
|
|
363
|
+
meta.requires_python = meta.requires_python or m2.requires_python
|
|
364
|
+
meta.core_deps.update(m2.core_deps)
|
|
365
|
+
for k, v in m2.extras.items():
|
|
366
|
+
meta.extras.setdefault(k, set()).update(v)
|
|
367
|
+
if cand.setup_py_path and cand.setup_py_path.exists():
|
|
368
|
+
m3 = parse_setup_py(cand.setup_py_path)
|
|
369
|
+
meta.name = meta.name or m3.name
|
|
370
|
+
meta.version = meta.version or m3.version
|
|
371
|
+
meta.requires_python = meta.requires_python or m3.requires_python
|
|
372
|
+
meta.core_deps.update(m3.core_deps)
|
|
373
|
+
for k, v in m3.extras.items():
|
|
374
|
+
meta.extras.setdefault(k, set()).update(v)
|
|
375
|
+
meta.build_requires.update(m3.build_requires)
|
|
376
|
+
for req in cand.req_files:
|
|
377
|
+
meta.core_deps.update(parse_requirements_txt(req))
|
|
378
|
+
for y in cand.env_yamls:
|
|
379
|
+
meta.core_deps.update(parse_conda_env_yaml(y))
|
|
380
|
+
return meta
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def select_primary_candidate( # noqa: C901
|
|
384
|
+
repo_name: str, candidates: dict[str, Candidate], install_cmds: set[str], analyzed: dict[str, CandidateMeta]
|
|
385
|
+
) -> str:
|
|
386
|
+
"""Heuristic to select the primary package root from multiple candidates."""
|
|
387
|
+
norm = lambda p: str(Path(p).as_posix().strip("./")) or "."
|
|
388
|
+
paths = []
|
|
389
|
+
for cmd in install_cmds:
|
|
390
|
+
toks = shlex.split(cmd)
|
|
391
|
+
for t in toks:
|
|
392
|
+
base = t.split("[", 1)[0]
|
|
393
|
+
if base.startswith((".", "/")) or "/" in base or base in (".",):
|
|
394
|
+
paths.append(norm(base))
|
|
395
|
+
for p in paths:
|
|
396
|
+
if p in candidates:
|
|
397
|
+
return p
|
|
398
|
+
if len(candidates) == 1:
|
|
399
|
+
return next(iter(candidates.keys()))
|
|
400
|
+
repo_suffix = repo_name.split("/", 1)[-1].lower().replace("_", "-")
|
|
401
|
+
by_name = []
|
|
402
|
+
for root, meta in analyzed.items():
|
|
403
|
+
if meta.name:
|
|
404
|
+
nm = meta.name.lower().replace("_", "-")
|
|
405
|
+
if nm == repo_suffix or nm == repo_suffix.replace("-", ""):
|
|
406
|
+
by_name.append(root)
|
|
407
|
+
if by_name:
|
|
408
|
+
return by_name[0]
|
|
409
|
+
for root, cand in candidates.items():
|
|
410
|
+
if cand.pyproject_path:
|
|
411
|
+
return root
|
|
412
|
+
return sorted(candidates.keys(), key=lambda s: (len(Path(s).parts), s))[0]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Data models for dependency resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Candidate:
|
|
11
|
+
"""Represents a potential package root in a repository."""
|
|
12
|
+
|
|
13
|
+
root_relpath: str
|
|
14
|
+
pyproject_path: Path | None = None
|
|
15
|
+
setup_cfg_path: Path | None = None
|
|
16
|
+
setup_py_path: Path | None = None
|
|
17
|
+
req_files: list[Path] = field(default_factory=list)
|
|
18
|
+
env_yamls: list[Path] = field(default_factory=list) # environment.yml/.yaml
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CandidateMeta:
|
|
23
|
+
"""Metadata extracted from packaging files."""
|
|
24
|
+
|
|
25
|
+
name: str | None = None # PyPI name
|
|
26
|
+
version: str | None = None
|
|
27
|
+
import_name: str | None = None # importable module (when we can guess)
|
|
28
|
+
requires_python: str | None = None
|
|
29
|
+
core_deps: set[str] = field(default_factory=set) # runtime
|
|
30
|
+
extras: dict[str, set[str]] = field(default_factory=dict)
|
|
31
|
+
build_requires: set[str] = field(default_factory=set) # [build-system].requires
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ASVCfgAggregate:
|
|
36
|
+
"""Aggregated configuration from ASV benchmark config files."""
|
|
37
|
+
|
|
38
|
+
pythons: set[tuple[int, ...]] = field(default_factory=set)
|
|
39
|
+
build_commands: set[str] = field(default_factory=set)
|
|
40
|
+
install_commands: set[str] = field(default_factory=set)
|
|
41
|
+
matrix: dict[str, set[str]] = field(default_factory=dict)
|