fix: wire checkpoint_dir through _get_path, fix status() and reset()#2
fix: wire checkpoint_dir through _get_path, fix status() and reset()#2himachhatbar17 wants to merge 4 commits intoShiv0087:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes custom checkpoint directory handling by threading a resolved base_dir through tracker path resolution, so progress/state/vars files are written/read from the intended directory instead of always using ~/.loopz/.
Changes:
- Add
base_dir: Optional[Path]plumbing to tracker path helpers and persistence functions (progress/state/vars + job listing). - Add
checkpoint_dirto@track(...), resolve it once, and passbase_dirthrough all tracker calls. - Update public helpers (
status,reset,reset_all) to acceptcheckpoint_dirand operate on the corresponding directory.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
loopz/tracker.py |
_get_path() now supports an optional base_dir; persistence and listing functions pass it through. |
loopz/decorator.py |
track(..., checkpoint_dir=...) resolves a base path once and uses it for all checkpoint reads/writes/clears. |
loopz/__init__.py |
status/reset/reset_all now accept checkpoint_dir and route calls via list_jobs/clear_* with base_dir. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def track( | ||
| job_name: str, | ||
| save_every: int = 10, | ||
| state: Optional[Dict] = None, | ||
| loop_vars: Optional[Dict] = None, | ||
| notify: Optional[Callable] = None, | ||
| checkpoint_dir: Optional[str] = None, # ✅ ADDED | ||
| ): |
There was a problem hiding this comment.
The new checkpoint_dir parameter is typed as Optional[str], but the implementation accepts any Path-like (e.g., Path, os.PathLike). To keep type hints accurate for public API users, consider widening the annotation (and docstring) to Optional[Union[str, Path]] / StrPath-style.
| def status(checkpoint_dir=None): | ||
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| jobs = list_jobs(base_dir=base_dir) | ||
| if not jobs: | ||
| print("\n✅ loopz: No saved jobs — all loops completed cleanly!\n") | ||
| print("\n📋 loopz — no saved jobs.\n") | ||
| return | ||
|
|
||
| print(f"\n📋 loopz — {len(jobs)} saved job(s):\n") | ||
| for job in jobs: | ||
| print(f" 🔁 {job['job_name']}") | ||
| print(f" Progress : {job['index']}/{job['total']} ({job['percent']}%)") | ||
| print(f" Saved at : {job['saved_at']}") | ||
| if job.get("meta", {}).get("error"): | ||
| print(f" Crashed : {job['meta']['error']}") | ||
| elapsed = job.get("meta", {}).get("elapsed_sec") | ||
| if elapsed: | ||
| print(f" Elapsed : {elapsed}s before crash") | ||
| for j in jobs: | ||
| print(f" 🔁 {j['job_name']}") | ||
| print(f" Progress : {j['index']}/{j['total']} ({j['percent']}%)") | ||
| print(f" Saved at : {j['saved_at']}") | ||
| if j.get("meta", {}).get("error"): | ||
| print(f" Crashed : {j['meta']['error']}") | ||
| print() | ||
|
|
||
|
|
||
| def reset(job_name: str): | ||
| """ | ||
| Delete ALL saved data for `job_name` — it will start fresh next run. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| job_name : str | ||
| The name passed to ``@loopz.track(job_name=...)``. | ||
|
|
||
| Example:: | ||
|
|
||
| loopz.reset("extract_features") | ||
| # → 🗑️ loopz: 'extract_features' cleared. Will start fresh next run. | ||
| """ | ||
| clear_progress(job_name) | ||
| clear_loop_vars(job_name) | ||
| print(f"🗑️ loopz: '{job_name}' cleared. Will start fresh next run.") | ||
|
|
||
|
|
||
| def reset_all(): | ||
| """ | ||
| Delete saved data for EVERY job. | ||
|
|
||
| Use with care — this cannot be undone. | ||
| def reset(job_name: str, checkpoint_dir=None): | ||
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| clear_progress(job_name, base_dir=base_dir) | ||
| clear_loop_vars(job_name, base_dir=base_dir) | ||
| print(f"🗑️ loopz: '{job_name}' reset.") | ||
|
|
||
| Example:: | ||
|
|
||
| loopz.reset_all() | ||
| # → 🗑️ loopz: All saved jobs cleared. | ||
| """ | ||
| jobs = list_jobs() | ||
| for job in jobs: | ||
| clear_progress(job["job_name"]) | ||
| clear_loop_vars(job["job_name"]) | ||
| n = len(jobs) | ||
| print(f"🗑️ loopz: All {n} saved job(s) cleared.") No newline at end of file | ||
| def reset_all(checkpoint_dir=None): | ||
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| for j in list_jobs(base_dir=base_dir): | ||
| clear_progress(j["job_name"], base_dir=base_dir) | ||
| clear_loop_vars(j["job_name"], base_dir=base_dir) | ||
| print("🗑️ loopz: all jobs reset.") No newline at end of file |
There was a problem hiding this comment.
The prior docstrings for status(), reset(), and reset_all() were removed. Since these are public helpers exported via __all__, please restore/update the docstrings to document parameters (including checkpoint_dir) and preserve the existing usage examples.
| def status(checkpoint_dir=None): | ||
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| jobs = list_jobs(base_dir=base_dir) | ||
| if not jobs: | ||
| print("\n✅ loopz: No saved jobs — all loops completed cleanly!\n") | ||
| print("\n📋 loopz — no saved jobs.\n") | ||
| return | ||
|
|
||
| print(f"\n📋 loopz — {len(jobs)} saved job(s):\n") | ||
| for job in jobs: | ||
| print(f" 🔁 {job['job_name']}") | ||
| print(f" Progress : {job['index']}/{job['total']} ({job['percent']}%)") | ||
| print(f" Saved at : {job['saved_at']}") | ||
| if job.get("meta", {}).get("error"): | ||
| print(f" Crashed : {job['meta']['error']}") | ||
| elapsed = job.get("meta", {}).get("elapsed_sec") | ||
| if elapsed: | ||
| print(f" Elapsed : {elapsed}s before crash") | ||
| for j in jobs: | ||
| print(f" 🔁 {j['job_name']}") | ||
| print(f" Progress : {j['index']}/{j['total']} ({j['percent']}%)") | ||
| print(f" Saved at : {j['saved_at']}") | ||
| if j.get("meta", {}).get("error"): | ||
| print(f" Crashed : {j['meta']['error']}") | ||
| print() | ||
|
|
||
|
|
||
| def reset(job_name: str): | ||
| """ | ||
| Delete ALL saved data for `job_name` — it will start fresh next run. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| job_name : str | ||
| The name passed to ``@loopz.track(job_name=...)``. | ||
|
|
||
| Example:: | ||
|
|
||
| loopz.reset("extract_features") | ||
| # → 🗑️ loopz: 'extract_features' cleared. Will start fresh next run. | ||
| """ | ||
| clear_progress(job_name) | ||
| clear_loop_vars(job_name) | ||
| print(f"🗑️ loopz: '{job_name}' cleared. Will start fresh next run.") | ||
|
|
||
|
|
||
| def reset_all(): | ||
| """ | ||
| Delete saved data for EVERY job. | ||
|
|
||
| Use with care — this cannot be undone. | ||
| def reset(job_name: str, checkpoint_dir=None): | ||
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| clear_progress(job_name, base_dir=base_dir) |
There was a problem hiding this comment.
checkpoint_dir is converted via Path(checkpoint_dir) without expanduser(), so ~/... won’t expand and will point at a different directory than users expect. Consider normalizing with Path(checkpoint_dir).expanduser() (and optionally .resolve()) before calling list_jobs / clear_*.
| def status(checkpoint_dir=None): | ||
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| jobs = list_jobs(base_dir=base_dir) |
There was a problem hiding this comment.
PR description mentions verification via test_fix.py, but this PR doesn’t add that file (and existing tests/test_hard.py has no coverage for checkpoint_dir). Please add/commit the described test(s), ideally exercising track(..., checkpoint_dir=...) + status(checkpoint_dir=...) + reset(..., checkpoint_dir=...) against a temp directory.
| # ✅ CHANGED: accepts optional base_dir instead of hardcoding CACHE_DIR | ||
| def _get_path(job_name: str, ext: str, base_dir: Optional[Path] = None) -> Path: | ||
| """Return a safe flat path inside base_dir (or CACHE_DIR if not given). | ||
| base_dir is resolved ONCE by the caller (decorator or public API) — | ||
| internal tracker functions never need to know about checkpoint_dir. |
There was a problem hiding this comment.
The # ✅ CHANGED / # ✅ ADDED markers look like review/diff annotations and add noise to the library source. Consider removing them (or converting to neutral comments) now that the change is merged, to keep the codebase clean and reduce churn in future diffs.
| # ✅ ADDED: resolve checkpoint_dir once here into a Path. | ||
| # Every internal call below uses _base_dir — the raw string | ||
| # never gets passed around. None means use ~/.loopz/ as before. | ||
| _base_dir: Optional[Path] = Path(checkpoint_dir) if checkpoint_dir else None |
There was a problem hiding this comment.
checkpoint_dir is converted via Path(checkpoint_dir) without expanduser(). This means values like ~/checkpoints won’t expand and will create a literal ~ directory relative to CWD. Consider normalizing with Path(checkpoint_dir).expanduser() (and optionally .resolve()) before passing it as base_dir.
| _base_dir: Optional[Path] = Path(checkpoint_dir) if checkpoint_dir else None | |
| _base_dir: Optional[Path] = ( | |
| Path(checkpoint_dir).expanduser() if checkpoint_dir else None | |
| ) |
|
The core fix is correct and exactly what we discussed — _get_path() accepting base_dir, resolving base_dir once Two small things before I merge:
Also please add these 3 tests to tests/hard_test.py:
Once those three things are done this is ready to merge. |
|
Copilot also caught some issues worth fixing in the same pass:
Fix these along with the docstrings and 3 tests and |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| clear_progress(job_name, base_dir=base_dir) | ||
| clear_loop_vars(job_name, base_dir=base_dir) |
There was a problem hiding this comment.
Similarly to status(), Path(checkpoint_dir) does not expand '', so reset(..., checkpoint_dir="/.loopz") may operate on an unexpected relative path. Consider applying expanduser() when converting checkpoint_dir to a Path.
| from pathlib import Path | ||
| base_dir = Path(checkpoint_dir) if checkpoint_dir else None | ||
| for j in list_jobs(base_dir=base_dir): | ||
| clear_progress(j["job_name"], base_dir=base_dir) |
There was a problem hiding this comment.
Similarly to status()/reset(), Path(checkpoint_dir) does not expand '', so reset_all(checkpoint_dir="/.loopz") may operate on an unexpected relative path. Consider applying expanduser() when converting checkpoint_dir to a Path.
| clear_progress(job_name, base_dir=base_dir) | ||
| clear_loop_vars(job_name, base_dir=base_dir) | ||
| print(f"🗑️ loopz: '{job_name}' reset.") |
There was a problem hiding this comment.
reset() calls clear_progress() and then clear_loop_vars(), but clear_progress() already deletes the .vars file (and .state) for the job. Consider removing the redundant clear_loop_vars() call (and similarly in reset_all) to avoid duplicated filesystem work and keep the deletion logic in one place.
| for j in list_jobs(base_dir=base_dir): | ||
| clear_progress(j["job_name"], base_dir=base_dir) | ||
| clear_loop_vars(j["job_name"], base_dir=base_dir) | ||
| print("🗑️ loopz: all jobs reset.") No newline at end of file |
There was a problem hiding this comment.
reset_all() calls both clear_progress() and clear_loop_vars(), but clear_progress() already removes the .vars file for each job. Consider dropping the redundant clear_loop_vars() call to avoid duplicated filesystem work and keep deletion semantics centralized in clear_progress().
…d 3 checkpoint_dir tests
Problem
checkpoint_dir created the folder but all files still wrote to ~/.loopz/
because _get_path() never received the custom path. status() and reset()
also hardcoded CACHE_DIR so they were blind to custom dirs entirely.
Fix
top of wrapper() — single resolution point, no string passed around
resolve and pass base_dir so they work correctly with custom dirs
Backward compatible
checkpoint_dir=None everywhere → falls back to ~/.loopz/ as before.
Zero breaking changes.
Verified
test_fix.py confirms:
✅ crash writes files to custom dir (not ~/.loopz/)
✅ status() finds the job in custom dir
✅ resume picks up from custom dir
✅ reset() clears from custom dir