-
Notifications
You must be signed in to change notification settings - Fork 2
feat: first-run auto-init, overall progress, completion celebration (#13, #15, #16) #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ class WorkspaceError(RuntimeError): | |
|
|
||
|
|
||
| CURRICULUM_DIRS = ("exercises", "checks", "solutions") | ||
| DEFAULT_WORKSPACE_DIRNAME = "pylings-workspace" | ||
| GITIGNORE_LINES = [ | ||
| ".pylings/state.json", | ||
| ".pylings_debug.log", | ||
|
|
@@ -79,6 +80,26 @@ def init_workspace(path: Path, *, force: bool = False) -> Path: | |
| return path | ||
|
|
||
|
|
||
| def ensure_workspace(root: Path) -> tuple[Path, bool]: | ||
| """Return a usable workspace for `root`, creating one if needed. | ||
|
|
||
| - If `root` is already a workspace (has `info.toml`), use it as-is. | ||
| - Else if a previously auto-created `root/pylings-workspace` exists, reuse it. | ||
| - Otherwise initialize a fresh workspace at `root/pylings-workspace`. | ||
|
|
||
| Returns `(workspace_path, created_new)`. | ||
| """ | ||
| root = root.expanduser().resolve() | ||
| if (root / "info.toml").exists(): | ||
| return root, False | ||
|
|
||
| default = root / DEFAULT_WORKSPACE_DIRNAME | ||
| if (default / "info.toml").exists(): | ||
| return default.resolve(), False | ||
|
|
||
| return init_workspace(default), True | ||
|
Comment on lines
+96
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edge case: non-empty If 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def update_workspace(path: Path) -> Path: | ||
| path = path.expanduser().resolve() | ||
| if not (path / "info.toml").exists(): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def _run(*args: str) -> subprocess.CompletedProcess[str]: | ||
| return subprocess.run( | ||
| [sys.executable, "-m", "pylings", *args], | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
|
|
||
|
|
||
| def test_launch_in_empty_dir_autoinits_workspace(tmp_path: Path) -> None: | ||
| # `start` with a non-existent topic returns before launching the TUI, so we | ||
| # can assert the auto-init side effect without needing a terminal. | ||
| _run("--root", str(tmp_path), "start", "__no_such_topic__") | ||
|
|
||
| assert (tmp_path / "pylings-workspace" / "info.toml").exists() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pylings.screens.track import celebration_message | ||
|
|
||
|
|
||
| def test_celebration_message_includes_count_and_is_celebratory() -> None: | ||
| msg = celebration_message(292) | ||
|
|
||
| assert "292" in msg | ||
| assert "🎉" in msg |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pylings.widgets.progress import format_progress | ||
|
|
||
|
|
||
| def test_format_progress_shows_topic_and_overall() -> None: | ||
| line = format_progress(2, 5, 10, 100) | ||
|
|
||
| # topic portion: 2/5 == 40% | ||
| assert "2/5" in line | ||
| assert "40%" in line | ||
| # overall portion: 10/100 == 10% | ||
| assert "10/100" in line | ||
| assert "10%" in line | ||
| assert "Overall" in line | ||
|
|
||
|
|
||
| def test_format_progress_handles_zero_totals() -> None: | ||
| line = format_progress(0, 0, 0, 0) | ||
|
|
||
| assert "0/0" in line | ||
| assert "0%" in line |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from pylings.core.curriculum import ensure_workspace, init_workspace | ||
|
|
||
|
|
||
| def test_autoinits_workspace_in_empty_dir(tmp_path: Path) -> None: | ||
| work, created = ensure_workspace(tmp_path) | ||
|
|
||
| assert created is True | ||
| assert work == (tmp_path / "pylings-workspace").resolve() | ||
| assert (work / "info.toml").exists() | ||
|
|
||
|
|
||
| def test_returns_root_unchanged_when_already_a_workspace(tmp_path: Path) -> None: | ||
| init_workspace(tmp_path) | ||
|
|
||
| work, created = ensure_workspace(tmp_path) | ||
|
|
||
| assert created is False | ||
| assert work == tmp_path.resolve() | ||
|
|
||
|
|
||
| def test_reuses_previously_autoinited_workspace(tmp_path: Path) -> None: | ||
| first, first_created = ensure_workspace(tmp_path) | ||
| second, second_created = ensure_workspace(tmp_path) | ||
|
|
||
| assert first_created is True | ||
| assert second_created is False | ||
| assert second == first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle
WorkspaceErrorfromensure_workspacegracefully.ensure_workspacecan raiseWorkspaceError(e.g., a non-empty./pylings-workspacewithoutinfo.toml). The handler at lines 316-322 only convertsManifestError, so this surfaces as a raw traceback — the opposite of the friendly first-run behavior this PR targets.🛡️ Proposed fix
🤖 Prompt for AI Agents