Repository navigation
Expand file tree
/
Copy pathsetup_primer_project.py
More file actions
executable file
·221 lines (187 loc) · 7.96 KB
/
Copy pathsetup_primer_project.py
File metadata and controls
executable file
·221 lines (187 loc) · 7.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#!/usr/bin/env -S uv run --script
#
# /// script
# requires-python = ">=3.11"
# dependencies = ["mypy-primer"]
#
# [tool.ty.rules]
# truthiness-test-of-none-union = "warn"
# blanket-ignore-comment = "warn"
# missing-type-argument = "warn"
# possibly-unresolved-reference = "warn"
# unsound-return-statement = "warn"
# unsound-yield = "warn"
# unsupported-dynamic-base = "warn"
# division-by-zero = "warn"
# dynamic-function-decorator-return = "warn"
# unsound-assignment = "warn"
# redundant-condition-strict = "warn"
# disjoint-cast = "warn"
# missing-direct-dependency = "warn"
#
# [tool.uv]
# no-build = true
# no-binary-package = ["mypy-primer"]
# build-constraint-dependencies = ["setuptools==84.0.0"]
# # This is the default for ad hoc use. Historical ecosystem reproduction must
# # bypass the adjacent lock and select ecosystem-analyzer's exact mypy-primer
# # revision and project Python version, as shown in the module docstring.
# # `exclude-newer` still constrains mypy-primer's registry dependencies.
# exclude-newer = "P7D"
#
# [tool.uv.sources]
# # Keep the script's lockfile in sync with the mypy-primer pin in the project's uv.lock file
# # so memory reports and ecosystem jobs use the same project definitions.
# mypy-primer = { git = "https://github.com/hauntsaninja/mypy_primer" }
# ///
"""Clone a mypy-primer project and set up a virtualenv with its dependencies installed.
For ecosystem-report reproduction, always select the project's ecosystem-analyzer Python version and bypass the adjacent lock with the exact mypy-primer revision pinned by ecosystem-analyzer:
uv run --python <version> --with "mypy-primer @ git+https://github.com/hauntsaninja/mypy_primer@<mypy-primer-revision>" --no-project python scripts/setup_primer_project.py <project-name> [directory] [options]
"""
from __future__ import annotations
import argparse
import shlex
import subprocess
import sys
from pathlib import Path
from typing import NoReturn
from mypy_primer.model import Project
from mypy_primer.projects import get_projects
def find_project(name: str) -> Project:
projects = get_projects()
for p in projects:
if p.name == name:
return p
_project_not_found(name, projects)
def _project_not_found(name: str, projects: list[Project]) -> NoReturn:
print(f"error: project {name!r} not found", file=sys.stderr)
print("available projects:", file=sys.stderr)
for p in sorted(projects, key=lambda p: p.name):
print(f" {p.name}", file=sys.stderr)
sys.exit(1)
class _FormatMap:
def __init__(self, **values: str | list[str] | None) -> None:
self.values = values
def __getitem__(self, key: str) -> str:
if key not in self.values:
raise KeyError(key)
value = self.values[key]
if value is None:
raise ValueError(f"Required {key} to be specified")
if isinstance(value, list):
return " ".join(value)
return value
def get_ty_command(project: Project, *, ty_binary: str, venv_dir: Path) -> str:
ty_cmd = project.ty_cmd
if ty_cmd is None:
ty_cmd = "{ty} check {paths}" if project.paths is not None else "{ty} check"
assert "{ty}" in ty_cmd
ty_cmd = ty_cmd.format_map(_FormatMap(ty=ty_binary, paths=project.paths))
return f"{ty_cmd} --python {shlex.quote(str(venv_dir))} --output-format concise"
def run(*args: str, cwd: Path | None = None) -> None:
subprocess.run(args, cwd=cwd, check=True)
def clone_project(
location: str, target_dir: Path, revision: str | None, *, full_history: bool
) -> None:
depth = [] if full_history else ["--depth", "1"]
if revision is not None and not full_history:
# Fetch the requested commit without first downloading the default branch.
run("git", "init", str(target_dir))
run("git", "remote", "add", "origin", location, cwd=target_dir)
run("git", "fetch", *depth, "origin", revision, cwd=target_dir)
checkout = "FETCH_HEAD"
else:
run("git", "clone", *depth, location, str(target_dir))
checkout = revision
if checkout is not None:
run("git", "checkout", checkout, "--", cwd=target_dir)
# Initialize only the selected revision's submodules. Force checkout so that
# configured merge/rebase strategies cannot override the recorded gitlinks.
run(
"git",
"submodule",
"update",
"--init",
"--recursive",
"--checkout",
*(depth or ["--no-recommend-shallow"]),
cwd=target_dir,
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("project", help="Name of a mypy-primer project")
parser.add_argument(
"directory",
nargs="?",
help="Directory to clone into (default: project name)",
)
parser.add_argument(
"--revision",
help="Git revision to check out before installing dependencies",
)
parser.add_argument(
"--full-history",
action="store_true",
help="Clone full history and tags if setup requires them or shallow fetches fail",
)
parser.add_argument(
"--exclude-newer",
help="Limit dependency resolution to packages uploaded before this timestamp",
)
parser.add_argument(
"--print-ty-command",
action="store_true",
help="Print the project-specific ty command without setting up the project",
)
args = parser.parse_args()
project = find_project(args.project)
revision = args.revision or project.revision
target_dir = Path(args.directory or project.name).resolve()
if args.print_ty_command:
print(get_ty_command(project, ty_binary="{ty}", venv_dir=target_dir / ".venv"))
return
if target_dir.exists() and (not target_dir.is_dir() or any(target_dir.iterdir())):
parser.error(
f"destination {target_dir} already exists and is not an empty directory"
)
print(f"Cloning {project.location} into {target_dir}...")
try:
clone_project(
project.location, target_dir, revision, full_history=args.full_history
)
except subprocess.CalledProcessError as error:
message = f"Git checkout failed: {error}.\n"
if not args.full_history:
message += "If the server rejects shallow fetches, retry in a fresh directory with --full-history.\n"
parser.exit(1, message)
# Create venv (matching primer's Venv.make_venv())
venv_dir = target_dir / ".venv"
print(f"Creating virtualenv at {venv_dir}...")
subprocess.run(
["uv", "venv", str(venv_dir), "--python", sys.executable, "--seed", "--clear"],
check=True,
)
venv_python = venv_dir / "bin" / "python"
install_base = f"uv pip install --python {shlex.quote(str(venv_python))}"
if args.exclude_newer:
install_base += f" --exclude-newer {shlex.quote(args.exclude_newer)}"
# Run custom install command if the project defines one (matching primer's setup())
if project.install_cmd is not None:
assert "{install}" in project.install_cmd
install_cmd = project.install_cmd.format(install=install_base)
print(f"Running install command: {install_cmd}")
# Primer install commands are trusted project metadata and may use shell syntax.
subprocess.run(install_cmd, cwd=target_dir, shell=True, check=True) # ruff: ignore[subprocess-popen-with-shell-equals-true]
# Install listed dependencies (matching primer's setup())
if project.deps is not None:
deps_cmd_parts = shlex.split(install_base) + project.deps
print(f"Installing dependencies: {', '.join(project.deps)}")
subprocess.run(deps_cmd_parts, cwd=target_dir, check=True)
print(f"\nDone! Project set up at {target_dir}")
print(f"Activate the venv with: source {venv_dir}/bin/activate")
print("\nProject-specific ty command:")
print(" ty_binary=/path/to/ty")
ty_command = get_ty_command(project, ty_binary='"$ty_binary"', venv_dir=venv_dir)
print(f" {ty_command}")
if __name__ == "__main__":
main()