From 6e261064d3dbafb08295c017bd555a512dad1761 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 27 Mar 2026 12:01:00 +0000 Subject: [PATCH 1/4] Add agentic workflow for py-cppmodel --- .devcontainer/Dockerfile | 54 +++++++++++++++++++++++++++++---- .devcontainer/devcontainer.json | 4 +-- scripts/gemini-sandbox.sh | 31 +++++++++++++++++++ 3 files changed, 81 insertions(+), 8 deletions(-) create mode 100755 scripts/gemini-sandbox.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d1c4445..0e3b76f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,13 +1,49 @@ -FROM python:3.12-slim +FROM ubuntu:24.04 -# Install essential packages and create non-root user -RUN apt-get update && apt-get install -y --no-install-recommends git curl sudo bash-completion vim \ - && useradd -m -s /bin/bash vscode \ +# Install gcc, clang and some supporting tools for downloading/installing later tools. +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash-completion \ + cmake \ + curl \ + g++ \ + gdb \ + git \ + gpg \ + lcov \ + llvm \ + ninja-build \ + python-is-python3 \ + python3-pip \ + python3-venv \ + software-properties-common \ + ssh \ + sudo \ + unzip \ + vim \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Install bazelisk. +RUN ARCH=$(dpkg --print-architecture) && \ + wget -q https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-${ARCH} -O /usr/local/bin/bazelisk \ + && chmod +x /usr/local/bin/bazelisk \ + && ln -s /usr/local/bin/bazelisk /usr/local/bin/bazel + +# Create non-root user and add to sudoers +RUN useradd -m -s /bin/bash vscode \ && echo "vscode ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \ && mkdir -p /workspace \ - && chown vscode:vscode /workspace \ + && chown vscode:vscode /workspace + +# Install Node.js (required for Gemini CLI) +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* +# Install Gemini CLI globally +RUN npm install -g @google/gemini-cli \ + && npm cache clean --force + WORKDIR /workspace # Switch to non-root user @@ -21,4 +57,10 @@ RUN echo 'source /usr/share/bash-completion/completions/git' >> ~/.bashrc \ RUN curl -LsSf https://astral.sh/uv/install.sh | sh # Set up environment variables -ENV PATH="/home/vscode/.local/bin:${PATH}" +ENV PATH="/home/vscode/.local/bin:/usr/local/bin:${PATH}" +ENV UV_PROJECT_ENVIRONMENT="/home/vscode/.venv" + +# Pre-configure Gemini CLI +RUN mkdir -p /home/vscode/.gemini \ + && echo '{"/workspace": "TRUST_FOLDER"}' > /home/vscode/.gemini/trustedFolders.json \ + && echo '{"security": {"auth": {"selectedType": "gemini-api-key"}}}' > /home/vscode/.gemini/settings.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fd24ccd..85c49dc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,10 +17,10 @@ "ms-toolsai.jupyter-renderers", "ms-toolsai.jupyter", "ms-toolsai.vscode-jupyter-cell-tags", - "ms-toolsai.vscode-jupyter-slideshow", + "ms-toolsai.vscode-jupyter-slideshow" ], "settings": { - "python.defaultInterpreterPath": "/${workspaceFolder}/.venv/bin/python", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, diff --git a/scripts/gemini-sandbox.sh b/scripts/gemini-sandbox.sh new file mode 100755 index 0000000..b8d3e38 --- /dev/null +++ b/scripts/gemini-sandbox.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Exit on error +set -euo pipefail + +# Check if GEMINI_API_KEY is set +if [ -z "$GEMINI_API_KEY" ]; then + echo "Error: GEMINI_API_KEY environment variable is not set." + echo "Please set it before running this script:" + echo " export GEMINI_API_KEY='your_api_key_here'" + exit 1 +fi + +IMAGE_NAME="py-cppmodel-sandbox" +DOCKERFILE=".devcontainer/Dockerfile" + +# Build the image +echo "--- Building Docker Sandbox: $IMAGE_NAME ---" +docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" . + +# Run the container +echo "--- Starting Sandboxed Gemini Session ---" +echo "Note: Your current directory $(pwd) is mounted to /workspace" + +docker run -it --rm \ + -v "$(pwd):/workspace" \ + -e GEMINI_API_KEY="$GEMINI_API_KEY" \ + -e TERM=${TERM:-} \ + -e COLORTERM=${COLORTERM:-} \ + "$IMAGE_NAME" \ + gemini From 201056120d4bf58afb4019943a4c8a17356c2cd5 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 27 Mar 2026 14:34:39 +0000 Subject: [PATCH 2/4] Implement _CursorMirror and _TypeMirror classes to enhance libclang AST object mapping --- src/xyz/_libclang_mirrors.py | 329 ++++++++++++++++++++++++++++++++ src/xyz/cppmodel.py | 78 ++++++-- tests/test_libclang_mappings.py | 88 +++++++++ 3 files changed, 474 insertions(+), 21 deletions(-) create mode 100644 src/xyz/_libclang_mirrors.py create mode 100644 tests/test_libclang_mappings.py diff --git a/src/xyz/_libclang_mirrors.py b/src/xyz/_libclang_mirrors.py new file mode 100644 index 0000000..b4dde12 --- /dev/null +++ b/src/xyz/_libclang_mirrors.py @@ -0,0 +1,329 @@ +""" +Base classes that mirror libclang AST objects to provide self-documenting APIs. + +This module contains `_CursorMirror` and `_TypeMirror`, which explicitly map +all relevant properties and methods from `clang.cindex.Cursor` and +`clang.cindex.Type`. + +By inheriting from these classes, `cppmodel` wrappers natively expose the full +power of `libclang` while remaining discoverable by IDEs and type-checkers. +""" + +from typing import Any + +from clang.cindex import Cursor +from clang.cindex import Type as _ClangType + + +class _CursorMirror: + _cursor: Cursor + + @property + def access_specifier(self) -> Any: + return self._cursor.access_specifier + + @property + def availability(self) -> Any: + return self._cursor.availability + + @property + def brief_comment(self) -> Any: + return self._cursor.brief_comment + + @property + def canonical(self) -> Any: + return self._cursor.canonical + + @property + def displayname(self) -> Any: + return self._cursor.displayname + + @property + def location(self) -> Any: + return self._cursor.location + + @property + def type(self) -> Any: + return self._cursor.type + + @property + def kind(self) -> Any: + return self._cursor.kind + + def enum_type(self) -> Any: + return self._cursor.enum_type + + @property + def enum_value(self) -> Any: + return self._cursor.enum_value + + @property + def exception_specification_kind(self) -> Any: + return self._cursor.exception_specification_kind + + @property + def extent(self) -> Any: + return self._cursor.extent + + @property + def hash(self) -> Any: + return self._cursor.hash + + @property + def lexical_parent(self) -> Any: + return self._cursor.lexical_parent + + @property + def linkage(self) -> Any: + return self._cursor.linkage + + @property + def mangled_name(self) -> Any: + return self._cursor.mangled_name + + @property + def objc_type_encoding(self) -> Any: + return self._cursor.objc_type_encoding + + @property + def raw_comment(self) -> Any: + return self._cursor.raw_comment + + @property + def referenced(self) -> Any: + return self._cursor.referenced + + @property + def result_type(self) -> Any: + return self._cursor.result_type + + @property + def semantic_parent(self) -> Any: + return self._cursor.semantic_parent + + @property + def spelling(self) -> Any: + return self._cursor.spelling + + @property + def storage_class(self) -> Any: + return self._cursor.storage_class + + @property + def tls_kind(self) -> Any: + return self._cursor.tls_kind + + @property + def translation_unit(self) -> Any: + return self._cursor.translation_unit + + @property + def underlying_typedef_type(self) -> Any: + return self._cursor.underlying_typedef_type + + def from_cursor_result(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.from_cursor_result(*args, **kwargs) + + def from_location(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.from_location(*args, **kwargs) + + def from_result(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.from_result(*args, **kwargs) + + def get_arguments(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_arguments(*args, **kwargs) + + def get_bitfield_width(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_bitfield_width(*args, **kwargs) + + def get_children(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_children(*args, **kwargs) + + def get_definition(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_definition(*args, **kwargs) + + def get_field_offsetof(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_field_offsetof(*args, **kwargs) + + def get_included_file(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_included_file(*args, **kwargs) + + def get_num_template_arguments(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_num_template_arguments(*args, **kwargs) + + def get_template_argument_kind(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_template_argument_kind(*args, **kwargs) + + def get_template_argument_type(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_template_argument_type(*args, **kwargs) + + def get_template_argument_unsigned_value(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_template_argument_unsigned_value(*args, **kwargs) + + def get_template_argument_value(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_template_argument_value(*args, **kwargs) + + def get_tokens(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_tokens(*args, **kwargs) + + def get_usr(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.get_usr(*args, **kwargs) + + def is_abstract_record(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_abstract_record(*args, **kwargs) + + def is_anonymous(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_anonymous(*args, **kwargs) + + def is_bitfield(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_bitfield(*args, **kwargs) + + def is_const_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_const_method(*args, **kwargs) + + def is_converting_constructor(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_converting_constructor(*args, **kwargs) + + def is_copy_assignment_operator_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_copy_assignment_operator_method(*args, **kwargs) + + def is_copy_constructor(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_copy_constructor(*args, **kwargs) + + def is_default_constructor(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_default_constructor(*args, **kwargs) + + def is_default_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_default_method(*args, **kwargs) + + def is_definition(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_definition(*args, **kwargs) + + def is_deleted_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_deleted_method(*args, **kwargs) + + def is_explicit_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_explicit_method(*args, **kwargs) + + def is_move_assignment_operator_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_move_assignment_operator_method(*args, **kwargs) + + def is_move_constructor(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_move_constructor(*args, **kwargs) + + def is_mutable_field(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_mutable_field(*args, **kwargs) + + def is_pure_virtual_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_pure_virtual_method(*args, **kwargs) + + def is_scoped_enum(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_scoped_enum(*args, **kwargs) + + def is_static_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_static_method(*args, **kwargs) + + def is_virtual_method(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.is_virtual_method(*args, **kwargs) + + def walk_preorder(self, *args: Any, **kwargs: Any) -> Any: + return self._cursor.walk_preorder(*args, **kwargs) + + +class _TypeMirror: + _type: _ClangType + + @property + def kind(self) -> Any: + return self._type.kind + + @property + def element_count(self) -> Any: + return self._type.element_count + + @property + def element_type(self) -> Any: + return self._type.element_type + + @property + def spelling(self) -> Any: + return self._type.spelling + + @property + def translation_unit(self) -> Any: + return self._type.translation_unit + + def argument_types(self, *args: Any, **kwargs: Any) -> Any: + return self._type.argument_types(*args, **kwargs) + + def from_result(self, *args: Any, **kwargs: Any) -> Any: + return self._type.from_result(*args, **kwargs) + + def get_address_space(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_address_space(*args, **kwargs) + + def get_align(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_align(*args, **kwargs) + + def get_array_element_type(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_array_element_type(*args, **kwargs) + + def get_array_size(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_array_size(*args, **kwargs) + + def get_canonical(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_canonical(*args, **kwargs) + + def get_class_type(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_class_type(*args, **kwargs) + + def get_declaration(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_declaration(*args, **kwargs) + + def get_exception_specification_kind(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_exception_specification_kind(*args, **kwargs) + + def get_fields(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_fields(*args, **kwargs) + + def get_named_type(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_named_type(*args, **kwargs) + + def get_num_template_arguments(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_num_template_arguments(*args, **kwargs) + + def get_offset(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_offset(*args, **kwargs) + + def get_pointee(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_pointee(*args, **kwargs) + + def get_ref_qualifier(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_ref_qualifier(*args, **kwargs) + + def get_result(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_result(*args, **kwargs) + + def get_size(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_size(*args, **kwargs) + + def get_template_argument_type(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_template_argument_type(*args, **kwargs) + + def get_typedef_name(self, *args: Any, **kwargs: Any) -> Any: + return self._type.get_typedef_name(*args, **kwargs) + + def is_const_qualified(self, *args: Any, **kwargs: Any) -> Any: + return self._type.is_const_qualified(*args, **kwargs) + + def is_function_variadic(self, *args: Any, **kwargs: Any) -> Any: + return self._type.is_function_variadic(*args, **kwargs) + + def is_pod(self, *args: Any, **kwargs: Any) -> Any: + return self._type.is_pod(*args, **kwargs) + + def is_restrict_qualified(self, *args: Any, **kwargs: Any) -> Any: + return self._type.is_restrict_qualified(*args, **kwargs) + + def is_volatile_qualified(self, *args: Any, **kwargs: Any) -> Any: + return self._type.is_volatile_qualified(*args, **kwargs) diff --git a/src/xyz/cppmodel.py b/src/xyz/cppmodel.py index f10102f..05266fa 100644 --- a/src/xyz/cppmodel.py +++ b/src/xyz/cppmodel.py @@ -7,11 +7,14 @@ from clang.cindex import CursorKind as _CursorKind from clang.cindex import Diagnostic from clang.cindex import ExceptionSpecificationKind as _ExceptionSpecificationKind -from clang.cindex import SourceLocation from clang.cindex import TranslationUnit +from clang.cindex import Type as _ClangType from clang.cindex import TypeKind as _TypeKind # Suppress type checking warnings for clang.cindex kinds. +from xyz._libclang_mirrors import _CursorMirror +from xyz._libclang_mirrors import _TypeMirror + AccessSpecifier: Any = _AccessSpecifier CursorKind: Any = _CursorKind ExceptionSpecificationKind: Any = _ExceptionSpecificationKind @@ -22,36 +25,47 @@ def _get_annotations(cursor: Cursor) -> List[str]: return [c.displayname for c in cursor.get_children() if c.kind == CursorKind.ANNOTATE_ATTR] -class Unmodelled: +class Unmodelled(_CursorMirror): def __init__(self, cursor: Cursor): - self.location: SourceLocation = cursor.location + self._cursor = cursor self.name: str = cursor.displayname def __repr__(self) -> str: return "".format(self.name, self.location) -class Type: - def __init__(self, cindex_type): - self.kind = cindex_type.kind +class Type(_TypeMirror): + def __init__(self, cindex_type: _ClangType): + self._type = cindex_type self.name = cindex_type.spelling self.is_pointer: bool = self.kind == TypeKind.POINTER self.is_reference: bool = self.kind == TypeKind.LVALUEREFERENCE - self.is_const: bool = cindex_type.is_const_qualified() if self.is_pointer or self.is_reference: self.pointee: Optional[Type] = Type(cindex_type.get_pointee()) else: self.pointee = None + @property + def is_const(self) -> bool: + import warnings + + warnings.warn("is_const is deprecated, use is_const_qualified() instead", DeprecationWarning, stacklevel=2) + return self.is_const_qualified() + def __repr__(self) -> str: return "".format(self.name) -class Member: +class Member(_CursorMirror): def __init__(self, cursor: Cursor): - self.type: Type = Type(cursor.type) + self._cursor = cursor + self._wrapped_type: Type = Type(cursor.type) self.name: str = cursor.spelling + @property + def type(self) -> Type: + return self._wrapped_type + def __repr__(self) -> str: return "".format(self.type, self.name) @@ -67,8 +81,9 @@ def __repr__(self) -> str: return "".format(self.type, self.name) -class _Function: - def __init__(self, cursor): +class _Function(_CursorMirror): + def __init__(self, cursor: Cursor): + self._cursor = cursor self.name: str = cursor.spelling arguments: List[Optional[str]] = [str(x.spelling) or None for x in cursor.get_arguments()] argument_types: List[Type] = [Type(x) for x in cursor.type.argument_types()] @@ -98,7 +113,7 @@ def __repr__(self) -> str: class Function(_Function): - def __init__(self, cursor, namespaces: list[str] | None = None): + def __init__(self, cursor: Cursor, namespaces: list[str] | None = None): namespaces = namespaces or [] _Function.__init__(self, cursor) self.namespace: str = "::".join(namespaces) @@ -113,7 +128,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() - def __eq__(self, f) -> bool: + def __eq__(self, f: Any) -> bool: if self.name != f.name: return False if self.namespace != f.namespace: @@ -127,20 +142,40 @@ def __eq__(self, f) -> bool: class Method(_Function): - def __init__(self, cursor): + def __init__(self, cursor: Cursor): _Function.__init__(self, cursor) - self.is_const: bool = cursor.is_const_method() - self.is_virtual: bool = cursor.is_virtual_method() - self.is_pure_virtual: bool = cursor.is_pure_virtual_method() self.is_public: bool = cursor.access_specifier == AccessSpecifier.PUBLIC + @property + def is_const(self) -> bool: + import warnings + + warnings.warn("is_const is deprecated, use is_const_method() instead", DeprecationWarning, stacklevel=2) + return self.is_const_method() + + @property + def is_virtual(self) -> bool: + import warnings + + warnings.warn("is_virtual is deprecated, use is_virtual_method() instead", DeprecationWarning, stacklevel=2) + return self.is_virtual_method() + + @property + def is_pure_virtual(self) -> bool: + import warnings + + warnings.warn( + "is_pure_virtual is deprecated, use is_pure_virtual_method() instead", DeprecationWarning, stacklevel=2 + ) + return self.is_pure_virtual_method() + def __str__(self) -> str: s = _Function.__str__(self) - if self.is_const: + if self.is_const_method(): s = "{} const".format(s) - if self.is_pure_virtual: + if self.is_pure_virtual_method(): s = "virtual {} = 0".format(s) - elif self.is_virtual: + elif self.is_virtual_method(): s = "virtual {}".format(s) return "".format(s) @@ -148,8 +183,9 @@ def __repr__(self) -> str: return self.__str__() -class Class: +class Class(_CursorMirror): def __init__(self, cursor: Cursor, namespaces: List[str]): + self._cursor = cursor self.name: str = cursor.spelling self.namespace: str = "::".join(namespaces) self.qualified_name: str = self.name diff --git a/tests/test_libclang_mappings.py b/tests/test_libclang_mappings.py new file mode 100644 index 0000000..8cc8fc3 --- /dev/null +++ b/tests/test_libclang_mappings.py @@ -0,0 +1,88 @@ +""" +Tests to ensure all relevant libclang AST attributes are explicitly mapped in cppmodel. +""" + +import inspect + +import pytest +from clang.cindex import Cursor +from clang.cindex import TranslationUnit +from clang.cindex import Type as ClangType + +import xyz.cppmodel + +COMPILER_ARGS = [ + "-x", + "c++", + "-std=c++20", +] + +SOURCE = """\ +int z = 0; + +class A { + int a; + void foo(); +}; +""" + + +@pytest.fixture +def model(): + tu = TranslationUnit.from_source( + "sample.cc", + COMPILER_ARGS, + unsaved_files=[("sample.cc", SOURCE)], + ) + return xyz.cppmodel.Model(tu) + + +def get_public_attributes(cls): + """Returns a list of public attribute names for a given class.""" + return [name for name, _ in inspect.getmembers(cls) if not name.startswith("_")] + + +LIBCLANG_CURSOR_ATTRS = [attr for attr in get_public_attributes(Cursor) if attr not in ("data", "xdata")] + +LIBCLANG_TYPE_ATTRS = [attr for attr in get_public_attributes(ClangType) if attr not in ("data", "xdata")] + + +def is_mapped(wrapper, attr): + """Check if an attribute is accessible, handling exceptions from property evaluation.""" + if attr in dir(wrapper): + return True + try: + getattr(wrapper, attr) + return True + except AttributeError: + return False + except BaseException: + # Any other exception (AssertionError, Exception) means the property exists + # but evaluation failed for this specific instance's state. + return True + + +@pytest.mark.parametrize("attr", LIBCLANG_CURSOR_ATTRS) +def test_cursor_mappings(model, attr): + """Ensure all public attributes of libclang's Cursor are accessible on cppmodel wrappers.""" + # Get different wrappers that mirror Cursor + wrappers = [ + model.classes[0], # Class + model.classes[0].methods[0], # Method + model.classes[0].members[0], # Member + model.unmodelled_nodes[0], # Unmodelled + ] + + for wrapper in wrappers: + assert is_mapped(wrapper, attr), f"Missing Cursor attribute '{attr}' on {type(wrapper).__name__}" + + +@pytest.mark.parametrize("attr", LIBCLANG_TYPE_ATTRS) +def test_type_mappings(model, attr): + """Ensure all public attributes of libclang's Type are accessible on cppmodel's Type wrapper.""" + # Get a wrapper that mirrors Type + type_wrapper = model.classes[0].members[0].type + + assert is_mapped(wrapper=type_wrapper, attr=attr), ( + f"Missing Type attribute '{attr}' on {type(type_wrapper).__name__}" + ) From 6b7391877e51d0120fda1c46cd389d5132855a55 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 27 Mar 2026 15:52:43 +0000 Subject: [PATCH 3/4] Correctly handle undefined GEMINI_API_KEY --- scripts/gemini-sandbox.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gemini-sandbox.sh b/scripts/gemini-sandbox.sh index b8d3e38..0762392 100755 --- a/scripts/gemini-sandbox.sh +++ b/scripts/gemini-sandbox.sh @@ -4,7 +4,7 @@ set -euo pipefail # Check if GEMINI_API_KEY is set -if [ -z "$GEMINI_API_KEY" ]; then +if [ -z "${GEMINI_API_KEY:-}" ]; then echo "Error: GEMINI_API_KEY environment variable is not set." echo "Please set it before running this script:" echo " export GEMINI_API_KEY='your_api_key_here'" From 4984f19e2de17829539a832199080d88115661ae Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 27 Mar 2026 15:57:42 +0000 Subject: [PATCH 4/4] Remove mistaken additions --- src/xyz/_libclang_mirrors.py | 329 -------------------------------- src/xyz/cppmodel.py | 78 ++------ tests/test_libclang_mappings.py | 88 --------- 3 files changed, 21 insertions(+), 474 deletions(-) delete mode 100644 src/xyz/_libclang_mirrors.py delete mode 100644 tests/test_libclang_mappings.py diff --git a/src/xyz/_libclang_mirrors.py b/src/xyz/_libclang_mirrors.py deleted file mode 100644 index b4dde12..0000000 --- a/src/xyz/_libclang_mirrors.py +++ /dev/null @@ -1,329 +0,0 @@ -""" -Base classes that mirror libclang AST objects to provide self-documenting APIs. - -This module contains `_CursorMirror` and `_TypeMirror`, which explicitly map -all relevant properties and methods from `clang.cindex.Cursor` and -`clang.cindex.Type`. - -By inheriting from these classes, `cppmodel` wrappers natively expose the full -power of `libclang` while remaining discoverable by IDEs and type-checkers. -""" - -from typing import Any - -from clang.cindex import Cursor -from clang.cindex import Type as _ClangType - - -class _CursorMirror: - _cursor: Cursor - - @property - def access_specifier(self) -> Any: - return self._cursor.access_specifier - - @property - def availability(self) -> Any: - return self._cursor.availability - - @property - def brief_comment(self) -> Any: - return self._cursor.brief_comment - - @property - def canonical(self) -> Any: - return self._cursor.canonical - - @property - def displayname(self) -> Any: - return self._cursor.displayname - - @property - def location(self) -> Any: - return self._cursor.location - - @property - def type(self) -> Any: - return self._cursor.type - - @property - def kind(self) -> Any: - return self._cursor.kind - - def enum_type(self) -> Any: - return self._cursor.enum_type - - @property - def enum_value(self) -> Any: - return self._cursor.enum_value - - @property - def exception_specification_kind(self) -> Any: - return self._cursor.exception_specification_kind - - @property - def extent(self) -> Any: - return self._cursor.extent - - @property - def hash(self) -> Any: - return self._cursor.hash - - @property - def lexical_parent(self) -> Any: - return self._cursor.lexical_parent - - @property - def linkage(self) -> Any: - return self._cursor.linkage - - @property - def mangled_name(self) -> Any: - return self._cursor.mangled_name - - @property - def objc_type_encoding(self) -> Any: - return self._cursor.objc_type_encoding - - @property - def raw_comment(self) -> Any: - return self._cursor.raw_comment - - @property - def referenced(self) -> Any: - return self._cursor.referenced - - @property - def result_type(self) -> Any: - return self._cursor.result_type - - @property - def semantic_parent(self) -> Any: - return self._cursor.semantic_parent - - @property - def spelling(self) -> Any: - return self._cursor.spelling - - @property - def storage_class(self) -> Any: - return self._cursor.storage_class - - @property - def tls_kind(self) -> Any: - return self._cursor.tls_kind - - @property - def translation_unit(self) -> Any: - return self._cursor.translation_unit - - @property - def underlying_typedef_type(self) -> Any: - return self._cursor.underlying_typedef_type - - def from_cursor_result(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.from_cursor_result(*args, **kwargs) - - def from_location(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.from_location(*args, **kwargs) - - def from_result(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.from_result(*args, **kwargs) - - def get_arguments(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_arguments(*args, **kwargs) - - def get_bitfield_width(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_bitfield_width(*args, **kwargs) - - def get_children(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_children(*args, **kwargs) - - def get_definition(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_definition(*args, **kwargs) - - def get_field_offsetof(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_field_offsetof(*args, **kwargs) - - def get_included_file(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_included_file(*args, **kwargs) - - def get_num_template_arguments(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_num_template_arguments(*args, **kwargs) - - def get_template_argument_kind(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_template_argument_kind(*args, **kwargs) - - def get_template_argument_type(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_template_argument_type(*args, **kwargs) - - def get_template_argument_unsigned_value(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_template_argument_unsigned_value(*args, **kwargs) - - def get_template_argument_value(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_template_argument_value(*args, **kwargs) - - def get_tokens(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_tokens(*args, **kwargs) - - def get_usr(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.get_usr(*args, **kwargs) - - def is_abstract_record(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_abstract_record(*args, **kwargs) - - def is_anonymous(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_anonymous(*args, **kwargs) - - def is_bitfield(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_bitfield(*args, **kwargs) - - def is_const_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_const_method(*args, **kwargs) - - def is_converting_constructor(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_converting_constructor(*args, **kwargs) - - def is_copy_assignment_operator_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_copy_assignment_operator_method(*args, **kwargs) - - def is_copy_constructor(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_copy_constructor(*args, **kwargs) - - def is_default_constructor(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_default_constructor(*args, **kwargs) - - def is_default_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_default_method(*args, **kwargs) - - def is_definition(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_definition(*args, **kwargs) - - def is_deleted_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_deleted_method(*args, **kwargs) - - def is_explicit_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_explicit_method(*args, **kwargs) - - def is_move_assignment_operator_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_move_assignment_operator_method(*args, **kwargs) - - def is_move_constructor(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_move_constructor(*args, **kwargs) - - def is_mutable_field(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_mutable_field(*args, **kwargs) - - def is_pure_virtual_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_pure_virtual_method(*args, **kwargs) - - def is_scoped_enum(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_scoped_enum(*args, **kwargs) - - def is_static_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_static_method(*args, **kwargs) - - def is_virtual_method(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.is_virtual_method(*args, **kwargs) - - def walk_preorder(self, *args: Any, **kwargs: Any) -> Any: - return self._cursor.walk_preorder(*args, **kwargs) - - -class _TypeMirror: - _type: _ClangType - - @property - def kind(self) -> Any: - return self._type.kind - - @property - def element_count(self) -> Any: - return self._type.element_count - - @property - def element_type(self) -> Any: - return self._type.element_type - - @property - def spelling(self) -> Any: - return self._type.spelling - - @property - def translation_unit(self) -> Any: - return self._type.translation_unit - - def argument_types(self, *args: Any, **kwargs: Any) -> Any: - return self._type.argument_types(*args, **kwargs) - - def from_result(self, *args: Any, **kwargs: Any) -> Any: - return self._type.from_result(*args, **kwargs) - - def get_address_space(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_address_space(*args, **kwargs) - - def get_align(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_align(*args, **kwargs) - - def get_array_element_type(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_array_element_type(*args, **kwargs) - - def get_array_size(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_array_size(*args, **kwargs) - - def get_canonical(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_canonical(*args, **kwargs) - - def get_class_type(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_class_type(*args, **kwargs) - - def get_declaration(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_declaration(*args, **kwargs) - - def get_exception_specification_kind(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_exception_specification_kind(*args, **kwargs) - - def get_fields(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_fields(*args, **kwargs) - - def get_named_type(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_named_type(*args, **kwargs) - - def get_num_template_arguments(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_num_template_arguments(*args, **kwargs) - - def get_offset(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_offset(*args, **kwargs) - - def get_pointee(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_pointee(*args, **kwargs) - - def get_ref_qualifier(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_ref_qualifier(*args, **kwargs) - - def get_result(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_result(*args, **kwargs) - - def get_size(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_size(*args, **kwargs) - - def get_template_argument_type(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_template_argument_type(*args, **kwargs) - - def get_typedef_name(self, *args: Any, **kwargs: Any) -> Any: - return self._type.get_typedef_name(*args, **kwargs) - - def is_const_qualified(self, *args: Any, **kwargs: Any) -> Any: - return self._type.is_const_qualified(*args, **kwargs) - - def is_function_variadic(self, *args: Any, **kwargs: Any) -> Any: - return self._type.is_function_variadic(*args, **kwargs) - - def is_pod(self, *args: Any, **kwargs: Any) -> Any: - return self._type.is_pod(*args, **kwargs) - - def is_restrict_qualified(self, *args: Any, **kwargs: Any) -> Any: - return self._type.is_restrict_qualified(*args, **kwargs) - - def is_volatile_qualified(self, *args: Any, **kwargs: Any) -> Any: - return self._type.is_volatile_qualified(*args, **kwargs) diff --git a/src/xyz/cppmodel.py b/src/xyz/cppmodel.py index 05266fa..f10102f 100644 --- a/src/xyz/cppmodel.py +++ b/src/xyz/cppmodel.py @@ -7,14 +7,11 @@ from clang.cindex import CursorKind as _CursorKind from clang.cindex import Diagnostic from clang.cindex import ExceptionSpecificationKind as _ExceptionSpecificationKind +from clang.cindex import SourceLocation from clang.cindex import TranslationUnit -from clang.cindex import Type as _ClangType from clang.cindex import TypeKind as _TypeKind # Suppress type checking warnings for clang.cindex kinds. -from xyz._libclang_mirrors import _CursorMirror -from xyz._libclang_mirrors import _TypeMirror - AccessSpecifier: Any = _AccessSpecifier CursorKind: Any = _CursorKind ExceptionSpecificationKind: Any = _ExceptionSpecificationKind @@ -25,47 +22,36 @@ def _get_annotations(cursor: Cursor) -> List[str]: return [c.displayname for c in cursor.get_children() if c.kind == CursorKind.ANNOTATE_ATTR] -class Unmodelled(_CursorMirror): +class Unmodelled: def __init__(self, cursor: Cursor): - self._cursor = cursor + self.location: SourceLocation = cursor.location self.name: str = cursor.displayname def __repr__(self) -> str: return "".format(self.name, self.location) -class Type(_TypeMirror): - def __init__(self, cindex_type: _ClangType): - self._type = cindex_type +class Type: + def __init__(self, cindex_type): + self.kind = cindex_type.kind self.name = cindex_type.spelling self.is_pointer: bool = self.kind == TypeKind.POINTER self.is_reference: bool = self.kind == TypeKind.LVALUEREFERENCE + self.is_const: bool = cindex_type.is_const_qualified() if self.is_pointer or self.is_reference: self.pointee: Optional[Type] = Type(cindex_type.get_pointee()) else: self.pointee = None - @property - def is_const(self) -> bool: - import warnings - - warnings.warn("is_const is deprecated, use is_const_qualified() instead", DeprecationWarning, stacklevel=2) - return self.is_const_qualified() - def __repr__(self) -> str: return "".format(self.name) -class Member(_CursorMirror): +class Member: def __init__(self, cursor: Cursor): - self._cursor = cursor - self._wrapped_type: Type = Type(cursor.type) + self.type: Type = Type(cursor.type) self.name: str = cursor.spelling - @property - def type(self) -> Type: - return self._wrapped_type - def __repr__(self) -> str: return "".format(self.type, self.name) @@ -81,9 +67,8 @@ def __repr__(self) -> str: return "".format(self.type, self.name) -class _Function(_CursorMirror): - def __init__(self, cursor: Cursor): - self._cursor = cursor +class _Function: + def __init__(self, cursor): self.name: str = cursor.spelling arguments: List[Optional[str]] = [str(x.spelling) or None for x in cursor.get_arguments()] argument_types: List[Type] = [Type(x) for x in cursor.type.argument_types()] @@ -113,7 +98,7 @@ def __repr__(self) -> str: class Function(_Function): - def __init__(self, cursor: Cursor, namespaces: list[str] | None = None): + def __init__(self, cursor, namespaces: list[str] | None = None): namespaces = namespaces or [] _Function.__init__(self, cursor) self.namespace: str = "::".join(namespaces) @@ -128,7 +113,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() - def __eq__(self, f: Any) -> bool: + def __eq__(self, f) -> bool: if self.name != f.name: return False if self.namespace != f.namespace: @@ -142,40 +127,20 @@ def __eq__(self, f: Any) -> bool: class Method(_Function): - def __init__(self, cursor: Cursor): + def __init__(self, cursor): _Function.__init__(self, cursor) + self.is_const: bool = cursor.is_const_method() + self.is_virtual: bool = cursor.is_virtual_method() + self.is_pure_virtual: bool = cursor.is_pure_virtual_method() self.is_public: bool = cursor.access_specifier == AccessSpecifier.PUBLIC - @property - def is_const(self) -> bool: - import warnings - - warnings.warn("is_const is deprecated, use is_const_method() instead", DeprecationWarning, stacklevel=2) - return self.is_const_method() - - @property - def is_virtual(self) -> bool: - import warnings - - warnings.warn("is_virtual is deprecated, use is_virtual_method() instead", DeprecationWarning, stacklevel=2) - return self.is_virtual_method() - - @property - def is_pure_virtual(self) -> bool: - import warnings - - warnings.warn( - "is_pure_virtual is deprecated, use is_pure_virtual_method() instead", DeprecationWarning, stacklevel=2 - ) - return self.is_pure_virtual_method() - def __str__(self) -> str: s = _Function.__str__(self) - if self.is_const_method(): + if self.is_const: s = "{} const".format(s) - if self.is_pure_virtual_method(): + if self.is_pure_virtual: s = "virtual {} = 0".format(s) - elif self.is_virtual_method(): + elif self.is_virtual: s = "virtual {}".format(s) return "".format(s) @@ -183,9 +148,8 @@ def __repr__(self) -> str: return self.__str__() -class Class(_CursorMirror): +class Class: def __init__(self, cursor: Cursor, namespaces: List[str]): - self._cursor = cursor self.name: str = cursor.spelling self.namespace: str = "::".join(namespaces) self.qualified_name: str = self.name diff --git a/tests/test_libclang_mappings.py b/tests/test_libclang_mappings.py deleted file mode 100644 index 8cc8fc3..0000000 --- a/tests/test_libclang_mappings.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Tests to ensure all relevant libclang AST attributes are explicitly mapped in cppmodel. -""" - -import inspect - -import pytest -from clang.cindex import Cursor -from clang.cindex import TranslationUnit -from clang.cindex import Type as ClangType - -import xyz.cppmodel - -COMPILER_ARGS = [ - "-x", - "c++", - "-std=c++20", -] - -SOURCE = """\ -int z = 0; - -class A { - int a; - void foo(); -}; -""" - - -@pytest.fixture -def model(): - tu = TranslationUnit.from_source( - "sample.cc", - COMPILER_ARGS, - unsaved_files=[("sample.cc", SOURCE)], - ) - return xyz.cppmodel.Model(tu) - - -def get_public_attributes(cls): - """Returns a list of public attribute names for a given class.""" - return [name for name, _ in inspect.getmembers(cls) if not name.startswith("_")] - - -LIBCLANG_CURSOR_ATTRS = [attr for attr in get_public_attributes(Cursor) if attr not in ("data", "xdata")] - -LIBCLANG_TYPE_ATTRS = [attr for attr in get_public_attributes(ClangType) if attr not in ("data", "xdata")] - - -def is_mapped(wrapper, attr): - """Check if an attribute is accessible, handling exceptions from property evaluation.""" - if attr in dir(wrapper): - return True - try: - getattr(wrapper, attr) - return True - except AttributeError: - return False - except BaseException: - # Any other exception (AssertionError, Exception) means the property exists - # but evaluation failed for this specific instance's state. - return True - - -@pytest.mark.parametrize("attr", LIBCLANG_CURSOR_ATTRS) -def test_cursor_mappings(model, attr): - """Ensure all public attributes of libclang's Cursor are accessible on cppmodel wrappers.""" - # Get different wrappers that mirror Cursor - wrappers = [ - model.classes[0], # Class - model.classes[0].methods[0], # Method - model.classes[0].members[0], # Member - model.unmodelled_nodes[0], # Unmodelled - ] - - for wrapper in wrappers: - assert is_mapped(wrapper, attr), f"Missing Cursor attribute '{attr}' on {type(wrapper).__name__}" - - -@pytest.mark.parametrize("attr", LIBCLANG_TYPE_ATTRS) -def test_type_mappings(model, attr): - """Ensure all public attributes of libclang's Type are accessible on cppmodel's Type wrapper.""" - # Get a wrapper that mirrors Type - type_wrapper = model.classes[0].members[0].type - - assert is_mapped(wrapper=type_wrapper, attr=attr), ( - f"Missing Type attribute '{attr}' on {type(type_wrapper).__name__}" - )