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

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# 

24 

25""" 

26Lazy access to the generated PEX25D protobuf modules. 

27 

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""" 

32 

33from __future__ import annotations 

34 

35import functools 

36from typing import * 

37 

38 

39class ProtobufNotGeneratedError(RuntimeError): 

40 """The generated PEX25D protobuf modules are not importable.""" 

41 

42 

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 

66 

67 

68_PACKAGE = 'klayout_pex_protobuf.kpex.pex25d' 

69_MODULE_PREFIX = 'pex25d_' 

70_MODULE_SUFFIX = '_pb2' 

71 

72 

73@functools.cache 

74def schema_names() -> Tuple[str, ...]: 

75 """ 

76 The PEX25D schema names the build generated, e.g. ``('diagnostics', …)``. 

77 

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 )) 

89 

90 

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}") 

97 

98 

99@functools.cache 

100def pex25d_file_pb2() -> Any: 

101 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_file_pb2') 

102 

103 

104@functools.cache 

105def pex25d_scene_pb2() -> Any: 

106 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_scene_pb2') 

107 

108 

109@functools.cache 

110def pex25d_dielectric_pb2() -> Any: 

111 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_dielectric_pb2') 

112 

113 

114@functools.cache 

115def pex25d_geometry_pb2() -> Any: 

116 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_geometry_pb2') 

117 

118 

119@functools.cache 

120def pex25d_terminal_pb2() -> Any: 

121 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_terminal_pb2') 

122 

123 

124@functools.cache 

125def pex25d_diagnostics_pb2() -> Any: 

126 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_diagnostics_pb2') 

127 

128 

129@functools.cache 

130def pex25d_source_ref_pb2() -> Any: 

131 return _import('klayout_pex_protobuf.kpex.pex25d.pex25d_source_ref_pb2') 

132 

133 

134def kind_for_message(message: Any) -> 'ArtifactKind': # noqa: F821 (avoid import cycle) 

135 """ 

136 Return the :class:`ArtifactKind` a generated message holds. 

137 

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 

142 

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 

150 

151 raise ValueError(f"'{message.DESCRIPTOR.name}' is not a PEX25D artifact message") 

152 

153 

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 

157 

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}")