Skip to content

fix: prevent crashes when unparsing count, extend, and append actions#406

Open
arpitjain099 wants to merge 1 commit into
sandialabs:masterfrom
arpitjain099:chore/unparse-action-crashes
Open

fix: prevent crashes when unparsing count, extend, and append actions#406
arpitjain099 wants to merge 1 commit into
sandialabs:masterfrom
arpitjain099:chore/unparse-action-crashes

Conversation

@arpitjain099

@arpitjain099 arpitjain099 commented Jul 16, 2026

Copy link
Copy Markdown

Type: Bug

Description

Three of the action unparsers raise (or emit malformed output) on valid input that reaches them through the public get_effective_command_line_invocation(). I ran into these while reversing a parser that used count, extend, and append options, then reduced each to a minimal repro.

  • _unparse_count_action builds the short flag with flag[0] + flag[1] * count. For a count option that was not given and whose default is None, the stored value is None and the multiplication raises TypeError: can't multiply sequence by non-int of type 'NoneType'. With a default of 0 (or a value equal to a nonzero default) count is 0, so it emits a bare prefix like - instead of omitting the unused flag.
  • _unparse_extend_action passes the stored values straight into " ".join(...), so a typed option such as type=int raises TypeError: sequence item 1: expected str instance, int found. It also never calls quote_arg_if_necessary, unlike the append unparser, so a value containing spaces is emitted unquoted and the round trip breaks.
  • _unparse_append_action indexes values[0] to detect the nested-list case, which raises IndexError: list index out of range when an append option with default=[] was not given (the values is None guard does not catch an empty list).

Related Issues/PRs

None that I could find; I came across these while using the library.

Motivation

get_effective_command_line_invocation() should either reproduce the invocation or omit an option that was not supplied. In these three cases it crashes or prints something that would not parse back, which defeats the point of the reconstructed command line.

Implementation Details

Each fix is small and local to the action it concerns:

  • count: return early when the value is None, and skip emitting anything when the effective count is <= 0 (the flag was not given beyond its default).
  • extend: quote each value with quote_arg_if_necessary(str(value)), matching what the append unparser already does. This fixes both the non-string crash and the unquoted-spaces round trip.
  • append: return early when the normalized value list is empty, before the values[0] access.

I kept the changes minimal rather than refactoring the shared logic, but happy to consolidate if you would prefer.

Testing

Added regression cases to the existing parametrized tests for all three actions (a not-given count with None/0/nonzero defaults, an extend with type=int and one with a space-containing value, and an append with default=[] that was not given). Confirmed each case failed before the change and passes after.

The full suite passes locally on Python 3.12:

75 passed in 0.07s

ruff check, ruff format --check, and mypy are also clean on both changed files.

Documentation

No documentation changes; behavior of the public API is unchanged except that these inputs no longer crash or emit malformed output.

Summary by Sourcery

Prevent crashes and malformed output when unparsing count, extend, and append actions from command-line invocations.

Bug Fixes:

  • Skip emitting append options when the normalized value list is empty to avoid index errors.
  • Avoid unparsing count options when the value is None or the effective count is non-positive, preventing type errors and stray flags.
  • Quote and stringify extend action values before emitting them so non-string values and space-containing arguments are handled correctly.

Tests:

  • Add parametrized regression tests covering edge cases for append, count, and extend actions, including absent options, defaults, and typed/space-containing values.

Three action unparsers raised on valid input reachable through the
public get_effective_command_line_invocation():

- _unparse_count_action multiplied the flag body by the stored value.
  When a count option was not given and its default was None, the value
  was None and the multiplication raised TypeError. A default of 0 (or a
  value equal to a nonzero default) produced a bare prefix character like
  "-" instead of omitting the unused flag.
- _unparse_extend_action passed the stored values straight into
  " ".join, so a typed option (for example type=int) raised TypeError,
  and unlike the append unparser it never quoted values, so entries
  containing spaces broke the round trip.
- _unparse_append_action indexed values[0] to detect nested lists, which
  raised IndexError when an append option with default=[] was not given.

Each case now emits the correct effective invocation or nothing when the
option was not supplied. Added regression tests covering all three.

Signed-off-by: arpitjain099 <arpitjain099@gmail.com>
@sourcery-ai

sourcery-ai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes reverse argparse unparsing for count, extend, and append actions to avoid crashes and ensure omitted/typed/space-containing options either round-trip correctly or are not emitted, with matching regression tests added.

Sequence diagram for updated action unparsing behavior in get_effective_command_line_invocation

sequenceDiagram
    actor User
    participant ReverseArgumentParser
    participant Namespace

    User->>ReverseArgumentParser: get_effective_command_line_invocation()
    ReverseArgumentParser->>Namespace: getattr(namespace, action.dest)

    rect rgb(240,240,240)
    Note over ReverseArgumentParser,Namespace: append action
    ReverseArgumentParser->>ReverseArgumentParser: _unparse_append_action(action)
    ReverseArgumentParser->>Namespace: getattr(self._namespace, action.dest)
    alt values not list
        ReverseArgumentParser->>ReverseArgumentParser: values = [values]
    end
    alt values is empty
        ReverseArgumentParser-->>ReverseArgumentParser: return (skip _append_list_of_args)
    else values non-empty
        ReverseArgumentParser->>ReverseArgumentParser: _append_list_of_args(result)
    end
    end

    rect rgb(240,240,240)
    Note over ReverseArgumentParser,Namespace: count action
    ReverseArgumentParser->>ReverseArgumentParser: _unparse_count_action(action)
    ReverseArgumentParser->>Namespace: getattr(self._namespace, action.dest)
    alt value is None
        ReverseArgumentParser-->>ReverseArgumentParser: return (no flag emitted)
    else value is not None
        ReverseArgumentParser->>ReverseArgumentParser: count = value - action.default
        alt count <= 0
            ReverseArgumentParser-->>ReverseArgumentParser: return (no flag emitted)
        else count > 0
            ReverseArgumentParser->>ReverseArgumentParser: _append_list_of_args([...])
        end
    end
    end

    rect rgb(240,240,240)
    Note over ReverseArgumentParser,Namespace: extend action
    ReverseArgumentParser->>ReverseArgumentParser: _unparse_extend_action(action)
    ReverseArgumentParser->>Namespace: getattr(self._namespace, action.dest)
    alt values is not None
        ReverseArgumentParser->>ReverseArgumentParser: quote_arg_if_necessary(str(value)) for each value
        ReverseArgumentParser->>ReverseArgumentParser: _append_list_of_args([option_string, quoted_values])
    else values is None
        ReverseArgumentParser-->>ReverseArgumentParser: return (no args emitted)
    end
    end

    ReverseArgumentParser-->>User: reconstructed command line
Loading

File-Level Changes

Change Details Files
Ensure append actions with empty normalized value lists are skipped to avoid IndexError for unused options with empty defaults.
  • Normalize non-list values for append actions into a list as before, then return early when the resulting list is empty.
  • Keep existing handling of nested-list vs flat-list append values intact, only guarding the values[0] access.
reverse_argparse/reverse_argparse.py
test/test_reverse_argparse.py
Make count actions omit flags when the effective count is None or non-positive, preventing crashes and stray short-option prefixes.
  • Return early from the count unparser if the stored value in the namespace is None (option not given and default is None).
  • Compute effective count by subtracting the default when present, then return early if the result is less than or equal to zero.
  • Reuse existing short vs long option emission logic for positive counts without changing behavior.
reverse_argparse/reverse_argparse.py
test/test_reverse_argparse.py
Quote and stringify extend action values during unparsing so typed and space-containing arguments round-trip correctly without TypeError.
  • Convert each extend value to a string and wrap it with quote_arg_if_necessary before emitting.
  • Build the argument list from the option string plus the processed values, then pass it to _append_list_of_args as before.
  • Replace the previous direct join/emission of raw values, which failed for non-string types and values containing spaces.
reverse_argparse/reverse_argparse.py
test/test_reverse_argparse.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="reverse_argparse/reverse_argparse.py" line_range="403-404" />
<code_context>
         flag = self._get_option_string(action)
         if not isinstance(values, list):
             values = [values]
+        if not values:
+            return
         result = []
         if isinstance(values[0], list):
</code_context>
<issue_to_address>
**question:** Empty append lists no longer emit the flag, unlike extend actions; consider whether this asymmetry is intentional.

With this early return, `append` actions whose namespace value is an empty list will no longer emit the flag, whereas `_unparse_extend_action` still emits the flag even when `values` is empty (via `[flag, *values]`). If `append` and `extend` should behave consistently for empty lists, consider emitting the flag without values instead of returning early.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +403 to +404
if not values:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: Empty append lists no longer emit the flag, unlike extend actions; consider whether this asymmetry is intentional.

With this early return, append actions whose namespace value is an empty list will no longer emit the flag, whereas _unparse_extend_action still emits the flag even when values is empty (via [flag, *values]). If append and extend should behave consistently for empty lists, consider emitting the flag without values instead of returning early.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I've reviewed these changes, and they are indeed correct. @sourcery-ai, are you saying we need to make similar changes to _unparse_extend_action as well?

@sourcery-ai sourcery-ai 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@jmgate

jmgate commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

praise: This is a fantastic contribution! 🎉

Thanks so much, @arpitjain099, for finding these bugs, building out unit tests to capture them, and then offering up the solution. Your thorough work is very much appreciated. I'll see about getting this merged in soon.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.37%. Comparing base (dddd314) to head (47a714c).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #406      +/-   ##
==========================================
+ Coverage   94.01%   95.37%   +1.36%     
==========================================
  Files           2        2              
  Lines         167      173       +6     
  Branches       37       40       +3     
==========================================
+ Hits          157      165       +8     
+ Misses          4        3       -1     
+ Partials        6        5       -1     

☔ 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.

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.

3 participants