fastws-cli 0.0.5__tar.gz → 0.0.7__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.
@@ -0,0 +1,31 @@
1
+ <!-- do not remove -->
2
+
3
+ ## 0.0.7
4
+
5
+ ### New Features
6
+
7
+ - Refactor Pyright editable path setup to write pyrightconfig.json instead of .pth files ([#4](https://github.com/AnswerDotAI/fastws/issues/4))
8
+
9
+
10
+ ## 0.0.6
11
+
12
+ ### New Features
13
+
14
+ - Add root-aware clone/pull helpers, auto-pull in ws-sync, and docstrings to CLI entry points ([#2](https://github.com/AnswerDotAI/fastws/issues/2))
15
+
16
+
17
+ ## 0.0.5
18
+
19
+ ### New Features
20
+
21
+ - Add ws-sync and ws-add commands with workspace metadata sync and Pyright editable path support ([#1](https://github.com/AnswerDotAI/fastws/issues/1))
22
+
23
+
24
+ ## 0.0.3
25
+
26
+ - change names
27
+
28
+ ## 0.0.2
29
+
30
+ - init release
31
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastws-cli
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: Fast workspace tools for multi-repo management
5
5
  Author: Jeremy Howard
6
6
  License: Apache-2.0
@@ -80,7 +80,7 @@ ws-branches --expected develop
80
80
 
81
81
  ### `ws-sync`
82
82
 
83
- Sync the workspace metadata, install updates, and refresh Pyright editable paths.
83
+ Sync the workspace metadata, pull local repos, install updates, and refresh Pyright editable paths.
84
84
  By default it uses the active venv parent as the workspace root, so you do not need to `cd` first:
85
85
  It respects `tool.uv.workspace.members` and `exclude` when scanning local projects.
86
86
 
@@ -59,7 +59,7 @@ ws-branches --expected develop
59
59
 
60
60
  ### `ws-sync`
61
61
 
62
- Sync the workspace metadata, install updates, and refresh Pyright editable paths.
62
+ Sync the workspace metadata, pull local repos, install updates, and refresh Pyright editable paths.
63
63
  By default it uses the active venv parent as the workspace root, so you do not need to `cd` first:
64
64
  It respects `tool.uv.workspace.members` and `exclude` when scanning local projects.
65
65
 
@@ -1,5 +1,4 @@
1
- __version__ = "0.0.5"
2
-
1
+ __version__ = "0.0.7"
3
2
 
4
3
 
5
4
  from .core import *
@@ -3,7 +3,7 @@
3
3
  __all__ = ["ws_clone", "ws_clone_cli", "ws_pull", "ws_pull_cli", "ws_status", "ws_status_cli", "ws_branches", "ws_branches_cli",
4
4
  "ws_sync", "ws_sync_cli", "ws_add", "ws_add_cli"]
5
5
 
6
- import ast, fnmatch, os, re, shutil, subprocess
6
+ import ast, fnmatch, json, os, re, shutil, subprocess
7
7
  from pathlib import Path
8
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
9
 
@@ -193,35 +193,38 @@ def _site_packages(root: Path) -> Path|None:
193
193
  if candidates: return candidates[0]
194
194
  return None
195
195
 
196
- def _write_pyright_pth_files(root: Path) -> list[Path]:
196
+ def _write_pyright_config(root: Path):
197
197
  site = _site_packages(root)
198
198
  if not site:
199
199
  print("No site-packages directory found for editable Pyright paths")
200
- return []
201
- created = []
202
- for finder in sorted(site.glob("__editable__*_finder.py")):
203
- for pkg,path in _editable_mapping(finder).items():
204
- pth = site/f"_pyright_editable_{pkg}.pth"
205
- pth.write_text(str(Path(path).parent) + "\n")
206
- created.append(pth)
207
- return created
208
-
209
- def _clone_one(repo: str) -> str:
210
- d = _repo_dir(repo)
211
- if Path(d).exists(): return f"✓ {d}: already exists"
200
+ return
201
+ paths = sorted({str(Path(p).parent) for f in site.glob("__editable__*_finder.py") for p in _editable_mapping(f).values()})
202
+ cfg_path = root / 'pyrightconfig.json'
203
+ cfg = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
204
+ cfg['extraPaths'] = paths
205
+ cfg_path.write_text(json.dumps(cfg, indent=2) + '\n')
206
+ for pth in site.glob('_pyright_editable_*.pth'): pth.unlink()
207
+
208
+ def _clone_one(repo: str, root: str = ".") -> str:
209
+ d = Path(root)/_repo_dir(repo)
210
+ if d.exists(): return f"✓ {d.name}: already exists"
212
211
  try:
213
- subprocess.run(["git", "clone", f"git@github.com:{repo}.git"], check=True, capture_output=True)
214
- return f"✓ {d}: cloned"
215
- except subprocess.CalledProcessError as e: return f"✗ {d}: {e.stderr.decode().strip()}"
212
+ subprocess.run(["git", "clone", f"git@github.com:{repo}.git", str(d)], check=True, capture_output=True)
213
+ return f"✓ {d.name}: cloned"
214
+ except subprocess.CalledProcessError as e: return f"✗ {d.name}: {e.stderr.decode().strip()}"
216
215
 
217
- def _pull_one(repo: str) -> str:
218
- d = _repo_dir(repo)
219
- if not Path(d).exists(): return f"✗ {d}: directory not found"
216
+ def _pull_one(repo: str, root: str = ".") -> str:
217
+ d = Path(root)/_repo_dir(repo)
218
+ if not d.exists(): return f"✗ {d.name}: directory not found"
220
219
  try:
221
- res = subprocess.run(["git", "-C", d, "pull", "-q", "--stat"], check=True, capture_output=True, text=True)
222
- return f"✓ {d}" + (f"\n{res.stdout.strip()}" if res.stdout.strip() else "")
220
+ res = subprocess.run(["git", "-C", str(d), "pull", "-q", "--stat"], check=True, capture_output=True, text=True)
221
+ return f"✓ {d.name}" + (f"\n{res.stdout.strip()}" if res.stdout.strip() else "")
223
222
  except subprocess.CalledProcessError as e: return f"✗ {d}: {e.stderr.strip()}"
224
223
 
224
+ def _pull(repos: list[str], workers: int = 16, root: str = "."):
225
+ with ThreadPoolExecutor(max_workers=workers) as ex:
226
+ for result in as_completed([ex.submit(_pull_one, r, root) for r in repos]): print(result.result())
227
+
225
228
  def ws_clone(
226
229
  repos_file: str = "repos.txt", # File containing repo list (one per line: owner/repo)
227
230
  workers: int = 16, # Number of parallel workers
@@ -235,22 +238,24 @@ def ws_clone(
235
238
  def ws_clone_cli(
236
239
  repos_file: str = "repos.txt", # File containing repo list (one per line: owner/repo)
237
240
  workers: int = 16, # Number of parallel workers
238
- ): ws_clone(repos_file, workers)
241
+ ):
242
+ "Clone all repos from a repos file."
243
+ ws_clone(repos_file, workers)
239
244
 
240
245
  def ws_pull(
241
246
  repos_file: str = "repos.txt", # File containing repo list
242
247
  workers: int = 16, # Number of parallel workers
243
248
  ):
244
249
  "Pull updates for all repos."
245
- repos = _load_repos(repos_file)
246
- with ThreadPoolExecutor(max_workers=workers) as ex:
247
- for result in as_completed([ex.submit(_pull_one, r) for r in repos]): print(result.result())
250
+ _pull(_load_repos(repos_file), workers)
248
251
 
249
252
  @call_parse
250
253
  def ws_pull_cli(
251
254
  repos_file: str = "repos.txt", # File containing repo list
252
255
  workers: int = 16, # Number of parallel workers
253
- ): ws_pull(repos_file, workers)
256
+ ):
257
+ "Pull updates for all repos."
258
+ ws_pull(repos_file, workers)
254
259
 
255
260
  def ws_status(
256
261
  repos_file: str = "repos.txt", # File containing repo list
@@ -281,7 +286,9 @@ def ws_status(
281
286
  def ws_status_cli(
282
287
  repos_file: str = "repos.txt", # File containing repo list
283
288
  branches: bool = False, # Show unpushed commit details
284
- ): ws_status(repos_file, branches)
289
+ ):
290
+ "Show uncommitted changes and optionally unpushed commit details across repos."
291
+ ws_status(repos_file, branches)
285
292
 
286
293
  def ws_branches(
287
294
  repos_file: str = "repos.txt", # File containing repo list
@@ -305,7 +312,9 @@ def ws_branches(
305
312
  def ws_branches_cli(
306
313
  repos_file: str = "repos.txt", # File containing repo list
307
314
  expected: str = "main", # Expected branch name
308
- ): ws_branches(repos_file, expected)
315
+ ):
316
+ "Check if all repos are on the expected branch."
317
+ ws_branches(repos_file, expected)
309
318
 
310
319
  def ws_sync(
311
320
  workspace: str = "", # Workspace root; defaults to active venv parent when available
@@ -313,18 +322,20 @@ def ws_sync(
313
322
  pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
314
323
  template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
315
324
  ):
316
- "Sync workspace metadata, run uv sync -U, and refresh Pyright editable paths."
325
+ "Sync workspace metadata, run uv sync -U, and update Pyright config."
317
326
  root = _ws_root(workspace, repos_file, pyproject_file, template_file)
318
327
  repos_path = _resolve_path(root, repos_file)
319
328
  pyproject_path = _resolve_path(root, pyproject_file)
320
329
  template_path = _resolve_path(root, template_file)
330
+ repos = _discover_ws_repos(root)
321
331
 
322
- if missing_repos := _update_repos_file(repos_path, _discover_ws_repos(root)): print(f"Added repos: {', '.join(missing_repos)}")
332
+ if missing_repos := _update_repos_file(repos_path, repos): print(f"Added repos: {', '.join(missing_repos)}")
333
+ _pull(repos, root=str(root))
323
334
 
324
335
  if missing_projects := _sync_ws_pyproject(pyproject_path, template_path, _ws_projects(root)): print(f"Added workspace projects: {', '.join(missing_projects)}")
325
336
 
337
+ _write_pyright_config(root)
326
338
  subprocess.run(["uv", "sync", "-U"], check=True, cwd=root)
327
- _write_pyright_pth_files(root)
328
339
 
329
340
  @call_parse
330
341
  def ws_sync_cli(
@@ -332,7 +343,9 @@ def ws_sync_cli(
332
343
  repos_file: str = "repos.txt", # Repo list to update from local git remotes
333
344
  pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
334
345
  template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
335
- ): ws_sync(workspace, repos_file, pyproject_file, template_file)
346
+ ):
347
+ "Sync workspace metadata, run uv sync -U, and update Pyright config."
348
+ ws_sync(workspace, repos_file, pyproject_file, template_file)
336
349
 
337
350
  def ws_add(
338
351
  repo: str, # Repo to add, e.g. AnswerDotAI/fastws
@@ -348,6 +361,7 @@ def ws_add(
348
361
  added = _update_repos_file(repos_path, [repo])
349
362
  if added: print(f"Added repo: {repo}")
350
363
  else: print(f"Repo already present: {repo}")
364
+ print(_clone_one(repo, str(root)))
351
365
  ws_sync(str(root), repos_file, pyproject_file, template_file)
352
366
 
353
367
  @call_parse
@@ -357,4 +371,6 @@ def ws_add_cli(
357
371
  repos_file: str = "repos.txt", # Repo list to update
358
372
  pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
359
373
  template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
360
- ): ws_add(repo, workspace, repos_file, pyproject_file, template_file)
374
+ ):
375
+ "Add a repo to repos.txt and then run ws-sync."
376
+ ws_add(repo, workspace, repos_file, pyproject_file, template_file)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastws-cli
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: Fast workspace tools for multi-repo management
5
5
  Author: Jeremy Howard
6
6
  License: Apache-2.0
@@ -80,7 +80,7 @@ ws-branches --expected develop
80
80
 
81
81
  ### `ws-sync`
82
82
 
83
- Sync the workspace metadata, install updates, and refresh Pyright editable paths.
83
+ Sync the workspace metadata, pull local repos, install updates, and refresh Pyright editable paths.
84
84
  By default it uses the active venv parent as the workspace root, so you do not need to `cd` first:
85
85
  It respects `tool.uv.workspace.members` and `exclude` when scanning local projects.
86
86
 
@@ -1,5 +1,3 @@
1
- from pathlib import Path
2
-
3
1
  import fastws.core as core
4
2
 
5
3
 
@@ -98,6 +96,9 @@ def test_ws_sync_updates_workspace_and_runs_uv(tmp_path, monkeypatch):
98
96
  if cmd[:5] == ["git", "-C", str(repo), "remote", "get-url"]:
99
97
  class Res: stdout = "git@github.com:AnswerDotAI/repo1.git\n"
100
98
  return Res()
99
+ if cmd[:4] == ["git", "-C", str(repo), "pull"]:
100
+ class Res: stdout = ""
101
+ return Res()
101
102
  if cmd == ["uv", "sync", "-U"]:
102
103
  class Res: stdout = ""
103
104
  return Res()
@@ -112,6 +113,7 @@ def test_ws_sync_updates_workspace_and_runs_uv(tmp_path, monkeypatch):
112
113
  assert 'newpkg = { workspace = true }' in pyproject
113
114
  assert '"newpkg"' in pyproject
114
115
  assert (site/"_pyright_editable_newpkg.pth").read_text() == "/tmp/ws/src/newpkg\n"
116
+ assert next(i for i,(cmd,_) in enumerate(calls) if cmd[:4] == ["git", "-C", str(repo), "pull"]) < next(i for i,(cmd,_) in enumerate(calls) if cmd == ["uv", "sync", "-U"])
115
117
  assert any(cmd == ["uv", "sync", "-U"] and kwargs["cwd"] == tmp_path for cmd,kwargs in calls)
116
118
 
117
119
 
@@ -138,6 +140,9 @@ def test_ws_sync_uses_active_venv_parent_by_default(tmp_path, monkeypatch):
138
140
  if cmd[:5] == ["git", "-C", str(repo), "remote", "get-url"]:
139
141
  class Res: stdout = "git@github.com:AnswerDotAI/repo1.git\n"
140
142
  return Res()
143
+ if cmd[:4] == ["git", "-C", str(repo), "pull"]:
144
+ class Res: stdout = ""
145
+ return Res()
141
146
  if cmd == ["uv", "sync", "-U"]:
142
147
  class Res: stdout = ""
143
148
  return Res()
@@ -153,19 +158,23 @@ def test_ws_sync_uses_active_venv_parent_by_default(tmp_path, monkeypatch):
153
158
  assert any(cmd == ["uv", "sync", "-U"] and kwargs["cwd"] == workspace for cmd,kwargs in calls)
154
159
 
155
160
 
156
- def test_ws_add_updates_repos_then_runs_sync(tmp_path, monkeypatch):
161
+ def test_ws_add_clones_and_syncs(tmp_path, monkeypatch):
157
162
  (tmp_path/"repos.txt").write_text("AnswerDotAI/existing\n")
158
- calls = []
163
+ sync_calls, clone_calls = [], []
159
164
 
160
- def fake_sync(workspace, repos_file, pyproject_file, template_file):
161
- calls.append((workspace, repos_file, pyproject_file, template_file))
165
+ def fake_sync(*a): sync_calls.append(a)
166
+ def fake_clone(repo, root="."):
167
+ clone_calls.append((repo, root))
168
+ return "✓ cloned"
162
169
 
163
170
  monkeypatch.setattr(core, "ws_sync", fake_sync)
171
+ monkeypatch.setattr(core, "_clone_one", fake_clone)
164
172
 
165
173
  core.ws_add("answerdotai/fastws", workspace=str(tmp_path))
166
174
 
167
175
  assert (tmp_path/"repos.txt").read_text() == "AnswerDotAI/existing\nanswerdotai/fastws\n"
168
- assert calls == [(str(tmp_path), "repos.txt", "pyproject.toml", "pyproject.tmpl")]
176
+ assert clone_calls == [("answerdotai/fastws", str(tmp_path))]
177
+ assert sync_calls == [(str(tmp_path), "repos.txt", "pyproject.toml", "pyproject.tmpl")]
169
178
 
170
179
 
171
180
  def test_ws_status_summarizes_unpushed_commits_by_default(tmp_path, monkeypatch, capsys):
@@ -1,17 +0,0 @@
1
- <!-- do not remove -->
2
-
3
- ## 0.0.5
4
-
5
- ### New Features
6
-
7
- - Add ws-sync and ws-add commands with workspace metadata sync and Pyright editable path support ([#1](https://github.com/AnswerDotAI/fastws/issues/1))
8
-
9
-
10
- ## 0.0.3
11
-
12
- - change names
13
-
14
- ## 0.0.2
15
-
16
- - init release
17
-
File without changes
File without changes
File without changes
File without changes