GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Fix typing, typevar, genericalias, and symboltable by youknowone · Pull Request #7091 · RustPython/RustPython

Fix typing, typevar, genericalias, and symboltable #7091

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.

Sign up for GitHub

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

Merged
youknowone merged 1 commit into RustPython:main from youknowone:typing
Feb 14, 2026

Uh oh!

There was an error while loading. Please reload this page.

Merged

Fix typing, typevar, genericalias, and symboltable #7091

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
1 commit
Select commit
35aa3cd
Fix typing, typevar, genericalias, and symboltable
youknowone Feb 13, 2026
File filter
Filter by extension

Clear filters
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
Diff view

There are no files selected for viewing

1 change: 1 addition & 0 deletions 1 .cspell.json
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"sysmodule",
"tracebacks",
"typealiases",
"typevartuples",
"unhashable",
"uninit",
"unraisable",
Expand Down
2 changes: 1 addition & 1 deletion 2 Lib/test/test_concurrent_futures/test_process_pool.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_traceback(self):
self.assertIn('raise RuntimeError(123) # some comment',
f1.getvalue())

@unittest.skipIf(sys.platform == 'linux', 'TODO: RUSTPYTHON flaky EOFError')
@unittest.skip('TODO: RUSTPYTHON flaky EOFError')
@hashlib_helper.requires_hashdigest('md5')
def test_ressources_gced_in_workers(self):
# Ensure that argument for a job are correctly gc-ed after the job
Expand Down
4 changes: 0 additions & 4 deletions 4 Lib/test/test_type_params.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ def func[A, B](a: dict[A, B]):
A, B = func.__type_params__
self.assertEqual(func.__annotations__["a"], dict[A, B])

@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: the symbol 'list' must be present in the symbol table
def test_function_access_02(self):
code = """
def func[A](a = list[A]()):
Expand Down Expand Up @@ -866,7 +865,6 @@ class Inner[T]:
self.assertEqual(Outer.Inner._Inner__x, "inner")
self.assertEqual(Outer._Outer__after, "after")

@unittest.expectedFailure # TODO: RUSTPYTHON; NameError: name '_Derived__Base' is not defined
def test_no_mangling_in_bases(self):
ns = run_code("""
class __Base:
Expand All @@ -880,7 +878,6 @@ class Derived[T](__Base, __kwarg=1):
self.assertEqual(Derived.__bases__, (ns["__Base"], Generic))
self.assertEqual(Derived.kwargs, {"__kwarg": 1})

@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: the symbol '_Y__X' must be present in the symbol table
def test_no_mangling_in_nested_scopes(self):
ns = run_code("""
from test.test_type_params import make_base
Expand Down Expand Up @@ -911,7 +908,6 @@ class Y[T: __X](
base3 = Y.__bases__[3]
self.assertEqual(list(base3.__arg__), [ns["__X"]])

@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: the symbol '_Foo__T' must be present in the symbol table
def test_type_params_are_mangled(self):
ns = run_code("""
from test.test_type_params import make_base
Expand Down
1 change: 0 additions & 1 deletion 1 Lib/test/test_typing.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -10914,7 +10914,6 @@ class TypeIterationTests(BaseTestCase):
Annotated[T, ''],
)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_cannot_iterate(self):
expected_error_regex = "object is not iterable"
for test_type in self._UNITERABLE_TYPES:
Expand Down
3 changes: 2 additions & 1 deletion 3 crates/codegen/src/compile.rs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1852,7 +1852,8 @@ impl Compiler {
.code_stack
.last()
.and_then(|info| info.private.as_deref());
symboltable::mangle_name(private, name)
let mangled_names = self.current_symbol_table().mangled_names.as_ref();
symboltable::maybe_mangle_name(private, mangled_names, name)
}

// = compiler_nameop
Expand Down
126 changes: 104 additions & 22 deletions 126 crates/codegen/src/symboltable.rs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ pub struct SymbolTable {

/// Whether `from __future__ import annotations` is active
pub future_annotations: bool,

/// Names of type parameters that should still be mangled in type param scopes.
/// When Some, only names in this set are mangled; other names are left unmangled.
/// Set on type param blocks for generic classes; inherited by non-class child scopes.
pub mangled_names: Option<IndexSet<String>>,
}

impl SymbolTable {
Expand All @@ -88,6 +93,7 @@ impl SymbolTable {
annotation_block: None,
has_conditional_annotations: false,
future_annotations: false,
mangled_names: None,
}
}

Expand Down Expand Up @@ -905,14 +911,26 @@ impl SymbolTableBuilder {
)
})
.unwrap_or(false);
let table = SymbolTable::new(name.to_owned(), typ, line_number, is_nested);
// Inherit mangled_names from parent for non-class scopes
let inherited_mangled_names = self
.tables
.last()
.and_then(|t| t.mangled_names.clone())
.filter(|_| typ != CompilerScope::Class);
let mut table = SymbolTable::new(name.to_owned(), typ, line_number, is_nested);
table.mangled_names = inherited_mangled_names;
self.tables.push(table);
// Save parent's varnames and start fresh for the new scope
self.varnames_stack
.push(core::mem::take(&mut self.current_varnames));
}

fn enter_type_param_block(&mut self, name: &str, line_number: u32) -> SymbolTableResult {
fn enter_type_param_block(
&mut self,
name: &str,
line_number: u32,
for_class: bool,
) -> SymbolTableResult {
// Check if we're in a class scope
let in_class = self
.tables
Expand All @@ -921,16 +939,21 @@ impl SymbolTableBuilder {

self.enter_scope(name, CompilerScope::TypeParams, line_number);

// If we're in a class, mark that this type param scope can see the class scope
// Set properties on the newly created type param scope
if let Some(table) = self.tables.last_mut() {
table.can_see_class_scope = in_class;

// Add __classdict__ as a USE symbol in type param scope if in class
if in_class {
self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?;
// For generic classes, create mangled_names set so that only
// type parameter names get mangled (not bases or other expressions)
if for_class {
table.mangled_names = Some(IndexSet::default());
}
}

// Add __classdict__ as a USE symbol in type param scope if in class
if in_class {
self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?;
}

// Register .type_params as a SET symbol (it will be converted to cell variable later)
self.register_name(".type_params", SymbolUsage::Assigned, TextRange::default())?;

Expand Down Expand Up @@ -1202,12 +1225,20 @@ impl SymbolTableBuilder {
None
};

// For generic functions, scan defaults before entering type_param_block
// (defaults are evaluated in the enclosing scope, not the type param scope)
let has_type_params = type_params.is_some();
if has_type_params {
self.scan_parameter_defaults(parameters)?;
}

// 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()),
self.line_index_start(type_params.range),
false,
)?;
self.scan_type_params(type_params)?;
}
Expand All @@ -1223,6 +1254,7 @@ impl SymbolTableBuilder {
self.line_index_start(*range),
has_return_annotation,
*is_async,
has_type_params, // skip_defaults: already scanned above
)?;
self.scan_statements(body)?;
self.leave_scope();
Expand All @@ -1244,11 +1276,16 @@ impl SymbolTableBuilder {
range,
node_index: _,
}) => {
// Save class_name for the entire ClassDef processing
let prev_class = self.class_name.take();
if let Some(type_params) = type_params {
self.enter_type_param_block(
&format!("<generic parameters of {}>", name.as_str()),
self.line_index_start(type_params.range),
true, // for_class: enable selective mangling
)?;
// Set class_name for mangling in type param scope
self.class_name = Some(name.to_string());
self.scan_type_params(type_params)?;
}
self.enter_scope(
Expand All @@ -1257,18 +1294,23 @@ impl SymbolTableBuilder {
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.class_name = Some(name.to_string());
self.register_name("__module__", SymbolUsage::Assigned, *range)?;
self.register_name("__qualname__", SymbolUsage::Assigned, *range)?;
self.register_name("__doc__", SymbolUsage::Assigned, *range)?;
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;
// For non-generic classes, restore class_name before base scanning.
// Bases are evaluated in the enclosing scope, not the class scope.
// For generic classes, bases are scanned within the type_param scope
// where class_name is already correctly set.
if type_params.is_none() {
self.class_name = prev_class.clone();
}
if let Some(arguments) = arguments {
self.scan_expressions(&arguments.args, ExpressionContext::Load)?;
for keyword in &arguments.keywords {
Expand All @@ -1278,6 +1320,8 @@ impl SymbolTableBuilder {
if type_params.is_some() {
self.leave_scope();
}
// Restore class_name after all ClassDef processing
self.class_name = prev_class;
self.scan_decorators(decorator_list, ExpressionContext::Load)?;
self.register_ident(name, SymbolUsage::Assigned)?;
}
Expand Down Expand Up @@ -1506,6 +1550,7 @@ impl SymbolTableBuilder {
self.enter_type_param_block(
"TypeAlias",
self.line_index_start(type_params.range),
false,
)?;
self.scan_type_params(type_params)?;
}
Expand Down Expand Up @@ -1833,6 +1878,7 @@ impl SymbolTableBuilder {
self.line_index_start(expression.range()),
false, // lambdas have no return annotation
false, // lambdas are never async
false, // don't skip defaults
)?;
} else {
self.enter_scope(
Expand Down Expand Up @@ -2233,23 +2279,32 @@ impl SymbolTableBuilder {
Ok(())
}

/// Scan default parameter values (evaluated in the enclosing scope)
fn scan_parameter_defaults(&mut self, parameters: &ast::Parameters) -> SymbolTableResult {
for default in parameters
.posonlyargs
.iter()
.chain(parameters.args.iter())
.chain(parameters.kwonlyargs.iter())
.filter_map(|arg| arg.default.as_ref())
{
self.scan_expression(default, ExpressionContext::Load)?;
}
Ok(())
}

fn enter_scope_with_parameters(
&mut self,
name: &str,
parameters: &ast::Parameters,
line_number: u32,
has_return_annotation: bool,
is_async: bool,
skip_defaults: bool,
) -> SymbolTableResult {
// Evaluate eventual default parameters:
for default in parameters
.posonlyargs
.iter()
.chain(parameters.args.iter())
.chain(parameters.kwonlyargs.iter())
.filter_map(|arg| arg.default.as_ref())
{
self.scan_expression(default, ExpressionContext::Load)?; // not ExprContext?
// Evaluate eventual default parameters (unless already scanned before type_param_block):
if !skip_defaults {
self.scan_parameter_defaults(parameters)?;
}

// Annotations are scanned in outer scope:
Expand Down Expand Up @@ -2381,7 +2436,18 @@ impl SymbolTableBuilder {
let scope_depth = self.tables.len();
let table = self.tables.last_mut().unwrap();

let name = mangle_name(self.class_name.as_deref(), name);
// Add type param names to mangled_names set for selective mangling
if matches!(role, SymbolUsage::TypeParam)
&& let Some(ref mut set) = table.mangled_names
{
set.insert(name.to_owned());
}

let name = maybe_mangle_name(
self.class_name.as_deref(),
table.mangled_names.as_ref(),
name,
);
// Some checks for the symbol that present on this scope level:
let symbol = if let Some(symbol) = table.symbols.get_mut(name.as_ref()) {
let flags = &symbol.flags;
Expand Down Expand Up @@ -2574,11 +2640,27 @@ pub(crate) fn mangle_name<'a>(class_name: Option<&str>, name: &'a str) -> Cow<'a
if !name.starts_with("__") || name.ends_with("__") || name.contains('.') {
return name.into();
}
// strip leading underscore
let class_name = class_name.strip_prefix(|c| c == '_').unwrap_or(class_name);
// Strip leading underscores from class name
let class_name = class_name.trim_start_matches('_');
Comment thread
Comment on lines +2643 to +2644

@coderabbitai coderabbitai Bot Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check the actual code at lines 2638-2661 in symboltable.rs
sed -n '2630,2670p' crates/codegen/src/symboltable.rs

Repository: RustPython/RustPython

Length of output: 1120


🏁 Script executed:

#!/bin/bash
# Search for mangle_name usage and test cases
rg -n "mangle_name|mangle" crates/codegen/src/symboltable.rs | head -30
# Check for test files related to name mangling
fd -e rs -e py | xargs grep -l "mangle" | head -10

Repository: RustPython/RustPython

Length of output: 1805


🏁 Script executed:

#!/bin/bash
# Search for test files in the codegen directory
fd -e rs crates/codegen/src/ | xargs grep -l "test.*mangle\|mangle.*test" 2>/dev/null
# Search for any tests that use double-underscore names (which trigger mangling)
rg -n "__.*__" crates/codegen/src/ | grep -i "test\|def\|class" | head -20
# Check if there are any Python test files related to name mangling
fd test crates/codegen/ -type f

Repository: RustPython/RustPython

Length of output: 2865


🏁 Script executed:

#!/bin/bash
# Search for Python test files that might test name mangling
find . -path ./target -prune -o -name "*.py" -type f -exec grep -l "^class _" {} \; | head -10

# Check if there are any tests for name mangling behavior
rg -i "name.*mangle\|mangle.*test" Lib/test/ | head -20

# Also check the Python standard library for all-underscore class names
rg "^class _[_]+[^_]" --type py | head -10

Repository: RustPython/RustPython

Length of output: 5821


🏁 Script executed:

#!/bin/bash
# Check the context around the all-underscore class definitions
grep -B2 -A10 "^class __:" Lib/test/pyclbr_input.py
echo "---"
grep -B2 -A10 "^class ___:" Lib/test/pyclbr_input.py
echo "---"
grep -B2 -A10 "^class ___(object):" Lib/test/test_copyreg.py

Repository: RustPython/RustPython

Length of output: 772


🏁 Script executed:

#!/bin/bash
# Create a simple test script to verify CPython's behavior
python3 << 'EOF'
# Test CPython's name mangling with all-underscore class names
class __:
    def __foo(self):
        return "foo"
    
class ___:
    def __bar(self):
        return "bar"

# Check what the actual method names are after mangling
import inspect

# For class __
cls1 = __
methods1 = [m for m in dir(cls1) if not m.startswith('_') or m.startswith('__')]
print("Class __ methods:", [m for m in dir(cls1) if 'foo' in m])

# For class ___
cls2 = ___
print("Class ___ methods:", [m for m in dir(cls2) if 'bar' in m])

# The key question: are the names mangled or not?
# If they're mangled, they'd have the class name prefix
# If not mangled, they'd stay as __foo and __bar
EOF

Repository: RustPython/RustPython

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Search for tests that use all-underscore class names with double-underscore methods
rg -n "class __|class ___" extra_tests/ Lib/test/ --type py

# Check if pyclbr_input.py is actually tested
rg -n "pyclbr_input" . --type py | head -10

# Check if there are specific tests for name mangling behavior
rg -n "mangle|__.*__" extra_tests/ --type py | grep -i "class\|def" | head -20

Repository: RustPython/RustPython

Length of output: 3837


Add empty-string check after stripping leading underscores from class name.

When class_name consists entirely of underscores (e.g., "__" or "___"), trim_start_matches('_') produces an empty string. CPython's mangling logic skips mangling in this case, but the current code proceeds to produce an incorrect mangled name.

For example, class __: with a method def __foo(self): should remain __foo (unmangled), not become ___foo.

Proposed fix
     // Strip leading underscores from class name
     let class_name = class_name.trim_start_matches('_');
+    if class_name.is_empty() {
+        return name.into();
+    }
     let mut ret = String::with_capacity(1 + class_name.len() + name.len());
🤖 Prompt for AI Agents
In `@crates/codegen/src/symboltable.rs` around lines 2638 - 2639, After trimming
leading underscores from class_name (let class_name =
class_name.trim_start_matches('_');), add a check for an empty result and skip
mangling if empty: if class_name.is_empty() { return original_name.to_string();
} so that classes made only of underscores (e.g., "__") do not trigger
private-name mangling; update the code path that builds the mangled identifier
to return the unmodified identifier when class_name is empty.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
let mut ret = String::with_capacity(1 + class_name.len() + name.len());
ret.push('_');
ret.push_str(class_name);
ret.push_str(name);
ret.into()
}

/// Selective mangling for type parameter scopes around generic classes.
/// If `mangled_names` is Some, only mangle names that are in the set;
/// other names are left unmangled.
pub(crate) fn maybe_mangle_name<'a>(
class_name: Option<&str>,
mangled_names: Option<&IndexSet<String>>,
name: &'a str,
) -> Cow<'a, str> {
if let Some(set) = mangled_names
&& !set.contains(name)
{
return name.into();
}
mangle_name(class_name, name)
}
12 changes: 5 additions & 7 deletions 12 crates/vm/src/builtins/genericalias.rs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -720,24 +720,22 @@ impl crate::types::IterNext for PyGenericAliasIterator {
}
}

/// Creates a GenericAlias from type parameters, equivalent to CPython's _Py_subscript_generic
/// This is used for PEP 695 classes to create Generic[T] from type parameters
/// Creates a GenericAlias from type parameters, equivalent to _Py_subscript_generic.
/// This is used for PEP 695 classes to create Generic[T] from type parameters.
// _Py_subscript_generic
pub fn subscript_generic(type_params: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// Get typing module and _GenericAlias
let typing_module = vm.import("typing", 0)?;
let generic_type = typing_module.get_attr("Generic", vm)?;

// Call typing._GenericAlias(Generic, type_params)
let generic_alias_class = typing_module.get_attr("_GenericAlias", vm)?;

let args = if let Ok(tuple) = type_params.try_to_ref::<PyTuple>(vm) {
let params = if let Ok(tuple) = type_params.try_to_ref::<PyTuple>(vm) {
tuple.to_owned()
} else {
PyTuple::new_ref(vec![type_params], &vm.ctx)
};

// Create _GenericAlias instance
let args = crate::stdlib::typing::unpack_typevartuples(&params, vm)?;

generic_alias_class.call((generic_type, args.to_pyobject(vm)), vm)
}

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.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.