Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 34 additions & 20 deletions mypyc/codegen/emitfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,19 +429,21 @@ def visit_get_attr(self, op: GetAttr) -> None:
):
# Generate code for the following branch here to avoid
# redundant branches in the generated code.
self.emit_attribute_error(branch, cl.name, op.attr)
self.emit_attribute_error(branch, cl, op.attr)
self.emit_line("goto %s;" % self.label(branch.true))
merged_branch = branch
self.emitter.emit_line("}")
if not merged_branch:
exc_class = "PyExc_AttributeError"
self.emitter.emit_line(
'PyErr_SetString({}, "attribute {} of {} undefined");'.format(
exc_class,
repr(op.attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX)),
repr(cl.name),
)
)
var_name = op.attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX)
if cl.is_generated:
# A generated class does not "exist" to the user, this is just an unbound
# variable in their code, not a missing attribute on the generated class.
exc_class = "PyExc_UnboundLocalError"
exc_msg = f"local variable {var_name!r} referenced before assignment"
else:
exc_class = "PyExc_AttributeError"
exc_msg = f"attribute {var_name!r} of {cl.name!r} undefined"
self.emitter.emit_line(f'PyErr_SetString({exc_class}, "{exc_msg}");')

if attr_rtype.is_refcounted and not op.is_borrowed:
if not merged_branch and not always_defined:
Expand Down Expand Up @@ -935,20 +937,32 @@ def emit_traceback(self, op: Branch) -> None:
if op.traceback_entry is not None:
self.emitter.emit_traceback(self.source_path, self.module_name, op.traceback_entry)

def emit_attribute_error(self, op: Branch, class_name: str, attr: str) -> None:
def emit_attribute_error(self, op: Branch, class_ir: ClassIR, attr: str) -> None:
assert op.traceback_entry is not None
globals_static = self.emitter.static_name("globals", self.module_name)
self.emit_line(
'CPy_AttributeError("%s", "%s", "%s", "%s", %d, %s);'
% (
self.source_path.replace("\\", "\\\\"),
op.traceback_entry[0],
class_name,
attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX),
op.traceback_entry[1],
globals_static,
if class_ir.is_generated:
Copy link
Contributor Author

@BobTheBuidler BobTheBuidler Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this here so it will do its job in any place emit_attribute_error might be used in the future, it didn't seem appropriate to add another helper but this doesn't really seem appropriate either

self.emit_line(
'CPy_UnboundLocalError("%s", "%s", "%s", %d, %s);'
% (
self.source_path.replace("\\", "\\\\"),
op.traceback_entry[0],
attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX),
op.traceback_entry[1],
globals_static,
)
)
else:
self.emit_line(
'CPy_AttributeError("%s", "%s", "%s", "%s", %d, %s);'
% (
self.source_path.replace("\\", "\\\\"),
op.traceback_entry[0],
class_ir.name,
attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX),
op.traceback_entry[1],
globals_static,
)
)
)
if DEBUG_ERRORS:
self.emit_line('assert(PyErr_Occurred() != NULL && "failure w/o err!");')

Expand Down
2 changes: 2 additions & 0 deletions mypyc/lib-rt/CPy.h
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,8 @@ void CPy_TypeErrorTraceback(const char *filename, const char *funcname, int line
PyObject *globals, const char *expected, PyObject *value);
void CPy_AttributeError(const char *filename, const char *funcname, const char *classname,
const char *attrname, int line, PyObject *globals);
void CPy_UnboundLocalError(const char *filename, const char *funcname, const char *attrname,
int line, PyObject *globals);


// Misc operations
Expand Down
8 changes: 8 additions & 0 deletions mypyc/lib-rt/exc_ops.c
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,11 @@ void CPy_AttributeError(const char *filename, const char *funcname, const char *
PyErr_SetString(PyExc_AttributeError, buf);
CPy_AddTraceback(filename, funcname, line, globals);
}

void CPy_UnboundLocalError(const char *filename, const char *funcname, const char *attrname,
int line, PyObject *globals) {
char buf[500];
snprintf(buf, sizeof(buf), "local variable '%.200s' referenced before assignment", attrname);
PyErr_SetString(PyExc_UnboundLocalError, buf);
CPy_AddTraceback(filename, funcname, line, globals);
}
11 changes: 5 additions & 6 deletions mypyc/test-data/run-generators.test
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ def test_bitmap_is_cleared_when_object_is_reused() -> None:
list(gen(True))

# Ensure bitmap has been cleared.
with assertRaises(AttributeError): # TODO: Should be UnboundLocalError
with assertRaises(UnboundLocalError):
list(gen(False))

def gen2(set: bool) -> Iterator[int]:
Expand All @@ -878,7 +878,7 @@ def gen2(set: bool) -> Iterator[int]:
def test_undefined_int_in_environment() -> None:
list(gen2(True))

with assertRaises(AttributeError): # TODO: Should be UnboundLocalError
with assertRaises(UnboundLocalError):
list(gen2(False))

[case testVariableWithSameNameAsHelperMethod]
Expand All @@ -902,10 +902,9 @@ def test_same_names() -> None:
assert list(gen_send()) == [2]
assert list(gen_throw()) == [84]

with assertRaises(AttributeError, "attribute 'send' of 'undefined_gen' undefined"):
# TODO: Should be UnboundLocalError, this test verifies that the attribute name
# matches the variable name in the input code, since internally it's generated
# with a prefix.
with assertRaises(UnboundLocalError, "local variable 'send' referenced before assignment"):
# this test verifies that the attribute name matches the variable name
# in the input code, since internally it's generated with a prefix.
list(undefined())

[case testGeneratorInheritance]
Expand Down