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
40 changes: 40 additions & 0 deletions pybind11_stubgen/parser/mixins/fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,10 +1105,20 @@ class RewritePybind11EnumValueRepr(IParser):
def __init__(self):
super().__init__()
self._pybind11_enum_locations: dict[re.Pattern, str] = {}
self._rewrite_enum_current_module: QualifiedName = QualifiedName()

def set_pybind11_enum_locations(self, locations: dict[re.Pattern, str]):
self._pybind11_enum_locations = locations

def handle_module(
self, path: QualifiedName, module: types.ModuleType
) -> Module | None:
old = self._rewrite_enum_current_module
self._rewrite_enum_current_module = path
result = super().handle_module(path, module)
self._rewrite_enum_current_module = old
return result

def parse_value_str(self, value: str) -> Value | InvalidExpression:
value = value.strip()
match = self._pybind11_enum_pattern.match(value)
Expand All @@ -1120,9 +1130,39 @@ def parse_value_str(self, value: str) -> Value | InvalidExpression:
continue
enum_class = self.parse_annotation_str(f"{prefix}.{enum_class_str}")
if isinstance(enum_class, ResolvedType):
if self._is_ancestor_attr_path(enum_class.name):
# Issue #304: a default expression rooted at a
# proper ancestor package of the current module
# (e.g. ``demo.NativeColor.Red`` inside the
# ``demo._bindings.enum`` stub) is evaluated at
# stub-import time. The ancestor's namespace is
# populated mid-``__init__`` by wildcard re-
# exports, so if the stub is imported during
# that init the lookup fails with a circular-
# import ``AttributeError``. The argument's
# type annotation is already rendered in the
# short form, so the enum class is in scope.
return Value(
repr=f"{enum_class_str}.{entry}",
is_print_safe=True,
)
return Value(repr=f"{enum_class.name}.{entry}", is_print_safe=True)
return super().parse_value_str(value)

def _is_ancestor_attr_path(self, name: QualifiedName) -> bool:
"""Would evaluating ``name`` cross an ancestor package attribute?

The enum class itself is ``name[-1]``; the segments preceding
it form the containing path. Return True when that containing
path is a *proper* ancestor (strict prefix) of the module
currently being processed, and False otherwise.
"""
container = name[:-1]
if len(container) == 0:
return False
current = self._rewrite_enum_current_module
return len(current) > len(container) and current[: len(container)] == container

def report_error(self, error: ParserError) -> None:
if isinstance(error, InvalidExpressionError):
match = self._pybind11_enum_pattern.match(error.expression)
Expand Down
1 change: 1 addition & 0 deletions tests/check-demo-stubs-generation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ run_stubgen() {
${NUMPY_FORMAT} \
--ignore-invalid-expressions="\(anonymous namespace\)::(Enum|Unbound)|<demo\._bindings\.flawed_bindings\..*" \
--enum-class-locations="ConsoleForegroundColor:demo._bindings.enum" \
--enum-class-locations="NativeColor:demo._bindings.enum" \
--print-value-comments \
--print-safe-value-reprs="Foo\(\d+\)" \
--exit-code
Expand Down
8 changes: 8 additions & 0 deletions tests/py-demo/bindings/src/modules/enum.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,13 @@ void bind_enum_module(py::module&&m) {
.value("Red", NativeColor::Red)
.value("Blue", NativeColor::Blue)
.finalize();

// Regression: https://github.com/pybind/pybind11-stubgen/issues/304
// Defaults referring to a py::native_enum must not be rendered in a form
// that triggers a parent-package lookup at stub import time.
m.def(
"accept_defaulted_native_enum",
[](const NativeColor &color) {},
py::arg("color") = NativeColor::Red);
#endif
}
Loading