Skip to content

Add --sort-by option to ls command for flexible result ordering - #446

Open
yarikoptic wants to merge 12 commits into
mainfrom
claude/pr-434-review-conflicts-q186ab
Open

Add --sort-by option to ls command for flexible result ordering#446
yarikoptic wants to merge 12 commits into
mainfrom
claude/pr-434-review-conflicts-q186ab

Conversation

@yarikoptic

@yarikoptic yarikoptic commented Aug 28, 2026

Copy link
Copy Markdown
Member

Summary

Adds a --sort-by command-line option to the ls command that allows users to sort results by one or more fields. This enables flexible ordering of duct run records beyond the default input order or simple reverse chronological sorting.

Key Changes

  • New CLI argument: Added --sort-by option to _create_ls_parser() that accepts one or more field names from LS_FIELD_CHOICES
  • Sorting implementation:
    • _sort_run_data(): Sorts raw run records by specified fields before formatting/display
    • _sort_key(): Converts field values to comparable sort keys, handling mixed types (numbers, strings, lists, dicts, None) that may appear in the same field across different runs
    • _natural_chunks(): Implements natural/semantic sorting for version strings and numeric components within text (e.g., "run2" < "run10")
  • Type-aware sorting: Values are ranked by type (numbers → text → other → missing) to prevent TypeErrors when comparing heterogeneous values
  • Non-displayed fields: Sorting works on any field, including those not in --fields (not displayed)
  • Numeric precision: Numeric fields sort by actual value, not rendered string representation
  • Warning on missing fields: Logs a warning if a sort field is not provided by any run

Implementation Details

  • Sorting occurs after loading records but before field restriction and formatting, allowing any field to be used as a sort key
  • Multiple --sort-by fields are applied in order, with later fields breaking ties from earlier ones
  • Natural sorting handles both pure numbers and version-like strings with numeric components
  • Heterogeneous field values (common in evolving info.json schemas) are handled gracefully by ranking types separately
  • Combines with existing --reverse flag for descending order

Tests Added

Comprehensive test coverage including:

  • Basic sorting by single and multiple fields
  • Natural/semantic version sorting
  • Numeric vs. lexical sorting
  • Sorting by non-displayed fields
  • Mixed types and missing values handling
  • Unknown field warnings
  • Parametrized tests for all available sort fields

https://claude.ai/code/session_017sEmfsHTr9Ki44PLwzEz8W

Copilot AI and others added 8 commits July 20, 2026 08:35
Co-authored-by: yarikoptic <39889+yarikoptic@users.noreply.github.com>
Follow-up on the --sort-by review:

- Replace the zip()/map() decorate-sort dance with a `_sort_run_data()`
  helper which flattens each record once and reads top-to-bottom.
- Replace `_make_sort_value()` with `_sort_key()`, which ranks values by
  type (numbers, text, other, missing).  info.json files accumulate
  across duct versions, so one field can hold a number in one run, a
  string in another, a list (e.g. `gpu`) in a third and be absent from a
  fourth -- previously any such mix raised TypeError.  Missing values
  keep sorting last.
- Sort strings naturally (digit runs compared as numbers), so
  schema_version 0.9.0 sorts before 0.10.0 and run2_ before run10_
  instead of lexicographically.
- Warn when no run provides a requested --sort-by field, instead of
  silently returning an unaffected order.
- Read `args.sort_by` directly rather than via getattr(), matching how
  the other ls arguments are used.
- Spell out in --sort-by help that later fields break ties, that runs
  missing a field are listed last and that --reverse gives descending
  order.

Tests: the sort tests no longer assert an output is sorted relative to
itself (which passed even when --sort-by was ignored) -- inputs are now
deliberately unordered and the exact expected order is asserted.  Added
coverage for numeric-not-lexical ordering, natural version ordering,
multi-field tie-breaking, mixed/missing values, the missing-field
warning, unit tests for _sort_key/_natural_chunks, and argparse tests
for --sort-by.  The per-LS_FIELD_CHOICES parametrization now feeds
numeric values to the numeric fields and displays the sorted field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sEmfsHTr9Ki44PLwzEz8W
Schema 0.2.3 (#425) backfills the OS/distro provenance fields into the
"system" block of older records, so the --sort-by test fixtures now write
a "system" key -- without it every record older than 0.2.3 fails to load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sEmfsHTr9Ki44PLwzEz8W
Copilot AI lite review requested due to automatic review settings August 28, 2026 13:52
@yarikoptic yarikoptic added the semver-minor Increment the minor version when merged label Aug 28, 2026 — with Claude
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.94%. Comparing base (303fa74) to head (b0e8c57).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #446      +/-   ##
==========================================
+ Coverage   91.32%   91.94%   +0.62%     
==========================================
  Files          15       15              
  Lines        1280     1329      +49     
  Branches      173      183      +10     
==========================================
+ Hits         1169     1222      +53     
+ Misses         77       71       -6     
- Partials       34       36       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds flexible multi-key sorting to the con-duct ls command via a new --sort-by CLI option, enabling stable ordering across heterogeneous info.json schemas (including natural sorting for version-like strings) while preserving existing --reverse behavior.

Changes:

  • Added --sort-by FIELD [FIELD ...] parsing/validation to the ls subcommand parser.
  • Implemented type-aware total-order sort keying (including natural chunking for strings) and applied sorting before formatting/field restriction.
  • Added extensive test coverage for sorting behavior, mixed types, missing fields, and CLI parsing.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/con_duct/cli.py Adds --sort-by argument to ls parser and updates --reverse help text.
src/con_duct/ls.py Implements _natural_chunks, _sort_key, _sort_run_data, and integrates sorting into ls().
test/test_cli.py Tests --sort-by parsing defaults, multi-arg parsing, and invalid-choice handling.
test/test_ls.py Adds helper utilities and comprehensive sorting tests (single/multi-field, natural, numeric, mixed, missing, warnings).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/con_duct/ls.py
claude added 2 commits August 28, 2026 13:59
A field can be present in every record and null in all of them -- "gpu"
is written as null whenever the machine has no GPU -- so "No run
provides" was inaccurate for the case a user is most likely to hit.
Both cases are equally a no-op for ordering and equally worth reporting,
so keep warning for both and word it for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sEmfsHTr9Ki44PLwzEz8W
From an independent review of the sort keys:

- A digit run longer than sys.get_int_max_str_digits() (4300 by default)
  made int() raise ValueError, killing the whole `ls` invocation.  Free
  form fields (message, command, ...) hold whatever the user typed, so
  this was reachable; digit runs too long to be a meaningful number are
  now compared as text.
- Ties were broken by the order the paths were given, which is arbitrary
  glob order when they were globbed -- two runs of the same command could
  come out in either order.  prefix, the only field guaranteed unique,
  now breaks any remaining tie.
- ensure_compliant_schema() backfills fields added in later schema
  versions with "", which ranked as text and sorted such runs *ahead* of
  runs that have a real value.  "" now counts as no value, like a missing
  key or a null, so it sorts last and triggers the same warning.
- NaN compares false against everything, which left the order dependent
  on the input order; it is treated as no value too.

Also correct the --sort-by help: valueless runs sort last, but --reverse
flips that, so it does not simply "list them last".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sEmfsHTr9Ki44PLwzEz8W
@yarikoptic
yarikoptic requested a review from asmacdo September 2, 2026 03:02
@yarikoptic

Copy link
Copy Markdown
Member Author
tested also on drogon as in original issue

that it does sort if asked by prefix (noted annoying traceback from pyout... we might want to address here or there even... likely here)

(duct) dandi@drogon:~$ con-duct ls --sort-by prefix | head
2026-09-01T23:07:42-0400 [INFO    ] con-duct: No .env files found
PREFIX                                  COMMAND                                                                                                                                                                                                                                               EXIT_CODE WALL_CLOCK_TIME PEAK_RSS
.duct/logs/2025.02.05T11.14.05-519190_  flock -E 0 -e -n /home/dandi/.run/s3invsync-cron.lock /home/dandi/.cargo/bin/s3invsync -l INFO --ok-errors missing-old-version s3://dandiarchive-inventory/dandiarchive/dandiset-manifest/ /mnt/backup/dandi/dandiarchive-s3invsync-backups/manifests 0         3.084 sec       35.0 MB 
.duct/logs/2025.02.05T11.16.52-519246_  /home/dandi/.cargo/bin/s3invsync -l INFO --ok-errors missing-old-version s3://dandiarchive-inventory/dandiarchive/dandiset-manifest/ /mnt/backup/dandi/dandiarchive-s3invsync-backups/manifests-20250205/                                             0         221.390 sec     34.9 MB 
.duct/logs/2026.05.21T13.37.59-1444087_ bash -c /mnt/backup/dandi/dandisets/tools/backups2datalad-update-cron                                                                                                                                                                                 0         306.398 sec     537.2 MB
.duct/logs/2026.05.21T14.10.53-1606911_ bash -c /mnt/backup/dandi/dandisets/tools/backups2datalad-update-cron                                                                                                                                                                                 0         565.979 sec     510.0 MB
.duct/logs/2026.05.21T15.00.01-1755299_ bash -c /mnt/backup/dandi/dandisets/tools/backups2datalad-update-cron                                                                                                                                                                                 0         109.008 sec     378.0 MB
.duct/logs/2026.05.21T15.15.01-1855868_ bash -c /mnt/backup/dandi/dandisets/tools/backups2datalad-update-cron                                                                                                                                                                                 0         258.399 sec     364.9 MB
.duct/logs/2026.05.21T15.30.01-1981536_ bash -c /mnt/backup/dandi/dandisets/tools/backups2datalad-update-cron                                                                                                                                                                                 0         103.832 sec     447.1 MB
.duct/logs/2026.05.21T15.45.01-2082771_ bash -c /mnt/backup/dandi/dandisets/tools/backups2datalad-update-cron                                                                                                                                                                                 0         111.106 sec     446.6 MB
.duct/logs/2026.05.21T16.00.01-2194685_ bash -c /mnt/backup/dandi/dandisets/tools/backups2datalad-update-cron                                                                                                                                                                                 0         99.247 sec      179.7 MB
Traceback (most recent call last):
  File "/home/dandi/proj/CON/duct/.venv/bin/con-duct", line 10, in <module>
    sys.exit(main())
             ~~~~^^
  File "/home/dandi/proj/CON/duct/src/con_duct/cli.py", line 564, in main
    sys.exit(execute(args))
             ~~~~~~~^^^^^^
  File "/home/dandi/proj/CON/duct/src/con_duct/cli.py", line 490, in execute
    result = args.func(args)
  File "/home/dandi/proj/CON/duct/src/con_duct/ls.py", line 315, in ls
    pyout_ls(output_rows, args.colors)
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dandi/proj/CON/duct/src/con_duct/ls.py", line 252, in pyout_ls
    with pyout.Tabular(
         ~~~~~~~~~~~~~^
        style=dict(
        ^^^^^^^^^^^
    ...<3 lines>...
        mode="final",
        ^^^^^^^^^^^^^
    ) as table:
    ^
  File "/home/dandi/proj/CON/duct/.venv/lib/python3.13/site-packages/pyout/interface.py", line 219, in __exit__
    self._stream.write(str(self._content))
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/home/dandi/proj/CON/duct/.venv/lib/python3.13/site-packages/pyout/tabular.py", line 51, in write
    self.term.stream.write(text)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^
BrokenPipeError: [Errno 32] Broken pipe

`con-duct ls | head` ended in a BrokenPipeError traceback out of pyout's
final write, and the same holds for the json/yaml/summaries paths -- any
of them can be writing when the reader goes away.

Catch BrokenPipeError around the subcommand and exit 141 (what a shell
reports for a command killed by SIGPIPE), pointing stdout at /dev/null
first so that Python's flush during shutdown does not raise a second
BrokenPipeError and print a traceback anyway, as the Python docs on
SIGPIPE recommend.

Verified with 10MB of `ls` output whose reader exits immediately: before,
exit 1 with a 19 line traceback ending in pyout/tabular.py; after, exit
141 and nothing on stderr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sEmfsHTr9Ki44PLwzEz8W

Copy link
Copy Markdown
Collaborator

Thanks for running it on drogon. Took the pyout traceback here, in 34bc105.

It is not pyout-specific — the json/json_pp/yaml/summaries paths can all be mid-write when the reader goes away — so cli.main() now catches BrokenPipeError around the subcommand, points stdout at /dev/null (otherwise Python's flush during shutdown raises a second BrokenPipeError and prints the traceback anyway, per the Python docs on SIGPIPE), and exits 141, which is what a shell reports for a command killed by SIGPIPE.

Verified against ~10 MB of ls output with a reader that exits immediately:

# before: exit 1, 19 lines of traceback ending in
#   File ".../pyout/tabular.py", line 51, in write
#     self.term.stream.write(text)
#   BrokenPipeError: [Errno 32] Broken pipe
$ con-duct ls huge/*_info.json --sort-by prefix 2>err.txt | true
exit=141
$ wc -l err.txt
0 err.txt

Two tests cover it: the exit code with no traceback, and the /dev/null redirect staying best-effort when stdout has no real fd.

Say the word if you would rather keep this PR to --sort-by and have the pipe fix on its own branch — it is an independent commit, so it lifts out cleanly.


Generated by Claude Code

The line handing the subcommand's return code to sys.exit() was never
exercised -- every test calling main() either stopped at the help/usage
path or, since the previous commit, took the BrokenPipeError branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sEmfsHTr9Ki44PLwzEz8W
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-minor Increment the minor version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ls: --sort-by FIELDS

5 participants