Skip to content

fix: wrap malformed info.toml failures in ManifestError - #54

Open
agu2347 wants to merge 2 commits into
abhiksark:mainfrom
agu2347:fix/friendly-manifest-errors
Open

fix: wrap malformed info.toml failures in ManifestError#54
agu2347 wants to merge 2 commits into
abhiksark:mainfrom
agu2347:fix/friendly-manifest-errors

Conversation

@agu2347

@agu2347 agu2347 commented Aug 9, 2026

Copy link
Copy Markdown

Closes #44.

manifest.load() already had a ManifestError type and the CLI already had a top-level handler in cli.py::main() that catches ManifestError specifically, prints a friendly pythonlings: ... message, and exits 2 — but several failure modes never went through ManifestError, so they fell through to the generic raise and surfaced a raw traceback instead:

  • Invalid TOML syntax raised tomllib.TOMLDecodeError directly.
  • A missing name or path field in an [[exercises]] entry raised KeyError (via entry["name"] / entry["path"]).
  • A wrongly-typed field (e.g. name = 123) would eventually hit a TypeError deeper in the loop.
  • An absolute path (e.g. /etc/passwd) or a path containing .. components (e.g. exercises/../../etc/passwd) was not explicitly rejected — path.parts[0] != "exercises" happens to reject absolute paths on POSIX as a side effect, but a ..-traversal path starting with exercises/ slipped past that check entirely and could reach outside the workspace once resolved.

This PR wraps all of these in ManifestError with contextual messages, checked before any filesystem access for the path-safety cases, so they're handled the same way as the failure modes that already worked (missing info.toml, bad format_version, empty exercises list, duplicate names, missing exercise/check files).

No schema changes, no change to valid-manifest behavior — confirmed via the existing fixture-based tests plus new ones below.

Tests added:

  • tests/unit/test_manifest.py: invalid TOML syntax, missing name, wrong-typed name, missing path, absolute path, traversal path (6 new tests, all raising ManifestError with a message matching what the CLI already expects).
  • tests/integration/test_cli_verify.py: malformed TOML and traversal-path cases both asserted to exit 2 with pythonlings:-prefixed stderr and no Traceback text (2 new tests).

Verification:

python -m pytest tests/unit/test_manifest.py tests/integration/test_cli_verify.py -q

30 passed (18 pre-existing + 6 new unit tests + 2 new integration tests, plus the 4 pre-existing integration tests in that file). Also ran ruff check on the three changed files — clean except one pre-existing, unrelated warning in test_cli_verify.py's already-there _run() helper (not touched by this diff).

Summary by CodeRabbit

  • Bug Fixes

    • Improved manifest validation for malformed TOML, missing or invalid exercise fields, empty exercise lists, absolute paths, directory traversal, and symlink-based escapes.
    • CLI verification now reports clear, prefixed errors without displaying tracebacks for invalid manifests.
  • Tests

    • Added coverage for malformed manifests, invalid exercise definitions, unsafe paths, and workspace escapes.

load() previously let several failure modes escape as raw exceptions
instead of ManifestError, so the CLI's top-level ManifestError handler
(which prints a friendly `pythonlings: ...` message and exits 2)
never caught them and a full traceback leaked to the user instead:

- invalid TOML syntax raised tomllib.TOMLDecodeError
- a missing/wrongly-typed `name` or `path` field raised KeyError or
  a downstream TypeError
- an absolute or `..`-traversal path was not explicitly rejected and
  could reach outside the exercises/ tree

All three now raise a contextual ManifestError before any unsafe
filesystem access, matching the existing behavior for the
already-handled cases (missing info.toml, bad format_version, empty
exercises list, duplicate names, missing exercise/check files).

Adds unit tests for each new rejection path plus two CLI integration
tests asserting exit code 2 with no traceback text in stderr.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The manifest loader now reports malformed TOML, invalid exercise fields, and unsafe exercise or check paths as ManifestError. Unit and CLI tests verify contextual errors, exit status 2, the pythonlings: prefix, and the absence of tracebacks.

Changes

Manifest safety

Layer / File(s) Summary
Manifest parsing and field validation
pythonlings/core/manifest.py, tests/unit/test_manifest.py, tests/integration/test_cli_verify.py
TOML decoding errors and invalid exercises entries now raise ManifestError. Tests cover malformed TOML and missing or incorrectly typed name and path fields.
Exercise and check path validation
pythonlings/core/manifest.py, tests/unit/test_manifest.py, tests/integration/test_cli_verify.py
Exercise paths must be relative, remain under exercises/, and not escape through symlinks. Derived check paths must remain under checks/. Tests cover absolute, traversal, and symlink escape paths through the loader and CLI.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ManifestLoader
  participant Filesystem
  CLI->>ManifestLoader: load info.toml
  ManifestLoader->>Filesystem: parse and resolve manifest paths
  Filesystem-->>ManifestLoader: parsed data or path error
  ManifestLoader-->>CLI: ManifestError
  CLI-->>CLI: print pythonlings: error and exit 2
Loading

Suggested reviewers: abhiksark

Poem

I’m a rabbit with a tidy TOML trail,
No broken paths can make me fail.
Errors hop out, clear and bright,
Tracebacks vanish from CLI sight.
Safe exercises, all tucked in right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: wrapping malformed manifest parsing failures in ManifestError.
Description check ✅ Passed The description explains the changes, tests, verification results, and behavior impact, but it omits the template headings and checklist.
Linked Issues check ✅ Passed The changes satisfy issue #44 by validating manifest fields, rejecting unsafe paths, preserving valid behavior, and testing friendly CLI errors.
Out of Scope Changes check ✅ Passed The modified implementation and tests remain within issue #44 scope, including the explicitly required symlink escape protection.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pythonlings/core/manifest.py`:
- Around line 93-102: Update manifest path validation around rel_path to resolve
each exercise and derived checks candidate after the existing lexical checks,
then require the resolved paths to remain relative to the resolved workspace
exercises/ and checks/ directories respectively before calling exists() or
downstream access. Reject symlink escapes with ManifestError while preserving
valid in-tree paths.

In `@tests/unit/test_manifest.py`:
- Around line 215-218: Update the pytest.raises match pattern in
test_load_rejects_invalid_toml_syntax to use a raw regex with the dot in
“info.toml” escaped, preserving the existing filename assertion while satisfying
Ruff RUF043.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e9f7aa2a-4c7a-4d4f-a14f-43304a399b85

📥 Commits

Reviewing files that changed from the base of the PR and between e3bdcc9 and 3636546.

📒 Files selected for processing (3)
  • pythonlings/core/manifest.py
  • tests/integration/test_cli_verify.py
  • tests/unit/test_manifest.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
tests/integration/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep integration tests in tests/integration/ directory

Files:

  • tests/integration/test_cli_verify.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python 3.11+ idioms in all Python code
Use 4-space indentation in all Python code
Prefer small, typed functions where practical in Python code

**/*.py: Guard newer-stdlib usage with requires-python = ">=3.9" and use fallbacks (e.g. tomllib falls back to tomli) in modules like core/manifest.py
Include from __future__ import annotations at the top of Python modules

Files:

  • tests/integration/test_cli_verify.py
  • tests/unit/test_manifest.py
  • pythonlings/core/manifest.py
tests/**/*test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

Name test files as test_<behavior>.py

Files:

  • tests/integration/test_cli_verify.py
  • tests/unit/test_manifest.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Name test functions as test_<expected_behavior>
Use pytest for all tests with pytest-asyncio in auto mode for async tests

Files:

  • tests/integration/test_cli_verify.py
  • tests/unit/test_manifest.py
**/test_*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Name tests as test_<behavior>.py or test_<expected_behavior> (e.g., test_runner.py, test_state.py)

Files:

  • tests/integration/test_cli_verify.py
  • tests/unit/test_manifest.py
tests/unit/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep unit tests in tests/unit/ directory

Files:

  • tests/unit/test_manifest.py
pythonlings/core/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Core exercise loading, workspace setup, state, reset, solutions, and runner logic must live in pythonlings/core/ directory

Keep UI behavior in screens/ and widgets/ modules; keep behavior logic in core/ modules—do not import UI in core

Files:

  • pythonlings/core/manifest.py
pythonlings/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep UI behavior in screens/ or widgets/ directories; keep filesystem, manifest, reset, and runner behavior in core/

Files:

  • pythonlings/core/manifest.py
🪛 Ruff (0.16.1)
tests/unit/test_manifest.py

[warning] 217-217: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

🔇 Additional comments (3)
pythonlings/core/manifest.py (1)

58-91: LGTM!

tests/unit/test_manifest.py (1)

221-281: LGTM!

tests/integration/test_cli_verify.py (1)

69-90: LGTM!

Comment thread pythonlings/core/manifest.py
Comment thread tests/unit/test_manifest.py
The lexical path checks (no absolute path, no '..' components, starts with exercises/) accept exercises/link/file.py even when 'link' is a symlink pointing outside the workspace -- CodeRabbit correctly flagged that a value can look clean lexically and still resolve elsewhere. Now resolve() both the exercise path and the derived check path and confirm they stay within the resolved exercises/ and checks/ directories before touching the filesystem further, raising ManifestError otherwise. Added a regression test that creates a real symlink escaping the workspace and confirms it raises; reverting the fix makes this test fail (with a different, wrong error), proving it's a real check.

Also fixed the RUF043 warning on the new test_load_rejects_invalid_toml_syntax test: match="info.toml" treated '.' as a regex wildcard; changed to the raw/escaped match=r"info\.toml".

tests/unit/test_manifest.py + tests/integration/test_cli_verify.py: 31 passed. ruff check: clean (the one PLW1510 warning ruff reports is pre-existing in test_cli_verify.py's _run() helper, untouched by this diff, as already noted in the original PR).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pythonlings/core/manifest.py (1)

58-61: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wrap info.toml reads and decodes into ManifestError.

Move the info_path.open("rb") call inside the try, then catch OSError for read failures. Also catch UnicodeDecodeError before tomllib.TOMLDecodeError and convert both to ManifestError while preserving the original exception as the cause.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pythonlings/core/manifest.py` around lines 58 - 61, Update the
manifest-loading method around info_path.open and tomllib.load: move the binary
open call inside the existing try, catch OSError and UnicodeDecodeError, and
convert each to ManifestError while chaining the original exception. Keep the
tomllib.TOMLDecodeError conversion and ensure UnicodeDecodeError is handled
before the TOML decode exception.
🧹 Nitpick comments (1)
tests/unit/test_manifest.py (1)

284-308: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add coverage for the derived check-path escape.

This test fails during exercise-path validation. It does not execute the derived check-path containment check in pythonlings/core/manifest.py lines 126-134. Add a test with a regular exercise file and a checks/<topic> symlink that resolves outside the workspace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_manifest.py` around lines 284 - 308, Add a separate
manifest-loading test covering derived check-path containment, using a regular
exercise file and a checks/<topic> symlink targeting a directory outside the
workspace. Ensure the exercise path passes validation, the derived check path
resolves through the symlink, and load raises ManifestError with the existing
“escapes the workspace” message from the check-path validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pythonlings/core/manifest.py`:
- Around line 58-61: Update the manifest-loading method around info_path.open
and tomllib.load: move the binary open call inside the existing try, catch
OSError and UnicodeDecodeError, and convert each to ManifestError while chaining
the original exception. Keep the tomllib.TOMLDecodeError conversion and ensure
UnicodeDecodeError is handled before the TOML decode exception.

---

Nitpick comments:
In `@tests/unit/test_manifest.py`:
- Around line 284-308: Add a separate manifest-loading test covering derived
check-path containment, using a regular exercise file and a checks/<topic>
symlink targeting a directory outside the workspace. Ensure the exercise path
passes validation, the derived check path resolves through the symlink, and load
raises ManifestError with the existing “escapes the workspace” message from the
check-path validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3258cb78-771c-416d-9659-dc89b53944d4

📥 Commits

Reviewing files that changed from the base of the PR and between 3636546 and 4f041b1.

📒 Files selected for processing (2)
  • pythonlings/core/manifest.py
  • tests/unit/test_manifest.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
tests/unit/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep unit tests in tests/unit/ directory

Files:

  • tests/unit/test_manifest.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python 3.11+ idioms in all Python code
Use 4-space indentation in all Python code
Prefer small, typed functions where practical in Python code

**/*.py: Guard newer-stdlib usage with requires-python = ">=3.9" and use fallbacks (e.g. tomllib falls back to tomli) in modules like core/manifest.py
Include from __future__ import annotations at the top of Python modules

Files:

  • tests/unit/test_manifest.py
  • pythonlings/core/manifest.py
tests/**/*test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

Name test files as test_<behavior>.py

Files:

  • tests/unit/test_manifest.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Name test functions as test_<expected_behavior>
Use pytest for all tests with pytest-asyncio in auto mode for async tests

Files:

  • tests/unit/test_manifest.py
**/test_*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Name tests as test_<behavior>.py or test_<expected_behavior> (e.g., test_runner.py, test_state.py)

Files:

  • tests/unit/test_manifest.py
pythonlings/core/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Core exercise loading, workspace setup, state, reset, solutions, and runner logic must live in pythonlings/core/ directory

Keep UI behavior in screens/ and widgets/ modules; keep behavior logic in core/ modules—do not import UI in core

Files:

  • pythonlings/core/manifest.py
pythonlings/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep UI behavior in screens/ or widgets/ directories; keep filesystem, manifest, reset, and runner behavior in core/

Files:

  • pythonlings/core/manifest.py
🔇 Additional comments (3)
pythonlings/core/manifest.py (2)

68-91: LGTM!

Also applies to: 105-134


58-60: 🩺 Stability & Availability

No change needed. The project declares requires-python = ">=3.9", and pythonlings/core/manifest.py uses a tomllib / tomli fallback.

tests/unit/test_manifest.py (1)

215-218: LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: return friendly errors for malformed info.toml files

1 participant