Skip to content

Conversation

@moreal
Copy link
Contributor

@moreal moreal commented Jan 16, 2026

The command was improved based on the process of updating the json module using the /update-pylib command. Additionally, the script was modified to add import unittest if it is missing.

I worked on #6743 with this pull request's changes.

Summary by CodeRabbit

  • Documentation

    • Added a detailed, step-by-step upgrade workflow for migrating tests, including examples, diff-review guidance, explicit restoration criteria for environment-specific markers, and a new example showing how to restore changes.
  • New Features

    • Enhanced test-upgrade tooling to support single-file and directory upgrades, a verification flow to run and mark tests based on outcomes, and automatic insertion of missing test imports when applying patches.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Adds a detailed multi-step test-upgrade workflow document and updates scripts/lib_updater.py to detect and insert a missing import unittest into the import block when applying modifications that require it.

Changes

Cohort / File(s) Summary
Documentation & Workflow
\.claude/commands/upgrade-pylib.md
Replaced a single-line "Upgrade tests" entry with a comprehensive step-by-step upgrade workflow: quick-upgrade for single files or directories, diff review and restoration criteria for RUSTPYTHON-specific changes, verification/run guidance with marker application, and an example for restoring RUSTPYTHON edits.
Import Detection & Insertion
scripts/lib_updater.py
Added has_unittest_import(tree: ast.Module) -> bool and find_import_insert_line(tree: ast.Module) -> int. Modified apply_patches() to append import unittest into the import block when actual modifications will be applied and the import is missing.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ShaharNaveh
  • youknowone
  • reactive-firewall

Poem

🐰 I hopped through tests, added a line,
An import snug where markers shine,
Docs now guide each careful mend,
Diffs reviewed before I end,
Tiny paws — a tidy vine.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Complement upgrade-pylib Claude Code command' accurately describes the main changes: enhancements to the upgrade-pylib command through improved documentation and updated script functionality.

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

✨ Finishing touches
  • 📝 Generate docstrings

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
Copy link
Contributor

Code has been automatically formatted

The code in this PR has been formatted using:

  • ruff format
    Please pull the latest changes before pushing again:
git pull origin enhance-upgrade-pylib

@moreal moreal force-pushed the enhance-upgrade-pylib branch from fab8624 to 708f227 Compare January 16, 2026 11:27
@moreal moreal marked this pull request as ready for review January 16, 2026 11:32
for alias in node.names:
if alias.name == UT:
return True
elif isinstance(node, ast.ImportFrom):
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should remove this elif since we want to bring unittest into scope

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right! 😓 I'll fix it now...

Copy link
Contributor

Choose a reason for hiding this comment

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

we can maybe adjust the decorator to __import__('unittest').expectedFailure and not needing this patch at all?

Copy link
Contributor Author

@moreal moreal Jan 16, 2026

Choose a reason for hiding this comment

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

That would certainly allow us to fully resolve the question of where to place the imports, but the code does feel a bit awkward to read, and I also wonder whether calling __import__ every time—while not significant—might slightly slow down the tests. I don’t have strong convictions or a firm opinion on this.

I’ll follow the maintainers’ decision.

Copy link
Contributor

Choose a reason for hiding this comment

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

In CPython this will have very minimal performance cost due to modules being cached. I ain't sure about RustPython:/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that’s right. I think it’s probably well optimized. It just feels a bit awkward to look at code like the example below.

@__import__('unittest').expectedFailure
def test_xxx():
  # Something

Functionally, there’s no major issue, so I’m mainly interested in hearing the maintainers’ (i.e., the people primarily responsible for maintenance) opinions. Would you be able to share your thoughts on this thread or on this PR? @youknowone

Copy link
Contributor

Choose a reason for hiding this comment

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

We can maybe check if there's a import unittest node in the AST and switch between @unittest.X or __import__('unittest').X, as I feel like most tests do import unittest

Copy link
Member

Choose a reason for hiding this comment

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

Well, while that will work perfectly, we still need to keep them human friendly.
Most of people with python-background are familiar with @unittest.X. So let's stick on it.

Performance doesn't matter.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@scripts/lib_updater.py`:
- Around line 273-283: has_unittest_import currently treats both "from unittest
import ..." and aliased imports as if the name unittest is available, which is
incorrect for decorators using `@unittest`.*; update has_unittest_import to only
return True when there is an ast.Import whose alias.name equals UT and
alias.asname is None (i.e., a plain "import unittest"), and do not treat
ast.ImportFrom (from unittest import ...) or imports with asname (import
unittest as ut) as satisfying the requirement; reference the has_unittest_import
function and the UT symbol when making this change.
- Around line 286-309: The current logic inserts "import unittest" at line 0
when no imports exist, which can place it before a module docstring or
encoding/shebang; update the insertion logic so it picks a safe line after any
shebang/encoding comments and the module docstring: either enhance
find_import_insert_line to detect and return the end line of the module
docstring (use ast.get_docstring(tree, clean=False) or inspect tree.body[0] if
it's an ast.Expr/ast.Constant string node) and use that end_lineno if > 0, and
also scan the top of the original file lines for a shebang (#!) and PEP-263
encoding comment and advance the insertion line past them; apply this change
where apply_patches calls find_import_insert_line (and keep the check using
modifications, has_unittest_import, and iter_patch_lines intact).
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 746e71a and 708f227.

📒 Files selected for processing (2)
  • .claude/commands/upgrade-pylib.md
  • scripts/lib_updater.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Follow PEP 8 style for custom Python code
Use ruff for linting Python code

Files:

  • scripts/lib_updater.py
🧠 Learnings (12)
📓 Common learnings
Learnt from: ShaharNaveh
Repo: RustPython/RustPython PR: 6089
File: scripts/lib_updater.py:292-297
Timestamp: 2025-08-30T14:40:05.858Z
Learning: In scripts/lib_updater.py, the --inplace flag intentionally writes to orig_file (not remote_file) even though patches are applied to remote_file content. This workflow allows updating the original RustPython test file with patches applied to new upstream CPython content.
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.778Z
Learning: Applies to Lib/test/**/*.py : Use `unittest.skip("TODO: RustPython <reason>")` or `unittest.expectedFailure` with `# TODO: RUSTPYTHON <reason>` comment when marking tests in Lib/ that cannot run
📚 Learning: 2026-01-14T14:52:10.778Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.778Z
Learning: Applies to Lib/**/*.py : Minimize modifications to CPython standard library files in the Lib/ directory; these files should be edited very conservatively and modifications should be minimal and only to work around RustPython limitations

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2026-01-14T14:52:10.778Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.778Z
Learning: Applies to Lib/**/*.py : Add a `# TODO: RUSTPYTHON` comment when modifications are made to Lib/ directory files

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2025-08-30T14:40:05.858Z
Learnt from: ShaharNaveh
Repo: RustPython/RustPython PR: 6089
File: scripts/lib_updater.py:292-297
Timestamp: 2025-08-30T14:40:05.858Z
Learning: In scripts/lib_updater.py, the --inplace flag intentionally writes to orig_file (not remote_file) even though patches are applied to remote_file content. This workflow allows updating the original RustPython test file with patches applied to new upstream CPython content.

Applied to files:

  • .claude/commands/upgrade-pylib.md
  • scripts/lib_updater.py
📚 Learning: 2026-01-14T14:52:10.778Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.778Z
Learning: Applies to Lib/test/**/*.py : Use `unittest.skip("TODO: RustPython <reason>")` or `unittest.expectedFailure` with `# TODO: RUSTPYTHON <reason>` comment when marking tests in Lib/ that cannot run

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2026-01-14T14:52:10.779Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.779Z
Learning: Do not edit the Lib/ directory directly except for copying files from CPython to work around RustPython limitations

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2026-01-14T14:52:10.779Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.779Z
Learning: When comparing behavior with CPython, use `python` command to explicitly run CPython and `cargo run -- script.py` to run RustPython

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2026-01-14T14:52:10.779Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.779Z
Learning: Use `cargo run -- script.py` instead of `python script.py` when testing Python code with RustPython

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2026-01-14T14:52:10.779Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.779Z
Learning: Run a full clean build when modifying bytecode instructions using: `rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r`

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2026-01-14T14:52:10.779Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.779Z
Learning: In most cases, Python code should not be edited; bug fixes should be made through Rust code modifications only

Applied to files:

  • .claude/commands/upgrade-pylib.md
📚 Learning: 2025-09-07T05:38:31.690Z
Learnt from: ShaharNaveh
Repo: RustPython/RustPython PR: 6089
File: scripts/lib_updater.py:198-202
Timestamp: 2025-09-07T05:38:31.690Z
Learning: In scripts/lib_updater.py, the iter_patches function intentionally does not handle SyntaxError from ast.parse(contents). The author confirmed this behavior is fine and intended - the tool should fail fast on unparseable files rather than silently skip processing.

Applied to files:

  • scripts/lib_updater.py
📚 Learning: 2026-01-14T14:52:10.778Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.778Z
Learning: Applies to **/test*.py : Only remove `unittest.expectedFailure` decorators and upper TODO comments from tests when tests actually pass, or add these decorators when tests cannot be fixed

Applied to files:

  • scripts/lib_updater.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check Rust code with clippy
🔇 Additional comments (1)
.claude/commands/upgrade-pylib.md (1)

18-107: Documentation updates are clear and actionable.

The expanded workflow and examples align well with the updated tooling and should reduce operator confusion.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@moreal moreal force-pushed the enhance-upgrade-pylib branch from 708f227 to 6679e0e Compare January 16, 2026 11:42
@moreal moreal marked this pull request as draft January 16, 2026 11:43
@moreal moreal force-pushed the enhance-upgrade-pylib branch from 6679e0e to 95f822d Compare January 16, 2026 12:01
@moreal moreal marked this pull request as ready for review January 16, 2026 12:21
@youknowone
Copy link
Member

@ShaharNaveh As the major author of lib_updater, please be a major reviewer of this PR. I will merge it once you approve it. Thanks!

if lines:
insert_line = 0
for i, line in enumerate(lines[:2]): # Only check first two lines per PEP 263
if line.startswith("#!") or re.match(
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this entire function is a bit of an overkill? inserting the import before the first:

  • class
  • function
  • constant

do the trick? we don't have lots of files that don't import unittest

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Come to think of it, Python test.test_* modules are for testing something, so they inherently have imports. I think the function's complexity is from implementing the suggestion of the below comment, so I'll revert it to first implementation:

#6742 (comment)

Like following lines:

def find_import_insert_line(tree: ast.Module) -> int:
    """Find the line number after the last import statement."""
    last_import_line = None
    for node in tree.body:
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            last_import_line = node.end_lineno or node.lineno
    assert last_import_line is not None  # expect passing because test module might have `import x` to test `x`
    return last_import_line

How do you think about it?

Copy link
Contributor

@ShaharNaveh ShaharNaveh Jan 17, 2026

Choose a reason for hiding this comment

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

This has an issue in the case of an import inside a test.

import posix

def test_posix_import():
    import nt
    ...

I'd put a condition to check that it it's indent is 0.


Or, insert it after the first import. It won't be pretty but I think it's putting a lot of effort to solve an uncommon problem

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think such a case would be an issue because it iterates ast.Module.body not recursively like visitor pattern. So, as long as the import is at the top like the usual pattern, it should be fine.

>>> import ast
>>> code = """import posix
...
... def test_posix_import():
...     import nt
...     ..."""
>>> ast.parse(code)
Module(body=[Import(names=[alias(name='posix', asname=None)]), FunctionDef(name='test_posix_import', args=arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Import(names=[alias(...)]), Expr(value=Constant(...))], decorator_list=[], returns=None, type_comment=None, type_params=[])], type_ignores=[])
>>> tree = ast.parse(code)
>>> for node in tree.body: print(node)
...
Import(names=[alias(name='posix', asname=None)])
FunctionDef(name='test_posix_import', args=arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Import(names=[alias(name='nt', asname=None)]), Expr(value=Constant(value=Ellipsis, kind=None))], decorator_list=[], returns=None, type_comment=None, type_params=[])

Copy link
Contributor

@ShaharNaveh ShaharNaveh Jan 17, 2026

Choose a reason for hiding this comment

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

Oh, you're right 🤦
let's go with that approach

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ShaharNaveh I just reverted the previous commit (at 5b54ec9) and added assert ... line for assumption (at 80dfec2). Please take a look!

@moreal moreal requested a review from ShaharNaveh January 17, 2026 09:22
Copy link
Contributor

@ShaharNaveh ShaharNaveh left a comment

Choose a reason for hiding this comment

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

LGTM!
@youknowone can we merge?

@moreal moreal requested a review from youknowone January 17, 2026 12:18
Copy link
Member

@youknowone youknowone left a comment

Choose a reason for hiding this comment

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

@moreal @ShaharNaveh Thank you so much!

@youknowone youknowone merged commit 3d86b26 into RustPython:main Jan 17, 2026
13 checks passed
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