Coverage for klayout_pex/pex25d/artifact.py: 75%

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

26Naming, format inference and stdio handling for PEX25D artifacts. 

27 

28There are two orthogonal properties of every PEX25D artifact on disk: 

29 

30``kind`` 

31 Which message it holds — the unresolved ``PEX25DFile`` as written by a 

32 reader, or the resolved ``PEX25DScene`` as consumed by a solver adapter. 

33 

34``format`` 

35 How that message is encoded — the normative PEX25D *text* format, binary 

36 protobuf, or protobuf text format. 

37 

38Both are inferred from the file name, and either can be overridden 

39(``--kind`` / ``--format``). The naming convention is:: 

40 

41 foo.pex25d text format, PEX25DFile 

42 foo.pex25d.pb binary protobuf, PEX25DFile 

43 foo.pex25d.textpb protobuf text format, PEX25DFile 

44 foo.pex25d.scene.pb binary protobuf, PEX25DScene 

45 foo.pex25d.scene.textpb protobuf text format, PEX25DScene 

46 

47i.e. the ``.scene`` infix selects the kind and the final extension selects the 

48format. There is deliberately no text-format spelling of a scene: the text 

49format is the *unresolved* format, and resolution is not reversible. 

50""" 

51 

52from __future__ import annotations 

53 

54import contextlib 

55import gzip 

56import io 

57import os 

58import sys 

59from dataclasses import dataclass 

60from enum import StrEnum 

61from typing import * 

62 

63 

64# A single '-' as a path means stdin (for inputs) or stdout (for outputs). 

65STDIO_PATH = '-' 

66 

67 

68class ArtifactKind(StrEnum): 

69 """Which PEX25D message an artifact holds.""" 

70 

71 AUTO = 'auto' # infer from the path 

72 FILE = 'file' # kpex.pex25d.PEX25DFile — literal, unresolved 

73 SCENE = 'scene' # kpex.pex25d.PEX25DScene — resolved, adapter-ready 

74 

75 

76class ArtifactFormat(StrEnum): 

77 """How a PEX25D message is encoded.""" 

78 

79 AUTO = 'auto' # infer from the path 

80 TEXT = 'text' # the normative PEX25D text format (PEX25DFile only) 

81 PB = 'pb' # binary protobuf 

82 TEXTPB = 'textpb' # protobuf text format 

83 

84 

85# Suffix table, longest first so that '.pex25d.scene.pb' is matched before 

86# '.pex25d.pb' would be. Gzip is handled separately, by stripping a trailing 

87# '.gz' before the lookup — the repo already writes '.gds.gz' and layout-sized 

88# PEX25D text compresses extremely well. 

89_SUFFIXES: List[Tuple[str, ArtifactKind, ArtifactFormat]] = [ 

90 ('.pex25d.scene.textpb', ArtifactKind.SCENE, ArtifactFormat.TEXTPB), 

91 ('.pex25d.scene.pb', ArtifactKind.SCENE, ArtifactFormat.PB), 

92 ('.pex25d.textpb', ArtifactKind.FILE, ArtifactFormat.TEXTPB), 

93 ('.pex25d.pb', ArtifactKind.FILE, ArtifactFormat.PB), 

94 ('.pex25d', ArtifactKind.FILE, ArtifactFormat.TEXT), 

95] 

96 

97# Canonical spelling for each (kind, format) pair, used when a verb has to 

98# derive an output name from an input name. 

99CANONICAL_SUFFIX: Dict[Tuple[ArtifactKind, ArtifactFormat], str] = { 

100 (kind, fmt): suffix for suffix, kind, fmt in reversed(_SUFFIXES) 

101} 

102 

103 

104class ArtifactNamingError(ValueError): 

105 """The kind or format of an artifact could not be determined.""" 

106 

107 

108@dataclass(frozen=True) 

109class ArtifactSpec: 

110 """A fully determined artifact: where it lives, what it holds, how it is encoded.""" 

111 

112 path: str 

113 kind: ArtifactKind 

114 format: ArtifactFormat 

115 gzipped: bool = False 

116 

117 @property 

118 def is_stdio(self) -> bool: 

119 return self.path == STDIO_PATH 

120 

121 @property 

122 def is_binary(self) -> bool: 

123 return self.format == ArtifactFormat.PB or self.gzipped 

124 

125 def __str__(self) -> str: 

126 where = '<stdin>/<stdout>' if self.is_stdio else self.path 

127 return f"{where} ({self.kind.value}, {self.format.value}{', gzipped' if self.gzipped else ''})" 

128 

129 

130def infer_artifact_spec(path: str, 

131 kind: ArtifactKind = ArtifactKind.AUTO, 

132 format: ArtifactFormat = ArtifactFormat.AUTO, 

133 default_kind: ArtifactKind = ArtifactKind.FILE, 

134 default_format: ArtifactFormat = ArtifactFormat.TEXT) -> ArtifactSpec: 

135 """ 

136 Determine an artifact's kind and encoding from its path, honouring explicit 

137 overrides. 

138 

139 Inference is by suffix (see the table above). For ``-`` (stdio) there is no 

140 suffix to inspect, so the explicit values are used, falling back to 

141 ``default_kind`` / ``default_format`` — which is why every verb that accepts 

142 ``-`` states its defaults in ``--help``. 

143 

144 :raises ArtifactNamingError: if the path carries no recognized suffix and no 

145 override was given, or if the resulting combination cannot exist. 

146 """ 

147 gzipped = False 

148 inferred_kind = default_kind 

149 inferred_format = default_format 

150 

151 if path != STDIO_PATH: 

152 name = os.path.basename(path) 

153 if name.endswith('.gz'): 

154 gzipped = True 

155 name = name[:-len('.gz')] 

156 

157 for suffix, suffix_kind, suffix_format in _SUFFIXES: 

158 if name.endswith(suffix): 

159 inferred_kind = suffix_kind 

160 inferred_format = suffix_format 

161 break 

162 else: 

163 if kind == ArtifactKind.AUTO or format == ArtifactFormat.AUTO: 

164 raise ArtifactNamingError( 

165 f"Can't tell what kind of PEX25D artifact '{path}' is meant to be. " 

166 f"Use one of the conventional suffixes " 

167 f"({', '.join(suffix for suffix, _, _ in reversed(_SUFFIXES))}), " 

168 f"or state the encoding explicitly — see --help for the " 

169 f"--format / --kind options this command offers." 

170 ) 

171 

172 effective_kind = inferred_kind if kind == ArtifactKind.AUTO else kind 

173 effective_format = inferred_format if format == ArtifactFormat.AUTO else format 

174 

175 if effective_kind == ArtifactKind.SCENE and effective_format == ArtifactFormat.TEXT: 

176 raise ArtifactNamingError( 

177 "The PEX25D text format has no spelling for a resolved scene — it is the " 

178 "unresolved format, and resolution is not reversible. Write the scene as " 

179 "'pb' or 'textpb', or write the unresolved file as 'text'." 

180 ) 

181 

182 return ArtifactSpec(path=path, 

183 kind=effective_kind, 

184 format=effective_format, 

185 gzipped=gzipped) 

186 

187 

188@contextlib.contextmanager 

189def open_artifact_read(spec: ArtifactSpec) -> Iterator[BinaryIO]: 

190 """ 

191 Open an artifact for reading, as bytes. 

192 

193 Bytes rather than text even for the text formats: the PEX25D reader scans 

194 decimal digits straight into scaled integers and wants to control decoding 

195 itself (the format is UTF-8 by definition, so there is nothing to negotiate). 

196 """ 

197 if spec.is_stdio: 

198 # sys.__stdin__ rather than sys.stdin: when an artifact is written to 

199 # stdout the process redirects sys.stdout to stderr so that logging can 

200 # never corrupt the stream, and the artifact I/O must bypass that. 

201 stream: BinaryIO = sys.__stdin__.buffer 

202 if spec.gzipped: 

203 with gzip.GzipFile(fileobj=stream, mode='rb') as f: 

204 yield cast(BinaryIO, f) 

205 else: 

206 yield stream 

207 return 

208 

209 opener = gzip.open if spec.gzipped else open 

210 with opener(spec.path, 'rb') as f: 

211 yield cast(BinaryIO, f) 

212 

213 

214@contextlib.contextmanager 

215def open_artifact_write(spec: ArtifactSpec) -> Iterator[BinaryIO]: 

216 """ 

217 Open an artifact for writing, as bytes. 

218 

219 Note that when the destination is stdout, *nothing else may be written 

220 there* — diagnostics and progress go to stderr. See 

221 :func:`klayout_pex.pex25d.diagnostics.diagnostics_stream`. 

222 """ 

223 if spec.is_stdio: 

224 # The real stdout, not the redirected sys.stdout — see open_artifact_read. 

225 stream: BinaryIO = sys.__stdout__.buffer 

226 if spec.gzipped: 

227 with gzip.GzipFile(fileobj=stream, mode='wb') as f: 

228 yield cast(BinaryIO, f) 

229 else: 

230 yield stream 

231 stream.flush() 

232 return 

233 

234 parent = os.path.dirname(os.path.abspath(spec.path)) 

235 os.makedirs(parent, exist_ok=True) 

236 

237 opener = gzip.open if spec.gzipped else open 

238 with opener(spec.path, 'wb') as f: 

239 yield cast(BinaryIO, f) 

240 

241 

242def derive_path(source_path: str, 

243 kind: ArtifactKind, 

244 format: ArtifactFormat) -> str: 

245 """ 

246 Derive a sibling artifact path from an existing one, e.g. ``foo.pex25d`` → 

247 ``foo.pex25d.scene.pb``. Used only where a verb offers a default output 

248 location; a verb never writes to a derived path without saying so. 

249 """ 

250 if source_path == STDIO_PATH: 

251 return STDIO_PATH 

252 

253 name = os.path.basename(source_path) 

254 if name.endswith('.gz'): 

255 name = name[:-len('.gz')] 

256 for suffix, _, _ in _SUFFIXES: 

257 if name.endswith(suffix): 

258 name = name[:-len(suffix)] 

259 break 

260 else: 

261 name = os.path.splitext(name)[0] 

262 

263 return os.path.join(os.path.dirname(source_path), 

264 name + CANONICAL_SUFFIX[(kind, format)])