fastws-cli 0.0.17__tar.gz → 0.0.19__tar.gz
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.
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/PKG-INFO +4 -4
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/README.md +2 -2
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws/__init__.py +3 -1
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws/core.py +27 -6
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws_cli.egg-info/PKG-INFO +4 -4
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws_cli.egg-info/requires.txt +1 -1
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/pyproject.toml +1 -1
- fastws_cli-0.0.19/tests/test_releases.py +28 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/tests/test_sync.py +23 -239
- fastws_cli-0.0.17/tests/test_releases.py +0 -96
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/LICENSE +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws/releases.py +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws_cli.egg-info/SOURCES.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/setup.cfg +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/tests/test_build.py +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.19}/tests/test_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License: Apache-2.0
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: fastcore>=2.2.
|
|
13
|
+
Requires-Dist: fastcore>=2.2.28
|
|
14
14
|
Requires-Dist: fastgit>=0.1.2
|
|
15
15
|
Requires-Dist: ghapi>=2.0.6
|
|
16
16
|
Requires-Dist: packaging
|
|
@@ -208,7 +208,7 @@ By default, `ws-sync` uses the active venv's parent as the workspace root. It cr
|
|
|
208
208
|
|
|
209
209
|
Project scanning respects `tool.uv.workspace.members` and `exclude`. If a member directory lacks `pyproject.toml`, such as a fresh empty clone, sync warns and skips the `uv sync` step.
|
|
210
210
|
|
|
211
|
-
Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
|
|
211
|
+
Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. It also excludes listed project templates, whose `[project].name` still holds a `{placeholder}`, because uv cannot parse that name. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
|
|
212
212
|
|
|
213
213
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
214
214
|
|
|
@@ -216,7 +216,7 @@ Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `
|
|
|
216
216
|
|
|
217
217
|
Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
|
|
218
218
|
|
|
219
|
-
When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved.
|
|
219
|
+
When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved. If neither a wrapper nor `sccache` is present, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it.
|
|
220
220
|
|
|
221
221
|
Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
|
|
222
222
|
|
|
@@ -183,7 +183,7 @@ By default, `ws-sync` uses the active venv's parent as the workspace root. It cr
|
|
|
183
183
|
|
|
184
184
|
Project scanning respects `tool.uv.workspace.members` and `exclude`. If a member directory lacks `pyproject.toml`, such as a fresh empty clone, sync warns and skips the `uv sync` step.
|
|
185
185
|
|
|
186
|
-
Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
|
|
186
|
+
Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. It also excludes listed project templates, whose `[project].name` still holds a `{placeholder}`, because uv cannot parse that name. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
|
|
187
187
|
|
|
188
188
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
189
189
|
|
|
@@ -191,7 +191,7 @@ Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `
|
|
|
191
191
|
|
|
192
192
|
Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
|
|
193
193
|
|
|
194
|
-
When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved.
|
|
194
|
+
When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved. If neither a wrapper nor `sccache` is present, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it.
|
|
195
195
|
|
|
196
196
|
Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
|
|
197
197
|
|
|
@@ -176,13 +176,21 @@ def _remove_from_pyproject(pyproject_path: Path, names: list[str]) -> list[str]:
|
|
|
176
176
|
pyproject_path.write_text(content)
|
|
177
177
|
return removed
|
|
178
178
|
|
|
179
|
-
def
|
|
179
|
+
def _pyproject_name(path: Path) -> str|None:
|
|
180
|
+
"`[project].name` as written, with any `{placeholder}` slots left in"
|
|
180
181
|
try: data = tomllib.loads(path.read_text())
|
|
181
182
|
except tomllib.TOMLDecodeError:
|
|
182
183
|
print(f"Skipping invalid TOML: {path}")
|
|
183
184
|
return None
|
|
184
185
|
name = data.get("project", {}).get("name")
|
|
185
|
-
return name if isinstance(name, str) and name
|
|
186
|
+
return name if isinstance(name, str) and name else None
|
|
187
|
+
|
|
188
|
+
def _is_placeholder(name: str|None) -> bool: return bool(name) and ("{" in name or "}" in name)
|
|
189
|
+
|
|
190
|
+
def _read_pyproject_name(path: Path) -> str|None:
|
|
191
|
+
"`[project].name`, or None when it is missing or still a template placeholder"
|
|
192
|
+
name = _pyproject_name(path)
|
|
193
|
+
return None if _is_placeholder(name) else name
|
|
186
194
|
|
|
187
195
|
def _ws_projects(root: Path) -> list[str]:
|
|
188
196
|
return [name for d in (o for o in _ws_dirs(root) if (o/"pyproject.toml").exists())
|
|
@@ -208,6 +216,10 @@ def _non_python_project(d: Path) -> bool:
|
|
|
208
216
|
"A Rust or JS project without a Python layer is not a pending Python scaffold."
|
|
209
217
|
return not (d/"pyproject.toml").exists() and any((d/f).exists() for f in ("Cargo.toml", "package.json"))
|
|
210
218
|
|
|
219
|
+
def _template_dir(d: Path) -> bool:
|
|
220
|
+
"A project template. Its `[project].name` still holds `{placeholder}` slots, so uv cannot parse it."
|
|
221
|
+
return (d/"pyproject.toml").exists() and _is_placeholder(_pyproject_name(d/"pyproject.toml"))
|
|
222
|
+
|
|
211
223
|
def _pending_dirs(root: Path) -> list[str]:
|
|
212
224
|
"uv workspace dirs that are not Python projects yet; sync stops rather than let uv fail on them"
|
|
213
225
|
return [d.name for d in _ws_dirs(root) if not (d/"pyproject.toml").exists()]
|
|
@@ -218,7 +230,7 @@ def _sync_ws_excludes(pyproject_path: Path, root: Path, tracked: set[str]) -> tu
|
|
|
218
230
|
Kept as-is: `[tool.fastws].exclude` entries (intent), globs, missing dirs, and tracked dirs
|
|
219
231
|
(repos.txt checkouts) that are still not valid Python projects. Auto-managed: entries for other
|
|
220
232
|
existing dirs are regenerated each sync, excluding dirs without a valid pyproject (tracked dirs
|
|
221
|
-
only when they are Cargo-only crates
|
|
233
|
+
only when they are Cargo-only crates, npm-only packages, or templates with a `{placeholder}` name, since a tracked dir with none of those files is a pending member
|
|
222
234
|
awaiting scaffolding) and un-excluding dirs that gained one; deliberately excluding a real
|
|
223
235
|
project takes a `[tool.fastws]` entry."""
|
|
224
236
|
if not pyproject_path.exists(): return [], []
|
|
@@ -231,7 +243,7 @@ def _sync_ws_excludes(pyproject_path: Path, root: Path, tracked: set[str]) -> tu
|
|
|
231
243
|
kept = [e for e in cur if e in intent or any(c in e for c in "*?[") or not (root/e).is_dir() or (e in tracked and not _valid_project_dir(root/e))]
|
|
232
244
|
kept += [e for e in intent if e not in kept]
|
|
233
245
|
auto = [d.name for d in _project_dirs(root, exclude=kept) if not d.name.startswith(".") and any(_matches_ws(d.name, m) for m in members)
|
|
234
|
-
and (d.name not in tracked or _non_python_project(d)) and not _valid_project_dir(d)]
|
|
246
|
+
and (d.name not in tracked or _non_python_project(d) or _template_dir(d)) and not _valid_project_dir(d)]
|
|
235
247
|
survivors = set(kept) | set(auto)
|
|
236
248
|
new = [e for e in cur if e in survivors] + [e for e in kept + auto if e not in cur]
|
|
237
249
|
if new == cur: return [], []
|
|
@@ -694,11 +706,14 @@ def _sync_cargo_patches(root: Path) -> tuple[list[str], list[str]]:
|
|
|
694
706
|
|
|
695
707
|
def _sync_cargo_wrapper(root: Path) -> bool:
|
|
696
708
|
"Add sccache to the generated Cargo config when installed, without overriding another wrapper"
|
|
697
|
-
if not (wrapper := shutil.which("sccache")): return False
|
|
698
709
|
config = root/".cargo"/"config.toml"
|
|
699
710
|
content = config.read_text() if config.exists() else ""
|
|
700
711
|
data = tomllib.loads(content) if content else {}
|
|
701
712
|
if data.get("build", {}).get("rustc-wrapper"): return False
|
|
713
|
+
if not (wrapper := shutil.which("sccache")):
|
|
714
|
+
print("⚠️ sccache not found; Rust builds won't use compiler caching.\n"
|
|
715
|
+
"Install it with: cargo install sccache --locked\nThen rerun ws-sync to enable it.")
|
|
716
|
+
return False
|
|
702
717
|
line = f'rustc-wrapper = {json.dumps(wrapper)}\n'
|
|
703
718
|
if match := re.search(r"(?m)^\[build\][^\n]*\n", content): new = content[:match.end()] + line + content[match.end():]
|
|
704
719
|
else: new = content.rstrip() + ("\n\n" if content.strip() else "") + "[build]\n" + line
|
|
@@ -839,7 +854,13 @@ def _npm_install(root: Path):
|
|
|
839
854
|
def _sync_js(root: Path, members: list[Path]) -> list[Path]:
|
|
840
855
|
"Install the JS workspace, run every native member's build script, and return those members. Cargo handles incremental compilation."
|
|
841
856
|
tool = _fastws_cfg(root).get("js", "npm")
|
|
842
|
-
if not shutil.which(tool):
|
|
857
|
+
if not shutil.which(tool):
|
|
858
|
+
msg = f"{tool} is not installed: install it, or set [tool.fastws].js to a package manager that is"
|
|
859
|
+
if tool == "npm": msg += ("\n\nInstall Node.js and npm using nvm:\n"
|
|
860
|
+
"curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash\n"
|
|
861
|
+
"# Restart your terminal, then:\n"
|
|
862
|
+
"nvm install --lts")
|
|
863
|
+
raise SystemExit(msg)
|
|
843
864
|
if tool == "npm": _npm_install(root)
|
|
844
865
|
else: subprocess.run([tool, "install"], check=True, cwd=root)
|
|
845
866
|
built = _native_js(members)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License: Apache-2.0
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: fastcore>=2.2.
|
|
13
|
+
Requires-Dist: fastcore>=2.2.28
|
|
14
14
|
Requires-Dist: fastgit>=0.1.2
|
|
15
15
|
Requires-Dist: ghapi>=2.0.6
|
|
16
16
|
Requires-Dist: packaging
|
|
@@ -208,7 +208,7 @@ By default, `ws-sync` uses the active venv's parent as the workspace root. It cr
|
|
|
208
208
|
|
|
209
209
|
Project scanning respects `tool.uv.workspace.members` and `exclude`. If a member directory lacks `pyproject.toml`, such as a fresh empty clone, sync warns and skips the `uv sync` step.
|
|
210
210
|
|
|
211
|
-
Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
|
|
211
|
+
Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level directories without a valid Python project. It also excludes listed Rust or JavaScript projects without `pyproject.toml`. It also excludes listed project templates, whose `[project].name` still holds a `{placeholder}`, because uv cannot parse that name. For a listed checkout with none of `pyproject.toml`, `Cargo.toml`, or `package.json`, sync warns and skips installation.
|
|
212
212
|
|
|
213
213
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
214
214
|
|
|
@@ -216,7 +216,7 @@ Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `
|
|
|
216
216
|
|
|
217
217
|
Do not commit a `Cargo.lock` generated under these patches. Its source-less local entries cannot be resolved on another machine.
|
|
218
218
|
|
|
219
|
-
When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved.
|
|
219
|
+
When `sccache` is installed, sync also configures it as Cargo's `rustc-wrapper`, allowing unchanged compilation units to be reused across the workspace's otherwise-independent Cargo target directories. An existing wrapper is always preserved. If neither a wrapper nor `sccache` is present, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it.
|
|
220
220
|
|
|
221
221
|
Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
|
|
222
222
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from fastcore.basics import AttrDict
|
|
2
|
+
|
|
3
|
+
import fastws.releases as relmod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _rel(tag): return AttrDict(tag_name=tag)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_newest_tag_by_version_not_publish_order():
|
|
10
|
+
assert relmod._newest_tag([_rel("v0.1.17"), _rel("v0.1.18"), _rel("v0.1.9")]) == "v0.1.18"
|
|
11
|
+
assert relmod._newest_tag([]) is None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_skip_pats_defaults_and_config(tmp_path):
|
|
15
|
+
(tmp_path/"pyproject.toml").write_text('[tool.fastws]\nrelease_skip = ["docs only"]\n')
|
|
16
|
+
pats = relmod._skip_pats("wip", root=tmp_path)
|
|
17
|
+
hits = lambda m: any(p.match(m) for p in pats)
|
|
18
|
+
assert all(hits(m) for m in ("bump", "docs only: fix typo", "wip checkpoint"))
|
|
19
|
+
assert not any(hits(m) for m in ("bumpy road ahead", "fixes #30"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_release_report_repr():
|
|
23
|
+
rep = relmod.ReleaseReport([("mdhtml", ["fixes #30"]), ("solveit", None), ("fastcore", []), ("bad", ValueError("boom"))])
|
|
24
|
+
txt = repr(rep)
|
|
25
|
+
assert "mdhtml (1 unreleased):" in txt and " - fixes #30" in txt
|
|
26
|
+
assert "no releases: solveit" in txt
|
|
27
|
+
assert "up to date: fastcore" in txt
|
|
28
|
+
assert "bad: ERROR boom" in txt
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import json, os,
|
|
2
|
-
from types import SimpleNamespace
|
|
1
|
+
import json, os, pytest
|
|
3
2
|
from fastgit import Git
|
|
4
3
|
import fastws.core as core
|
|
5
4
|
|
|
@@ -25,19 +24,6 @@ def mk_repo(d, origin=None):
|
|
|
25
24
|
return g
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
@pytest.fixture
|
|
29
|
-
def fake_uv(monkeypatch):
|
|
30
|
-
"Intercept uv/cargo invocations (external tools stay out of tests); real git passes through"
|
|
31
|
-
calls, real = [], core.subprocess.run
|
|
32
|
-
def run(cmd, **kw):
|
|
33
|
-
if cmd[0] in ('uv', 'cargo'):
|
|
34
|
-
calls.append(cmd)
|
|
35
|
-
return SimpleNamespace(returncode=0, stdout='', stderr='')
|
|
36
|
-
return real(cmd, **kw)
|
|
37
|
-
monkeypatch.setattr(core.subprocess, 'run', run)
|
|
38
|
-
return calls
|
|
39
|
-
|
|
40
|
-
|
|
41
27
|
def test_repos_file_roundtrip(tmp_path):
|
|
42
28
|
repos_path = tmp_path/'repos.txt'
|
|
43
29
|
repos_path.write_text('# header\nAnswerDotAI/fastws[dev]\njph00/private[docs] ~/private\nfastai/fastai sub/dir\n')
|
|
@@ -82,63 +68,6 @@ def test_invalid_repo_extras(entry):
|
|
|
82
68
|
with pytest.raises(SystemExit, match='Invalid repo entry'): core._parse_repo_line(entry)
|
|
83
69
|
|
|
84
70
|
|
|
85
|
-
async def test_sync_updates_baseline_before_cloning_and_keeps_removed_repos(tmp_path, monkeypatch, fake_uv):
|
|
86
|
-
origins, seed, root = tmp_path/'origins', tmp_path/'seed', tmp_path/'ws'
|
|
87
|
-
config = tmp_path/'gitconfig'
|
|
88
|
-
config.write_text(f'[init]\ndefaultBranch = main\n[url "{origins.as_uri()}/"]\ninsteadOf = git@github.com:org/\n')
|
|
89
|
-
monkeypatch.setenv('GIT_CONFIG_GLOBAL', str(config))
|
|
90
|
-
monkeypatch.setattr(core, '_parse_github_repo', lambda url: f'org/{core.Path(url).stem}' if str(origins) in url else None)
|
|
91
|
-
async def offline_heads(refs): raise ValueError('No GitHub API in local Git test')
|
|
92
|
-
monkeypatch.setattr(core, '_remote_heads', offline_heads)
|
|
93
|
-
for name in ('old', 'new', 'extra'):
|
|
94
|
-
d = tmp_path/name
|
|
95
|
-
d.mkdir()
|
|
96
|
-
(d/'pyproject.toml').write_text(f'[project]\nname = "{name}-pkg"\n[project.optional-dependencies]\ndev = []\ntest = []\n')
|
|
97
|
-
mk_repo(d, origin=origins/f'{name}.git')
|
|
98
|
-
seed.mkdir()
|
|
99
|
-
(seed/'repos.txt').write_text('org/old[dev]\n')
|
|
100
|
-
(seed/'.gitignore').write_text('*/\npyproject.toml\nrepos-local.txt\n')
|
|
101
|
-
(seed/'pyproject.tmpl').write_text('[project]\nname = "workspace"\ndependencies = []\n[tool.uv.workspace]\nmembers = ["./*"]\n')
|
|
102
|
-
g = mk_repo(seed, origin=origins/'workspace.git')
|
|
103
|
-
Git(tmp_path, raise_exc=True).clone(str(origins/'workspace.git'), str(root))
|
|
104
|
-
Git(root, raise_exc=True).clone('git@github.com:org/extra.git', str(root/'extra'))
|
|
105
|
-
(root/'repos-local.txt').write_text('org/old[test]\n')
|
|
106
|
-
|
|
107
|
-
await core.ws_sync(str(root))
|
|
108
|
-
assert (root/'old'/'pyproject.toml').exists()
|
|
109
|
-
assert (root/'repos.txt').read_text() == 'org/old[dev]\n'
|
|
110
|
-
assert (root/'repos-local.txt').read_text() == 'org/old[test]\norg/extra\n'
|
|
111
|
-
assert set(core.tomllib.loads((root/'pyproject.toml').read_text())['project']['dependencies']) == {'old-pkg[dev,test]', 'extra-pkg'}
|
|
112
|
-
|
|
113
|
-
(seed/'repos.txt').write_text('org/new\n')
|
|
114
|
-
g.commit('-a', m='new shared baseline')
|
|
115
|
-
g.push()
|
|
116
|
-
await core.ws_sync(str(root))
|
|
117
|
-
assert (root/'new'/'pyproject.toml').exists()
|
|
118
|
-
assert (root/'old'/'pyproject.toml').exists()
|
|
119
|
-
assert (root/'repos.txt').read_text() == 'org/new\n'
|
|
120
|
-
assert set(core._load_repos(root/'repos-local.txt')) == {'org/extra', 'org/old'}
|
|
121
|
-
assert set(core.tomllib.loads((root/'pyproject.toml').read_text())['project']['dependencies']) == {'old-pkg[test]', 'new-pkg', 'extra-pkg'}
|
|
122
|
-
(root/'repos-local.txt').write_text('org/extra\n')
|
|
123
|
-
await core.ws_sync(str(root))
|
|
124
|
-
assert set(core.tomllib.loads((root/'pyproject.toml').read_text())['project']['dependencies']) == {'old-pkg', 'new-pkg', 'extra-pkg'}
|
|
125
|
-
|
|
126
|
-
(seed/'repos.txt').write_text('org/missing\n')
|
|
127
|
-
g.commit('-a', m='unavailable member')
|
|
128
|
-
g.push()
|
|
129
|
-
fake_uv.clear()
|
|
130
|
-
with pytest.raises(SystemExit, match='clone'): await core.ws_sync(str(root))
|
|
131
|
-
assert not fake_uv
|
|
132
|
-
|
|
133
|
-
(root/'repos.txt').write_text('local edits\n')
|
|
134
|
-
(seed/'repos.txt').write_text('org/new\n')
|
|
135
|
-
g.commit('-a', m='restore baseline')
|
|
136
|
-
g.push()
|
|
137
|
-
with pytest.raises(core.subprocess.CalledProcessError): await core.ws_sync(str(root))
|
|
138
|
-
assert (root/'repos.txt').read_text() == 'local edits\n'
|
|
139
|
-
assert not fake_uv
|
|
140
|
-
|
|
141
|
-
|
|
142
71
|
def test_ws_pyproject_from_template_adds_projects(tmp_path):
|
|
143
72
|
(tmp_path/'pyproject.tmpl').write_text('[project]\nname = "uvws"\ndependencies = [\n "ipython>=8.34.0",\n]\n\n[tool.uv.sources]\n\n')
|
|
144
73
|
for name in ('alpha', 'beta'):
|
|
@@ -194,19 +123,19 @@ def test_ws_excludes_generates_from_intent_and_auto(tmp_path):
|
|
|
194
123
|
pyproject.write_text(
|
|
195
124
|
'[project]\nname = "uvws"\n\n[tool.uv.workspace]\nmembers = ["./*"]\n'
|
|
196
125
|
'exclude = ["_*", "tmp", "example", "stale", "pending"]\n\n[tool.fastws]\nexclude = ["wanted-out"]\n')
|
|
197
|
-
for name in ('junk', 'fresh-clone', 'realpkg', 'tmpl', 'example', 'stale', 'wanted-out', 'pending', 'rustcrate'): (tmp_path/name).mkdir()
|
|
126
|
+
for name in ('junk', 'fresh-clone', 'realpkg', 'tmpl', 'tracked-tmpl', 'example', 'stale', 'wanted-out', 'pending', 'rustcrate'): (tmp_path/name).mkdir()
|
|
198
127
|
(tmp_path/'realpkg'/'pyproject.toml').write_text('[project]\nname = "realpkg"\n')
|
|
199
|
-
(
|
|
128
|
+
for name in ('tmpl', 'tracked-tmpl'): (tmp_path/name/'pyproject.toml').write_text('[project]\nname = "{repo}"\n')
|
|
200
129
|
(tmp_path/'rustcrate'/'Cargo.toml').write_text('[package]\nname = "rustcrate"\n')
|
|
201
130
|
for name in ('example', 'stale', 'wanted-out'): (tmp_path/name/'pyproject.toml').write_text(f'[project]\nname = "{name}"\n')
|
|
202
131
|
|
|
203
|
-
tracked = {'fresh-clone', 'example', 'pending', 'rustcrate'}
|
|
132
|
+
tracked = {'fresh-clone', 'example', 'pending', 'rustcrate', 'tracked-tmpl'}
|
|
204
133
|
added, removed = core._sync_ws_excludes(pyproject, tmp_path, tracked)
|
|
205
134
|
data = core.tomllib.loads(pyproject.read_text())
|
|
206
135
|
|
|
207
|
-
# kept: glob, missing dir, tracked non-project; auto: junk, placeholder
|
|
208
|
-
assert set(data['tool']['uv']['workspace']['exclude']) == {'_*', 'tmp', 'pending', 'wanted-out', 'junk', 'tmpl', 'rustcrate'}
|
|
209
|
-
assert set(added) == {'wanted-out', 'junk', 'tmpl', 'rustcrate'}
|
|
136
|
+
# kept: glob, missing dir, tracked non-project; auto: junk, placeholder templates tracked or not, tracked Cargo-only crate; intent: wanted-out; dropped: valid projects, tracked or not
|
|
137
|
+
assert set(data['tool']['uv']['workspace']['exclude']) == {'_*', 'tmp', 'pending', 'wanted-out', 'junk', 'tmpl', 'tracked-tmpl', 'rustcrate'}
|
|
138
|
+
assert set(added) == {'wanted-out', 'junk', 'tmpl', 'tracked-tmpl', 'rustcrate'}
|
|
210
139
|
assert set(removed) == {'example', 'stale'}
|
|
211
140
|
assert data['tool']['fastws']['exclude'] == ['wanted-out'] # fastws table untouched
|
|
212
141
|
|
|
@@ -298,7 +227,7 @@ async def test_git_repo_resolution(tmp_path):
|
|
|
298
227
|
assert sorted(await core._discover_ws_repos(tmp_path)) == ['AnswerDotAI/Proj', 'AnswerDotAI/crate']
|
|
299
228
|
|
|
300
229
|
|
|
301
|
-
def
|
|
230
|
+
def test_ws_remove_refuses_unsafe_repos(tmp_path):
|
|
302
231
|
(tmp_path/'repos.txt').write_text('AnswerDotAI/keep\n')
|
|
303
232
|
local = tmp_path/'repos-local.txt'
|
|
304
233
|
local.write_text('AnswerDotAI/repo1\nAnswerDotAI/repo2\norg/external ../external\n')
|
|
@@ -308,43 +237,17 @@ def test_ws_remove_workflow(tmp_path, monkeypatch, fake_uv):
|
|
|
308
237
|
d.mkdir()
|
|
309
238
|
(d/'pyproject.toml').write_text(f'[project]\nname = "{d.name}pkg"\n')
|
|
310
239
|
g = mk_repo(d, origin=tmp_path/'origins'/d.name)
|
|
311
|
-
answer = ['n', 'y', 'y']
|
|
312
|
-
monkeypatch.setattr('builtins.input', lambda *a: answer.pop(0))
|
|
313
|
-
|
|
314
240
|
with pytest.raises(SystemExit, match='baseline'): core.ws_remove('repo1', 'AnswerDotAI/keep', workspace=str(tmp_path))
|
|
315
241
|
with pytest.raises(SystemExit, match='location'): core.ws_remove('repo1', 'org/external', workspace=str(tmp_path))
|
|
316
|
-
assert 'org/external ../external' in local.read_text()
|
|
317
|
-
assert (tmp_path/'repos.txt').read_text() == 'AnswerDotAI/keep\n'
|
|
318
|
-
assert not fake_uv
|
|
319
242
|
|
|
320
|
-
# a dirty tree refuses before any mutation
|
|
321
243
|
(repo2/'pyproject.toml').write_text('[project]\nname = "repo2pkg"\nversion = "1"\n')
|
|
322
|
-
with pytest.raises(SystemExit): core.ws_remove('
|
|
323
|
-
assert repo.exists() and repo2.exists() and 'repo1' in local.read_text() and 'repo2' in local.read_text()
|
|
324
|
-
assert (tmp_path/'pyproject.toml').read_text() == WS_META
|
|
325
|
-
|
|
326
|
-
# so does an unpushed commit on a clean tree
|
|
244
|
+
with pytest.raises(SystemExit, match='uncommitted'): core.ws_remove('repo1', 'repo2', workspace=str(tmp_path))
|
|
327
245
|
g.commit('-a', m='ahead')
|
|
328
|
-
with pytest.raises(SystemExit): core.ws_remove('
|
|
246
|
+
with pytest.raises(SystemExit, match='unpushed'): core.ws_remove('repo1', 'repo2', workspace=str(tmp_path))
|
|
329
247
|
assert repo.exists() and repo2.exists()
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
g.push()
|
|
333
|
-
monkeypatch.setattr(sys, 'argv', ['ws-remove', 'AnswerDotAI/repo1', 'repo2', 'repo1', '--workspace', str(tmp_path)])
|
|
334
|
-
with pytest.raises(SystemExit) as exc: runpy.run_path(shutil.which('ws-remove'), run_name='__main__')
|
|
335
|
-
assert exc.value.code in (None, 0)
|
|
336
|
-
assert repo.exists() and not repo2.exists()
|
|
337
|
-
assert 'repo1' not in local.read_text() and 'repo2' not in local.read_text()
|
|
338
|
-
assert 'repo1pkg' not in (content := (tmp_path/'pyproject.toml').read_text()) and 'repo2pkg' not in content
|
|
339
|
-
assert 'keeppkg' in content and fake_uv == [['uv', 'sync']]
|
|
340
|
-
|
|
341
|
-
# answering 'y' (by folder name this time) also deletes the checkout
|
|
342
|
-
local.write_text('AnswerDotAI/repo1\n')
|
|
343
|
-
(tmp_path/'pyproject.toml').write_text(WS_META)
|
|
344
|
-
core.ws_remove('repo1', workspace=str(tmp_path))
|
|
345
|
-
assert not repo.exists()
|
|
248
|
+
assert local.read_text() == 'AnswerDotAI/repo1\nAnswerDotAI/repo2\norg/external ../external\n'
|
|
249
|
+
assert (tmp_path/'pyproject.toml').read_text() == WS_META
|
|
346
250
|
assert (tmp_path/'repos.txt').read_text() == 'AnswerDotAI/keep\n'
|
|
347
|
-
assert not answer
|
|
348
251
|
|
|
349
252
|
|
|
350
253
|
def test_upgrade_stamp(tmp_path):
|
|
@@ -413,31 +316,6 @@ def test_cargo_key_ignores_unused_patch_order(tmp_path):
|
|
|
413
316
|
assert key.read_text() != first
|
|
414
317
|
|
|
415
318
|
|
|
416
|
-
async def test_changed_dirs_pulls_only_moved_repos(tmp_path, monkeypatch):
|
|
417
|
-
# ahead: a second clone pushes to the shared bare origin, so `ahead`'s origin/main falls behind
|
|
418
|
-
ahead, current, weird = tmp_path/'ahead', tmp_path/'current', tmp_path/'weird'
|
|
419
|
-
for d in (ahead, current):
|
|
420
|
-
d.mkdir()
|
|
421
|
-
g = mk_repo(d, origin=tmp_path/'origins'/d.name)
|
|
422
|
-
g.remote('set-url', '--push', 'origin', str(tmp_path/'origins'/d.name))
|
|
423
|
-
other = tmp_path/'other'
|
|
424
|
-
Git(tmp_path, raise_exc=True).clone(str(tmp_path/'origins'/'ahead'), str(other))
|
|
425
|
-
og = Git(other, raise_exc=True)
|
|
426
|
-
(other/'g.txt').write_text('new')
|
|
427
|
-
og.add('.')
|
|
428
|
-
og.commit(m='advance')
|
|
429
|
-
og.push()
|
|
430
|
-
weird.mkdir()
|
|
431
|
-
mk_repo(weird) # no origin remote: can't be checked, so it must be pulled
|
|
432
|
-
|
|
433
|
-
async def fake_heads(refs): return [str(Git(tmp_path/'origins'/spec.split('/')[1], raise_exc=True).rev_parse(branch)) for spec, branch in refs]
|
|
434
|
-
monkeypatch.setattr(core, '_remote_heads', fake_heads)
|
|
435
|
-
monkeypatch.setattr(core, '_parse_github_repo', lambda url: f'o/{core.Path(url).name}' if 'origins' in url else None)
|
|
436
|
-
|
|
437
|
-
changed = await core._changed_dirs([ahead, current, weird])
|
|
438
|
-
assert set(changed) == {ahead, weird}
|
|
439
|
-
|
|
440
|
-
|
|
441
319
|
def test_sync_cargo_patches_generates_and_preserves(tmp_path):
|
|
442
320
|
(tmp_path/'.cargo').mkdir()
|
|
443
321
|
config = tmp_path/'.cargo'/'config.toml'
|
|
@@ -478,25 +356,9 @@ def test_sync_cargo_patches_generates_and_preserves(tmp_path):
|
|
|
478
356
|
assert core.tomllib.loads((bare/'.cargo'/'config.toml').read_text())['patch']['crates-io']['c']['path'] == str(bare/'c')
|
|
479
357
|
|
|
480
358
|
|
|
481
|
-
def
|
|
359
|
+
def test_sync_cargo_wrapper_preserves_existing(tmp_path):
|
|
482
360
|
config = tmp_path/'.cargo'/'config.toml'
|
|
483
361
|
config.parent.mkdir()
|
|
484
|
-
config.write_text('[term]\nquiet = true\n')
|
|
485
|
-
monkeypatch.setattr(core.shutil, 'which', lambda name: '/opt/homebrew/bin/sccache')
|
|
486
|
-
|
|
487
|
-
assert core._sync_cargo_wrapper(tmp_path)
|
|
488
|
-
data = core.tomllib.loads(config.read_text())
|
|
489
|
-
assert data['build']['rustc-wrapper'] == '/opt/homebrew/bin/sccache'
|
|
490
|
-
assert data['term']['quiet'] is True
|
|
491
|
-
assert not core._sync_cargo_wrapper(tmp_path)
|
|
492
|
-
|
|
493
|
-
(tmp_path/'crate').mkdir()
|
|
494
|
-
(tmp_path/'crate'/'Cargo.toml').write_text('[package]\nname = "crate"\n')
|
|
495
|
-
core._sync_cargo_patches(tmp_path)
|
|
496
|
-
content = config.read_text()
|
|
497
|
-
assert not core._sync_cargo_wrapper(tmp_path)
|
|
498
|
-
assert core._sync_cargo_patches(tmp_path) == ([], []) and config.read_text() == content
|
|
499
|
-
|
|
500
362
|
config.write_text('[build]\nrustc-wrapper = "other-cache"\n')
|
|
501
363
|
assert not core._sync_cargo_wrapper(tmp_path)
|
|
502
364
|
assert core.tomllib.loads(config.read_text())['build']['rustc-wrapper'] == 'other-cache'
|
|
@@ -527,54 +389,13 @@ def test_npm_dirs_honours_fastws_exclude(tmp_path):
|
|
|
527
389
|
assert core._npm_dirs(tmp_path) == [tmp_path/'lib', tmp_path/'py', tmp_path/'py'/'wasm']
|
|
528
390
|
|
|
529
391
|
|
|
530
|
-
def
|
|
531
|
-
pkg = tmp_path/'package.json'
|
|
532
|
-
pkg.write_text('{\n "name": "ws",\n "private": true,\n "workspaces": ["../outside", "gone", "tools/*"]\n}\n')
|
|
533
|
-
members = [tmp_path/'app', tmp_path/'crate'/'wasm']
|
|
534
|
-
|
|
535
|
-
added, removed = core._sync_ws_package_json(tmp_path, members)
|
|
536
|
-
data = json.loads(pkg.read_text())
|
|
537
|
-
# kept: entries outside the root and globs; managed: existing dirs regenerated from discovery
|
|
538
|
-
assert data['workspaces'] == ['../outside', 'tools/*', 'app', 'crate/wasm']
|
|
539
|
-
assert data['name'] == 'ws' and data['private'] is True # other keys untouched
|
|
540
|
-
assert added == ['app', 'crate/wasm'] and removed == ['gone']
|
|
541
|
-
|
|
542
|
-
content = pkg.read_text()
|
|
543
|
-
assert core._sync_ws_package_json(tmp_path, members) == ([], [])
|
|
544
|
-
assert pkg.read_text() == content
|
|
545
|
-
|
|
546
|
-
# a missing root package.json is created
|
|
547
|
-
bare = tmp_path/'ws2'
|
|
548
|
-
bare.mkdir()
|
|
549
|
-
assert core._sync_ws_package_json(bare, [bare/'a']) == (['a'], [])
|
|
550
|
-
assert json.loads((bare/'package.json').read_text()) == {'private': True, 'workspaces': ['a']}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
@pytest.mark.parametrize('workspaces', [{'packages': ['packages/*']}, {}])
|
|
554
|
-
def test_sync_ws_package_json_rejects_object_form(tmp_path, workspaces):
|
|
555
|
-
pkg = tmp_path/'package.json'
|
|
556
|
-
content = json.dumps({'private': True, 'workspaces': workspaces}) + '\n'
|
|
557
|
-
pkg.write_text(content)
|
|
558
|
-
|
|
559
|
-
with pytest.raises(SystemExit, match='workspaces.*list') as exc: core._sync_ws_package_json(tmp_path, [tmp_path/'app'])
|
|
560
|
-
assert str(pkg) in str(exc.value)
|
|
561
|
-
assert pkg.read_text() == content
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
def test_sync_ws_package_json_merges_shared_settings(tmp_path):
|
|
392
|
+
def test_sync_ws_package_json_preserves_local_and_merges_shared(tmp_path):
|
|
565
393
|
pkg, shared = tmp_path/'package.json', tmp_path/'package.json.shared'
|
|
566
|
-
pkg.write_text(json.dumps({'name': 'local', 'workspaces': ['
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
assert
|
|
570
|
-
|
|
571
|
-
'allowScripts': {'wasm-pack@0.15.0': True, 'other': False}}
|
|
572
|
-
content, mtime = pkg.read_text(), pkg.stat().st_mtime_ns
|
|
573
|
-
core._sync_ws_package_json(tmp_path, [tmp_path/'app'])
|
|
574
|
-
assert pkg.stat().st_mtime_ns == mtime
|
|
575
|
-
shared.write_text('{}')
|
|
576
|
-
core._sync_ws_package_json(tmp_path, [tmp_path/'app'])
|
|
577
|
-
assert pkg.read_text() == content
|
|
394
|
+
pkg.write_text(json.dumps({'name': 'local', 'workspaces': ['../outside', 'gone', 'tools/*'], 'allowScripts': {'other': False}}))
|
|
395
|
+
shared.write_text('{"allowScripts": {"wasm-pack": true}}')
|
|
396
|
+
assert core._sync_ws_package_json(tmp_path, [tmp_path/'app']) == (['app'], ['gone'])
|
|
397
|
+
assert json.loads(pkg.read_text()) == {'name': 'local', 'workspaces': ['../outside', 'tools/*', 'app'],
|
|
398
|
+
'allowScripts': {'other': False, 'wasm-pack': True}}
|
|
578
399
|
|
|
579
400
|
|
|
580
401
|
def test_ws_excludes_treat_npm_only_dirs_like_cargo_only(tmp_path):
|
|
@@ -588,44 +409,7 @@ def test_ws_excludes_treat_npm_only_dirs_like_cargo_only(tmp_path):
|
|
|
588
409
|
assert core._pending_dirs(tmp_path) == ['pending']
|
|
589
410
|
|
|
590
411
|
|
|
591
|
-
def
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
for d in (crate, wasm): (d/'Cargo.toml').write_text('')
|
|
596
|
-
(crate/'package.json').write_text('{}')
|
|
597
|
-
(wasm/'package.json').write_text('{"scripts": {"build": "cargo build"}}')
|
|
598
|
-
assert core._native_js([app, crate, wasm]) == [wasm]
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
def test_check_rustup_missing(monkeypatch):
|
|
602
|
-
monkeypatch.setattr(core.shutil, 'which', lambda tool: None)
|
|
603
|
-
with pytest.raises(SystemExit, match='Install it from https://rustup.rs'): core._check_rustup()
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
@pytest.mark.parametrize('shadowed', [None, 'rustc', 'cargo'])
|
|
607
|
-
def test_check_rustup_proxies(tmp_path, monkeypatch, shadowed):
|
|
608
|
-
rustup = tmp_path/'rustup'
|
|
609
|
-
rustup.touch()
|
|
610
|
-
(tmp_path/'rustc').symlink_to(rustup)
|
|
611
|
-
(tmp_path/'cargo').hardlink_to(rustup)
|
|
612
|
-
other = tmp_path/'brew-rust'
|
|
613
|
-
other.touch()
|
|
614
|
-
monkeypatch.setattr(core.shutil, 'which', lambda tool: str(other if tool == shadowed else tmp_path/tool))
|
|
615
|
-
if shadowed:
|
|
616
|
-
with pytest.raises(SystemExit, match=f'{shadowed} resolves to') as err: core._check_rustup()
|
|
617
|
-
assert 'Homebrew Rust can remain installed' in str(err.value)
|
|
618
|
-
assert 'PATH' in str(err.value)
|
|
619
|
-
else: core._check_rustup()
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
async def test_sync_checks_rustup_before_installs(tmp_path, monkeypatch, fake_uv):
|
|
623
|
-
(tmp_path/'repos.txt').write_text('')
|
|
624
|
-
(tmp_path/'pyproject.toml').write_text('[project]\nname = "ws"\n[tool.uv.workspace]\nmembers = ["*"]\n')
|
|
625
|
-
wasm = tmp_path/'wasm'
|
|
626
|
-
wasm.mkdir()
|
|
627
|
-
(wasm/'Cargo.toml').write_text('[package]\nname = "wasm"\n')
|
|
628
|
-
(wasm/'package.json').write_text('{"name": "wasm", "scripts": {"build": "wasm-pack build"}}')
|
|
629
|
-
monkeypatch.setattr(core.shutil, 'which', lambda tool: None)
|
|
630
|
-
with pytest.raises(SystemExit, match='require rustup'): await core.ws_sync(str(tmp_path))
|
|
631
|
-
assert not fake_uv
|
|
412
|
+
def test_check_rustup_rejects_shadowed_compiler(tmp_path, monkeypatch):
|
|
413
|
+
for tool in ('rustup', 'rustc'): (tmp_path/tool).touch(mode=0o755)
|
|
414
|
+
monkeypatch.setenv('PATH', str(tmp_path))
|
|
415
|
+
with pytest.raises(SystemExit, match='rustc resolves to'): core._check_rustup()
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import asyncio, pytest
|
|
2
|
-
|
|
3
|
-
from fastcore.basics import AttrDict
|
|
4
|
-
|
|
5
|
-
import fastws.releases as relmod
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _rel(tag): return AttrDict(tag_name=tag)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_newest_tag_by_version_not_publish_order():
|
|
12
|
-
assert relmod._newest_tag([_rel("v0.1.17"), _rel("v0.1.18"), _rel("v0.1.9")]) == "v0.1.18"
|
|
13
|
-
assert relmod._newest_tag([]) is None
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_skip_pats_defaults_and_config(tmp_path):
|
|
17
|
-
(tmp_path/"pyproject.toml").write_text('[tool.fastws]\nrelease_skip = ["docs only"]\n', encoding="utf-8")
|
|
18
|
-
pats = relmod._skip_pats("wip", root=tmp_path)
|
|
19
|
-
hits = lambda m: any(p.match(m) for p in pats)
|
|
20
|
-
assert hits("bump") and hits("Bump version to 0.2.2") and hits("update .gitignore")
|
|
21
|
-
assert hits("docs only: fix typo") # workspace config
|
|
22
|
-
assert hits("wip checkpoint") # per-call extra
|
|
23
|
-
assert not hits("bumpy road ahead") # bump$ is anchored
|
|
24
|
-
assert not hits("fixes #30")
|
|
25
|
-
for m in (".gitignore", "gitignore", "docs", "doc", "clean", "CI", "meta", "ignore", "allowed_metadata_keys"): assert hits(m)
|
|
26
|
-
for m in ("deps", "CI fixups", "document the API", "cleanup tests"): assert not hits(m) # deps IS release-worthy; anchored bare words only
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_release_report_repr():
|
|
30
|
-
rep = relmod.ReleaseReport([("mdhtml", ["fixes #30"]), ("solveit", None), ("fastcore", []), ("bad", ValueError("boom"))])
|
|
31
|
-
txt = repr(rep)
|
|
32
|
-
assert "mdhtml (1 unreleased):" in txt and " - fixes #30" in txt
|
|
33
|
-
assert "no releases: solveit" in txt
|
|
34
|
-
assert "up to date: fastcore" in txt
|
|
35
|
-
assert "bad: ERROR boom" in txt
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_release_exclude(tmp_path, monkeypatch):
|
|
39
|
-
(tmp_path/"repos.txt").write_text("AnswerDotAI/libx\nAnswerDotAI/appy\n", encoding="utf-8")
|
|
40
|
-
(tmp_path/"pyproject.toml").write_text('[tool.fastws]\nrelease_exclude = ["Appy"]\n', encoding="utf-8")
|
|
41
|
-
checked = []
|
|
42
|
-
async def fake(repo, skip=None): return checked.append(repo) or []
|
|
43
|
-
monkeypatch.setattr(relmod, "check_release", fake)
|
|
44
|
-
|
|
45
|
-
rep = asyncio.run(relmod.check_releases(workspace=str(tmp_path)))
|
|
46
|
-
|
|
47
|
-
assert checked == ["AnswerDotAI/libx"] # appy excluded, case-insensitively
|
|
48
|
-
assert [r for r, _ in rep] == ["libx"]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _member(tmp_path, dirname, pkg=None, deps=()):
|
|
52
|
-
d = tmp_path/dirname
|
|
53
|
-
d.mkdir()
|
|
54
|
-
dep_s = ", ".join(f'"{x}"' for x in deps)
|
|
55
|
-
(d/"pyproject.toml").write_text(f'[project]\nname = "{pkg or dirname}"\ndependencies = [{dep_s}]\n', encoding="utf-8")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def test_check_releases_scoping(tmp_path, monkeypatch):
|
|
59
|
-
(tmp_path/"repos.txt").write_text("AnswerDotAI/libx\nAnswerDotAI/appy\n", encoding="utf-8")
|
|
60
|
-
(tmp_path/"repos-local.txt").write_text("AnswerDotAI/exty\n", encoding="utf-8")
|
|
61
|
-
_member(tmp_path, "libx", pkg="libx-cli")
|
|
62
|
-
_member(tmp_path, "appy", deps=["libx-cli"])
|
|
63
|
-
_member(tmp_path, "exty")
|
|
64
|
-
checked = []
|
|
65
|
-
async def fake(repo, skip=None): return checked.append(repo) or []
|
|
66
|
-
monkeypatch.setattr(relmod, "check_release", fake)
|
|
67
|
-
run = lambda **kw: asyncio.run(relmod.check_releases(workspace=str(tmp_path), **kw))
|
|
68
|
-
|
|
69
|
-
monkeypatch.chdir(tmp_path)
|
|
70
|
-
run()
|
|
71
|
-
assert checked == ["AnswerDotAI/libx", "AnswerDotAI/appy", "AnswerDotAI/exty"] # at root: full sweep
|
|
72
|
-
|
|
73
|
-
checked.clear()
|
|
74
|
-
(tmp_path/"appy"/"sub").mkdir()
|
|
75
|
-
monkeypatch.chdir(tmp_path/"appy"/"sub")
|
|
76
|
-
run()
|
|
77
|
-
assert checked == ["AnswerDotAI/libx", "AnswerDotAI/appy"] # inside a member: its closure, dir<->package mapped
|
|
78
|
-
|
|
79
|
-
checked.clear()
|
|
80
|
-
run(nodeps=True)
|
|
81
|
-
assert checked == ["AnswerDotAI/appy"] # nodeps: the repo alone
|
|
82
|
-
|
|
83
|
-
checked.clear()
|
|
84
|
-
monkeypatch.chdir(tmp_path)
|
|
85
|
-
run(project="appy")
|
|
86
|
-
assert checked == ["AnswerDotAI/libx", "AnswerDotAI/appy"] # explicit project: closure, as before
|
|
87
|
-
|
|
88
|
-
checked.clear()
|
|
89
|
-
run(project="libx")
|
|
90
|
-
assert checked == ["AnswerDotAI/libx"] # explicit project by dir name maps to its package
|
|
91
|
-
|
|
92
|
-
checked.clear()
|
|
93
|
-
run(project="appy", nodeps=True)
|
|
94
|
-
assert checked == ["AnswerDotAI/appy"]
|
|
95
|
-
|
|
96
|
-
with pytest.raises(SystemExit): run(nodeps=True) # no project in play
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|