Align lazy imports and f/t-string interpolation with Python 3.15 (#9179)

* Preserve interpolation source text and canonical metadata

Source-only checkpoint; candidate validation and the existing annotation expectation reconciliation remain pending.

Assisted-by: OpenAI Codex:model version unavailable

* Align existing annotation expectations with CPython metadata

Update only the nine existing expected strings confirmed against pinned CPython 3.15, with explicit user approval.

Assisted-by: OpenAI Codex:model version unavailable

* Preserve template metadata through compilation and interactive input

Escape literal debug prefixes, retain None AST expression metadata internally, and continue incomplete triple-quoted t-strings in the existing shell.

Assisted-by: OpenAI Codex:model version unavailable

* Classify normalized unclosed brackets as REPL line continuations

Assisted-by: OpenAI Codex:model version unavailable

* Nest equivalent REPL parse-error alternatives for Clippy

Assisted-by: OpenAI Codex:model version unavailable

* Remove fulfilled annotation migration lint workaround

The canonical expected strings no longer trigger literal_string_with_formatting_args, so the previous lint expectation is obsolete.

Assisted-by: OpenAI Codex:model version unavailable

* Complete lazy-import exception and traceback library closure

Copy canonical traceback and legacy exception mappings with preserved RustPython guards. Copy explicit non-Python library assets by exact path so exception hierarchy data does not expand into the entire test package.

Assisted-by: OpenAI Codex:model version unavailable

* Provide Python 3.15 traceback metadata for missing modules and attributes

Preserve AttributeError context for missing class attribute deletion, matching type_update_dict. Expose canonical generated missing-stdlib messages through the native sysconfig modules without importing CPython build-specific disabled-module assumptions.

Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-18 17:11:26 +09:00
1 parent 6e8e7e0481
commit 9f6f771965
135 files changed
+7502 -1100

No files matched your search

+9 -5
View File
@@ -239,11 +239,11 @@ class Unparser(NodeVisitor):
self.traverse(node.value)
def visit_Import(self, node):
self.fill("import ")
self.fill("lazy import " if node.is_lazy else "import ")
self.interleave(lambda: self.write(", "), self.traverse, node.names)
def visit_ImportFrom(self, node):
self.fill("from ")
self.fill("lazy from " if node.is_lazy else "from ")
self.write("." * (node.level or 0))
if node.module:
self.write(node.module)
@@ -738,9 +738,13 @@ class Unparser(NodeVisitor):
def visit_DictComp(self, node):
with self.delimit("{", "}"):
self.traverse(node.key)
self.write(": ")
self.traverse(node.value)
if node.value:
self.traverse(node.key)
self.write(": ")
self.traverse(node.value)
else:
self.write("**")
self.traverse(node.key)
for gen in node.generators:
self.traverse(gen)
+302 -10
View File
@@ -1,21 +1,19 @@
import builtins
import os
import sys
from collections.abc import Callable, Iterator, Mapping
from dataclasses import dataclass, field, Field
lazy from typing import IO, Literal, Self, ClassVar
COLORIZE = True
# types
if False:
from typing import IO, Self, ClassVar
_theme: Theme
_theme: Theme
type BackgroundStyle = Literal["dark", "light"]
class ANSIColors:
RESET = "\x1b[0m"
BLACK = "\x1b[30m"
BLUE = "\x1b[34m"
CYAN = "\x1b[36m"
@@ -74,6 +72,19 @@ for attr, code in ANSIColors.__dict__.items():
setattr(NoColors, attr, "")
class CursesColors:
"""Curses color constants for terminal UI theming."""
BLACK = 0
RED = 1
GREEN = 2
YELLOW = 3
BLUE = 4
MAGENTA = 5
CYAN = 6
WHITE = 7
DEFAULT = -1
#
# Experimental theming support (see gh-133346)
#
@@ -168,10 +179,227 @@ class Argparse(ThemeSection):
short_option: str = ANSIColors.BOLD_GREEN
label: str = ANSIColors.BOLD_YELLOW
action: str = ANSIColors.BOLD_GREEN
default: str = ANSIColors.GREY
interpolated_value: str = ANSIColors.YELLOW
reset: str = ANSIColors.RESET
error: str = ANSIColors.BOLD_MAGENTA
warning: str = ANSIColors.BOLD_YELLOW
message: str = ANSIColors.MAGENTA
@dataclass(frozen=True, kw_only=True)
class Ast(ThemeSection):
node: str = ANSIColors.CYAN
field: str = ANSIColors.BLUE
attribute: str = ANSIColors.GREY
string: str = ANSIColors.GREEN
number: str = ANSIColors.YELLOW
keyword: str = ANSIColors.BOLD_BLUE
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Calendar(ThemeSection):
header: str = ANSIColors.BOLD
highlight: str = ANSIColors.BLACK + ANSIColors.BACKGROUND_YELLOW
weekday: str = ANSIColors.CYAN
reset: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class Difflib(ThemeSection):
"""A 'git diff'-like theme for `difflib.unified_diff`."""
added: str = ANSIColors.GREEN
context: str = ANSIColors.RESET # context lines
header: str = ANSIColors.BOLD # eg "---" and "+++" lines
hunk: str = ANSIColors.CYAN # the "@@" lines
removed: str = ANSIColors.RED
reset: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class FancyCompleter(ThemeSection):
# functions and methods
function: builtins.str = ANSIColors.BOLD_BLUE
builtin_function_or_method: builtins.str = ANSIColors.BOLD_BLUE
method: builtins.str = ANSIColors.BOLD_CYAN
method_wrapper: builtins.str = ANSIColors.BOLD_CYAN
wrapper_descriptor: builtins.str = ANSIColors.BOLD_CYAN
method_descriptor: builtins.str = ANSIColors.BOLD_CYAN
# numbers
int: builtins.str = ANSIColors.BOLD_YELLOW
float: builtins.str = ANSIColors.BOLD_YELLOW
complex: builtins.str = ANSIColors.BOLD_YELLOW
bool: builtins.str = ANSIColors.BOLD_YELLOW
# others
type: builtins.str = ANSIColors.BOLD_MAGENTA
module: builtins.str = ANSIColors.CYAN
NoneType: builtins.str = ANSIColors.GREY
bytes: builtins.str = ANSIColors.BOLD_GREEN
str: builtins.str = ANSIColors.BOLD_GREEN
@dataclass(frozen=True, kw_only=True)
class HttpServer(ThemeSection):
error: str = ANSIColors.YELLOW
path: str = ANSIColors.CYAN
serving: str = ANSIColors.GREEN
size: str = ANSIColors.GREY
status_informational: str = ANSIColors.RESET
status_ok: str = ANSIColors.GREEN
status_redirect: str = ANSIColors.INTENSE_CYAN
status_client_error: str = ANSIColors.YELLOW
status_server_error: str = ANSIColors.RED
timestamp: str = ANSIColors.GREY
url: str = ANSIColors.CYAN
reset: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class LiveProfiler(ThemeSection):
"""Theme section for the live profiling TUI (Tachyon profiler).
Colors use CursesColors constants (BLACK, RED, GREEN, YELLOW,
BLUE, MAGENTA, CYAN, WHITE, DEFAULT).
"""
# Header colors
title_fg: int = CursesColors.CYAN
title_bg: int = CursesColors.DEFAULT
# Status display colors
pid_fg: int = CursesColors.CYAN
uptime_fg: int = CursesColors.GREEN
time_fg: int = CursesColors.YELLOW
interval_fg: int = CursesColors.MAGENTA
# Thread view colors
thread_all_fg: int = CursesColors.GREEN
thread_single_fg: int = CursesColors.MAGENTA
# Progress bar colors
bar_good_fg: int = CursesColors.GREEN
bar_bad_fg: int = CursesColors.RED
# Stats colors
on_gil_fg: int = CursesColors.GREEN
off_gil_fg: int = CursesColors.RED
waiting_gil_fg: int = CursesColors.YELLOW
gc_fg: int = CursesColors.MAGENTA
# Function display colors
func_total_fg: int = CursesColors.CYAN
func_exec_fg: int = CursesColors.GREEN
func_stack_fg: int = CursesColors.YELLOW
func_shown_fg: int = CursesColors.MAGENTA
# Table header colors (for sorted column highlight)
sorted_header_fg: int = CursesColors.BLACK
sorted_header_bg: int = CursesColors.CYAN
# Normal header colors (non-sorted columns) - use reverse video style
normal_header_fg: int = CursesColors.BLACK
normal_header_bg: int = CursesColors.WHITE
# Data row colors
samples_fg: int = CursesColors.CYAN
file_fg: int = CursesColors.GREEN
func_fg: int = CursesColors.YELLOW
# Trend indicator colors
trend_up_fg: int = CursesColors.GREEN
trend_down_fg: int = CursesColors.RED
# Medal colors for top functions
medal_gold_fg: int = CursesColors.RED
medal_silver_fg: int = CursesColors.YELLOW
medal_bronze_fg: int = CursesColors.GREEN
# Background style: 'dark' or 'light'
background_style: BackgroundStyle = "dark"
LiveProfilerLight = LiveProfiler(
# Header colors
title_fg=CursesColors.BLUE, # Blue is more readable than cyan on light bg
# Status display colors - darker colors for light backgrounds
pid_fg=CursesColors.BLUE,
uptime_fg=CursesColors.BLACK,
time_fg=CursesColors.BLACK,
interval_fg=CursesColors.BLUE,
# Thread view colors
thread_all_fg=CursesColors.BLACK,
thread_single_fg=CursesColors.BLUE,
# Stats colors
waiting_gil_fg=CursesColors.RED,
gc_fg=CursesColors.BLUE,
# Function display colors
func_total_fg=CursesColors.BLUE,
func_exec_fg=CursesColors.BLACK,
func_stack_fg=CursesColors.BLACK,
func_shown_fg=CursesColors.BLUE,
# Table header colors (for sorted column highlight)
sorted_header_fg=CursesColors.WHITE,
sorted_header_bg=CursesColors.BLUE,
# Normal header colors (non-sorted columns)
normal_header_fg=CursesColors.WHITE,
normal_header_bg=CursesColors.BLACK,
# Data row colors - use dark colors readable on white
samples_fg=CursesColors.BLACK,
file_fg=CursesColors.BLACK,
func_fg=CursesColors.BLUE, # Blue is more readable than magenta on light bg
# Medal colors for top functions
medal_silver_fg=CursesColors.BLUE,
# Background style
background_style="light",
)
@dataclass(frozen=True, kw_only=True)
class ProfilerDump(ThemeSection):
header: str = ANSIColors.BOLD_BLUE
interpreter: str = ANSIColors.GREY
thread: str = ANSIColors.BOLD_CYAN
status: str = ANSIColors.YELLOW
frame_index: str = ANSIColors.GREY
frame: str = ANSIColors.BOLD_GREEN
filename: str = ANSIColors.CYAN
line_no: str = ANSIColors.YELLOW
source: str = ANSIColors.WHITE
source_highlight: str = ANSIColors.BOLD_YELLOW
opcode: str = ANSIColors.GREY
warning: str = ANSIColors.YELLOW
reset: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class Pickletools(ThemeSection):
annotation: str = ANSIColors.GREY
arg_number: str = ANSIColors.YELLOW
arg_string: str = ANSIColors.GREEN
mark: str = ANSIColors.GREY
op_call: str = ANSIColors.GREEN
op_container: str = ANSIColors.INTENSE_BLUE
op_memo: str = ANSIColors.MAGENTA
op_meta: str = ANSIColors.GREY
op_stack: str = ANSIColors.BOLD_RED
opcode_code: str = ANSIColors.CYAN
position: str = ANSIColors.GREY
proto: str = ANSIColors.YELLOW
reset: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class Syntax(ThemeSection):
prompt: str = ANSIColors.BOLD_MAGENTA
keyword: str = ANSIColors.BOLD_BLUE
@@ -186,10 +414,31 @@ class Syntax(ThemeSection):
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Timeit(ThemeSection):
timing: str = ANSIColors.CYAN
best: str = ANSIColors.BOLD_GREEN
per_loop: str = ANSIColors.GREEN
punctuation: str = ANSIColors.GREY
warning: str = ANSIColors.YELLOW
warning_worst: str = ANSIColors.MAGENTA
warning_best: str = ANSIColors.GREEN
reset: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class Tokenize(ThemeSection):
whitespace: str = ANSIColors.GREY
error: str = ANSIColors.BOLD_RED
position: str = ANSIColors.GREY
delimiter: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class Traceback(ThemeSection):
type: str = ANSIColors.BOLD_MAGENTA
message: str = ANSIColors.MAGENTA
note: str = ANSIColors.CYAN
filename: str = ANSIColors.MAGENTA
line_no: str = ANSIColors.MAGENTA
frame: str = ANSIColors.MAGENTA
@@ -198,7 +447,7 @@ class Traceback(ThemeSection):
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Unittest(ThemeSection):
passed: str = ANSIColors.GREEN
warn: str = ANSIColors.YELLOW
@@ -207,7 +456,7 @@ class Unittest(ThemeSection):
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Theme:
"""A suite of themes for all sections of Python.
@@ -215,7 +464,17 @@ class Theme:
below.
"""
argparse: Argparse = field(default_factory=Argparse)
ast: Ast = field(default_factory=Ast)
calendar: Calendar = field(default_factory=Calendar)
difflib: Difflib = field(default_factory=Difflib)
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
http_server: HttpServer = field(default_factory=HttpServer)
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
pickletools: Pickletools = field(default_factory=Pickletools)
profiler_dump: ProfilerDump = field(default_factory=ProfilerDump)
syntax: Syntax = field(default_factory=Syntax)
timeit: Timeit = field(default_factory=Timeit)
tokenize: Tokenize = field(default_factory=Tokenize)
traceback: Traceback = field(default_factory=Traceback)
unittest: Unittest = field(default_factory=Unittest)
@@ -223,7 +482,17 @@ class Theme:
self,
*,
argparse: Argparse | None = None,
ast: Ast | None = None,
calendar: Calendar | None = None,
difflib: Difflib | None = None,
fancycompleter: FancyCompleter | None = None,
http_server: HttpServer | None = None,
live_profiler: LiveProfiler | None = None,
pickletools: Pickletools | None = None,
profiler_dump: ProfilerDump | None = None,
syntax: Syntax | None = None,
timeit: Timeit | None = None,
tokenize: Tokenize | None = None,
traceback: Traceback | None = None,
unittest: Unittest | None = None,
) -> Self:
@@ -234,7 +503,17 @@ class Theme:
"""
return type(self)(
argparse=argparse or self.argparse,
ast=ast or self.ast,
calendar=calendar or self.calendar,
difflib=difflib or self.difflib,
fancycompleter=fancycompleter or self.fancycompleter,
http_server=http_server or self.http_server,
live_profiler=live_profiler or self.live_profiler,
pickletools=pickletools or self.pickletools,
profiler_dump=profiler_dump or self.profiler_dump,
syntax=syntax or self.syntax,
timeit=timeit or self.timeit,
tokenize=tokenize or self.tokenize,
traceback=traceback or self.traceback,
unittest=unittest or self.unittest,
)
@@ -249,7 +528,17 @@ class Theme:
"""
return cls(
argparse=Argparse.no_colors(),
ast=Ast.no_colors(),
calendar=Calendar.no_colors(),
difflib=Difflib.no_colors(),
fancycompleter=FancyCompleter.no_colors(),
http_server=HttpServer.no_colors(),
live_profiler=LiveProfiler.no_colors(),
pickletools=Pickletools.no_colors(),
profiler_dump=ProfilerDump.no_colors(),
syntax=Syntax.no_colors(),
timeit=Timeit.no_colors(),
tokenize=Tokenize.no_colors(),
traceback=Traceback.no_colors(),
unittest=Unittest.no_colors(),
)
@@ -318,6 +607,9 @@ def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
default_theme = Theme()
theme_no_color = default_theme.no_colors()
# Convenience theme with light profiler colors (for white/light terminal backgrounds)
light_profiler_theme = default_theme.copy_with(live_profiler=LiveProfilerLight)
def get_theme(
*,
+1 -3
View File
@@ -183,7 +183,6 @@ IMPORT_MAPPING.update({
'SimpleDialog': 'tkinter.simpledialog',
'DocXMLRPCServer': 'xmlrpc.server',
'SimpleHTTPServer': 'http.server',
'CGIHTTPServer': 'http.server',
# For compatibility with broken pickles saved in old Python 3 versions
'UserDict': 'collections',
'UserList': 'collections',
@@ -225,8 +224,6 @@ REVERSE_NAME_MAPPING.update({
('DocXMLRPCServer', 'DocCGIXMLRPCRequestHandler'),
('http.server', 'SimpleHTTPRequestHandler'):
('SimpleHTTPServer', 'SimpleHTTPRequestHandler'),
('http.server', 'CGIHTTPRequestHandler'):
('CGIHTTPServer', 'CGIHTTPRequestHandler'),
('_socket', 'socket'): ('socket', '_socketobject'),
})
@@ -251,6 +248,7 @@ for excname in PYTHON3_OSERROR_EXCEPTIONS:
REVERSE_NAME_MAPPING[('builtins', excname)] = ('exceptions', 'OSError')
PYTHON3_IMPORTERROR_EXCEPTIONS = (
'ImportCycleError',
'ModuleNotFoundError',
)
+18 -3
View File
@@ -191,12 +191,27 @@ class ForwardRef:
arg = self.__forward_arg__
if arg.isidentifier() and not keyword.iskeyword(arg):
resolved = _sentinel
if arg in locals:
return locals[arg]
resolved = locals[arg]
elif arg in globals:
return globals[arg]
resolved = globals[arg]
elif hasattr(builtins, arg):
return getattr(builtins, arg)
resolved = getattr(builtins, arg)
if resolved is not _sentinel:
if isinstance(resolved, types.LazyImportType):
# We try reifying the lazy object. If this fails, we propagate
# the error in the VALUE format and leave the
# ForwardRef unresolved in the FORWARDREF format.
try:
return resolved.resolve()
except Exception:
if not is_forwardref_format:
raise
return self
else:
return resolved
elif is_forwardref_format:
return self
else:
Vendored
+103 -65
View File
@@ -24,7 +24,7 @@ from _ast import *
def parse(source, filename='<unknown>', mode='exec', *,
type_comments=False, feature_version=None, optimize=-1):
type_comments=False, feature_version=None, optimize=-1, module=None):
"""
Parse the source into an AST node.
Equivalent to compile(source, filename, mode, PyCF_ONLY_AST).
@@ -44,7 +44,8 @@ def parse(source, filename='<unknown>', mode='exec', *,
feature_version = minor
# Else it should be an int giving the minor version for 3.x.
return compile(source, filename, mode, flags,
_feature_version=feature_version, optimize=optimize)
_feature_version=feature_version, optimize=optimize,
module=module)
def literal_eval(node_or_string):
@@ -57,73 +58,93 @@ def literal_eval(node_or_string):
Caution: A complex expression can overflow the C stack and cause a crash.
"""
if isinstance(node_or_string, str):
node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval')
if isinstance(node_or_string, Expression):
node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval').body
elif isinstance(node_or_string, Expression):
node_or_string = node_or_string.body
def _raise_malformed_node(node):
msg = "malformed node or string"
if lno := getattr(node, 'lineno', None):
msg += f' on line {lno}'
raise ValueError(msg + f': {node!r}')
def _convert_num(node):
if not isinstance(node, Constant) or type(node.value) not in (int, float, complex):
_raise_malformed_node(node)
return _convert_literal(node_or_string)
def _convert_literal(node):
"""
Used by `literal_eval` to convert an AST node into a value.
"""
if isinstance(node, Constant):
return node.value
def _convert_signed_num(node):
if isinstance(node, UnaryOp) and isinstance(node.op, (UAdd, USub)):
operand = _convert_num(node.operand)
if isinstance(node.op, UAdd):
return + operand
else:
return - operand
return _convert_num(node)
def _convert(node):
if isinstance(node, Constant):
return node.value
elif isinstance(node, Tuple):
return tuple(map(_convert, node.elts))
elif isinstance(node, List):
return list(map(_convert, node.elts))
elif isinstance(node, Set):
return set(map(_convert, node.elts))
elif (isinstance(node, Call) and isinstance(node.func, Name) and
node.func.id == 'set' and node.args == node.keywords == []):
return set()
elif isinstance(node, Dict):
if len(node.keys) != len(node.values):
_raise_malformed_node(node)
return dict(zip(map(_convert, node.keys),
map(_convert, node.values)))
elif isinstance(node, BinOp) and isinstance(node.op, (Add, Sub)):
left = _convert_signed_num(node.left)
right = _convert_num(node.right)
if isinstance(left, (int, float)) and isinstance(right, complex):
if isinstance(node.op, Add):
return left + right
else:
return left - right
return _convert_signed_num(node)
return _convert(node_or_string)
if isinstance(node, Dict) and len(node.keys) == len(node.values):
return dict(zip(
map(_convert_literal, node.keys),
map(_convert_literal, node.values),
))
if isinstance(node, Tuple):
return tuple(map(_convert_literal, node.elts))
if isinstance(node, List):
return list(map(_convert_literal, node.elts))
if isinstance(node, Set):
return set(map(_convert_literal, node.elts))
if (
isinstance(node, Call) and isinstance(node.func, Name)
and node.func.id == 'set' and node.args == node.keywords == []
):
return set()
if (
isinstance(node, UnaryOp)
and isinstance(node.op, (UAdd, USub))
and isinstance(node.operand, Constant)
and type(operand := node.operand.value) in (int, float, complex)
):
if isinstance(node.op, UAdd):
return + operand
else:
return - operand
if (
isinstance(node, BinOp)
and isinstance(node.op, (Add, Sub))
and isinstance(node.left, (Constant, UnaryOp))
and isinstance(node.right, Constant)
and type(left := _convert_literal(node.left)) in (int, float)
and type(right := _convert_literal(node.right)) is complex
):
if isinstance(node.op, Add):
return left + right
else:
return left - right
msg = "malformed node or string"
if lno := getattr(node, 'lineno', None):
msg += f' on line {lno}'
raise ValueError(msg + f': {node!r}')
def dump(
node, annotate_fields=True, include_attributes=False,
*,
indent=None, show_empty=False,
color=False, indent=None, show_empty=False,
):
"""
Return a formatted dump of the tree in node. This is mainly useful for
debugging purposes. If annotate_fields is true (by default),
the returned string will show the names and the values for fields.
If annotate_fields is false, the result string will be more compact by
omitting unambiguous field names. Attributes such as line
numbers and column offsets are not dumped by default. If this is wanted,
include_attributes can be set to true. If indent is a non-negative
integer or string, then the tree will be pretty-printed with that indent
level. None (the default) selects the single line representation.
debugging purposes.
If annotate_fields is true (by default), the returned string will show the
names and the values for fields. If annotate_fields is false, the result
string will be more compact by omitting unambiguous field names.
Attributes such as line numbers and column offsets are not dumped by default.
If this is wanted, include_attributes can be set to true.
If color is true, the returned string is syntax highlighted using ANSI
escape sequences. If color is false (the default), colored output is always
disabled.
If indent is a non-negative integer or string, then the tree will be
pretty-printed with that indent level. If indent is None (the default),
the tree is dumped on a single line.
If show_empty is False, then empty lists and fields that are None
will be omitted from the output for better readability.
"""
from _colorize import get_theme
t = get_theme(force_color=color, force_no_color=not color).ast
def _format(node, level=0):
if indent is not None:
level += 1
@@ -154,13 +175,21 @@ def dump(
if not keywords:
args_buffer.append(repr(value))
continue
elif isinstance(value, Load):
field_type = cls._field_types.get(name, object)
if field_type is expr_context:
if not keywords:
args_buffer.append(
f'{t.node}{type(value).__name__}'
f'{t.reset}()')
continue
if not keywords:
args.extend(args_buffer)
args_buffer = []
value, simple = _format(value, level)
allsimple = allsimple and simple
if keywords:
args.append('%s=%s' % (name, value))
args.append(f'{t.field}{name}{t.reset}={value}')
else:
args.append(value)
if include_attributes and node._attributes:
@@ -173,14 +202,21 @@ def dump(
continue
value, simple = _format(value, level)
allsimple = allsimple and simple
args.append('%s=%s' % (name, value))
args.append(f'{t.attribute}{name}{t.reset}={value}')
cls_name = f'{t.node}{cls.__name__}{t.reset}'
if allsimple and len(args) <= 3:
return '%s(%s)' % (node.__class__.__name__, ', '.join(args)), not args
return '%s(%s%s)' % (node.__class__.__name__, prefix, sep.join(args)), False
return f'{cls_name}({", ".join(args)})', not args
return f'{cls_name}({prefix}{sep.join(args)})', False
elif isinstance(node, list):
if not node:
return '[]', True
return '[%s%s]' % (prefix, sep.join(_format(x, level)[0] for x in node)), False
if isinstance(node, bool) or node is None or node is Ellipsis:
return f'{t.keyword}{node!r}{t.reset}', True
if isinstance(node, (int, float, complex)):
return f'{t.number}{node!r}{t.reset}', True
if isinstance(node, (str, bytes)):
return f'{t.string}{node!r}{t.reset}', True
return repr(node), True
if not isinstance(node, AST):
@@ -628,9 +664,9 @@ def main(args=None):
import argparse
import sys
parser = argparse.ArgumentParser(color=True)
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('infile', nargs='?', default='-',
help='the file to parse; defaults to stdin')
help='the file to parse; defaults to `stdin`')
parser.add_argument('-m', '--mode', default='exec',
choices=('exec', 'single', 'eval', 'func_type'),
help='specify what kind of code must be parsed')
@@ -643,11 +679,11 @@ def main(args=None):
help='indentation of nodes (number of spaces)')
parser.add_argument('--feature-version',
type=str, default=None, metavar='VERSION',
help='Python version in the format 3.x '
'(for example, 3.10)')
help='Python version in the format `3.x` '
'(for example, `3.10`)')
parser.add_argument('-O', '--optimize',
type=int, default=-1, metavar='LEVEL',
help='optimization level for parser (default -1)')
help='optimization level for parser')
parser.add_argument('--show-empty', default=False, action='store_true',
help='show empty lists and fields in dump output')
args = parser.parse_args(args)
@@ -673,7 +709,9 @@ def main(args=None):
tree = parse(source, name, args.mode, type_comments=args.no_type_comments,
feature_version=feature_version, optimize=args.optimize)
from _colorize import can_colorize
print(dump(tree, include_attributes=args.include_attributes,
color=can_colorize(file=sys.stdout),
indent=args.indent, show_empty=args.show_empty))
if __name__ == '__main__':
+1
View File
@@ -14,6 +14,7 @@ BaseException
├── EOFError
├── ExceptionGroup [BaseExceptionGroup]
├── ImportError
│ └── ImportCycleError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
+129 -1
View File
@@ -654,6 +654,13 @@ class TestGetAnnotations(unittest.TestCase):
result = get_annotations(f, eval_str=True)
self.assertEqual(result, {'x': int, 'return': str})
def test_eval_str_wrapped_partial_cycle_self(self):
def f(x: 'int') -> 'str': ...
f.__wrapped__ = functools.partial(f, 0)
# Cycle is detected and broken; globals from f itself are used.
result = get_annotations(f, eval_str=True)
self.assertEqual(result, {'x': int, 'return': str})
def test_eval_str_wrapped_cycle_mutual(self):
# gh-146556: mutual __wrapped__ cycle (a -> b -> a) must not hang.
def a(x: 'int'): ...
@@ -1862,7 +1869,7 @@ class TestTypeRepr(unittest.TestCase):
self.assertEqual(type_repr(t'''{ 0
& 1
| 2
}'''), 't"""{ 0\n & 1\n | 2}"""')
}'''), 't"""{ 0\n & 1\n | 2\n }"""')
self.assertEqual(
type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'"
)
@@ -1874,6 +1881,10 @@ class TestTypeRepr(unittest.TestCase):
type_repr(Template("hi", Interpolation(42, " "))),
"Template('hi', Interpolation(42, ' ', None, ''))",
)
self.assertEqual(
type_repr(Template("hi", Interpolation(42, "4!2"))),
"Template('hi', Interpolation(42, '4!2', None, ''))",
)
# gh138558: perhaps in the future, we can improve this behavior:
self.assertEqual(type_repr(Template(Interpolation(42, "99"))), "t'{99}'")
@@ -2133,8 +2144,13 @@ class TestForwardRefClass(unittest.TestCase):
self.assertIsNone(fr.__resolved_str_cache__)
self.assertEqual(fr.evaluate(format=Format.STRING), "unknown | str | int | list[str] | tuple[int, ...]")
# Test that the cache is now set correctly
self.assertEqual(fr.__resolved_str_cache__, "unknown | str | int | list[str] | tuple[int, ...]")
# Test that future evaluations return the same cache
self.assertIs(fr.evaluate(format=Format.STRING), fr.__resolved_str_cache__)
def test_evaluate_forwardref_format(self):
fr = ForwardRef("undef")
evaluated = fr.evaluate(format=Format.FORWARDREF)
@@ -2163,6 +2179,118 @@ class TestForwardRefClass(unittest.TestCase):
support.EqualToForwardRef('"a" + 1'),
)
def test_evaluate_lazy_import(self):
ns = {}
exec(
textwrap.dedent(
"""
lazy from test.test_lazy_import.data.basic2 import x
lazy from test.test_lazy_import.data.broken_module import y
class A:
a: x
class B:
b: y
"""
),
ns,
)
self.addCleanup(
import_helper.unload, "test.test_lazy_import.data.basic2"
)
self.addCleanup(
import_helper.unload, "test.test_lazy_import.data.broken_module"
)
self.assertIs(type(ns["x"]), types.LazyImportType)
self.assertIs(type(ns["y"]), types.LazyImportType)
# The lazy import resolves successfully:
for format in (Format.VALUE, Format.FORWARDREF):
with self.subTest(format=format):
self.assertEqual(
ForwardRef("x").evaluate(globals=ns, format=format), 42
)
self.assertEqual(
ForwardRef("x").evaluate(locals=ns, format=format), 42
)
self.assertEqual(
get_annotations(ns["A"], format=Format.FORWARDREF), {"a": 42}
)
# The lazy import fails to resolve:
fr = ForwardRef("y")
with self.assertRaisesRegex(ValueError, "always fails to import"):
fr.evaluate(globals=ns, format=Format.VALUE)
with self.assertRaisesRegex(ValueError, "always fails to import"):
fr.evaluate(locals=ns, format=Format.VALUE)
self.assertIs(fr.evaluate(globals=ns, format=Format.FORWARDREF), fr)
self.assertIs(fr.evaluate(locals=ns, format=Format.FORWARDREF), fr)
annos = get_annotations(ns["B"], format=Format.FORWARDREF)
self.assertEqual(
annos,
{"b": support.EqualToForwardRef("y", is_class=True, owner=ns["B"])},
)
with self.assertRaisesRegex(ValueError, "always fails to import"):
annos["b"].evaluate(format=Format.VALUE)
with self.assertRaisesRegex(ValueError, "always fails to import"):
get_annotations(ns["B"], format=Format.VALUE)
def test_get_annotations_lazy_import(self):
ns = {}
exec(
textwrap.dedent(
"""
lazy from test.test_lazy_import.data.basic2 import x
lazy from test.test_lazy_import.data.broken_module import y
class A:
a: object.fail
b: x
class B:
a: object.fail
b: y
"""
),
ns,
)
self.addCleanup(
import_helper.unload, "test.test_lazy_import.data.basic2"
)
self.addCleanup(
import_helper.unload, "test.test_lazy_import.data.broken_module"
)
self.assertIs(type(ns["x"]), types.LazyImportType)
self.assertIs(type(ns["y"]), types.LazyImportType)
annos = get_annotations(ns["A"], format=Format.FORWARDREF)
self.assertEqual(
annos,
{
"a": support.EqualToForwardRef(
"object.fail", is_class=True, owner=ns["A"]
),
"b": 42,
},
)
annos = get_annotations(ns["B"], format=Format.FORWARDREF)
self.assertEqual(
annos,
{
"a": support.EqualToForwardRef(
"object.fail", is_class=True, owner=ns["B"]
),
"b": support.EqualToForwardRef(
"y", is_class=True, owner=ns["B"]
),
},
)
with self.assertRaisesRegex(ValueError, "always fails to import"):
annos["b"].evaluate(format=Format.VALUE)
def test_evaluate_notimplemented_format(self):
class C:
x: alias
+8 -4
View File
@@ -69,10 +69,14 @@ Module(body=[Try(body=[Pass()], handlers=[ExceptHandler(type=Name(...), name='ex
Module(body=[TryStar(body=[Pass()], handlers=[ExceptHandler(type=Name(...), name='exc', body=[Pass(...)])], orelse=[Pass()], finalbody=[Pass()])], type_ignores=[])
Module(body=[Assert(test=Name(id='v', ctx=Load(...)), msg=None)], type_ignores=[])
Module(body=[Assert(test=Name(id='v', ctx=Load(...)), msg=Constant(value='message', kind=None))], type_ignores=[])
Module(body=[Import(names=[alias(name='sys', asname=None)])], type_ignores=[])
Module(body=[Import(names=[alias(name='foo', asname='bar')])], type_ignores=[])
Module(body=[ImportFrom(module='sys', names=[alias(name='x', asname='y')], level=0)], type_ignores=[])
Module(body=[ImportFrom(module='sys', names=[alias(name='v', asname=None)], level=0)], type_ignores=[])
Module(body=[Import(names=[alias(name='sys', asname=None)], is_lazy=0)], type_ignores=[])
Module(body=[Import(names=[alias(name='foo', asname='bar')], is_lazy=0)], type_ignores=[])
Module(body=[ImportFrom(module='sys', names=[alias(name='x', asname='y')], level=0, is_lazy=0)], type_ignores=[])
Module(body=[ImportFrom(module='sys', names=[alias(name='v', asname=None)], level=0, is_lazy=0)], type_ignores=[])
Module(body=[Import(names=[alias(name='sys', asname=None)], is_lazy=1)], type_ignores=[])
Module(body=[Import(names=[alias(name='foo', asname='bar')], is_lazy=1)], type_ignores=[])
Module(body=[ImportFrom(module='sys', names=[alias(name='x', asname='y')], level=0, is_lazy=1)], type_ignores=[])
Module(body=[ImportFrom(module='sys', names=[alias(name='v', asname=None)], level=0, is_lazy=1)], type_ignores=[])
Module(body=[Global(names=['v'])], type_ignores=[])
Module(body=[Expr(value=Constant(value=1, kind=None))], type_ignores=[])
Module(body=[Pass()], type_ignores=[])
+14 -4
View File
@@ -118,6 +118,12 @@ exec_tests = [
# ImportFrom
"from sys import x as y",
"from sys import v",
# Lazy Import
"lazy import sys",
"lazy import foo as bar",
# Lazy ImportFrom
"lazy from sys import x as y",
"lazy from sys import v",
# Global
"global v",
# Expr
@@ -460,10 +466,14 @@ exec_results = [
('Module', [('TryStar', (1, 0, 7, 6), [('Pass', (2, 2, 2, 6))], [('ExceptHandler', (3, 0, 4, 6), ('Name', (3, 8, 3, 17), 'Exception', ('Load',)), 'exc', [('Pass', (4, 2, 4, 6))])], [('Pass', (5, 7, 5, 11))], [('Pass', (7, 2, 7, 6))])], []),
('Module', [('Assert', (1, 0, 1, 8), ('Name', (1, 7, 1, 8), 'v', ('Load',)), None)], []),
('Module', [('Assert', (1, 0, 1, 19), ('Name', (1, 7, 1, 8), 'v', ('Load',)), ('Constant', (1, 10, 1, 19), 'message', None))], []),
('Module', [('Import', (1, 0, 1, 10), [('alias', (1, 7, 1, 10), 'sys', None)])], []),
('Module', [('Import', (1, 0, 1, 17), [('alias', (1, 7, 1, 17), 'foo', 'bar')])], []),
('Module', [('ImportFrom', (1, 0, 1, 22), 'sys', [('alias', (1, 16, 1, 22), 'x', 'y')], 0)], []),
('Module', [('ImportFrom', (1, 0, 1, 17), 'sys', [('alias', (1, 16, 1, 17), 'v', None)], 0)], []),
('Module', [('Import', (1, 0, 1, 10), [('alias', (1, 7, 1, 10), 'sys', None)], 0)], []),
('Module', [('Import', (1, 0, 1, 17), [('alias', (1, 7, 1, 17), 'foo', 'bar')], 0)], []),
('Module', [('ImportFrom', (1, 0, 1, 22), 'sys', [('alias', (1, 16, 1, 22), 'x', 'y')], 0, 0)], []),
('Module', [('ImportFrom', (1, 0, 1, 17), 'sys', [('alias', (1, 16, 1, 17), 'v', None)], 0, 0)], []),
('Module', [('Import', (1, 0, 1, 15), [('alias', (1, 12, 1, 15), 'sys', None)], 1)], []),
('Module', [('Import', (1, 0, 1, 22), [('alias', (1, 12, 1, 22), 'foo', 'bar')], 1)], []),
('Module', [('ImportFrom', (1, 0, 1, 27), 'sys', [('alias', (1, 21, 1, 27), 'x', 'y')], 0, 1)], []),
('Module', [('ImportFrom', (1, 0, 1, 22), 'sys', [('alias', (1, 21, 1, 22), 'v', None)], 0, 1)], []),
('Module', [('Global', (1, 0, 1, 8), ['v'])], []),
('Module', [('Expr', (1, 0, 1, 1), ('Constant', (1, 0, 1, 1), 1, None))], []),
('Module', [('Pass', (1, 0, 1, 4))], []),
+179 -117
View File
@@ -1,4 +1,5 @@
import _ast_unparse
import _ast
import ast
import builtins
import contextlib
@@ -13,6 +14,7 @@ import tempfile
import textwrap
import types
import unittest
import warnings
import weakref
from io import StringIO
from pathlib import Path
@@ -86,7 +88,9 @@ class AST_Tests(unittest.TestCase):
self.assertEqual(ast_node._fields, ast_node.__match_args__)
def test_AST_objects(self):
x = ast.AST()
# Directly instantiating abstract node class AST is allowed (but deprecated)
with self.assertWarns(DeprecationWarning):
x = ast.AST()
self.assertEqual(x._fields, ())
x.foobar = 42
self.assertEqual(x.foobar, 42)
@@ -95,7 +99,7 @@ class AST_Tests(unittest.TestCase):
with self.assertRaises(AttributeError):
x.vararg
with self.assertRaises(TypeError):
with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning):
# "ast.AST constructor takes 0 positional arguments"
ast.AST(2)
@@ -111,15 +115,21 @@ class AST_Tests(unittest.TestCase):
msg = "type object 'ast.AST' has no attribute '_fields'"
# Both examples used to crash:
with self.assertRaisesRegex(AttributeError, msg):
with (
self.assertRaisesRegex(AttributeError, msg),
self.assertWarns(DeprecationWarning),
):
ast.AST(arg1=123)
with self.assertRaisesRegex(AttributeError, msg):
with (
self.assertRaisesRegex(AttributeError, msg),
self.assertWarns(DeprecationWarning),
):
ast.AST()
def test_AST_garbage_collection(self):
def test_node_garbage_collection(self):
class X:
pass
a = ast.AST()
a = ast.Module()
a.x = X()
a.x.a = a
ref = weakref.ref(a.x)
@@ -133,7 +143,7 @@ class AST_Tests(unittest.TestCase):
(eval_tests, eval_results, "eval")):
for i, o in zip(input, output):
with self.subTest(action="parsing", input=i):
ast_tree = compile(i, "?", kind, ast.PyCF_ONLY_AST)
ast_tree = compile(i, "?", kind, ast.PyCF_ONLY_AST, optimize=False)
self.assertEqual(to_tuple(ast_tree), o)
self._assertTrueorder(ast_tree, (0, 0))
with self.subTest(action="compiling", input=i, kind=kind):
@@ -143,7 +153,7 @@ class AST_Tests(unittest.TestCase):
# compile() is the only function that calls PyAST_Validate
snippets_to_validate = exec_tests + single_tests + eval_tests
for snippet in snippets_to_validate:
tree = ast.parse(snippet)
tree = ast.parse(snippet, optimize=False)
compile(tree, '<string>', 'exec')
def test_parse_invalid_ast(self):
@@ -152,6 +162,15 @@ class AST_Tests(unittest.TestCase):
self.assertRaises(TypeError, ast.parse, ast.Constant(42),
optimize=optval)
def test_parse_ast_func_type(self):
# see gh-156689
tree = ast.parse('(int, str) -> bool', mode='func_type')
self.assertEqual(ast.dump(ast.parse(tree, mode='func_type')),
ast.dump(tree))
self.assertRaises(TypeError, ast.parse, ast.Constant(42),
mode='func_type')
self.assertRaises(TypeError, ast.parse, tree, mode='exec')
def test_optimization_levels__debug__(self):
cases = [(-1, '__debug__'), (0, '__debug__'), (1, False), (2, False)]
for (optval, expected) in cases:
@@ -419,6 +438,17 @@ class AST_Tests(unittest.TestCase):
if isinstance(x, ast.AST):
self.assertIs(type(x._fields), tuple)
def test_dynamic_attr(self):
for name, item in ast.__dict__.items():
# constructor has a different signature
if name == 'Index':
continue
if self._is_ast_node(name, item):
x = self._construct_ast_class(item)
# Custom attribute assignment is allowed
x.foo = 5
self.assertEqual(x.foo, 5)
def _construct_ast_class(self, cls):
kwargs = {}
for name, typ in cls.__annotations__.items():
@@ -429,7 +459,15 @@ class AST_Tests(unittest.TestCase):
elif typ is object:
kwargs[name] = b'capybara'
elif isinstance(typ, type) and issubclass(typ, ast.AST):
kwargs[name] = self._construct_ast_class(typ)
if _ast._is_abstract(typ):
# Use an arbitrary concrete subclass
concrete = next((sub for sub in typ.__subclasses__()
if not _ast._is_abstract(sub)), None)
msg = f"abstract node class {typ} has no concrete subclasses"
self.assertIsNotNone(concrete, msg)
else:
concrete = typ
kwargs[name] = self._construct_ast_class(concrete)
return cls(**kwargs)
def test_arguments(self):
@@ -568,6 +606,10 @@ class AST_Tests(unittest.TestCase):
with self.assertRaisesRegex(TypeError, re.escape(msg)):
ast.BinOp(1, 2, 3, foobarbaz=42)
# Directly instantiating abstract node types is allowed (but deprecated)
self.assertWarns(DeprecationWarning, ast.stmt)
self.assertWarns(DeprecationWarning, ast.expr_context)
def test_no_fields(self):
# this used to fail because Sub._fields was None
x = ast.Sub()
@@ -575,7 +617,10 @@ class AST_Tests(unittest.TestCase):
def test_invalid_sum(self):
pos = dict(lineno=2, col_offset=3)
m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], [])
with self.assertWarns(DeprecationWarning):
# Creating instances of ast.expr is deprecated
e = ast.expr(**pos)
m = ast.Module([ast.Expr(e, **pos)], [])
with self.assertRaises(TypeError) as cm:
compile(m, "<test>", "exec")
self.assertIn("but got expr()", str(cm.exception))
@@ -942,6 +987,34 @@ class AST_Tests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, f"identifier field can't represent '{constant}' constant"):
compile(expr, "<test>", "eval")
def test_constant_in_identifier_fields(self):
# gh-85260: an identifier field holding a constant name used to
# crash the compiler
for statement in [
"def x(): pass",
"async def x(): pass",
"class x: pass",
"from a import x",
"from a import b as x",
"from a import b, c, d as x",
"import x",
"import a, b, x",
"try: pass\nexcept A as x: pass",
"try: pass\nexcept A as b: pass\nexcept B as x: pass\n",
]:
for constant in "True", "False", "None":
with self.subTest(statement=statement, constant=constant):
tree = ast.parse(statement)
for node in ast.walk(tree):
for field, value in ast.iter_fields(node):
if value == "x":
setattr(node, field, constant)
with self.assertRaisesRegex(
ValueError,
f"identifier field can't represent "
f"'{constant}' constant"):
compile(tree, "<test>", "exec")
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError not raised
def test_constant_as_unicode_name(self):
constants = [
@@ -985,8 +1058,7 @@ class AST_Tests(unittest.TestCase):
enum._test_simple_enum(_Precedence, _ast_unparse._Precedence)
@support.cpython_only
@support.run_with_limited_c_stack(
100_000 if sys.platform == "android" else 500_000)
@support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 500_000)
@skip_wasi_stack_overflow()
@skip_emscripten_stack_overflow()
def test_ast_recursion_limit(self):
@@ -1048,7 +1120,7 @@ class AST_Tests(unittest.TestCase):
snapshots = AST_REPR_DATA_FILE.read_text().split("\n")
for test, snapshot in zip(ast_repr_get_test_cases(), snapshots, strict=True):
with self.subTest(test_input=test):
self.assertEqual(repr(ast.parse(test)), snapshot)
self.assertEqual(repr(ast.parse(test, optimize=False)), snapshot)
def test_repr_large_input_crash(self):
# gh-125010: Fix use-after-free in ast repr()
@@ -1069,13 +1141,28 @@ class AST_Tests(unittest.TestCase):
self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
self.assertIsInstance(tree.body[0].value.values[1], ast.Interpolation)
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_optimization_levels_const_folding(self):
return super().test_optimization_levels_const_folding()
def test_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
with open(filename, 'rb') as f:
source = f.read()
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=r'<unknown>\z')
ast.parse(source)
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10])
for wm in wlog:
self.assertEqual(wm.filename, '<unknown>')
self.assertIs(wm.category, SyntaxWarning)
@unittest.expectedFailure # TODO: RUSTPYTHON; will be removed in Python 3.14
def test_constant_subclasses_deprecated(self):
return super().test_constant_subclasses_deprecated()
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=r'package\.module\z')
warnings.filterwarnings('error', module=r'<unknown>')
ast.parse(source, filename, module='package.module')
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10])
for wm in wlog:
self.assertEqual(wm.filename, filename)
self.assertIs(wm.category, SyntaxWarning)
class CopyTests(unittest.TestCase):
@@ -1085,14 +1172,19 @@ class CopyTests(unittest.TestCase):
def iter_ast_classes():
"""Iterate over the (native) subclasses of ast.AST recursively.
This excludes the special class ast.Index since its constructor
returns an integer.
This excludes:
* abstract AST nodes
* the special class ast.Index, since its constructor returns
an integer.
"""
def do(cls):
if cls.__module__ != 'ast':
return
if cls is ast.Index:
return
# Don't attempt to create instances of abstract AST nodes
if _ast._is_abstract(cls):
return
yield cls
for sub in cls.__subclasses__():
@@ -1359,14 +1451,14 @@ class CopyTests(unittest.TestCase):
self.assertRaises(AttributeError, getattr, repl, 'extra')
def test_replace_reject_missing_field(self):
# case: warn if deleted field is not replaced
# case: raise if deleted field is not replaced
node = ast.parse('x').body[0].value
context = node.ctx
del node.id
self.assertRaises(AttributeError, getattr, node, 'id')
self.assertIs(node.ctx, context)
msg = "Name.__replace__ missing 1 keyword argument: 'id'."
msg = "ast.Name.__init__ missing 1 required positional argument: 'id'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
copy.replace(node)
# assert that there is no side-effect
@@ -1403,7 +1495,7 @@ class CopyTests(unittest.TestCase):
# explicit rejection of known instance fields
self.assertHasAttr(node, 'extra')
msg = "Name.__replace__ got an unexpected keyword argument 'extra'."
msg = "ast.Name.__init__ got an unexpected keyword argument 'extra'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
copy.replace(node, extra=1)
# assert that there is no side-effect
@@ -1417,7 +1509,7 @@ class CopyTests(unittest.TestCase):
# explicit rejection of unknown extra fields
self.assertRaises(AttributeError, getattr, node, 'unknown')
msg = "Name.__replace__ got an unexpected keyword argument 'unknown'."
msg = "ast.Name.__init__ got an unexpected keyword argument 'unknown'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
copy.replace(node, unknown=1)
# assert that there is no side-effect
@@ -1452,17 +1544,17 @@ class ASTHelpers_Test(unittest.TestCase):
def test_dump(self):
node = ast.parse('spam(eggs, "and cheese")')
self.assertEqual(ast.dump(node),
"Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), "
"args=[Name(id='eggs', ctx=Load()), Constant(value='and cheese')]))])"
"Module(body=[Expr(value=Call(func=Name(id='spam'), "
"args=[Name(id='eggs'), Constant(value='and cheese')]))])"
)
self.assertEqual(ast.dump(node, annotate_fields=False),
"Module([Expr(Call(Name('spam', Load()), [Name('eggs', Load()), "
"Module([Expr(Call(Name('spam'), [Name('eggs'), "
"Constant('and cheese')]))])"
)
self.assertEqual(ast.dump(node, include_attributes=True),
"Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load(), "
"Module(body=[Expr(value=Call(func=Name(id='spam', "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=4), "
"args=[Name(id='eggs', ctx=Load(), lineno=1, col_offset=5, "
"args=[Name(id='eggs', lineno=1, col_offset=5, "
"end_lineno=1, end_col_offset=9), Constant(value='and cheese', "
"lineno=1, col_offset=11, end_lineno=1, end_col_offset=23)], "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=24), "
@@ -1476,18 +1568,18 @@ Module(
body=[
Expr(
value=Call(
func=Name(id='spam', ctx=Load()),
func=Name(id='spam'),
args=[
Name(id='eggs', ctx=Load()),
Name(id='eggs'),
Constant(value='and cheese')]))])""")
self.assertEqual(ast.dump(node, annotate_fields=False, indent='\t'), """\
Module(
\t[
\t\tExpr(
\t\t\tCall(
\t\t\t\tName('spam', Load()),
\t\t\t\tName('spam'),
\t\t\t\t[
\t\t\t\t\tName('eggs', Load()),
\t\t\t\t\tName('eggs'),
\t\t\t\t\tConstant('and cheese')]))])""")
self.assertEqual(ast.dump(node, include_attributes=True, indent=3), """\
Module(
@@ -1496,7 +1588,6 @@ Module(
value=Call(
func=Name(
id='spam',
ctx=Load(),
lineno=1,
col_offset=0,
end_lineno=1,
@@ -1504,7 +1595,6 @@ Module(
args=[
Name(
id='eggs',
ctx=Load(),
lineno=1,
col_offset=5,
end_lineno=1,
@@ -1534,23 +1624,23 @@ Module(
)
node = ast.Raise(exc=ast.Name(id='e', ctx=ast.Load()), lineno=3, col_offset=4)
self.assertEqual(ast.dump(node),
"Raise(exc=Name(id='e', ctx=Load()))"
"Raise(exc=Name(id='e'))"
)
self.assertEqual(ast.dump(node, annotate_fields=False),
"Raise(Name('e', Load()))"
"Raise(Name('e'))"
)
self.assertEqual(ast.dump(node, include_attributes=True),
"Raise(exc=Name(id='e', ctx=Load()), lineno=3, col_offset=4)"
"Raise(exc=Name(id='e'), lineno=3, col_offset=4)"
)
self.assertEqual(ast.dump(node, annotate_fields=False, include_attributes=True),
"Raise(Name('e', Load()), lineno=3, col_offset=4)"
"Raise(Name('e'), lineno=3, col_offset=4)"
)
node = ast.Raise(cause=ast.Name(id='e', ctx=ast.Load()))
self.assertEqual(ast.dump(node),
"Raise(cause=Name(id='e', ctx=Load()))"
"Raise(cause=Name(id='e'))"
)
self.assertEqual(ast.dump(node, annotate_fields=False),
"Raise(cause=Name('e', Load()))"
"Raise(cause=Name('e'))"
)
# Arguments:
node = ast.arguments(args=[ast.arg("x")])
@@ -1582,10 +1672,10 @@ Module(
[ast.Name('dataclass', ctx=ast.Load())],
)
self.assertEqual(ast.dump(node),
"ClassDef(name='T', keywords=[keyword(arg='a', value=Constant(value=None))], decorator_list=[Name(id='dataclass', ctx=Load())])",
"ClassDef(name='T', keywords=[keyword(arg='a', value=Constant(value=None))], decorator_list=[Name(id='dataclass')])",
)
self.assertEqual(ast.dump(node, annotate_fields=False),
"ClassDef('T', [], [keyword('a', Constant(None))], [], [Name('dataclass', Load())])",
"ClassDef('T', [], [keyword('a', Constant(None))], [], [Name('dataclass')])",
)
def test_dump_show_empty(self):
@@ -1613,7 +1703,7 @@ Module(
check_node(
# Corner case: there are no real `Name` instances with `id=''`:
ast.Name(id='', ctx=ast.Load()),
empty="Name(id='', ctx=Load())",
empty="Name(id='')",
full="Name(id='', ctx=Load())",
)
@@ -1661,35 +1751,45 @@ Module(
check_text(
"def a(b: int = 0, *, c): ...",
empty="Module(body=[FunctionDef(name='a', args=arguments(args=[arg(arg='b', annotation=Name(id='int', ctx=Load()))], kwonlyargs=[arg(arg='c')], kw_defaults=[None], defaults=[Constant(value=0)]), body=[Expr(value=Constant(value=Ellipsis))])])",
empty="Module(body=[FunctionDef(name='a', args=arguments(args=[arg(arg='b', annotation=Name(id='int'))], kwonlyargs=[arg(arg='c')], kw_defaults=[None], defaults=[Constant(value=0)]), body=[Expr(value=Constant(value=Ellipsis))])])",
full="Module(body=[FunctionDef(name='a', args=arguments(posonlyargs=[], args=[arg(arg='b', annotation=Name(id='int', ctx=Load()))], kwonlyargs=[arg(arg='c')], kw_defaults=[None], defaults=[Constant(value=0)]), body=[Expr(value=Constant(value=Ellipsis))], decorator_list=[], type_params=[])], type_ignores=[])",
)
check_text(
"def a(b: int = 0, *, c): ...",
empty="Module(body=[FunctionDef(name='a', args=arguments(args=[arg(arg='b', annotation=Name(id='int', ctx=Load(), lineno=1, col_offset=9, end_lineno=1, end_col_offset=12), lineno=1, col_offset=6, end_lineno=1, end_col_offset=12)], kwonlyargs=[arg(arg='c', lineno=1, col_offset=21, end_lineno=1, end_col_offset=22)], kw_defaults=[None], defaults=[Constant(value=0, lineno=1, col_offset=15, end_lineno=1, end_col_offset=16)]), body=[Expr(value=Constant(value=Ellipsis, lineno=1, col_offset=25, end_lineno=1, end_col_offset=28), lineno=1, col_offset=25, end_lineno=1, end_col_offset=28)], lineno=1, col_offset=0, end_lineno=1, end_col_offset=28)])",
empty="Module(body=[FunctionDef(name='a', args=arguments(args=[arg(arg='b', annotation=Name(id='int', lineno=1, col_offset=9, end_lineno=1, end_col_offset=12), lineno=1, col_offset=6, end_lineno=1, end_col_offset=12)], kwonlyargs=[arg(arg='c', lineno=1, col_offset=21, end_lineno=1, end_col_offset=22)], kw_defaults=[None], defaults=[Constant(value=0, lineno=1, col_offset=15, end_lineno=1, end_col_offset=16)]), body=[Expr(value=Constant(value=Ellipsis, lineno=1, col_offset=25, end_lineno=1, end_col_offset=28), lineno=1, col_offset=25, end_lineno=1, end_col_offset=28)], lineno=1, col_offset=0, end_lineno=1, end_col_offset=28)])",
full="Module(body=[FunctionDef(name='a', args=arguments(posonlyargs=[], args=[arg(arg='b', annotation=Name(id='int', ctx=Load(), lineno=1, col_offset=9, end_lineno=1, end_col_offset=12), lineno=1, col_offset=6, end_lineno=1, end_col_offset=12)], kwonlyargs=[arg(arg='c', lineno=1, col_offset=21, end_lineno=1, end_col_offset=22)], kw_defaults=[None], defaults=[Constant(value=0, lineno=1, col_offset=15, end_lineno=1, end_col_offset=16)]), body=[Expr(value=Constant(value=Ellipsis, lineno=1, col_offset=25, end_lineno=1, end_col_offset=28), lineno=1, col_offset=25, end_lineno=1, end_col_offset=28)], decorator_list=[], type_params=[], lineno=1, col_offset=0, end_lineno=1, end_col_offset=28)], type_ignores=[])",
include_attributes=True,
)
check_text(
'spam(eggs, "and cheese")',
empty="Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), args=[Name(id='eggs', ctx=Load()), Constant(value='and cheese')]))])",
empty="Module(body=[Expr(value=Call(func=Name(id='spam'), args=[Name(id='eggs'), Constant(value='and cheese')]))])",
full="Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), args=[Name(id='eggs', ctx=Load()), Constant(value='and cheese')], keywords=[]))], type_ignores=[])",
)
check_text(
'spam(eggs, text="and cheese")',
empty="Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), args=[Name(id='eggs', ctx=Load())], keywords=[keyword(arg='text', value=Constant(value='and cheese'))]))])",
empty="Module(body=[Expr(value=Call(func=Name(id='spam'), args=[Name(id='eggs')], keywords=[keyword(arg='text', value=Constant(value='and cheese'))]))])",
full="Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), args=[Name(id='eggs', ctx=Load())], keywords=[keyword(arg='text', value=Constant(value='and cheese'))]))], type_ignores=[])",
)
check_text(
"import _ast as ast; from module import sub",
empty="Module(body=[Import(names=[alias(name='_ast', asname='ast')]), ImportFrom(module='module', names=[alias(name='sub')], level=0)])",
full="Module(body=[Import(names=[alias(name='_ast', asname='ast')]), ImportFrom(module='module', names=[alias(name='sub')], level=0)], type_ignores=[])",
empty="Module(body=[Import(names=[alias(name='_ast', asname='ast')], is_lazy=0), ImportFrom(module='module', names=[alias(name='sub')], level=0, is_lazy=0)])",
full="Module(body=[Import(names=[alias(name='_ast', asname='ast')], is_lazy=0), ImportFrom(module='module', names=[alias(name='sub')], level=0, is_lazy=0)], type_ignores=[])",
)
def test_dump_with_color(self):
node = ast.parse("x = 1")
self.assertNotIn("\x1b[", ast.dump(node))
self.assertNotIn("\x1b[", ast.dump(node, color=False))
self.assertIn("\x1b[", ast.dump(node, color=True))
node = ast.Constant(value="\x1b[31m")
self.assertEqual(ast.dump(node), "Constant(value='\\x1b[31m')")
self.assertIn("'\\x1b[31m'", ast.dump(node, color=True))
def test_copy_location(self):
src = ast.parse('1 + 1', mode='eval')
src.body.right = ast.copy_location(ast.Constant(2), src.body.right)
@@ -1714,12 +1814,12 @@ Module(
self.assertEqual(src, ast.fix_missing_locations(src))
self.maxDiff = None
self.assertEqual(ast.dump(src, include_attributes=True),
"Module(body=[Expr(value=Call(func=Name(id='write', ctx=Load(), "
"Module(body=[Expr(value=Call(func=Name(id='write', "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=5), "
"args=[Constant(value='spam', lineno=1, col_offset=6, end_lineno=1, "
"end_col_offset=12)], lineno=1, col_offset=0, end_lineno=1, "
"end_col_offset=13), lineno=1, col_offset=0, end_lineno=1, "
"end_col_offset=13), Expr(value=Call(func=Name(id='spam', ctx=Load(), "
"end_col_offset=13), Expr(value=Call(func=Name(id='spam', "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=0), "
"args=[Constant(value='eggs', lineno=1, col_offset=0, end_lineno=1, "
"end_col_offset=0)], lineno=1, col_offset=0, end_lineno=1, "
@@ -1780,22 +1880,22 @@ Module(
)
def test_get_docstring(self):
node = ast.parse('"""line one\n line two"""')
node = ast.parse('"""line one\n line two"""', optimize=False)
self.assertEqual(ast.get_docstring(node),
'line one\nline two')
node = ast.parse('class foo:\n """line one\n line two"""')
node = ast.parse('class foo:\n """line one\n line two"""', optimize=False)
self.assertEqual(ast.get_docstring(node.body[0]),
'line one\nline two')
node = ast.parse('def foo():\n """line one\n line two"""')
node = ast.parse('def foo():\n """line one\n line two"""', optimize=False)
self.assertEqual(ast.get_docstring(node.body[0]),
'line one\nline two')
node = ast.parse('async def foo():\n """spam\n ham"""')
node = ast.parse('async def foo():\n """spam\n ham"""', optimize=False)
self.assertEqual(ast.get_docstring(node.body[0]), 'spam\nham')
node = ast.parse('async def foo():\n """spam\n ham"""')
node = ast.parse('async def foo():\n """spam\n ham"""', optimize=False)
self.assertEqual(ast.get_docstring(node.body[0], clean=False), 'spam\n ham')
node = ast.parse('x')
@@ -1834,7 +1934,8 @@ Module(
'def foo():\n """line one\n line two"""\n\n'
' def bar():\n """line one\n line two"""\n'
' """line one\n line two"""\n'
'"""line one\nline two"""\n\n'
'"""line one\nline two"""\n\n',
optimize=False
)
self.assertEqual(node.body[0].col_offset, 0)
self.assertEqual(node.body[0].lineno, 1)
@@ -2407,9 +2508,9 @@ class ASTValidatorTests(unittest.TestCase):
fn = os.path.join(STDLIB, module)
with open(fn, "r", encoding="utf-8") as fp:
source = fp.read()
mod = ast.parse(source, fn)
mod = ast.parse(source, fn, optimize=False)
compile(mod, fn, "exec")
mod2 = ast.parse(source, fn)
mod2 = ast.parse(source, fn, optimize=False)
self.assertTrue(ast.compare(mod, mod2))
constant_1 = ast.Constant(1)
@@ -2623,16 +2724,17 @@ class ConstantTests(unittest.TestCase):
"to in Store context")
def test_get_docstring(self):
tree = ast.parse("'docstring'\nx = 1")
tree = ast.parse("'docstring'\nx = 1", optimize=False)
self.assertEqual(ast.get_docstring(tree), 'docstring')
def get_load_const(self, tree):
# Compile to bytecode, disassemble and get parameter of LOAD_CONST
# instructions
# and LOAD_COMMON_CONSTANT instructions
co = compile(tree, '<string>', 'exec')
consts = []
for instr in dis.get_instructions(co):
if instr.opcode in dis.hasconst:
if instr.opcode in dis.hasconst or \
instr.opname == 'LOAD_COMMON_CONSTANT':
consts.append(instr.argval)
return consts
@@ -3055,8 +3157,8 @@ class EndPositionTests(unittest.TestCase):
class NodeTransformerTests(ASTTestMixin, unittest.TestCase):
def assertASTTransformation(self, transformer_class,
initial_code, expected_code):
initial_ast = ast.parse(dedent(initial_code))
code, expected_code):
initial_ast = ast.parse(dedent(code))
expected_ast = ast.parse(dedent(expected_code))
transformer = transformer_class()
@@ -3392,6 +3494,7 @@ class ModuleStateTests(unittest.TestCase):
self.assertEqual(res, 0)
@support.force_not_colorized_test_class
class CommandLineTests(unittest.TestCase):
def setUp(self):
self.filename = tempfile.mktemp()
@@ -3464,7 +3567,7 @@ class CommandLineTests(unittest.TestCase):
body=[
AnnAssign(
target=Name(id='x', ctx=Store()),
annotation=Name(id='bool', ctx=Load()),
annotation=Name(id='bool'),
value=Constant(value=1),
simple=1)],
type_ignores=[
@@ -3492,7 +3595,7 @@ class CommandLineTests(unittest.TestCase):
expect = '''
Expression(
body=Call(
func=Name(id='print', ctx=Load()),
func=Name(id='print'),
args=[
Constant(value=1),
Constant(value=2),
@@ -3508,12 +3611,11 @@ class CommandLineTests(unittest.TestCase):
expect = '''
FunctionType(
argtypes=[
Name(id='int', ctx=Load()),
Name(id='str', ctx=Load())],
Name(id='int'),
Name(id='str')],
returns=Subscript(
value=Name(id='list', ctx=Load()),
slice=Name(id='int', ctx=Load()),
ctx=Load()))
value=Name(id='list'),
slice=Name(id='int')))
'''
for flag in ('-m=func_type', '--mode=func_type'):
with self.subTest(flag=flag):
@@ -3527,7 +3629,7 @@ class CommandLineTests(unittest.TestCase):
body=[
AnnAssign(
target=Name(id='x', ctx=Store()),
annotation=Name(id='bool', ctx=Load()),
annotation=Name(id='bool'),
value=Constant(value=1),
simple=1)])
'''
@@ -3572,7 +3674,7 @@ class CommandLineTests(unittest.TestCase):
Module(
body=[
Match(
subject=Name(id='x', ctx=Load()),
subject=Name(id='x'),
cases=[
match_case(
pattern=MatchValue(
@@ -3595,7 +3697,7 @@ class CommandLineTests(unittest.TestCase):
Module(
body=[
Match(
subject=Name(id='a', ctx=Load()),
subject=Name(id='a'),
cases=[
match_case(
pattern=MatchValue(
@@ -3621,7 +3723,7 @@ class CommandLineTests(unittest.TestCase):
Module(
body=[
Match(
subject=Name(id='a', ctx=Load()),
subject=Name(id='a'),
cases=[
match_case(
pattern=MatchValue(
@@ -3776,46 +3878,6 @@ class ASTOptimizationTests(unittest.TestCase):
for optval in (0, 1, 2):
self.assertIn(folded if optval else unfolded, ast.dump(ast.parse(src, optimize=optval)))
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_binop(self):
return super().test_folding_binop()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_comparator(self):
return super().test_folding_comparator()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_iter(self):
return super().test_folding_iter()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_not(self):
return super().test_folding_not()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_subscript(self):
return super().test_folding_subscript()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_tuple(self):
return super().test_folding_tuple()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_type_param_in_class_def(self):
return super().test_folding_type_param_in_class_def()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_type_param_in_function_def(self):
return super().test_folding_type_param_in_function_def()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_type_param_in_type_alias(self):
return super().test_folding_type_param_in_type_alias()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_unaryop(self):
return super().test_folding_unaryop()
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '--snapshot-update':
File diff suppressed because it is too large. Load diff
+3
View File
@@ -0,0 +1,3 @@
import unittest
unittest.main('test.test_lazy_import')
@@ -0,0 +1,3 @@
# SyntaxError: lazy import inside class body is not allowed
class Foo:
lazy import json
@@ -0,0 +1 @@
lazy from __future__ import annotations
@@ -0,0 +1,2 @@
def f():
lazy import foo
@@ -0,0 +1,4 @@
try:
lazy import foo
except:
pass
@@ -0,0 +1,4 @@
try:
lazy from foo import bar
except:
pass
@@ -0,0 +1 @@
lazy from foo import *
+4
View File
@@ -0,0 +1,4 @@
def f():
pass
x = 42
@@ -0,0 +1,2 @@
__lazy_modules__ = ['test.test_lazy_import.data.basic2']
import test.test_lazy_import.data.basic2
@@ -0,0 +1,2 @@
__lazy_modules__ = ['test.test_lazy_import.data.basic2']
lazy from .basic2 import f
@@ -0,0 +1,3 @@
__lazy_modules__ = ['test.test_lazy_import.data.basic2']
import test.test_lazy_import.data.basic2
test.test_lazy_import.data.basic2.f()
+2
View File
@@ -0,0 +1,2 @@
lazy import test.test_lazy_import.data.basic2
x = dir()
+1
View File
@@ -0,0 +1 @@
lazy from test.test_lazy_import.data import basic2
+1
View File
@@ -0,0 +1 @@
lazy import test.test_lazy_import.data.basic2
+3
View File
@@ -0,0 +1,3 @@
lazy import test.test_lazy_import.data.basic2 as basic2
basic2.f()
+3
View File
@@ -0,0 +1,3 @@
# Module that exists but doesn't have expected attributes
x = 42
# No 'nonexistent_attr' here
+2
View File
@@ -0,0 +1,2 @@
# Module that raises an error during import
raise ValueError("This module always fails to import")
@@ -0,0 +1,3 @@
# Module that raises an exception with explicit cause during import
cause = ValueError("Cause of failure")
raise ValueError("This module always fails to import") from cause
@@ -0,0 +1,5 @@
# Module that raises an exception with context during import
try:
raise ValueError("Cause of failure")
except:
raise ValueError("This module always fails to import")
@@ -0,0 +1,2 @@
# Module that raises an exception with suppressed context during import
raise ValueError("This module always fails to import") from None
Whitespace-only changes.
@@ -0,0 +1,2 @@
from .x import X2
X2()
@@ -0,0 +1,7 @@
def X1():
return "X"
from .y import Y1
def X2():
return Y1()
@@ -0,0 +1,7 @@
def Y1():
return "Y"
from .x import X2
def Y2():
return X2()
@@ -0,0 +1,5 @@
__lazy_modules__ = ['test.test_lazy_import.data.basic2']
def f():
import test.test_lazy_import.data.basic2
f()
@@ -0,0 +1,5 @@
__lazy_modules__ = ['test.test_lazy_import.data.basic2']
try:
import test.test_lazy_import.data.basic2
except:
pass
+1
View File
@@ -0,0 +1 @@
basic = __lazy_import__('test.test_lazy_import.data.basic2')
@@ -0,0 +1,14 @@
import sys
def myimport(*args):
return sys.modules[__name__]
new_globals = dict(globals())
new_globals["__builtins__"] = {
"__import__": myimport,
}
basic2 = 42
basic = __lazy_import__("test.test_lazy_import.data.basic2",
globals=new_globals)
basic
@@ -0,0 +1,3 @@
basic = __lazy_import__('test.test_lazy_import.data',
fromlist=("basic2", ))
basic
+3
View File
@@ -0,0 +1,3 @@
def f():
import test.test_lazy_import.data.basic2 as basic2
return basic2
+10
View File
@@ -0,0 +1,10 @@
import sys
def filter(module_name, imported_name, from_list):
assert module_name == __name__
assert imported_name == "test.test_lazy_import.data.basic2"
return False
sys.set_lazy_imports_filter(filter)
lazy import test.test_lazy_import.data.basic2 as basic2
+11
View File
@@ -0,0 +1,11 @@
import sys
def filter(module_name, imported_name, from_list):
assert module_name == __name__
assert imported_name == "test.test_lazy_import.data.basic2"
assert from_list == ('f',)
return False
sys.set_lazy_imports_filter(filter)
lazy from test.test_lazy_import.data.basic2 import f
@@ -0,0 +1,12 @@
import sys
def filter(module_name, imported_name, from_list):
assert module_name == __name__
assert imported_name == "test.test_lazy_import.data.basic2"
assert from_list == ('f',)
return True
sys.set_lazy_imports("normal")
sys.set_lazy_imports_filter(filter)
lazy from test.test_lazy_import.data.basic2 import f
+11
View File
@@ -0,0 +1,11 @@
import sys
def filter(module_name, imported_name, from_list):
assert module_name == __name__
assert imported_name == "test.test_lazy_import.data.basic2"
return True
sys.set_lazy_imports("normal")
sys.set_lazy_imports_filter(filter)
lazy import test.test_lazy_import.data.basic2 as basic2
+5
View File
@@ -0,0 +1,5 @@
import sys
sys.set_lazy_imports("all")
import test.test_lazy_import.data.basic2 as basic2
+9
View File
@@ -0,0 +1,9 @@
# Test that globals() returns lazy proxy objects without reifying
lazy import test.test_lazy_import.data.basic2 as basic2
def get_from_globals():
g = globals()
return g['basic2']
def get_direct():
return basic2
+4
View File
@@ -0,0 +1,4 @@
# Imported by test_initializing_module_is_still_tracked. The module it imports
# lazily imports this one back while this one is still initializing.
import test.test_lazy_import.data.lazy_on_init_fails
raise ValueError("initialization failed")
+6
View File
@@ -0,0 +1,6 @@
# Test __lazy_modules__ with from imports
__lazy_modules__ = ['test.test_lazy_import.data.basic2']
from test.test_lazy_import.data.basic2 import x, f
def get_x():
return x
+7
View File
@@ -0,0 +1,7 @@
lazy import test.test_lazy_import.data.basic2 as basic2
def f():
x = globals()
return x['basic2'].resolve()
f()
+2
View File
@@ -0,0 +1,2 @@
lazy import test.test_lazy_import.data.pkg.bar
x = test.test_lazy_import.data.pkg.bar.f
+1
View File
@@ -0,0 +1 @@
lazy import test.test_lazy_import.data.init_fails
+3
View File
@@ -0,0 +1,3 @@
import contextlib
with contextlib.nullcontext():
lazy import test.test_lazy_import.data.basic2
+3
View File
@@ -0,0 +1,3 @@
import contextlib
with contextlib.nullcontext():
lazy import test.test_lazy_import.data.basic2 as basic2
+1
View File
@@ -0,0 +1 @@
lazy from . import bar
+2
View File
@@ -0,0 +1,2 @@
print("BAR_MODULE_LOADED")
def f(): pass
Whitespace-only changes.
@@ -0,0 +1 @@
Foo = "Foo"
@@ -0,0 +1 @@
Ack = "Ack"
@@ -0,0 +1 @@
Bar = "Bar"
@@ -0,0 +1 @@
Baz = "Baz"
@@ -0,0 +1 @@
Qux = "Qux"
@@ -0,0 +1 @@
Thud = "Thud"
+9
View File
@@ -0,0 +1,9 @@
from .foo import Foo
from .foo.ack import Ack
from .foo.bar import Bar
from .foo.bar.baz import Baz
from .foo.bar.thud import Thud
from .waldo import Waldo
from .waldo.fred import Fred
from .plugh import Plugh
Metasyntactic = "Metasyntactic"
@@ -0,0 +1 @@
Plugh = "Plugh"
@@ -0,0 +1 @@
Waldo = "Waldo"
@@ -0,0 +1 @@
Fred = "Fred"
+1
View File
@@ -0,0 +1 @@
import missing_dependency_for_lazy_import_test
@@ -0,0 +1,3 @@
from .bar import Bar
bar = "Blah"
Bar
@@ -0,0 +1 @@
Bar = "Bar"
@@ -0,0 +1,3 @@
bar = "Blah"
from .bar import Bar
Bar
@@ -0,0 +1 @@
Bar = "Bar"
+12
View File
@@ -0,0 +1,12 @@
def __getattr__(name):
if name == "dynamic_attr":
return "from_getattr"
elif name == "raising_attr":
raise ValueError("from_getattr")
elif name == "import_error_attr":
raise ImportError(name)
elif name == "warning_attr":
import warnings
warnings.warn("from_getattr", category=UserWarning)
return "from_warning_attr"
raise AttributeError(name)
+5
View File
@@ -0,0 +1,5 @@
lazy import test.test_lazy_import.data.basic2 as basic2
import sys
mod = sys.modules[__name__]
x = mod.__dict__
+5
View File
@@ -0,0 +1,5 @@
lazy import test.test_lazy_import.data.basic2 as basic2
import sys
mod = sys.modules[__name__]
x = mod.basic2
@@ -0,0 +1,5 @@
lazy import test.test_lazy_import.data.basic2 as basic2
import sys
mod = sys.modules[__name__]
x = mod.__name__
+5
View File
@@ -0,0 +1,5 @@
# Test that lazy from import with multiple names only reifies accessed names
lazy from test.test_lazy_import.data.basic2 import f, x
def get_globals():
return globals()
+14
View File
@@ -0,0 +1,14 @@
x = 42
def __getattr__(name):
if name == "dynamic_attr":
return "from_getattr"
elif name == "raising_attr":
raise ValueError("from_getattr")
elif name == "import_error_attr":
raise ImportError(name)
elif name == "warning_attr":
import warnings
warnings.warn("from_getattr", category=UserWarning)
return "from_warning_attr"
raise AttributeError(name)
+2
View File
@@ -0,0 +1,2 @@
def foo():
return 'foo'
+2
View File
@@ -0,0 +1,2 @@
print("BAR_MODULE_LOADED")
def f(): pass
+2
View File
@@ -0,0 +1,2 @@
# Submodule that raises an error during import
raise ValueError("This module always fails to import")
+4
View File
@@ -0,0 +1,4 @@
lazy from . import b, x
def get_globals():
return globals()
+5
View File
@@ -0,0 +1,5 @@
# Test relative imports with lazy keyword
lazy from . import basic2
def get_basic2():
return basic2
+8
View File
@@ -0,0 +1,8 @@
# Test relative from imports with lazy keyword
lazy from .basic2 import x, f
def get_x():
return x
def get_f():
return f
@@ -0,0 +1 @@
raise ModuleNotFoundError("boom", name=__name__)
+4
View File
@@ -0,0 +1,4 @@
try:
import test.test_lazy_import.data.basic2
except:
pass
@@ -0,0 +1,4 @@
try:
from test.test_lazy_import.data.basic2 import f
except:
pass
+2
View File
@@ -0,0 +1,2 @@
from .__version__ import __version__
from .__version__ import __copyright__
@@ -0,0 +1,2 @@
__version__ = "1.0"
__copyright__ = "Copyright (c) 2001-2022 Python Software Foundation."
+15
View File
@@ -1233,6 +1233,20 @@ f'''
FSTRING_END \'"\' (2, 2) (2, 3)
""")
def test_ineq_tokens(self):
self.check_tokenize("1 != 2", """\
NUMBER '1' (1, 0) (1, 1)
OP '!=' (1, 2) (1, 4)
NUMBER '2' (1, 5) (1, 6)
""")
self.check_tokenize("1 <> 2", """\
NUMBER '1' (1, 0) (1, 1)
OP '<' (1, 2) (1, 3)
OP '>' (1, 3) (1, 4)
NUMBER '2' (1, 5) (1, 6)
""")
class GenerateTokensTest(TokenizeTest):
def check_tokenize(self, s, expected):
# Format the tokens in s in a table format.
@@ -3332,6 +3346,7 @@ class CTokenizerBufferTests(unittest.TestCase):
run_test_script(file_name)
@support.force_not_colorized_test_class
class CommandLineTest(unittest.TestCase):
def setUp(self):
self.filename = tempfile.mktemp()
+720 -50
View File
@@ -9,18 +9,21 @@ import inspect
import builtins
import unittest
import unittest.mock
import os
import re
import tempfile
import random
import string
import importlib.machinery
import sysconfig
from test import support
import shutil
from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ,
requires_debug_ranges, has_no_debug_ranges,
requires_subprocess)
requires_subprocess, os_helper)
from test.support.os_helper import TESTFN, temp_dir, unlink
from test.support.script_helper import assert_python_ok, assert_python_failure, make_script
from test.support.import_helper import forget
from test.support.import_helper import ensure_lazy_imports, forget
from test.support import force_not_colorized, force_not_colorized_test_class
import json
@@ -37,7 +40,7 @@ test_code.co_positions = lambda _: iter([(6, 6, 0, 0)])
test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals'])
test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next', 'tb_lasti'])
color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E"}
color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E", "note": "n"}
colors = {
color_overrides.get(k, k[0].lower()): v
for k, v in _colorize.default_theme.traceback.items()
@@ -88,6 +91,12 @@ class TracebackCases(unittest.TestCase):
def tokenizer_error_with_caret_range(self):
compile("blech ( ", "?", "exec")
def syntax_error_with_caret_wide_char(self):
compile("女女女=1; 女女女/", "?", "exec")
def syntax_error_with_caret_wide_char_range(self):
compile("f(x, 女女女 for 女女女 in range(30), z)", "?", "exec")
def test_caret(self):
err = self.get_exception_format(self.syntax_error_with_caret,
SyntaxError)
@@ -125,6 +134,20 @@ class TracebackCases(unittest.TestCase):
self.assertEqual(err[1].find("("), err[2].find("^")) # in the right place
self.assertEqual(err[2].count("^"), 1)
def test_caret_wide_char(self):
err = self.get_exception_format(self.syntax_error_with_caret_wide_char,
SyntaxError)
self.assertIn("^", err[2])
# "女女女=1; 女女女/" has display width 17
self.assertEqual(err[2].find("^"), 4 + 17)
err = self.get_exception_format(self.syntax_error_with_caret_wide_char_range,
SyntaxError)
self.assertIn("^", err[2])
self.assertEqual(err[2].find("^"), 4 + 5)
# "女女女 for 女女女 in range(30)" has display width 30
self.assertEqual(err[2].count("^"), 30)
def test_nocaret(self):
exc = SyntaxError("error", ("x.py", 23, None, "bad syntax"))
err = traceback.format_exception_only(SyntaxError, exc)
@@ -1767,6 +1790,7 @@ class TracebackErrorLocationCaretTestBase:
]
self.assertEqual(result_lines, expected)
class TestKeywordTypoSuggestions(unittest.TestCase):
TYPO_CASES = [
("with block ad something:\n pass", "and"),
@@ -1811,6 +1835,23 @@ class TestKeywordTypoSuggestions(unittest.TestCase):
stderr_text = stderr.decode('utf-8')
self.assertIn(f"Did you mean '{expected_kw}'", stderr_text)
def test_no_keyword_suggestion_for_comma_errors(self):
# When the parser identifies a missing comma, don't suggest
# bogus keyword replacements like 'print' -> 'not'
code = '''\
import sys
print(
"line1"
"line2"
file=sys.stderr
)
'''
source = textwrap.dedent(code).strip()
rc, stdout, stderr = assert_python_failure('-c', source)
stderr_text = stderr.decode('utf-8')
self.assertIn("Perhaps you forgot a comma", stderr_text)
self.assertNotIn("Did you mean", stderr_text)
@requires_debug_ranges()
@force_not_colorized_test_class
class PurePythonTracebackErrorCaretTests(
@@ -3852,7 +3893,7 @@ class TestTracebackException(unittest.TestCase):
def test_dont_swallow_cause_or_context_of_falsey_exception(self):
# see gh-132308: Ensure that __cause__ or __context__ attributes of exceptions
# that evaluate as falsey are included in the output. For falsey term,
# see https://docs.python.org/3/library/stdtypes.html#truth-value-testing.
# see https://docs.python.org/3/builtins/stdtypes.html#truth-value-testing.
try:
raise FalseyException from KeyError
@@ -4071,7 +4112,7 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self):
# see gh-132308: Ensure that subexceptions of exception groups
# that evaluate as falsey are displayed in the output. For falsey term,
# see https://docs.python.org/3/library/stdtypes.html#truth-value-testing.
# see https://docs.python.org/3/builtins/stdtypes.html#truth-value-testing.
try:
raise FalseyExceptionGroup("Gih", (KeyError(), NameError()))
@@ -4092,11 +4133,13 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
global_for_suggestions = None
class SuggestionFormattingTestBase:
class SuggestionFormattingTestMixin:
attr_function = getattr
def get_suggestion(self, obj, attr_name=None):
if attr_name is not None:
def callable():
getattr(obj, attr_name)
self.attr_function(obj, attr_name)
else:
callable = obj
@@ -4105,7 +4148,9 @@ class SuggestionFormattingTestBase:
)
return result_lines[0]
def test_getattr_suggestions(self):
class BaseSuggestionTests(SuggestionFormattingTestMixin):
def test_suggestions(self):
class Substitution:
noise = more_noise = a = bc = None
blech = None
@@ -4136,40 +4181,41 @@ class SuggestionFormattingTestBase:
BLuch = None
for cls, suggestion in [
(Addition, "'bluchin'?"),
(Substitution, "'blech'?"),
(Elimination, "'blch'?"),
(Addition, "'bluchin'?"),
(SubstitutionOverElimination, "'blach'?"),
(SubstitutionOverAddition, "'blach'?"),
(EliminationOverAddition, "'bluc'?"),
(CaseChangeOverSubstitution, "'BLuch'?"),
(Addition, "'.bluchin'"),
(Substitution, "'.blech'"),
(Elimination, "'.blch'"),
(Addition, "'.bluchin'"),
(SubstitutionOverElimination, "'.blach'"),
(SubstitutionOverAddition, "'.blach'"),
(EliminationOverAddition, "'.bluc'"),
(CaseChangeOverSubstitution, "'.BLuch'"),
]:
actual = self.get_suggestion(cls(), 'bluch')
self.assertIn(suggestion, actual)
self.assertIn('Did you mean ' + suggestion, actual)
def test_getattr_suggestions_underscored(self):
def test_suggestions_underscored(self):
class A:
bluch = None
self.assertIn("'bluch'", self.get_suggestion(A(), 'blach'))
self.assertIn("'bluch'", self.get_suggestion(A(), '_luch'))
self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch'))
self.assertIn("'.bluch'", self.get_suggestion(A(), 'blach'))
self.assertIn("'.bluch'", self.get_suggestion(A(), '_luch'))
self.assertIn("'.bluch'", self.get_suggestion(A(), '_bluch'))
attr_function = self.attr_function
class B:
_bluch = None
def method(self, name):
getattr(self, name)
attr_function(self, name)
self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach'))
self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch'))
self.assertNotIn("'_bluch'", self.get_suggestion(B(), 'bluch'))
self.assertIn("'._bluch'", self.get_suggestion(B(), '_blach'))
self.assertIn("'._bluch'", self.get_suggestion(B(), '_luch'))
self.assertNotIn("'._bluch'", self.get_suggestion(B(), 'bluch'))
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_blach')))
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_luch')))
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, 'bluch')))
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_blach')))
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_luch')))
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, 'bluch')))
def test_getattr_suggestions_with_custom___dir__(self):
def test_suggestions_with_custom___dir__(self):
class M(type):
def __dir__(cls):
return [None, "fox"]
@@ -4181,29 +4227,30 @@ class SuggestionFormattingTestBase:
class C1(C0, metaclass=M):
pass
self.assertNotIn("'bluch'", self.get_suggestion(C0, "blach"))
self.assertIn("'bluch'", self.get_suggestion(C0(), "blach"))
self.assertNotIn("'.bluch'", self.get_suggestion(C0, "blach"))
self.assertIn("'.bluch'", self.get_suggestion(C0(), "blach"))
self.assertIn("'fox'", self.get_suggestion(C1, "foo"))
self.assertNotIn("'fox'", self.get_suggestion(C1(), "foo"))
self.assertIn("'.fox'", self.get_suggestion(C1, "foo"))
self.assertNotIn("'.fox'", self.get_suggestion(C1(), "foo"))
self.assertNotIn("'bluch'", self.get_suggestion(C1, "blach"))
self.assertIn("'bluch'", self.get_suggestion(C1(), "blach"))
self.assertNotIn("'.bluch'", self.get_suggestion(C1, "blach"))
self.assertIn("'.bluch'", self.get_suggestion(C1(), "blach"))
def test_getattr_suggestions_do_not_trigger_for_long_attributes(self):
def test_do_not_trigger_for_long_attributes(self):
class A:
blech = None
actual = self.get_suggestion(A(), 'somethingverywrong')
self.assertNotIn("blech", actual)
def test_getattr_error_bad_suggestions_do_not_trigger_for_small_names(self):
def test_do_not_trigger_for_small_names(self):
class MyClass:
vvv = mom = w = id = pytho = None
for name in ("b", "v", "m", "py"):
with self.subTest(name=name):
actual = self.get_suggestion(MyClass, name)
actual = self.get_suggestion(MyClass(), name)
self.assertNotIn("Did you mean", actual)
self.assertNotIn("'vvv", actual)
self.assertNotIn("'mom'", actual)
@@ -4211,7 +4258,7 @@ class SuggestionFormattingTestBase:
self.assertNotIn("'w'", actual)
self.assertNotIn("'pytho'", actual)
def test_getattr_suggestions_do_not_trigger_for_big_dicts(self):
def test_do_not_trigger_for_big_dicts(self):
class A:
blech = None
# A class with a very big __dict__ will not be considered
@@ -4222,7 +4269,36 @@ class SuggestionFormattingTestBase:
actual = self.get_suggestion(A(), 'bluch')
self.assertNotIn("blech", actual)
def test_getattr_suggestions_no_args(self):
def test_suggestions_for_same_name(self):
class A:
def __dir__(self):
return ['blech']
actual = self.get_suggestion(A(), 'blech')
self.assertNotIn("Did you mean", actual)
def test_suggestions_not_normalized(self):
class A:
analization = None
fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None
suggestion = self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ')
self.assertIn("'.finalization'", suggestion)
self.assertNotIn("analization", suggestion)
class B:
attr_a = None
attr_µ = None # attr_\xb5
suggestion = self.get_suggestion(B(), 'attr_\xb5')
self.assertIn(
"'.attr_\u03bc' ('attr_\\u03bc') "
"instead of '.attr_\xb5' ('attr_\\xb5')",
suggestion)
self.assertNotIn("attr_a", suggestion)
class GetattrSuggestionTests(BaseSuggestionTests):
def test_suggestions_no_args(self):
class A:
blech = None
def __getattr__(self, attr):
@@ -4239,7 +4315,7 @@ class SuggestionFormattingTestBase:
actual = self.get_suggestion(A(), 'bluch')
self.assertIn("blech", actual)
def test_getattr_suggestions_invalid_args(self):
def test_suggestions_invalid_args(self):
class NonStringifyClass:
__str__ = None
__repr__ = None
@@ -4263,13 +4339,12 @@ class SuggestionFormattingTestBase:
actual = self.get_suggestion(cls(), 'bluch')
self.assertIn("blech", actual)
def test_getattr_suggestions_for_same_name(self):
class A:
def __dir__(self):
return ['blech']
actual = self.get_suggestion(A(), 'blech')
self.assertNotIn("Did you mean", actual)
class DelattrSuggestionTests(BaseSuggestionTests):
attr_function = delattr
class SuggestionFormattingTestBase(SuggestionFormattingTestMixin):
def test_attribute_error_with_failing_dict(self):
class T:
bluch = 1
@@ -4311,6 +4386,275 @@ class SuggestionFormattingTestBase:
self.assertIn("Did you mean", actual)
self.assertIn("bluch", actual)
def test_getattr_nested_attribute_suggestions(self):
# Test that nested attributes are suggested when no direct match
class Inner:
def __init__(self):
self.value = 42
self.data = "test"
class Outer:
def __init__(self):
self.inner = Inner()
# Should suggest 'inner.value'
actual = self.get_suggestion(Outer(), 'value')
self.assertIn("Did you mean '.inner.value' instead of '.value'", actual)
# Should suggest 'inner.data'
actual = self.get_suggestion(Outer(), 'data')
self.assertIn("Did you mean '.inner.data' instead of '.data'", actual)
def test_getattr_nested_prioritizes_direct_matches(self):
# Test that direct attribute matches are prioritized over nested ones
class Inner:
def __init__(self):
self.foo = 42
class Outer:
def __init__(self):
self.inner = Inner()
self.fooo = 100 # Similar to 'foo'
# Should suggest 'fooo' (direct) not 'inner.foo' (nested)
actual = self.get_suggestion(Outer(), 'foo')
self.assertIn("Did you mean '.fooo'", actual)
self.assertNotIn("inner.foo", actual)
def test_getattr_nested_with_property(self):
# Property suggestions should not execute the property getter.
class Inner:
@property
def computed(self):
return missing_name
class Outer:
def __init__(self):
self.inner = Inner()
actual = self.get_suggestion(Outer(), 'computed')
self.assertIn(
"Did you mean '.inner.computed' instead of '.computed'",
actual,
)
def test_getattr_nested_no_suggestion_for_deep_nesting(self):
# Test that deeply nested attributes (2+ levels) are not suggested
class Deep:
def __init__(self):
self.value = 42
class Middle:
def __init__(self):
self.deep = Deep()
class Outer:
def __init__(self):
self.middle = Middle()
# Should not suggest 'middle.deep.value' (too deep)
actual = self.get_suggestion(Outer(), 'value')
self.assertNotIn("Did you mean", actual)
def test_getattr_nested_ignores_private_attributes(self):
# Test that nested suggestions ignore private attributes
class Inner:
def __init__(self):
self.public_value = 42
class Outer:
def __init__(self):
self._private_inner = Inner()
# Should not suggest '_private_inner.public_value'
actual = self.get_suggestion(Outer(), 'public_value')
self.assertNotIn("Did you mean", actual)
def test_getattr_nested_limits_attribute_checks(self):
# Test that nested suggestions are limited to checking first 20 non-private attributes
class Inner:
def __init__(self):
self.target_value = 42
class Outer:
def __init__(self):
# Add many attributes before 'inner'
for i in range(25):
setattr(self, f'attr_{i:02d}', i)
# Add the inner object after 20+ attributes
self.inner = Inner()
obj = Outer()
# Verify that 'inner' is indeed present but after position 20
attrs = [x for x in sorted(dir(obj)) if not x.startswith('_')]
inner_position = attrs.index('inner')
self.assertGreater(inner_position, 19, "inner should be after position 20 in sorted attributes")
# Should not suggest 'inner.target_value' because inner is beyond the first 20 attributes checked
actual = self.get_suggestion(obj, 'target_value')
self.assertNotIn("inner.target_value", actual)
def test_getattr_nested_returns_first_match_only(self):
# Test that only the first nested match is returned (not multiple)
class Inner1:
def __init__(self):
self.value = 1
class Inner2:
def __init__(self):
self.value = 2
class Inner3:
def __init__(self):
self.value = 3
class Outer:
def __init__(self):
# Multiple inner objects with same attribute
self.a_inner = Inner1()
self.b_inner = Inner2()
self.c_inner = Inner3()
# Should suggest only the first match (alphabetically)
actual = self.get_suggestion(Outer(), 'value')
self.assertIn("'.a_inner.value'", actual)
# Verify it's a single suggestion, not multiple
self.assertEqual(actual.count("Did you mean"), 1)
def test_getattr_nested_handles_attribute_access_exceptions(self):
# Test that exceptions raised when accessing attributes don't crash the suggestion system
class ExplodingProperty:
@property
def exploding_attr(self):
raise RuntimeError("BOOM! This property always explodes")
def __repr__(self):
raise RuntimeError("repr also explodes")
class SafeInner:
def __init__(self):
self.target = 42
class Outer:
def __init__(self):
self.exploder = ExplodingProperty() # Accessing attributes will raise
self.safe_inner = SafeInner()
# Should still suggest '.safe_inner.target' without crashing
# even though accessing exploder.target would raise an exception
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'.safe_inner.target'", actual)
def test_getattr_nested_handles_hasattr_exceptions(self):
# Test that exceptions in hasattr don't crash the system
class WeirdObject:
def __getattr__(self, name):
if name == 'target':
raise RuntimeError("Can't check for target attribute")
raise AttributeError(f"No attribute {name}")
class NormalInner:
def __init__(self):
self.target = 100
class Outer:
def __init__(self):
self.weird = WeirdObject() # hasattr will raise for 'target'
self.normal = NormalInner()
# Should still find 'normal.target' even though weird.target check fails
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'.normal.target'", actual)
@force_not_colorized
def test_cross_language(self):
cases = [
# (type, attr, hint_attr)
(list, 'push', 'append'),
(list, 'concat', 'extend'),
(list, 'addAll', 'extend'),
(str, 'toUpperCase', 'upper'),
(str, 'toLowerCase', 'lower'),
(str, 'trimStart', 'lstrip'),
(str, 'trimEnd', 'rstrip'),
(dict, 'keySet', 'keys'),
(dict, 'entrySet', 'items'),
(dict, 'entries', 'items'),
(dict, 'putAll', 'update'),
]
for test_type, attr, hint_attr in cases:
with self.subTest(type=test_type.__name__, attr=attr):
obj = test_type()
actual = self.get_suggestion(obj, attr)
self.assertEndsWith(actual, f"Did you mean '.{hint_attr}'?")
cases = [
# (type, attr, hint)
(list, 'contains', "Use 'x in list'."),
(list, 'add', "Did you mean to use a 'set' object?"),
(dict, 'put', "Use d[k] = v."),
]
for test_type, attr, expected in cases:
with self.subTest(type=test_type, attr=attr):
obj = test_type()
actual = self.get_suggestion(obj, attr)
self.assertEndsWith(actual, expected)
@force_not_colorized
def test_cross_language_levenshtein_fallback(self):
# When no cross-language entry exists, Levenshtein still works
# (e.g., trim->strip is not in the table but Levenshtein catches it)
actual = self.get_suggestion('', 'trim')
self.assertIn("strip", actual)
@force_not_colorized
def test_cross_language_no_hint_for_unknown_attr(self):
actual = self.get_suggestion([], 'completely_unknown_method')
self.assertNotIn("Did you mean", actual)
@force_not_colorized
def test_cross_language_works_for_subclasses(self):
# isinstance() check means subclasses also get hints
class MyList(list):
pass
actual = self.get_suggestion(MyList(), 'push')
self.assertEndsWith(actual, "Did you mean '.append'?")
class MyDict(dict):
pass
actual = self.get_suggestion(MyDict(), 'keySet')
self.assertEndsWith(actual, "Did you mean '.keys'?")
@force_not_colorized
def test_cross_language_mutable_on_immutable(self):
# Mutable method on immutable type suggests the mutable counterpart
cases = [
(tuple, 'append', "Did you mean to use a 'list' object?"),
(tuple, 'extend', "Did you mean to use a 'list' object?"),
(tuple, 'insert', "Did you mean to use a 'list' object?"),
(tuple, 'remove', "Did you mean to use a 'list' object?"),
(frozenset, 'add', "Did you mean to use a 'set' object?"),
(frozenset, 'discard', "Did you mean to use a 'set' object?"),
(frozenset, 'remove', "Did you mean to use a 'set' object?"),
(frozenset, 'update', "Did you mean to use a 'set' object?"),
(frozendict, 'update', "Did you mean to use a 'dict' object?"),
]
for test_type, attr, expected in cases:
with self.subTest(type=test_type.__name__, attr=attr):
obj = test_type()
actual = self.get_suggestion(obj, attr)
self.assertEndsWith(actual, expected)
@force_not_colorized
def test_cross_language_float_bitwise(self):
# Bitwise operators on float suggest using int
cases = ['__or__', '__and__', '__xor__', '__lshift__', '__rshift__']
for attr in cases:
with self.subTest(attr=attr):
actual = self.get_suggestion(1.0, attr)
self.assertIn("'int'", actual)
self.assertIn("Bitwise operators", actual)
def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmpdir)
@@ -4665,6 +5009,34 @@ class SuggestionFormattingTestBase:
actual = self.get_suggestion(instance.foo)
self.assertIn("self.blech", actual)
def test_name_error_with_instance_not_normalized(self):
class A:
def __init__(self):
self.fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None
def foo(self):
analization = 1
x = fiⁿₐˡᵢᶻₐᵗᵢᵒₙ
instance = A()
actual = self.get_suggestion(instance.foo)
self.assertIn("self.finalization", actual)
self.assertNotIn("fiⁿₐˡᵢᶻₐᵗᵢᵒₙ", actual)
self.assertNotIn("analization", actual)
class B:
def __init__(self):
self.attr_µ = None # attr_\xb5
def foo(self):
attr_a = 1
x = attr_µ # attr_\xb5
instance = B()
actual = self.get_suggestion(instance.foo)
self.assertIn("self.attr_\u03bc", actual)
self.assertIn(r"self.attr_\u03bc", actual)
self.assertNotIn("attr_\xb5", actual)
self.assertNotIn("attr_a", actual)
def test_unbound_local_error_with_instance(self):
class A:
def __init__(self):
@@ -4747,6 +5119,51 @@ class CPythonSuggestionFormattingTests(
"""
class PurePythonGetattrSuggestionFormattingTests(
PurePythonExceptionFormattingMixin,
GetattrSuggestionTests,
unittest.TestCase,
):
"""
Same set of tests (for attribute access) as above using the pure Python
implementation of traceback printing in traceback.py.
"""
class PurePythonDelattrSuggestionFormattingTests(
PurePythonExceptionFormattingMixin,
DelattrSuggestionTests,
unittest.TestCase,
):
"""
Same set of tests (for attribute deletion) as above using the pure Python
implementation of traceback printing in traceback.py.
"""
@cpython_only
class CPythonGetattrSuggestionFormattingTests(
CAPIExceptionFormattingMixin,
GetattrSuggestionTests,
unittest.TestCase,
):
"""
Same set of tests (for attribute access) as above but with Python's
internal traceback printing.
"""
@cpython_only
class CPythonDelattrSuggestionFormattingTests(
CAPIExceptionFormattingMixin,
DelattrSuggestionTests,
unittest.TestCase,
):
"""
Same set of tests (for attribute deletion) as above but with Python's
internal traceback printing.
"""
class MiscTest(unittest.TestCase):
def test_all(self):
@@ -4841,7 +5258,109 @@ class MiscTest(unittest.TestCase):
with self.assertRaises(TypeError):
_suggestions._generate_suggestions(MyList(), "")
def test_no_site_package_flavour(self):
code = """import boo"""
_, _, stderr = assert_python_failure('-S', '-c', code)
self.assertIn(
(b"Site initialization is disabled, did you forget to "
b"add the site-packages directory to sys.path "
b"or to enable your virtual environment?"), stderr
)
code = """
import sys
sys.stdlib_module_names = sys.stdlib_module_names + ("boo",)
import boo
"""
_, _, stderr = assert_python_failure('-S', '-c', code)
self.assertNotIn(
(b"Site initialization is disabled, did you forget to "
b"add the site-packages directory to sys.path "
b"or to enable your virtual environment?"), stderr
)
def test_missing_stdlib_module(self):
code = """
import sys
sys.stdlib_module_names |= {'spam'}
import spam
"""
_, _, stderr = assert_python_failure('-S', '-c', code)
self.assertIn(b"Standard library module 'spam' was not found", stderr)
code = """
import sys
import traceback
traceback._MISSING_STDLIB_MODULE_MESSAGES = {'spam': "Install 'spam4life' for 'spam'"}
sys.stdlib_module_names |= {'spam'}
import spam
"""
_, _, stderr = assert_python_failure('-S', '-c', code)
self.assertIn(b"Install 'spam4life' for 'spam'", stderr)
@unittest.skipIf(sys.platform == "win32", "Non-Windows test")
def test_windows_only_module_error(self):
try:
import msvcrt # noqa: F401
except ModuleNotFoundError:
formatted = traceback.format_exc()
self.assertIn("Unsupported platform for Windows-only standard library module 'msvcrt'", formatted)
else:
self.fail("ModuleNotFoundError was not raised")
@unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules')
def test_find_incompatible_extension_modules(self):
"""_find_incompatible_extension_modules assumes the last extension in
importlib.machinery.EXTENSION_SUFFIXES (defined in Python/dynload_*.c)
is untagged (eg. .so, .pyd).
This test exists to make sure that assumption is correct.
"""
last_extension_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
if shlib_suffix := sysconfig.get_config_var('SHLIB_SUFFIX'):
self.assertEqual(last_extension_suffix, shlib_suffix)
else:
before_dot, *extensions = last_extension_suffix.split('.')
expected_prefixes = ['']
if os.name == 'nt':
# Windows puts the debug tag in the module file stem (eg. foo_d.pyd)
expected_prefixes.append('_d')
self.assertIn(before_dot, expected_prefixes, msg=(
f'Unexpected prefix {before_dot!r} in extension module '
f'suffix {last_extension_suffix!r}. '
'traceback._find_incompatible_extension_module needs to be '
'updated to take this into account!'
))
# if SHLIB_SUFFIX is not define, we assume the native
# shared library suffix only contains one extension
# (eg. '.so', bad eg. '.cpython-315-x86_64-linux-gnu.so')
self.assertEqual(len(extensions), 1, msg=(
'The last suffix in importlib.machinery.EXTENSION_SUFFIXES '
'contains more than one extension, so it is probably different '
'than SHLIB_SUFFIX. It probably contains an ABI tag! '
'If this is a false positive, define SHLIB_SUFFIX in sysconfig.'
))
@unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules')
def test_incompatible_extension_modules_hint(self):
untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
with os_helper.temp_dir() as tmp:
# create a module with a incompatible ABI tag
incompatible_module = f'foo.some-abi{untagged_suffix}'
open(os.path.join(tmp, incompatible_module), "wb").close()
# try importing it
code = f'''
import sys
sys.path.insert(0, {tmp!r})
import foo
'''
_, _, stderr = assert_python_failure('-c', code, __cwd=tmp)
hint = f'Although a module with this name was found for a different Python version ({incompatible_module}).'
self.assertIn(hint, stderr.decode())
class TestColorizedTraceback(unittest.TestCase):
@@ -4879,6 +5398,23 @@ class TestColorizedTraceback(unittest.TestCase):
self.assertIn("return baz1(1,\n 2,3\n ,4)", lines)
self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
def test_colorized_exception_notes(self):
def foo():
raise ValueError()
try:
foo()
except Exception as e:
e.add_note("First note")
e.add_note("Second note")
exc = traceback.TracebackException.from_exception(e)
lines = "".join(exc.format(colorize=True))
note = colors["n"]
reset = colors["z"]
self.assertIn(note + "First note" + reset, lines)
self.assertIn(note + "Second note" + reset, lines)
def test_colorized_syntax_error(self):
try:
compile("a $ b", "<string>", "exec")
@@ -4887,7 +5423,7 @@ class TestColorizedTraceback(unittest.TestCase):
e, capture_locals=True
)
actual = "".join(exc.format(colorize=True))
def expected(t, m, fn, l, f, E, e, z):
def expected(t, m, fn, l, f, E, e, z, n):
return "".join(
[
f' File {fn}"<string>"{z}, line {l}1{z}\n',
@@ -4914,7 +5450,7 @@ class TestColorizedTraceback(unittest.TestCase):
actual = tbstderr.getvalue().splitlines()
lno_foo = foo.__code__.co_firstlineno
def expected(t, m, fn, l, f, E, e, z):
def expected(t, m, fn, l, f, E, e, z, n):
return [
'Traceback (most recent call last):',
f' File {fn}"{__file__}"{z}, '
@@ -4947,7 +5483,7 @@ class TestColorizedTraceback(unittest.TestCase):
lno_foo = foo.__code__.co_firstlineno
actual = "".join(exc.format(colorize=True)).splitlines()
def expected(t, m, fn, l, f, E, e, z):
def expected(t, m, fn, l, f, E, e, z, n):
return [
f" + Exception Group Traceback (most recent call last):",
f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+9}{z}, in {f}test_colorized_traceback_from_exception_group{z}',
@@ -4971,5 +5507,139 @@ class TestColorizedTraceback(unittest.TestCase):
]
self.assertEqual(actual, expected(**colors))
def test_colorized_traceback_unicode(self):
try:
啊哈=1; 啊哈/0####
except Exception as e:
exc = traceback.TracebackException.from_exception(e)
actual = "".join(exc.format(colorize=True)).splitlines()
def expected(t, m, fn, l, f, E, e, z, n):
return [
f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####",
f" {e}~~~~{z}{E}^{z}{e}~{z}",
]
self.assertEqual(actual[2:4], expected(**colors))
try:
ééééé/0
except Exception as e:
exc = traceback.TracebackException.from_exception(e)
actual = "".join(exc.format(colorize=True)).splitlines()
def expected(t, m, fn, l, f, E, e, z, n):
return [
f" {E}ééééé{z}/0",
f" {E}^^^^^{z}",
]
self.assertEqual(actual[2:4], expected(**colors))
def test_colorized_syntax_error_ascii_display_width(self):
"""Caret alignment for ASCII edge cases handled by _wlen.
The old ASCII fast track in _display_width returned the raw character
offset for ASCII strings, which is wrong for CTRL-Z (display width 2)
and ANSI escape sequences (display width 0).
"""
E = colors["E"]
z = colors["z"]
t = colors["t"]
m = colors["m"]
fn = colors["fn"]
l = colors["l"]
def _make_syntax_error(text, offset, end_offset):
err = SyntaxError("invalid syntax")
err.filename = "<string>"
err.lineno = 1
err.end_lineno = 1
err.text = text
err.offset = offset
err.end_offset = end_offset
return err
# CTRL-Z (\x1a) is ASCII but displayed as ^Z (2 columns).
# Verify caret aligns when CTRL-Z precedes the error.
err = _make_syntax_error("a\x1a$\n", offset=3, end_offset=4)
exc = traceback.TracebackException.from_exception(err)
actual = "".join(exc.format(colorize=True))
# 'a' (1 col) + '\x1a' (2 cols) = 3 cols before '$'
self.assertIn(
f' File {fn}"<string>"{z}, line {l}1{z}\n'
f' a\x1a{E}${z}\n'
f' {" " * 3}{E}^{z}\n'
f'{t}SyntaxError{z}: {m}invalid syntax{z}\n',
actual,
)
# CTRL-Z in the highlighted (error) region counts as 2 columns.
err = _make_syntax_error("$\x1a\n", offset=1, end_offset=3)
exc = traceback.TracebackException.from_exception(err)
actual = "".join(exc.format(colorize=True))
# '$' (1 col) + '\x1a' (2 cols) = 3 columns of carets
self.assertIn(
f' {E}$\x1a{z}\n'
f' {E}{"^" * 3}{z}\n',
actual,
)
# ANSI escape sequences are ASCII but take 0 display columns.
err = _make_syntax_error("a\x1b[1mb$\n", offset=7, end_offset=8)
exc = traceback.TracebackException.from_exception(err)
actual = "".join(exc.format(colorize=True))
# 'a' (1 col) + '\x1b[1m' (0 cols) + 'b' (1 col) = 2 before '$'
self.assertIn(
f' a\x1b[1mb{E}${z}\n'
f' {" " * 2}{E}^{z}\n',
actual,
)
class TestLazyImportSuggestions(unittest.TestCase):
"""Test that lazy imports are not reified when computing AttributeError suggestions."""
def test_attribute_error_does_not_reify_lazy_imports(self):
"""Printing an AttributeError should not trigger lazy import reification."""
# lazypkg.bar prints "BAR_MODULE_LOADED" when imported.
# If lazy import is reified during suggestion computation, we'll see it.
code = textwrap.dedent("""
lazy import test.test_lazy_import.data.lazypkg
test.test_lazy_import.data.lazypkg.nonexistent
""")
rc, stdout, stderr = assert_python_failure('-c', code)
self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
def test_traceback_formatting_does_not_reify_lazy_imports(self):
"""Formatting a traceback should not trigger lazy import reification."""
code = textwrap.dedent("""
import traceback
lazy import test.test_lazy_import.data.lazypkg
try:
test.test_lazy_import.data.lazypkg.nonexistent
except AttributeError:
traceback.format_exc()
print("OK")
""")
rc, stdout, stderr = assert_python_ok('-c', code)
self.assertIn(b"OK", stdout)
self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
def test_suggestion_still_works_for_non_lazy_attributes(self):
"""Suggestions should still work for non-lazy module attributes."""
code = textwrap.dedent("""
lazy import test.test_lazy_import.data.lazypkg
# Typo for __name__
test.test_lazy_import.data.lazypkg.__nme__
""")
rc, stdout, stderr = assert_python_failure('-c', code)
self.assertIn(b"__name__", stderr)
self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
class LazyImportTest(unittest.TestCase):
@support.cpython_only
def test_lazy_import(self):
ensure_lazy_imports("traceback", {"_colorize"})
if __name__ == "__main__":
unittest.main()
+123 -1
View File
@@ -1,5 +1,9 @@
import subprocess
import unittest
from test import support
from test.support.os_helper import temp_cwd
from test.support.script_helper import assert_python_ok, spawn_python
from test.test_string._support import TStringBaseCase, fstring
@@ -79,6 +83,44 @@ class TestTString(unittest.TestCase, TStringBaseCase):
)
self.assertEqual(fstring(t), "Name: Bob, Age: 30")
@support.requires_subprocess()
def test_expression_in_interactive_after_buffer_resize(self):
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
source = (
f"result = t'''{{{expression}}}'''\n"
"print(repr(result.interpolations[0].expression))\n"
)
with spawn_python('-i', '-q', stderr=subprocess.PIPE) as process:
stdout, stderr = process.communicate(
source.encode(), timeout=support.SHORT_TIMEOUT)
self.assertEqual(process.returncode, 0, stderr)
self.assertEqual(stdout.decode().strip(), repr(expression))
def test_interpolation_expression_in_file_after_buffer_resize(self):
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
with temp_cwd():
script = 'script.py'
source = (
f"template = t'''{{{expression}}}'''\n"
"interpolation = template.interpolations[0]\n"
f"assert interpolation.expression == {expression!r}\n"
)
with open(script, 'w') as f:
f.write(source)
assert_python_ok(script)
@support.requires_resource('cpu')
def test_many_tstrings_in_module(self):
fields = ''.join(f'{{x{i}}}' for i in range(100))
source = ''.join(
f"value_{i} = t'{fields}'\n" for i in range(1_000)
)
namespace = {f'x{i}': str(i) for i in range(100)}
expected = ''.join(str(i) for i in range(100))
exec(source, namespace)
self.assertEqual(fstring(namespace['value_0']), expected)
self.assertEqual(fstring(namespace['value_999']), expected)
def test_format_specifiers(self):
# Test basic format specifiers
value = 3.14159
@@ -88,6 +130,14 @@ class TestTString(unittest.TestCase, TStringBaseCase):
)
self.assertEqual(fstring(t), "Pi: 3.14")
a = 3
b = 4
t = t"{a!=b:>10}"
self.assertTStringEqual(
t, ("", ""), [(a != b, "a!=b", None, ">10")]
)
self.assertEqual(fstring(t), " 1")
def test_conversions(self):
# Test !s conversion (str)
obj = object()
@@ -136,10 +186,82 @@ class TestTString(unittest.TestCase, TStringBaseCase):
# Test white space in debug specifier
t = t"Value: {value = }"
self.assertTStringEqual(
t, ("Value: value = ", ""), [(value, "value", "r")]
t, ("Value: value = ", ""), [(value, "value ", "r")]
)
self.assertEqual(fstring(t), "Value: value = 42")
# Explicit line continuations after the debug marker are part of
# the debug text, not the interpolation expression.
for template, strings, interpolation, rendered in (
(
t"""Value: {value =\
}""",
("Value: value =\\\n", ""),
(value, "value ", "r"),
"Value: value =\\\n42",
),
(
t"""Value: {value =\
!r}""",
("Value: value =\\\n", ""),
(value, "value ", "r"),
"Value: value =\\\n42",
),
(
t"""Value: {value =\
:04}""",
("Value: value =\\\n", ""),
(value, "value ", None, "04"),
"Value: value =\\\n0042",
),
(
t"""Value: {value =\
\
}""",
("Value: value =\\\n\\\n", ""),
(value, "value ", "r"),
"Value: value =\\\n\\\n42",
),
):
with self.subTest(template=template):
self.assertTStringEqual(template, strings, [interpolation])
self.assertEqual(fstring(template), rendered)
def test_interpolation_expression_whitespace(self):
x = 42
for template, expected in (
(t"{x}", "x"),
(t"{x }", "x "),
(t"{ x}", " x"),
(t"{ x }", " x "),
(t"{ x }", " x "),
(t"""{
x
}""", "\n x\n"),
(t"{ x !r}", " x "),
(t"{ x :.2f}", " x "),
(t"{ x = }", " x "),
(t"{ x = !r}", " x "),
(t"{ x = :.2f}", " x "),
(t"{x == 42 = }", "x == 42 "),
):
with self.subTest(template=template):
self.assertEqual(
template.interpolations[0].expression,
expected,
)
def test_interpolation_expression_with_reconstructed_metadata(self):
regular = t'''{(
1, # Force lexer metadata reconstruction.
"\"#")}'''
debug = t'''{(
1, # Force lexer metadata reconstruction.
"\"#")=}'''
expected = '(\n 1, \n "\\"#")'
self.assertEqual(regular.interpolations[0].expression, expected)
self.assertEqual(debug.interpolations[0].expression, expected)
def test_raw_tstrings(self):
path = r"C:\Users"
t = rt"{path}\Documents"
-1
View File
@@ -46,7 +46,6 @@ def clear_typing_caches():
class TypesTests(unittest.TestCase):
@unittest.expectedFailureIf(not hasattr(c_types, "LazyImportType"), "TODO: RUSTPYTHON; PEP 810 LazyImportType is not implemented")
def test_names(self):
c_only_names = {'CapsuleType', 'LazyImportType'}
ignored = {'new_class', 'resolve_bases', 'prepare_class',
+56 -10
View File
@@ -35,6 +35,7 @@ import sys
from token import *
from token import EXACT_TOKEN_TYPES
import _tokenize
lazy import _colorize
cookie_re = re.compile(br'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII)
blank_re = re.compile(br'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)
@@ -403,7 +404,7 @@ def detect_encoding(readline):
return None
encoding = _get_normal_name(match.group(1).decode())
try:
codec = lookup(encoding)
lookup(encoding)
except LookupError:
# This behaviour mimics the Python interpreter
if filename is None:
@@ -505,6 +506,56 @@ def generate_tokens(readline):
"""
return _generate_tokens_from_c_tokenizer(readline, extra_tokens=True)
def _get_token_colors(syntax, tokenize):
"""Map token type numbers to theme colors."""
return frozendict({
COMMENT: syntax.comment,
DEDENT: tokenize.whitespace,
ENCODING: tokenize.whitespace,
ENDMARKER: tokenize.whitespace,
ERRORTOKEN: tokenize.error,
FSTRING_START: syntax.string,
FSTRING_MIDDLE: syntax.string,
FSTRING_END: syntax.string,
INDENT: tokenize.whitespace,
NAME: syntax.reset,
NEWLINE: tokenize.whitespace,
NL: tokenize.whitespace,
NUMBER: syntax.number,
OP: syntax.op,
SOFT_KEYWORD: syntax.soft_keyword,
STRING: syntax.string,
TSTRING_START: syntax.string,
TSTRING_MIDDLE: syntax.string,
TSTRING_END: syntax.string,
})
def _format_tokens(tokens, *, color=False, exact=False):
theme = _colorize.get_theme(force_no_color=not color)
s = theme.syntax
t = theme.tokenize
token_colors = _get_token_colors(s, t)
for token in tokens:
token_range = (
f"{t.position}{token.start[0]}"
f"{t.delimiter},{t.position}{token.start[1]}"
f"{t.delimiter}-"
f"{t.position}{token.end[0]}"
f"{t.delimiter},{t.position}{token.end[1]}"
f"{t.delimiter}:"
)
token_color = token_colors.get(token.type, s.reset)
token_name = tok_name[token.exact_type if exact else token.type]
visible_range = f"{token.start[0]},{token.start[1]}-{token.end[0]},{token.end[1]}:"
yield (
f"{token_range}{' ' * (20 - len(visible_range))}"
f"{token_color}{token_name:<15}"
f"{s.reset}{token.string!r:<15}"
)
def _main(args=None):
import argparse
@@ -524,10 +575,10 @@ def _main(args=None):
sys.exit(1)
# Parse the arguments and options
parser = argparse.ArgumentParser(color=True)
parser = argparse.ArgumentParser()
parser.add_argument(dest='filename', nargs='?',
metavar='filename.py',
help='the file to tokenize; defaults to stdin')
help='the file to tokenize; defaults to `stdin`')
parser.add_argument('-e', '--exact', dest='exact', action='store_true',
help='display token names using the exact type')
args = parser.parse_args(args)
@@ -545,13 +596,8 @@ def _main(args=None):
# Output the tokenization
for token in tokens:
token_type = token.type
if args.exact:
token_type = token.exact_type
token_range = "%d,%d-%d,%d:" % (token.start + token.end)
print("%-20s%-15s%-15r" %
(token_range, tok_name[token_type], token.string))
for line in _format_tokens(tokens, color=True, exact=args.exact):
print(line)
except IndentationError as err:
line, column = err.args[1][1:3]
error(err.args[0], filename, (line, column))
+384 -45
View File
@@ -1,18 +1,29 @@
"""Extract, format and print information about Python stack traces."""
import collections.abc
import functools
import itertools
import linecache
import os
import re
import sys
import textwrap
import types
import warnings
import codeop
import keyword
import tokenize
import io
import _colorize
import importlib.util
import pathlib
from contextlib import suppress
lazy import _colorize
try:
from _missing_stdlib_info import _MISSING_STDLIB_MODULE_MESSAGES
except ImportError:
_MISSING_STDLIB_MODULE_MESSAGES = {}
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
'format_exception_only', 'format_list', 'format_stack',
@@ -21,6 +32,36 @@ __all__ = ['extract_stack', 'extract_tb', 'format_exception',
'FrameSummary', 'StackSummary', 'TracebackException',
'walk_stack', 'walk_tb', 'print_list']
class _ShutdownTheme:
"""Empty stand-in if `_colorize` cannot be imported during late shutdown."""
def __getattr__(self, _): return self
def __getitem__(self, _): return ""
def __format__(self, _): return ""
def __str__(self): return ""
def __add__(self, other): return other
__radd__ = __add__
_shutdown_theme = _ShutdownTheme()
def _safe_get_theme(*, force_color=False, force_no_color=False):
try:
return _colorize.get_theme(
force_color=force_color, force_no_color=force_no_color
)
except ImportError:
return _shutdown_theme
def _safe_can_colorize(*, file=None):
try:
return _colorize.can_colorize(file=file)
except ImportError:
return False
#
# Formatting and printing lists of traceback lines.
#
@@ -137,9 +178,10 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
BUILTIN_EXCEPTION_LIMIT = object()
def _print_exception_bltin(exc, /):
file = sys.stderr if sys.stderr is not None else sys.__stderr__
colorize = _colorize.can_colorize(file=file)
def _print_exception_bltin(exc, file=None, /):
if file is None:
file = sys.stderr if sys.stderr is not None else sys.__stderr__
colorize = _safe_can_colorize(file=file)
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
@@ -187,9 +229,9 @@ def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=
valuestr = _safe_string(value, 'exception')
end_char = "\n" if insert_final_newline else ""
if colorize:
theme = _colorize.get_theme(force_color=True).traceback
theme = _safe_get_theme(force_color=True).traceback
else:
theme = _colorize.get_theme(force_no_color=True).traceback
theme = _safe_get_theme(force_no_color=True).traceback
if value is None or not valuestr:
line = f"{theme.type}{etype}{theme.reset}{end_char}"
else:
@@ -205,9 +247,9 @@ def _safe_string(value, what, func=str):
# --
def print_exc(limit=None, file=None, chain=True):
def print_exc(limit=None, file=None, chain=True, **kwargs):
"""Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'."""
print_exception(sys.exception(), limit=limit, file=file, chain=chain)
print_exception(sys.exception(), limit=limit, file=file, chain=chain, **kwargs)
def format_exc(limit=None, chain=True):
"""Like print_exc() but return a string."""
@@ -286,6 +328,12 @@ class FrameSummary:
of code that was running when the frame was captured.
- :attr:`locals` Either None if locals were not supplied, or a dict
mapping the name to the repr() of the variable.
- :attr:`end_lineno` The last line number of the source code for this frame.
By default, it is set to lineno and indexation starts from 1.
- :attr:`colno` The column number of the source code for this frame.
By default, it is None and indexation starts from 0.
- :attr:`end_colno` The last column number of the source code for this frame.
By default, it is None and indexation starts from 0.
"""
__slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno',
@@ -536,9 +584,9 @@ class StackSummary(list):
if frame_summary.filename.startswith("<stdin-") and frame_summary.filename.endswith('>'):
filename = "<stdin>"
if colorize:
theme = _colorize.get_theme(force_color=True).traceback
theme = _safe_get_theme(force_color=True).traceback
else:
theme = _colorize.get_theme(force_no_color=True).traceback
theme = _safe_get_theme(force_no_color=True).traceback
row.append(
' File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(
theme.filename,
@@ -667,12 +715,12 @@ class StackSummary(list):
colorized_line_parts = []
colorized_carets_parts = []
for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):
for color, group in itertools.groupby(_zip_display_width(line, carets), key=lambda x: x[1]):
caret_group = list(group)
if color == "^":
if "^" in color:
colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)
colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)
elif color == "~":
elif "~" in color:
colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)
colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)
else:
@@ -954,7 +1002,54 @@ def _extract_caret_anchors_from_line_segment(segment):
return None
_WIDE_CHAR_SPECIFIERS = "WF"
def _zip_display_width(line, carets):
carets = iter(carets)
if line.isascii() and '\x1a' not in line:
for char in line:
yield char, next(carets, "")
return
import unicodedata
for char in unicodedata.iter_graphemes(line):
char = str(char)
char_width = _display_width(char)
yield char, "".join(itertools.islice(carets, char_width))
@functools.cache
def _str_width(c: str) -> int:
# copied from _pyrepl.utils to fix gh-130273
if ord(c) < 128:
return 1
import unicodedata
# gh-139246 for zero-width joiner and combining characters
if unicodedata.combining(c):
return 0
category = unicodedata.category(c)
if category == "Cf" and c != "\u00ad":
return 0
w = unicodedata.east_asian_width(c)
if w in ("N", "Na", "H", "A"):
return 1
return 2
_ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")
def _wlen(s: str) -> int:
# copied from _pyrepl.utils to fix gh-130273
if len(s) == 1 and s != "\x1a":
return _str_width(s)
length = sum(_str_width(i) for i in s)
# remove lengths of any escape sequences
sequence = _ANSI_ESCAPE_SEQUENCE.findall(s)
ctrl_z_cnt = s.count("\x1a")
return length - sum(len(i) for i in sequence) + ctrl_z_cnt
def _display_width(line, offset=None):
"""Calculate the amount of width space the given source
@@ -962,19 +1057,14 @@ def _display_width(line, offset=None):
width output device. Supports wide unicode characters and emojis."""
if offset is None:
offset = len(line)
return _wlen(line)
# Fast track for ASCII-only strings
if line.isascii():
return offset
return _wlen(line[:offset])
import unicodedata
return sum(
2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1
for char in line[:offset]
)
def _format_note(note, indent, theme):
for l in note.split("\n"):
yield f"{indent}{theme.note}{l}{theme.reset}\n"
class _ExceptionPrintContext:
@@ -1098,20 +1188,62 @@ class TracebackException:
wrong_name = getattr(exc_value, "name_from", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \
if suggestion.isascii():
self._str += f". Did you mean: '{suggestion}'?"
else:
self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?"
elif exc_type and issubclass(exc_type, ModuleNotFoundError):
module_name = getattr(exc_value, "name", None)
if module_name in sys.stdlib_module_names:
message = _MISSING_STDLIB_MODULE_MESSAGES.get(
module_name,
f"Standard library module {module_name!r} was not found"
)
self._str = message
elif sys.flags.no_site:
self._str += (". Site initialization is disabled, did you forget to "
+ "add the site-packages directory to sys.path "
+ "or to enable your virtual environment?")
elif abi_tag := _find_incompatible_extension_module(module_name):
self._str += (
". Although a module with this name was found for a "
f"different Python version ({abi_tag})."
)
else:
suggestion = _compute_suggestion_error(exc_value, exc_traceback, module_name)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
elif exc_type and issubclass(exc_type, AttributeError) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
# Check cross-language/wrong-type hints first (more specific),
# then fall back to Levenshtein distance suggestions.
hint = None
if hasattr(exc_value, 'obj'):
hint = _get_cross_language_hint(exc_value.obj, wrong_name)
if hint:
self._str += f". {hint}"
else:
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
if suggestion.isascii():
self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?"
else:
self._str += f". Did you mean '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}' ({wrong_name!a})?"
elif exc_type and issubclass(exc_type, NameError) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
if issubclass(exc_type, NameError):
wrong_name = getattr(exc_value, "name", None)
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
if suggestion:
self._str += f" Or did you forget to import '{wrong_name}'?"
else:
self._str += f". Did you forget to import '{wrong_name}'?"
if suggestion.isascii():
self._str += f". Did you mean: '{suggestion}'?"
else:
self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?"
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
if suggestion:
self._str += f" Or did you forget to import '{wrong_name}'?"
else:
self._str += f". Did you forget to import '{wrong_name}'?"
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
@@ -1240,6 +1372,10 @@ class TracebackException:
well, recursively, with indentation relative to their nesting depth.
"""
colorize = kwargs.get("colorize", False)
if colorize:
theme = _safe_get_theme(force_color=True).traceback
else:
theme = _safe_get_theme(force_no_color=True).traceback
indent = 3 * _depth * ' '
if not self._have_exc_type:
@@ -1268,9 +1404,10 @@ class TracebackException:
):
for note in self.__notes__:
note = _safe_string(note, 'note')
yield from [indent + l + '\n' for l in note.split('\n')]
yield from _format_note(note, indent, theme)
elif self.__notes__ is not None:
yield indent + "{}\n".format(_safe_string(self.__notes__, '__notes__', func=repr))
note = _safe_string(self.__notes__, '__notes__', func=repr)
yield from _format_note(note, indent, theme)
if self.exceptions and show_group:
for ex in self.exceptions:
@@ -1315,6 +1452,15 @@ class TracebackException:
if len(error_code) > 1024:
return
# If the original code doesn't raise SyntaxError, we can't validate
# that a keyword replacement actually fixes anything
try:
codeop.compile_command(error_code, symbol="exec", flags=codeop.PyCF_ONLY_AST)
except SyntaxError:
pass # Good - the original code has a syntax error we might fix
else:
return # Original code compiles or is incomplete - can't validate fixes
error_lines = error_code.splitlines()
tokens = tokenize.generate_tokens(io.StringIO(error_code).readline)
tokens_left_to_process = 10
@@ -1377,9 +1523,9 @@ class TracebackException:
# Show exactly where the problem was found.
colorize = kwargs.get("colorize", False)
if colorize:
theme = _colorize.get_theme(force_color=True).traceback
theme = _safe_get_theme(force_color=True).traceback
else:
theme = _colorize.get_theme(force_no_color=True).traceback
theme = _safe_get_theme(force_no_color=True).traceback
filename_suffix = ''
if self.lineno is not None:
yield ' File {}"{}"{}, line {}{}{}\n'.format(
@@ -1430,10 +1576,11 @@ class TracebackException:
# Convert 1-based column offset to 0-based index into stripped text
colno = offset - 1 - spaces
end_colno = end_offset - 1 - spaces
caretspace = ' '
if colno >= 0:
# non-space whitespace (likes tabs) must be kept for alignment
caretspace = ((c if c.isspace() else ' ') for c in ltext[:colno])
# Calculate display width to account for wide characters
dp_colno = _display_width(ltext, colno)
highlighted = ltext[colno:end_colno]
caret_count = _display_width(highlighted) if highlighted else (end_colno - colno)
start_color = end_color = ""
if colorize:
# colorize from colno to end_colno
@@ -1446,9 +1593,9 @@ class TracebackException:
end_color = theme.reset
yield ' {}\n'.format(ltext)
yield ' {}{}{}{}\n'.format(
"".join(caretspace),
' ' * dp_colno,
start_color,
('^' * (end_colno - colno)),
'^' * caret_count,
end_color,
)
else:
@@ -1579,6 +1726,62 @@ _MAX_STRING_SIZE = 40
_MOVE_COST = 2
_CASE_COST = 1
# Cross-language method suggestions for builtin types.
# Consulted as a fallback when Levenshtein-based suggestions find no match.
#
# Inclusion criteria:
#
# 1. Must have evidence of real cross-language confusion (Stack Overflow
# traffic, bug reports in production repos, developer survey data).
# 2. Must not be catchable by Levenshtein distance (too different from
# the correct Python method name).
#
# Each entry maps a wrong method name to a list of (type, suggestion, is_raw)
# tuples. The lookup checks isinstance() so subclasses are also matched.
# If is_raw is False, the suggestion is wrapped in "Did you mean '.X'?".
# If is_raw is True, the suggestion is rendered as-is.
#
# See https://github.com/python/cpython/issues/146406.
_CROSS_LANGUAGE_HINTS = frozendict({
# list -- JavaScript/Ruby equivalents
"push": ((list, "append", False),),
"concat": ((list, "extend", False),),
# list -- Java/C# equivalents
"addAll": ((list, "extend", False),),
"contains": ((list, "Use 'x in list'.", True),),
# list -- wrong-type suggestion (user expected a set)
"add": ((list, "Did you mean to use a 'set' object?", True),
(frozenset, "Did you mean to use a 'set' object?", True)),
# str -- JavaScript equivalents
"toUpperCase": ((str, "upper", False),),
"toLowerCase": ((str, "lower", False),),
"trimStart": ((str, "lstrip", False),),
"trimEnd": ((str, "rstrip", False),),
# dict -- Java/JavaScript equivalents
"keySet": ((dict, "keys", False),),
"entrySet": ((dict, "items", False),),
"entries": ((dict, "items", False),),
"putAll": ((dict, "update", False),),
"put": ((dict, "Use d[k] = v.", True),),
# tuple -- mutable method on immutable type (user expected a list)
"append": ((tuple, "Did you mean to use a 'list' object?", True),),
"extend": ((tuple, "Did you mean to use a 'list' object?", True),),
"insert": ((tuple, "Did you mean to use a 'list' object?", True),),
"remove": ((tuple, "Did you mean to use a 'list' object?", True),
(frozenset, "Did you mean to use a 'set' object?", True)),
# frozenset -- mutable method on immutable type (user expected a set)
"discard": ((frozenset, "Did you mean to use a 'set' object?", True),),
# frozendict -- mutable method on immutable type (user expected a dict)
"update": ((frozenset, "Did you mean to use a 'set' object?", True),
(frozendict, "Did you mean to use a 'dict' object?", True)),
# float -- bitwise operators belong to int
"__or__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__and__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__xor__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__lshift__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__rshift__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
})
def _substitution_cost(ch_a, ch_b):
if ch_a == ch_b:
@@ -1588,19 +1791,100 @@ def _substitution_cost(ch_a, ch_b):
return _MOVE_COST
def _is_lazy_import(obj, attr_name):
"""Check if attr_name in obj's __dict__ is a lazy import.
Returns True if obj is a module and the attribute is a LazyImportType,
False otherwise. This avoids triggering module loading when computing
suggestions for AttributeError.
"""
if not isinstance(obj, types.ModuleType):
return False
obj_dict = getattr(obj, '__dict__', None)
if obj_dict is None:
return False
attr_value = obj_dict.get(attr_name)
return isinstance(attr_value, types.LazyImportType)
def _check_for_nested_attribute(obj, wrong_name, attrs):
"""Check if any attribute of obj has the wrong_name as a nested attribute.
Returns the first nested attribute suggestion found, or None.
Limited to checking 20 attributes.
Only considers non-descriptor outer attributes to avoid executing
arbitrary code. Checks nested attributes statically so descriptors such
as properties can still be suggested without invoking them.
Skips lazy imports to avoid triggering module loading.
"""
from inspect import getattr_static
# Check for nested attributes (only one level deep)
attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check
for attr_name in attrs_to_check:
with suppress(Exception):
# Check if attr_name is a descriptor - if so, skip it
attr_from_class = getattr_static(type(obj), attr_name, _sentinel)
if attr_from_class is not _sentinel and hasattr(attr_from_class, '__get__'):
continue # Skip descriptors to avoid executing arbitrary code
# Skip lazy imports to avoid triggering module loading
if _is_lazy_import(obj, attr_name):
continue
# Safe to get the attribute since it's not a descriptor
attr_obj = getattr(obj, attr_name)
if _is_lazy_import(attr_obj, wrong_name):
continue
if getattr_static(attr_obj, wrong_name, _sentinel) is not _sentinel:
return f"{attr_name}.{wrong_name}"
return None
def _get_cross_language_hint(obj, wrong_name):
"""Check if wrong_name is a common method name from another language,
a mutable method on an immutable type, or a method tried on None.
Uses isinstance() so subclasses of builtin types also get hints.
Returns a formatted hint string, or None.
"""
entries = _CROSS_LANGUAGE_HINTS.get(wrong_name)
if entries is None:
return None
for check_type, hint, is_raw in entries:
if isinstance(obj, check_type):
if is_raw:
return hint
return f"Did you mean '.{hint}'?"
return None
def _get_safe___dir__(obj):
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
# See gh-131001 and gh-139933.
# Also filters out lazy imports to avoid triggering module loading.
try:
d = obj.__dir__()
except TypeError: # when obj is a class
d = type(obj).__dir__(obj)
return sorted(x for x in d if isinstance(x, str))
return sorted(
x for x in d if isinstance(x, str) and not _is_lazy_import(obj, x)
)
def _compute_suggestion_error(exc_value, tb, wrong_name):
if wrong_name is None or not isinstance(wrong_name, str):
return None
not_normalized = False
if not wrong_name.isascii():
from unicodedata import normalize
normalized_name = normalize('NFKC', wrong_name)
if normalized_name != wrong_name:
not_normalized = True
wrong_name = normalized_name
if isinstance(exc_value, AttributeError):
obj = exc_value.obj
try:
@@ -1616,6 +1900,18 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
d = [x for x in d if x[:1] != '_']
except Exception:
return None
elif isinstance(exc_value, ModuleNotFoundError):
try:
if parent_name := wrong_name.rpartition('.')[0]:
parent = importlib.util.find_spec(parent_name)
else:
parent = None
d = []
for finder in sys.meta_path:
if discover := getattr(finder, 'discover', None):
d += [spec.name for spec in discover(parent)]
except Exception:
return None
elif isinstance(exc_value, ImportError):
try:
mod = __import__(exc_value.name)
@@ -1638,6 +1934,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
+ list(frame.f_builtins)
)
d = [x for x in d if isinstance(x, str)]
if not_normalized and wrong_name in d:
return wrong_name
# Check first if we are in a method and the instance
# has the wrong name as attribute
@@ -1650,12 +1948,16 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
if has_wrong_name:
return f"self.{wrong_name}"
if not_normalized and wrong_name in d:
return wrong_name
try:
import _suggestions
except ImportError:
pass
else:
return _suggestions._generate_suggestions(d, wrong_name)
suggestion = _suggestions._generate_suggestions(d, wrong_name)
if suggestion:
return suggestion
# Compute closest match
@@ -1680,6 +1982,14 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
if not suggestion or current_distance < best_distance:
suggestion = possible_name
best_distance = current_distance
# If no direct attribute match found, check for nested attributes
if not suggestion and isinstance(exc_value, AttributeError):
with suppress(Exception):
nested_suggestion = _check_for_nested_attribute(exc_value.obj, wrong_name, d)
if nested_suggestion:
return nested_suggestion
return suggestion
@@ -1742,3 +2052,32 @@ def _levenshtein_distance(a, b, max_cost):
# Everything in this row is too big, so bail early.
return max_cost + 1
return result
def _find_incompatible_extension_module(module_name):
import importlib.machinery
import importlib.resources.readers
if not module_name or not importlib.machinery.EXTENSION_SUFFIXES:
return
# We assume the last extension is untagged (eg. .so, .pyd)!
# tests.test_traceback.MiscTest.test_find_incompatible_extension_modules
# tests that assumption.
untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
# On Windows the debug tag is part of the module file stem, instead of the
# extension (eg. foo_d.pyd), so let's remove it and just look for .pyd.
if os.name == 'nt':
untagged_suffix = untagged_suffix.removeprefix('_d')
parent, _, child = module_name.rpartition('.')
if parent:
traversable = importlib.resources.files(parent)
else:
traversable = importlib.resources.readers.MultiplexedPath(
*map(pathlib.Path, filter(os.path.isdir, sys.path))
)
for entry in traversable.iterdir():
if entry.name.startswith(child + '.') and entry.name.endswith(untagged_suffix):
return entry.name
+34 -45
View File
@@ -3683,6 +3683,19 @@ impl<'warnings> Compiler<'warnings> {
})
}
fn import_policy(&mut self, explicit: bool, star: bool) -> u32 {
if explicit {
1
} else if star
|| self.current_symbol_table().typ != CompilerScope::Module
|| self.in_exception_handler()
{
2
} else {
0
}
}
fn compile_statement(&mut self, statement: &ast::Stmt) -> CompileResult<()> {
trace!("Compiling {statement:?}");
let prev_source_range = self.current_source_span;
@@ -3703,7 +3716,7 @@ impl<'warnings> Compiler<'warnings> {
}
match &statement {
ast::Stmt::Import(ast::StmtImport { names, .. }) => {
ast::Stmt::Import(ast::StmtImport { names, is_lazy, .. }) => {
// import a, b, c as d
for name in names {
let name = &name;
@@ -3711,15 +3724,7 @@ impl<'warnings> Compiler<'warnings> {
value: num_traits::Zero::zero(),
});
self.emit_load_const(ConstantData::None);
// force eager import in try/except block
let low = if self.in_exception_handler()
|| self.current_symbol_table().typ != CompilerScope::Module
{
2
} else {
0
};
let namei = (self.name(&name.name) << 2) | low;
let namei = (self.name(&name.name) << 2) | self.import_policy(*is_lazy, false);
emit!(self, Instruction::ImportName { namei });
if let Some(alias) = &name.asname {
let parts: Vec<&str> = name.name.split('.').skip(1).collect();
@@ -3744,6 +3749,7 @@ impl<'warnings> Compiler<'warnings> {
level,
module,
names,
is_lazy,
..
}) => {
let import_star = names.first().is_some_and(|n| &n.name == "*");
@@ -3764,16 +3770,8 @@ impl<'warnings> Compiler<'warnings> {
});
let module_name = module.as_ref().map_or("", |s| s.as_str());
// forced non-lazy import due to try/except or import *
let low = if self.in_exception_handler()
|| self.current_symbol_table().typ != CompilerScope::Module
|| import_star
{
2
} else {
0
};
let module_idx = (self.name(module_name) << 2) | low;
let module_idx =
(self.name(module_name) << 2) | self.import_policy(*is_lazy, import_star);
emit!(self, Instruction::ImportName { namei: module_idx });
if import_star {
@@ -13821,18 +13819,13 @@ impl<'warnings> Compiler<'warnings> {
self.compile_expression(&interp.expression)?;
let expr_range = interp.expression.range();
let expr_source = if interp.range.start() < expr_range.start()
&& interp.range.end() >= expr_range.end()
{
let after_brace = interp.range.start() + TextSize::new(1);
self.source_file
.source_text()
.slice(TextRange::new(after_brace, expr_range.end()))
} else {
self.source_file.source_text().slice(expr_range)
}
.to_string();
let expr_source = crate::interpolation_expression_text(&self.source_file, interp)
.unwrap_or_else(|| {
self.source_file
.source_text()
.slice(interp.expression.range())
.to_owned()
});
self.set_source_node(interp.range, &interp.node_index);
self.emit_load_const(ConstantData::Str {
value: expr_source.into(),
@@ -17530,10 +17523,6 @@ class C[T]: pass
}
#[test]
#[expect(
clippy::literal_string_with_formatting_args,
reason = "the literal is the expected t-string annotation"
)]
fn future_tstring_annotation_preserves_interpolation_source_like_cpython() {
let code = compile_exec(
"from __future__ import annotations\nx: t'{a + b}'\ny: t'{ a + b }'\nz: f'{a + b =}'\nu: t'{a + b =}'\nv: t'{a + b =:>10}'\np: t'{(a)}'\nq: t'{((a))!r}'\nr: t'{ ((a)) = !r:>10}'\ns: t'{(a)=}'\nt: t'{a == b = }'\na1: t'''{a= # x=y\n}'''\na2: t'''{a # x=y\n}'''\na3: t'''{(a # x=y\n)}'''\na4: t'''{a # x=y\n!r}'''\na5: t'''{a # x=y\n:>10}'''\na6: t'''{'#'}'''\na7: t'''{('#', a) # c=d\n}'''\n",
@@ -17556,22 +17545,22 @@ class C[T]: pass
annotation_strings,
[
"t'{a + b}'",
"t'{ a + b}'",
"t'{ a + b }'",
"f'a + b ={a + b!r}'",
"t'a + b ={a + b!r}'",
"t'a + b ={a + b:>10}'",
"t'a + b ={a + b !r}'",
"t'a + b ={a + b :>10}'",
"t'{(a)}'",
"t'{((a))!r}'",
"t' ((a)) = { ((a))!r:>10}'",
"t' ((a)) = { ((a)) !r:>10}'",
"t'(a)={(a)!r}'",
"t'a == b = {a == b!r}'",
"t'a == b = {a == b !r}'",
"t'a= \\n{a!r}'",
"t'{a}'",
"t'{a \\n}'",
"t'{(a \\n)}'",
"t'{a!r}'",
"t'{a:>10}'",
"t'{a \\n!r}'",
"t'{a \\n:>10}'",
"t\"{'#'}\"",
"t\"{('#', a)}\"",
"t\"{('#', a) \\n}\"",
]
);
}
+102 -55
View File
@@ -8,7 +8,7 @@ extern crate log;
extern crate alloc;
use alloc::{string::String, vec::Vec};
use alloc::string::String;
use rustpython_compiler_core::bytecode::ConstantData;
type IndexMap<K, V> = indexmap::IndexMap<K, V, rapidhash::quality::RandomState>;
@@ -189,12 +189,14 @@ pub fn interpolation_debug_text(
) -> (String, TextRange) {
let leading = debug_text.leading();
let trailing = debug_text.trailing();
let text = [
strip_python_comments(leading).as_str(),
source_file.source_text().slice(expression_range),
strip_python_comments(trailing).as_str(),
]
.concat();
let text = strip_python_comments(
&[
leading,
source_file.source_text().slice(expression_range),
trailing,
]
.concat(),
);
let width =
|len: usize| TextSize::new(u32::try_from(len).expect("debug interpolation text too long"));
let range = TextRange::new(
@@ -204,64 +206,109 @@ pub fn interpolation_debug_text(
(text, range)
}
/// The source text stored in a template interpolation, including whitespace
/// before its debug marker, conversion, format specifier, or closing brace.
/// Returns `None` for a synthetic node without a source interpolation range.
#[must_use]
pub fn interpolation_expression_text(
source_file: &SourceFile,
interpolation: &ast::InterpolatedElement,
) -> Option<String> {
if let Some(debug_text) = &interpolation.debug_text {
let mut text = strip_python_comments(debug_text.as_str());
let end = interpolation_suffix_end(&text);
if text[..end].ends_with('=') {
text.truncate(end - 1);
}
return Some(text);
}
let expression_range = interpolation.expression.range();
if interpolation.range.start() >= expression_range.start()
|| interpolation.range.end() <= expression_range.end()
{
return None;
}
let start = interpolation.range.start() + TextSize::new(1);
let end = interpolation
.format_spec
.as_ref()
.map_or_else(
|| interpolation.range.end(),
|format_spec| format_spec.range.start(),
)
.checked_sub(TextSize::new(1))?;
if start > expression_range.start() || end < expression_range.end() {
return None;
}
let mut text =
strip_python_comments(source_file.source_text().slice(TextRange::new(start, end)));
if interpolation.conversion != ast::ConversionFlag::None {
let end = interpolation_suffix_end(&text);
let conversion_start = end.checked_sub(2)?;
if text.as_bytes().get(conversion_start) != Some(&b'!') {
return None;
}
text.truncate(conversion_start);
}
Some(text)
}
fn interpolation_suffix_end(text: &str) -> usize {
let mut end = text.len();
// Debug markers and conversions can be followed by whitespace and line
// continuations. Whitespace preceding either marker remains significant.
loop {
let trimmed = text[..end].trim_end();
let has_newline = text[trimmed.len()..end].contains(['\n', '\r']);
end = trimmed.len();
if has_newline && text[..end].ends_with('\\') {
end -= 1;
} else {
return end;
}
}
}
fn strip_python_comments(text: &str) -> String {
let chars = text.chars().collect::<Vec<_>>();
// Tokenizer metadata uses universal newlines even when source ranges refer
// to the original CRLF or CR input.
let normalized;
let text = if text.contains('\r') {
normalized = text.replace("\r\n", "\n").replace('\r', "\n");
normalized.as_str()
} else {
text
};
let mut chars = text.chars();
let mut result = String::with_capacity(text.len());
let mut quote = None;
let mut triple_quoted = false;
let mut escaped = false;
let mut in_comment = false;
let mut index = 0;
while index < chars.len() {
let ch = chars[index];
if in_comment {
if matches!(ch, '\n' | '\r') {
in_comment = false;
result.push(ch);
}
index += 1;
continue;
}
if let Some(delimiter) = quote {
while let Some(ch) = chars.next() {
// Match tokenizer metadata reconstruction, including escaped quotes
// outside literals and its per-quote handling of triple strings.
if ch == '\\' {
result.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if triple_quoted
&& ch == delimiter
&& chars.get(index + 1) == Some(&delimiter)
&& chars.get(index + 2) == Some(&delimiter)
{
result.push(delimiter);
result.push(delimiter);
quote = None;
index += 2;
} else if !triple_quoted && ch == delimiter {
if let Some(escaped) = chars.next() {
result.push(escaped);
}
} else if matches!(ch, '\'' | '"') {
if quote.is_none() {
quote = Some(ch);
} else if quote == Some(ch) {
quote = None;
}
index += 1;
continue;
}
match ch {
'#' => in_comment = true,
'\'' | '"' => {
quote = Some(ch);
triple_quoted =
chars.get(index + 1) == Some(&ch) && chars.get(index + 2) == Some(&ch);
result.push(ch);
if triple_quoted {
result.push(ch);
result.push(ch);
index += 2;
result.push(ch);
} else if ch == '#' && quote.is_none() {
for comment_ch in chars.by_ref() {
if comment_ch == '\n' {
result.push('\n');
break;
}
}
_ => result.push(ch),
} else {
result.push(ch);
}
index += 1;
}
result
}
+35
View File
@@ -1081,6 +1081,7 @@ struct SymbolTableBuilder {
comprehension_yield_context: Option<&'static str>,
// PEP 649: Track if we're inside a conditional block (if/for/while/etc.)
in_conditional_block: bool,
lazy_prohibited_scopes: Vec<usize>,
// Mirrors symtable ENTER_RECURSIVE guards during compilation.
recursion_depth: usize,
recursion_limit: usize,
@@ -1122,6 +1123,7 @@ impl SymbolTableBuilder {
in_iter_def_exp: false,
comprehension_yield_context: None,
in_conditional_block: false,
lazy_prohibited_scopes: Vec::new(),
recursion_depth: 0,
recursion_limit: DEFAULT_RECURSION_LIMIT,
next_block_index: 0,
@@ -1630,6 +1632,36 @@ impl SymbolTableBuilder {
Ok(())
}
fn validate_lazy_import(&self, statement: &ast::Stmt) -> SymbolTableResult {
let (kind, star) = match statement {
ast::Stmt::Import(node) if node.is_lazy => ("import", false),
ast::Stmt::ImportFrom(node) if node.is_lazy => (
"from ... import",
node.names
.first()
.is_some_and(|name| name.name.as_str() == "*"),
),
_ => return Ok(()),
};
let reason = if self.lazy_prohibited_scopes.contains(&self.tables.len()) {
Some(format!("lazy {kind} not allowed inside try/except blocks"))
} else {
match self.tables.last().unwrap().typ {
CompilerScope::Function | CompilerScope::AsyncFunction => {
Some(format!("lazy {kind} not allowed inside functions"))
}
CompilerScope::Class => Some(format!("lazy {kind} not allowed inside classes")),
_ if star => Some("lazy from ... import * is not allowed".to_owned()),
_ => None,
}
};
if let Some(reason) = reason {
Err(self.error_ranged(reason, statement.range()))
} else {
Ok(())
}
}
fn scan_statement(&mut self, statement: &ast::Stmt) -> SymbolTableResult {
if self.recursion_depth >= self.recursion_limit {
return Err(SymbolTableError {
@@ -1641,6 +1673,7 @@ impl SymbolTableBuilder {
self.recursion_depth += 1;
let result = (|| {
use ast::*;
self.validate_lazy_import(statement)?;
self.track_future_statement(statement)?;
match &statement {
Stmt::Global(StmtGlobal { names, .. }) => {
@@ -2005,6 +2038,7 @@ impl SymbolTableBuilder {
// PEP 649: Track conditional block for annotations
let saved_in_conditional_block = self.in_conditional_block;
self.in_conditional_block = true;
self.lazy_prohibited_scopes.push(self.tables.len());
self.scan_statements(body)?;
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
@@ -2023,6 +2057,7 @@ impl SymbolTableBuilder {
}
self.scan_statements(orelse)?;
self.scan_statements(finalbody)?;
self.lazy_prohibited_scopes.pop();
self.in_conditional_block = saved_in_conditional_block;
}
Stmt::Match(StmtMatch { subject, cases, .. }) => {
+16 -59
View File
@@ -1,8 +1,8 @@
use crate::strip_python_comments;
use crate::{interpolation_debug_text, interpolation_expression_text};
use alloc::fmt;
use core::fmt::Display as _;
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextSize, TextSlice};
use ruff_text_size::Ranged;
use rustpython_compiler_core::SourceFile;
use rustpython_literal::escape::{AsciiEscape, UnicodeEscape};
@@ -615,11 +615,8 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
fmt::from_fn(|f| Unparser::new(f, self.source).unparse_expr(val, precedence::TEST + 1))
.to_string();
if let Some(debug_text) = debug_text {
let leading = debug_text.leading();
let trailing = debug_text.trailing();
self.p(leading)?;
self.p(self.source.source_text().slice(val.range()))?;
self.p(trailing)?;
let (text, _) = interpolation_debug_text(self.source, debug_text, val.range());
self.unparse_fstring_str(&text)?;
if conversion == ast::ConversionFlag::None && spec.is_none() {
conversion = ast::ConversionFlag::Repr;
}
@@ -725,23 +722,11 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
&mut self,
interpolation: &ast::InterpolatedElement,
) -> fmt::Result {
let source_conversion = interpolation.conversion;
let mut conversion = source_conversion;
let debug_parts = interpolation.debug_text.as_ref().map(|debug_text| {
(
strip_python_comments(debug_text.leading()),
strip_python_comments(
self.source
.source_text()
.slice(interpolation.expression.range()),
),
strip_python_comments(debug_text.trailing()),
)
});
if let Some((leading, source, trailing)) = &debug_parts {
self.p(leading)?;
self.p(source)?;
self.p(trailing)?;
let mut conversion = interpolation.conversion;
if let Some(debug_text) = &interpolation.debug_text {
let (text, _) =
interpolation_debug_text(self.source, debug_text, interpolation.expression.range());
self.unparse_fstring_str(&text)?;
if conversion == ast::ConversionFlag::None && interpolation.format_spec.is_none() {
conversion = ast::ConversionFlag::Repr;
}
@@ -749,42 +734,14 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
let expression = if let Some(ast::ConstantValue::Str(value)) = &interpolation.runtime_str {
value.to_string()
} else if let Some((leading, source, trailing)) = &debug_parts {
let mut expression = leading.clone();
expression.push_str(source);
let equal = trailing
.rfind('=')
.expect("debug interpolation must contain '='");
expression.push_str(&trailing[..equal]);
expression.trim_end().to_owned()
} else if let Some(expression) = interpolation_expression_text(self.source, interpolation) {
expression
} else {
let expression_range = interpolation.expression.range();
let after_brace = interpolation.range.start() + TextSize::new(1);
let mut expression_end = interpolation.format_spec.as_ref().map_or_else(
|| interpolation.range.end() - TextSize::new(1),
|format_spec| format_spec.range.start() - TextSize::new(1),
);
if source_conversion != ast::ConversionFlag::None {
expression_end -= TextSize::new(2);
}
if interpolation.range.start() < expression_range.start()
&& interpolation.range.end() >= expression_range.end()
&& after_brace <= expression_end
{
strip_python_comments(
self.source
.source_text()
.slice(ruff_text_size::TextRange::new(after_brace, expression_end)),
)
.trim_end()
.to_owned()
} else {
fmt::from_fn(|f| {
Unparser::new(f, self.source)
.unparse_expr(&interpolation.expression, precedence::TEST + 1)
})
.to_string()
}
fmt::from_fn(|f| {
Unparser::new(f, self.source)
.unparse_expr(&interpolation.expression, precedence::TEST + 1)
})
.to_string()
};
self.p(if expression.starts_with('{') {
+14 -1
View File
@@ -955,7 +955,8 @@ pub fn too_deeply_nested_error(
/// A bare generator expression is one: `f(x for x in y)` is the `primary
/// genexp` alternative of a call, and a class header only takes `arguments`,
/// which has no such alternative. A format spec nested more than two deep is
/// the other; the tokenizer runs out of nesting levels for it.
/// the other; the tokenizer runs out of nesting levels for it. A lazy
/// `__future__` import is rejected by the `checked_from_import` action.
#[doc(hidden)]
#[must_use]
pub fn unsupported_grammar_error(ast: &ast::Mod, source_file: &SourceFile) -> Option<CompileError> {
@@ -1011,6 +1012,18 @@ pub fn unsupported_grammar_error(ast: &ast::Mod, source_file: &SourceFile) -> Op
if self.error.is_some() {
return;
}
if let ast::Stmt::ImportFrom(import) = stmt
&& import.is_lazy
&& import.level == 0
&& import
.module
.as_ref()
.is_some_and(|module| module.as_str() == "__future__")
{
let lazy = ruff_text_size::TextRange::at(import.range.start(), 4.into());
self.fail("lazy from __future__ import is not allowed", lazy);
return;
}
if let ast::Stmt::ClassDef(class_def) = stmt
&& let Some(arguments) = &class_def.arguments
&& let [ast::Expr::Generator(generator)] = &arguments.args[..]
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff. Show more