Skip to content

scripts/update_lib migrate to preserve patches on classes#8057

Merged
youknowone merged 8 commits into
RustPython:mainfrom
ShaharNaveh:patches-on-class
Jun 10, 2026
Merged

scripts/update_lib migrate to preserve patches on classes#8057
youknowone merged 8 commits into
RustPython:mainfrom
ShaharNaveh:patches-on-class

Conversation

@ShaharNaveh

@ShaharNaveh ShaharNaveh commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Previously, patches on classes itself were drop when migrating patches.

@unittest.skip("TODO RUSTPYTHON; Segfault") # <-- this would get dropped
class TestFoo:
  ...

Summary by CodeRabbit

  • Refactor

    • Streamlined patch extraction and application logic in development tooling for improved maintainability.
  • Tests

    • Added test coverage for multi-decorator patch handling and re-application in development scripts.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Patch spec module refactored to centralize extraction via AST visitor pattern and application via shared helper. Decorator parsing moved into PatchSpec.try_from_ast_node and PatchEntryVisitor; patch insertion line calculation extracted into modification_from_node_specs for consistent handling across function and class-level modifications. New round-trip test validates class-level decorator extraction and re-application.

Changes

Patch extraction and application refactoring

Layer / File(s) Summary
AST-based patch extraction with visitor pattern
scripts/update_lib/patch_spec.py
Imports contextlib and re; adds PatchSpec.try_from_ast_node classmethod to parse decorators from AST nodes; introduces PatchEntryVisitor to traverse AST and collect patch entries for functions and async functions while tracking enclosing class; implements temp_attr context manager for state restoration during traversal; delegates PatchEntry.iter_patch_entries to visitor.
Patch application refactoring with shared helper
scripts/update_lib/patch_spec.py
Adds modification_from_node_specs helper to compute insertion line and indentation; refactors _iter_patch_lines to precompute all class nodes and use helper consistently in phase 1 (function-level) and phase 3 (class-level); updates phase 2 to skip __self__ entries; refactors class-level modification logic to iterate over collected nodes.
Round-trip test for class-level decorators
scripts/update_lib/tests/test_patch_spec.py
Adds test_round_trip_with_patches_on_class to extract multiple class-level decorators (@unittest.skipIf, @unittest.expectedFailure with patch comment marker) from an original module and re-apply them to a clean module, verifying decorator and comment preservation.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • RustPython/RustPython#7802: Both PRs modify patch-spec extraction for unittest decorators and extend test coverage with multi-decorator scenarios.

Suggested reviewers

  • youknowone

Poem

🐰 A visitor hops through the AST tree,
Collecting each decorator it shall see,
The helper computes indents with care,
Patches extracted and re-applied fair,
Refactored to patterns both clean and neat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective: enabling the migrate command to preserve patches (decorators) on class definitions, which was the core problem addressed by refactoring the patch extraction and application logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

@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: 3

🤖 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 `@scripts/update_lib/patch_spec.py`:
- Around line 145-149: The code reads prev_line = lines[dec_lineno - 2] without
checking for file start, which makes lines[-1] get used when a decorator is on
the first line; change the logic around dec_lineno (from node.lineno) so you
only index dec_lineno - 2 when dec_lineno > 1, otherwise set prev_line to an
empty string or None; update any downstream checks that read prev_line (in the
same function handling decorator parsing) to handle the empty/None value to
avoid treating EOF/comments as decorator metadata.
- Around line 141-143: When extracting the decorator condition in patch_spec.py
(inside the block guarded by ut_method.has_cond()), avoid indexing node.args
directly; instead if node.args is non-empty keep using node.args[0], otherwise
search node.keywords for the keyword named "condition" (i.e. iterate
node.keywords and match kw.arg == "condition") and set cond =
ast.unparse(kw.value) if found; if neither exists set cond = None or skip
extraction so the code using cond won't raise IndexError. Reference:
ut_method.has_cond(), cond variable, and node.args / node.keywords.

In `@scripts/update_lib/tests/test_patch_spec.py`:
- Around line 348-373: The new test method test_round_trip_with_patches_on_class
violates the repo policy against modifying test files; remove this added test
(the function test_round_trip_with_patches_on_class and its assertions
referencing extract_patches, apply_patches, and COMMENT) from the patch so no
new test logic or assertions are introduced in *test*.py files, or if needed,
move equivalent validation into an allowed non-test location (e.g., a new
integration/example script) and keep the test file unchanged.
🪄 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: 4a9b38eb-6436-4447-a7cd-65fb4c870c64

📥 Commits

Reviewing files that changed from the base of the PR and between 51c97b9 and 2bf0b6a.

📒 Files selected for processing (2)
  • scripts/update_lib/patch_spec.py
  • scripts/update_lib/tests/test_patch_spec.py

Comment on lines +141 to +143
if ut_method.has_cond():
cond = ast.unparse(node.args[0])
else:

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 | 🟠 Major | ⚡ Quick win

Handle keyword-based conditional decorators without crashing.

Line 142 assumes a positional condition (node.args[0]). Valid forms like @unittest.skipIf(condition=..., reason=...) make node.args empty and raise IndexError during extraction.

Proposed fix
-            if ut_method.has_cond():
-                cond = ast.unparse(node.args[0])
+            if ut_method.has_cond():
+                if not isinstance(node, ast.Call):
+                    return
+                if node.args:
+                    cond_node = node.args[0]
+                else:
+                    cond_node = next(
+                        (kw.value for kw in node.keywords if kw.arg in {"condition", "cond"}),
+                        None,
+                    )
+                    if cond_node is None:
+                        return
+                cond = ast.unparse(cond_node)
🤖 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 `@scripts/update_lib/patch_spec.py` around lines 141 - 143, When extracting the
decorator condition in patch_spec.py (inside the block guarded by
ut_method.has_cond()), avoid indexing node.args directly; instead if node.args
is non-empty keep using node.args[0], otherwise search node.keywords for the
keyword named "condition" (i.e. iterate node.keywords and match kw.arg ==
"condition") and set cond = ast.unparse(kw.value) if found; if neither exists
set cond = None or skip extraction so the code using cond won't raise
IndexError. Reference: ut_method.has_cond(), cond variable, and node.args /
node.keywords.

Comment on lines +145 to +149
dec_lineno = node.lineno

curr_line = lines[dec_lineno - 1]
prev_line = lines[dec_lineno - 2]

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 | 🟡 Minor | ⚡ Quick win

Guard previous-line lookup at file start.

Line 148 reads lines[dec_lineno - 2] unconditionally. If the decorator is on the first line, this becomes lines[-1] and can incorrectly treat EOF comments as decorator metadata.

Proposed fix
-            prev_line = lines[dec_lineno - 2]
+            prev_line = lines[dec_lineno - 2] if dec_lineno > 1 else ""
🤖 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 `@scripts/update_lib/patch_spec.py` around lines 145 - 149, The code reads
prev_line = lines[dec_lineno - 2] without checking for file start, which makes
lines[-1] get used when a decorator is on the first line; change the logic
around dec_lineno (from node.lineno) so you only index dec_lineno - 2 when
dec_lineno > 1, otherwise set prev_line to an empty string or None; update any
downstream checks that read prev_line (in the same function handling decorator
parsing) to handle the empty/None value to avoid treating EOF/comments as
decorator metadata.

Comment on lines +348 to +373
def test_round_trip_with_patches_on_class(self):
"""Test that extracted patches can be re-applied."""
original = f"""import unittest

@unittest.skipIf(a == b, "{COMMENT}")
@unittest.expectedFailure # {COMMENT}
class TestFoo(unittest.TestCase):
...
"""
# Extract patches
patches = extract_patches(original)

# Apply to clean code
clean = """import unittest

class TestFoo(unittest.TestCase):
def test_one(self):
pass
"""
result = apply_patches(clean, patches)

# Should have the decorator
self.assertIn("@unittest.expectedFailure", result)
self.assertIn("@unittest.skipIf", result)
self.assertIn(COMMENT, result)

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 | 🟠 Major | ⚡ Quick win

This test addition violates the repository’s test-file change policy.

Lines 348-373 introduce new test logic/data/assertions in a *test*.py file, which is outside the allowed modification scope in this repo policy.

As per coding guidelines, **/*test*.py says: “NEVER modify test assertions, test logic, or test data” and limits acceptable edits to expectedFailure/TODO handling. Based on learnings, AGENTS guidance repeats the same restriction.

🤖 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 `@scripts/update_lib/tests/test_patch_spec.py` around lines 348 - 373, The new
test method test_round_trip_with_patches_on_class violates the repo policy
against modifying test files; remove this added test (the function
test_round_trip_with_patches_on_class and its assertions referencing
extract_patches, apply_patches, and COMMENT) from the patch so no new test logic
or assertions are introduced in *test*.py files, or if needed, move equivalent
validation into an allowed non-test location (e.g., a new integration/example
script) and keep the test file unchanged.

Sources: Coding guidelines, Learnings

@youknowone youknowone merged commit 7f75ef1 into RustPython:main Jun 10, 2026
31 of 32 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.

2 participants