Merge pull request #6718 from youknowone/py-3.14

RustPython version to 3.14
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-01-17 20:58:23 +09:00
commit 273c5a349b
81 files changed
+4669 -1272

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ aexit
aiter
anext
anextawaitable
annotationlib
appendleft
argcount
arrayiterator
+1 -1
View File
@@ -4,7 +4,7 @@ This document provides guidelines for working with GitHub Copilot when contribut
## Project Overview
RustPython is a Python 3 interpreter written in Rust, implementing Python 3.13.0+ compatibility. The project aims to provide:
RustPython is a Python 3 interpreter written in Rust, implementing Python 3.14.0+ compatibility. The project aims to provide:
- A complete Python-3 environment entirely in Rust (not CPython bindings)
- A clean implementation without compatibility hacks
+1 -1
View File
@@ -115,7 +115,7 @@ env:
test.test_multiprocessing_spawn.test_processes
ENV_POLLUTING_TESTS_WINDOWS: >-
# Python version targeted by the CI.
PYTHON_VERSION: "3.13.1"
PYTHON_VERSION: "3.14.2"
X86_64_PC_WINDOWS_MSVC_OPENSSL_LIB_DIR: C:\Program Files\OpenSSL\lib\VC\x64\MD
X86_64_PC_WINDOWS_MSVC_OPENSSL_INCLUDE_DIR: C:\Program Files\OpenSSL\include
+1 -1
View File
@@ -13,7 +13,7 @@ name: Periodic checks/tasks
env:
CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,ssl-rustls,jit
PYTHON_VERSION: "3.13.1"
PYTHON_VERSION: "3.14.2"
jobs:
# codecov collects code coverage data from the rust tests, python snippets and python test suite.
+1 -1
View File
@@ -9,7 +9,7 @@ on:
python-version:
description: Target python version to generate doc db for
type: string
default: "3.13.9"
default: "3.14.2"
ref:
description: Branch to commit to (leave empty for current branch)
type: string
+1 -1
View File
@@ -25,7 +25,7 @@ RustPython requires the following:
stable version: `rustup update stable`
- If you do not have Rust installed, use [rustup](https://rustup.rs/) to
do so.
- CPython version 3.13 or higher
- CPython version 3.14 or higher
- CPython can be installed by your operating system's package manager,
from the [Python website](https://www.python.org/downloads/), or
using a third-party distribution, such as
+253 -10
View File
@@ -1,13 +1,16 @@
from __future__ import annotations
import io
import os
import sys
from collections.abc import Callable, Iterator, Mapping
from dataclasses import dataclass, field, Field
COLORIZE = True
# types
if False:
from typing import IO
from typing import IO, Self, ClassVar
_theme: Theme
class ANSIColors:
@@ -17,11 +20,13 @@ class ANSIColors:
BLUE = "\x1b[34m"
CYAN = "\x1b[36m"
GREEN = "\x1b[32m"
GREY = "\x1b[90m"
MAGENTA = "\x1b[35m"
RED = "\x1b[31m"
WHITE = "\x1b[37m" # more like LIGHT GRAY
YELLOW = "\x1b[33m"
BOLD = "\x1b[1m"
BOLD_BLACK = "\x1b[1;30m" # DARK GRAY
BOLD_BLUE = "\x1b[1;34m"
BOLD_CYAN = "\x1b[1;36m"
@@ -60,13 +65,196 @@ class ANSIColors:
INTENSE_BACKGROUND_YELLOW = "\x1b[103m"
ColorCodes = set()
NoColors = ANSIColors()
for attr in dir(NoColors):
for attr, code in ANSIColors.__dict__.items():
if not attr.startswith("__"):
ColorCodes.add(code)
setattr(NoColors, attr, "")
#
# Experimental theming support (see gh-133346)
#
# - Create a theme by copying an existing `Theme` with one or more sections
# replaced, using `default_theme.copy_with()`;
# - create a theme section by copying an existing `ThemeSection` with one or
# more colors replaced, using for example `default_theme.syntax.copy_with()`;
# - create a theme from scratch by instantiating a `Theme` data class with
# the required sections (which are also dataclass instances).
#
# Then call `_colorize.set_theme(your_theme)` to set it.
#
# Put your theme configuration in $PYTHONSTARTUP for the interactive shell,
# or sitecustomize.py in your virtual environment or Python installation for
# other uses. Your applications can call `_colorize.set_theme()` too.
#
# Note that thanks to the dataclasses providing default values for all fields,
# creating a new theme or theme section from scratch is possible without
# specifying all keys.
#
# For example, here's a theme that makes punctuation and operators less prominent:
#
# try:
# from _colorize import set_theme, default_theme, Syntax, ANSIColors
# except ImportError:
# pass
# else:
# theme_with_dim_operators = default_theme.copy_with(
# syntax=Syntax(op=ANSIColors.INTENSE_BLACK),
# )
# set_theme(theme_with_dim_operators)
# del set_theme, default_theme, Syntax, ANSIColors, theme_with_dim_operators
#
# Guarding the import ensures that your .pythonstartup file will still work in
# Python 3.13 and older. Deleting the variables ensures they don't remain in your
# interactive shell's global scope.
class ThemeSection(Mapping[str, str]):
"""A mixin/base class for theme sections.
It enables dictionary access to a section, as well as implements convenience
methods.
"""
# The two types below are just that: types to inform the type checker that the
# mixin will work in context of those fields existing
__dataclass_fields__: ClassVar[dict[str, Field[str]]]
_name_to_value: Callable[[str], str]
def __post_init__(self) -> None:
name_to_value = {}
for color_name in self.__dataclass_fields__:
name_to_value[color_name] = getattr(self, color_name)
super().__setattr__('_name_to_value', name_to_value.__getitem__)
def copy_with(self, **kwargs: str) -> Self:
color_state: dict[str, str] = {}
for color_name in self.__dataclass_fields__:
color_state[color_name] = getattr(self, color_name)
color_state.update(kwargs)
return type(self)(**color_state)
@classmethod
def no_colors(cls) -> Self:
color_state: dict[str, str] = {}
for color_name in cls.__dataclass_fields__:
color_state[color_name] = ""
return cls(**color_state)
def __getitem__(self, key: str) -> str:
return self._name_to_value(key)
def __len__(self) -> int:
return len(self.__dataclass_fields__)
def __iter__(self) -> Iterator[str]:
return iter(self.__dataclass_fields__)
@dataclass(frozen=True, kw_only=True)
class Argparse(ThemeSection):
usage: str = ANSIColors.BOLD_BLUE
prog: str = ANSIColors.BOLD_MAGENTA
prog_extra: str = ANSIColors.MAGENTA
heading: str = ANSIColors.BOLD_BLUE
summary_long_option: str = ANSIColors.CYAN
summary_short_option: str = ANSIColors.GREEN
summary_label: str = ANSIColors.YELLOW
summary_action: str = ANSIColors.GREEN
long_option: str = ANSIColors.BOLD_CYAN
short_option: str = ANSIColors.BOLD_GREEN
label: str = ANSIColors.BOLD_YELLOW
action: str = ANSIColors.BOLD_GREEN
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
class Syntax(ThemeSection):
prompt: str = ANSIColors.BOLD_MAGENTA
keyword: str = ANSIColors.BOLD_BLUE
keyword_constant: str = ANSIColors.BOLD_BLUE
builtin: str = ANSIColors.CYAN
comment: str = ANSIColors.RED
string: str = ANSIColors.GREEN
number: str = ANSIColors.YELLOW
op: str = ANSIColors.RESET
definition: str = ANSIColors.BOLD
soft_keyword: str = ANSIColors.BOLD_BLUE
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
class Traceback(ThemeSection):
type: str = ANSIColors.BOLD_MAGENTA
message: str = ANSIColors.MAGENTA
filename: str = ANSIColors.MAGENTA
line_no: str = ANSIColors.MAGENTA
frame: str = ANSIColors.MAGENTA
error_highlight: str = ANSIColors.BOLD_RED
error_range: str = ANSIColors.RED
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
class Unittest(ThemeSection):
passed: str = ANSIColors.GREEN
warn: str = ANSIColors.YELLOW
fail: str = ANSIColors.RED
fail_info: str = ANSIColors.BOLD_RED
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
class Theme:
"""A suite of themes for all sections of Python.
When adding a new one, remember to also modify `copy_with` and `no_colors`
below.
"""
argparse: Argparse = field(default_factory=Argparse)
syntax: Syntax = field(default_factory=Syntax)
traceback: Traceback = field(default_factory=Traceback)
unittest: Unittest = field(default_factory=Unittest)
def copy_with(
self,
*,
argparse: Argparse | None = None,
syntax: Syntax | None = None,
traceback: Traceback | None = None,
unittest: Unittest | None = None,
) -> Self:
"""Return a new Theme based on this instance with some sections replaced.
Themes are immutable to protect against accidental modifications that
could lead to invalid terminal states.
"""
return type(self)(
argparse=argparse or self.argparse,
syntax=syntax or self.syntax,
traceback=traceback or self.traceback,
unittest=unittest or self.unittest,
)
@classmethod
def no_colors(cls) -> Self:
"""Return a new Theme where colors in all sections are empty strings.
This allows writing user code as if colors are always used. The color
fields will be ANSI color code strings when colorization is desired
and possible, and empty strings otherwise.
"""
return cls(
argparse=Argparse.no_colors(),
syntax=Syntax.no_colors(),
traceback=Traceback.no_colors(),
unittest=Unittest.no_colors(),
)
def get_colors(
colorize: bool = False, *, file: IO[str] | IO[bytes] | None = None
) -> ANSIColors:
@@ -76,22 +264,37 @@ def get_colors(
return NoColors
def decolor(text: str) -> str:
"""Remove ANSI color codes from a string."""
for code in ColorCodes:
text = text.replace(code, "")
return text
def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
def _safe_getenv(k: str, fallback: str | None = None) -> str | None:
"""Exception-safe environment retrieval. See gh-128636."""
try:
return os.environ.get(k, fallback)
except Exception:
return fallback
if file is None:
file = sys.stdout
if not sys.flags.ignore_environment:
if os.environ.get("PYTHON_COLORS") == "0":
if _safe_getenv("PYTHON_COLORS") == "0":
return False
if os.environ.get("PYTHON_COLORS") == "1":
if _safe_getenv("PYTHON_COLORS") == "1":
return True
if os.environ.get("NO_COLOR"):
if _safe_getenv("NO_COLOR"):
return False
if not COLORIZE:
return False
if os.environ.get("FORCE_COLOR"):
if _safe_getenv("FORCE_COLOR"):
return True
if os.environ.get("TERM") == "dumb":
if _safe_getenv("TERM") == "dumb":
return False
if not hasattr(file, "fileno"):
@@ -108,5 +311,45 @@ def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
try:
return os.isatty(file.fileno())
except io.UnsupportedOperation:
except OSError:
return hasattr(file, "isatty") and file.isatty()
default_theme = Theme()
theme_no_color = default_theme.no_colors()
def get_theme(
*,
tty_file: IO[str] | IO[bytes] | None = None,
force_color: bool = False,
force_no_color: bool = False,
) -> Theme:
"""Returns the currently set theme, potentially in a zero-color variant.
In cases where colorizing is not possible (see `can_colorize`), the returned
theme contains all empty strings in all color definitions.
See `Theme.no_colors()` for more information.
It is recommended not to cache the result of this function for extended
periods of time because the user might influence theme selection by
the interactive shell, a debugger, or application-specific code. The
environment (including environment variable state and console configuration
on Windows) can also change in the course of the application life cycle.
"""
if force_color or (not force_no_color and
can_colorize(file=tty_file)):
return _theme
return theme_no_color
def set_theme(t: Theme) -> None:
global _theme
if not isinstance(t, Theme):
raise ValueError(f"Expected Theme object, found {t}")
_theme = t
set_theme(default_theme)
+1143
View File
File diff suppressed because it is too large. Load diff
+247 -132
View File
@@ -18,11 +18,12 @@ command-line and writes the result to a file::
'integers', metavar='int', nargs='+', type=int,
help='an integer to be summed')
parser.add_argument(
'--log', default=sys.stdout, type=argparse.FileType('w'),
'--log',
help='the file where the sum should be written')
args = parser.parse_args()
args.log.write('%s' % sum(args.integers))
args.log.close()
with (open(args.log, 'w') if args.log is not None
else contextlib.nullcontext(sys.stdout)) as log:
log.write('%s' % sum(args.integers))
The module contains the following public classes:
@@ -39,7 +40,8 @@ The module contains the following public classes:
- FileType -- A factory for defining types of files to be created. As the
example above shows, instances of FileType are typically passed as
the type= argument of add_argument() calls.
the type= argument of add_argument() calls. Deprecated since
Python 3.14.
- Action -- The base class for parser actions. Typically actions are
selected by passing strings like 'store_true' or 'append_const' to
@@ -159,18 +161,21 @@ class HelpFormatter(object):
provided by the class are considered an implementation detail.
"""
def __init__(self,
prog,
indent_increment=2,
max_help_position=24,
width=None):
def __init__(
self,
prog,
indent_increment=2,
max_help_position=24,
width=None,
color=True,
):
# default setting for width
if width is None:
import shutil
width = shutil.get_terminal_size().columns
width -= 2
self._set_color(color)
self._prog = prog
self._indent_increment = indent_increment
self._max_help_position = min(max_help_position,
@@ -187,6 +192,16 @@ class HelpFormatter(object):
self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII)
self._long_break_matcher = _re.compile(r'\n\n\n+')
def _set_color(self, color):
from _colorize import can_colorize, decolor, get_theme
if color and can_colorize():
self._theme = get_theme(force_color=True).argparse
self._decolor = decolor
else:
self._theme = get_theme(force_no_color=True).argparse
self._decolor = lambda text: text
# ===============================
# Section and indentation methods
# ===============================
@@ -225,7 +240,11 @@ class HelpFormatter(object):
if self.heading is not SUPPRESS and self.heading is not None:
current_indent = self.formatter._current_indent
heading_text = _('%(heading)s:') % dict(heading=self.heading)
heading = '%*s%s\n' % (current_indent, '', heading_text)
t = self.formatter._theme
heading = (
f'{" " * current_indent}'
f'{t.heading}{heading_text}{t.reset}\n'
)
else:
heading = ''
@@ -262,7 +281,7 @@ class HelpFormatter(object):
if action.help is not SUPPRESS:
# find all invocations
get_invocation = self._format_action_invocation
get_invocation = lambda x: self._decolor(self._format_action_invocation(x))
invocation_lengths = [len(get_invocation(action)) + self._current_indent]
for subaction in self._iter_indented_subactions(action):
invocation_lengths.append(len(get_invocation(subaction)) + self._current_indent)
@@ -296,16 +315,23 @@ class HelpFormatter(object):
if part and part is not SUPPRESS])
def _format_usage(self, usage, actions, groups, prefix):
t = self._theme
if prefix is None:
prefix = _('usage: ')
# if usage is specified, use that
if usage is not None:
usage = usage % dict(prog=self._prog)
usage = (
t.prog_extra
+ usage
% {"prog": f"{t.prog}{self._prog}{t.reset}{t.prog_extra}"}
+ t.reset
)
# if no optionals or positionals are available, usage is just prog
elif usage is None and not actions:
usage = '%(prog)s' % dict(prog=self._prog)
usage = f"{t.prog}{self._prog}{t.reset}"
# if optionals and positionals are available, calculate usage
elif usage is None:
@@ -327,7 +353,7 @@ class HelpFormatter(object):
# wrap the usage parts if it's too long
text_width = self._width - self._current_indent
if len(prefix) + len(usage) > text_width:
if len(prefix) + len(self._decolor(usage)) > text_width:
# break usage into wrappable parts
opt_parts = self._get_actions_usage_parts(optionals, groups)
@@ -343,12 +369,13 @@ class HelpFormatter(object):
else:
line_len = indent_length - 1
for part in parts:
if line_len + 1 + len(part) > text_width and line:
part_len = len(self._decolor(part))
if line_len + 1 + part_len > text_width and line:
lines.append(indent + ' '.join(line))
line = []
line_len = indent_length - 1
line.append(part)
line_len += len(part) + 1
line_len += part_len + 1
if line:
lines.append(indent + ' '.join(line))
if prefix is not None:
@@ -356,8 +383,9 @@ class HelpFormatter(object):
return lines
# if prog is short, follow it with optionals or positionals
if len(prefix) + len(prog) <= 0.75 * text_width:
indent = ' ' * (len(prefix) + len(prog) + 1)
prog_len = len(self._decolor(prog))
if len(prefix) + prog_len <= 0.75 * text_width:
indent = ' ' * (len(prefix) + prog_len + 1)
if opt_parts:
lines = get_lines([prog] + opt_parts, indent, prefix)
lines.extend(get_lines(pos_parts, indent))
@@ -380,12 +408,18 @@ class HelpFormatter(object):
# join lines into usage
usage = '\n'.join(lines)
usage = usage.removeprefix(prog)
usage = f"{t.prog}{prog}{t.reset}{usage}"
# prefix with 'usage:'
return '%s%s\n\n' % (prefix, usage)
return f'{t.usage}{prefix}{t.reset}{usage}\n\n'
def _format_actions_usage(self, actions, groups):
return ' '.join(self._get_actions_usage_parts(actions, groups))
def _is_long_option(self, string):
return len(string) > 2
def _get_actions_usage_parts(self, actions, groups):
# find group indices and identify actions in groups
group_actions = set()
@@ -398,17 +432,18 @@ class HelpFormatter(object):
continue
try:
start = actions.index(group._group_actions[0])
start = min(actions.index(item) for item in group._group_actions)
except ValueError:
continue
else:
end = start + len(group._group_actions)
if actions[start:end] == group._group_actions:
if set(actions[start:end]) == set(group._group_actions):
group_actions.update(group._group_actions)
inserts[start, end] = group
# collect all actions format strings
parts = []
t = self._theme
for action in actions:
# suppressed arguments are marked with None
@@ -418,7 +453,11 @@ class HelpFormatter(object):
# produce all arg strings
elif not action.option_strings:
default = self._get_default_metavar_for_positional(action)
part = self._format_args(action, default)
part = (
t.summary_action
+ self._format_args(action, default)
+ t.reset
)
# if it's in a group, strip the outer []
if action in group_actions:
@@ -428,18 +467,26 @@ class HelpFormatter(object):
# produce the first way to invoke the option in brackets
else:
option_string = action.option_strings[0]
if self._is_long_option(option_string):
option_color = t.summary_long_option
else:
option_color = t.summary_short_option
# if the Optional doesn't take a value, format is:
# -s or --long
if action.nargs == 0:
part = action.format_usage()
part = f"{option_color}{part}{t.reset}"
# if the Optional takes a value, format is:
# -s ARGS or --long ARGS
else:
default = self._get_default_metavar_for_optional(action)
args_string = self._format_args(action, default)
part = '%s %s' % (option_string, args_string)
part = (
f"{option_color}{option_string} "
f"{t.summary_label}{args_string}{t.reset}"
)
# make it look optional if it's not required or in a group
if not action.required and action not in group_actions:
@@ -486,6 +533,7 @@ class HelpFormatter(object):
help_width = max(self._width - help_position, 11)
action_width = help_position - self._current_indent - 2
action_header = self._format_action_invocation(action)
action_header_no_color = self._decolor(action_header)
# no help; start on same line and add a final newline
if not action.help:
@@ -493,9 +541,15 @@ class HelpFormatter(object):
action_header = '%*s%s\n' % tup
# short action name; start on the same line and pad two spaces
elif len(action_header) <= action_width:
tup = self._current_indent, '', action_width, action_header
elif len(action_header_no_color) <= action_width:
# calculate widths without color codes
action_header_color = action_header
tup = self._current_indent, '', action_width, action_header_no_color
action_header = '%*s%-*s ' % tup
# swap in the colored header
action_header = action_header.replace(
action_header_no_color, action_header_color
)
indent_first = 0
# long action name; start on the next line
@@ -528,23 +582,42 @@ class HelpFormatter(object):
return self._join_parts(parts)
def _format_action_invocation(self, action):
t = self._theme
if not action.option_strings:
default = self._get_default_metavar_for_positional(action)
return ' '.join(self._metavar_formatter(action, default)(1))
return (
t.action
+ ' '.join(self._metavar_formatter(action, default)(1))
+ t.reset
)
else:
def color_option_strings(strings):
parts = []
for s in strings:
if self._is_long_option(s):
parts.append(f"{t.long_option}{s}{t.reset}")
else:
parts.append(f"{t.short_option}{s}{t.reset}")
return parts
# if the Optional doesn't take a value, format is:
# -s, --long
if action.nargs == 0:
return ', '.join(action.option_strings)
option_strings = color_option_strings(action.option_strings)
return ', '.join(option_strings)
# if the Optional takes a value, format is:
# -s, --long ARGS
else:
default = self._get_default_metavar_for_optional(action)
args_string = self._format_args(action, default)
return ', '.join(action.option_strings) + ' ' + args_string
option_strings = color_option_strings(action.option_strings)
args_string = (
f"{t.label}{self._format_args(action, default)}{t.reset}"
)
return ', '.join(option_strings) + ' ' + args_string
def _metavar_formatter(self, action, default_metavar):
if action.metavar is not None:
@@ -590,16 +663,19 @@ class HelpFormatter(object):
return result
def _expand_help(self, action):
help_string = self._get_help_string(action)
if '%' not in help_string:
return help_string
params = dict(vars(action), prog=self._prog)
for name in list(params):
if params[name] is SUPPRESS:
value = params[name]
if value is SUPPRESS:
del params[name]
for name in list(params):
if hasattr(params[name], '__name__'):
params[name] = params[name].__name__
elif hasattr(value, '__name__'):
params[name] = value.__name__
if params.get('choices') is not None:
params['choices'] = ', '.join(map(str, params['choices']))
return self._get_help_string(action) % params
return help_string % params
def _iter_indented_subactions(self, action):
try:
@@ -844,22 +920,16 @@ class Action(_AttributeHolder):
return self.option_strings[0]
def __call__(self, parser, namespace, values, option_string=None):
raise NotImplementedError(_('.__call__() not defined'))
raise NotImplementedError('.__call__() not defined')
# FIXME: remove together with `BooleanOptionalAction` deprecated arguments.
_deprecated_default = object()
class BooleanOptionalAction(Action):
def __init__(self,
option_strings,
dest,
default=None,
type=_deprecated_default,
choices=_deprecated_default,
required=False,
help=None,
metavar=_deprecated_default,
deprecated=False):
_option_strings = []
@@ -867,38 +937,19 @@ class BooleanOptionalAction(Action):
_option_strings.append(option_string)
if option_string.startswith('--'):
if option_string.startswith('--no-'):
raise ValueError(f'invalid option name {option_string!r} '
f'for BooleanOptionalAction')
option_string = '--no-' + option_string[2:]
_option_strings.append(option_string)
# We need `_deprecated` special value to ban explicit arguments that
# match default value. Like:
# parser.add_argument('-f', action=BooleanOptionalAction, type=int)
for field_name in ('type', 'choices', 'metavar'):
if locals()[field_name] is not _deprecated_default:
import warnings
warnings._deprecated(
field_name,
"{name!r} is deprecated as of Python 3.12 and will be "
"removed in Python {remove}.",
remove=(3, 14))
if type is _deprecated_default:
type = None
if choices is _deprecated_default:
choices = None
if metavar is _deprecated_default:
metavar = None
super().__init__(
option_strings=_option_strings,
dest=dest,
nargs=0,
default=default,
type=type,
choices=choices,
required=required,
help=help,
metavar=metavar,
deprecated=deprecated)
@@ -1180,6 +1231,7 @@ class _SubParsersAction(Action):
self._name_parser_map = {}
self._choices_actions = []
self._deprecated = set()
self._color = True
super(_SubParsersAction, self).__init__(
option_strings=option_strings,
@@ -1195,23 +1247,30 @@ class _SubParsersAction(Action):
if kwargs.get('prog') is None:
kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
# set color
if kwargs.get('color') is None:
kwargs['color'] = self._color
aliases = kwargs.pop('aliases', ())
if name in self._name_parser_map:
raise ArgumentError(self, _('conflicting subparser: %s') % name)
raise ValueError(f'conflicting subparser: {name}')
for alias in aliases:
if alias in self._name_parser_map:
raise ArgumentError(
self, _('conflicting subparser alias: %s') % alias)
raise ValueError(f'conflicting subparser alias: {alias}')
# create a pseudo-action to hold the choice help
if 'help' in kwargs:
help = kwargs.pop('help')
choice_action = self._ChoicesPseudoAction(name, aliases, help)
self._choices_actions.append(choice_action)
else:
choice_action = None
# create the parser and add it to the map
parser = self._parser_class(**kwargs)
if choice_action is not None:
parser._check_help(choice_action)
self._name_parser_map[name] = parser
# make parser available under aliases also
@@ -1276,7 +1335,7 @@ class _ExtendAction(_AppendAction):
# ==============
class FileType(object):
"""Factory for creating file object types
"""Deprecated factory for creating file object types
Instances of FileType are typically passed as type= arguments to the
ArgumentParser add_argument() method.
@@ -1293,6 +1352,12 @@ class FileType(object):
"""
def __init__(self, mode='r', bufsize=-1, encoding=None, errors=None):
import warnings
warnings.warn(
"FileType is deprecated. Simply open files after parsing arguments.",
category=PendingDeprecationWarning,
stacklevel=2
)
self._mode = mode
self._bufsize = bufsize
self._encoding = encoding
@@ -1396,7 +1461,7 @@ class _ActionsContainer(object):
self._defaults = {}
# determines whether an "option" looks like a negative number
self._negative_number_matcher = _re.compile(r'^-\d+$|^-\d*\.\d+$')
self._negative_number_matcher = _re.compile(r'-\.?\d')
# whether or not there are any optionals that look like negative
# numbers -- uses a list so it can be shared and edited
@@ -1449,7 +1514,8 @@ class _ActionsContainer(object):
chars = self.prefix_chars
if not args or len(args) == 1 and args[0][0] not in chars:
if args and 'dest' in kwargs:
raise ValueError('dest supplied twice for positional argument')
raise TypeError('dest supplied twice for positional argument,'
' did you mean metavar?')
kwargs = self._get_positional_kwargs(*args, **kwargs)
# otherwise, we're adding an optional argument
@@ -1465,27 +1531,34 @@ class _ActionsContainer(object):
kwargs['default'] = self.argument_default
# create the action object, and add it to the parser
action_name = kwargs.get('action')
action_class = self._pop_action_class(kwargs)
if not callable(action_class):
raise ValueError('unknown action "%s"' % (action_class,))
raise ValueError(f'unknown action {action_class!r}')
action = action_class(**kwargs)
# raise an error if action for positional argument does not
# consume arguments
if not action.option_strings and action.nargs == 0:
raise ValueError(f'action {action_name!r} is not valid for positional arguments')
# raise an error if the action type is not callable
type_func = self._registry_get('type', action.type, action.type)
if not callable(type_func):
raise ValueError('%r is not callable' % (type_func,))
raise TypeError(f'{type_func!r} is not callable')
if type_func is FileType:
raise ValueError('%r is a FileType class object, instance of it'
' must be passed' % (type_func,))
raise TypeError(f'{type_func!r} is a FileType class object, '
f'instance of it must be passed')
# raise an error if the metavar does not match the type
if hasattr(self, "_get_formatter"):
formatter = self._get_formatter()
try:
self._get_formatter()._format_args(action, None)
formatter._format_args(action, None)
except TypeError:
raise ValueError("length of metavar tuple does not match nargs")
self._check_help(action)
return self._add_action(action)
def add_argument_group(self, *args, **kwargs):
@@ -1529,8 +1602,8 @@ class _ActionsContainer(object):
if group.title in title_group_map:
# This branch could happen if a derived class added
# groups with duplicated titles in __init__
msg = _('cannot merge actions - two groups are named %r')
raise ValueError(msg % (group.title))
msg = f'cannot merge actions - two groups are named {group.title!r}'
raise ValueError(msg)
title_group_map[group.title] = group
# map each action to its group
@@ -1571,13 +1644,15 @@ class _ActionsContainer(object):
def _get_positional_kwargs(self, dest, **kwargs):
# make sure required is not specified
if 'required' in kwargs:
msg = _("'required' is an invalid argument for positionals")
msg = "'required' is an invalid argument for positionals"
raise TypeError(msg)
# mark positional arguments as required if at least one is
# always required
nargs = kwargs.get('nargs')
if nargs not in [OPTIONAL, ZERO_OR_MORE, REMAINDER, SUPPRESS, 0]:
if nargs == 0:
raise ValueError('nargs for positionals must be != 0')
if nargs not in [OPTIONAL, ZERO_OR_MORE, REMAINDER, SUPPRESS]:
kwargs['required'] = True
# return the keyword arguments with no option strings
@@ -1590,11 +1665,9 @@ class _ActionsContainer(object):
for option_string in args:
# error on strings that don't start with an appropriate prefix
if not option_string[0] in self.prefix_chars:
args = {'option': option_string,
'prefix_chars': self.prefix_chars}
msg = _('invalid option string %(option)r: '
'must start with a character %(prefix_chars)r')
raise ValueError(msg % args)
raise ValueError(
f'invalid option string {option_string!r}: '
f'must start with a character {self.prefix_chars!r}')
# strings starting with two prefix characters are long options
option_strings.append(option_string)
@@ -1610,8 +1683,8 @@ class _ActionsContainer(object):
dest_option_string = option_strings[0]
dest = dest_option_string.lstrip(self.prefix_chars)
if not dest:
msg = _('dest= is required for options like %r')
raise ValueError(msg % option_string)
msg = f'dest= is required for options like {option_string!r}'
raise TypeError(msg)
dest = dest.replace('-', '_')
# return the updated keyword arguments
@@ -1627,8 +1700,8 @@ class _ActionsContainer(object):
try:
return getattr(self, handler_func_name)
except AttributeError:
msg = _('invalid conflict_resolution value: %r')
raise ValueError(msg % self.conflict_handler)
msg = f'invalid conflict_resolution value: {self.conflict_handler!r}'
raise ValueError(msg)
def _check_conflict(self, action):
@@ -1667,10 +1740,26 @@ class _ActionsContainer(object):
if not action.option_strings:
action.container._remove_action(action)
def _check_help(self, action):
if action.help and hasattr(self, "_get_formatter"):
formatter = self._get_formatter()
try:
formatter._expand_help(action)
except (ValueError, TypeError, KeyError) as exc:
raise ValueError('badly formed help string') from exc
class _ArgumentGroup(_ActionsContainer):
def __init__(self, container, title=None, description=None, **kwargs):
if 'prefix_chars' in kwargs:
import warnings
depr_msg = (
"The use of the undocumented 'prefix_chars' parameter in "
"ArgumentParser.add_argument_group() is deprecated."
)
warnings.warn(depr_msg, DeprecationWarning, stacklevel=3)
# add any missing keyword arguments by checking the container
update = kwargs.setdefault
update('conflict_handler', container.conflict_handler)
@@ -1702,14 +1791,7 @@ class _ArgumentGroup(_ActionsContainer):
self._group_actions.remove(action)
def add_argument_group(self, *args, **kwargs):
import warnings
warnings.warn(
"Nesting argument groups is deprecated.",
category=DeprecationWarning,
stacklevel=2
)
return super().add_argument_group(*args, **kwargs)
raise ValueError('argument groups cannot be nested')
class _MutuallyExclusiveGroup(_ArgumentGroup):
@@ -1720,7 +1802,7 @@ class _MutuallyExclusiveGroup(_ArgumentGroup):
def _add_action(self, action):
if action.required:
msg = _('mutually exclusive arguments must be optional')
msg = 'mutually exclusive arguments must be optional'
raise ValueError(msg)
action = self._container._add_action(action)
self._group_actions.append(action)
@@ -1730,14 +1812,29 @@ class _MutuallyExclusiveGroup(_ArgumentGroup):
self._container._remove_action(action)
self._group_actions.remove(action)
def add_mutually_exclusive_group(self, *args, **kwargs):
import warnings
warnings.warn(
"Nesting mutually exclusive groups is deprecated.",
category=DeprecationWarning,
stacklevel=2
)
return super().add_mutually_exclusive_group(*args, **kwargs)
def add_mutually_exclusive_group(self, **kwargs):
raise ValueError('mutually exclusive groups cannot be nested')
def _prog_name(prog=None):
if prog is not None:
return prog
arg0 = _sys.argv[0]
try:
modspec = _sys.modules['__main__'].__spec__
except (KeyError, AttributeError):
# possibly PYTHONSTARTUP or -X presite or other weird edge case
# no good answer here, so fall back to the default
modspec = None
if modspec is None:
# simple script
return _os.path.basename(arg0)
py = _os.path.basename(_sys.executable)
if modspec.name != '__main__':
# imported module or package
modname = modspec.name.removesuffix('.__main__')
return f'{py} -m {modname}'
# directory or ZIP file
return f'{py} {arg0}'
class ArgumentParser(_AttributeHolder, _ActionsContainer):
@@ -1760,6 +1857,9 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
- allow_abbrev -- Allow long options to be abbreviated unambiguously
- exit_on_error -- Determines whether or not ArgumentParser exits with
error info when an error occurs
- suggest_on_error - Enables suggestions for mistyped argument choices
and subparser names (default: ``False``)
- color - Allow color output in help messages (default: ``False``)
"""
def __init__(self,
@@ -1775,19 +1875,18 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
conflict_handler='error',
add_help=True,
allow_abbrev=True,
exit_on_error=True):
exit_on_error=True,
*,
suggest_on_error=False,
color=True,
):
superinit = super(ArgumentParser, self).__init__
superinit(description=description,
prefix_chars=prefix_chars,
argument_default=argument_default,
conflict_handler=conflict_handler)
# default setting for prog
if prog is None:
prog = _os.path.basename(_sys.argv[0])
self.prog = prog
self.prog = _prog_name(prog)
self.usage = usage
self.epilog = epilog
self.formatter_class = formatter_class
@@ -1795,6 +1894,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
self.add_help = add_help
self.allow_abbrev = allow_abbrev
self.exit_on_error = exit_on_error
self.suggest_on_error = suggest_on_error
self.color = color
add_group = self.add_argument_group
self._positionals = add_group(_('positional arguments'))
@@ -1844,7 +1945,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
def add_subparsers(self, **kwargs):
if self._subparsers is not None:
raise ArgumentError(None, _('cannot have multiple subparser arguments'))
raise ValueError('cannot have multiple subparser arguments')
# add the parser class to the arguments if it's not present
kwargs.setdefault('parser_class', type(self))
@@ -1859,15 +1960,19 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# prog defaults to the usage message of this parser, skipping
# optional arguments and with no "usage:" prefix
if kwargs.get('prog') is None:
formatter = self._get_formatter()
# Create formatter without color to avoid storing ANSI codes in prog
formatter = self.formatter_class(prog=self.prog)
formatter._set_color(False)
positionals = self._get_positional_actions()
groups = self._mutually_exclusive_groups
formatter.add_usage(self.usage, positionals, groups, '')
formatter.add_usage(None, positionals, groups, '')
kwargs['prog'] = formatter.format_help().strip()
# create the parsers action and add it to the positionals list
parsers_class = self._pop_action_class(kwargs, 'parsers')
action = parsers_class(option_strings=[], **kwargs)
action._color = self.color
self._check_help(action)
self._subparsers._add_action(action)
# return the created parsers action
@@ -2498,7 +2603,6 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
value = action.default
if isinstance(value, str) and value is not SUPPRESS:
value = self._get_value(action, value)
self._check_value(action, value)
# when nargs='*' on a positional, if there were no command-line
# args, use the default if it is anything other than None
@@ -2506,11 +2610,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
not action.option_strings):
if action.default is not None:
value = action.default
self._check_value(action, value)
else:
# since arg_strings is always [] at this point
# there is no need to use self._check_value(action, value)
value = arg_strings
value = []
# single argument or optional argument produces a single value
elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]:
@@ -2543,8 +2644,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
def _get_value(self, action, arg_string):
type_func = self._registry_get('type', action.type, action.type)
if not callable(type_func):
msg = _('%r is not callable')
raise ArgumentError(action, msg % type_func)
raise TypeError(f'{type_func!r} is not callable')
# convert the value to the appropriate type
try:
@@ -2568,14 +2668,27 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
def _check_value(self, action, value):
# converted value must be one of the choices (if specified)
choices = action.choices
if choices is not None:
if isinstance(choices, str):
choices = iter(choices)
if value not in choices:
args = {'value': str(value),
'choices': ', '.join(map(str, action.choices))}
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
raise ArgumentError(action, msg % args)
if choices is None:
return
if isinstance(choices, str):
choices = iter(choices)
if value not in choices:
args = {'value': str(value),
'choices': ', '.join(map(str, action.choices))}
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
if self.suggest_on_error and isinstance(value, str):
if all(isinstance(choice, str) for choice in action.choices):
import difflib
suggestions = difflib.get_close_matches(value, action.choices, 1)
if suggestions:
args['closest'] = suggestions[0]
msg = _('invalid choice: %(value)r, maybe you meant %(closest)r? '
'(choose from %(choices)s)')
raise ArgumentError(action, msg % args)
# =======================
# Help-formatting methods
@@ -2611,7 +2724,9 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
return formatter.format_help()
def _get_formatter(self):
return self.formatter_class(prog=self.prog)
formatter = self.formatter_class(prog=self.prog)
formatter._set_color(self.color)
return formatter
# =====================
# Help-printing methods
+119 -6
View File
@@ -428,6 +428,7 @@ class TextCalendar(Calendar):
headers = (header for k in months)
a(formatstring(headers, colwidth, c).rstrip())
a('\n'*l)
# max number of weeks for this row
height = max(len(cal) for cal in row)
for j in range(height):
@@ -646,6 +647,117 @@ class LocaleHTMLCalendar(HTMLCalendar):
with different_locale(self.locale):
return super().formatmonthname(theyear, themonth, withyear)
class _CLIDemoCalendar(TextCalendar):
def __init__(self, highlight_day=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.highlight_day = highlight_day
def formatweek(self, theweek, width, *, highlight_day=None):
"""
Returns a single week in a string (no newline).
"""
if highlight_day:
from _colorize import get_colors
ansi = get_colors()
highlight = f"{ansi.BLACK}{ansi.BACKGROUND_YELLOW}"
reset = ansi.RESET
else:
highlight = reset = ""
return ' '.join(
(
f"{highlight}{self.formatday(d, wd, width)}{reset}"
if d == highlight_day
else self.formatday(d, wd, width)
)
for (d, wd) in theweek
)
def formatmonth(self, theyear, themonth, w=0, l=0):
"""
Return a month's calendar string (multi-line).
"""
if (
self.highlight_day
and self.highlight_day.year == theyear
and self.highlight_day.month == themonth
):
highlight_day = self.highlight_day.day
else:
highlight_day = None
w = max(2, w)
l = max(1, l)
s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
s = s.rstrip()
s += '\n' * l
s += self.formatweekheader(w).rstrip()
s += '\n' * l
for week in self.monthdays2calendar(theyear, themonth):
s += self.formatweek(week, w, highlight_day=highlight_day).rstrip()
s += '\n' * l
return s
def formatyear(self, theyear, w=2, l=1, c=6, m=3):
"""
Returns a year's calendar as a multi-line string.
"""
w = max(2, w)
l = max(1, l)
c = max(2, c)
colwidth = (w + 1) * 7 - 1
v = []
a = v.append
a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
a('\n'*l)
header = self.formatweekheader(w)
for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
# months in this row
months = range(m*i+1, min(m*(i+1)+1, 13))
a('\n'*l)
names = (self.formatmonthname(theyear, k, colwidth, False)
for k in months)
a(formatstring(names, colwidth, c).rstrip())
a('\n'*l)
headers = (header for k in months)
a(formatstring(headers, colwidth, c).rstrip())
a('\n'*l)
if (
self.highlight_day
and self.highlight_day.year == theyear
and self.highlight_day.month in months
):
month_pos = months.index(self.highlight_day.month)
else:
month_pos = None
# max number of weeks for this row
height = max(len(cal) for cal in row)
for j in range(height):
weeks = []
for k, cal in enumerate(row):
if j >= len(cal):
weeks.append('')
else:
day = (
self.highlight_day.day if k == month_pos else None
)
weeks.append(
self.formatweek(cal[j], w, highlight_day=day)
)
a(formatstring(weeks, colwidth, c).rstrip())
a('\n' * l)
return ''.join(v)
class _CLIDemoLocaleCalendar(LocaleTextCalendar, _CLIDemoCalendar):
def __init__(self, highlight_day=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.highlight_day = highlight_day
# Support for old module level interface
c = TextCalendar()
@@ -698,7 +810,7 @@ def timegm(tuple):
def main(args=None):
import argparse
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(color=True)
textgroup = parser.add_argument_group('text only arguments')
htmlgroup = parser.add_argument_group('html only arguments')
textgroup.add_argument(
@@ -765,6 +877,7 @@ def main(args=None):
sys.exit(1)
locale = options.locale, options.encoding
today = datetime.date.today()
if options.type == "html":
if options.month:
@@ -781,23 +894,23 @@ def main(args=None):
optdict = dict(encoding=encoding, css=options.css)
write = sys.stdout.buffer.write
if options.year is None:
write(cal.formatyearpage(datetime.date.today().year, **optdict))
write(cal.formatyearpage(today.year, **optdict))
else:
write(cal.formatyearpage(options.year, **optdict))
else:
if options.locale:
cal = LocaleTextCalendar(locale=locale)
cal = _CLIDemoLocaleCalendar(highlight_day=today, locale=locale)
else:
cal = TextCalendar()
cal = _CLIDemoCalendar(highlight_day=today)
cal.setfirstweekday(options.first_weekday)
optdict = dict(w=options.width, l=options.lines)
if options.month is None:
optdict["c"] = options.spacing
optdict["m"] = options.months
if options.month is not None:
else:
_validate_month(options.month)
if options.year is None:
result = cal.formatyear(datetime.date.today().year, **optdict)
result = cal.formatyear(today.year, **optdict)
elif options.month is None:
result = cal.formatyear(options.year, **optdict)
else:
+2 -2
View File
@@ -10,7 +10,7 @@ from shutil import copy2
__all__ = ["version", "bootstrap"]
_PIP_VERSION = "25.2"
_PIP_VERSION = "25.3"
# Directory of system wheel packages. Some Linux distribution packaging
# policies recommend against bundling dependencies. For example, Fedora
@@ -205,7 +205,7 @@ def _uninstall_helper(*, verbosity=0):
def _main(argv=None):
import argparse
parser = argparse.ArgumentParser(prog="python -m ensurepip")
parser = argparse.ArgumentParser(color=True)
parser.add_argument(
"--version",
action="version",
+1 -1
View File
@@ -6,7 +6,7 @@ import sys
def _main(argv=None):
parser = argparse.ArgumentParser(prog="python -m ensurepip._uninstall")
parser = argparse.ArgumentParser()
parser.add_argument(
"--version",
action="version",
+19 -6
View File
@@ -220,13 +220,26 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False):
"""
if isinstance(obj, type):
# class
obj_dict = getattr(obj, '__dict__', None)
if obj_dict and hasattr(obj_dict, 'get'):
ann = obj_dict.get('__annotations__', None)
if isinstance(ann, types.GetSetDescriptorType):
ann = None
else:
# XXX: RUSTPYTHON
# PEP 649: Use descriptor access to get __annotations__
# The __annotations__ attribute is now provided via a descriptor
# that calls __annotate__ to compute the annotations lazily
try:
ann = obj.__annotations__
except AttributeError:
ann = None
# Ensure we got this class's own annotations, not inherited
if ann is not None:
# Check if __annotations__ is in this class's __dict__
# or is computed by a descriptor (both are valid)
obj_dict = getattr(obj, '__dict__', None)
if obj_dict is not None:
dict_ann = obj_dict.get('__annotations__', None)
# If it's a descriptor or cached dict, use what we got from attribute access
# If it's inherited (not in __dict__ at all and no __annotate__), return empty
# XXX: RUSTPYTHON - also check __annotate_func__ (PEP 649 descriptor)
if dict_ann is None and '__annotate__' not in obj_dict and '__annotations_cache__' not in obj_dict and not hasattr(obj, '__annotate_func__'):
ann = None
obj_globals = None
module_name = getattr(obj, '__module__', None)
Vendored
+18 -12
View File
@@ -73,7 +73,7 @@ import sys
import os
import builtins
import _sitebuiltins
import io
import _io as io
import stat
import errno
@@ -95,6 +95,12 @@ def _trace(message):
print(message, file=sys.stderr)
def _warn(*args, **kwargs):
import warnings
warnings.warn(*args, **kwargs)
def makepath(*paths):
dir = os.path.join(*paths)
try:
@@ -444,9 +450,9 @@ def setcopyright():
"""Set 'copyright' and 'credits' in builtins"""
builtins.copyright = _sitebuiltins._Printer("copyright", sys.copyright)
builtins.credits = _sitebuiltins._Printer("credits", """\
Thanks to CWI, CNRI, BeOpen, Zope Corporation, the Python Software
Foundation, and a cast of thousands for supporting Python
development. See www.python.org for more information.""")
Thanks to CWI, CNRI, BeOpen, Zope Corporation, the Python Software
Foundation, and a cast of thousands for supporting Python
development. See www.python.org for more information.""")
files, dirs = [], []
# Not all modules are required to have a __file__ attribute. See
# PEP 420 for more details.
@@ -574,7 +580,7 @@ def register_readline():
def write_history():
try:
readline_module.write_history_file(history)
except (FileNotFoundError, PermissionError):
except FileNotFoundError, PermissionError:
# home directory does not exist or is not writable
# https://bugs.python.org/issue19891
pass
@@ -626,17 +632,17 @@ def venv(known_paths):
elif key == 'home':
sys._home = value
sys.prefix = sys.exec_prefix = site_prefix
if sys.prefix != site_prefix:
_warn(f'Unexpected value in sys.prefix, expected {site_prefix}, got {sys.prefix}', RuntimeWarning)
if sys.exec_prefix != site_prefix:
_warn(f'Unexpected value in sys.exec_prefix, expected {site_prefix}, got {sys.exec_prefix}', RuntimeWarning)
# Doing this here ensures venv takes precedence over user-site
addsitepackages(known_paths, [sys.prefix])
# addsitepackages will process site_prefix again if its in PREFIXES,
# but that's ok; known_paths will prevent anything being added twice
if system_site == "true":
PREFIXES.insert(0, sys.prefix)
PREFIXES += [sys.base_prefix, sys.base_exec_prefix]
else:
PREFIXES = [sys.prefix]
ENABLE_USER_SITE = False
return known_paths
@@ -646,7 +652,7 @@ def execsitecustomize():
"""Run custom site specific code, if available."""
try:
try:
import sitecustomize
import sitecustomize # noqa: F401
except ImportError as exc:
if exc.name == 'sitecustomize':
pass
@@ -666,7 +672,7 @@ def execusercustomize():
"""Run custom user specific code, if available."""
try:
try:
import usercustomize
import usercustomize # noqa: F401
except ImportError as exc:
if exc.name == 'usercustomize':
pass
+38 -22
View File
@@ -49,11 +49,18 @@ def capwords(s, sep=None):
####################################################################
import re as _re
from collections import ChainMap as _ChainMap
_sentinel_dict = {}
class _TemplatePattern:
# This descriptor is overwritten in ``Template._compile_pattern()``.
def __get__(self, instance, cls=None):
if cls is None:
return self
return cls._compile_pattern()
_TemplatePattern = _TemplatePattern()
class Template:
"""A string class for supporting $-substitutions."""
@@ -64,14 +71,21 @@ class Template:
# See https://bugs.python.org/issue31672
idpattern = r'(?a:[_a-z][_a-z0-9]*)'
braceidpattern = None
flags = _re.IGNORECASE
flags = None # default: re.IGNORECASE
pattern = _TemplatePattern # use a descriptor to compile the pattern
def __init_subclass__(cls):
super().__init_subclass__()
if 'pattern' in cls.__dict__:
pattern = cls.pattern
else:
delim = _re.escape(cls.delimiter)
cls._compile_pattern()
@classmethod
def _compile_pattern(cls):
import re # deferred import, for performance
pattern = cls.__dict__.get('pattern', _TemplatePattern)
if pattern is _TemplatePattern:
delim = re.escape(cls.delimiter)
id = cls.idpattern
bid = cls.braceidpattern or cls.idpattern
pattern = fr"""
@@ -82,7 +96,10 @@ class Template:
(?P<invalid>) # Other ill-formed delimiter exprs
)
"""
cls.pattern = _re.compile(pattern, cls.flags | _re.VERBOSE)
if cls.flags is None:
cls.flags = re.IGNORECASE
pat = cls.pattern = re.compile(pattern, cls.flags | re.VERBOSE)
return pat
def __init__(self, template):
self.template = template
@@ -105,7 +122,8 @@ class Template:
if mapping is _sentinel_dict:
mapping = kws
elif kws:
mapping = _ChainMap(kws, mapping)
from collections import ChainMap
mapping = ChainMap(kws, mapping)
# Helper function for .sub()
def convert(mo):
# Check the most common path first.
@@ -124,7 +142,8 @@ class Template:
if mapping is _sentinel_dict:
mapping = kws
elif kws:
mapping = _ChainMap(kws, mapping)
from collections import ChainMap
mapping = ChainMap(kws, mapping)
# Helper function for .sub()
def convert(mo):
named = mo.group('named') or mo.group('braced')
@@ -170,10 +189,6 @@ class Template:
self.pattern)
return ids
# Initialize Template.pattern. __init_subclass__() is automatically called
# only for subclasses, not for the Template class itself.
Template.__init_subclass__()
########################################################################
# the Formatter class
@@ -212,19 +227,20 @@ class Formatter:
# this is some markup, find the object and do
# the formatting
# handle arg indexing when empty field_names are given.
if field_name == '':
# handle arg indexing when empty field first parts are given.
field_first, _ = _string.formatter_field_name_split(field_name)
if field_first == '':
if auto_arg_index is False:
raise ValueError('cannot switch from manual field '
'specification to automatic field '
'numbering')
field_name = str(auto_arg_index)
field_name = str(auto_arg_index) + field_name
auto_arg_index += 1
elif field_name.isdigit():
elif isinstance(field_first, int):
if auto_arg_index:
raise ValueError('cannot switch from manual field '
'specification to automatic field '
'numbering')
raise ValueError('cannot switch from automatic field '
'numbering to manual field '
'specification')
# disable auto arg incrementing, if it gets
# used later on, then an exception will be raised
auto_arg_index = False
+33
View File
@@ -0,0 +1,33 @@
"""Support for template string literals (t-strings)."""
t = t"{0}"
Template = type(t)
Interpolation = type(t.interpolations[0])
del t
def convert(obj, /, conversion):
"""Convert *obj* using formatted string literal semantics."""
if conversion is None:
return obj
if conversion == 'r':
return repr(obj)
if conversion == 's':
return str(obj)
if conversion == 'a':
return ascii(obj)
raise ValueError(f'invalid conversion specifier: {conversion}')
def _template_unpickle(*args):
import itertools
if len(args) != 2:
raise ValueError('Template expects tuple of length 2 to unpickle')
strings, interpolations = args
parts = []
for string, interpolation in itertools.zip_longest(strings, interpolations):
if string is not None:
parts.append(string)
if interpolation is not None:
parts.append(interpolation)
return Template(*parts)
+48
View File
@@ -1,4 +1,5 @@
import contextlib
import dataclasses
import io
import sys
import unittest
@@ -21,6 +22,42 @@ def supports_virtual_terminal():
return contextlib.nullcontext()
class TestTheme(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_attributes(self):
# only theme configurations attributes by default
for field in dataclasses.fields(_colorize.Theme):
with self.subTest(field.name):
self.assertIsSubclass(field.type, _colorize.ThemeSection)
self.assertIsNotNone(field.default_factory)
def test_copy_with(self):
theme = _colorize.Theme()
copy = theme.copy_with()
self.assertEqual(theme, copy)
unittest_no_colors = _colorize.Unittest.no_colors()
copy = theme.copy_with(unittest=unittest_no_colors)
self.assertEqual(copy.argparse, theme.argparse)
self.assertEqual(copy.syntax, theme.syntax)
self.assertEqual(copy.traceback, theme.traceback)
self.assertEqual(copy.unittest, unittest_no_colors)
def test_no_colors(self):
# idempotence test
theme_no_colors = _colorize.Theme().no_colors()
theme_no_colors_no_colors = theme_no_colors.no_colors()
self.assertEqual(theme_no_colors, theme_no_colors_no_colors)
# attributes check
for section in dataclasses.fields(_colorize.Theme):
with self.subTest(section.name):
section_theme = getattr(theme_no_colors, section.name)
self.assertEqual(section_theme, section.type.no_colors())
class TestColorizeFunction(unittest.TestCase):
def test_colorized_detection_checks_for_environment_variables(self):
def check(env, fallback, expected):
@@ -129,6 +166,17 @@ class TestColorizeFunction(unittest.TestCase):
file.isatty.return_value = False
self.assertEqual(_colorize.can_colorize(file=file), False)
# The documentation for file.fileno says:
# > An OSError is raised if the IO object does not use a file descriptor.
# gh-141570: Check OSError is caught and handled
with unittest.mock.patch("os.isatty", side_effect=ZeroDivisionError):
file = unittest.mock.MagicMock()
file.fileno.side_effect = OSError
file.isatty.return_value = True
self.assertEqual(_colorize.can_colorize(file=file), True)
file.isatty.return_value = False
self.assertEqual(_colorize.can_colorize(file=file), False)
if __name__ == "__main__":
unittest.main()
+801 -194
View File
File diff suppressed because it is too large. Load diff
+9 -12
View File
@@ -2010,21 +2010,18 @@ class BuiltinTest(unittest.TestCase):
self.assertRaises(TypeError, tp, 1, 2)
self.assertRaises(TypeError, tp, a=1, b=2)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_warning_notimplemented(self):
# Issue #35712: NotImplemented is a sentinel value that should never
def test_bool_notimplemented(self):
# GH-79893: NotImplemented is a sentinel value that should never
# be evaluated in a boolean context (virtually all such use cases
# are a result of accidental misuse implementing rich comparison
# operations in terms of one another).
# For the time being, it will continue to evaluate as a true value, but
# issue a deprecation warning (with the eventual intent to make it
# a TypeError).
self.assertWarns(DeprecationWarning, bool, NotImplemented)
with self.assertWarns(DeprecationWarning):
self.assertTrue(NotImplemented)
with self.assertWarns(DeprecationWarning):
self.assertFalse(not NotImplemented)
msg = "NotImplemented should not be used in a boolean context"
self.assertRaisesRegex(TypeError, msg, bool, NotImplemented)
with self.assertRaisesRegex(TypeError, msg):
if NotImplemented:
pass
with self.assertRaisesRegex(TypeError, msg):
not NotImplemented
class TestBreakpoint(unittest.TestCase):
+4 -2
View File
@@ -987,10 +987,11 @@ class CommandLineTestCase(unittest.TestCase):
self.assertCLIFails(*args)
self.assertCmdFails(*args)
@support.force_not_colorized
def test_help(self):
stdout = self.run_cmd_ok('-h')
self.assertIn(b'usage:', stdout)
self.assertIn(b'calendar.py', stdout)
self.assertIn(b' -m calendar ', stdout)
self.assertIn(b'--help', stdout)
# special case: stdout but sys.exit()
@@ -1089,6 +1090,7 @@ class CommandLineTestCase(unittest.TestCase):
output = run('--months', '1', '2004')
self.assertIn(conv('\nMo Tu We Th Fr Sa Su\n'), output)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_option_type(self):
self.assertFailure('-t')
self.assertFailure('--type')
@@ -1097,7 +1099,7 @@ class CommandLineTestCase(unittest.TestCase):
output = run('--type', 'text', '2004')
self.assertEqual(output, conv(result_2004_text))
output = run('--type', 'html', '2004')
self.assertEqual(output[:6], b'<?xml ')
self.assertStartsWith(output, b'<?xml ')
self.assertIn(b'<title>Calendar for 2004</title>', output)
def test_html_output_current_year(self):
-1
View File
@@ -6,7 +6,6 @@ import tempfile
import test.support
import unittest
import unittest.mock
from importlib.resources.abc import Traversable
from pathlib import Path
import ensurepip
+2
View File
@@ -374,6 +374,7 @@ class GrammarTests(unittest.TestCase):
self.assertEqual(F.__annotations__, {})
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_var_annot_metaclass_semantics(self):
class CMeta(type):
@classmethod
@@ -403,6 +404,7 @@ class GrammarTests(unittest.TestCase):
with self.assertRaises(NameError):
ann_module3.D_bad_ann(5)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_var_annot_simple_exec(self):
gns = {}; lns= {}
exec("'docstring'\n"
+10
View File
@@ -248,6 +248,7 @@ class TestBasicOps(unittest.TestCase):
self.assertRaises(TypeError, list, chain.from_iterable([2, 3]))
self.assertEqual(list(islice(chain.from_iterable(repeat(range(5))), 2)), [0, 1])
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_chain_reducible(self):
for oper in [copy.deepcopy] + picklecopiers:
@@ -567,6 +568,7 @@ class TestBasicOps(unittest.TestCase):
self.assertEqual(comb, list(filter(set(perm).__contains__, cwr))) # comb: cwr that is a perm
self.assertEqual(comb, sorted(set(cwr) & set(perm))) # comb: both a cwr and a perm
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_compress(self):
self.assertEqual(list(compress(data='ABCDEF', selectors=[1,0,1,0,1,1])), list('ACEF'))
@@ -601,6 +603,7 @@ class TestBasicOps(unittest.TestCase):
next(testIntermediate)
self.assertEqual(list(op(testIntermediate)), list(result2))
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_count(self):
self.assertEqual(lzip('abc',count()), [('a', 0), ('b', 1), ('c', 2)])
@@ -1035,6 +1038,7 @@ class TestBasicOps(unittest.TestCase):
c = filter(isEven, range(6))
self.pickletest(proto, c)
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_filterfalse(self):
self.assertEqual(list(filterfalse(isEven, range(6))), [1,3,5])
@@ -1142,6 +1146,7 @@ class TestBasicOps(unittest.TestCase):
ids = list(map(id, list(zip_longest('abc', 'def'))))
self.assertEqual(len(dict.fromkeys(ids)), len(ids))
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_zip_longest_pickling(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
@@ -1365,6 +1370,7 @@ class TestBasicOps(unittest.TestCase):
self.assertEqual(len(set(map(id, product('abc', 'def')))), 1)
self.assertNotEqual(len(set(map(id, list(product('abc', 'def'))))), 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_product_pickling(self):
# check copy, deepcopy, pickle
@@ -1393,6 +1399,7 @@ class TestBasicOps(unittest.TestCase):
p.__setstate__((0, 0, 0x1000)) # will access tuple element 1 if not clamped
self.assertRaises(StopIteration, next, p)
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_repeat(self):
self.assertEqual(list(repeat(object='a', times=3)), ['a', 'a', 'a'])
@@ -1458,6 +1465,7 @@ class TestBasicOps(unittest.TestCase):
c = map(tupleize, 'abc', count())
self.pickletest(proto, c)
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_starmap(self):
self.assertEqual(list(starmap(operator.pow, zip(range(3), range(1,7)))),
@@ -1582,6 +1590,7 @@ class TestBasicOps(unittest.TestCase):
self.assertEqual(list(islice(range(100), IntLike(10), IntLike(50), IntLike(5))),
list(range(10,50,5)))
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_takewhile(self):
data = [1, 3, 5, 20, 2, 4, 6, 8]
@@ -1941,6 +1950,7 @@ class TestExamples(unittest.TestCase):
def test_accumulate(self):
self.assertEqual(list(accumulate([1,2,3,4,5])), [1, 3, 6, 10, 15])
@unittest.expectedFailure # TODO: RUSTPYTHON
@pickle_deprecated
def test_accumulate_reducible(self):
# check copy, deepcopy, pickle
-12
View File
@@ -745,9 +745,6 @@ class CBytesIOTest(PyBytesIOTest):
def test_issue5449(self):
super().test_issue5449()
def test_pickling(self):
super().test_pickling()
def test_read(self):
super().test_read()
@@ -907,9 +904,6 @@ class CStringIOTest(PyStringIOTest):
def test_newlines_property(self):
super().test_newlines_property()
def test_pickling(self):
super().test_pickling()
def test_read(self):
super().test_read()
@@ -998,17 +992,11 @@ class CStringIOPickleTest(PyStringIOPickleTest):
def test_newline_crlf(self):
super().test_newline_crlf()
def test_newline_default(self):
super().test_newline_default()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_newline_empty(self):
super().test_newline_empty()
def test_newline_lf(self):
super().test_newline_lf()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_newline_none(self):
+5 -3
View File
@@ -293,8 +293,6 @@ a = A(destroyed)"""
melon = Descr()
self.assertRaises(RuntimeError, getattr, M("mymod"), "melon")
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_lazy_create_annotations(self):
# module objects lazy create their __annotations__ dict on demand.
# the annotations dict is stored in module.__dict__.
@@ -334,7 +332,11 @@ a = A(destroyed)"""
del foo.__annotations__
def test_annotations_are_created_correctly(self):
ann_module4 = import_helper.import_fresh_module('test.typinganndata.ann_module4')
ann_module4 = import_helper.import_fresh_module(
'test.typinganndata.ann_module4',
)
self.assertFalse("__annotations__" in ann_module4.__dict__)
self.assertEqual(ann_module4.__annotations__, {"a": int, "b": str})
self.assertTrue("__annotations__" in ann_module4.__dict__)
del ann_module4.__annotations__
self.assertFalse("__annotations__" in ann_module4.__dict__)
+4
View File
@@ -36,11 +36,15 @@ class OpcodeTest(unittest.TestCase):
class C: pass
self.assertEqual(C.__annotations__, {})
# TODO: RustPython - test expectation changed in 3.14 due to PEP 649
@unittest.expectedFailure
def test_use_existing_annotations(self):
ns = {'__annotations__': {1: 2}}
exec('x: int', ns)
self.assertEqual(ns['__annotations__'], {'x': int, 1: 2})
# TODO: RustPython - test expectation changed in 3.14 due to PEP 649
@unittest.expectedFailure
def test_do_not_recreate_annotations(self):
# Don't rely on the existence of the '__annotations__' global.
with support.swap_item(globals(), '__annotations__', {}):
-4
View File
@@ -131,8 +131,6 @@ class PyPicklerTests(AbstractPickleTests, unittest.TestCase):
def test_c_methods(self): # TODO(RUSTPYTHON): Remove this test when it passes
return super().test_c_methods()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_buffers_error(self): # TODO(RUSTPYTHON): Remove this test when it passes
@@ -227,7 +225,6 @@ class InMemoryPickleTests(AbstractPickleTests, AbstractUnpickleTests,
def test_c_methods(self): # TODO(RUSTPYTHON): Remove this test when it passes
return super().test_c_methods()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_badly_escaped_string(self): # TODO(RUSTPYTHON): Remove this test when it passes
@@ -243,7 +240,6 @@ class InMemoryPickleTests(AbstractPickleTests, AbstractUnpickleTests,
def test_load_python2_str_as_bytes(self): # TODO(RUSTPYTHON): Remove this test when it passes
return super().test_load_python2_str_as_bytes()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_oob_buffers_writable_to_readonly(self): # TODO(RUSTPYTHON): Remove this test when it passes
-2
View File
@@ -97,8 +97,6 @@ class OptimizedPickleTests(AbstractPickleTests, unittest.TestCase):
def test_oob_buffers_writable_to_readonly(self): # TODO(RUSTPYTHON): Remove this test when it passes
return super().test_oob_buffers_writable_to_readonly()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_c_methods(self): # TODO(RUSTPYTHON): Remove this test when it passes
+22 -9
View File
@@ -8,6 +8,7 @@ import unittest
import test.support
from test import support
from test.support.script_helper import assert_python_ok
from test.support import import_helper
from test.support import os_helper
from test.support import socket_helper
from test.support import captured_stderr
@@ -308,8 +309,7 @@ class HelperFunctionsTests(unittest.TestCase):
with EnvironmentVarGuard() as environ:
environ['PYTHONUSERBASE'] = 'xoxo'
self.assertTrue(site.getuserbase().startswith('xoxo'),
site.getuserbase())
self.assertTrue(site.getuserbase().startswith('xoxo'))
@unittest.skipUnless(HAS_USER_SITE, 'need user site')
def test_getusersitepackages(self):
@@ -319,7 +319,7 @@ class HelperFunctionsTests(unittest.TestCase):
# the call sets USER_BASE *and* USER_SITE
self.assertEqual(site.USER_SITE, user_site)
self.assertTrue(user_site.startswith(site.USER_BASE), user_site)
self.assertTrue(user_site.startswith(site.USER_BASE))
self.assertEqual(site.USER_BASE, site.getuserbase())
def test_getsitepackages(self):
@@ -362,11 +362,10 @@ class HelperFunctionsTests(unittest.TestCase):
environ.unset('PYTHONUSERBASE', 'APPDATA')
user_base = site.getuserbase()
self.assertTrue(user_base.startswith('~' + os.sep),
user_base)
self.assertTrue(user_base.startswith('~' + os.sep))
user_site = site.getusersitepackages()
self.assertTrue(user_site.startswith(user_base), user_site)
self.assertTrue(user_site.startswith(user_base))
with mock.patch('os.path.isdir', return_value=False) as mock_isdir, \
mock.patch.object(site, 'addsitedir') as mock_addsitedir, \
@@ -515,7 +514,7 @@ class ImportSideEffectTests(unittest.TestCase):
# If sitecustomize is available, it should have been imported.
if "sitecustomize" not in sys.modules:
try:
import sitecustomize
import sitecustomize # noqa: F401
except ImportError:
pass
else:
@@ -578,6 +577,17 @@ class ImportSideEffectTests(unittest.TestCase):
code = e.code
self.assertEqual(code, 200, msg="Can't find " + url)
@support.cpython_only
def test_lazy_imports(self):
import_helper.ensure_lazy_imports("site", [
"io",
"locale",
"traceback",
"atexit",
"warnings",
"textwrap",
])
class StartupImportTests(unittest.TestCase):
@@ -843,12 +853,15 @@ class CommandLineTests(unittest.TestCase):
return 10, None
def invoke_command_line(self, *args):
args = ["-m", "site", *args]
cmd_args = []
if sys.flags.no_user_site:
cmd_args.append("-s")
cmd_args.extend(["-m", "site", *args])
with EnvironmentVarGuard() as env:
env["PYTHONUTF8"] = "1"
env["PYTHONIOENCODING"] = "utf-8"
proc = spawn_python(*args, text=True, env=env,
proc = spawn_python(*cmd_args, text=True, env=env,
encoding='utf-8', errors='replace')
output = kill_python(proc)
+2
View File
@@ -3803,6 +3803,7 @@ class ProtocolTests(BaseTestCase):
self.assertNotIsInstance(B(), P)
self.assertNotIsInstance(C(), P)
@unittest.expectedFailure # TODO: RUSTPYTHON; test doesn't include PEP 649 attrs
def test_non_protocol_subclasses(self):
class P(Protocol):
x = 1
@@ -6868,6 +6869,7 @@ class GetTypeHintTests(BaseTestCase):
'default_b': Optional[mod_generics_cache.B]}
self.assertEqual(gth(mod_generics_cache), mgc_hints)
@unittest.expectedFailure # TODO: RUSTPYTHON; test expects outdated result
def test_get_type_hints_classes(self):
self.assertEqual(gth(ann_module.C), # gth will find the right globalns
{'y': Optional[ann_module.C]})
-4
View File
@@ -8,8 +8,6 @@ Empty lines above are for good reason (testing for correct line numbers)
from typing import Optional
from functools import wraps
__annotations__[1] = 2
class C:
x = 5; y: Optional['C'] = None
@@ -18,8 +16,6 @@ from typing import Tuple
x: int = 5; y: str = x; f: Tuple[int, int]
class M(type):
__annotations__['123'] = 123
o: type = object
(pars): bool = True
+73 -16
View File
@@ -161,7 +161,17 @@ __all__ = [
]
def _type_convert(arg, module=None, *, allow_special_forms=False):
class _LazyAnnotationLib:
def __getattr__(self, attr):
global _lazy_annotationlib
import annotationlib
_lazy_annotationlib = annotationlib
return getattr(annotationlib, attr)
_lazy_annotationlib = _LazyAnnotationLib()
def _type_convert(arg, module=None, *, allow_special_forms=False, owner=None):
"""For converting None to type(None), and strings to ForwardRef."""
if arg is None:
return type(None)
@@ -170,7 +180,7 @@ def _type_convert(arg, module=None, *, allow_special_forms=False):
return arg
def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=False):
def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=False, owner=None):
"""Check that the argument is a type, and return it (internal helper).
As a special case, accept None and return type(None) instead. Also wrap strings
@@ -188,7 +198,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=
if is_argument:
invalid_generic_forms += (Final,)
arg = _type_convert(arg, module=module, allow_special_forms=allow_special_forms)
arg = _type_convert(arg, module=module, allow_special_forms=allow_special_forms, owner=owner)
if (isinstance(arg, _GenericAlias) and
arg.__origin__ in invalid_generic_forms):
raise TypeError(f"{arg} is not valid as type argument")
@@ -1918,6 +1928,7 @@ _SPECIAL_NAMES = frozenset({
'__init__', '__module__', '__new__', '__slots__',
'__subclasshook__', '__weakref__', '__class_getitem__',
'__match_args__', '__static_attributes__', '__firstlineno__',
'__annotate__', '__annotate_func__', '__annotations_cache__',
})
# These special attributes will be not collected as protocol members.
@@ -2442,7 +2453,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {})
else:
base_globals = globalns
ann = base.__dict__.get('__annotations__', {})
ann = _lazy_annotationlib.get_annotations(base)
if isinstance(ann, types.GetSetDescriptorType):
ann = {}
base_locals = dict(vars(base)) if localns is None else localns
@@ -2476,7 +2487,10 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
localns = globalns
elif localns is None:
localns = globalns
hints = getattr(obj, '__annotations__', None)
try:
hints = _lazy_annotationlib.get_annotations(obj)
except TypeError:
hints = getattr(obj, '__annotations__', None)
if hints is None:
# Return empty annotations for something that _could_ have them.
if isinstance(obj, _allowed_types):
@@ -2992,7 +3006,8 @@ _prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
'_fields', '_field_defaults',
'_make', '_replace', '_asdict', '_source'})
_special = frozenset({'__module__', '__name__', '__annotations__'})
_special = frozenset({'__module__', '__name__', '__annotations__', '__annotate__',
'__annotate_func__', '__annotations_cache__'})
class NamedTupleMeta(type):
@@ -3003,7 +3018,13 @@ class NamedTupleMeta(type):
raise TypeError(
'can only inherit from a NamedTuple type and Generic')
bases = tuple(tuple if base is _NamedTuple else base for base in bases)
types = ns.get('__annotations__', {})
if "__annotations__" in ns:
types = ns["__annotations__"]
elif (annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
types = _lazy_annotationlib.call_annotate_function(
annotate, _lazy_annotationlib.Format.VALUE)
else:
types = {}
default_names = []
for field_name in types:
if field_name in ns:
@@ -3158,16 +3179,26 @@ class _TypedDictMeta(type):
else:
generic_base = ()
ns_annotations = ns.pop('__annotations__', None)
tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict), ns)
if not hasattr(tp_dict, '__orig_bases__'):
tp_dict.__orig_bases__ = bases
annotations = {}
own_annotations = ns.get('__annotations__', {})
if ns_annotations is not None:
own_annotate = None
own_annotations = ns_annotations
elif (own_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
own_annotations = _lazy_annotationlib.call_annotate_function(
own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict
)
else:
own_annotate = None
own_annotations = {}
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
own_annotations = {
n: _type_check(tp, msg, module=tp_dict.__module__)
own_checked_annotations = {
n: _type_check(tp, msg, owner=tp_dict, module=tp_dict.__module__)
for n, tp in own_annotations.items()
}
required_keys = set()
@@ -3176,8 +3207,6 @@ class _TypedDictMeta(type):
mutable_keys = set()
for base in bases:
annotations.update(base.__dict__.get('__annotations__', {}))
base_required = base.__dict__.get('__required_keys__', set())
required_keys |= base_required
optional_keys -= base_required
@@ -3189,8 +3218,7 @@ class _TypedDictMeta(type):
readonly_keys.update(base.__dict__.get('__readonly_keys__', ()))
mutable_keys.update(base.__dict__.get('__mutable_keys__', ()))
annotations.update(own_annotations)
for annotation_key, annotation_type in own_annotations.items():
for annotation_key, annotation_type in own_checked_annotations.items():
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
if Required in qualifiers:
is_required = True
@@ -3221,7 +3249,36 @@ class _TypedDictMeta(type):
f"Required keys overlap with optional keys in {name}:"
f" {required_keys=}, {optional_keys=}"
)
tp_dict.__annotations__ = annotations
def __annotate__(format):
annos = {}
for base in bases:
if base is Generic:
continue
base_annotate = base.__annotate__
if base_annotate is None:
continue
base_annos = _lazy_annotationlib.call_annotate_function(
base_annotate, format, owner=base)
annos.update(base_annos)
if own_annotate is not None:
own = _lazy_annotationlib.call_annotate_function(
own_annotate, format, owner=tp_dict)
if format != _lazy_annotationlib.Format.STRING:
own = {
n: _type_check(tp, msg, module=tp_dict.__module__)
for n, tp in own.items()
}
elif format == _lazy_annotationlib.Format.STRING:
own = _lazy_annotationlib.annotations_to_string(own_annotations)
elif format in (_lazy_annotationlib.Format.FORWARDREF, _lazy_annotationlib.Format.VALUE):
own = own_checked_annotations
else:
raise NotImplementedError(format)
annos.update(own)
return annos
tp_dict.__annotate__ = __annotate__
tp_dict.__required_keys__ = frozenset(required_keys)
tp_dict.__optional_keys__ = frozenset(optional_keys)
tp_dict.__readonly_keys__ = frozenset(readonly_keys)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -2,7 +2,7 @@
# [RustPython](https://rustpython.github.io/)
A Python-3 (CPython >= 3.13.0) Interpreter written in Rust :snake: :scream:
A Python-3 (CPython >= 3.14.0) Interpreter written in Rust :snake: :scream:
:metal:.
[![Build Status](https://github.com/RustPython/RustPython/workflows/CI/badge.svg)](https://github.com/RustPython/RustPython/actions?query=workflow%3ACI)
+558 -77
View File
@@ -29,7 +29,7 @@ use ruff_python_ast::{
InterpolatedStringElements, Keyword, MatchCase, ModExpression, ModModule, Operator, Parameters,
Pattern, PatternMatchAs, PatternMatchClass, PatternMatchMapping, PatternMatchOr,
PatternMatchSequence, PatternMatchSingleton, PatternMatchStar, PatternMatchValue, Singleton,
Stmt, StmtExpr, TString, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
Stmt, StmtAnnAssign, StmtExpr, TString, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem,
visitor::{Visitor, walk_expr},
};
@@ -130,6 +130,10 @@ struct Compiler {
ctx: CompileContext,
opts: CompileOpts,
in_annotation: bool,
// PEP 649: Track if we're inside a conditional block (if/for/while/etc.)
in_conditional_block: bool,
// PEP 649: Next index for conditional annotation tracking
next_conditional_annotation_index: u32,
}
enum DoneWithFuture {
@@ -437,6 +441,8 @@ impl Compiler {
},
opts,
in_annotation: false,
in_conditional_block: false,
next_conditional_annotation_index: 0,
}
}
@@ -676,6 +682,62 @@ impl Compiler {
Ok(self.current_symbol_table())
}
/// Push the annotation symbol table from the next sub_table's annotation_block
/// The annotation_block is stored in the function's scope, which is the next sub_table
/// Returns true if annotation_block exists, false otherwise
fn push_annotation_symbol_table(&mut self) -> bool {
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
// The annotation_block is in the next sub_table (function scope)
let next_idx = current_table.next_sub_table;
if next_idx >= current_table.sub_tables.len() {
return false;
}
let next_table = &mut current_table.sub_tables[next_idx];
if let Some(annotation_block) = next_table.annotation_block.take() {
self.symbol_table_stack.push(*annotation_block);
true
} else {
false
}
}
/// Push the annotation symbol table for module/class level annotations
/// This takes annotation_block from the current symbol table (not sub_tables)
fn push_current_annotation_symbol_table(&mut self) -> bool {
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
// For modules/classes, annotation_block is directly in the current table
if let Some(annotation_block) = current_table.annotation_block.take() {
self.symbol_table_stack.push(*annotation_block);
true
} else {
false
}
}
/// Pop the annotation symbol table and restore it to the function scope's annotation_block
fn pop_annotation_symbol_table(&mut self) {
let annotation_table = self.symbol_table_stack.pop().expect("compiler bug");
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
// Restore to the next sub_table (function scope) where it came from
let next_idx = current_table.next_sub_table;
if next_idx < current_table.sub_tables.len() {
current_table.sub_tables[next_idx].annotation_block = Some(Box::new(annotation_table));
}
}
/// Pop the current symbol table off the stack
fn pop_symbol_table(&mut self) -> SymbolTable {
self.symbol_table_stack.pop().expect("compiler bug")
@@ -896,6 +958,12 @@ impl Compiler {
cellvar_cache.insert("__classdict__".to_string());
}
// Handle implicit __conditional_annotations__ cell if needed
// Only for class scope - module scope uses NAME operations, not DEREF
if ste.has_conditional_annotations && scope_type == CompilerScope::Class {
cellvar_cache.insert("__conditional_annotations__".to_string());
}
// Build freevars using dictbytype (FREE scope, offset by cellvars size)
let mut freevar_cache = IndexSet::default();
let mut free_names: Vec<_> = ste
@@ -933,6 +1001,12 @@ impl Compiler {
0,
0,
),
CompilerScope::Annotation => (
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
0,
1, // annotation scope takes one argument (format)
0,
),
};
// Get private name from parent scope
@@ -1060,6 +1134,81 @@ impl Compiler {
unwrap_internal(self, stack_top.finalize_code(&self.opts))
}
/// Exit annotation scope - similar to exit_scope but restores annotation_block to parent
fn exit_annotation_scope(&mut self) -> CodeObject {
self.pop_annotation_symbol_table();
let pop = self.code_stack.pop();
let stack_top = compiler_unwrap_option(self, pop);
unwrap_internal(self, stack_top.finalize_code(&self.opts))
}
/// Enter annotation scope using the symbol table's annotation_block
/// Returns false if no annotation_block exists
fn enter_annotation_scope(&mut self, func_name: &str) -> CompileResult<bool> {
if !self.push_annotation_symbol_table() {
return Ok(false);
}
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get();
let annotate_name = format!("<annotate of {func_name}>");
self.enter_scope(
&annotate_name,
CompilerScope::Annotation,
key,
lineno.to_u32(),
)?;
// Override arg_count since enter_scope sets it to 1 but we need the varnames
// setup to be correct too
self.current_code_info()
.metadata
.varnames
.insert("format".to_owned());
// Emit format validation: if format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
// VALUE_WITH_FAKE_GLOBALS = 2 (from annotationlib.Format)
self.emit_format_validation()?;
Ok(true)
}
/// Emit format parameter validation for annotation scope
/// if format > VALUE_WITH_FAKE_GLOBALS (2): raise NotImplementedError
fn emit_format_validation(&mut self) -> CompileResult<()> {
use bytecode::ComparisonOperator::Greater;
// Load format parameter (first local variable, index 0)
emit!(self, Instruction::LoadFast(0));
// Load VALUE_WITH_FAKE_GLOBALS constant (2)
self.emit_load_const(ConstantData::Integer { value: 2.into() });
// Compare: format > 2
emit!(self, Instruction::CompareOp { op: Greater });
// Jump to body if format <= 2 (comparison is false)
let body_block = self.new_block();
emit!(self, Instruction::PopJumpIfFalse { target: body_block });
// Raise NotImplementedError
let not_implemented_error = self.name("NotImplementedError");
emit!(self, Instruction::LoadGlobal(not_implemented_error));
emit!(
self,
Instruction::RaiseVarargs {
kind: bytecode::RaiseKind::Raise
}
);
// Body label - continue with annotation evaluation
self.switch_to_block(body_block);
Ok(())
}
/// Push a new fblock
// = compiler_push_fblock
fn push_fblock(
@@ -1495,6 +1644,8 @@ impl Compiler {
symbol_table: SymbolTable,
) -> CompileResult<()> {
let size_before = self.code_stack.len();
// Set future_annotations from symbol table (detected during symbol table scan)
self.future_annotations = symbol_table.future_annotations;
self.symbol_table_stack.push(symbol_table);
let (doc, statements) = split_doc(&body.body, &self.opts);
@@ -1506,10 +1657,24 @@ impl Compiler {
emit!(self, Instruction::StoreGlobal(doc))
}
// Handle annotations based on future_annotations flag
if Self::find_ann(statements) {
emit!(self, Instruction::SetupAnnotations);
if self.future_annotations {
// PEP 563: Initialize __annotations__ dict
emit!(self, Instruction::SetupAnnotations);
} else {
// PEP 649: Generate __annotate__ function FIRST (before statements)
self.compile_module_annotate(statements)?;
// PEP 649: Initialize __conditional_annotations__ set after __annotate__
if self.current_symbol_table().has_conditional_annotations {
emit!(self, Instruction::BuildSet { size: 0 });
self.store_name("__conditional_annotations__")?;
}
}
}
// Compile all statements
self.compile_statements(statements)?;
assert_eq!(self.code_stack.len(), size_before);
@@ -1524,10 +1689,25 @@ impl Compiler {
body: &[Stmt],
symbol_table: SymbolTable,
) -> CompileResult<()> {
// Set future_annotations from symbol table (detected during symbol table scan)
self.future_annotations = symbol_table.future_annotations;
self.symbol_table_stack.push(symbol_table);
// Handle annotations based on future_annotations flag
if Self::find_ann(body) {
emit!(self, Instruction::SetupAnnotations);
if self.future_annotations {
// PEP 563: Initialize __annotations__ dict
emit!(self, Instruction::SetupAnnotations);
} else {
// PEP 649: Generate __annotate__ function FIRST (before statements)
self.compile_module_annotate(body)?;
// PEP 649: Initialize __conditional_annotations__ set after __annotate__
if self.current_symbol_table().has_conditional_annotations {
emit!(self, Instruction::BuildSet { size: 0 });
self.store_name("__conditional_annotations__")?;
}
}
}
if let Some((last, body)) = body.split_last() {
@@ -1650,6 +1830,7 @@ impl Compiler {
Global,
Deref,
Name,
DictOrGlobals, // PEP 649: can_see_class_scope
}
let name = self.mangle(name);
@@ -1666,16 +1847,18 @@ impl Compiler {
// Determine the operation type based on symbol scope
let is_function_like = self.ctx.in_func();
// Look up the symbol, handling TypeParams scope specially
let (symbol_scope, _is_typeparams) = {
// Look up the symbol, handling TypeParams and Annotation scopes specially
let (symbol_scope, can_see_class_scope) = {
let current_table = self.current_symbol_table();
let is_typeparams = current_table.typ == CompilerScope::TypeParams;
let is_annotation = current_table.typ == CompilerScope::Annotation;
let can_see_class = current_table.can_see_class_scope;
// First try to find in current table
let symbol = current_table.lookup(name.as_ref());
// If not found and we're in TypeParams scope, try parent scope
let symbol = if symbol.is_none() && is_typeparams {
// If not found and we're in TypeParams or Annotation scope, try parent scope
let symbol = if symbol.is_none() && (is_typeparams || is_annotation) {
self.symbol_table_stack
.get(self.symbol_table_stack.len() - 2) // Try to get parent index
.expect("Symbol has no parent! This is a compiler bug.")
@@ -1684,14 +1867,46 @@ impl Compiler {
symbol
};
(symbol.map(|s| s.scope), is_typeparams)
(symbol.map(|s| s.scope), can_see_class)
};
let actual_scope = symbol_scope.ok_or_else(|| {
self.error(CodegenErrorType::SyntaxError(format!(
"The symbol '{name}' must be present in the symbol table"
)))
})?;
// Special handling for class scope implicit cell variables
// These are treated as Cell even if not explicitly marked in symbol table
// Only for LOAD operations - explicit stores like `__class__ = property(...)`
// should use STORE_NAME to store in class namespace dict
let symbol_scope = {
let current_table = self.current_symbol_table();
if current_table.typ == CompilerScope::Class
&& usage == NameUsage::Load
&& (name == "__class__"
|| name == "__classdict__"
|| name == "__conditional_annotations__")
{
Some(SymbolScope::Cell)
} else {
symbol_scope
}
};
// In annotation or type params scope, missing symbols are treated as global implicit
// This allows referencing global names like Union, Optional, etc. that are imported
// at module level but not explicitly bound in the function scope
let actual_scope = match symbol_scope {
Some(scope) => scope,
None => {
let current_table = self.current_symbol_table();
if matches!(
current_table.typ,
CompilerScope::Annotation | CompilerScope::TypeParams
) {
SymbolScope::GlobalImplicit
} else {
return Err(self.error(CodegenErrorType::SyntaxError(format!(
"the symbol '{name}' must be present in the symbol table"
))));
}
}
};
// Determine operation type based on scope
let op_type = match actual_scope {
@@ -1705,7 +1920,11 @@ impl Compiler {
}
}
SymbolScope::GlobalImplicit => {
if is_function_like {
// PEP 649: In annotation scope with class visibility, use DictOrGlobals
// to check classdict first before globals
if can_see_class_scope {
NameOp::DictOrGlobals
} else if is_function_like {
NameOp::Global
} else {
NameOp::Name
@@ -1765,6 +1984,25 @@ impl Compiler {
};
self.emit_arg(idx, op);
}
NameOp::DictOrGlobals => {
// PEP 649: First check classdict (from __classdict__ freevar), then globals
let idx = self.get_global_name_index(&name);
match usage {
NameUsage::Load => {
// Load __classdict__ first (it's a free variable in annotation scope)
let classdict_idx = self.get_free_var_index("__classdict__")?;
self.emit_arg(classdict_idx, Instruction::LoadDeref);
self.emit_arg(idx, Instruction::LoadFromDictOrGlobals);
}
// Store/Delete in annotation scope should use Name ops
NameUsage::Store => {
self.emit_arg(idx, Instruction::StoreName);
}
NameUsage::Delete => {
self.emit_arg(idx, Instruction::DeleteName);
}
}
}
}
Ok(())
@@ -2117,8 +2355,9 @@ impl Compiler {
target,
annotation,
value,
simple,
..
}) => self.compile_annotated_assign(target, annotation, value.as_deref())?,
}) => self.compile_annotated_assign(target, annotation, value.as_deref(), *simple)?,
Stmt::Delete(StmtDelete { targets, .. }) => {
for target in targets {
self.compile_delete(target)?;
@@ -3306,12 +3545,19 @@ impl Compiler {
// Set qualname
self.set_qualname();
// Handle docstring
// Handle docstring - store in co_consts[0] if present
let (doc_str, body) = split_doc(body, &self.opts);
self.current_code_info()
.metadata
.consts
.insert_full(ConstantData::None);
if let Some(doc) = &doc_str {
// Docstring present: store in co_consts[0] and set HAS_DOCSTRING flag
self.current_code_info()
.metadata
.consts
.insert_full(ConstantData::Str {
value: doc.to_string().into(),
});
self.current_code_info().flags |= bytecode::CodeFlags::HAS_DOCSTRING;
}
// If no docstring, don't add None to co_consts
// Compile body statements
self.compile_statements(body)?;
@@ -3331,30 +3577,41 @@ impl Compiler {
// Create function object with closure
self.make_closure(code, funcflags)?;
// Handle docstring if present
if let Some(doc) = doc_str {
emit!(self, Instruction::Copy { index: 1_u32 });
self.emit_load_const(ConstantData::Str {
value: doc.to_string().into(),
});
emit!(self, Instruction::Swap { index: 2 });
let doc_attr = self.name("__doc__");
emit!(self, Instruction::StoreAttr { idx: doc_attr });
}
// Note: docstring is now retrieved from co_consts[0] by the VM
// when HAS_DOCSTRING flag is set, so no runtime __doc__ assignment needed
Ok(())
}
/// Compile function annotations
// = compiler_visit_annotations
fn visit_annotations(
/// Compile function annotations as a closure (PEP 649)
/// Returns true if an __annotate__ closure was created
/// Uses symbol table's annotation_block for proper scoping.
fn compile_annotations_closure(
&mut self,
func_name: &str,
parameters: &Parameters,
returns: Option<&Expr>,
) -> CompileResult<u32> {
let mut num_annotations = 0;
) -> CompileResult<bool> {
// Try to enter annotation scope - returns false if no annotation_block exists
if !self.enter_annotation_scope(func_name)? {
return Ok(false);
}
// Handle parameter annotations
// Count annotations
let parameters_iter = core::iter::empty()
.chain(&parameters.posonlyargs)
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.map(|x| &x.parameter)
.chain(parameters.vararg.as_deref())
.chain(parameters.kwarg.as_deref());
let num_annotations: u32 =
u32::try_from(parameters_iter.filter(|p| p.annotation.is_some()).count())
.expect("too many annotations")
+ if returns.is_some() { 1 } else { 0 };
// Compile annotations inside the annotation scope
let parameters_iter = core::iter::empty()
.chain(&parameters.posonlyargs)
.chain(&parameters.args)
@@ -3369,20 +3626,193 @@ impl Compiler {
value: self.mangle(param.name.as_str()).into_owned().into(),
});
self.compile_annotation(annotation)?;
num_annotations += 1;
}
}
// Handle return annotation last
// Handle return annotation
if let Some(annotation) = returns {
self.emit_load_const(ConstantData::Str {
value: "return".into(),
});
self.compile_annotation(annotation)?;
num_annotations += 1;
}
Ok(num_annotations)
// Build the map and return it
emit!(
self,
Instruction::BuildMap {
size: num_annotations,
}
);
emit!(self, Instruction::ReturnValue);
// Exit the annotation scope and get the code object
let annotate_code = self.exit_annotation_scope();
// Make a closure from the code object
self.make_closure(annotate_code, bytecode::MakeFunctionFlags::empty())?;
Ok(true)
}
/// Collect simple (non-conditional) annotations from module body
/// Returns list of (name, annotation_expr) pairs
fn collect_simple_annotations(body: &[Stmt]) -> Vec<(&str, &Expr)> {
let mut annotations = Vec::new();
for stmt in body {
if let Stmt::AnnAssign(StmtAnnAssign {
target,
annotation,
simple,
..
}) = stmt
&& *simple
&& let Expr::Name(ExprName { id, .. }) = target.as_ref()
{
annotations.push((id.as_str(), annotation.as_ref()));
}
}
annotations
}
/// Compile module-level __annotate__ function (PEP 649)
/// Returns true if __annotate__ was created and stored
fn compile_module_annotate(&mut self, body: &[Stmt]) -> CompileResult<bool> {
// Collect simple annotations from module body first
let annotations = Self::collect_simple_annotations(body);
if annotations.is_empty() {
return Ok(false);
}
// Check if we have conditional annotations
let has_conditional = self.current_symbol_table().has_conditional_annotations;
// Get parent scope type and name BEFORE pushing annotation symbol table
let parent_scope_type = self.current_symbol_table().typ;
let parent_name = self
.symbol_table_stack
.last()
.map(|t| t.name.as_str())
.unwrap_or("module")
.to_owned();
let scope_name = format!("<annotate of {parent_name}>");
// Try to push annotation symbol table from current scope
if !self.push_current_annotation_symbol_table() {
return Ok(false);
}
// Enter annotation scope for code generation
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get();
self.enter_scope(&scope_name, CompilerScope::Annotation, key, lineno.to_u32())?;
// Add 'format' parameter to varnames
self.current_code_info()
.metadata
.varnames
.insert("format".to_owned());
// Emit format validation: if format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
self.emit_format_validation()?;
if has_conditional {
// PEP 649: Build dict incrementally, checking conditional annotations
// Start with empty dict
emit!(self, Instruction::BuildMap { size: 0 });
// Process each annotation
for (idx, (name, annotation)) in annotations.iter().enumerate() {
// Check if index is in __conditional_annotations__
let not_set_block = self.new_block();
// LOAD_CONST index
self.emit_load_const(ConstantData::Integer { value: idx.into() });
// Load __conditional_annotations__ from appropriate scope
// Class scope: LoadDeref (freevars), Module scope: LoadGlobal
if parent_scope_type == CompilerScope::Class {
let idx = self.get_free_var_index("__conditional_annotations__")?;
emit!(self, Instruction::LoadDeref(idx));
} else {
let cond_annotations_name = self.name("__conditional_annotations__");
emit!(self, Instruction::LoadGlobal(cond_annotations_name));
}
// CONTAINS_OP (in)
emit!(self, Instruction::ContainsOp(bytecode::Invert::No));
// POP_JUMP_IF_FALSE not_set
emit!(
self,
Instruction::PopJumpIfFalse {
target: not_set_block
}
);
// Annotation value
self.compile_annotation(annotation)?;
// COPY dict to TOS
emit!(self, Instruction::Copy { index: 2 });
// LOAD_CONST name
self.emit_load_const(ConstantData::Str {
value: self.mangle(name).into_owned().into(),
});
// STORE_SUBSCR - dict[name] = value
emit!(self, Instruction::StoreSubscr);
// not_set label
self.switch_to_block(not_set_block);
}
// Return the dict
emit!(self, Instruction::ReturnValue);
} else {
// No conditional annotations - use simple BuildMap
let num_annotations = u32::try_from(annotations.len()).expect("too many annotations");
// Compile annotations inside the annotation scope
for (name, annotation) in annotations {
self.emit_load_const(ConstantData::Str {
value: self.mangle(name).into_owned().into(),
});
self.compile_annotation(annotation)?;
}
// Build the map and return it
emit!(
self,
Instruction::BuildMap {
size: num_annotations,
}
);
emit!(self, Instruction::ReturnValue);
}
// Exit annotation scope - pop symbol table, restore to parent's annotation_block, and get code
let annotation_table = self.pop_symbol_table();
// Restore annotation_block to module's symbol table
self.symbol_table_stack
.last_mut()
.expect("no module symbol table")
.annotation_block = Some(Box::new(annotation_table));
// Exit code scope
let pop = self.code_stack.pop();
let annotate_code = unwrap_internal(
self,
compiler_unwrap_option(self, pop).finalize_code(&self.opts),
);
// Make a closure from the code object
self.make_closure(annotate_code, bytecode::MakeFunctionFlags::empty())?;
// Store as __annotate_func__ for classes, __annotate__ for modules
let name = if parent_scope_type == CompilerScope::Class {
"__annotate_func__"
} else {
"__annotate__"
};
self.store_name(name)?;
Ok(true)
}
// = compiler_function
@@ -3449,18 +3879,12 @@ impl Compiler {
}
}
// Compile annotations
let mut annotations_flag = bytecode::MakeFunctionFlags::empty();
let num_annotations = self.visit_annotations(parameters, returns)?;
if num_annotations > 0 {
annotations_flag = bytecode::MakeFunctionFlags::ANNOTATIONS;
emit!(
self,
Instruction::BuildMap {
size: num_annotations,
}
);
}
// Compile annotations as closure (PEP 649)
let annotations_flag = if self.compile_annotations_closure(name, parameters, returns)? {
bytecode::MakeFunctionFlags::ANNOTATE
} else {
bytecode::MakeFunctionFlags::empty()
};
// Compile function body
let final_funcflags = funcflags | annotations_flag;
@@ -3543,10 +3967,14 @@ impl Compiler {
fn get_ref_type(&self, name: &str) -> Result<SymbolScope, CodegenErrorType> {
let table = self.symbol_table_stack.last().unwrap();
// Special handling for __class__ and __classdict__ in class scope
// Special handling for __class__, __classdict__, and __conditional_annotations__ in class scope
// This should only apply when we're actually IN a class body,
// not when we're in a method nested inside a class.
if table.typ == CompilerScope::Class && (name == "__class__" || name == "__classdict__") {
if table.typ == CompilerScope::Class
&& (name == "__class__"
|| name == "__classdict__"
|| name == "__conditional_annotations__")
{
return Ok(SymbolScope::Cell);
}
match table.lookup(name) {
@@ -3669,6 +4097,16 @@ impl Compiler {
);
}
// Set __annotate__ closure if present (PEP 649)
if flags.contains(bytecode::MakeFunctionFlags::ANNOTATE) {
emit!(
self,
Instruction::SetFunctionAttribute {
attr: bytecode::MakeFunctionFlags::ANNOTATE
}
);
}
// Set kwdefaults if present
if flags.contains(bytecode::MakeFunctionFlags::KW_ONLY_DEFAULTS) {
emit!(
@@ -3799,9 +4237,31 @@ impl Compiler {
emit!(self, Instruction::StoreName(dunder_type_params));
}
// Setup annotations if needed
// PEP 649: Initialize __classdict__ cell for class annotation scope
if self.current_symbol_table().needs_classdict {
let locals_name = self.name("locals");
emit!(self, Instruction::LoadName(locals_name));
emit!(self, Instruction::PushNull);
emit!(self, Instruction::Call { nargs: 0 });
let classdict_idx = self.get_cell_var_index("__classdict__")?;
emit!(self, Instruction::StoreDeref(classdict_idx));
}
// Handle class annotations based on future_annotations flag
if Self::find_ann(body) {
emit!(self, Instruction::SetupAnnotations);
if self.future_annotations {
// PEP 563: Initialize __annotations__ dict for class
emit!(self, Instruction::SetupAnnotations);
} else {
// PEP 649: Initialize __conditional_annotations__ set if needed for class
if self.current_symbol_table().has_conditional_annotations {
emit!(self, Instruction::BuildSet { size: 0 });
self.store_name("__conditional_annotations__")?;
}
// PEP 649: Generate __annotate__ function for class annotations
self.compile_module_annotate(body)?;
}
}
// 3. Compile the class body
@@ -5457,31 +5917,55 @@ impl Compiler {
target: &Expr,
annotation: &Expr,
value: Option<&Expr>,
simple: bool,
) -> CompileResult<()> {
// Perform the actual assignment first
if let Some(value) = value {
self.compile_expression(value)?;
self.compile_store(target)?;
}
// Annotations are only evaluated in a module or class.
if self.ctx.in_func() {
return Ok(());
}
// If we have a simple name in module or class scope, store annotation
if simple
&& !self.ctx.in_func()
&& let Expr::Name(ExprName { id, .. }) = target
{
if self.future_annotations {
// PEP 563: Store stringified annotation directly to __annotations__
// Compile annotation as string
self.compile_annotation(annotation)?;
// Load __annotations__
let annotations_name = self.name("__annotations__");
emit!(self, Instruction::LoadName(annotations_name));
// Load the variable name
self.emit_load_const(ConstantData::Str {
value: self.mangle(id.as_str()).into_owned().into(),
});
// Store: __annotations__[name] = annotation
emit!(self, Instruction::StoreSubscr);
} else {
// PEP 649: Handle conditional annotations
if self.current_symbol_table().has_conditional_annotations {
// Determine if this annotation is conditional
let is_module = self.current_symbol_table().typ == CompilerScope::Module;
let is_conditional = is_module || self.in_conditional_block;
// Compile annotation:
self.compile_annotation(annotation)?;
if is_conditional {
// Get the current annotation index and increment
let annotation_index = self.next_conditional_annotation_index;
self.next_conditional_annotation_index += 1;
if let Expr::Name(ExprName { id, .. }) = &target {
// Store as dict entry in __annotations__ dict:
let annotations = self.name("__annotations__");
emit!(self, Instruction::LoadName(annotations));
self.emit_load_const(ConstantData::Str {
value: self.mangle(id.as_str()).into_owned().into(),
});
emit!(self, Instruction::StoreSubscr);
} else {
// Drop annotation if not assigned to simple identifier.
emit!(self, Instruction::PopTop);
// Add index to __conditional_annotations__ set
let cond_annotations_name = self.name("__conditional_annotations__");
emit!(self, Instruction::LoadName(cond_annotations_name));
self.emit_load_const(ConstantData::Integer {
value: annotation_index.into(),
});
emit!(self, Instruction::SetAdd { i: 0_u32 });
emit!(self, Instruction::PopTop);
}
}
}
}
Ok(())
@@ -6100,10 +6584,7 @@ impl Compiler {
in_async_scope: false,
};
self.current_code_info()
.metadata
.consts
.insert_full(ConstantData::None);
// Lambda cannot have docstrings, so no None is added to co_consts
self.compile_expression(body)?;
self.emit_return_value();
+353 -19
View File
@@ -63,6 +63,17 @@ pub struct SymbolTable {
/// Whether this comprehension scope should be inlined (PEP 709)
/// True for list/set/dict comprehensions in non-generator expressions
pub comp_inlined: bool,
/// PEP 649: Reference to annotation scope for this block
/// Annotations are compiled as a separate `__annotate__` function
pub annotation_block: Option<Box<SymbolTable>>,
/// PEP 649: Whether this scope has conditional annotations
/// (annotations inside if/for/while/etc. blocks or at module level)
pub has_conditional_annotations: bool,
/// Whether `from __future__ import annotations` is active
pub future_annotations: bool,
}
impl SymbolTable {
@@ -80,6 +91,9 @@ impl SymbolTable {
needs_classdict: false,
can_see_class_scope: false,
comp_inlined: false,
annotation_block: None,
has_conditional_annotations: false,
future_annotations: false,
}
}
@@ -109,6 +123,8 @@ pub enum CompilerScope {
Lambda,
Comprehension,
TypeParams,
/// PEP 649: Annotation scope for deferred evaluation
Annotation,
}
impl fmt::Display for CompilerScope {
@@ -121,9 +137,8 @@ impl fmt::Display for CompilerScope {
Self::Lambda => write!(f, "lambda"),
Self::Comprehension => write!(f, "comprehension"),
Self::TypeParams => write!(f, "type parameter"),
Self::Annotation => write!(f, "annotation"),
// TODO missing types from the C implementation
// if self._table.type == _symtable.TYPE_ANNOTATION:
// return "annotation"
// if self._table.type == _symtable.TYPE_TYPE_VAR_BOUND:
// return "TypeVar bound"
// if self._table.type == _symtable.TYPE_TYPE_ALIAS:
@@ -246,7 +261,8 @@ impl core::fmt::Debug for SymbolTable {
fn analyze_symbol_table(symbol_table: &mut SymbolTable) -> SymbolTableResult {
let mut analyzer = SymbolTableAnalyzer::default();
// Discard the newfree set at the top level - it's only needed for propagation
let _newfree = analyzer.analyze_symbol_table(symbol_table)?;
// Pass None for class_entry at top level
let _newfree = analyzer.analyze_symbol_table(symbol_table, None)?;
Ok(())
}
@@ -269,6 +285,12 @@ fn drop_class_free(symbol_table: &mut SymbolTable, newfree: &mut HashSet<String>
if newfree.remove("__classdict__") {
symbol_table.needs_classdict = true;
}
// Check if __conditional_annotations__ is in the free variables collected from children
// Remove it from free set - it's handled specially in class scope
if newfree.remove("__conditional_annotations__") {
symbol_table.has_conditional_annotations = true;
}
}
type SymbolMap = IndexMap<String, Symbol>;
@@ -339,9 +361,11 @@ struct SymbolTableAnalyzer {
impl SymbolTableAnalyzer {
/// Analyze a symbol table and return the set of free variables.
/// See symtable.c analyze_block().
/// class_entry: PEP 649 - enclosing class symbols for annotation scopes
fn analyze_symbol_table(
&mut self,
symbol_table: &mut SymbolTable,
class_entry: Option<&SymbolMap>,
) -> SymbolTableResult<HashSet<String>> {
let symbols = core::mem::take(&mut symbol_table.symbols);
let sub_tables = &mut *symbol_table.sub_tables;
@@ -349,15 +373,43 @@ impl SymbolTableAnalyzer {
// Collect free variables from all child scopes
let mut newfree = HashSet::new();
let annotation_block = &mut symbol_table.annotation_block;
// PEP 649: Determine class_entry to pass to children
// If current scope is a class with annotation block that can_see_class_scope,
// we need to pass class symbols to the annotation scope
let is_class = symbol_table.typ == CompilerScope::Class;
// Clone class symbols if needed for annotation scope (to avoid borrow conflict)
let class_symbols_for_ann = if is_class
&& annotation_block
.as_ref()
.is_some_and(|b| b.can_see_class_scope)
{
Some(symbols.clone())
} else {
None
};
let mut info = (symbols, symbol_table.typ);
self.tables.with_append(&mut info, |list| {
let inner_scope = unsafe { &mut *(list as *mut _ as *mut Self) };
// Analyze sub scopes and collect their free variables
for sub_table in sub_tables.iter_mut() {
let child_free = inner_scope.analyze_symbol_table(sub_table)?;
// Sub-scopes (functions, nested classes) don't inherit class_entry
let child_free = inner_scope.analyze_symbol_table(sub_table, None)?;
// Propagate child's free variables to this scope
newfree.extend(child_free);
}
// PEP 649: Analyze annotation block if present
if let Some(annotation_table) = annotation_block {
// Pass class symbols to annotation scope if can_see_class_scope
let ann_class_entry = class_symbols_for_ann.as_ref().or(class_entry);
let child_free =
inner_scope.analyze_symbol_table(annotation_table, ann_class_entry)?;
// Propagate annotation's free variables to this scope
newfree.extend(child_free);
}
Ok(())
})?;
@@ -396,7 +448,7 @@ impl SymbolTableAnalyzer {
// Analyze symbols in current scope
for symbol in symbol_table.symbols.values_mut() {
self.analyze_symbol(symbol, symbol_table.typ, sub_tables)?;
self.analyze_symbol(symbol, symbol_table.typ, sub_tables, class_entry)?;
// Collect free variables from this scope
// These will be propagated to the parent scope
@@ -420,6 +472,7 @@ impl SymbolTableAnalyzer {
symbol: &mut Symbol,
st_typ: CompilerScope,
sub_tables: &[SymbolTable],
class_entry: Option<&SymbolMap>,
) -> SymbolTableResult {
if symbol
.flags
@@ -439,7 +492,8 @@ impl SymbolTableAnalyzer {
// check if the name is already defined in any outer scope
// therefore
if scope_depth < 2
|| self.found_in_outer_scope(&symbol.name) != Some(SymbolScope::Free)
|| self.found_in_outer_scope(&symbol.name, st_typ)
!= Some(SymbolScope::Free)
{
return Err(SymbolTableError {
error: format!("no binding for nonlocal '{}' found", symbol.name),
@@ -465,11 +519,23 @@ impl SymbolTableAnalyzer {
// all is well
}
SymbolScope::Unknown => {
// PEP 649: Check class_entry first (like analyze_name)
// If name is bound in enclosing class, mark as GlobalImplicit
if let Some(class_symbols) = class_entry
&& let Some(class_sym) = class_symbols.get(&symbol.name)
{
// DEF_BOUND && !DEF_NONLOCAL -> GLOBAL_IMPLICIT
if class_sym.is_bound() && class_sym.scope != SymbolScope::Free {
symbol.scope = SymbolScope::GlobalImplicit;
return Ok(());
}
}
// Try hard to figure out what the scope of this symbol is.
let scope = if symbol.is_bound() {
self.found_in_inner_scope(sub_tables, &symbol.name, st_typ)
.unwrap_or(SymbolScope::Local)
} else if let Some(scope) = self.found_in_outer_scope(&symbol.name) {
} else if let Some(scope) = self.found_in_outer_scope(&symbol.name, st_typ) {
scope
} else if self.tables.is_empty() {
// Don't make assumptions when we don't know.
@@ -485,18 +551,40 @@ impl SymbolTableAnalyzer {
Ok(())
}
fn found_in_outer_scope(&mut self, name: &str) -> Option<SymbolScope> {
fn found_in_outer_scope(&mut self, name: &str, st_typ: CompilerScope) -> Option<SymbolScope> {
let mut decl_depth = None;
for (i, (symbols, typ)) in self.tables.iter().rev().enumerate() {
if matches!(typ, CompilerScope::Module)
|| matches!(typ, CompilerScope::Class if name != "__class__")
|| matches!(typ, CompilerScope::Class if name != "__class__" && name != "__classdict__" && name != "__conditional_annotations__")
{
continue;
}
// __class__ is implicitly declared in class scope
// This handles the case where super() is called in a nested class method
if name == "__class__" && matches!(typ, CompilerScope::Class) {
// PEP 649: Annotation scope is conceptually a sibling of the function,
// not a child. Skip the immediate parent function scope when looking
// for outer variables from annotation scope.
if st_typ == CompilerScope::Annotation
&& i == 0
&& matches!(
typ,
CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda
)
{
continue;
}
// __class__ and __classdict__ are implicitly declared in class scope
// This handles the case where nested scopes reference them
if (name == "__class__" || name == "__classdict__")
&& matches!(typ, CompilerScope::Class)
{
decl_depth = Some(i);
break;
}
// __conditional_annotations__ is implicitly declared in class scope
// for classes with conditional annotations
if name == "__conditional_annotations__" && matches!(typ, CompilerScope::Class) {
decl_depth = Some(i);
break;
}
@@ -657,6 +745,13 @@ impl SymbolTableAnalyzer {
location: None,
});
}
CompilerScope::Annotation => {
// Named expression is not allowed in annotation scope
return Err(SymbolTableError {
error: "named expression cannot be used within an annotation".to_string(),
location: None,
});
}
}
Ok(())
}
@@ -695,6 +790,8 @@ struct SymbolTableBuilder {
in_comp_inner_loop_target: bool,
// Scope info for error messages (e.g., "a TypeVar bound")
scope_info: Option<&'static str>,
// PEP 649: Track if we're inside a conditional block (if/for/while/etc.)
in_conditional_block: bool,
}
/// Enum to indicate in what mode an expression
@@ -723,6 +820,7 @@ impl SymbolTableBuilder {
in_type_alias: false,
in_comp_inner_loop_target: false,
scope_info: None,
in_conditional_block: false,
};
this.enter_scope("top", CompilerScope::Module, 0);
this
@@ -733,6 +831,8 @@ impl SymbolTableBuilder {
let mut symbol_table = self.tables.pop().unwrap();
// Save varnames for the top-level module scope
symbol_table.varnames = self.current_varnames;
// Propagate future_annotations to the symbol table
symbol_table.future_annotations = self.future_annotations;
analyze_symbol_table(&mut symbol_table)?;
Ok(symbol_table)
}
@@ -782,6 +882,79 @@ impl SymbolTableBuilder {
self.tables.last_mut().unwrap().sub_tables.push(table);
}
/// Enter annotation scope (PEP 649)
/// Creates or reuses the annotation block for the current scope
fn enter_annotation_scope(&mut self, line_number: u32) {
let current = self.tables.last_mut().unwrap();
let can_see_class_scope = current.typ == CompilerScope::Class;
let has_conditional = current.has_conditional_annotations;
// Create annotation block if not exists
if current.annotation_block.is_none() {
let mut annotation_table = SymbolTable::new(
"__annotate__".to_owned(),
CompilerScope::Annotation,
line_number,
true, // is_nested
);
// Annotation scope in class can see class scope
annotation_table.can_see_class_scope = can_see_class_scope;
// Add 'format' parameter
annotation_table.varnames.push("format".to_owned());
current.annotation_block = Some(Box::new(annotation_table));
}
// Take the annotation block and push to stack for processing
let annotation_table = current.annotation_block.take().unwrap();
self.tables.push(*annotation_table);
self.current_varnames.clear();
if can_see_class_scope && !self.future_annotations {
self.add_classdict_freevar();
// Also add __conditional_annotations__ as free var if parent has conditional annotations
if has_conditional {
self.add_conditional_annotations_freevar();
}
}
}
/// Leave annotation scope (PEP 649)
/// Stores the annotation block back to parent instead of sub_tables
fn leave_annotation_scope(&mut self) {
let mut table = self.tables.pop().unwrap();
// Save the collected varnames to the symbol table
table.varnames = core::mem::take(&mut self.current_varnames);
// Store back to parent's annotation_block (not sub_tables)
let parent = self.tables.last_mut().unwrap();
parent.annotation_block = Some(Box::new(table));
}
fn add_classdict_freevar(&mut self) {
let table = self.tables.last_mut().unwrap();
let name = "__classdict__";
let symbol = table
.symbols
.entry(name.to_owned())
.or_insert_with(|| Symbol::new(name));
symbol.scope = SymbolScope::Free;
symbol
.flags
.insert(SymbolFlags::REFERENCED | SymbolFlags::FREE_CLASS);
}
fn add_conditional_annotations_freevar(&mut self) {
let table = self.tables.last_mut().unwrap();
let name = "__conditional_annotations__";
let symbol = table
.symbols
.entry(name.to_owned())
.or_insert_with(|| Symbol::new(name));
symbol.scope = SymbolScope::Free;
symbol
.flags
.insert(SymbolFlags::REFERENCED | SymbolFlags::FREE_CLASS);
}
fn line_index_start(&self, range: TextRange) -> u32 {
self.source_file
.to_source_code()
@@ -830,15 +1003,58 @@ impl SymbolTableBuilder {
}
fn scan_annotation(&mut self, annotation: &Expr) -> SymbolTableResult {
let current_scope = self.tables.last().map(|t| t.typ);
// PEP 649: Check if this is a conditional annotation
// Module-level: always conditional (module may be partially executed)
// Class-level: conditional only when inside if/for/while/etc.
if !self.future_annotations {
let is_conditional = matches!(current_scope, Some(CompilerScope::Module))
|| (matches!(current_scope, Some(CompilerScope::Class))
&& self.in_conditional_block);
if is_conditional && !self.tables.last().unwrap().has_conditional_annotations {
self.tables.last_mut().unwrap().has_conditional_annotations = true;
// Register __conditional_annotations__ symbol in the scope (USE flag, not DEF)
self.register_name(
"__conditional_annotations__",
SymbolUsage::Used,
annotation.range(),
)?;
}
}
// Create annotation scope for deferred evaluation
let line_number = self.line_index_start(annotation.range());
self.enter_annotation_scope(line_number);
if self.future_annotations {
Ok(())
} else {
// PEP 563: annotations are stringified at compile time
// Don't scan expression - symbols would fail to resolve
// Just create the annotation_block structure
self.leave_annotation_scope();
return Ok(());
}
// PEP 649: scan expression for symbol references
// Class annotations are evaluated in class locals (not module globals)
let was_in_annotation = self.in_annotation;
self.in_annotation = true;
let result = self.scan_expression(annotation, ExpressionContext::Load);
self.in_annotation = was_in_annotation;
self.leave_annotation_scope();
// Module scope: re-scan to register symbols (builtins like str, int)
// Class scope: do NOT re-scan to preserve class-local symbol resolution
if matches!(current_scope, Some(CompilerScope::Module)) {
let was_in_annotation = self.in_annotation;
self.in_annotation = true;
let result = self.scan_expression(annotation, ExpressionContext::Load);
let _ = self.scan_expression(annotation, ExpressionContext::Load);
self.in_annotation = was_in_annotation;
result
}
result
}
fn scan_statement(&mut self, statement: &Stmt) -> SymbolTableResult {
@@ -873,9 +1089,23 @@ impl SymbolTableBuilder {
}) => {
self.scan_decorators(decorator_list, ExpressionContext::Load)?;
self.register_ident(name, SymbolUsage::Assigned)?;
if let Some(expression) = returns {
self.scan_annotation(expression)?;
}
// Save the parent's annotation_block before scanning function annotations,
// so function annotations don't interfere with parent scope annotations.
// This applies to both class scope (methods) and module scope (top-level functions).
let parent_scope_typ = self.tables.last().map(|t| t.typ);
let should_save_annotation_block = matches!(
parent_scope_typ,
Some(CompilerScope::Class) | Some(CompilerScope::Module)
);
let saved_annotation_block = if should_save_annotation_block {
self.tables.last_mut().unwrap().annotation_block.take()
} else {
None
};
// For generic functions, enter type_param block FIRST so that
// annotation scopes are nested inside and can see type parameters.
if let Some(type_params) = type_params {
self.enter_type_param_block(
&format!("<generic parameters of {}>", name.as_str()),
@@ -883,16 +1113,28 @@ impl SymbolTableBuilder {
)?;
self.scan_type_params(type_params)?;
}
let has_return_annotation = if let Some(expression) = returns {
self.scan_annotation(expression)?;
true
} else {
false
};
self.enter_scope_with_parameters(
name.as_str(),
parameters,
self.line_index_start(*range),
has_return_annotation,
)?;
self.scan_statements(body)?;
self.leave_scope();
if type_params.is_some() {
self.leave_scope();
}
// Restore parent's annotation_block after processing the function
if let Some(block) = saved_annotation_block {
self.tables.last_mut().unwrap().annotation_block = Some(block);
}
}
Stmt::ClassDef(StmtClassDef {
name,
@@ -915,6 +1157,10 @@ impl SymbolTableBuilder {
CompilerScope::Class,
self.line_index_start(*range),
);
// Reset in_conditional_block for new class scope
// (each scope has its own conditional context)
let saved_in_conditional = self.in_conditional_block;
self.in_conditional_block = false;
let prev_class = self.class_name.replace(name.to_string());
self.register_name("__module__", SymbolUsage::Assigned, *range)?;
self.register_name("__qualname__", SymbolUsage::Assigned, *range)?;
@@ -922,6 +1168,7 @@ impl SymbolTableBuilder {
self.register_name("__class__", SymbolUsage::Assigned, *range)?;
self.scan_statements(body)?;
self.leave_scope();
self.in_conditional_block = saved_in_conditional;
self.class_name = prev_class;
if let Some(arguments) = arguments {
self.scan_expressions(&arguments.args, ExpressionContext::Load)?;
@@ -945,6 +1192,9 @@ impl SymbolTableBuilder {
..
}) => {
self.scan_expression(test, ExpressionContext::Load)?;
// PEP 649: Track conditional block for annotations
let saved_in_conditional_block = self.in_conditional_block;
self.in_conditional_block = true;
self.scan_statements(body)?;
for elif in elif_else_clauses {
if let Some(test) = &elif.test {
@@ -952,6 +1202,7 @@ impl SymbolTableBuilder {
}
self.scan_statements(&elif.body)?;
}
self.in_conditional_block = saved_in_conditional_block;
}
Stmt::For(StmtFor {
target,
@@ -962,15 +1213,23 @@ impl SymbolTableBuilder {
}) => {
self.scan_expression(target, ExpressionContext::Store)?;
self.scan_expression(iter, ExpressionContext::Load)?;
// PEP 649: Track conditional block for annotations
let saved_in_conditional_block = self.in_conditional_block;
self.in_conditional_block = true;
self.scan_statements(body)?;
self.scan_statements(orelse)?;
self.in_conditional_block = saved_in_conditional_block;
}
Stmt::While(StmtWhile {
test, body, orelse, ..
}) => {
self.scan_expression(test, ExpressionContext::Load)?;
// PEP 649: Track conditional block for annotations
let saved_in_conditional_block = self.in_conditional_block;
self.in_conditional_block = true;
self.scan_statements(body)?;
self.scan_statements(orelse)?;
self.in_conditional_block = saved_in_conditional_block;
}
Stmt::Break(_) | Stmt::Continue(_) | Stmt::Pass(_) => {
// No symbols here.
@@ -1037,6 +1296,21 @@ impl SymbolTableBuilder {
match &**target {
Expr::Name(ast::ExprName { id, .. }) if *simple => {
self.register_name(id.as_str(), SymbolUsage::AnnotationAssigned, *range)?;
// PEP 649: Register annotate function in module/class scope
let current_scope = self.tables.last().map(|t| t.typ);
match current_scope {
Some(CompilerScope::Module) => {
self.register_name("__annotate__", SymbolUsage::Assigned, *range)?;
}
Some(CompilerScope::Class) => {
self.register_name(
"__annotate_func__",
SymbolUsage::Assigned,
*range,
)?;
}
_ => {}
}
}
_ => {
self.scan_expression(target, ExpressionContext::Store)?;
@@ -1054,7 +1328,11 @@ impl SymbolTableBuilder {
self.scan_expression(expression, ExpressionContext::Store)?;
}
}
// PEP 649: Track conditional block for annotations
let saved_in_conditional_block = self.in_conditional_block;
self.in_conditional_block = true;
self.scan_statements(body)?;
self.in_conditional_block = saved_in_conditional_block;
}
Stmt::Try(StmtTry {
body,
@@ -1063,6 +1341,9 @@ impl SymbolTableBuilder {
finalbody,
..
}) => {
// PEP 649: Track conditional block for annotations
let saved_in_conditional_block = self.in_conditional_block;
self.in_conditional_block = true;
self.scan_statements(body)?;
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
@@ -1081,9 +1362,13 @@ impl SymbolTableBuilder {
}
self.scan_statements(orelse)?;
self.scan_statements(finalbody)?;
self.in_conditional_block = saved_in_conditional_block;
}
Stmt::Match(StmtMatch { subject, cases, .. }) => {
self.scan_expression(subject, ExpressionContext::Load)?;
// PEP 649: Track conditional block for annotations
let saved_in_conditional_block = self.in_conditional_block;
self.in_conditional_block = true;
for case in cases {
self.scan_pattern(&case.pattern)?;
if let Some(guard) = &case.guard {
@@ -1091,6 +1376,7 @@ impl SymbolTableBuilder {
}
self.scan_statements(&case.body)?;
}
self.in_conditional_block = saved_in_conditional_block;
}
Stmt::Raise(StmtRaise { exc, cause, .. }) => {
if let Some(expression) = exc {
@@ -1412,6 +1698,7 @@ impl SymbolTableBuilder {
"lambda",
parameters,
self.line_index_start(expression.range()),
false, // lambdas have no return annotation
)?;
} else {
self.enter_scope(
@@ -1774,6 +2061,7 @@ impl SymbolTableBuilder {
name: &str,
parameters: &Parameters,
line_number: u32,
has_return_annotation: bool,
) -> SymbolTableResult {
// Evaluate eventual default parameters:
for default in parameters
@@ -1811,8 +2099,40 @@ impl SymbolTableBuilder {
self.scan_annotation(annotation)?;
}
// Check if this function has any annotations (parameter or return)
let has_param_annotations = parameters
.posonlyargs
.iter()
.chain(parameters.args.iter())
.chain(parameters.kwonlyargs.iter())
.any(|p| p.parameter.annotation.is_some())
|| parameters
.vararg
.as_ref()
.is_some_and(|p| p.annotation.is_some())
|| parameters
.kwarg
.as_ref()
.is_some_and(|p| p.annotation.is_some());
let has_any_annotations = has_param_annotations || has_return_annotation;
// Take annotation_block if this function has any annotations.
// When in class scope, the class's annotation_block was saved before scanning
// function annotations, so the current annotation_block belongs to this function.
let annotation_block = if has_any_annotations {
self.tables.last_mut().unwrap().annotation_block.take()
} else {
None
};
self.enter_scope(name, CompilerScope::Function, line_number);
// Move annotation_block to function scope only if we have one
if let Some(block) = annotation_block {
self.tables.last_mut().unwrap().annotation_block = Some(block);
}
// Fill scope with parameter names:
self.scan_parameters(&parameters.posonlyargs)?;
self.scan_parameters(&parameters.args)?;
@@ -1841,6 +2161,20 @@ impl SymbolTableBuilder {
.to_source_code()
.source_location(range.start(), PositionEncoding::Utf8);
let location = Some(location);
// Check for forbidden names like __debug__
if name == "__debug__"
&& matches!(
role,
SymbolUsage::Parameter | SymbolUsage::AnnotationParameter | SymbolUsage::Assigned
)
{
return Err(SymbolTableError {
error: "cannot assign to __debug__".to_owned(),
location,
});
}
let scope_depth = self.tables.len();
let table = self.tables.last_mut().unwrap();
+4 -1
View File
@@ -290,13 +290,16 @@ pub struct CodeObject<C: Constant = ConstantData> {
bitflags! {
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct CodeFlags: u16 {
pub struct CodeFlags: u32 {
const OPTIMIZED = 0x0001;
const NEWLOCALS = 0x0002;
const VARARGS = 0x0004;
const VARKEYWORDS = 0x0008;
const GENERATOR = 0x0020;
const COROUTINE = 0x0080;
/// If a code object represents a function and has a docstring,
/// this bit is set and the first item in co_consts is the docstring.
const HAS_DOCSTRING = 0x4000000;
}
}
@@ -327,6 +327,8 @@ bitflags! {
const KW_ONLY_DEFAULTS = 0x04;
const DEFAULTS = 0x08;
const TYPE_PARAMS = 0x10;
/// PEP 649: __annotate__ function closure (instead of __annotations__ dict)
const ANNOTATE = 0x20;
}
}
+2 -2
View File
@@ -202,7 +202,7 @@ pub fn deserialize_code<R: Read, Bag: ConstantBag>(
})
.collect::<Result<Box<[(SourceLocation, SourceLocation)]>>>()?;
let flags = CodeFlags::from_bits_truncate(rdr.read_u16()?);
let flags = CodeFlags::from_bits_truncate(rdr.read_u32()?);
let posonlyarg_count = rdr.read_u32()?;
let arg_count = rdr.read_u32()?;
@@ -660,7 +660,7 @@ pub fn serialize_code<W: Write, C: Constant>(buf: &mut W, code: &CodeObject<C>)
buf.write_u32(end.character_offset.to_zero_indexed() as _);
}
buf.write_u16(code.flags.bits());
buf.write_u32(code.flags.bits());
buf.write_u32(code.posonlyarg_count);
buf.write_u32(code.arg_count);
+4 -30
View File
@@ -778,36 +778,10 @@ pub(crate) fn impl_pyexception_impl(attr: PunctuatedNestedMeta, item: Item) -> R
}
};
// We need this method, because of how `CPython` copies `__init__`
// from `BaseException` in `SimpleExtendsException` macro.
// See: `(initproc)BaseException_init`
// spell-checker:ignore initproc
let slot_init = if with_contains(&with_items, "Initializer") {
quote!()
} else {
with_items.push(Ident::new("Initializer", Span::call_site()));
quote! {
impl ::rustpython_vm::types::Initializer for #self_ty {
type Args = ::rustpython_vm::function::FuncArgs;
fn slot_init(
zelf: ::rustpython_vm::PyObjectRef,
args: ::rustpython_vm::function::FuncArgs,
vm: &::rustpython_vm::VirtualMachine,
) -> ::rustpython_vm::PyResult<()> {
<Self as ::rustpython_vm::class::PyClassDef>::Base::slot_init(zelf, args, vm)
}
fn init(
_zelf: ::rustpython_vm::PyRef<Self>,
_args: Self::Args,
_vm: &::rustpython_vm::VirtualMachine
) -> ::rustpython_vm::PyResult<()> {
unreachable!("slot_init is defined")
}
}
}
};
// SimpleExtendsException: inherits BaseException_init from the base class via MRO.
// Only exceptions that explicitly specify `with(Initializer)` will have
// their own __init__ in __dict__.
let slot_init = quote!();
let extra_attrs_tokens = if extra_attrs.is_empty() {
quote!()
+91 -5
View File
@@ -63,6 +63,72 @@ impl From<ConstantData> for StackValue {
}
}
/// Extract annotations from an annotate function's bytecode.
/// The annotate function uses BUILD_MAP with key-value pairs loaded before it.
/// Keys are parameter names (from LOAD_CONST), values are type names (from LOAD_NAME/LOAD_GLOBAL).
fn extract_annotations_from_annotate_code(code: &CodeObject) -> HashMap<String, StackValue> {
let mut annotations = HashMap::new();
let mut stack: Vec<(bool, usize)> = Vec::new(); // (is_const, index)
let mut op_arg_state = OpArgState::default();
for &word in code.instructions.iter() {
let (instruction, arg) = op_arg_state.get(word);
match instruction {
Instruction::LoadConst { idx } => {
stack.push((true, idx.get(arg) as usize));
}
Instruction::LoadName(idx) | Instruction::LoadGlobal(idx) => {
stack.push((false, idx.get(arg) as usize));
}
Instruction::BuildMap { size, .. } => {
let count = size.get(arg) as usize;
// Stack has key-value pairs in order: k1, v1, k2, v2, ...
// So we need count * 2 items from the stack
let start = stack.len().saturating_sub(count * 2);
let pairs: Vec<_> = stack.drain(start..).collect();
for chunk in pairs.chunks(2) {
if chunk.len() == 2 {
let (key_is_const, key_idx) = chunk[0];
let (_val_is_const, val_idx) = chunk[1];
// Key should be a const string (parameter name)
if key_is_const
&& let ConstantData::Str { value } = &code.constants[key_idx]
{
let param_name = value.to_string_lossy().into_owned();
// Value should be a name (type name)
if let Some(type_name) = code.names.get(val_idx) {
annotations
.insert(param_name, StackValue::String(type_name.clone()));
}
}
}
}
// Return after processing BUILD_MAP - we got our annotations
return annotations;
}
Instruction::Resume { .. }
| Instruction::LoadFast(_)
| Instruction::CompareOp { .. }
| Instruction::ExtendedArg => {
// Ignore these instructions for annotation extraction
}
Instruction::ReturnValue | Instruction::ReturnConst { .. } => {
// End of function - return what we have
return annotations;
}
_ => {
// For other instructions, clear the stack tracking as we don't understand the effect
stack.clear();
}
}
}
annotations
}
pub struct StackMachine {
stack: Vec<StackValue>,
locals: HashMap<String, StackValue>,
@@ -92,6 +158,9 @@ impl StackMachine {
names: &[String],
) -> ControlFlow<()> {
match instruction {
Instruction::Resume { .. } => {
// No-op for JIT tests - just marks function entry point
}
Instruction::LoadConst { idx } => {
let idx = idx.get(arg);
self.stack.push(constants[idx as usize].clone().into())
@@ -143,13 +212,31 @@ impl StackMachine {
};
let attr_value = self.stack.pop().expect("Expected attribute value on stack");
// For now, we only handle ANNOTATIONS flag in JIT tests
if attr
.get(arg)
let flags = attr.get(arg);
// Handle ANNOTATE flag (PEP 649 style - Python 3.14+)
// The attr_value is a function that returns annotations when called
if flags.contains(rustpython_compiler_core::bytecode::MakeFunctionFlags::ANNOTATE) {
if let StackValue::Function(annotate_func) = attr_value {
// Parse the annotate function's bytecode to extract annotations
// The pattern is: LOAD_CONST (key), LOAD_NAME (value), ... BUILD_MAP
let annotate_code = &annotate_func.code;
let annotations = extract_annotations_from_annotate_code(annotate_code);
let updated_func = Function {
code: func.code,
annotations,
};
self.stack.push(StackValue::Function(updated_func));
} else {
panic!("Expected annotate function for ANNOTATE flag");
}
}
// Handle old ANNOTATIONS flag (Python 3.12 style)
else if flags
.contains(rustpython_compiler_core::bytecode::MakeFunctionFlags::ANNOTATIONS)
{
if let StackValue::Map(annotations) = attr_value {
// Update function's annotations
let updated_func = Function {
code: func.code,
annotations,
@@ -160,7 +247,6 @@ impl StackMachine {
}
} else {
// For other attributes, just push the function back unchanged
// (since JIT tests mainly care about type annotations)
self.stack.push(StackValue::Function(func));
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ pub mod _hashlib {
class::StaticType,
convert::ToPyObject,
function::{ArgBytesLike, ArgStrOrBytesLike, FuncArgs, OptionalArg},
types::{Constructor, Initializer, Representable},
types::{Constructor, Representable},
};
use blake2::{Blake2b512, Blake2s256};
use digest::{DynDigest, core_api::BlockSizeUser};
+1 -1
View File
@@ -7,7 +7,7 @@ pub(crate) mod ssl_error {
use crate::vm::{
Py, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyBaseException, PyOSError, PyStrRef},
types::{Constructor, Initializer},
types::Constructor,
};
// Error type constants - exposed as pyattr and available for internal use
+3 -3
View File
@@ -114,12 +114,12 @@ mod tests {
let cfg_path = temp_dir.join("test_pyvenv.cfg");
let mut file = fs::File::create(&cfg_path).unwrap();
writeln!(file, "home = C:\\Python313").unwrap();
writeln!(file, "home = C:\\Python314").unwrap();
writeln!(file, "include-system-site-packages = false").unwrap();
writeln!(file, "version = 3.13.0").unwrap();
writeln!(file, "version = 3.14.0").unwrap();
let home = read_home(&cfg_path).unwrap();
assert_eq!(home, "C:\\Python313");
assert_eq!(home, "C:\\Python314");
fs::remove_file(&cfg_path).unwrap();
}
+2 -2
View File
@@ -322,8 +322,8 @@ impl PyByteArray {
}
#[pyclassmethod]
fn fromhex(cls: PyTypeRef, string: PyStrRef, vm: &VirtualMachine) -> PyResult {
let bytes = PyBytesInner::fromhex(string.as_bytes(), vm)?;
fn fromhex(cls: PyTypeRef, string: PyObjectRef, vm: &VirtualMachine) -> PyResult {
let bytes = PyBytesInner::fromhex_object(string, vm)?;
let bytes = vm.ctx.new_bytes(bytes);
let args = vec![bytes.into()].into();
PyType::call(&cls, args, vm)
+2 -2
View File
@@ -316,8 +316,8 @@ impl PyBytes {
}
#[pyclassmethod]
fn fromhex(cls: PyTypeRef, string: PyStrRef, vm: &VirtualMachine) -> PyResult {
let bytes = PyBytesInner::fromhex(string.as_bytes(), vm)?;
fn fromhex(cls: PyTypeRef, string: PyObjectRef, vm: &VirtualMachine) -> PyResult {
let bytes = PyBytesInner::fromhex_object(string, vm)?;
let bytes = vm.ctx.new_bytes(bytes).into();
PyType::call(&cls, vec![bytes].into(), vm)
}
+3 -3
View File
@@ -152,7 +152,7 @@ pub struct ReplaceArgs {
#[pyarg(named, optional)]
co_names: OptionalArg<Vec<PyObjectRef>>,
#[pyarg(named, optional)]
co_flags: OptionalArg<u16>,
co_flags: OptionalArg<u32>,
#[pyarg(named, optional)]
co_varnames: OptionalArg<Vec<PyObjectRef>>,
#[pyarg(named, optional)]
@@ -411,7 +411,7 @@ pub struct PyCodeNewArgs {
kwonlyargcount: u32,
nlocals: u32,
stacksize: u32,
flags: u16,
flags: u32,
co_code: PyBytesRef,
consts: PyTupleRef,
names: PyTupleRef,
@@ -628,7 +628,7 @@ impl PyCode {
}
#[pygetset]
const fn co_flags(&self) -> u16 {
const fn co_flags(&self) -> u32 {
self.code.flags.bits()
}
+117 -8
View File
@@ -36,7 +36,8 @@ pub struct PyFunction {
name: PyMutex<PyStrRef>,
qualname: PyMutex<PyStrRef>,
type_params: PyMutex<PyTupleRef>,
annotations: PyMutex<PyDictRef>,
annotations: PyMutex<Option<PyDictRef>>,
annotate: PyMutex<Option<PyObjectRef>>,
module: PyMutex<PyObjectRef>,
doc: PyMutex<PyObjectRef>,
#[cfg(feature = "jit")]
@@ -77,6 +78,17 @@ impl PyFunction {
builtins
};
// Get docstring from co_consts[0] if HAS_DOCSTRING flag is set
let doc = if code.code.flags.contains(bytecode::CodeFlags::HAS_DOCSTRING) {
code.code
.constants
.first()
.map(|c| c.as_object().to_owned())
.unwrap_or_else(|| vm.ctx.none())
} else {
vm.ctx.none()
};
let qualname = vm.ctx.new_str(code.qualname.as_str());
let func = Self {
code: PyMutex::new(code.clone()),
@@ -87,9 +99,10 @@ impl PyFunction {
name,
qualname: PyMutex::new(qualname),
type_params: PyMutex::new(vm.ctx.empty_tuple.clone()),
annotations: PyMutex::new(vm.ctx.new_dict()),
annotations: PyMutex::new(None),
annotate: PyMutex::new(None),
module: PyMutex::new(module),
doc: PyMutex::new(vm.ctx.none()),
doc: PyMutex::new(doc),
#[cfg(feature = "jit")]
jitted_code: OnceCell::new(),
};
@@ -364,7 +377,7 @@ impl PyFunction {
)));
}
};
*self.annotations.lock() = annotations;
*self.annotations.lock() = Some(annotations);
} else if attr == bytecode::MakeFunctionFlags::CLOSURE {
// For closure, we need special handling
// The closure tuple contains cell objects
@@ -388,6 +401,12 @@ impl PyFunction {
))
})?;
*self.type_params.lock() = type_params;
} else if attr == bytecode::MakeFunctionFlags::ANNOTATE {
// PEP 649: Store the __annotate__ function closure
if !attr_value.is_callable() {
return Err(vm.new_type_error("__annotate__ must be callable".to_owned()));
}
*self.annotate.lock() = Some(attr_value);
} else {
unreachable!("This is a compiler bug");
}
@@ -574,13 +593,103 @@ impl PyFunction {
}
#[pygetset]
fn __annotations__(&self) -> PyDictRef {
self.annotations.lock().clone()
fn __annotations__(&self, vm: &VirtualMachine) -> PyResult<PyDictRef> {
// First check if we have cached annotations
{
let annotations = self.annotations.lock();
if let Some(ref ann) = *annotations {
return Ok(ann.clone());
}
}
// Check for callable __annotate__ and clone it before calling
let annotate_fn = {
let annotate = self.annotate.lock();
if let Some(ref func) = *annotate
&& func.is_callable()
{
Some(func.clone())
} else {
None
}
};
// Release locks before calling __annotate__ to avoid deadlock
if let Some(annotate_fn) = annotate_fn {
let one = vm.ctx.new_int(1);
let ann_dict = annotate_fn.call((one,), vm)?;
let ann_dict = ann_dict
.downcast::<crate::builtins::PyDict>()
.map_err(|obj| {
vm.new_type_error(format!(
"__annotate__ returned non-dict of type '{}'",
obj.class().name()
))
})?;
// Cache the result
*self.annotations.lock() = Some(ann_dict.clone());
return Ok(ann_dict);
}
// No __annotate__ or not callable, create empty dict
let new_dict = vm.ctx.new_dict();
*self.annotations.lock() = Some(new_dict.clone());
Ok(new_dict)
}
#[pygetset(setter)]
fn set___annotations__(&self, annotations: PyDictRef) {
*self.annotations.lock() = annotations
fn set___annotations__(
&self,
value: PySetterValue<Option<PyObjectRef>>,
vm: &VirtualMachine,
) -> PyResult<()> {
let annotations = match value {
PySetterValue::Assign(Some(value)) => {
let annotations = value.downcast::<crate::builtins::PyDict>().map_err(|_| {
vm.new_type_error("__annotations__ must be set to a dict object")
})?;
Some(annotations)
}
PySetterValue::Assign(None) | PySetterValue::Delete => None,
};
*self.annotations.lock() = annotations;
// Clear __annotate__ when __annotations__ is set
*self.annotate.lock() = None;
Ok(())
}
#[pygetset]
fn __annotate__(&self, vm: &VirtualMachine) -> PyObjectRef {
self.annotate
.lock()
.clone()
.unwrap_or_else(|| vm.ctx.none())
}
#[pygetset(setter)]
fn set___annotate__(
&self,
value: PySetterValue<Option<PyObjectRef>>,
vm: &VirtualMachine,
) -> PyResult<()> {
let annotate = match value {
PySetterValue::Assign(Some(value)) => {
if !value.is_callable() {
return Err(vm.new_type_error("__annotate__ must be callable or None"));
}
// Clear cached __annotations__ when __annotate__ is set
*self.annotations.lock() = None;
Some(value)
}
PySetterValue::Assign(None) => None,
PySetterValue::Delete => {
return Err(vm.new_type_error("__annotate__ cannot be deleted"));
}
};
*self.annotate.lock() = annotate;
Ok(())
}
#[pygetset]
+11 -3
View File
@@ -12,7 +12,8 @@ use crate::{
},
convert::{IntoPyException, ToPyObject, ToPyResult},
function::{
ArgByteOrder, ArgIntoBool, FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue,
ArgByteOrder, ArgIntoBool, FuncArgs, OptionalArg, OptionalOption, PyArithmeticValue,
PyComparisonValue,
},
protocol::{PyNumberMethods, handle_bytes_to_int_err},
types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
@@ -286,6 +287,13 @@ impl PyInt {
where
I: PrimInt + TryFrom<&'a BigInt>,
{
// TODO: Python 3.14+: ValueError for negative int to unsigned type
// See stdlib_socket.py socket.htonl(-1)
//
// if I::min_value() == I::zero() && self.as_bigint().sign() == Sign::Minus {
// return Err(vm.new_value_error("Cannot convert negative int".to_owned()));
// }
I::try_from(self.as_bigint()).map_err(|_| {
vm.new_overflow_error(format!(
"Python int too large to convert to Rust {}",
@@ -381,10 +389,10 @@ impl PyInt {
#[pymethod]
fn __round__(
zelf: PyRef<Self>,
ndigits: OptionalArg<PyIntRef>,
ndigits: OptionalOption<PyIntRef>,
vm: &VirtualMachine,
) -> PyResult<PyRef<Self>> {
if let OptionalArg::Present(ndigits) = ndigits {
if let Some(ndigits) = ndigits.flatten() {
let ndigits = ndigits.as_bigint();
// round(12345, -2) == 12300
// If precision >= 0, then any integer is already rounded correctly
+111 -1
View File
@@ -4,7 +4,7 @@ use crate::{
builtins::{PyStrInterned, pystr::AsPyStr},
class::PyClassImpl,
convert::ToPyObject,
function::{FuncArgs, PyMethodDef},
function::{FuncArgs, PyMethodDef, PySetterValue},
types::{GetAttr, Initializer, Representable},
};
@@ -182,6 +182,116 @@ impl PyModule {
let attrs = dict.into_iter().map(|(k, _v)| k).collect();
Ok(attrs)
}
#[pygetset]
fn __annotate__(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let dict = zelf.dict();
// Get __annotate__ from dict; if not present, insert None and return it
// See: module_get_annotate()
if let Some(annotate) = dict.get_item_opt(identifier!(vm, __annotate__), vm)? {
Ok(annotate)
} else {
let none = vm.ctx.none();
dict.set_item(identifier!(vm, __annotate__), none.clone(), vm)?;
Ok(none)
}
}
#[pygetset(setter)]
fn set___annotate__(
zelf: &Py<Self>,
value: PySetterValue,
vm: &VirtualMachine,
) -> PyResult<()> {
match value {
PySetterValue::Assign(value) => {
if !vm.is_none(&value) && !value.is_callable() {
return Err(vm.new_type_error("__annotate__ must be callable or None"));
}
let dict = zelf.dict();
dict.set_item(identifier!(vm, __annotate__), value.clone(), vm)?;
// Clear __annotations__ if value is not None
if !vm.is_none(&value) {
dict.del_item(identifier!(vm, __annotations__), vm).ok();
}
Ok(())
}
PySetterValue::Delete => Err(vm.new_type_error("cannot delete __annotate__ attribute")),
}
}
#[pygetset]
fn __annotations__(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let dict = zelf.dict();
// Check if __annotations__ is already in dict (explicitly set)
if let Some(annotations) = dict.get_item_opt(identifier!(vm, __annotations__), vm)? {
return Ok(annotations);
}
// Check if module is initializing
let is_initializing = Self::is_initializing(&dict, vm);
// PEP 649: Get __annotate__ and call it if callable
let annotations = if let Some(annotate) =
dict.get_item_opt(identifier!(vm, __annotate__), vm)?
&& annotate.is_callable()
{
// Call __annotate__(1) where 1 is FORMAT_VALUE
let result = annotate.call((1i32,), vm)?;
if !result.class().is(vm.ctx.types.dict_type) {
return Err(vm.new_type_error(format!(
"__annotate__ returned non-dict of type '{}'",
result.class().name()
)));
}
result
} else {
vm.ctx.new_dict().into()
};
// Cache result unless module is initializing
if !is_initializing {
dict.set_item(identifier!(vm, __annotations__), annotations.clone(), vm)?;
}
Ok(annotations)
}
/// Check if module is initializing via __spec__._initializing
fn is_initializing(dict: &PyDictRef, vm: &VirtualMachine) -> bool {
if let Ok(Some(spec)) = dict.get_item_opt(vm.ctx.intern_str("__spec__"), vm)
&& let Ok(initializing) = spec.get_attr(vm.ctx.intern_str("_initializing"), vm)
{
return initializing.try_to_bool(vm).unwrap_or(false);
}
false
}
#[pygetset(setter)]
fn set___annotations__(
zelf: &Py<Self>,
value: PySetterValue,
vm: &VirtualMachine,
) -> PyResult<()> {
let dict = zelf.dict();
match value {
PySetterValue::Assign(value) => {
dict.set_item(identifier!(vm, __annotations__), value, vm)?;
// Clear __annotate__ from dict
dict.del_item(identifier!(vm, __annotate__), vm).ok();
Ok(())
}
PySetterValue::Delete => {
if dict.del_item(identifier!(vm, __annotations__), vm).is_err() {
return Err(vm.new_attribute_error("__annotations__".to_owned()));
}
// Also clear __annotate__
dict.del_item(identifier!(vm, __annotate__), vm).ok();
Ok(())
}
}
}
}
impl Initializer for PyModule {
+11 -3
View File
@@ -244,6 +244,12 @@ fn object_getstate_default(obj: &PyObject, required: bool, vm: &VirtualMachine)
let slots = vm.ctx.new_dict();
for i in 0..slot_names_len {
let borrowed_names = slot_names.borrow_vec();
// Check if slotnames changed during iteration
if borrowed_names.len() != slot_names_len {
return Err(vm.new_runtime_error(
"__slotnames__ changed size during iteration".to_owned(),
));
}
let name = borrowed_names[i].downcast_ref::<PyStr>().unwrap();
let Ok(value) = obj.get_attr(name, vm) else {
continue;
@@ -702,11 +708,13 @@ fn reduce_newobj(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
(newobj, newargs.into())
} else {
// args == NULL with non-empty kwargs is BadInternalCall
let Some(args) = args else {
return Err(vm.new_system_error("bad internal call".to_owned()));
};
// Use copyreg.__newobj_ex__
let newobj = copyreg.get_attr("__newobj_ex__", vm)?;
let args_tuple: PyObjectRef = args
.map(|a| a.into())
.unwrap_or_else(|| vm.ctx.empty_tuple.clone().into());
let args_tuple: PyObjectRef = args.into();
let kwargs_dict: PyObjectRef = kwargs
.map(|k| k.into())
.unwrap_or_else(|| vm.ctx.new_dict().into());
+5 -4
View File
@@ -108,11 +108,12 @@ impl PyNotImplemented {
impl AsNumber for PyNotImplemented {
fn as_number() -> &'static PyNumberMethods {
// TODO: As per https://bugs.python.org/issue35712, using NotImplemented
// in boolean contexts will need to raise a DeprecationWarning in 3.9
// and, eventually, a TypeError.
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
boolean: Some(|_number, _vm| Ok(true)),
boolean: Some(|_number, vm| {
Err(vm.new_type_error(
"NotImplemented should not be used in a boolean context".to_owned(),
))
}),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER
+108 -24
View File
@@ -851,6 +851,58 @@ impl PyType {
Ok(())
}
#[pygetset]
fn __annotate__(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
if !self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) {
return Err(vm.new_attribute_error(format!(
"type object '{}' has no attribute '__annotate__'",
self.name()
)));
}
let mut attrs = self.attributes.write();
// First try __annotate__, in case that's been set explicitly
if let Some(annotate) = attrs.get(identifier!(vm, __annotate__)).cloned() {
return Ok(annotate);
}
// Then try __annotate_func__
if let Some(annotate) = attrs.get(identifier!(vm, __annotate_func__)).cloned() {
// TODO: Apply descriptor tp_descr_get if needed
return Ok(annotate);
}
// Set __annotate_func__ = None and return None
let none = vm.ctx.none();
attrs.insert(identifier!(vm, __annotate_func__), none.clone());
Ok(none)
}
#[pygetset(setter)]
fn set___annotate__(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) -> PyResult<()> {
if value.is_none() {
return Err(vm.new_type_error("cannot delete __annotate__ attribute".to_owned()));
}
let value = value.unwrap();
if self.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
return Err(vm.new_type_error(format!(
"cannot set '__annotate__' attribute of immutable type '{}'",
self.name()
)));
}
if !vm.is_none(&value) && !value.is_callable() {
return Err(vm.new_type_error("__annotate__ must be callable or None".to_owned()));
}
let mut attrs = self.attributes.write();
// Store to __annotate_func__
attrs.insert(identifier!(vm, __annotate_func__), value.clone());
// Always clear cached annotations when __annotate__ is updated
attrs.swap_remove(identifier!(vm, __annotations_cache__));
Ok(())
}
#[pygetset]
fn __annotations__(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
if !self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) {
@@ -860,20 +912,37 @@ impl PyType {
)));
}
let __annotations__ = identifier!(vm, __annotations__);
let annotations = self.attributes.read().get(__annotations__).cloned();
// First try __annotations__ (e.g. for "from __future__ import annotations")
let attrs = self.attributes.read();
if let Some(annotations) = attrs.get(identifier!(vm, __annotations__)).cloned() {
return Ok(annotations);
}
// Then try __annotations_cache__
if let Some(annotations) = attrs.get(identifier!(vm, __annotations_cache__)).cloned() {
return Ok(annotations);
}
drop(attrs);
let annotations = if let Some(annotations) = annotations {
annotations
// Get __annotate__ and call it if callable
let annotate = self.__annotate__(vm)?;
let annotations = if annotate.is_callable() {
// Call __annotate__(1) where 1 is FORMAT_VALUE
let result = annotate.call((1i32,), vm)?;
if !result.class().is(vm.ctx.types.dict_type) {
return Err(vm.new_type_error(format!(
"__annotate__ returned non-dict of type '{}'",
result.class().name()
)));
}
result
} else {
let annotations: PyObjectRef = vm.ctx.new_dict().into();
let removed = self
.attributes
.write()
.insert(__annotations__, annotations.clone());
debug_assert!(removed.is_none());
annotations
vm.ctx.new_dict().into()
};
// Cache the result in __annotations_cache__
self.attributes
.write()
.insert(identifier!(vm, __annotations_cache__), annotations.clone());
Ok(annotations)
}
@@ -886,21 +955,36 @@ impl PyType {
)));
}
let __annotations__ = identifier!(vm, __annotations__);
if let Some(value) = value {
self.attributes.write().insert(__annotations__, value);
let mut attrs = self.attributes.write();
// conditional update based on __annotations__ presence
let has_annotations = attrs.contains_key(identifier!(vm, __annotations__));
if has_annotations {
// If __annotations__ is in dict, update it
if let Some(value) = value {
attrs.insert(identifier!(vm, __annotations__), value);
} else if attrs
.swap_remove(identifier!(vm, __annotations__))
.is_none()
{
return Err(vm.new_attribute_error("__annotations__".to_owned()));
}
// Also clear __annotations_cache__
attrs.swap_remove(identifier!(vm, __annotations_cache__));
} else {
self.attributes
.read()
.get(__annotations__)
.cloned()
.ok_or_else(|| {
vm.new_attribute_error(format!(
"'{}' object has no attribute '__annotations__'",
self.name()
))
})?;
// Otherwise update only __annotations_cache__
if let Some(value) = value {
attrs.insert(identifier!(vm, __annotations_cache__), value);
} else if attrs
.swap_remove(identifier!(vm, __annotations_cache__))
.is_none()
{
return Err(vm.new_attribute_error("__annotations__".to_owned()));
}
}
// Always clear __annotate_func__ and __annotate__
attrs.swap_remove(identifier!(vm, __annotate_func__));
attrs.swap_remove(identifier!(vm, __annotate__));
Ok(())
}
+33 -10
View File
@@ -423,12 +423,13 @@ impl PyBytesInner {
pub fn fromhex(bytes: &[u8], vm: &VirtualMachine) -> PyResult<Vec<u8>> {
let mut iter = bytes.iter().enumerate();
let mut bytes: Vec<u8> = Vec::with_capacity(bytes.len() / 2);
let i = loop {
let mut result: Vec<u8> = Vec::with_capacity(bytes.len() / 2);
// None means odd number of hex digits, Some(i) means invalid char at position i
let invalid_char: Option<usize> = loop {
let (i, &b) = match iter.next() {
Some(val) => val,
None => {
return Ok(bytes);
return Ok(result);
}
};
@@ -440,27 +441,49 @@ impl PyBytesInner {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => 10 + b - b'a',
b'A'..=b'F' => 10 + b - b'A',
_ => break i,
_ => break Some(i),
};
let (i, b) = match iter.next() {
Some(val) => val,
None => break i + 1,
None => break None, // odd number of hex digits
};
let bot = match b {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => 10 + b - b'a',
b'A'..=b'F' => 10 + b - b'A',
_ => break i,
_ => break Some(i),
};
bytes.push((top << 4) + bot);
result.push((top << 4) + bot);
};
Err(vm.new_value_error(format!(
"non-hexadecimal number found in fromhex() arg at position {i}"
)))
match invalid_char {
None => Err(vm.new_value_error(
"fromhex() arg must contain an even number of hexadecimal digits".to_owned(),
)),
Some(i) => Err(vm.new_value_error(format!(
"non-hexadecimal number found in fromhex() arg at position {i}"
))),
}
}
/// Parse hex string from str or bytes-like object
pub fn fromhex_object(string: PyObjectRef, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
if let Some(s) = string.downcast_ref::<PyStr>() {
Self::fromhex(s.as_bytes(), vm)
} else if let Ok(buffer) = PyBuffer::try_from_borrowed_object(vm, &string) {
let borrowed = buffer.as_contiguous().ok_or_else(|| {
vm.new_buffer_error("fromhex() requires a contiguous buffer".to_owned())
})?;
Self::fromhex(&borrowed, vm)
} else {
Err(vm.new_type_error(format!(
"fromhex() argument must be str or bytes-like, not {}",
string.class().name()
)))
}
}
#[inline]
+12 -2
View File
@@ -348,8 +348,18 @@ pub(super) mod types {
impl Initializer for PyBaseExceptionGroup {
type Args = FuncArgs;
fn slot_init(_zelf: PyObjectRef, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult<()> {
// No-op: __new__ already set up the correct args (message, exceptions_tuple)
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
// BaseExceptionGroup_init: no kwargs allowed
if !args.kwargs.is_empty() {
return Err(vm.new_type_error(format!(
"{} does not take keyword arguments",
zelf.class().name()
)));
}
// Do NOT call PyBaseException::slot_init here.
// slot_new already set args to (message, exceptions_tuple).
// Calling base init would overwrite with original args (message, exceptions_list).
let _ = (zelf, args, vm);
Ok(())
}
+90 -39
View File
@@ -1086,15 +1086,16 @@ fn syntax_error_set_msg(
}
fn system_exit_code(exc: PyBaseExceptionRef) -> Option<PyObjectRef> {
exc.args.read().first().map(|code| {
match_class!(match code {
ref tup @ PyTuple => match tup.as_slice() {
[x] => x.clone(),
_ => code.clone(),
},
other => other.clone(),
})
})
// SystemExit.code based on args length:
// - size == 0: code is None
// - size == 1: code is args[0]
// - size > 1: code is args (the whole tuple)
let args = exc.args.read();
match args.len() {
0 => None,
1 => Some(args.first().unwrap().clone()),
_ => Some(args.as_object().to_owned()),
}
}
#[cfg(feature = "serde")]
@@ -1255,7 +1256,7 @@ pub(super) mod types {
},
convert::ToPyObject,
convert::ToPyResult,
function::{ArgBytesLike, FuncArgs},
function::{ArgBytesLike, FuncArgs, KwArgs},
types::{Constructor, Initializer},
};
use crossbeam_utils::atomic::AtomicCell;
@@ -1393,11 +1394,29 @@ pub(super) mod types {
pub(super) args: PyRwLock<PyTupleRef>,
}
#[pyexception(name, base = PyBaseException, ctx = "system_exit", impl)]
#[pyexception(name, base = PyBaseException, ctx = "system_exit")]
#[derive(Debug)]
#[repr(transparent)]
pub struct PySystemExit(PyBaseException);
// SystemExit_init: has its own __init__ that sets the code attribute
#[pyexception(with(Initializer))]
impl PySystemExit {}
impl Initializer for PySystemExit {
type Args = FuncArgs;
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
// Call BaseException_init first (handles args)
PyBaseException::slot_init(zelf, args, vm)
// Note: code is computed dynamically via system_exit_code getter
// so we don't need to set it here explicitly
}
fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
unreachable!("slot_init is defined")
}
}
#[pyexception(name, base = PyBaseException, ctx = "generator_exit", impl)]
#[derive(Debug)]
#[repr(transparent)]
@@ -1474,16 +1493,25 @@ pub(super) mod types {
type Args = FuncArgs;
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
zelf.set_attr(
"name",
vm.unwrap_or_none(args.kwargs.get("name").cloned()),
vm,
)?;
zelf.set_attr(
"obj",
vm.unwrap_or_none(args.kwargs.get("obj").cloned()),
vm,
)?;
// Only 'name' and 'obj' kwargs are allowed
let mut kwargs = args.kwargs.clone();
let name = kwargs.swap_remove("name");
let obj = kwargs.swap_remove("obj");
// Reject unknown kwargs
if let Some(invalid_key) = kwargs.keys().next() {
return Err(vm.new_type_error(format!(
"AttributeError() got an unexpected keyword argument '{invalid_key}'"
)));
}
// Pass args without kwargs to BaseException_init
let base_args = FuncArgs::new(args.args.clone(), KwArgs::default());
PyBaseException::slot_init(zelf.clone(), base_args, vm)?;
// Set attributes
zelf.set_attr("name", vm.unwrap_or_none(name), vm)?;
zelf.set_attr("obj", vm.unwrap_or_none(obj), vm)?;
Ok(())
}
@@ -1529,9 +1557,11 @@ pub(super) mod types {
type Args = FuncArgs;
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
// Only 'name', 'path', 'name_from' kwargs are allowed
let mut kwargs = args.kwargs.clone();
let name = kwargs.swap_remove("name");
let path = kwargs.swap_remove("path");
let name_from = kwargs.swap_remove("name_from");
// Check for any remaining invalid keyword arguments
if let Some(invalid_key) = kwargs.keys().next() {
@@ -1543,6 +1573,7 @@ pub(super) mod types {
let dict = zelf.dict().unwrap();
dict.set_item("name", vm.unwrap_or_none(name), vm)?;
dict.set_item("path", vm.unwrap_or_none(path), vm)?;
dict.set_item("name_from", vm.unwrap_or_none(name_from), vm)?;
PyBaseException::slot_init(zelf, args, vm)
}
@@ -1592,11 +1623,45 @@ pub(super) mod types {
#[repr(transparent)]
pub struct PyMemoryError(PyException);
#[pyexception(name, base = PyException, ctx = "name_error", impl)]
#[pyexception(name, base = PyException, ctx = "name_error")]
#[derive(Debug)]
#[repr(transparent)]
pub struct PyNameError(PyException);
// NameError_init: handles the .name. kwarg
#[pyexception(with(Initializer))]
impl PyNameError {}
impl Initializer for PyNameError {
type Args = FuncArgs;
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
// Only 'name' kwarg is allowed
let mut kwargs = args.kwargs.clone();
let name = kwargs.swap_remove("name");
// Reject unknown kwargs
if let Some(invalid_key) = kwargs.keys().next() {
return Err(vm.new_type_error(format!(
"NameError() got an unexpected keyword argument '{invalid_key}'"
)));
}
// Pass args without kwargs to BaseException_init
let base_args = FuncArgs::new(args.args.clone(), KwArgs::default());
PyBaseException::slot_init(zelf.clone(), base_args, vm)?;
// Set name attribute if provided
if let Some(name) = name {
zelf.set_attr("name", name, vm)?;
}
Ok(())
}
fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
unreachable!("slot_init is defined")
}
}
#[pyexception(name, base = PyNameError, ctx = "unbound_local_error", impl)]
#[derive(Debug)]
#[repr(transparent)]
@@ -2232,31 +2297,17 @@ pub(super) mod types {
}
}
// MiddlingExtendsException: inherits __init__ from SyntaxError via MRO
#[pyexception(
name = "_IncompleteInputError",
base = PySyntaxError,
ctx = "incomplete_input_error"
ctx = "incomplete_input_error",
impl
)]
#[derive(Debug)]
#[repr(transparent)]
pub struct PyIncompleteInputError(PySyntaxError);
#[pyexception(with(Initializer))]
impl PyIncompleteInputError {}
impl Initializer for PyIncompleteInputError {
type Args = FuncArgs;
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
zelf.set_attr("name", vm.ctx.new_str("SyntaxError"), vm)?;
PySyntaxError::slot_init(zelf, args, vm)
}
fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
unreachable!("slot_init is defined")
}
}
#[pyexception(name, base = PySyntaxError, ctx = "indentation_error", impl)]
#[derive(Debug)]
#[repr(transparent)]
+23
View File
@@ -1168,6 +1168,29 @@ impl ExecutingFrame<'_> {
});
Ok(None)
}
Instruction::LoadFromDictOrGlobals(idx) => {
// PEP 649: Pop dict from stack (classdict), check there first, then globals
let dict = self.pop_value();
let name = self.code.names[idx.get(arg) as usize];
// Only treat KeyError as "not found", propagate other exceptions
let value = if let Some(dict_obj) = dict.downcast_ref::<PyDict>() {
dict_obj.get_item_opt(name, vm)?
} else {
// Not an exact dict, use mapping protocol
match dict.get_item(name, vm) {
Ok(v) => Some(v),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => None,
Err(e) => return Err(e),
}
};
self.push_value(match value {
Some(v) => v,
None => self.load_global_or_builtin(name, vm)?,
});
Ok(None)
}
Instruction::LoadClosure(i) => {
let value = self.cells_frees[i.get(arg) as usize].clone();
self.push_value(value.into());
+14 -377
View File
@@ -2,23 +2,18 @@ pub(crate) use decl::make_module;
#[pymodule(name = "itertools")]
mod decl {
use crate::stdlib::itertools::decl::int::get_value;
use crate::{
AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, PyWeakRef, TryFromObject,
VirtualMachine,
builtins::{
PyGenericAlias, PyInt, PyIntRef, PyList, PyTuple, PyTupleRef, PyType, PyTypeRef, int,
tuple::IntoPyTuple,
},
AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, PyWeakRef, VirtualMachine,
builtins::{PyGenericAlias, PyInt, PyIntRef, PyList, PyTuple, PyType, PyTypeRef, int},
common::{
lock::{PyMutex, PyRwLock, PyRwLockWriteGuard},
rc::PyRc,
},
convert::ToPyObject,
function::{ArgCallable, ArgIntoBool, FuncArgs, OptionalArg, OptionalOption, PosArgs},
function::{ArgCallable, FuncArgs, OptionalArg, OptionalOption, PosArgs},
protocol::{PyIter, PyIterReturn, PyNumber},
raise_if_stop,
stdlib::{sys, warnings},
stdlib::sys,
types::{Constructor, IterNext, Iterable, Representable, SelfIter},
};
use crossbeam_utils::atomic::AtomicCell;
@@ -28,15 +23,6 @@ mod decl {
use alloc::fmt;
use num_traits::{Signed, ToPrimitive};
fn pickle_deprecation(vm: &VirtualMachine) -> PyResult<()> {
warnings::warn(
vm.ctx.exceptions.deprecation_warning,
"Itertool pickle/copy/deepcopy support will be removed in a Python 3.14.".to_owned(),
1,
vm,
)
}
#[pyattr]
#[pyclass(name = "chain")]
#[derive(Debug, PyPayload)]
@@ -79,55 +65,6 @@ mod decl {
) -> PyGenericAlias {
PyGenericAlias::from_args(cls, args, vm)
}
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
pickle_deprecation(vm)?;
let source = zelf.source.read().clone();
let active = zelf.active.read().clone();
let cls = zelf.class().to_owned();
let empty_tuple = vm.ctx.empty_tuple.clone();
let reduced = match source {
Some(source) => match active {
Some(active) => vm.new_tuple((cls, empty_tuple, (source, active))),
None => vm.new_tuple((cls, empty_tuple, (source,))),
},
None => vm.new_tuple((cls, empty_tuple)),
};
Ok(reduced)
}
#[pymethod]
fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
let args = state.as_slice();
if args.is_empty() {
return Err(vm.new_type_error("function takes at least 1 arguments (0 given)"));
}
if args.len() > 2 {
return Err(vm.new_type_error(format!(
"function takes at most 2 arguments ({} given)",
args.len()
)));
}
let source = &args[0];
if args.len() == 1 {
if !PyIter::check(source.as_ref()) {
return Err(vm.new_type_error("Arguments must be iterators."));
}
*zelf.source.write() = source.to_owned().try_into_value(vm)?;
return Ok(());
}
let active = &args[1];
if !PyIter::check(source.as_ref()) || !PyIter::check(active.as_ref()) {
return Err(vm.new_type_error("Arguments must be iterators."));
}
let mut source_lock = zelf.source.write();
let mut active_lock = zelf.active.write();
*source_lock = source.to_owned().try_into_value(vm)?;
*active_lock = active.to_owned().try_into_value(vm)?;
Ok(())
}
}
impl SelfIter for PyItertoolsChain {}
@@ -209,16 +146,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
impl PyItertoolsCompress {
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> (PyTypeRef, (PyIter, PyIter)) {
let _ = pickle_deprecation(vm);
(
zelf.class().to_owned(),
(zelf.data.clone(), zelf.selectors.clone()),
)
}
}
impl PyItertoolsCompress {}
impl SelfIter for PyItertoolsCompress {}
@@ -275,16 +203,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor, Representable))]
impl PyItertoolsCount {
// TODO: Implement this
// if (lz->cnt == PY_SSIZE_T_MAX)
// return Py_BuildValue("0(00)", Py_TYPE(lz), lz->long_cnt, lz->long_step);
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> (PyTypeRef, (PyObjectRef,)) {
let _ = pickle_deprecation(vm);
(zelf.class().to_owned(), (zelf.cur.read().clone(),))
}
}
impl PyItertoolsCount {}
impl SelfIter for PyItertoolsCount {}
@@ -406,16 +325,6 @@ mod decl {
.ok_or_else(|| vm.new_type_error("length of unsized object."))?;
Ok(*times.read())
}
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
pickle_deprecation(vm)?;
let cls = zelf.class().to_owned();
Ok(match zelf.times {
Some(ref times) => vm.new_tuple((cls, (zelf.object.clone(), *times.read()))),
None => vm.new_tuple((cls, (zelf.object.clone(),))),
})
}
}
impl SelfIter for PyItertoolsRepeat {}
@@ -474,19 +383,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
impl PyItertoolsStarmap {
#[pymethod]
fn __reduce__(
zelf: PyRef<Self>,
vm: &VirtualMachine,
) -> (PyTypeRef, (PyObjectRef, PyIter)) {
let _ = pickle_deprecation(vm);
(
zelf.class().to_owned(),
(zelf.function.clone(), zelf.iterable.clone()),
)
}
}
impl PyItertoolsStarmap {}
impl SelfIter for PyItertoolsStarmap {}
@@ -541,31 +438,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
impl PyItertoolsTakewhile {
#[pymethod]
fn __reduce__(
zelf: PyRef<Self>,
vm: &VirtualMachine,
) -> (PyTypeRef, (PyObjectRef, PyIter), u32) {
let _ = pickle_deprecation(vm);
(
zelf.class().to_owned(),
(zelf.predicate.clone(), zelf.iterable.clone()),
zelf.stop_flag.load() as _,
)
}
#[pymethod]
fn __setstate__(
zelf: PyRef<Self>,
state: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
if let Ok(obj) = ArgIntoBool::try_from_object(vm, state) {
zelf.stop_flag.store(obj.into_bool());
}
Ok(())
}
}
impl PyItertoolsTakewhile {}
impl SelfIter for PyItertoolsTakewhile {}
@@ -627,32 +500,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
impl PyItertoolsDropwhile {
#[pymethod]
fn __reduce__(
zelf: PyRef<Self>,
vm: &VirtualMachine,
) -> (PyTypeRef, (PyObjectRef, PyIter), u32) {
let _ = pickle_deprecation(vm);
(
zelf.class().to_owned(),
(zelf.predicate.clone().into(), zelf.iterable.clone()),
(zelf.start_flag.load() as _),
)
}
#[pymethod]
fn __setstate__(
zelf: PyRef<Self>,
state: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
if let Ok(obj) = ArgIntoBool::try_from_object(vm, state) {
zelf.start_flag.store(obj.into_bool());
}
Ok(())
}
}
impl PyItertoolsDropwhile {}
impl SelfIter for PyItertoolsDropwhile {}
@@ -942,38 +790,6 @@ mod decl {
.into_ref_with_type(vm, cls)
.map(Into::into)
}
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
pickle_deprecation(vm)?;
let cls = zelf.class().to_owned();
let itr = zelf.iterable.clone();
let cur = zelf.cur.take();
let next = zelf.next.take();
let step = zelf.step;
match zelf.stop {
Some(stop) => Ok(vm.new_tuple((cls, (itr, next, stop, step), (cur,)))),
_ => Ok(vm.new_tuple((cls, (itr, next, vm.new_pyobj(()), step), (cur,)))),
}
}
#[pymethod]
fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
let args = state.as_slice();
if args.len() != 1 {
return Err(vm.new_type_error(format!(
"function takes exactly 1 argument ({} given)",
args.len()
)));
}
let cur = &args[0];
if let Ok(cur) = cur.try_to_value(vm) {
zelf.cur.store(cur);
} else {
return Err(vm.new_type_error("Argument must be usize."));
}
Ok(())
}
}
impl SelfIter for PyItertoolsIslice {}
@@ -1037,19 +853,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
impl PyItertoolsFilterFalse {
#[pymethod]
fn __reduce__(
zelf: PyRef<Self>,
vm: &VirtualMachine,
) -> (PyTypeRef, (PyObjectRef, PyIter)) {
let _ = pickle_deprecation(vm);
(
zelf.class().to_owned(),
(zelf.predicate.clone(), zelf.iterable.clone()),
)
}
}
impl PyItertoolsFilterFalse {}
impl SelfIter for PyItertoolsFilterFalse {}
@@ -1106,59 +910,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor))]
impl PyItertoolsAccumulate {
#[pymethod]
fn __setstate__(
zelf: PyRef<Self>,
state: PyObjectRef,
_vm: &VirtualMachine,
) -> PyResult<()> {
*zelf.acc_value.write() = Some(state);
Ok(())
}
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyTupleRef {
let _ = pickle_deprecation(vm);
let class = zelf.class().to_owned();
let bin_op = zelf.bin_op.clone();
let it = zelf.iterable.clone();
let acc_value = zelf.acc_value.read().clone();
if let Some(initial) = &zelf.initial {
let chain_args = PyList::from(vec![initial.clone(), it.to_pyobject(vm)]);
let chain = PyItertoolsChain {
source: PyRwLock::new(Some(chain_args.to_pyobject(vm).get_iter(vm).unwrap())),
active: PyRwLock::new(None),
};
let tup = vm.new_tuple((chain, bin_op));
return vm.new_tuple((class, tup, acc_value));
}
match acc_value {
Some(obj) if obj.is(&vm.ctx.none) => {
let chain_args = PyList::from(vec![]);
let chain = PyItertoolsChain {
source: PyRwLock::new(Some(
chain_args.to_pyobject(vm).get_iter(vm).unwrap(),
)),
active: PyRwLock::new(None),
}
.into_pyobject(vm);
let acc = Self {
iterable: PyIter::new(chain),
bin_op,
initial: None,
acc_value: PyRwLock::new(None),
};
let tup = vm.new_tuple((acc, 1, None::<PyObjectRef>));
let islice_cls = PyItertoolsIslice::class(&vm.ctx).to_owned();
return vm.new_tuple((islice_cls, tup));
}
_ => {}
}
let tup = vm.new_tuple((it, bin_op));
vm.new_tuple((class, tup, acc_value))
}
}
impl PyItertoolsAccumulate {}
impl SelfIter for PyItertoolsAccumulate {}
@@ -1359,58 +1111,6 @@ mod decl {
self.cur.store(idxs.len() - 1);
}
}
#[pymethod]
fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
let args = state.as_slice();
if args.len() != zelf.pools.len() {
return Err(vm.new_type_error("Invalid number of arguments"));
}
let mut idxs: PyRwLockWriteGuard<'_, Vec<usize>> = zelf.idxs.write();
idxs.clear();
for s in 0..args.len() {
let index = get_value(state.get(s).unwrap()).to_usize().unwrap();
let pool_size = zelf.pools.get(s).unwrap().len();
if pool_size == 0 {
zelf.stop.store(true);
return Ok(());
}
if index >= pool_size {
idxs.push(pool_size - 1);
} else {
idxs.push(index);
}
}
zelf.stop.store(false);
Ok(())
}
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyTupleRef {
let _ = pickle_deprecation(vm);
let class = zelf.class().to_owned();
if zelf.stop.load() {
return vm.new_tuple((class, (vm.ctx.empty_tuple.clone(),)));
}
let mut pools: Vec<PyObjectRef> = Vec::new();
for element in &zelf.pools {
pools.push(element.clone().into_pytuple(vm).into());
}
let mut indices: Vec<PyObjectRef> = Vec::new();
for item in &zelf.idxs.read()[..] {
indices.push(vm.new_pyobj(*item));
}
vm.new_tuple((
class,
pools.clone().into_pytuple(vm),
indices.into_pytuple(vm),
))
}
}
impl SelfIter for PyItertoolsProduct {}
@@ -1492,36 +1192,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor))]
impl PyItertoolsCombinations {
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyTupleRef {
let _ = pickle_deprecation(vm);
let r = zelf.r.load();
let class = zelf.class().to_owned();
if zelf.exhausted.load() {
return vm.new_tuple((
class,
vm.new_tuple((vm.ctx.empty_tuple.clone(), vm.ctx.new_int(r))),
));
}
let tup = vm.new_tuple((zelf.pool.clone().into_pytuple(vm), vm.ctx.new_int(r)));
if zelf.result.read().is_none() {
vm.new_tuple((class, tup))
} else {
let mut indices: Vec<PyObjectRef> = Vec::new();
for item in &zelf.indices.read()[..r] {
indices.push(vm.new_pyobj(*item));
}
vm.new_tuple((class, tup, indices.into_pytuple(vm)))
}
}
}
impl PyItertoolsCombinations {}
impl SelfIter for PyItertoolsCombinations {}
impl IterNext for PyItertoolsCombinations {
@@ -1730,16 +1401,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor))]
impl PyItertoolsPermutations {
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyRef<PyTuple> {
let _ = pickle_deprecation(vm);
vm.new_tuple((
zelf.class().to_owned(),
vm.new_tuple((zelf.pool.clone(), vm.ctx.new_int(zelf.r.load()))),
))
}
}
impl PyItertoolsPermutations {}
impl SelfIter for PyItertoolsPermutations {}
@@ -1846,32 +1508,7 @@ mod decl {
}
#[pyclass(with(IterNext, Iterable, Constructor))]
impl PyItertoolsZipLongest {
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
pickle_deprecation(vm)?;
let args: Vec<PyObjectRef> = zelf
.iterators
.iter()
.map(|i| i.clone().to_pyobject(vm))
.collect();
Ok(vm.new_tuple((
zelf.class().to_owned(),
vm.new_tuple(args),
zelf.fillvalue.read().to_owned(),
)))
}
#[pymethod]
fn __setstate__(
zelf: PyRef<Self>,
state: PyObjectRef,
_vm: &VirtualMachine,
) -> PyResult<()> {
*zelf.fillvalue.write() = state;
Ok(())
}
}
impl PyItertoolsZipLongest {}
impl SelfIter for PyItertoolsZipLongest {}
+5 -2
View File
@@ -1,8 +1,10 @@
//! Implementation of the _thread module
#[cfg(unix)]
pub(crate) use _thread::after_fork_child;
#[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
pub(crate) use _thread::{
CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, after_fork_child,
get_all_current_frames, get_ident, init_main_thread_ident, make_module,
CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, get_all_current_frames, get_ident,
init_main_thread_ident, make_module,
};
#[pymodule]
@@ -882,6 +884,7 @@ pub(crate) mod _thread {
/// Called after fork() in child process to mark all other threads as done.
/// This prevents join() from hanging on threads that don't exist in the child.
#[cfg(unix)]
pub fn after_fork_child(vm: &VirtualMachine) {
let current_ident = get_ident();
+2 -2
View File
@@ -4,9 +4,9 @@ use chrono::{Local, prelude::DateTime};
use core::time::Duration;
use std::time::UNIX_EPOCH;
// = 3.13.0alpha
// = 3.14.0alpha
pub const MAJOR: usize = 3;
pub const MINOR: usize = 13;
pub const MINOR: usize = 14;
pub const MICRO: usize = 0;
pub const RELEASELEVEL: &str = "alpha";
pub const RELEASELEVEL_N: usize = 0xA;
+3
View File
@@ -91,7 +91,10 @@ declare_const_name! {
__all__,
__and__,
__anext__,
__annotate__,
__annotate_func__,
__annotations__,
__annotations_cache__,
__args__,
__await__,
__bases__,
+1 -1
View File
@@ -523,7 +523,7 @@ impl VirtualMachine {
/// ```no_run
/// use rustpython_vm::Interpreter;
/// Interpreter::without_stdlib(Default::default()).enter(|vm| {
/// let bytes = std::fs::read("__pycache__/<input>.rustpython-313.pyc").unwrap();
/// let bytes = std::fs::read("__pycache__/<input>.rustpython-314.pyc").unwrap();
/// let main_scope = vm.new_scope_with_main().unwrap();
/// vm.run_pyc_bytes(&bytes, main_scope);
/// });
-4
View File
@@ -65,10 +65,6 @@ impl VirtualMachine {
pub fn new_scope_with_main(&self) -> PyResult<Scope> {
let scope = self.new_scope_with_builtins();
let main_module = self.new_module("__main__", scope.globals.clone(), None);
main_module
.dict()
.set_item("__annotations__", self.ctx.new_dict().into(), self)
.expect("Failed to initialize __main__.__annotations__");
self.sys_module.get_attr("modules", self)?.set_item(
"__main__",
+3 -1
View File
@@ -18,7 +18,9 @@ assert bool() is False
assert bool(1) is True
assert bool({}) is False
assert bool(NotImplemented) is True
# NotImplemented cannot be used in a boolean context (Python 3.14+)
with assert_raises(TypeError):
bool(NotImplemented)
assert bool(...) is True
if not 1:
+28 -3
View File
@@ -153,16 +153,41 @@ assert b"AaBbCc123'@/".swapcase() == b"aAbBcC123'@/"
# # hex from hex
assert bytearray([0, 1, 9, 23, 90, 234]).hex() == "000109175aea"
bytearray.fromhex("62 6c7a 34350a ") == b"blz45\n"
# fromhex with str
assert bytearray.fromhex("62 6c7a 34350a ") == b"blz45\n"
# fromhex with bytes
assert bytearray.fromhex(b"62 6c7a 34350a ") == b"blz45\n"
assert bytearray.fromhex(b"B9 01EF") == b"\xb9\x01\xef"
# fromhex with bytearray (bytes-like object)
assert bytearray.fromhex(bytearray(b"4142")) == b"AB"
# fromhex with memoryview (bytes-like object)
assert bytearray.fromhex(memoryview(b"4142")) == b"AB"
# fromhex error: non-hexadecimal character
try:
bytearray.fromhex("62 a 21")
except ValueError as e:
str(e) == "non-hexadecimal number found in fromhex() arg at position 4"
assert str(e) == "non-hexadecimal number found in fromhex() arg at position 4"
try:
bytearray.fromhex("6Z2")
except ValueError as e:
str(e) == "non-hexadecimal number found in fromhex() arg at position 1"
assert str(e) == "non-hexadecimal number found in fromhex() arg at position 1"
# fromhex error: odd number of hex digits
try:
bytearray.fromhex("abc")
except ValueError as e:
assert str(e) == "fromhex() arg must contain an even number of hexadecimal digits"
# fromhex error: wrong type
with assert_raises(TypeError):
bytearray.fromhex(123)
# fromhex with bytes containing invalid hex raises ValueError
with assert_raises(ValueError):
bytearray.fromhex(b"hhjjk")
# center
assert [bytearray(b"koki").center(i, b"|") for i in range(3, 10)] == [
+28 -3
View File
@@ -137,16 +137,41 @@ assert b"AaBbCc123'@/".swapcase() == b"aAbBcC123'@/"
# hex from hex
assert bytes([0, 1, 9, 23, 90, 234]).hex() == "000109175aea"
bytes.fromhex("62 6c7a 34350a ") == b"blz45\n"
# fromhex with str
assert bytes.fromhex("62 6c7a 34350a ") == b"blz45\n"
# fromhex with bytes
assert bytes.fromhex(b"62 6c7a 34350a ") == b"blz45\n"
assert bytes.fromhex(b"B9 01EF") == b"\xb9\x01\xef"
# fromhex with bytearray (bytes-like object)
assert bytes.fromhex(bytearray(b"4142")) == b"AB"
# fromhex with memoryview (bytes-like object)
assert bytes.fromhex(memoryview(b"4142")) == b"AB"
# fromhex error: non-hexadecimal character
try:
bytes.fromhex("62 a 21")
except ValueError as e:
str(e) == "non-hexadecimal number found in fromhex() arg at position 4"
assert str(e) == "non-hexadecimal number found in fromhex() arg at position 4"
try:
bytes.fromhex("6Z2")
except ValueError as e:
str(e) == "non-hexadecimal number found in fromhex() arg at position 1"
assert str(e) == "non-hexadecimal number found in fromhex() arg at position 1"
# fromhex error: odd number of hex digits
try:
bytes.fromhex("abc")
except ValueError as e:
assert str(e) == "fromhex() arg must contain an even number of hexadecimal digits"
# fromhex error: wrong type
with assert_raises(TypeError):
bytes.fromhex(123)
# fromhex with bytes containing invalid hex raises ValueError
with assert_raises(ValueError):
bytes.fromhex(b"hhjjk")
# center
assert [b"koki".center(i, b"|") for i in range(3, 10)] == [
+27 -12
View File
@@ -239,12 +239,10 @@ try:
raise e
except MyError as exc:
# It was a segmentation fault before, will print info to stdout:
if platform.python_implementation() == "RustPython":
# For some reason `CPython` hangs on this code:
sys.excepthook(type(exc), exc, exc.__traceback__)
assert isinstance(exc, MyError)
assert exc.__cause__ is None
assert exc.__context__ is e
sys.excepthook(type(exc), exc, exc.__traceback__)
assert isinstance(exc, MyError)
assert exc.__cause__ is None
assert exc.__context__ is e
# Regression to
@@ -255,26 +253,42 @@ assert BaseException.__new__.__qualname__ == "BaseException.__new__"
assert BaseException.__init__.__qualname__ == "BaseException.__init__"
assert BaseException().__dict__ == {}
# Exception inherits __init__ from BaseException
assert Exception.__new__.__qualname__ == "Exception.__new__", (
Exception.__new__.__qualname__
)
assert Exception.__init__.__qualname__ == "Exception.__init__", (
assert Exception.__init__.__qualname__ == "BaseException.__init__", (
Exception.__init__.__qualname__
)
assert Exception().__dict__ == {}
# Extends `BaseException`, simple:
# Extends `BaseException`, simple - inherits __init__ from BaseException:
assert KeyboardInterrupt.__new__.__qualname__ == "KeyboardInterrupt.__new__", (
KeyboardInterrupt.__new__.__qualname__
)
assert KeyboardInterrupt.__init__.__qualname__ == "KeyboardInterrupt.__init__"
assert KeyboardInterrupt.__init__.__qualname__ == "BaseException.__init__"
assert KeyboardInterrupt().__dict__ == {}
# Extends `Exception`, simple:
# Extends `BaseException`, complex - has its own __init__:
# SystemExit_init sets self.code based on args length
assert SystemExit.__init__.__qualname__ == "SystemExit.__init__"
assert SystemExit.__dict__.get("__init__") is not None, (
"SystemExit must have its own __init__"
)
assert SystemExit.__init__ is not BaseException.__init__
assert SystemExit().__dict__ == {}
# SystemExit.code behavior:
assert SystemExit().code is None
assert SystemExit(1).code == 1
assert SystemExit(1, 2).code == (1, 2)
assert SystemExit(1, 2, 3).code == (1, 2, 3)
# Extends `Exception`, simple - inherits __init__ from BaseException:
assert TypeError.__new__.__qualname__ == "TypeError.__new__"
assert TypeError.__init__.__qualname__ == "TypeError.__init__"
assert TypeError.__init__.__qualname__ == "BaseException.__init__"
assert TypeError().__dict__ == {}
@@ -356,7 +370,8 @@ assert str(x) == "('a', 'b', 'c', 'd', 'e', 'f')"
# Custom `__new__` and `__init__`:
assert ImportError.__init__.__qualname__ == "ImportError.__init__"
assert ImportError(name="a").name == "a"
assert ModuleNotFoundError.__init__.__qualname__ == "ModuleNotFoundError.__init__"
# ModuleNotFoundError inherits __init__ from ImportError via MRO (MiddlingExtendsException)
assert ModuleNotFoundError.__init__.__qualname__ == "ImportError.__init__"
assert ModuleNotFoundError(name="a").name == "a"
+3 -2
View File
@@ -318,8 +318,9 @@ assert isinstance((0).__round__(0), int)
assert isinstance((1).__round__(0), int)
assert (0).__round__(0) == 0
assert (1).__round__(0) == 1
assert_raises(TypeError, lambda: (0).__round__(None))
assert_raises(TypeError, lambda: (1).__round__(None))
# Python 3.14+: __round__(None) is now allowed, same as __round__()
assert (0).__round__(None) == 0
assert (1).__round__(None) == 1
assert_raises(TypeError, lambda: (0).__round__(0.0))
assert_raises(TypeError, lambda: (1).__round__(0.0))
+98 -25
View File
@@ -1,39 +1,112 @@
"""
Test co_consts behavior for Python 3.14+
In Python 3.14+:
- Functions with docstrings have the docstring as co_consts[0]
- CO_HAS_DOCSTRING flag (0x4000000) indicates docstring presence
- Functions without docstrings do NOT have None added as placeholder for docstring
Note: Other constants (small integers, code objects, etc.) may still appear in co_consts
depending on optimization level. This test focuses on docstring behavior.
"""
# Test function with docstring - docstring should be co_consts[0]
def with_doc():
"""This is a docstring"""
return 1
assert with_doc.__code__.co_consts[0] == "This is a docstring", (
with_doc.__code__.co_consts
)
assert with_doc.__doc__ == "This is a docstring"
# Check CO_HAS_DOCSTRING flag (0x4000000)
assert with_doc.__code__.co_flags & 0x4000000, hex(with_doc.__code__.co_flags)
# Test function without docstring - should NOT have HAS_DOCSTRING flag
def no_doc():
return 1
assert not (no_doc.__code__.co_flags & 0x4000000), hex(no_doc.__code__.co_flags)
assert no_doc.__doc__ is None
# Test async function with docstring
from asyncio import sleep
def f():
def g():
return 1
assert g.__code__.co_consts[0] == None
return 2
assert f.__code__.co_consts[0] == None
def generator():
yield 1
yield 2
assert generator().gi_code.co_consts[0] == None
async def async_f():
async def async_with_doc():
"""Async docstring"""
await sleep(1)
return 1
assert async_f.__code__.co_consts[0] == None
assert async_with_doc.__code__.co_consts[0] == "Async docstring", (
async_with_doc.__code__.co_consts
)
assert async_with_doc.__doc__ == "Async docstring"
assert async_with_doc.__code__.co_flags & 0x4000000
# Test async function without docstring
async def async_no_doc():
await sleep(1)
return 1
assert not (async_no_doc.__code__.co_flags & 0x4000000)
assert async_no_doc.__doc__ is None
# Test generator with docstring
def gen_with_doc():
"""Generator docstring"""
yield 1
yield 2
assert gen_with_doc.__code__.co_consts[0] == "Generator docstring"
assert gen_with_doc.__doc__ == "Generator docstring"
assert gen_with_doc.__code__.co_flags & 0x4000000
# Test generator without docstring
def gen_no_doc():
yield 1
yield 2
assert not (gen_no_doc.__code__.co_flags & 0x4000000)
assert gen_no_doc.__doc__ is None
# Test lambda - cannot have docstring
lambda_f = lambda: 0
assert lambda_f.__code__.co_consts[0] == None
assert not (lambda_f.__code__.co_flags & 0x4000000)
assert lambda_f.__doc__ is None
class cls:
def f():
# Test class method with docstring
class cls_with_doc:
def method():
"""Method docstring"""
return 1
assert cls().f.__code__.co_consts[0] == None
assert cls_with_doc.method.__code__.co_consts[0] == "Method docstring"
assert cls_with_doc.method.__doc__ == "Method docstring"
# Test class method without docstring
class cls_no_doc:
def method():
return 1
assert not (cls_no_doc.method.__code__.co_flags & 0x4000000)
assert cls_no_doc.method.__doc__ is None
print("All co_consts tests passed!")
+2 -2
View File
@@ -4,7 +4,7 @@ code_class = type(c1)
def f(x, y, *args, power=1, **kwargs):
print("Constant String", 2, None, (2, 4))
print("Constant String", 256, None, (2, 4))
assert code_class == type(c1)
z = x * y
return z**power
@@ -19,7 +19,7 @@ assert c2.co_argcount == 2
# assert isinstance(c2.co_code, bytes)
assert "Constant String" in c2.co_consts, c2.co_consts
print(c2.co_consts)
assert 2 in c2.co_consts, c2.co_consts
assert 256 in c2.co_consts, c2.co_consts
assert "example_interactive.py" in c2.co_filename
assert c2.co_firstlineno == 6, str(c2.co_firstlineno)
# assert isinstance(c2.co_flags, int) # 'OPTIMIZED, NEWLOCALS, NOFREE'
-134
View File
@@ -1,5 +1,4 @@
import itertools
import pickle
from testutils import assert_raises
@@ -181,10 +180,6 @@ with assert_raises(TypeError):
# itertools.takewhile tests
def underten(x):
return x < 10
from itertools import takewhile as tw
t = tw(lambda n: n < 5, [1, 2, 5, 1, 3])
@@ -226,70 +221,6 @@ with assert_raises(StopIteration):
with assert_raises(StopIteration):
next(t)
it = tw(underten, [1, 3, 5, 20, 2, 4, 6, 8])
assert (
pickle.dumps(it, 0)
== b"citertools\ntakewhile\np0\n(c__main__\nunderten\np1\nc__builtin__\niter\np2\n((lp3\nI1\naI3\naI5\naI20\naI2\naI4\naI6\naI8\natp4\nRp5\nI0\nbtp6\nRp7\nI0\nb."
)
assert (
pickle.dumps(it, 1)
== b"citertools\ntakewhile\nq\x00(c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02(]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08etq\x04Rq\x05K\x00btq\x06Rq\x07K\x00b."
)
assert (
pickle.dumps(it, 2)
== b"\x80\x02citertools\ntakewhile\nq\x00c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x00b\x86q\x06Rq\x07K\x00b."
)
assert (
pickle.dumps(it, 3)
== b"\x80\x03citertools\ntakewhile\nq\x00c__main__\nunderten\nq\x01cbuiltins\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x00b\x86q\x06Rq\x07K\x00b."
)
assert (
pickle.dumps(it, 4)
== b"\x80\x04\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\ttakewhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x00b\x86\x94R\x94K\x00b."
)
assert (
pickle.dumps(it, 5)
== b"\x80\x05\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\ttakewhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x00b\x86\x94R\x94K\x00b."
)
next(it)
next(it)
next(it)
try:
next(it)
except StopIteration:
pass
assert (
pickle.dumps(it, 0)
== b"citertools\ntakewhile\np0\n(c__main__\nunderten\np1\nc__builtin__\niter\np2\n((lp3\nI1\naI3\naI5\naI20\naI2\naI4\naI6\naI8\natp4\nRp5\nI4\nbtp6\nRp7\nI1\nb."
)
assert (
pickle.dumps(it, 1)
== b"citertools\ntakewhile\nq\x00(c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02(]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08etq\x04Rq\x05K\x04btq\x06Rq\x07K\x01b."
)
assert (
pickle.dumps(it, 2)
== b"\x80\x02citertools\ntakewhile\nq\x00c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x04b\x86q\x06Rq\x07K\x01b."
)
assert (
pickle.dumps(it, 3)
== b"\x80\x03citertools\ntakewhile\nq\x00c__main__\nunderten\nq\x01cbuiltins\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x04b\x86q\x06Rq\x07K\x01b."
)
assert (
pickle.dumps(it, 4)
== b"\x80\x04\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\ttakewhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x04b\x86\x94R\x94K\x01b."
)
assert (
pickle.dumps(it, 5)
== b"\x80\x05\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\ttakewhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x04b\x86\x94R\x94K\x01b."
)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
try:
next(pickle.loads(pickle.dumps(it, proto)))
assert False
except StopIteration:
pass
# itertools.islice tests
@@ -297,40 +228,28 @@ def assert_matches_seq(it, seq):
assert list(it) == list(seq)
def test_islice_pickle(it):
for p in range(pickle.HIGHEST_PROTOCOL + 1):
it == pickle.loads(pickle.dumps(it, p))
i = itertools.islice
it = i([1, 2, 3, 4, 5], 3)
assert_matches_seq(it, [1, 2, 3])
test_islice_pickle(it)
it = i([0.5, 1, 1.5, 2, 2.5, 3, 4, 5], 1, 6, 2)
assert_matches_seq(it, [1, 2, 3])
test_islice_pickle(it)
it = i([1, 2], None)
assert_matches_seq(it, [1, 2])
test_islice_pickle(it)
it = i([1, 2, 3], None, None, None)
assert_matches_seq(it, [1, 2, 3])
test_islice_pickle(it)
it = i([1, 2, 3], 1, None, None)
assert_matches_seq(it, [2, 3])
test_islice_pickle(it)
it = i([1, 2, 3], None, 2, None)
assert_matches_seq(it, [1, 2])
test_islice_pickle(it)
it = i([1, 2, 3], None, None, 3)
assert_matches_seq(it, [1])
test_islice_pickle(it)
# itertools.filterfalse
it = itertools.filterfalse(lambda x: x % 2, range(10))
@@ -359,59 +278,6 @@ assert 1 == next(it)
with assert_raises(StopIteration):
next(it)
it = itertools.dropwhile(underten, [1, 3, 5, 20, 2, 4, 6, 8])
assert (
pickle.dumps(it, 0)
== b"citertools\ndropwhile\np0\n(c__main__\nunderten\np1\nc__builtin__\niter\np2\n((lp3\nI1\naI3\naI5\naI20\naI2\naI4\naI6\naI8\natp4\nRp5\nI0\nbtp6\nRp7\nI0\nb."
)
assert (
pickle.dumps(it, 1)
== b"citertools\ndropwhile\nq\x00(c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02(]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08etq\x04Rq\x05K\x00btq\x06Rq\x07K\x00b."
)
assert (
pickle.dumps(it, 2)
== b"\x80\x02citertools\ndropwhile\nq\x00c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x00b\x86q\x06Rq\x07K\x00b."
)
assert (
pickle.dumps(it, 3)
== b"\x80\x03citertools\ndropwhile\nq\x00c__main__\nunderten\nq\x01cbuiltins\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x00b\x86q\x06Rq\x07K\x00b."
)
assert (
pickle.dumps(it, 4)
== b"\x80\x04\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\tdropwhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x00b\x86\x94R\x94K\x00b."
)
assert (
pickle.dumps(it, 5)
== b"\x80\x05\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\tdropwhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x00b\x86\x94R\x94K\x00b."
)
next(it)
assert (
pickle.dumps(it, 0)
== b"citertools\ndropwhile\np0\n(c__main__\nunderten\np1\nc__builtin__\niter\np2\n((lp3\nI1\naI3\naI5\naI20\naI2\naI4\naI6\naI8\natp4\nRp5\nI4\nbtp6\nRp7\nI1\nb."
)
assert (
pickle.dumps(it, 1)
== b"citertools\ndropwhile\nq\x00(c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02(]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08etq\x04Rq\x05K\x04btq\x06Rq\x07K\x01b."
)
assert (
pickle.dumps(it, 2)
== b"\x80\x02citertools\ndropwhile\nq\x00c__main__\nunderten\nq\x01c__builtin__\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x04b\x86q\x06Rq\x07K\x01b."
)
assert (
pickle.dumps(it, 3)
== b"\x80\x03citertools\ndropwhile\nq\x00c__main__\nunderten\nq\x01cbuiltins\niter\nq\x02]q\x03(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85q\x04Rq\x05K\x04b\x86q\x06Rq\x07K\x01b."
)
assert (
pickle.dumps(it, 4)
== b"\x80\x04\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\tdropwhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x04b\x86\x94R\x94K\x01b."
)
assert (
pickle.dumps(it, 5)
== b"\x80\x05\x95i\x00\x00\x00\x00\x00\x00\x00\x8c\titertools\x94\x8c\tdropwhile\x94\x93\x94\x8c\x08__main__\x94\x8c\x08underten\x94\x93\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x03K\x05K\x14K\x02K\x04K\x06K\x08e\x85\x94R\x94K\x04b\x86\x94R\x94K\x01b."
)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
assert next(pickle.loads(pickle.dumps(it, proto))) == 2
# itertools.accumulate
it = itertools.accumulate([6, 3, 7, 1, 0, 9, 8, 8])
+3 -2
View File
@@ -131,8 +131,9 @@ with assert_raises(OSError):
with assert_raises(OSError):
socket.inet_aton("test")
with assert_raises(OverflowError):
socket.htonl(-1)
# TODO: RUSTPYTHON
# with assert_raises(ValueError):
# socket.htonl(-1)
assert socket.htonl(0) == 0
assert socket.htonl(10) == 167772160
+23
View File
@@ -12,3 +12,26 @@ def abort_signal_handler(
# Ensure PEP 604 unions work with typing.Callable aliases.
TracebackFilter = bool | Callable[[int], int]
# Test that Union/Optional in function parameter annotations work correctly.
# This tests that annotation scopes can access global implicit symbols (like Union)
# that are imported at module level but not explicitly bound in the function scope.
# Regression test for: rich
from typing import Optional, Union
def function_with_union_param(x: Optional[Union[int, str]] = None) -> None:
pass
class ClassWithUnionParams:
def __init__(
self,
color: Optional[Union[str, int]] = None,
bold: Optional[bool] = None,
) -> None:
pass
def method(self, value: Union[int, float]) -> Union[str, bytes]:
return str(value)
+12 -1
View File
@@ -59,7 +59,18 @@ b: bool = False
assert a == 1337
assert b == False
assert __annotations__['a'] == bool
# PEP 649: In Python 3.14, __annotations__ is not automatically defined at module level
# Accessing it raises NameError
from testutils import assert_raises
with assert_raises(NameError):
__annotations__
# Use __annotate__ to get annotations (PEP 649)
assert callable(__annotate__)
annotations = __annotate__(1) # 1 = FORMAT_VALUE
assert annotations['a'] == bool
assert annotations['b'] == bool
n = 0
+2 -1
View File
@@ -80,6 +80,7 @@ f6()
def f7():
# PEP 649: annotations are deferred, so void is not evaluated at definition time
try:
def t() -> void: # noqa: F821
pass
@@ -87,7 +88,7 @@ def f7():
return True
return False
assert f7()
assert not f7() # PEP 649: no NameError because annotation is deferred
def f8() -> int:
+2 -1
View File
@@ -24,6 +24,7 @@ Manual workflow:
import argparse
import ast
import re
import shutil
import sys
from pathlib import Path
@@ -82,7 +83,7 @@ def parse_results(result):
test_results.stdout = result.stdout
in_test_results = False
for line in lines:
if line == "Run tests sequentially":
if re.match(r"Run tests? sequentially", line):
in_test_results = True
elif line.startswith("-----------"):
in_test_results = False
+6 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env -S python3 -I
# /// script
# requires-python = ">=3.13"
# requires-python = ">=3.14"
# ///
# This script generates Lib/snippets/whats_left_data.py with these variables defined:
@@ -37,9 +37,9 @@ GENERATED_FILE = "extra_tests/not_impl.py"
implementation = platform.python_implementation()
if implementation != "CPython":
sys.exit(f"whats_left.py must be run under CPython, got {implementation} instead")
if sys.version_info[:2] < (3, 13):
if sys.version_info[:2] < (3, 14):
sys.exit(
f"whats_left.py must be run under CPython 3.13 or newer, got {implementation} {sys.version} instead. If you have uv, try `uv run python -I whats_left.py` to select a proper Python interpreter easier."
f"whats_left.py must be run under CPython 3.14 or newer, got {implementation} {sys.version} instead. If you have uv, try `uv run python -I whats_left.py` to select a proper Python interpreter easier."
)
@@ -195,6 +195,9 @@ def gen_methods():
typ = eval(typ_code)
attrs = []
for attr in dir(typ):
# Skip attributes in dir() but not actually accessible (e.g., descriptor that raises)
if not hasattr(typ, attr):
continue
if attr_is_not_inherited(typ, attr):
attrs.append((attr, extra_info(getattr(typ, attr))))
methods[typ.__name__] = (typ_code, extra_info(typ), attrs)