Coverage for klayout_pex/pex25d/protobuf.py: 88%
66 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2026 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
25"""
26Lazy access to the generated PEX25D protobuf modules.
28The ``*_pb2.py`` modules are produced by the build (``build.sh`` / CMake) and are ``.gitignore``d,
29so importing them at module scope would make ``--help`` fail on a fresh checkout.
30Every accessor here imports on demand, ``ImportError``s are turned into actionable messages instead of tracebacks.
31"""
33from __future__ import annotations
35import functools
36from typing import *
39class ProtobufNotGeneratedError(RuntimeError):
40 """The generated PEX25D protobuf modules are not importable."""
43def _import(module_name: str) -> Any:
44 import importlib
45 try:
46 return importlib.import_module(module_name)
47 except ImportError as e:
48 raise ProtobufNotGeneratedError(
49 f"Can't import the generated PEX25D protobuf module '{module_name}'.\n"
50 f"The *_pb2.py modules are generated by the build and are not checked in — "
51 f"run ./build.sh (or the CMake target) and try again.\n"
52 f"Original error: {e}"
53 ) from e
54 except Exception as e:
55 # A gencode/runtime version mismatch raises protobuf's own VersionError,
56 # which is not an ImportError and would otherwise surface as a traceback
57 # from deep inside the generated module. It means the *_pb2.py files were
58 # produced by a different protoc than the protobuf runtime in use.
59 if type(e).__name__ != 'VersionError':
60 raise
61 raise ProtobufNotGeneratedError(
62 f"The generated module '{module_name}' does not match the installed "
63 f"protobuf runtime — re-run ./build.sh (or the CMake target) to "
64 f"regenerate it.\nOriginal error: {e}"
65 ) from e
68_PACKAGE = 'klayout_pex_protobuf.kpex.pex25d'
69_MODULE_PREFIX = 'pex25d_'
70_MODULE_SUFFIX = '_pb2'
73@functools.cache
74def schema_names() -> Tuple[str, ...]:
75 """
76 The PEX25D schema names the build generated, e.g. ``('diagnostics', …)``.
78 Discovered in the generated package rather than listed here, so that adding
79 or removing a ``.proto`` needs no edit: the module naming convention
80 ``pex25d_<name>_pb2`` is the whole mapping.
81 """
82 import pkgutil
83 package = _import(_PACKAGE)
84 return tuple(sorted(
85 name[len(_MODULE_PREFIX):-len(_MODULE_SUFFIX)]
86 for _, name, _ in pkgutil.iter_modules(package.__path__)
87 if name.startswith(_MODULE_PREFIX) and name.endswith(_MODULE_SUFFIX)
88 ))
91def schema_module(name: str) -> Any:
92 """The generated module of one schema, see :func:`schema_names`."""
93 if name not in schema_names():
94 raise ValueError(f"PEX25D has no schema '{name}'; the build generated "
95 f"{', '.join(schema_names())}")
96 return _import(f"{_PACKAGE}.{_MODULE_PREFIX}{name}{_MODULE_SUFFIX}")
99@functools.cache
100def pex25d_file_pb2() -> Any:
101 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_file_pb2')
104@functools.cache
105def pex25d_scene_pb2() -> Any:
106 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_scene_pb2')
109@functools.cache
110def pex25d_dielectric_pb2() -> Any:
111 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_dielectric_pb2')
114@functools.cache
115def pex25d_geometry_pb2() -> Any:
116 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_geometry_pb2')
119@functools.cache
120def pex25d_terminal_pb2() -> Any:
121 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_terminal_pb2')
124@functools.cache
125def pex25d_diagnostics_pb2() -> Any:
126 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_diagnostics_pb2')
129@functools.cache
130def pex25d_source_ref_pb2() -> Any:
131 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_source_ref_pb2')
134def kind_for_message(message: Any) -> 'ArtifactKind': # noqa: F821 (avoid import cycle)
135 """
136 Return the :class:`ArtifactKind` a generated message holds.
138 Derived from :func:`message_class_for_kind`, so that a message renamed in
139 the schema is one edit rather than two that can drift apart.
140 """
141 from .artifact import ArtifactKind
143 for kind in ArtifactKind:
144 try:
145 message_class = message_class_for_kind(kind)
146 except ValueError: # a kind that describes no message, i.e. AUTO
147 continue
148 if message.DESCRIPTOR is message_class.DESCRIPTOR:
149 return kind
151 raise ValueError(f"'{message.DESCRIPTOR.name}' is not a PEX25D artifact message")
154def message_class_for_kind(kind: 'ArtifactKind') -> Any: # noqa: F821 (avoid import cycle)
155 """Return the generated message class matching an :class:`ArtifactKind`."""
156 from .artifact import ArtifactKind
158 match kind:
159 case ArtifactKind.FILE:
160 return pex25d_file_pb2().PEX25DFile
161 case ArtifactKind.SCENE:
162 return pex25d_scene_pb2().PEX25DScene
163 case _:
164 raise ValueError(f"No message class for artifact kind {kind}")