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
« 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#
25"""
26Loading and saving PEX25D artifacts in any of the three encodings.
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"""
33from __future__ import annotations
35from typing import *
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
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``).
56 Syntax- and semantic-tier findings from the text reader are appended to
57 ``report`` when one is given; a fatal parse failure raises.
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()
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
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
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)
101 case _:
102 raise ValueError(f"Unknown artifact format {spec.format}")
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
119def _other_kind(spec: ArtifactSpec) -> ArtifactKind:
120 return ArtifactKind.SCENE if spec.kind == ArtifactKind.FILE else ArtifactKind.FILE
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)
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)]}")
145def save_artifact(message: Any, spec: ArtifactSpec, comments: bool = False) -> None:
146 """
147 Write a ``PEX25DFile`` / ``PEX25DScene`` in the encoding ``spec`` asks for.
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}")
160 match spec.format:
161 case ArtifactFormat.PB:
162 data = message.SerializeToString()
164 case ArtifactFormat.TEXTPB:
165 from google.protobuf import text_format
166 data = text_format.MessageToString(message, as_utf8=True).encode('utf-8')
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)
174 case _:
175 raise ValueError(f"Unknown artifact format {spec.format}")
177 with open_artifact_write(spec) as f:
178 f.write(data)