-
Notifications
You must be signed in to change notification settings - Fork 4
refactor: CLI subcommand structure #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,14 +12,35 @@ | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| from sounddiff import __version__ | ||||||||||||||||||||||||||||||||||||
| from sounddiff.core import diff | ||||||||||||||||||||||||||||||||||||
| from sounddiff.formats import FFMPEG_FORMATS | ||||||||||||||||||||||||||||||||||||
| from sounddiff.formats import FFMPEG_FORMATS, load_audio | ||||||||||||||||||||||||||||||||||||
| from sounddiff.report import render | ||||||||||||||||||||||||||||||||||||
| from sounddiff.types import OutputFormat | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| _PROGRESS_THRESHOLD = 30 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @click.command() | ||||||||||||||||||||||||||||||||||||
| @click.group(invoke_without_command=True) | ||||||||||||||||||||||||||||||||||||
| @click.version_option(version=__version__, prog_name="sounddiff") | ||||||||||||||||||||||||||||||||||||
| @click.pass_context | ||||||||||||||||||||||||||||||||||||
| def main(ctx: click.Context) -> None: | ||||||||||||||||||||||||||||||||||||
| """Compare and inspect audio files. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| sounddiff compare FILE_A FILE_B | ||||||||||||||||||||||||||||||||||||
| sounddiff inspect FILE | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| if ctx.invoked_subcommand is None: | ||||||||||||||||||||||||||||||||||||
| # Handle legacy positional arguments: sounddiff FILE_A FILE_B | ||||||||||||||||||||||||||||||||||||
| # We only do this if exactly two arguments are provided and they look like paths. | ||||||||||||||||||||||||||||||||||||
| args = sys.argv[1:] | ||||||||||||||||||||||||||||||||||||
| # Filter out flags/options | ||||||||||||||||||||||||||||||||||||
| pos_args = [a for a in args if not a.startswith("-")] | ||||||||||||||||||||||||||||||||||||
| if len(pos_args) == 2: | ||||||||||||||||||||||||||||||||||||
| ctx.invoke(compare, file_a=pos_args[0], file_b=pos_args[1]) | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Legacy invocation silently ignores CLI options. When invoking If full backward compatibility is intended, consider parsing and forwarding the relevant options, or document that legacy mode only supports the two-file invocation without options. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
`@click.group`(invoke_without_command=True)
`@click.version_option`(version=__version__, prog_name="sounddiff")
`@click.pass_context`
def main(ctx: click.Context) -> None:
"""Compare and inspect audio files.
sounddiff compare FILE_A FILE_B
sounddiff inspect FILE
Invoking sounddiff with two paths directly (legacy mode) is equivalent to
``sounddiff compare FILE_A FILE_B`` but ignores all options such as
--format, --verbose, and --no-color. Use the compare subcommand explicitly
for the full option set.
"""✏️ Learnings added
|
||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||
| click.echo(ctx.get_help()) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify that ctx.args is the correct mechanism for Click groups
# and check how the existing tests invoke the CLI
# Check Click documentation pattern for groups with leftover args
rg -n "ctx\.args" --type py
# See how tests currently invoke main
rg -nA10 "runner\.invoke" tests/Repository: systemblueteam/sounddiff Length of output: 2789 Legacy fallback breaks with Click's
Use Proposed fix using ctx.args `@click.group`(invoke_without_command=True)
`@click.version_option`(version=__version__, prog_name="sounddiff")
`@click.pass_context`
def main(ctx: click.Context) -> None:
"""Compare and inspect audio files.
sounddiff compare FILE_A FILE_B
sounddiff inspect FILE
"""
if ctx.invoked_subcommand is None:
- # Handle legacy positional arguments: sounddiff FILE_A FILE_B
- # We only do this if exactly two arguments are provided and they look like paths.
- args = sys.argv[1:]
- # Filter out flags/options
- pos_args = [a for a in args if not a.startswith("-")]
- if len(pos_args) == 2:
- ctx.invoke(compare, file_a=pos_args[0], file_b=pos_args[1])
+ # Handle legacy positional arguments: sounddiff FILE_A FILE_B
+ # ctx.args contains unparsed positional arguments after Click processing.
+ if len(ctx.args) == 2:
+ ctx.invoke(compare, file_a=ctx.args[0], file_b=ctx.args[1])
else:
click.echo(ctx.get_help())Note: This fix uses Click's internal argument parser (tested by 📝 Committable suggestion
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Quick reminder: with (´• ω •`) 🐇 ~ carefully noting things down ✏️ Learnings added
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @main.command() | ||||||||||||||||||||||||||||||||||||
| @click.argument("file_a", type=click.Path(exists=True)) | ||||||||||||||||||||||||||||||||||||
| @click.argument("file_b", type=click.Path(exists=True)) | ||||||||||||||||||||||||||||||||||||
| @click.option( | ||||||||||||||||||||||||||||||||||||
|
|
@@ -49,8 +70,7 @@ | |||||||||||||||||||||||||||||||||||
| default=False, | ||||||||||||||||||||||||||||||||||||
| help="Disable colored terminal output.", | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| @click.version_option(version=__version__, prog_name="sounddiff") | ||||||||||||||||||||||||||||||||||||
| def main( | ||||||||||||||||||||||||||||||||||||
| def compare( | ||||||||||||||||||||||||||||||||||||
| file_a: str, | ||||||||||||||||||||||||||||||||||||
| file_b: str, | ||||||||||||||||||||||||||||||||||||
| output_format: str, | ||||||||||||||||||||||||||||||||||||
|
|
@@ -60,8 +80,6 @@ def main( | |||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||
| """Compare two audio files and report what changed. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| sounddiff FILE_A FILE_B | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Compares FILE_A (reference) against FILE_B (comparison) and reports | ||||||||||||||||||||||||||||||||||||
| differences in loudness, spectral content, timing, and potential issues. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
|
|
@@ -113,3 +131,27 @@ def main( | |||||||||||||||||||||||||||||||||||
| click.echo(f"Report written to {output_path}") | ||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||
| click.echo(output) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @main.command() | ||||||||||||||||||||||||||||||||||||
| @click.argument("file", type=click.Path(exists=True)) | ||||||||||||||||||||||||||||||||||||
| def inspect(file: str) -> None: | ||||||||||||||||||||||||||||||||||||
| """Inspect a single audio file's metadata and properties. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Displays duration, sample rate, channel count, bit depth, and format. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||
| _, meta = load_audio(file) | ||||||||||||||||||||||||||||||||||||
| click.echo(f"File: {meta.path}") | ||||||||||||||||||||||||||||||||||||
| click.echo(f"Duration: {meta.duration:.3f}s") | ||||||||||||||||||||||||||||||||||||
| click.echo(f"Sample Rate: {meta.sample_rate}Hz") | ||||||||||||||||||||||||||||||||||||
| click.echo(f"Channels: {meta.channels}") | ||||||||||||||||||||||||||||||||||||
| click.echo(f"Bit Depth: {meta.bit_depth or 'Unknown'}") | ||||||||||||||||||||||||||||||||||||
| click.echo(f"Format: {meta.format_name}") | ||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||
| click.echo(f"Error: {e}", err=True) | ||||||||||||||||||||||||||||||||||||
| sys.exit(1) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+136
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Well-implemented Type annotations are complete, docstring follows Google style, and errors are properly caught and reported to stderr with exit code 1. One consideration: the broad There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
`@main.command`()
`@click.argument`("file", type=click.Path(exists=True))
def inspect(file: str) -> None:
"""Inspect a single audio file's metadata and properties.
Displays duration, sample rate, channel count, bit depth, and format.
"""
try:
_, meta = load_audio(file)
click.echo(f"File: {meta.path}")
click.echo(f"Duration: {meta.duration:.3f}s")
click.echo(f"Sample Rate: {meta.sample_rate}Hz")
click.echo(f"Channels: {meta.channels}")
click.echo(f"Bit Depth: {meta.bit_depth or 'Unknown'}")
click.echo(f"Format: {meta.format_name}")
except FileNotFoundError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
except ValueError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
except RuntimeError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)This keeps the two commands consistent and lets any truly unexpected errors (e.g., |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||||||||||||||||||||||
| main() | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The legacy argument parser incorrectly includes option values (e.g.,
jsonfrom--format json) as positional arguments, causing the command to fail when options are used.Severity: MEDIUM
Suggested Fix
To fix the legacy argument parsing, use a proper argument parser like
argparse.ArgumentParserwithparse_known_args(). This will correctly separate known options and their values from the positional arguments, ensuring that only the file paths are passed toctx.invoke(compare, ...). This avoids the flawed logic of filtering arguments based on astartswith("-")check.Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.