fastws-cli 0.0.17__tar.gz → 0.0.18__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.18}/PKG-INFO +3 -3
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/README.md +1 -1
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws/__init__.py +2 -1
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws/core.py +11 -2
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws_cli.egg-info/PKG-INFO +3 -3
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws_cli.egg-info/requires.txt +1 -1
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/pyproject.toml +1 -1
- fastws_cli-0.0.18/tests/test_releases.py +28 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/tests/test_sync.py +17 -233
- fastws_cli-0.0.17/tests/test_releases.py +0 -96
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/LICENSE +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws/releases.py +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws_cli.egg-info/SOURCES.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/setup.cfg +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/tests/test_build.py +0 -0
- {fastws_cli-0.0.17 → fastws_cli-0.0.18}/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.18
|
|
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
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -694,11 +694,14 @@ def _sync_cargo_patches(root: Path) -> tuple[list[str], list[str]]:
|
|
|
694
694
|
|
|
695
695
|
def _sync_cargo_wrapper(root: Path) -> bool:
|
|
696
696
|
"Add sccache to the generated Cargo config when installed, without overriding another wrapper"
|
|
697
|
-
if not (wrapper := shutil.which("sccache")): return False
|
|
698
697
|
config = root/".cargo"/"config.toml"
|
|
699
698
|
content = config.read_text() if config.exists() else ""
|
|
700
699
|
data = tomllib.loads(content) if content else {}
|
|
701
700
|
if data.get("build", {}).get("rustc-wrapper"): return False
|
|
701
|
+
if not (wrapper := shutil.which("sccache")):
|
|
702
|
+
print("⚠️ sccache not found; Rust builds won't use compiler caching.\n"
|
|
703
|
+
"Install it with: cargo install sccache --locked\nThen rerun ws-sync to enable it.")
|
|
704
|
+
return False
|
|
702
705
|
line = f'rustc-wrapper = {json.dumps(wrapper)}\n'
|
|
703
706
|
if match := re.search(r"(?m)^\[build\][^\n]*\n", content): new = content[:match.end()] + line + content[match.end():]
|
|
704
707
|
else: new = content.rstrip() + ("\n\n" if content.strip() else "") + "[build]\n" + line
|
|
@@ -839,7 +842,13 @@ def _npm_install(root: Path):
|
|
|
839
842
|
def _sync_js(root: Path, members: list[Path]) -> list[Path]:
|
|
840
843
|
"Install the JS workspace, run every native member's build script, and return those members. Cargo handles incremental compilation."
|
|
841
844
|
tool = _fastws_cfg(root).get("js", "npm")
|
|
842
|
-
if not shutil.which(tool):
|
|
845
|
+
if not shutil.which(tool):
|
|
846
|
+
msg = f"{tool} is not installed: install it, or set [tool.fastws].js to a package manager that is"
|
|
847
|
+
if tool == "npm": msg += ("\n\nInstall Node.js and npm using nvm:\n"
|
|
848
|
+
"curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash\n"
|
|
849
|
+
"# Restart your terminal, then:\n"
|
|
850
|
+
"nvm install --lts")
|
|
851
|
+
raise SystemExit(msg)
|
|
843
852
|
if tool == "npm": _npm_install(root)
|
|
844
853
|
else: subprocess.run([tool, "install"], check=True, cwd=root)
|
|
845
854
|
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.18
|
|
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
|
|
@@ -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'):
|
|
@@ -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
|