fastws-cli 0.0.5__tar.gz → 0.0.6__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.
@@ -1,5 +1,12 @@
1
1
  <!-- do not remove -->
2
2
 
3
+ ## 0.0.6
4
+
5
+ ### New Features
6
+
7
+ - 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))
8
+
9
+
3
10
  ## 0.0.5
4
11
 
5
12
  ### New Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastws-cli
3
- Version: 0.0.5
3
+ Version: 0.0.6
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
 
@@ -0,0 +1,4 @@
1
+ __version__ = "0.0.6"
2
+
3
+ from .core import *
4
+
@@ -206,22 +206,26 @@ def _write_pyright_pth_files(root: Path) -> list[Path]:
206
206
  created.append(pth)
207
207
  return created
208
208
 
209
- def _clone_one(repo: str) -> str:
210
- d = _repo_dir(repo)
211
- if Path(d).exists(): return f"✓ {d}: already exists"
209
+ def _clone_one(repo: str, root: str = ".") -> str:
210
+ d = Path(root)/_repo_dir(repo)
211
+ if d.exists(): return f"✓ {d.name}: already exists"
212
212
  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()}"
213
+ subprocess.run(["git", "clone", f"git@github.com:{repo}.git", str(d)], check=True, capture_output=True)
214
+ return f"✓ {d.name}: cloned"
215
+ except subprocess.CalledProcessError as e: return f"✗ {d.name}: {e.stderr.decode().strip()}"
216
216
 
217
- def _pull_one(repo: str) -> str:
218
- d = _repo_dir(repo)
219
- if not Path(d).exists(): return f"✗ {d}: directory not found"
217
+ def _pull_one(repo: str, root: str = ".") -> str:
218
+ d = Path(root)/_repo_dir(repo)
219
+ if not d.exists(): return f"✗ {d.name}: directory not found"
220
220
  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 "")
221
+ res = subprocess.run(["git", "-C", str(d), "pull", "-q", "--stat"], check=True, capture_output=True, text=True)
222
+ return f"✓ {d.name}" + (f"\n{res.stdout.strip()}" if res.stdout.strip() else "")
223
223
  except subprocess.CalledProcessError as e: return f"✗ {d}: {e.stderr.strip()}"
224
224
 
225
+ def _pull(repos: list[str], workers: int = 16, root: str = "."):
226
+ with ThreadPoolExecutor(max_workers=workers) as ex:
227
+ for result in as_completed([ex.submit(_pull_one, r, root) for r in repos]): print(result.result())
228
+
225
229
  def ws_clone(
226
230
  repos_file: str = "repos.txt", # File containing repo list (one per line: owner/repo)
227
231
  workers: int = 16, # Number of parallel workers
@@ -235,22 +239,24 @@ def ws_clone(
235
239
  def ws_clone_cli(
236
240
  repos_file: str = "repos.txt", # File containing repo list (one per line: owner/repo)
237
241
  workers: int = 16, # Number of parallel workers
238
- ): ws_clone(repos_file, workers)
242
+ ):
243
+ "Clone all repos from a repos file."
244
+ ws_clone(repos_file, workers)
239
245
 
240
246
  def ws_pull(
241
247
  repos_file: str = "repos.txt", # File containing repo list
242
248
  workers: int = 16, # Number of parallel workers
243
249
  ):
244
250
  "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())
251
+ _pull(_load_repos(repos_file), workers)
248
252
 
249
253
  @call_parse
250
254
  def ws_pull_cli(
251
255
  repos_file: str = "repos.txt", # File containing repo list
252
256
  workers: int = 16, # Number of parallel workers
253
- ): ws_pull(repos_file, workers)
257
+ ):
258
+ "Pull updates for all repos."
259
+ ws_pull(repos_file, workers)
254
260
 
255
261
  def ws_status(
256
262
  repos_file: str = "repos.txt", # File containing repo list
@@ -281,7 +287,9 @@ def ws_status(
281
287
  def ws_status_cli(
282
288
  repos_file: str = "repos.txt", # File containing repo list
283
289
  branches: bool = False, # Show unpushed commit details
284
- ): ws_status(repos_file, branches)
290
+ ):
291
+ "Show uncommitted changes and optionally unpushed commit details across repos."
292
+ ws_status(repos_file, branches)
285
293
 
286
294
  def ws_branches(
287
295
  repos_file: str = "repos.txt", # File containing repo list
@@ -305,7 +313,9 @@ def ws_branches(
305
313
  def ws_branches_cli(
306
314
  repos_file: str = "repos.txt", # File containing repo list
307
315
  expected: str = "main", # Expected branch name
308
- ): ws_branches(repos_file, expected)
316
+ ):
317
+ "Check if all repos are on the expected branch."
318
+ ws_branches(repos_file, expected)
309
319
 
310
320
  def ws_sync(
311
321
  workspace: str = "", # Workspace root; defaults to active venv parent when available
@@ -318,8 +328,10 @@ def ws_sync(
318
328
  repos_path = _resolve_path(root, repos_file)
319
329
  pyproject_path = _resolve_path(root, pyproject_file)
320
330
  template_path = _resolve_path(root, template_file)
331
+ repos = _discover_ws_repos(root)
321
332
 
322
- if missing_repos := _update_repos_file(repos_path, _discover_ws_repos(root)): print(f"Added repos: {', '.join(missing_repos)}")
333
+ if missing_repos := _update_repos_file(repos_path, repos): print(f"Added repos: {', '.join(missing_repos)}")
334
+ _pull(repos, root=str(root))
323
335
 
324
336
  if missing_projects := _sync_ws_pyproject(pyproject_path, template_path, _ws_projects(root)): print(f"Added workspace projects: {', '.join(missing_projects)}")
325
337
 
@@ -332,7 +344,9 @@ def ws_sync_cli(
332
344
  repos_file: str = "repos.txt", # Repo list to update from local git remotes
333
345
  pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
334
346
  template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
335
- ): ws_sync(workspace, repos_file, pyproject_file, template_file)
347
+ ):
348
+ "Sync workspace metadata, run uv sync -U, and refresh Pyright editable paths."
349
+ ws_sync(workspace, repos_file, pyproject_file, template_file)
336
350
 
337
351
  def ws_add(
338
352
  repo: str, # Repo to add, e.g. AnswerDotAI/fastws
@@ -348,6 +362,7 @@ def ws_add(
348
362
  added = _update_repos_file(repos_path, [repo])
349
363
  if added: print(f"Added repo: {repo}")
350
364
  else: print(f"Repo already present: {repo}")
365
+ print(_clone_one(repo, str(root)))
351
366
  ws_sync(str(root), repos_file, pyproject_file, template_file)
352
367
 
353
368
  @call_parse
@@ -357,4 +372,6 @@ def ws_add_cli(
357
372
  repos_file: str = "repos.txt", # Repo list to update
358
373
  pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
359
374
  template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
360
- ): ws_add(repo, workspace, repos_file, pyproject_file, template_file)
375
+ ):
376
+ "Add a repo to repos.txt and then run ws-sync."
377
+ 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.6
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,6 +0,0 @@
1
- __version__ = "0.0.5"
2
-
3
-
4
-
5
- from .core import *
6
-
File without changes
File without changes
File without changes
File without changes