GeezerVison is showing github.com — Implement more typing by youknowone · Pull Request #7078 · RustPython/RustPython
Implement more typing
#7078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement . We’ll occasionally send you account related emails.
Already on GitHub?
Sign in
to your account
Uh oh!
There was an error while loading. Please reload this page.
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
d1e8122
Add _ConstEvaluator, evaluate_* getters, format validation
youknowone Feb 11, 2026
c91f27c
Fix nonlocal type param check, __class_getitem__, __set_name__ note, …
youknowone Feb 11, 2026
6a6e375
Propagate can_see_class_scope to annotation scopes in type param blocks
youknowone Feb 11, 2026
5242dd4
Rename constevaluator_alloc to const_evaluator_alloc, add to cspell dict
youknowone Feb 11, 2026
File filter
Filter by extension
.py
(3)
.rs
(9)
.txt
(1)
All 3 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Hide whitespace
Diff view
Hide whitespace
There are no files selected for viewing
Original file line number
Diff line number
Diff line change
Expand Up
@@ -31,6 +31,7 @@ CLASSDEREF
classdict
cmpop
codedepth
constevaluator
CODEUNIT
CONVFUNC
convparam
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -2107,7 +2107,6 @@ def test_issue17998(self):
self.assertEqual(re.compile(pattern, re.S).findall(b'xyz'),
[b'xyz'], msg=pattern)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_match_repr(self):
for string in '[abracadabra]', S('[abracadabra]'):
m = re.search(r'(.+)(.*?)\1', string)
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -165,7 +165,6 @@ def inner[X]():
"""
check_syntax_error(self, code)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
def test_nonlocal_disallowed_02(self):
code = """
def outer2[T]():
Expand All
@@ -174,7 +173,6 @@ def inner1():
"""
check_syntax_error(self, textwrap.dedent(code))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
def test_nonlocal_disallowed_03(self):
code = """
class Cls[T]:
Expand Down
Expand Up
@@ -268,7 +266,6 @@ def func[A]():
with self.assertRaisesRegex(NameError, "name 'A' is not defined"):
run_code(code)
@unittest.expectedFailure # TODO: RUSTPYTHON; NameError: name 'x' is not defined
def test_method_access_01(self):
ns = run_code("""
class ClassA:
Expand Down
Expand Up
@@ -320,7 +317,6 @@ def funcB[B](self): ...
with self.assertRaisesRegex(NameError, "name 'B' is not defined"):
run_code(code)
@unittest.expectedFailure # TODO: RUSTPYTHON; NameError: name 'x' is not defined
def test_class_scope_interaction_01(self):
ns = run_code("""
class C:
Expand Down
Expand Up
@@ -486,7 +482,6 @@ class Inner[U](make_base([T for _ in (1,)]), make_base(T)):
self.assertEqual(base1.__arg__, [ns["C"].__type_params__[0]])
self.assertEqual(base2.__arg__, "class")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ~T != 'class'
def test_gen_exp_in_generic_method(self):
code = """
class C[T]:
Expand Down
Expand Up
@@ -1374,13 +1369,11 @@ def test_starred_typevartuple(self):
Ts, = ns["Alias"].__type_params__
self.assertEqual(Ts.__default__, next(iter(ns["default"])))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
def test_nondefault_after_default(self):
check_syntax_error(self, "def func[T=int, U](): pass", "non-default type parameter 'U' follows default type parameter")
check_syntax_error(self, "class C[T=int, U]: pass", "non-default type parameter 'U' follows default type parameter")
check_syntax_error(self, "type A[T=int, U] = int", "non-default type parameter 'U' follows default type parameter")
@unittest.expectedFailure # TODO: RUSTPYTHON; + defined
def test_lazy_evaluation(self):
ns = run_code("""
type Alias[T = Undefined, *U = Undefined, **V = Undefined] = int
Expand Down
Expand Up
@@ -1431,7 +1424,6 @@ def test_symtable_key_regression_name(self):
class TestEvaluateFunctions(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'TypeAliasType' object has no attribute 'evaluate_value'
def test_general(self):
type Alias = int
Alias2 = TypeAliasType("Alias2", int)
Expand Down
Expand Up
@@ -1459,7 +1451,6 @@ def f[T: int = int, **P = int, *Ts = int](): pass
self.assertIs(annotationlib.call_evaluate_function(case, annotationlib.Format.FORWARDREF), int)
self.assertEqual(annotationlib.call_evaluate_function(case, annotationlib.Format.STRING), 'int')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_constraints(self):
def f[T: (int, str)](): pass
T, = f.__type_params__
Expand All
@@ -1471,7 +1462,6 @@ def f[T: (int, str)](): pass
self.assertEqual(annotationlib.call_evaluate_function(case.evaluate_constraints, annotationlib.Format.FORWARDREF), (int, str))
self.assertEqual(annotationlib.call_evaluate_function(case.evaluate_constraints, annotationlib.Format.STRING), '(int, str)')
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'TypeVar' object has no attribute 'evaluate_bound'
def test_const_evaluator(self):
T = TypeVar("T", bound=int)
self.assertEqual(repr(T.evaluate_bound), "<constevaluator <class 'int'>>")
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -8437,7 +8437,6 @@ class Bar(NamedTuple):
self.assertIsInstance(bar.attr, Vanilla)
self.assertEqual(bar.attr.name, "attr")
@unittest.expectedFailure # TODO: RUSTPYTHON; + Error calling __set_name__ on 'Annoying' instance attr in 'NamedTupleClass'
def test_setname_raises_the_same_as_on_other_classes(self):
class CustomException(BaseException): pass
Expand Down
Expand Up
@@ -9433,7 +9432,6 @@ def test_repr(self):
self.assertEqual(repr(Match[str]), 'typing.Match[str]')
self.assertEqual(repr(Match[bytes]), 'typing.Match[bytes]')
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "type 're\.Match' is not an acceptable base type" does not match "type '_sre.Match' is not an acceptable base type"
def test_cannot_subclass(self):
with self.assertRaisesRegex(
TypeError,
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -2461,6 +2461,10 @@ impl Compiler {
self.push_symbol_table()?;
let inner_key = self.symbol_table_stack.len() - 1;
self.enter_scope("TypeAlias", CompilerScope::TypeParams, inner_key, lineno)?;
// Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
self.emit_format_validation()?;
self.compile_expression(value)?;
Comment thread
Comment on lines
+2464
to
2468
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
⚠️ Potential issue | 🟠 Major
Add the format local before emit_format_validation() to keep LoadFast(0) valid.
emit_format_validation() uses LoadFast(0), but these evaluator scopes only set argcount/posonlyargcount and don’t insert "format" into varnames. That can misread a different local or panic if the varnames set is empty. Add "format" to varnames in these evaluator scopes (mirroring enter_annotation_scope).
✅ Suggested fix
@@
- // Evaluator takes a positional-only format parameter
+ // Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
+ self.current_code_info()
+ .metadata
+ .varnames
+ .insert("format".to_owned());
self.emit_format_validation()?;
@@
- // Evaluator takes a positional-only format parameter
+ // Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
+ self.current_code_info()
+ .metadata
+ .varnames
+ .insert("format".to_owned());
self.emit_format_validation()?;
@@
- // Evaluator takes a positional-only format parameter
+ // Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
+ self.current_code_info()
+ .metadata
+ .varnames
+ .insert("format".to_owned());
self.emit_format_validation()?;
As per coding guidelines, follow Rust best practices for error handling and memory management.
Also applies to: 2501-2505, 2646-2651
🤖 Prompt for AI Agents
In `@crates/codegen/src/compile.rs` around lines 2464 - 2468, The evaluator scopes
set argcount/posonlyargcount then call emit_format_validation(), but they don't
register the "format" local in the current code object's varnames so
emit_format_validation()'s LoadFast(0) can read the wrong slot or panic; update
the relevant evaluator scope blocks (the ones around the calls to
self.current_code_info().metadata.argcount/posonlyargcount and
emit_format_validation(), including the similar blocks at the other locations
noted) to push "format" into self.current_code_info().metadata.varnames
(mirroring enter_annotation_scope's behavior) before calling
emit_format_validation(), ensuring varnames contains "format" and preserving
Rust error handling conventions when modifying the metadata.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
emit!(self, Instruction::ReturnValue);
let value_code = self.exit_scope();
Expand Down
Expand Up
@@ -2494,6 +2498,10 @@ impl Compiler {
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get().to_u32();
self.enter_scope("TypeAlias", CompilerScope::TypeParams, key, lineno)?;
// Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
self.emit_format_validation()?;
let prev_ctx = self.ctx;
self.ctx = CompileContext {
Expand Down
Expand Up
@@ -2635,6 +2643,12 @@ impl Compiler {
// Enter scope with the type parameter name
self.enter_scope(name, CompilerScope::TypeParams, key, lineno)?;
// Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
self.emit_format_validation()?;
// TypeParams scope is function-like
let prev_ctx = self.ctx;
self.ctx = CompileContext {
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -528,7 +528,6 @@ impl SymbolTableAnalyzer {
if !self.tables.as_ref().is_empty() {
let scope_depth = self.tables.as_ref().len();
// check if the name is already defined in any outer scope
// therefore
if scope_depth < 2
|| self.found_in_outer_scope(&symbol.name, st_typ)
!= Some(SymbolScope::Free)
Expand All
@@ -539,6 +538,25 @@ impl SymbolTableAnalyzer {
location: None,
});
}
// Check if the nonlocal binding refers to a type parameter
if symbol.flags.contains(SymbolFlags::NONLOCAL) {
for (symbols, _typ) in self.tables.iter().rev() {
if let Some(sym) = symbols.get(&symbol.name) {
if sym.flags.contains(SymbolFlags::TYPE_PARAM) {
return Err(SymbolTableError {
error: format!(
"nonlocal binding not allowed for type parameter '{}'",
symbol.name
),
location: None,
});
}
if sym.is_bound() {
break;
}
}
}
}
} else {
return Err(SymbolTableError {
error: format!(
Expand Down
Expand Up
@@ -933,7 +951,8 @@ impl SymbolTableBuilder {
/// 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 can_see_class_scope =
current.typ == CompilerScope::Class || current.can_see_class_scope;
let has_conditional = current.has_conditional_annotations;
// Create annotation block if not exists
Expand Down
Expand Up
@@ -1485,6 +1504,8 @@ impl SymbolTableBuilder {
CompilerScope::TypeParams,
self.line_index_start(value.range()),
);
// Evaluator takes a format parameter
self.register_name("format", SymbolUsage::Parameter, TextRange::default())?;
if in_class {
if let Some(table) = self.tables.last_mut() {
table.can_see_class_scope = true;
Expand Down
Expand Up
@@ -1990,6 +2011,8 @@ impl SymbolTableBuilder {
let in_class = self.tables.last().is_some_and(|t| t.can_see_class_scope);
let line_number = self.line_index_start(expr.range());
self.enter_scope(scope_name, CompilerScope::TypeParams, line_number);
// Evaluator takes a format parameter
self.register_name("format", SymbolUsage::Parameter, TextRange::default())?;
if in_class {
if let Some(table) = self.tables.last_mut() {
Expand All
@@ -2015,11 +2038,15 @@ impl SymbolTableBuilder {
fn scan_type_params(&mut self, type_params: &ast::TypeParams) -> SymbolTableResult {
// Check for duplicate type parameter names
let mut seen_names: IndexSet<&str> = IndexSet::default();
// Check for non-default type parameter after default type parameter
let mut default_seen = false;
for type_param in &type_params.type_params {
let (name, range) = match type_param {
ast::TypeParam::TypeVar(tv) => (tv.name.as_str(), tv.range),
ast::TypeParam::ParamSpec(ps) => (ps.name.as_str(), ps.range),
ast::TypeParam::TypeVarTuple(tvt) => (tvt.name.as_str(), tvt.range),
let (name, range, has_default) = match type_param {
ast::TypeParam::TypeVar(tv) => (tv.name.as_str(), tv.range, tv.default.is_some()),
ast::TypeParam::ParamSpec(ps) => (ps.name.as_str(), ps.range, ps.default.is_some()),
ast::TypeParam::TypeVarTuple(tvt) => {
(tvt.name.as_str(), tvt.range, tvt.default.is_some())
}
};
if !seen_names.insert(name) {
return Err(SymbolTableError {
Expand All
@@ -2031,6 +2058,21 @@ impl SymbolTableBuilder {
),
});
}
if has_default {
default_seen = true;
} else if default_seen {
return Err(SymbolTableError {
error: format!(
"non-default type parameter '{}' follows default type parameter",
name
),
location: Some(
self.source_file
.to_source_code()
.source_location(range.start(), PositionEncoding::Utf8),
),
});
}
}
// Register .type_params as a type parameter (automatically becomes cell variable)
Expand Down
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,7 @@
use super::{PyStr, PyStrRef, PyTupleRef, PyType, tuple::IntoPyTuple};
use super::{
PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, genericalias::PyGenericAlias,
tuple::IntoPyTuple,
};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
class::PyClassImpl,
Expand Down
Expand Up
@@ -135,6 +138,11 @@ impl PyInterpolation {
self.format_spec.clone()
}
#[pyclassmethod]
fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
PyGenericAlias::from_args(cls, args, vm)
}
#[pymethod]
fn __reduce__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyTupleRef {
let cls = zelf.class().to_owned();
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -552,8 +552,7 @@ impl Py<PyMemoryView> {
flags(SEQUENCE)
)]
impl PyMemoryView {
// TODO: Uncomment when Python adds __class_getitem__ to memoryview
// #[pyclassmethod]
#[pyclassmethod]
fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
PyGenericAlias::from_args(cls, args, vm)
}
Expand Down
Original file line number
Diff line number
Diff line change
@@ -1,9 +1,9 @@
use super::{PyStr, PyTupleRef, PyType};
use crate::common::lock::LazyLock;
use super::{PyStr, PyTupleRef, PyType, PyTypeRef, genericalias::PyGenericAlias};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
atomic_func,
class::PyClassImpl,
common::lock::LazyLock,
function::{FuncArgs, PyComparisonValue},
protocol::{PyIterReturn, PySequenceMethods},
types::{
Expand Down
Expand Up
@@ -178,6 +178,11 @@ impl PyTemplate {
self.concat(&other, vm)
}
#[pyclassmethod]
fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
PyGenericAlias::from_args(cls, args, vm)
}
#[pymethod]
fn __reduce__(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
// Import string.templatelib._template_unpickle
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -1629,7 +1629,7 @@ impl Constructor for PyType {
// PEP 678: Add a note to the original exception instead of wrapping it
// (Python 3.12+, gh-77757)
let note = format!(
"Error calling __set_name__ on '{}' instance {} in '{}'",
"Error calling __set_name__ on '{}' instance '{}' in '{}'",
obj.class().name(),
name,
typ.name()
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -190,7 +190,7 @@ mod _sre {
}
#[pyattr]
#[pyclass(name = "Pattern")]
#[pyclass(module = "re", name = "Pattern")]
#[derive(Debug, PyPayload)]
pub(crate) struct Pattern {
pub pattern: PyObjectRef,
Expand Down
Expand Up
@@ -597,7 +597,7 @@ mod _sre {
}
#[pyattr]
#[pyclass(name = "Match")]
#[pyclass(module = "re", name = "Match")]
#[derive(Debug, PyPayload)]
pub(crate) struct Match {
string: PyObjectRef,
Expand Down
Loading
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Loading
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.