mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
6e8e7e0481
commit
9f6f771965
135 files changed
+7502
-1100
No files matched your search
Vendored
+9
-5
@@ -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)
|
||||
|
||||
|
||||
Vendored
+302
-10
@@ -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(
|
||||
*,
|
||||
|
||||
Vendored
+1
-3
@@ -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',
|
||||
)
|
||||
|
||||
|
||||
Vendored
+18
-3
@@ -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
@@ -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__':
|
||||
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ BaseException
|
||||
├── EOFError
|
||||
├── ExceptionGroup [BaseExceptionGroup]
|
||||
├── ImportError
|
||||
│ └── ImportCycleError
|
||||
│ └── ModuleNotFoundError
|
||||
├── LookupError
|
||||
│ ├── IndexError
|
||||
|
||||
Vendored
+129
-1
@@ -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
|
||||
|
||||
Vendored
+8
-4
@@ -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=[])
|
||||
|
||||
Vendored
+14
-4
@@ -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))], []),
|
||||
|
||||
Vendored
+179
-117
@@ -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':
|
||||
|
||||
+2826
File diff suppressed because it is too large.
Load diff
+3
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
lazy import test.test_lazy_import.data.basic2
|
||||
x = dir()
|
||||
@@ -0,0 +1 @@
|
||||
lazy from test.test_lazy_import.data import basic2
|
||||
@@ -0,0 +1 @@
|
||||
lazy import test.test_lazy_import.data.basic2
|
||||
@@ -0,0 +1,3 @@
|
||||
lazy import test.test_lazy_import.data.basic2 as basic2
|
||||
|
||||
basic2.f()
|
||||
@@ -0,0 +1,3 @@
|
||||
# Module that exists but doesn't have expected attributes
|
||||
x = 42
|
||||
# No 'nonexistent_attr' here
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
def f():
|
||||
import test.test_lazy_import.data.basic2 as basic2
|
||||
return basic2
|
||||
+10
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
|
||||
sys.set_lazy_imports("all")
|
||||
|
||||
import test.test_lazy_import.data.basic2 as basic2
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
lazy import test.test_lazy_import.data.basic2 as basic2
|
||||
|
||||
def f():
|
||||
x = globals()
|
||||
return x['basic2'].resolve()
|
||||
|
||||
f()
|
||||
@@ -0,0 +1,2 @@
|
||||
lazy import test.test_lazy_import.data.pkg.bar
|
||||
x = test.test_lazy_import.data.pkg.bar.f
|
||||
@@ -0,0 +1 @@
|
||||
lazy import test.test_lazy_import.data.init_fails
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import contextlib
|
||||
with contextlib.nullcontext():
|
||||
lazy import test.test_lazy_import.data.basic2
|
||||
@@ -0,0 +1,3 @@
|
||||
import contextlib
|
||||
with contextlib.nullcontext():
|
||||
lazy import test.test_lazy_import.data.basic2 as basic2
|
||||
@@ -0,0 +1 @@
|
||||
lazy from . import bar
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
lazy import test.test_lazy_import.data.basic2 as basic2
|
||||
|
||||
import sys
|
||||
mod = sys.modules[__name__]
|
||||
x = mod.__dict__
|
||||
@@ -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__
|
||||
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
return 'foo'
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
print("BAR_MODULE_LOADED")
|
||||
def f(): pass
|
||||
@@ -0,0 +1,2 @@
|
||||
# Submodule that raises an error during import
|
||||
raise ValueError("This module always fails to import")
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
lazy from . import b, x
|
||||
|
||||
def get_globals():
|
||||
return globals()
|
||||
@@ -0,0 +1,5 @@
|
||||
# Test relative imports with lazy keyword
|
||||
lazy from . import basic2
|
||||
|
||||
def get_basic2():
|
||||
return basic2
|
||||
@@ -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__)
|
||||
@@ -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
|
||||
@@ -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."
|
||||
Vendored
+15
@@ -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()
|
||||
|
||||
Vendored
+720
-50
@@ -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()
|
||||
Vendored
+123
-1
@@ -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"
|
||||
|
||||
Vendored
-1
@@ -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',
|
||||
|
||||
Vendored
+56
-10
@@ -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))
|
||||
|
||||
Vendored
+384
-45
@@ -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
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, .. }) => {
|
||||
|
||||
@@ -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('{') {
|
||||
|
||||
@@ -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
Reference in new issue
Block a user