Skip to content

Dedent -c code argument#8056

Open
roryyorke wants to merge 3 commits into
RustPython:mainfrom
roryyorke:rory/test_cmd_line
Open

Dedent -c code argument#8056
roryyorke wants to merge 3 commits into
RustPython:mainfrom
roryyorke:rory/test_cmd_line

Conversation

@roryyorke

@roryyorke roryyorke commented Jun 7, 2026

Copy link
Copy Markdown

I haven't used AI at all for this. (I'm not sure if an "anti-disclaimer" is needed...)

Summary

  • dedents -c code argument
  • removes expectedFailure marker from test.test_cmd_line.CmdLineTest.test_cmd_dedent
  • updated expectedFailure comment for test.test_os.ExecTests.test_execve_env_concurrent_mutation_with_fspath_posix, which used to fail with IndentationError, but now fails with a TypeError.

Summary by CodeRabbit

  • Bug Fixes
    • Improved -c command-line argument handling to strip common leading indentation from multi-line command strings, so indented or formatted commands are normalized before execution.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 50aaa0c9-7662-4efe-80b6-f85fa71956d2

📥 Commits

Reviewing files that changed from the base of the PR and between 0598ea2 and 31d9128.

📒 Files selected for processing (1)
  • src/settings.rs

📝 Walkthrough

Walkthrough

The -c option now preprocesses its argument with a new dedent() helper that removes a common leading whitespace prefix across non-empty lines before storing the command in RunMode::Command.

Changes

Command-line -c dedent preprocessing

Layer / File(s) Summary
Dedent helper implementation
src/settings.rs
A private dedent() helper (lines 472–538) converts all-whitespace lines to empty, computes the common leading whitespace prefix across non-empty lines, strips that prefix from each non-empty line, and joins lines with \n.
-c argument integration
src/settings.rs
The -c parser (around lines 125–129) now wraps the input with dedent() when constructing RunMode::Command, so the stored command string is the dedented version.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A dedent so fine,
Makes -c code shine so bright,
Whitespace stripped away,
CPython's way—
RustPython hops in, all right! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing dedent functionality for the -c command-line argument.
Linked Issues check ✅ Passed The PR implements dedenting for -c argument, removes expectedFailure marker, and aligns with CPython 3.14 behavior as required by issue #8055.
Out of Scope Changes check ✅ Passed All changes directly support the core objective of implementing -c dedent functionality and its test; cargo fmt styling is a minor incidental change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] lib: cpython/Lib/os.py
[x] test: cpython/Lib/test/test_os.py (TODO: 2)
[x] test: cpython/Lib/test/test_popen.py

dependencies:

  • os

dependent tests: (183 tests)

  • os: test___all__ test__osx_support test_argparse test_asdl_parser test_ast test_asyncio test_atexit test_base64 test_baseexception test_bdb test_bool test_buffer test_builtin test_bytes test_bz2 test_c_locale_coercion test_calendar test_clinic test_cmd_line test_cmd_line_script test_codecs test_compile test_compileall test_concurrent_futures test_configparser test_contextlib test_ctypes test_dbm test_dbm_dumb test_dbm_sqlite3 test_decimal test_devpoll test_doctest test_dtrace test_eintr test_embed test_ensurepip test_enum test_epoll test_exception_hierarchy test_exceptions test_faulthandler test_fcntl test_file test_file_eintr test_filecmp test_fileinput test_fileio test_float test_fnmatch test_fork1 test_fractions test_fstring test_ftplib test_future_stmt test_generated_cases test_genericalias test_genericpath test_getpass test_gettext test_glob test_graphlib test_gzip test_hash test_hashlib test_http_cookiejar test_httplib test_httpservers test_imaplib test_importlib test_inspect test_io test_ioctl test_json test_kqueue test_largefile test_launcher test_linecache test_locale test_logging test_lzma test_mailbox test_marshal test_math test_mimetypes test_mmap test_modulefinder test_msvcrt test_multiprocessing_forkserver test_multiprocessing_main_handling test_multiprocessing_spawn test_netrc test_ntpath test_openpty test_optparse test_os test_pathlib test_peg_generator test_perfmaps test_pkg test_pkgutil test_platform test_plistlib test_poll test_popen test_poplib test_posix test_posixpath test_profile test_pstats test_pty test_py_compile test_pydoc test_pyexpat test_random test_regrtest test_repl test_reprlib test_robotparser test_runpy test_sax test_script_helper test_selectors test_shelve test_shutil test_signal test_site test_smtpnet test_socket test_socketserver test_sqlite3 test_ssl test_stat test_string_literals test_strptime test_structseq test_subprocess test_support test_sys test_sys_settrace test_sysconfig test_tabnanny test_tarfile test_tempfile test_termios test_thread test_threading test_threadsignals test_time test_tokenize test_tools test_trace test_tracemalloc test_tty test_turtle test_typing test_unicode_file test_unicode_file_functions test_unittest test_univnewlines test_urllib test_urllib2 test_urllib2_localnet test_urllib2net test_urllibnet test_uuid test_venv test_wait3 test_wait4 test_wave test_webbrowser test_winapi test_winconsoleio test_winreg test_winsound test_wsgiref test_xml_etree test_xpickle test_zipfile test_zipimport test_zipimport_support test_zoneinfo test_zstd

[ ] test: cpython/Lib/test/test_cmd_line.py (TODO: 24)

dependencies:

dependent tests: (no tests depend on cmd_line)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/settings.rs (2)

474-474: 💤 Low value

Inline comments should have a space after //.

The Rust style guide recommends a space between // and the comment text for readability.

✨ Suggested improvements
-    //all-whitespace lines become empty
+    // all-whitespace lines become empty
     let deblanked: Vec<&str> = input
-    //find maximum common whitespace prefix, if any
+    // find maximum common whitespace prefix, if any
     for line in deblanked.iter() {
-        //strip prefix
+        // strip prefix
         deblanked
-        //no prefix: all lines blank
+        // no prefix: all lines blank
         deblanked.join("\n")

Also applies to: 480-480, 507-507, 522-522

🤖 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 `@src/settings.rs` at line 474, Fix the inline comment spacing by ensuring
there is a single space after the comment slashes (change occurrences like
"//all-whitespace lines become empty" to "// all-whitespace lines become empty"
and likewise for the other inline comments in the same section), updating each
inline comment that currently lacks a space after "//" so they follow Rust style
guide spacing.

469-469: 💤 Low value

Doc comment style: Capitalize and punctuate properly.

Rust doc comments should start with a capital letter and end with a period.

✨ Suggested improvement
-///like textwrap.dedent; for processing -c argument
+/// Remove common leading whitespace from lines, like textwrap.dedent.
 fn dedent(input: &str) -> String {
🤖 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 `@src/settings.rs` at line 469, Update the doc comment currently written as
"///like textwrap.dedent; for processing -c argument" to follow Rust doc style:
start with a capital letter and end with a period; e.g. change it to something
like "/// Like textwrap::dedent. Used for processing the -c argument." Locate
the exact doc comment string to modify so the surrounding item (the
function/const handling inline `-c` processing) gets the corrected comment.
🤖 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 `@src/settings.rs`:
- Around line 486-491: The loop using line.chars().zip(pstr.chars()) can leave
pstr too long when line is shorter; change it to iterate with char_indices (e.g.
line.char_indices().zip(pstr.char_indices()), tracking a matched_byte_offset
variable that you update to pi + pc.len_utf8() on each matched pair and set to
the correct byte offset when a mismatch occurs, and after the loop always call
pstr.truncate(matched_byte_offset) so pstr is correctly truncated to the true
common prefix; reference the existing variables/operations pstr, line, and
pstr.truncate in your fix.

---

Nitpick comments:
In `@src/settings.rs`:
- Line 474: Fix the inline comment spacing by ensuring there is a single space
after the comment slashes (change occurrences like "//all-whitespace lines
become empty" to "// all-whitespace lines become empty" and likewise for the
other inline comments in the same section), updating each inline comment that
currently lacks a space after "//" so they follow Rust style guide spacing.
- Line 469: Update the doc comment currently written as "///like
textwrap.dedent; for processing -c argument" to follow Rust doc style: start
with a capital letter and end with a period; e.g. change it to something like
"/// Like textwrap::dedent. Used for processing the -c argument." Locate the
exact doc comment string to modify so the surrounding item (the function/const
handling inline `-c` processing) gets the corrected comment.
🪄 Autofix (Beta)

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: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9e488883-5a71-49b7-a6d1-00f7ff8f724f

📥 Commits

Reviewing files that changed from the base of the PR and between 51c97b9 and 0598ea2.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_cmd_line.py is excluded by !Lib/**
  • Lib/test/test_os.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • src/settings.rs

Comment thread src/settings.rs
Comment on lines +486 to +491
for (i, (c, pc)) in line.chars().zip(pstr.chars()).enumerate() {
if c != pc {
pstr.truncate(i);
break;
}
}

@coderabbitai coderabbitai Bot Jun 7, 2026

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical bug: Prefix not truncated when line is shorter than current prefix.

The zip terminates when either iterator ends. If line has fewer characters than pstr, the loop completes without calling truncate, leaving pstr longer than the actual common prefix.

Example: if pstr = " " (4 spaces) and line = " x" (2 spaces + x), zip yields only 2 character pairs, both matching. The loop never calls truncate, so pstr incorrectly remains 4 spaces instead of being reduced to 2.

🐛 Proposed fix to track matched length and always truncate
         if let Some(ref mut pstr) = prefix {
+            let mut matched = 0;
             for (i, (c, pc)) in line.chars().zip(pstr.chars()).enumerate() {
                 if c != pc {
                     pstr.truncate(i);
                     break;
                 }
+                matched = i + 1;
             }
-        } else {
+            // Truncate to matched length (handles case where line is shorter than pstr)
+            let byte_idx = pstr.char_indices()
+                .nth(matched)
+                .map(|(idx, _)| idx)
+                .unwrap_or(pstr.len());
+            pstr.truncate(byte_idx);
+        } else {

Note: truncate takes a byte index, so we must map the character count to the byte offset via char_indices.

🤖 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 `@src/settings.rs` around lines 486 - 491, The loop using
line.chars().zip(pstr.chars()) can leave pstr too long when line is shorter;
change it to iterate with char_indices (e.g.
line.char_indices().zip(pstr.char_indices()), tracking a matched_byte_offset
variable that you update to pi + pc.len_utf8() on each matched pair and set to
the correct byte offset when a mismatch occurs, and after the loop always call
pstr.truncate(matched_byte_offset) so pstr is correctly truncated to the true
common prefix; reference the existing variables/operations pstr, line, and
pstr.truncate in your fix.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

in the example given, pstr = " " and line = " x", zip will yield 3 character pairs; the tuples (i, (c, pc))will be

  • (0, (' ', ' ')) - match, continue
  • (1, (' ', ' ')) - match, continue
  • (2, (' ', 'x')) - mismatch, truncate prefix to length 2

In general, any line processed here must have at least one non-whitespace character; since pstr comprises only whitespace, either pstr matches line up to the length of pstr (no truncation), or some element of pstr does not match, and pstr will be truncated. In particular, if line is shorter than pstr, a non-whitespace character from line must be compared to a whitespace character from pstr before either zipped iterator is exhausted.

This is possibly not very obvious; I will add 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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List my comments on PR `#8056` to find the comment ID to resolve
gh api repos/RustPython/RustPython/pulls/8056/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, body: .body[0:80]}'

Length of output: 374


🏁 Script executed:

#!/bin/bash
# Resolve the review comment by marking it as resolved
gh api repos/RustPython/RustPython/pulls/8056/reviews --jq '.'
# Use the GraphQL API to resolve the review thread
gh api graphql -f query='
query {
  repository(owner: "RustPython", name: "RustPython") {
    pullRequest(number: 8056) {
      reviewThreads(first: 20) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes {
              databaseId
              author { login }
              body
            }
          }
        }
      }
    }
  }
}'

Length of output: 18198


🏁 Script executed:

#!/bin/bash
# Resolve the review thread using GraphQL
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOCA8Bec6HpK50"}) {
    thread {
      id
      isResolved
    }
  }
}'

Length of output: 719



@roryyorke You're correct, and I apologize for the false positive. My mental model of the zip was wrong — since line.chars() is the left iterator and all non-empty lines are guaranteed to contain at least one non-whitespace character (while pstr is purely whitespace), a mismatch will always be found before either iterator is exhausted when line is shorter than pstr. The logic is sound.

Adding a comment to document that invariant is a great idea for future readers. I attempted to resolve this review thread programmatically but don't have the necessary permissions — you're welcome to resolve it manually.

🐰 scratch scratch ...oh, the bug was a ghost.

@ShaharNaveh ShaharNaveh 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.

TYSM!
Welcome to the project:)

Comment thread src/settings.rs
}

///like textwrap.dedent; for processing -c argument
fn dedent(input: &str) -> String {

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ruff's dedent does not match CPython's behavior.

I checked its source. Ruff's dedent only counts the leading space. But CPython's impl and textwrap.dedent also checks its content.

For example,

cpython.dedent("\t 1\n \t1")  "\t 1\n \t1"
ruff.dedent("\t 1\n \t1")  "1\n1"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I wasn't aware of ruff's dedent. It's interesting that it doesn't distinguish tab and space -- I wonder if that's deliberate?

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.

Ruff's dedent does not match CPython's behavior.

I checked its source. Ruff's dedent only counts the leading space. But CPython's impl and textwrap.dedent also checks its content.

For example,

cpython.dedent("\t 1\n \t1")  "\t 1\n \t1"
ruff.dedent("\t 1\n \t1")  "1\n1"

Thanks for the info!

I'm wondering if that's intentional

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.

@MichaReiser you know more about this

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.

This was discussed a bit in astral-sh/ruff#4731 (comment) when dedent was introduced to Ruff. There's also a test (https://github.com/astral-sh/ruff/blob/04008db76b3876d3576366e839c5e8067368d117/crates/ruff_python_trivia/src/textwrap.rs#L423-L435). I suspect that the reasoning simply was that this is fine for Ruff where we only apply fixes if the source code is valid. But it might also just have been a simplification to ship faster.

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.

I suspect that the reasoning simply was that this is fine for Ruff where we only apply fixes if the source code is valid. But it might also just have been a simplification to ship faster.

Thanks for the reply @MichaReiser :)
Will ruff be open for a PR patching detent behavior to behave like CPython's textwrap.dedent?

Although, I'm not sure what impact will this have on ruff. I can open a PR and see if there's any failing tests because of it if you'd be fine with merging it

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.

I'm not familiar enough with textwrap.dedent and how it exactly differs from Ruff. I think a PR probably gives us most signal

@roryyorke

Copy link
Copy Markdown
Author

Welcome to the project:)

Thank you!

I forgot to run cargo fmt before submitting; I've now done that and also fixed the style nitpicks code rabbit raised. Is it OK to push those? I don't want to use up all the project's CI time.

@ShaharNaveh

ShaharNaveh commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Welcome to the project:)

Thank you!

I forgot to run cargo fmt before submitting; I've now done that and also fixed the style nitpicks code rabbit raised. Is it OK to push those? I don't want to use up all the project's CI time.

dw about the CI, put whenever you feel like it:)

Can you please add a commend saying that this is different from ruff's impl?

@youknowone youknowone left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for contributing!
You seem to use wrong email to your git commits. That might be your correct email address, but in that case it means you didn't add the email to your github account.
Could you please check your setup and fix to commits to have your correct email?

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.

-c code argument should be dedented

6 participants