Skip to content

fix: wire checkpoint_dir through _get_path, fix status() and reset()#2

Open
himachhatbar17 wants to merge 4 commits intoShiv0087:mainfrom
himachhatbar17:fix-checkpoint-dir
Open

fix: wire checkpoint_dir through _get_path, fix status() and reset()#2
himachhatbar17 wants to merge 4 commits intoShiv0087:mainfrom
himachhatbar17:fix-checkpoint-dir

Conversation

@himachhatbar17
Copy link
Copy Markdown

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

  • _get_path() now accepts base_dir: Optional[Path] = None
  • All 8 internal tracker functions accept base_dir and pass it down
  • decorator.py resolves checkpoint_dir once into _base_dir at the
    top of wrapper() — single resolution point, no string passed around
  • status(checkpoint_dir=) and reset(job_name, checkpoint_dir=) now
    resolve and pass base_dir so they work correctly with custom dirs
  • reset_all(checkpoint_dir=) updated the same way

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

@Shiv0087 Shiv0087 requested a review from Copilot April 25, 2026 16:06
@Shiv0087 Shiv0087 added the enhancement New feature or request label Apr 25, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_dir to @track(...), resolve it once, and pass base_dir through all tracker calls.
  • Update public helpers (status, reset, reset_all) to accept checkpoint_dir and 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.

Comment thread loopz/decorator.py
Comment on lines 63 to 70
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
):
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread loopz/__init__.py Outdated
Comment on lines +85 to +116
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
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread loopz/__init__.py
Comment on lines +85 to +105
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)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_*.

Copilot uses AI. Check for mistakes.
Comment thread loopz/__init__.py
Comment on lines +85 to +88
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)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread loopz/tracker.py Outdated
Comment on lines +39 to +43
# ✅ 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.
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread loopz/decorator.py Outdated
# ✅ 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
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
_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
)

Copilot uses AI. Check for mistakes.
@Shiv0087
Copy link
Copy Markdown
Owner

The core fix is correct and exactly what we discussed —
thank you for implementing this properly.

_get_path() accepting base_dir, resolving base_dir once
at the top of wrapper(), and updating all 8 internal
functions are all right. The bonus fix to list_jobs()
using loopz
*.json instead of *.json is a good catch too.

Two small things before I merge:

  1. Please restore the docstrings on reset() and reset_all()
    that were removed — just add checkpoint_dir to them.
    Users see these when they call help(loopz.reset).

  2. Please revert the status() "no saved jobs" message back
    to the original:
    ✅ loopz: No saved jobs — all loops completed cleanly!
    The new message removes information without reason.

Also please add these 3 tests to tests/hard_test.py:

  • crash writes files to custom dir, not ~/.loopz/
  • status(checkpoint_dir=) finds the job in custom dir
  • reset(job_name, checkpoint_dir=) clears from custom dir

Once those three things are done this is ready to merge.
Great contribution.

@Shiv0087
Copy link
Copy Markdown
Owner

Copilot also caught some issues worth fixing in the same pass:

  1. status() has a duplicate print loop — jobs prints twice.
    Please remove the second for loop (the one using j).

  2. Path(checkpoint_dir) needs .expanduser() in both
    decorator.py and init.py so ~/my_dir works correctly.
    Copilot already provided the exact fix for decorator.py —
    apply the same to init.py status(), reset(), reset_all().

  3. Remove the ✅ CHANGED / ✅ ADDED comments from the
    source code — those are diff annotations, not production
    comments. Clean source only.

  4. Widen checkpoint_dir type hint from Optional[str] to
    Optional[Union[str, os.PathLike]] in track().

Fix these along with the docstrings and 3 tests and
this merges immediately.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread loopz/__init__.py
Comment on lines +143 to +146
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)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread loopz/__init__.py
Comment on lines +169 to +172
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)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread loopz/__init__.py Outdated
Comment on lines +145 to +147
clear_progress(job_name, base_dir=base_dir)
clear_loop_vars(job_name, base_dir=base_dir)
print(f"🗑️ loopz: '{job_name}' reset.")
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread loopz/__init__.py Outdated
Comment on lines +171 to +174
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
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants