Coverage for klayout_pex/pex25d/codec.py: 89%

79 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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""" 

26Loading and saving PEX25D artifacts in any of the three encodings. 

27 

28The two protobuf encodings are handled here — they are a 

29``SerializeToString`` away. The normative *text* format is delegated to 

30:mod:`~klayout_pex.pex25d.reader` and :mod:`~klayout_pex.pex25d.writer`. 

31""" 

32 

33from __future__ import annotations 

34 

35from typing import * 

36 

37from .artifact import ( 

38 CANONICAL_SUFFIX, 

39 ArtifactFormat, 

40 ArtifactKind, 

41 ArtifactSpec, 

42 open_artifact_read, 

43 open_artifact_write, 

44) 

45from .diagnostics import DiagnosticsReport 

46from .protobuf import kind_for_message, message_class_for_kind 

47 

48 

49def load_artifact(spec: ArtifactSpec, 

50 report: Optional[DiagnosticsReport] = None, 

51 with_source_refs: bool = False) -> Any: 

52 """ 

53 Read an artifact and return the generated protobuf message it holds 

54 (``PEX25DFile`` or ``PEX25DScene``, per ``spec.kind``). 

55 

56 Syntax- and semantic-tier findings from the text reader are appended to 

57 ``report`` when one is given; a fatal parse failure raises. 

58 

59 :raises ValueError: also if the protobuf can't be read, or holds the other kind 

60 (which binary protobuf doesn't record: only its name or --in-kind says which) 

61 """ 

62 # Read first: a missing or unreadable input is the more likely mistake, and 

63 # is a better error than 'the generated protobuf modules are not built yet'. 

64 with open_artifact_read(spec) as f: 

65 data = f.read() 

66 

67 match spec.format: 

68 case ArtifactFormat.PB: 

69 from google.protobuf.message import DecodeError 

70 message = message_class_for_kind(spec.kind)() 

71 try: 

72 message.ParseFromString(data) 

73 except DecodeError as e: 

74 raise ValueError(f"Not a PEX25D {spec.kind.value} in binary protobuf: {e}") from e 

75 # NOTE: the fields of the other kind are unknown fields here, rather than an error 

76 if _unknown_field_count(message) and _holds_the_other_kind(spec, lambda m: m.ParseFromString(data)): 

77 raise _other_kind_error(spec) 

78 return message 

79 

80 case ArtifactFormat.TEXTPB: 

81 from google.protobuf import text_format 

82 text = data.decode('utf-8') 

83 message = message_class_for_kind(spec.kind)() 

84 try: 

85 text_format.Parse(text, message) 

86 except text_format.ParseError as e: 

87 if _holds_the_other_kind(spec, lambda m: text_format.Parse(text, m)): 

88 raise _other_kind_error(spec) from e 

89 raise ValueError(f"Not a PEX25D {spec.kind.value} in protobuf text format: {e}") from e 

90 return message 

91 

92 case ArtifactFormat.TEXT: 

93 if spec.kind != ArtifactKind.FILE: 

94 raise ValueError("The PEX25D text format only spells PEX25DFile") 

95 from .reader import read_pex25d_text 

96 return read_pex25d_text(data, 

97 source_name=spec.path, 

98 report=report, 

99 with_source_refs=with_source_refs) 

100 

101 case _: 

102 raise ValueError(f"Unknown artifact format {spec.format}") 

103 

104 

105def _unknown_field_count(message: Any) -> int: 

106 """ 

107 The fields of the message and of those within it that they don't know, which binary protobuf keeps 

108 """ 

109 from google.protobuf import unknown_fields 

110 count = len(unknown_fields.UnknownFieldSet(message)) 

111 for field, value in message.ListFields(): 

112 if field.message_type is None or field.message_type.GetOptions().map_entry: 

113 continue 

114 for submessage in (value if field.is_repeated else [value]): 

115 count += _unknown_field_count(submessage) 

116 return count 

117 

118 

119def _other_kind(spec: ArtifactSpec) -> ArtifactKind: 

120 return ArtifactKind.SCENE if spec.kind == ArtifactKind.FILE else ArtifactKind.FILE 

121 

122 

123def _holds_the_other_kind(spec: ArtifactSpec, parse: Callable[[Any], None]) -> bool: 

124 """ 

125 :param parse: reads the data into the given message 

126 :return: whether the message of the other kind reads all of the data 

127 (if neither kind does, it may be of a newer format version) 

128 """ 

129 from google.protobuf.message import DecodeError 

130 from google.protobuf.text_format import ParseError 

131 other = message_class_for_kind(_other_kind(spec))() 

132 try: 

133 parse(other) 

134 except (DecodeError, ParseError): 

135 return False 

136 return not _unknown_field_count(other) 

137 

138 

139def _other_kind_error(spec: ArtifactSpec) -> ValueError: 

140 other = _other_kind(spec) 

141 return ValueError(f"It holds a PEX25D {other.value}, not a {spec.kind.value}: read it with --in-kind {other.value}, " 

142 f"or name it NAME{CANONICAL_SUFFIX[(other, spec.format)]}") 

143 

144 

145def save_artifact(message: Any, spec: ArtifactSpec, comments: bool = False) -> None: 

146 """ 

147 Write a ``PEX25DFile`` / ``PEX25DScene`` in the encoding ``spec`` asks for. 

148 

149 :param comments: emit the specification's syntax hints. Text format only — 

150 the protobuf encodings have no comments. 

151 :raises ValueError: if the message is not the kind the spec describes. The 

152 text format spells a ``PEX25DFile`` only, and a scene written under a 

153 spec that says file would otherwise fail deep inside the writer. 

154 """ 

155 kind = kind_for_message(message) 

156 if kind != spec.kind: 

157 raise ValueError(f"The artifact spec for '{spec.path}' says " 

158 f"{spec.kind}, but the message is a {kind}") 

159 

160 match spec.format: 

161 case ArtifactFormat.PB: 

162 data = message.SerializeToString() 

163 

164 case ArtifactFormat.TEXTPB: 

165 from google.protobuf import text_format 

166 data = text_format.MessageToString(message, as_utf8=True).encode('utf-8') 

167 

168 case ArtifactFormat.TEXT: 

169 if spec.kind != ArtifactKind.FILE: 

170 raise ValueError("The PEX25D text format only spells PEX25DFile") 

171 from .writer import write_pex25d_text 

172 data = write_pex25d_text(message, comments=comments) 

173 

174 case _: 

175 raise ValueError(f"Unknown artifact format {spec.format}") 

176 

177 with open_artifact_write(spec) as f: 

178 f.write(data)