Skip to content

Add --fields support for machine-readable CLI output#71

Merged
erseco merged 3 commits into
mainfrom
issue-66-cli-fields
Jul 7, 2026
Merged

Add --fields support for machine-readable CLI output#71
erseco merged 3 commits into
mainfrom
issue-66-cli-fields

Conversation

@erseco

@erseco erseco commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a reusable field-selection layer to the CLI output module and wires a --fields field1,field2 option into the four migrated list commands (courses, categories, sections, users). Automation users can now project --output json|yaml|csv payloads down to exactly the top-level fields they want, in the order they ask for, without post-processing with jq/awk. The change is fully opt-in and does not alter any command's default output.

Linked issue

Closes #66

Specification (SDD)

  • New reusable helpers in src/py_moodle/cli/output.py:
    • select_fields(rows, fields) -> list[dict]: projects each row to exactly the requested top-level keys, preserving the user-provided order; a field present in some rows but missing from a given row is filled with None so all rows share a consistent shape. Raises the typed UnknownFieldError (naming the offending field(s) and listing available fields) when a field is present in no row.
    • parse_fields(value) -> Optional[list[str]]: splits a comma-separated value, strips whitespace, drops empty parts; an empty/whitespace-only value (e.g. --fields "") means "no filtering" (equivalent to omitting the flag).
  • emit() gains an optional, backwards-compatible fields parameter. When set, filtering is applied to json/yaml/csv; table accepts but ignores it (best-effort, documented). For CSV the selected keys become the column headers (the pretty csv_fields are bypassed) so all machine-readable formats share the same projected shape.
  • Public contract: unknown field -> clear message on stderr + non-zero exit; field order follows user order; flat top-level keys only (no nested selectors).

TDD evidence

Red (helpers/flag absent), from the worktree root:

$ .venv/bin/pytest tests/unit/test_cli_fields.py -q
ImportError: cannot import name 'UnknownFieldError' from 'py_moodle.cli.output'
!!! Interrupted: 1 error during collection !!!

Green after implementing the helpers + wiring the option:

$ .venv/bin/pytest tests/unit/test_cli_fields.py -q
...................                                                       [100%]
19 passed

Full suite, no regressions:

$ .venv/bin/pytest tests/unit -q
200 passed, 1 warning in 0.54s

Test plan

  • Added/authored failing test(s) first.
  • Confirmed red before implementing.
  • Implemented minimal change.
  • Focused tests green.
  • Full "pytest tests/unit" green.

Commands run:

git fetch origin && git checkout -B issue-66-cli-fields origin/main
.venv/bin/pytest tests/unit/test_cli_fields.py -q          # RED: ImportError (UnknownFieldError)
# implement select_fields/parse_fields/UnknownFieldError + emit(fields=...) + wire --fields
.venv/bin/pytest tests/unit/test_cli_fields.py -q          # GREEN: 19 passed
.venv/bin/pytest tests/unit -q                             # 200 passed
.venv/bin/black --check src tests                          # clean
.venv/bin/isort --check-only src tests                     # clean
.venv/bin/flake8                                           # exit 0

New tests in tests/unit/test_cli_fields.py cover: select_fields key selection + order, unknown-field typed error, empty-rows short-circuit, missing-key None fill; parse_fields split/strip and empty-as-None; CLI json field selection + order, CLI csv columns/headers in order, unknown-field non-zero exit with stderr message, empty --fields == unchanged output, table ignores --fields, all four commands apply --fields for json, and all four advertise --fields in --help.

Backwards compatibility

Fully backwards compatible. --fields is opt-in and defaults to off; emit()'s new fields parameter defaults to None = current behavior. No change to any command's default output. Existing tests/unit/test_cli_output_formats.py passes unchanged.

Documentation

docs/cli.md is auto-generated and git-ignored, so no docs commit is included (per the issue). docs/recipes.md and docs/development.md were intentionally left untouched (owned by sibling wave-1 PRs). The new --fields help text is emitted by each command's Typer option.

Notes for reviewers

  • Design choice: filtering lives in one place (select_fields) invoked via emit(fields=...), so the four commands share a single implementation (no copy-paste). The UnknownFieldError is converted to a stderr message + typer.Exit(1) inside emit(), keeping each command's call site a one-liner.
  • CSV behavior with --fields: headers become the raw selected keys (e.g. id,shortname) in user order, matching the json/yaml projected shape, rather than the pretty ID,Shortname headers used when --fields is absent. This is covered by a test.
  • Empty-result guard: select_fields([], ...) returns [] without raising, so --fields never spuriously errors on a command that returned zero rows.
  • File-ownership boundary respected. Files changed: src/py_moodle/cli/output.py, src/py_moodle/cli/courses.py, src/py_moodle/cli/categories.py, src/py_moodle/cli/sections.py, src/py_moodle/cli/users.py, and new tests/unit/test_cli_fields.py. No other files touched (no tests/conftest.py, no docs/*).

erseco added 3 commits July 7, 2026 23:41
Add a reusable field-selection helper to the CLI output layer and wire a
--fields option into the four migrated list commands (courses, categories,
sections, users).

- select_fields(rows, fields): projects each row to the requested top-level
  keys, preserving the user-provided order, and raises a typed
  UnknownFieldError (naming the offending field and listing the available
  ones) when a field is absent from every row.
- parse_fields(value): parses a comma-separated --fields value, treating an
  empty/whitespace-only value as 'no filtering' (equivalent to omitting it).
- emit() gains an optional, backwards-compatible fields parameter applied to
  json/yaml/csv output; table output accepts but ignores it (best-effort).
  For CSV the selected keys become the column headers so all machine-readable
  formats share the same projected shape. Unknown fields exit non-zero with
  the error reported on stderr.
The help-advertisement test parsed rendered --help text, which the
newer typer/rich in CI (typer 0.26 vs 0.23 locally) truncates in the
options panel at an 80-column terminal, so the literal '--fields' string
was absent even though the option is wired correctly (all functional
--fields tests pass; only these 4 help-render assertions failed:
4 failed, 196 passed on CI). Introspect the underlying Click command's
declared option strings instead — deterministic and width/version
independent.
@erseco erseco merged commit 0bc0476 into main Jul 7, 2026
30 of 33 checks passed
@erseco erseco deleted the issue-66-cli-fields branch July 7, 2026 23:51
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.

Add --fields support for machine-readable CLI output

1 participant