-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add tool for easier test updates #6089
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
Changes from 1 commit
7c0a8fe
69ed76b
dc3a6de
5c09ef0
ad061b2
a6f2324
302b3d1
db74d2f
11694e3
d8b4e26
2308a5a
2fb6842
db5eb4b
af1c28d
42365d2
fdce40b
32baa80
fb7324d
f4056ac
4296b59
e2aa220
74b47a0
51c6ad9
01a90ef
a0e56ae
43a63a8
3e2c1f1
789cf6e
3e9872c
d0763e4
e00bb28
8a5875e
6c615cc
c0e90cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
gen subcommand
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,199 @@ | ||||||||||||||||||||||||||
| #!/usr/bin/env python | ||||||||||||||||||||||||||
| import argparse | ||||||||||||||||||||||||||
| import ast | ||||||||||||||||||||||||||
| import dataclasses | ||||||||||||||||||||||||||
| import enum | ||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||
| from typing import TYPE_CHECKING, Self | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||||||||
| from collections.abc import Iterator | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| COMMENT = "TODO: RUSTPYTHON" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @enum.unique | ||||||||||||||||||||||||||
| class ProgName(enum.StrEnum): | ||||||||||||||||||||||||||
| Gen = enum.auto() | ||||||||||||||||||||||||||
| Patch = enum.auto() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @enum.unique | ||||||||||||||||||||||||||
| class UtMethod(enum.StrEnum): | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| UnitTest Method | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ExpectedFailure = "expectedFailure" | ||||||||||||||||||||||||||
| ExpectedFailureIf = "expectedFailureIf" | ||||||||||||||||||||||||||
| ExpectedFailureIfWindows = "expectedFailureIfWindows" | ||||||||||||||||||||||||||
| SkipUnless = "skipUnless" | ||||||||||||||||||||||||||
| Skip = enum.auto() | ||||||||||||||||||||||||||
| SkipIf = "skipIf" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @dataclasses.dataclass(frozen=True, slots=True) | ||||||||||||||||||||||||||
| class PatchEntry: | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| Stores patch metadata. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Attributes | ||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||
| parent_class : str | ||||||||||||||||||||||||||
| Parent class of test. | ||||||||||||||||||||||||||
| test_name : str | ||||||||||||||||||||||||||
| Test name. | ||||||||||||||||||||||||||
| ut_method : UtMethod | ||||||||||||||||||||||||||
| unittest method. | ||||||||||||||||||||||||||
| cond : str, optional | ||||||||||||||||||||||||||
| `ut_method` condition. Relevant only for UtMethod.{expectedFailureIf,skipIf}. | ||||||||||||||||||||||||||
| reason : str, optional | ||||||||||||||||||||||||||
| Reason for why the test is patched in this way. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| parent_class: str | ||||||||||||||||||||||||||
| test_name: str | ||||||||||||||||||||||||||
| ut_method: UtMethod | ||||||||||||||||||||||||||
| cond: str | None = None | ||||||||||||||||||||||||||
| reason: str = "" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||
| def iter_patch_entires(cls, tree: ast.Module, lines: list[str]) -> "Iterator[Self]": | ||||||||||||||||||||||||||
| for cls_node, fn_node in iter_tests(tree): | ||||||||||||||||||||||||||
| parent_class = cls_node.name | ||||||||||||||||||||||||||
| for dec_node in fn_node.decorator_list: | ||||||||||||||||||||||||||
| if not isinstance(dec_node, (ast.Attribute, ast.Call)): | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| attr_node = ( | ||||||||||||||||||||||||||
| dec_node if isinstance(dec_node, ast.Attribute) else dec_node.func | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if isinstance(attr_node, ast.Name) or attr_node.value.id != "unittest": | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| if isinstance(attr_node, ast.Name) or attr_node.value.id != "unittest": | |
| if isinstance(attr_node, ast.Name): | |
| continue | |
| if isinstance(attr_node.value, ast.Name) and attr_node.value.id != "unittest": |
$ python ./scripts/lib_updater.py --from Lib/test/test_property.py --to ../cpython/Lib/test/test_property.py
Attribute(value=Name(id='spam', ctx=Load()), attr='setter', ctx=Load())
Attribute(value=Name(id='spam', ctx=Load()), attr='deleter', ctx=Load())
Attribute(value=Attribute(value=Name(id='BaseClass', ctx=Load()), attr='spam', ctx=Load()), attr='getter', ctx=Load())
Traceback (most recent call last):
File "/Users/user/Projects/rustpython2/./scripts/lib_updater.py", line 314, in <module>
patches = build_patch_dict(iter_patches(args.gather_from.read_text()))
File "/Users/user/Projects/rustpython2/./scripts/lib_updater.py", line 213, in build_patch_dict
for entry in it:
^^
File "/Users/user/Projects/rustpython2/./scripts/lib_updater.py", line 208, in iter_patches
yield from PatchEntry.iter_patch_entires(tree, lines)
File "/Users/user/Projects/rustpython2/./scripts/lib_updater.py", line 131, in iter_patch_entires
if isinstance(attr_node, ast.Name) or attr_node.value.id != "unittest":
^^^^^^^^^^^^^^^^^^
AttributeError: 'Attribute' object has no attribute 'id'
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.
That's a bug 🎉
Will use getattr instead.
And attr_node can be of type Name, but if that's the case then we skip as it's not a unittest patch
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
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.
expectedFailure reason extraction misses same-line comments and regex is too strict
- You’re scanning the two lines above the decorator, but
fmt()renders the comment on the same line. - The regex requires a space after the marker;
fmt()uses a semicolon.
- for line in lines[dec_node.lineno - 2 : dec_node.lineno]:
- if COMMENT not in line:
- continue
- reason = "".join(re.findall(rf"{COMMENT} (.*)", line))
+ # Look at the decorator line itself and accept :, ; or space after the marker
+ line = lines[dec_node.lineno - 1]
+ if COMMENT in line:
+ m = re.search(rf"{re.escape(COMMENT)}\s*[:;]?\s*(.*)$", line)
+ reason = m.group(1) if m else ""
break
else:
continueAlso applies to: 118-118
🤖 Prompt for AI Agents
In scripts/lib_updater.py around lines 115 to 121 (and similarly at 118), the
current loop only checks the two lines before the decorator and uses a regex
that requires a space after the COMMENT marker, missing comments placed on the
same line or separated by a semicolon; update the slice to include the decorator
line (e.g., lines[dec_node.lineno - 2 : dec_node.lineno + 1]) so same-line
comments are scanned, and replace the strict regex with one that allows optional
separators and flexible spacing (e.g., use re.search with a pattern like
rf"{COMMENT}\s*(?:[:;]\s*)?(.*)" and extract group(1).strip() ), breaking when a
hit is found.
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.
🛠️ Refactor suggestion
Handle unparseable files gracefully (bad-syntax tests)
ast.parse can raise SyntaxError on intentionally invalid test files; fail gracefully with a clear message.
-def iter_patches(contents: str) -> "Iterator[PatchEntry]":
- lines = contents.splitlines()
- tree = ast.parse(contents)
- yield from PatchEntry.iter_patch_entires(tree, lines)
+def iter_patches(contents: str) -> "Iterator[PatchEntry]":
+ lines = contents.splitlines()
+ try:
+ tree = ast.parse(contents)
+ except SyntaxError as e:
+ print(f"ERROR: could not parse source for patch extraction: {e}", file=sys.stderr)
+ return
+ yield from PatchEntry.iter_patch_entires(tree, lines)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def iter_patches(contents: str) -> "Iterator[PatchEntry]": | |
| lines = contents.splitlines() | |
| tree = ast.parse(contents) | |
| yield from PatchEntry.iter_patch_entires(tree, lines) | |
| def iter_patches(contents: str) -> "Iterator[PatchEntry]": | |
| lines = contents.splitlines() | |
| try: | |
| tree = ast.parse(contents) | |
| except SyntaxError as e: | |
| print(f"ERROR: could not parse source for patch extraction: {e}", file=sys.stderr) | |
| return | |
| yield from PatchEntry.iter_patch_entires(tree, lines) |
🤖 Prompt for AI Agents
In scripts/lib_updater.py around lines 198 to 202, ast.parse(contents) can raise
SyntaxError for intentionally invalid test files; wrap the parse call in a
try/except that catches SyntaxError and handle it gracefully by raising a
clearer ValueError (or RuntimeError) with a human-readable message that includes
the original exception details (or alternatively return an empty iterator),
preserving the function signature and ensuring the original exception is chained
for debugging.
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.
This is fine and intended. you can ignore it
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.
@ShaharNaveh Understood! I'll ignore the error handling suggestion. The current behavior of letting ast.parse() raise SyntaxError directly is intentional for your use case.
✏️ Learnings added
Learnt from: ShaharNaveh
PR: RustPython/RustPython#6089
File: scripts/lib_updater.py:198-202
Timestamp: 2025-09-07T05:38:31.644Z
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.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: CR
PR: RustPython/RustPython#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-19T03:16:56.511Z
Learning: Applies to Lib/test/**/*.py : When tests fail due to unsupported syntax, keep the test as `unittest.expectedFailure`, document that it requires PEP 695 support, and focus on tests that can be fixed through Rust code changes only
Learnt from: ShaharNaveh
PR: RustPython/RustPython#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.
Uh oh!
There was an error while loading. Please reload this page.