lint/stubgen: fix typing issues

This commit is contained in:
2026-07-09 15:54:59 +08:00
parent b335519832
commit 7156567302
11 changed files with 80 additions and 88 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ class Emitter:
"""
def __init__(self, source_filename: str = "") -> None:
self._source_filename = source_filename
self._source_filename: str = source_filename
self._header_lines: list[str] = []
self._body_lines: list[str] = []
self._indent_level: int = 0
+10 -10
View File
@@ -11,7 +11,7 @@ import math
from typing import TYPE_CHECKING
from .type_resolver import resolve_type
from .utils import type_id_to_int
from .utils import NodeDict, type_id_to_int
if TYPE_CHECKING:
from .emitter import Emitter
@@ -31,10 +31,10 @@ def generate_const(
globalText: str = "foobar"
voidConst: None = None
"""
node: dict = type_info.node
const_body: dict = node.get("const", {})
type_dict: dict = const_body.get("type", {})
value_dict: dict = const_body.get("value", {})
node: NodeDict = type_info.node
const_body: NodeDict = node.get("const", {})
type_dict: NodeDict = const_body.get("type", {})
value_dict: NodeDict = const_body.get("value", {})
python_type_str = _resolve_const_type(type_dict, registry)
python_value_str = _render_const_value(value_dict, type_dict, registry)
@@ -49,7 +49,7 @@ def generate_const(
# ── type resolution ────────────────────────────────────────────────────────
def _resolve_const_type(type_dict: dict, registry: TypeRegistry) -> str:
def _resolve_const_type(type_dict: NodeDict, registry: TypeRegistry) -> str:
"""Resolve a constant's type to a Python type annotation string."""
# Handle enum references
if "enum" in type_dict:
@@ -83,8 +83,8 @@ def _resolve_const_type(type_dict: dict, registry: TypeRegistry) -> str:
def _render_const_value(
value_dict: dict,
type_dict: dict,
value_dict: NodeDict,
type_dict: NodeDict,
registry: TypeRegistry,
) -> str:
"""Render a constant's value as a Python literal."""
@@ -131,7 +131,7 @@ def _render_const_value(
def _resolve_enum_value(
type_dict: dict,
type_dict: NodeDict,
ordinal: int,
registry: TypeRegistry,
) -> str:
@@ -144,7 +144,7 @@ def _resolve_enum_value(
if info is None:
return repr(ordinal)
enum_node: dict = info.node
enum_node: NodeDict = info.node
for e in enum_node.get("enum", {}).get("enumerants", []):
if e.get("ordinal") == ordinal or e.get("codeOrder") == ordinal:
return f'"{e["name"]}"'
+5 -3
View File
@@ -7,6 +7,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from .utils import NodeDict
if TYPE_CHECKING:
from .emitter import Emitter
from .models import TypeInfo
@@ -22,9 +24,9 @@ def generate_enum(
Gender = Literal["male", "female", "other"]
"""
node: dict = type_info.node
enum_body: dict = node.get("enum", {})
enumerants: list[dict] = enum_body.get("enumerants", [])
node: NodeDict = type_info.node
enum_body: NodeDict = node.get("enum", {})
enumerants: list[NodeDict] = enum_body.get("enumerants", [])
literals: list[str] = []
for e in enumerants:
+4 -2
View File
@@ -8,6 +8,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from .utils import NodeDict
if TYPE_CHECKING:
from .emitter import Emitter
from .models import TypeInfo, TypeRegistry
@@ -21,7 +23,7 @@ def generate_type_vars(
Returns the list of TypeVar names for use in the ``Generic[...]`` base.
"""
node: dict = type_info.node
node: NodeDict = type_info.node
params = node.get("parameters", [])
tv_names: list[str] = []
@@ -38,7 +40,7 @@ def generate_type_vars(
def resolve_brand(
brand_dict: dict,
brand_dict: NodeDict,
registry: TypeRegistry,
) -> list[str] | None:
"""Resolve brand bindings to Python type strings.
+12 -12
View File
@@ -14,7 +14,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from .utils import type_id_to_int
from .utils import NodeDict, type_id_to_int
if TYPE_CHECKING:
from .emitter import Emitter
@@ -29,11 +29,11 @@ def fixup_interface_methods(registry: TypeRegistry) -> None:
the interface by setting their ``parent_type_id`` and adjusting their
``scoped_name``.
"""
for type_info in list(registry._types.values()):
for type_info in registry.all_types():
if type_info.kind != "interface":
continue
node: dict = type_info.node
node: NodeDict = type_info.node
for method in node.get("interface", {}).get("methods", []):
mname = method["name"]
param_id = type_id_to_int(method["paramStructType"])
@@ -47,7 +47,7 @@ def fixup_interface_methods(registry: TypeRegistry) -> None:
f"{type_info.scoped_name}.{mname}.{suffix}"
)
# Override name for class nesting
sinfo._display_name = (
sinfo.display_name = (
f"{mname}{suffix}"
)
@@ -58,13 +58,13 @@ def generate_interface(
registry: TypeRegistry,
) -> None:
"""Generate interface classes for a Cap'n Proto interface."""
node: dict = type_info.node
iface_body: dict = node.get("interface", {})
node: NodeDict = type_info.node
iface_body: NodeDict = node.get("interface", {})
name = type_info.name
methods: list[dict] = iface_body.get("methods", [])
methods: list[NodeDict] = iface_body.get("methods", [])
# Build method info list
method_infos: list[dict] = []
method_infos: list[NodeDict] = []
for method in methods:
param_id = type_id_to_int(method["paramStructType"])
result_id = type_id_to_int(method["resultStructType"])
@@ -129,22 +129,22 @@ def generate_interface(
def _method_param_type(
mi: dict,
mi: NodeDict,
iface_name: str,
) -> str:
"""Get the type string for a method's params."""
param_info = mi.get("param_info")
if param_info is not None:
return f"{iface_name}.{param_info._display_name or param_info.name}Reader"
return f"{iface_name}.{param_info.display_name or param_info.name}Reader"
return f"{iface_name}.{mi['name']}ParamsReader"
def _method_result_type(
mi: dict,
mi: NodeDict,
iface_name: str,
) -> str:
"""Get the type string for a method's results."""
result_info = mi.get("result_info")
if result_info is not None:
return f"{iface_name}.{result_info._display_name or result_info.name}Reader"
return f"{iface_name}.{result_info.display_name or result_info.name}Reader"
return f"{iface_name}.{mi['name']}ResultsReader"
+7 -7
View File
@@ -16,7 +16,7 @@ from .gen_const import generate_const
from .gen_enum import generate_enum
from .gen_generic import generate_type_vars
from .type_resolver import resolve_type
from .utils import type_id_to_int
from .utils import NodeDict, type_id_to_int
if TYPE_CHECKING:
from .emitter import Emitter
@@ -32,9 +32,9 @@ def generate_struct(
registry: TypeRegistry,
) -> None:
"""Generate the class triple for a Cap'n Proto struct."""
node: dict = type_info.node
struct_body: dict = node.get("struct", {})
name = type_info._display_name or type_info.name
node: NodeDict = type_info.node
struct_body: NodeDict = node.get("struct", {})
name = type_info.display_name or type_info.name
discriminant_count = struct_body.get("discriminantCount", 0)
nested_types = registry.get_children(type_info.type_id)
@@ -138,7 +138,7 @@ def generate_struct(
def _resolve_field_type(
type_dict: dict, registry: TypeRegistry, emitter: Emitter,
type_dict: NodeDict, registry: TypeRegistry, emitter: Emitter,
) -> str | None:
"""Resolve a field's type to a Python type annotation string."""
which = _type_which(type_dict)
@@ -159,7 +159,7 @@ def _resolve_field_type(
def _resolve_list_field_type(
list_dict: dict, registry: TypeRegistry, emitter: Emitter,
list_dict: NodeDict, registry: TypeRegistry, emitter: Emitter,
) -> str:
"""Resolve a ``List(T)`` field type."""
emitter.add_typing_import("Sequence", "collections.abc")
@@ -195,7 +195,7 @@ def _resolve_list_field_type(
return result
def _type_which(type_dict: dict) -> str | None:
def _type_which(type_dict: NodeDict) -> str | None:
"""Return the union variant key for a type dict."""
for key in ("void", "bool", "int8", "int16", "int32", "int64",
"uint8", "uint16", "uint32", "uint64",
+5 -1
View File
@@ -109,7 +109,7 @@ class TypeInfo:
"""The ID of the file node that contains this type (for import resolution)."""
# May be overridden for method param/result structs
_display_name: str | None = None
display_name: str | None = None
"""Override for the display name (used for method param/result structs)."""
@@ -172,5 +172,9 @@ class TypeRegistry:
def __len__(self) -> int:
return len(self._types)
def all_types(self) -> list[TypeInfo]:
"""Return all registered types (for iteration/filtering)."""
return list(self._types.values())
def __contains__(self, type_id: int) -> bool:
return type_id in self._types
+14 -19
View File
@@ -13,19 +13,16 @@ from __future__ import annotations
import os
import sys
from typing import TYPE_CHECKING
from typing import Any
from .emitter import Emitter
from .gen_const import generate_const
from .gen_enum import generate_enum
from .gen_interface import fixup_interface_methods, generate_interface
from .gen_struct import generate_struct
from .models import TypeRegistry
from .models import TypeInfo, TypeRegistry
from .schema_walker import build_type_registry
from .utils import filename_to_module_name, type_id_to_int
if TYPE_CHECKING:
from typing import Any
from .utils import NodeDict, filename_to_module_name, type_id_to_int
def main() -> int:
@@ -38,15 +35,13 @@ def main() -> int:
def _main_impl() -> int:
import capnp
# ── 1. Read CodeGeneratorRequest from stdin ────────────────────────
request = _read_request()
# Convert to dict for processing (avoids C++ DynamicStruct quirks)
request_dict = request.to_dict()
nodes: list[dict] = request_dict["nodes"]
requested_files: list[dict] = request_dict["requestedFiles"]
nodes: list[NodeDict] = request_dict["nodes"]
requested_files: list[NodeDict] = request_dict["requestedFiles"]
# ── 2. Build type registry ─────────────────────────────────────────
registry = build_type_registry(nodes, requested_files)
@@ -79,7 +74,7 @@ def _main_impl() -> int:
output_path = f"{module_name}.pyi"
with open(output_path, "w") as f:
f.write(content)
_ = f.write(content)
except Exception as exc:
print(
@@ -100,13 +95,13 @@ def _read_request() -> Any:
Uses pycapnp's ``SchemaParser`` to dynamically load ``schema.capnp``
(shipped with pycapnp), then casts the stdin message.
"""
import capnp
import capnp # pyright: ignore[reportMissingTypeStubs]
capnp_dir = os.path.dirname(capnp.__file__)
schema_path = os.path.join(capnp_dir, "schema.capnp")
import_base = os.path.dirname(capnp_dir)
parser = capnp.SchemaParser()
parser = capnp.SchemaParser() # pyright: ignore[reportAttributeAccessIssue]
schema_module = parser.load(schema_path, imports=[import_base])
return schema_module.CodeGeneratorRequest.read(sys.stdin.buffer)
@@ -130,7 +125,7 @@ def _generate_file(
_ORDER = {"enum": 0, "struct": 1, "const": 2, "interface": 3}
def _sort_key(info):
def _sort_key(info: TypeInfo) -> tuple[int, str]:
return (_ORDER.get(info.kind, 99), info.name)
top_level.sort(key=_sort_key)
@@ -163,12 +158,12 @@ def _resolve_cross_imports(
# Collect all types belonging to this file
own_types: set[int] = set()
for info in registry._types.values():
for info in registry.all_types():
if info.file_id == file_id and info.kind in ("struct", "enum", "interface"):
own_types.add(info.type_id)
# Walk types and find external references
for info in registry._types.values():
for info in registry.all_types():
if info.file_id != file_id:
continue
@@ -196,7 +191,7 @@ def _resolve_cross_imports(
all_imports: list[str] = []
for tname in sorted(type_names):
ref_info = None
for info in registry._types.values():
for info in registry.all_types():
if info.scoped_name == tname and info.file_id != file_id:
ref_info = info
break
@@ -226,7 +221,7 @@ def _collect_type_references(info: TypeInfo, registry: TypeRegistry) -> set[int]
return refs
def _collect_field_references(field: dict, refs: set[int], registry: TypeRegistry) -> None:
def _collect_field_references(field: NodeDict, refs: set[int], _registry: TypeRegistry) -> None:
"""Recursively collect type IDs from a field's type."""
if "slot" in field:
_collect_type_references_from_dict(field["slot"].get("type", {}), refs)
@@ -234,7 +229,7 @@ def _collect_field_references(field: dict, refs: set[int], registry: TypeRegistr
refs.add(type_id_to_int(field["group"]["typeId"]))
def _collect_type_references_from_dict(type_dict: dict, refs: set[int]) -> None:
def _collect_type_references_from_dict(type_dict: NodeDict, refs: set[int]) -> None:
"""Recursively collect type IDs from a type dict."""
if "struct" in type_dict:
refs.add(type_id_to_int(type_dict["struct"]["typeId"]))
+8 -13
View File
@@ -6,27 +6,22 @@ building a ``TypeRegistry`` that maps type IDs to ``TypeInfo``.
from __future__ import annotations
from typing import TYPE_CHECKING
from .models import TypeInfo, TypeRegistry
from .utils import get_display_name, get_node_kind, type_id_to_int
if TYPE_CHECKING:
from typing import Any
from .utils import NodeDict, get_display_name, get_node_kind, type_id_to_int
_GENERATABLE_KINDS = frozenset({"struct", "enum", "interface", "const"})
def build_type_registry(
nodes: list[dict],
requested_files: list[dict],
nodes: list[NodeDict],
requested_files: list[NodeDict],
) -> TypeRegistry:
"""Build a ``TypeRegistry`` from the to-dict CodeGeneratorRequest nodes."""
registry = TypeRegistry()
# Build ID → node lookup
nodes_by_id: dict[int, dict] = {}
nodes_by_id: dict[int, NodeDict] = {}
for node in nodes:
nodes_by_id[type_id_to_int(node["id"])] = node
@@ -71,8 +66,8 @@ def build_type_registry(
def _build_file_associations(
nodes_by_id: dict[int, dict],
requested_files: list[dict],
nodes_by_id: dict[int, NodeDict],
_requested_files: list[NodeDict],
registry: TypeRegistry,
) -> None:
for node_id in nodes_by_id:
@@ -82,7 +77,7 @@ def _build_file_associations(
def _find_containing_file(
node_id: int, nodes_by_id: dict[int, dict]
node_id: int, nodes_by_id: dict[int, NodeDict]
) -> int | None:
visited: set[int] = set()
current_id = node_id
@@ -109,7 +104,7 @@ def _find_containing_file(
def _build_scoped_name(
type_id: int, nodes_by_id: dict[int, dict]
type_id: int, nodes_by_id: dict[int, NodeDict]
) -> str:
parts: list[str] = []
visited: set[int] = set()
+5 -10
View File
@@ -5,13 +5,8 @@ Maps type dicts (from ``CodeGeneratorRequest.to_dict()``) to ``PythonType``.
from __future__ import annotations
from typing import TYPE_CHECKING
from .models import PythonType, TypeRegistry
from .utils import type_id_to_int
if TYPE_CHECKING:
from typing import Any
from .utils import NodeDict, type_id_to_int
# Cap'n Proto type key → Python type name
@@ -34,7 +29,7 @@ CAPNP_TO_PYTHON: dict[str, str] = {
def resolve_type(
type_dict: dict,
type_dict: NodeDict,
registry: TypeRegistry,
) -> PythonType | None:
"""Resolve a type dict to a ``PythonType``.
@@ -84,7 +79,7 @@ def resolve_type(
return PythonType(name="Any")
def _type_which(type_dict: dict) -> str | None:
def _type_which(type_dict: NodeDict) -> str | None:
"""Return the union variant key for a type dict."""
for key in CAPNP_TO_PYTHON:
if key in type_dict:
@@ -95,7 +90,7 @@ def _type_which(type_dict: dict) -> str | None:
return None
def _resolve_list_type(list_dict: dict, registry: TypeRegistry) -> PythonType:
def _resolve_list_type(list_dict: NodeDict, registry: TypeRegistry) -> PythonType:
"""Resolve a List(T) type."""
depth = 1
inner = list_dict.get("elementType", {})
@@ -129,7 +124,7 @@ def _resolve_named_type(type_id: int, registry: TypeRegistry) -> PythonType:
def _resolve_any_pointer(
ap_dict: dict, registry: TypeRegistry
ap_dict: NodeDict, registry: TypeRegistry
) -> PythonType | None:
"""Resolve an AnyPointer type."""
if "unconstrained" in ap_dict:
+9 -10
View File
@@ -1,6 +1,11 @@
"""Utility functions for capnp-stubgen."""
import os
from typing import Any
#: A dict from capnp's ``CodeGeneratorRequest.to_dict()``.
#: Keys are strings, values are arbitrary nested capnp data.
NodeDict = dict[str, Any]
def filename_to_module_name(filename: str) -> str:
@@ -14,18 +19,12 @@ def filename_to_module_name(filename: str) -> str:
return base + "_capnp"
def get_display_name(node: dict) -> str:
"""Extract the short display name from a node dict.
The node's ``displayName`` field contains a path like
``path/to/file.capnp:TypeName``. This removes the file prefix.
"""
name: str = node["displayName"]
prefix_len: int = node["displayNamePrefixLength"]
return name[prefix_len:]
def get_display_name(node: NodeDict) -> str:
"""Extract the short display name from a node dict."""
return str(node["displayName"])[int(node["displayNamePrefixLength"]):]
def get_node_kind(node: dict) -> str:
def get_node_kind(node: NodeDict) -> str:
"""Determine the kind of a node from its dict representation.
Returns one of: "file", "struct", "enum", "interface", "const", "annotation".