From 38fc3066443c0a26e73b2b51b8bb144e2139012a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Aug 2022 23:06:41 -0700 Subject: [PATCH 01/42] Move to hatch-based build --- .gitignore | 3 ++- klamath/LICENSE.md | 1 + klamath/README.md | 1 + klamath/VERSION.py | 4 --- klamath/__init__.py | 4 +-- pyproject.toml | 58 +++++++++++++++++++++++++++++++++++++++++ setup.py | 63 --------------------------------------------- 7 files changed, 64 insertions(+), 70 deletions(-) create mode 120000 klamath/LICENSE.md create mode 120000 klamath/README.md delete mode 100644 klamath/VERSION.py create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 6e1d4a7..71e10d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.pyc -__pycache__ +__pycache__/ *.idea @@ -7,6 +7,7 @@ build/ dist/ *.egg-info/ .mypy_cache/ +.pytest_cache/ *.swp *.swo diff --git a/klamath/LICENSE.md b/klamath/LICENSE.md new file mode 120000 index 0000000..7eabdb1 --- /dev/null +++ b/klamath/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/klamath/README.md b/klamath/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/klamath/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/klamath/VERSION.py b/klamath/VERSION.py deleted file mode 100644 index b99a6d4..0000000 --- a/klamath/VERSION.py +++ /dev/null @@ -1,4 +0,0 @@ -""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ -__version__ = ''' -1.2 -'''.strip() diff --git a/klamath/__init__.py b/klamath/__init__.py index 7a8c54e..a7e137b 100644 --- a/klamath/__init__.py +++ b/klamath/__init__.py @@ -33,6 +33,6 @@ from . import records from . import elements from . import library -from .VERSION import __version__ - __author__ = 'Jan Petykiewicz' +__version__ = '1.2' + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..11d7918 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "klamath" +description = "GDSII format reader/writer" +readme = "README.md" +license = { file = "LICENSE.md" } +authors = [ + { name="Jan Petykiewicz", email="jan@mpxd.net" }, + ] +homepage = "https://mpxd.net/code/jan/klamath" +repository = "https://mpxd.net/code/jan/klamath" +keywords = [ + "layout", + "gds", + "gdsii", + "gds2", + "Calma", + "stream", + "design", + "CAD", + "EDA", + "electronics", + "photonics", + "IC", + "mask", + "pattern", + "drawing", + "lithography", + "litho", + "geometry", + "geometric", + "polygon", + "vector", + ] +classifiers = [ + "Programming Language :: Python :: 3", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: Manufacturing", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", + ] +requires-python = ">=3.8" +include = [ + "LICENSE.md" + ] +dynamic = ["version"] +dependencies = [ + "numpy~=1.21", + ] + +[tool.hatch.version] +path = "klamath/__init__.py" diff --git a/setup.py b/setup.py deleted file mode 100644 index 732b4c9..0000000 --- a/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 - -from setuptools import setup, find_packages - - -with open('README.md', 'r') as f: - long_description = f.read() - -with open('klamath/VERSION.py', 'rt') as f: - version = f.readlines()[2].strip() - -setup(name='klamath', - version=version, - description='GDSII format reader/writer', - long_description=long_description, - long_description_content_type='text/markdown', - author='Jan Petykiewicz', - author_email='jan@mpxd.net', - url='https://mpxd.net/code/jan/klamath', - packages=find_packages(), - package_data={ - 'klamath': ['py.typed'], - }, - install_requires=[ - 'numpy', - ], - classifiers=[ - 'Programming Language :: Python :: 3', - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Intended Audience :: Manufacturing', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', - ], - keywords=[ - 'layout', - 'design', - 'CAD', - 'EDA', - 'electronics', - 'photonics', - 'IC', - 'mask', - 'pattern', - 'drawing', - 'lithography', - 'litho', - 'geometry', - 'geometric', - 'polygon', - 'gds', - 'gdsii', - 'gds2', - 'stream', - 'vector', - 'freeform', - 'manhattan', - 'angle', - 'Calma', - ], - ) From e90e44bd156fc2e0c93260737aa387fa3dcdf3d9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Aug 2022 23:07:23 -0700 Subject: [PATCH 02/42] bump version to v1.3 --- klamath/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/__init__.py b/klamath/__init__.py index a7e137b..4aef521 100644 --- a/klamath/__init__.py +++ b/klamath/__init__.py @@ -34,5 +34,5 @@ from . import elements from . import library __author__ = 'Jan Petykiewicz' -__version__ = '1.2' +__version__ = '1.3' From 0bbc8f8e0874effb25d2fa7a14823c8b74ca9851 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 26 Jan 2023 12:32:19 -0800 Subject: [PATCH 03/42] Loosen constraints from BinaryIO to IO[bytes] --- klamath/basic.py | 4 ++-- klamath/elements.py | 34 +++++++++++++++++----------------- klamath/library.py | 16 ++++++++-------- klamath/record.py | 36 ++++++++++++++++++------------------ 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/klamath/basic.py b/klamath/basic.py index fd1b417..e680cb1 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -1,7 +1,7 @@ """ Functionality for encoding/decoding basic datatypes """ -from typing import Sequence, BinaryIO, List +from typing import Sequence, IO, List import struct from datetime import datetime @@ -172,7 +172,7 @@ def pack_datetime(data: Sequence[datetime]) -> bytes: return pack_int2(parts) -def read(stream: BinaryIO, size: int) -> bytes: +def read(stream: IO[bytes], size: int) -> bytes: """ Read and check for failure """ data = stream.read(size) if len(data) != size: diff --git a/klamath/elements.py b/klamath/elements.py index a477642..1fe3837 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -2,7 +2,7 @@ Functionality for reading/writing elements (geometry, text labels, structure references) and associated properties. """ -from typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type, Union +from typing import Dict, Tuple, Optional, IO, TypeVar, Type, Union from abc import ABCMeta, abstractmethod from dataclasses import dataclass @@ -29,7 +29,7 @@ X = TypeVar('X', bound='Box') -def read_properties(stream: BinaryIO) -> Dict[int, bytes]: +def read_properties(stream: IO[bytes]) -> Dict[int, bytes]: """ Read element properties. @@ -56,7 +56,7 @@ def read_properties(stream: BinaryIO) -> Dict[int, bytes]: return properties -def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int: +def write_properties(stream: IO[bytes], properties: Dict[int, bytes]) -> int: """ Write element properties. @@ -78,7 +78,7 @@ class Element(metaclass=ABCMeta): """ @classmethod @abstractmethod - def read(cls: Type[E], stream: BinaryIO) -> E: + def read(cls: Type[E], stream: IO[bytes]) -> E: """ Read from a stream to construct this object. Consumes up to (and including) the ENDEL record. @@ -92,7 +92,7 @@ class Element(metaclass=ABCMeta): pass @abstractmethod - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: """ Write this element to a stream. Finishes with an ENDEL record. @@ -151,7 +151,7 @@ class Reference(Element): """ Properties associated with this reference. """ @classmethod - def read(cls: Type[R], stream: BinaryIO) -> R: + def read(cls: Type[R], stream: IO[bytes]) -> R: invert_y = False mag = 1 angle_deg = 0 @@ -177,7 +177,7 @@ class Reference(Element): return cls(struct_name=struct_name, xy=xy, properties=properties, colrow=colrow, invert_y=invert_y, mag=mag, angle_deg=angle_deg) - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: b = 0 if self.colrow is None: b += SREF.write(stream, None) @@ -226,14 +226,14 @@ class Boundary(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[B], stream: BinaryIO) -> B: + def read(cls: Type[B], stream: IO[bytes]) -> B: layer = LAYER.skip_and_read(stream)[0] dtype = DATATYPE.read(stream)[0] xy = XY.read(stream).reshape(-1, 2) properties = read_properties(stream) return cls(layer=(layer, dtype), xy=xy, properties=properties) - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: b = BOUNDARY.write(stream, None) b += LAYER.write(stream, self.layer[0]) b += DATATYPE.write(stream, self.layer[1]) @@ -272,7 +272,7 @@ class Path(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[P], stream: BinaryIO) -> P: + def read(cls: Type[P], stream: IO[bytes]) -> P: path_type = 0 width = 0 bgn_ext = 0 @@ -299,7 +299,7 @@ class Path(Element): properties=properties, extension=(bgn_ext, end_ext), path_type=path_type, width=width) - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: b = PATH.write(stream, None) b += LAYER.write(stream, self.layer[0]) b += DATATYPE.write(stream, self.layer[1]) @@ -337,14 +337,14 @@ class Box(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[X], stream: BinaryIO) -> X: + def read(cls: Type[X], stream: IO[bytes]) -> X: layer = LAYER.skip_and_read(stream)[0] dtype = BOXTYPE.read(stream)[0] xy = XY.read(stream).reshape(-1, 2) properties = read_properties(stream) return cls(layer=(layer, dtype), xy=xy, properties=properties) - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: b = BOX.write(stream, None) b += LAYER.write(stream, self.layer[0]) b += BOXTYPE.write(stream, self.layer[1]) @@ -371,14 +371,14 @@ class Node(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[N], stream: BinaryIO) -> N: + def read(cls: Type[N], stream: IO[bytes]) -> N: layer = LAYER.skip_and_read(stream)[0] dtype = NODETYPE.read(stream)[0] xy = XY.read(stream).reshape(-1, 2) properties = read_properties(stream) return cls(layer=(layer, dtype), xy=xy, properties=properties) - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: b = NODE.write(stream, None) b += LAYER.write(stream, self.layer[0]) b += NODETYPE.write(stream, self.layer[1]) @@ -431,7 +431,7 @@ class Text(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[T], stream: BinaryIO) -> T: + def read(cls: Type[T], stream: IO[bytes]) -> T: path_type = 0 presentation = 0 invert_y = False @@ -467,7 +467,7 @@ class Text(Element): string=string, presentation=presentation, path_type=path_type, width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg) - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: b = TEXT.write(stream, None) b += LAYER.write(stream, self.layer[0]) b += TEXTTYPE.write(stream, self.layer[1]) diff --git a/klamath/library.py b/klamath/library.py index 1318854..50c8398 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -1,7 +1,7 @@ """ File-level read/write functionality. """ -from typing import List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type, MutableMapping +from typing import List, Dict, Tuple, Optional, IO, TypeVar, Type, MutableMapping import io from datetime import datetime from dataclasses import dataclass @@ -45,7 +45,7 @@ class FileHeader: """ Last-accessed time """ @classmethod - def read(cls: Type[FH], stream: BinaryIO) -> FH: + def read(cls: Type[FH], stream: IO[bytes]) -> FH: """ Read and construct a header from the provided stream. @@ -63,7 +63,7 @@ class FileHeader: return cls(mod_time=mod_time, acc_time=acc_time, name=name, user_units_per_db_unit=uu, meters_per_db_unit=dbu) - def write(self, stream: BinaryIO) -> int: + def write(self, stream: IO[bytes]) -> int: """ Write the header to a stream @@ -80,7 +80,7 @@ class FileHeader: return b -def scan_structs(stream: BinaryIO) -> Dict[bytes, int]: +def scan_structs(stream: IO[bytes]) -> Dict[bytes, int]: """ Scan through a GDS file, building a table of {b'structure_name': byte_offset}. @@ -107,7 +107,7 @@ def scan_structs(stream: BinaryIO) -> Dict[bytes, int]: return positions -def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]: +def try_read_struct(stream: IO[bytes]) -> Optional[Tuple[bytes, List[Element]]]: """ Skip to the next structure and attempt to read it. @@ -125,7 +125,7 @@ def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]: return name, elements -def write_struct(stream: BinaryIO, +def write_struct(stream: IO[bytes], name: bytes, elements: List[Element], cre_time: datetime = datetime(1900, 1, 1), @@ -150,7 +150,7 @@ def write_struct(stream: BinaryIO, return b -def read_elements(stream: BinaryIO) -> List[Element]: +def read_elements(stream: IO[bytes]) -> List[Element]: """ Read elements from the stream until an ENDSTR record is encountered. The ENDSTR record is also @@ -186,7 +186,7 @@ def read_elements(stream: BinaryIO) -> List[Element]: return data -def scan_hierarchy(stream: BinaryIO) -> Dict[bytes, Dict[bytes, int]]: +def scan_hierarchy(stream: IO[bytes]) -> Dict[bytes, Dict[bytes, int]]: """ Scan through a GDS file, building a table of instance counts `{b'structure_name': {b'ref_name': count}}`. diff --git a/klamath/record.py b/klamath/record.py index e6fb5fb..69769dd 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -1,7 +1,7 @@ """ Generic record-level read/write functionality. """ -from typing import Optional, Sequence, BinaryIO +from typing import Optional, Sequence, IO from typing import TypeVar, List, Tuple, ClassVar, Type import struct import io @@ -19,7 +19,7 @@ from .basic import parse_ascii, pack_ascii, read _RECORD_HEADER_FMT = struct.Struct('>HH') -def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int: +def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int: record_size = data_size + 4 if record_size > 0xFFFF: raise KlamathError(f'Record size is too big: {record_size}') @@ -27,7 +27,7 @@ def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int: return stream.write(header) -def read_record_header(stream: BinaryIO) -> Tuple[int, int]: +def read_record_header(stream: IO[bytes]) -> Tuple[int, int]: """ Read a record's header (size and tag). Args: @@ -46,7 +46,7 @@ def read_record_header(stream: BinaryIO) -> Tuple[int, int]: return data_size, tag -def expect_record(stream: BinaryIO, tag: int) -> int: +def expect_record(stream: IO[bytes], tag: int) -> int: data_size, actual_tag = read_record_header(stream) if tag != actual_tag: raise KlamathError(f'Unexpected record! Got tag 0x{actual_tag:04x}, expected 0x{tag:04x}') @@ -71,7 +71,7 @@ class Record(metaclass=ABCMeta): @classmethod @abstractmethod - def read_data(cls, stream: BinaryIO, size: int): + def read_data(cls, stream: IO[bytes], size: int): pass @classmethod @@ -80,15 +80,15 @@ class Record(metaclass=ABCMeta): pass @staticmethod - def read_header(stream: BinaryIO) -> Tuple[int, int]: + def read_header(stream: IO[bytes]) -> Tuple[int, int]: return read_record_header(stream) @classmethod - def write_header(cls, stream: BinaryIO, data_size: int) -> int: + def write_header(cls, stream: IO[bytes], data_size: int) -> int: return write_record_header(stream, data_size, cls.tag) @classmethod - def skip_past(cls, stream: BinaryIO) -> bool: + def skip_past(cls, stream: IO[bytes]) -> bool: """ Skip to the end of the next occurence of this record. @@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta): return True @classmethod - def skip_and_read(cls, stream: BinaryIO): + def skip_and_read(cls, stream: IO[bytes]): size, tag = Record.read_header(stream) while tag != cls.tag: stream.seek(size, io.SEEK_CUR) @@ -119,13 +119,13 @@ class Record(metaclass=ABCMeta): return data @classmethod - def read(cls: Type[R], stream: BinaryIO): + def read(cls: Type[R], stream: IO[bytes]): size = expect_record(stream, cls.tag) data = cls.read_data(stream, size) return data @classmethod - def write(cls, stream: BinaryIO, data) -> int: + def write(cls, stream: IO[bytes], data) -> int: data_bytes = cls.pack_data(data) b = cls.write_header(stream, len(data_bytes)) b += stream.write(data_bytes) @@ -136,7 +136,7 @@ class NoDataRecord(Record): expected_size: ClassVar[Optional[int]] = 0 @classmethod - def read_data(cls, stream: BinaryIO, size: int) -> None: + def read_data(cls, stream: IO[bytes], size: int) -> None: stream.read(size) @classmethod @@ -150,7 +150,7 @@ class BitArrayRecord(Record): expected_size: ClassVar[Optional[int]] = 2 @classmethod - def read_data(cls, stream: BinaryIO, size: int) -> int: + def read_data(cls, stream: IO[bytes], size: int) -> int: return parse_bitarray(read(stream, 2)) @classmethod @@ -160,7 +160,7 @@ class BitArrayRecord(Record): class Int2Record(Record): @classmethod - def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: return parse_int2(read(stream, size)) @classmethod @@ -170,7 +170,7 @@ class Int2Record(Record): class Int4Record(Record): @classmethod - def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: return parse_int4(read(stream, size)) @classmethod @@ -180,7 +180,7 @@ class Int4Record(Record): class Real8Record(Record): @classmethod - def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: return parse_real8(read(stream, size)) @classmethod @@ -190,7 +190,7 @@ class Real8Record(Record): class ASCIIRecord(Record): @classmethod - def read_data(cls, stream: BinaryIO, size: int) -> bytes: + def read_data(cls, stream: IO[bytes], size: int) -> bytes: return parse_ascii(read(stream, size)) @classmethod @@ -200,7 +200,7 @@ class ASCIIRecord(Record): class DateTimeRecord(Record): @classmethod - def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]: + def read_data(cls, stream: IO[bytes], size: int) -> List[datetime]: return parse_datetime(read(stream, size)) @classmethod From 4d362f8e0936c15c9ce6637758ab11f5886f715b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 26 Jan 2023 12:40:54 -0800 Subject: [PATCH 04/42] do some linting with flake8 --- .flake8 | 30 ++++++++++++++++++++++++++ klamath/basic.py | 4 ++-- klamath/elements.py | 5 ++--- klamath/library.py | 4 ++-- klamath/records.py | 10 ++++----- klamath/test_basic.py | 50 +++++++++++++++++++++---------------------- 6 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..1bdbec5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,30 @@ +[flake8] +ignore = + # E501 line too long + E501, + # W391 newlines at EOF + W391, + # E241 multiple spaces after comma + E241, + # E302 expected 2 newlines + E302, + # W503 line break before binary operator (to be deprecated) + W503, + # E265 block comment should start with '# ' + E265, + # E123 closing bracket does not match indentation of opening bracket's line + E123, + # E124 closing bracket does not match visual indentation + E124, + # E221 multiple spaces before operator + E221, + # E201 whitespace after '[' + E201, +# # E741 ambiguous variable name 'I' +# E741, + + +per-file-ignores = + # F401 import without use + */__init__.py: F401, + __init__.py: F401, diff --git a/klamath/basic.py b/klamath/basic.py index e680cb1..3a319e9 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -68,7 +68,7 @@ def parse_datetime(data: bytes) -> List[datetime]: raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.') dts = [] for ii in range(0, len(data), 12): - year, *date_parts = parse_int2(data[ii:ii+12]) + year, *date_parts = parse_int2(data[ii:ii + 12]) dts.append(datetime(year + 1900, *date_parts)) return dts @@ -149,7 +149,7 @@ def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray: real8 = sign | gds_exp_bits | gds_mant real8[zero] = 0 - real8[gds_exp < -14] = 0 # number is too small + real8[gds_exp < -14] = 0 # number is too small return real8 diff --git a/klamath/elements.py b/klamath/elements.py index 1fe3837..b29cfe2 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -28,7 +28,6 @@ T = TypeVar('T', bound='Text') X = TypeVar('X', bound='Box') - def read_properties(stream: IO[bytes]) -> Dict[int, bytes]: """ Read element properties. @@ -189,7 +188,7 @@ class Reference(Element): b += STRANS.write(stream, int(self.invert_y) << 15) if self.mag != 1: b += MAG.write(stream, self.mag) - if self.angle_deg !=0: + if self.angle_deg != 0: b += ANGLE.write(stream, self.angle_deg) if self.colrow is not None: @@ -481,7 +480,7 @@ class Text(Element): b += STRANS.write(stream, int(self.invert_y) << 15) if self.mag != 1: b += MAG.write(stream, self.mag) - if self.angle_deg !=0: + if self.angle_deg != 0: b += ANGLE.write(stream, self.angle_deg) b += XY.write(stream, self.xy) b += STRING.write(stream, self.string) diff --git a/klamath/library.py b/klamath/library.py index 50c8398..be9693b 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -55,7 +55,7 @@ class FileHeader: Returns: FileHeader object """ - version = HEADER.read(stream)[0] + _version = HEADER.read(stream)[0] # noqa: F841 # var is unused mod_time, acc_time = BGNLIB.read(stream) name = LIBNAME.skip_and_read(stream) uu, dbu = UNITS.skip_and_read(stream) @@ -223,7 +223,7 @@ def scan_hierarchy(stream: IO[bytes]) -> Dict[bytes, Dict[bytes, int]]: elif tag == ENDEL.tag: if ref_count is None: ref_count = 1 - assert(ref_name is not None) + assert ref_name is not None cur_structure[ref_name] += ref_count else: stream.seek(size, io.SEEK_CUR) diff --git a/klamath/records.py b/klamath/records.py index 3d3e838..96fbc7f 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -111,7 +111,7 @@ class PRESENTATION(BitArrayRecord): class SPACING(Int2Record): - tag = 0x1802 #Not sure about 02; Unused + tag = 0x1802 # Not sure about 02; Unused class STRING(ASCIIRecord): @@ -133,11 +133,11 @@ class ANGLE(Real8Record): class UINTEGER(Int2Record): - tag = 0x1d02 #Unused; not sure about 02 + tag = 0x1d02 # Unused; not sure about 02 class USTRING(ASCIIRecord): - tag = 0x1e06 #Unused; not sure about 06 + tag = 0x1e06 # Unused; not sure about 06 class REFLIBS(ASCIIRecord): @@ -183,11 +183,11 @@ class ATTRTABLE(ASCIIRecord): class STYPTABLE(ASCIIRecord): - tag = 0x2406 #UNUSED, not sure about 06 + tag = 0x2406 # UNUSED, not sure about 06 class STRTYPE(Int2Record): - tag = 0x2502 #UNUSED + tag = 0x2502 # UNUSED class ELFLAGS(BitArrayRecord): diff --git a/klamath/test_basic.py b/klamath/test_basic.py index 058c808..ae284b6 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -12,9 +12,9 @@ from .basic import KlamathError def test_parse_bitarray(): - assert(parse_bitarray(b'59') == 13625) - assert(parse_bitarray(b'\0\0') == 0) - assert(parse_bitarray(b'\xff\xff') == 65535) + assert parse_bitarray(b'59') == 13625 + assert parse_bitarray(b'\0\0') == 0 + assert parse_bitarray(b'\xff\xff') == 65535 # 4 bytes (too long) with pytest.raises(KlamathError): @@ -51,12 +51,12 @@ def test_parse_int4(): def test_decode_real8(): # zeroes - assert(decode_real8(numpy.array([0x0])) == 0) - assert(decode_real8(numpy.array([1<<63])) == 0) # negative - assert(decode_real8(numpy.array([0xff << 56])) == 0) # denormalized + assert decode_real8(numpy.array([0x0])) == 0 + assert decode_real8(numpy.array([1 << 63])) == 0 # negative + assert decode_real8(numpy.array([0xff << 56])) == 0 # denormalized - assert(decode_real8(numpy.array([0x4110 << 48])) == 1.0) - assert(decode_real8(numpy.array([0xC120 << 48])) == -2.0) + assert decode_real8(numpy.array([0x4110 << 48])) == 1.0 + assert decode_real8(numpy.array([0xC120 << 48])) == -2.0 def test_parse_real8(): @@ -73,36 +73,36 @@ def test_parse_real8(): def test_parse_ascii(): -# # empty data Now allowed! -# with pytest.raises(KlamathError): -# parse_ascii(b'') + # # empty data Now allowed! + # with pytest.raises(KlamathError): + # parse_ascii(b'') - assert(parse_ascii(b'12345') == b'12345') - assert(parse_ascii(b'12345\0') == b'12345') # strips trailing null byte + assert parse_ascii(b'12345') == b'12345' + assert parse_ascii(b'12345\0') == b'12345' # strips trailing null byte def test_pack_bitarray(): packed = pack_bitarray(321) - assert(len(packed) == 2) - assert(packed == struct.pack('>H', 321)) + assert len(packed) == 2 + assert packed == struct.pack('>H', 321) def test_pack_int2(): packed = pack_int2((3, 2, 1)) - assert(len(packed) == 3*2) - assert(packed == struct.pack('>3h', 3, 2, 1)) - assert(pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1)) + assert len(packed) == 3 * 2 + assert packed == struct.pack('>3h', 3, 2, 1) + assert pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1) def test_pack_int4(): packed = pack_int4((3, 2, 1)) - assert(len(packed) == 3*4) - assert(packed == struct.pack('>3l', 3, 2, 1)) - assert(pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1)) + assert len(packed) == 3 * 4 + assert packed == struct.pack('>3l', 3, 2, 1) + assert pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1) def test_encode_real8(): - assert(encode_real8(numpy.array([0.0])) == 0) + assert encode_real8(numpy.array([0.0])) == 0 arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12)) assert_array_equal(decode_real8(encode_real8(arr)), arr) @@ -110,10 +110,10 @@ def test_encode_real8(): def test_pack_real8(): reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12) packed = pack_real8(reals) - assert(len(packed) == len(reals) * 8) + assert len(packed) == len(reals) * 8 assert_array_equal(parse_real8(packed), reals) def test_pack_ascii(): - assert(pack_ascii(b'4321') == b'4321') - assert(pack_ascii(b'321') == b'321\0') + assert pack_ascii(b'4321') == b'4321' + assert pack_ascii(b'321') == b'321\0' From 9d14bf27c681bd1d88f45ef147c9138f6706eef6 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 14 Apr 2023 11:57:31 -0700 Subject: [PATCH 05/42] Update type annotations and some formatting --- README.md | 2 +- klamath/basic.py | 31 ++++++++++++----------- klamath/elements.py | 58 ++++++++++++++++++++++++------------------- klamath/library.py | 25 ++++++++++--------- klamath/record.py | 24 +++++++++--------- klamath/test_basic.py | 4 +-- 6 files changed, 77 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index e65fb24..65263e1 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The goal is to keep this library simple: ## Installation Requirements: -* python >= 3.7 (written and tested with 3.8) +* python >= 3.10 (written and tested with 3.11) * numpy diff --git a/klamath/basic.py b/klamath/basic.py index 3a319e9..5a15714 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -1,20 +1,21 @@ """ Functionality for encoding/decoding basic datatypes """ -from typing import Sequence, IO, List +from typing import Sequence, IO import struct from datetime import datetime -import numpy # type: ignore +import numpy +from numpy.typing import NDArray class KlamathError(Exception): pass -""" -Parse functions -""" +# +# Parse functions +# def parse_bitarray(data: bytes) -> int: if len(data) != 2: raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.') @@ -22,21 +23,21 @@ def parse_bitarray(data: bytes) -> int: return val -def parse_int2(data: bytes) -> numpy.ndarray: +def parse_int2(data: bytes) -> NDArray[numpy.int16]: data_len = len(data) if data_len == 0 or (data_len % 2) != 0: raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.') return numpy.frombuffer(data, dtype='>i2', count=data_len // 2) -def parse_int4(data: bytes) -> numpy.ndarray: +def parse_int4(data: bytes) -> NDArray[numpy.int32]: data_len = len(data) if data_len == 0 or (data_len % 4) != 0: raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.') return numpy.frombuffer(data, dtype='>i4', count=data_len // 4) -def decode_real8(nums: numpy.ndarray) -> numpy.ndarray: +def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]: """ Convert GDS REAL8 data to IEEE float64. """ nums = nums.astype(numpy.uint64) neg = nums & 0x8000_0000_0000_0000 @@ -46,7 +47,7 @@ def decode_real8(nums: numpy.ndarray) -> numpy.ndarray: return numpy.ldexp(mant, (4 * (exp - 64) - 56).astype(numpy.int64)) -def parse_real8(data: bytes) -> numpy.ndarray: +def parse_real8(data: bytes) -> NDArray[numpy.float64]: data_len = len(data) if data_len == 0 or (data_len % 8) != 0: raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.') @@ -62,7 +63,7 @@ def parse_ascii(data: bytes) -> bytes: return data -def parse_datetime(data: bytes) -> List[datetime]: +def parse_datetime(data: bytes) -> list[datetime]: """ Parse date/time data (12 byte blocks) """ if len(data) == 0 or len(data) % 12 != 0: raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.') @@ -73,9 +74,9 @@ def parse_datetime(data: bytes) -> List[datetime]: return dts -""" -Pack functions -""" +# +# Pack functions +# def pack_bitarray(data: int) -> bytes: if data > 65535 or data < 0: raise KlamathError(f'bitarray data out of range: {data}') @@ -96,7 +97,7 @@ def pack_int4(data: Sequence[int]) -> bytes: return arr.astype('>i4').tobytes() -def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray: +def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]: """ Convert from float64 to GDS REAL8 representation. """ # Split the ieee float bitfields ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64)) @@ -151,7 +152,7 @@ def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray: real8[zero] = 0 real8[gds_exp < -14] = 0 # number is too small - return real8 + return real8.astype(numpy.uint64, copy=False) def pack_real8(data: Sequence[float]) -> bytes: diff --git a/klamath/elements.py b/klamath/elements.py index b29cfe2..ba4098f 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -2,11 +2,12 @@ Functionality for reading/writing elements (geometry, text labels, structure references) and associated properties. """ -from typing import Dict, Tuple, Optional, IO, TypeVar, Type, Union +from typing import Optional, IO, TypeVar, Type, Union from abc import ABCMeta, abstractmethod from dataclasses import dataclass -import numpy # type: ignore +import numpy +from numpy.typing import NDArray from .basic import KlamathError from .record import Record @@ -28,7 +29,7 @@ T = TypeVar('T', bound='Text') X = TypeVar('X', bound='Box') -def read_properties(stream: IO[bytes]) -> Dict[int, bytes]: +def read_properties(stream: IO[bytes]) -> dict[int, bytes]: """ Read element properties. @@ -55,7 +56,7 @@ def read_properties(stream: IO[bytes]) -> Dict[int, bytes]: return properties -def write_properties(stream: IO[bytes], properties: Dict[int, bytes]) -> int: +def write_properties(stream: IO[bytes], properties: dict[int, bytes]) -> int: """ Write element properties. @@ -130,7 +131,7 @@ class Reference(Element): angle_deg: float """ Rotation (degrees counterclockwise) """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ (For SREF) Location in the parent structure corresponding to the instance's origin (0, 0). (For AREF) 3 locations: @@ -143,10 +144,10 @@ class Reference(Element): basis vectors to match it. """ - colrow: Optional[Union[Tuple[int, int], numpy.ndarray]] + colrow: tuple[int, int] | NDArray[numpy.int16] | None """ Number of columns and rows (AREF) or None (SREF) """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties associated with this reference. """ @classmethod @@ -173,8 +174,15 @@ class Reference(Element): size, tag = Record.read_header(stream) xy = XY.read_data(stream, size).reshape(-1, 2) properties = read_properties(stream) - return cls(struct_name=struct_name, xy=xy, properties=properties, colrow=colrow, - invert_y=invert_y, mag=mag, angle_deg=angle_deg) + return cls( + struct_name=struct_name, + xy=xy, + properties=properties, + colrow=colrow, + invert_y=invert_y, + mag=mag, + angle_deg=angle_deg, + ) def write(self, stream: IO[bytes]) -> int: b = 0 @@ -215,13 +223,13 @@ class Boundary(Element): """ __slots__ = ('layer', 'xy', 'properties') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, data_type) tuple """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Ordered vertices of the shape. First and last points should be identical. """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -252,7 +260,7 @@ class Path(Element): """ __slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, data_type) tuple """ path_type: int @@ -261,13 +269,13 @@ class Path(Element): width: int """ Path width """ - extension: Tuple[int, int] + extension: tuple[int, int] """ Extension when using path_type=4. Ignored otherwise. """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Path centerline coordinates """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -326,13 +334,13 @@ class Box(Element): """ __slots__ = ('layer', 'xy', 'properties') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, box_type) tuple """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Box coordinates (5 pairs) """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -360,13 +368,13 @@ class Node(Element): """ __slots__ = ('layer', 'xy', 'properties') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, node_type) tuple """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ 1-50 pairs of coordinates. """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -395,7 +403,7 @@ class Text(Element): __slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type', 'width', 'invert_y', 'mag', 'angle_deg', 'string') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, node_type) tuple """ presentation: int @@ -420,13 +428,13 @@ class Text(Element): angle_deg: float """ Rotation (ccw). Default 0. """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Position (1 pair only) """ string: bytes """ Text content """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod diff --git a/klamath/library.py b/klamath/library.py index be9693b..239609d 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -1,7 +1,7 @@ """ File-level read/write functionality. """ -from typing import List, Dict, Tuple, Optional, IO, TypeVar, Type, MutableMapping +from typing import IO, TypeVar, Type, MutableMapping import io from datetime import datetime from dataclasses import dataclass @@ -80,7 +80,7 @@ class FileHeader: return b -def scan_structs(stream: IO[bytes]) -> Dict[bytes, int]: +def scan_structs(stream: IO[bytes]) -> dict[bytes, int]: """ Scan through a GDS file, building a table of {b'structure_name': byte_offset}. @@ -107,7 +107,7 @@ def scan_structs(stream: IO[bytes]) -> Dict[bytes, int]: return positions -def try_read_struct(stream: IO[bytes]) -> Optional[Tuple[bytes, List[Element]]]: +def try_read_struct(stream: IO[bytes]) -> tuple[bytes, list[Element]] | None: """ Skip to the next structure and attempt to read it. @@ -125,12 +125,13 @@ def try_read_struct(stream: IO[bytes]) -> Optional[Tuple[bytes, List[Element]]]: return name, elements -def write_struct(stream: IO[bytes], - name: bytes, - elements: List[Element], - cre_time: datetime = datetime(1900, 1, 1), - mod_time: datetime = datetime(1900, 1, 1), - ) -> int: +def write_struct( + stream: IO[bytes], + name: bytes, + elements: list[Element], + cre_time: datetime = datetime(1900, 1, 1), + mod_time: datetime = datetime(1900, 1, 1), + ) -> int: """ Write a structure to the provided stream. @@ -150,7 +151,7 @@ def write_struct(stream: IO[bytes], return b -def read_elements(stream: IO[bytes]) -> List[Element]: +def read_elements(stream: IO[bytes]) -> list[Element]: """ Read elements from the stream until an ENDSTR record is encountered. The ENDSTR record is also @@ -162,7 +163,7 @@ def read_elements(stream: IO[bytes]) -> List[Element]: Returns: List of element objects. """ - data: List[Element] = [] + data: list[Element] = [] size, tag = Record.read_header(stream) while tag != ENDSTR.tag: if tag == BOUNDARY.tag: @@ -186,7 +187,7 @@ def read_elements(stream: IO[bytes]) -> List[Element]: return data -def scan_hierarchy(stream: IO[bytes]) -> Dict[bytes, Dict[bytes, int]]: +def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]: """ Scan through a GDS file, building a table of instance counts `{b'structure_name': {b'ref_name': count}}`. diff --git a/klamath/record.py b/klamath/record.py index 69769dd..c0b4a6c 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -1,14 +1,14 @@ """ Generic record-level read/write functionality. """ -from typing import Optional, Sequence, IO -from typing import TypeVar, List, Tuple, ClassVar, Type +from typing import Sequence, IO, TypeVar, ClassVar, Type import struct import io from datetime import datetime from abc import ABCMeta, abstractmethod -import numpy # type: ignore +import numpy +from numpy.typing import NDArray from .basic import KlamathError from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray @@ -27,7 +27,7 @@ def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int: return stream.write(header) -def read_record_header(stream: IO[bytes]) -> Tuple[int, int]: +def read_record_header(stream: IO[bytes]) -> tuple[int, int]: """ Read a record's header (size and tag). Args: @@ -58,7 +58,7 @@ R = TypeVar('R', bound='Record') class Record(metaclass=ABCMeta): tag: ClassVar[int] = -1 - expected_size: ClassVar[Optional[int]] = None + expected_size: ClassVar[int | None] = None @classmethod def check_size(cls, size: int): @@ -80,7 +80,7 @@ class Record(metaclass=ABCMeta): pass @staticmethod - def read_header(stream: IO[bytes]) -> Tuple[int, int]: + def read_header(stream: IO[bytes]) -> tuple[int, int]: return read_record_header(stream) @classmethod @@ -133,7 +133,7 @@ class Record(metaclass=ABCMeta): class NoDataRecord(Record): - expected_size: ClassVar[Optional[int]] = 0 + expected_size: ClassVar[int | None] = 0 @classmethod def read_data(cls, stream: IO[bytes], size: int) -> None: @@ -147,7 +147,7 @@ class NoDataRecord(Record): class BitArrayRecord(Record): - expected_size: ClassVar[Optional[int]] = 2 + expected_size: ClassVar[int | None] = 2 @classmethod def read_data(cls, stream: IO[bytes], size: int) -> int: @@ -160,7 +160,7 @@ class BitArrayRecord(Record): class Int2Record(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int16]: return parse_int2(read(stream, size)) @classmethod @@ -170,7 +170,7 @@ class Int2Record(Record): class Int4Record(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int32]: return parse_int4(read(stream, size)) @classmethod @@ -180,7 +180,7 @@ class Int4Record(Record): class Real8Record(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.float64]: return parse_real8(read(stream, size)) @classmethod @@ -200,7 +200,7 @@ class ASCIIRecord(Record): class DateTimeRecord(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> List[datetime]: + def read_data(cls, stream: IO[bytes], size: int) -> list[datetime]: return parse_datetime(read(stream, size)) @classmethod diff --git a/klamath/test_basic.py b/klamath/test_basic.py index ae284b6..f15fe8a 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -1,8 +1,8 @@ import struct import pytest # type: ignore -import numpy # type: ignore -from numpy.testing import assert_array_equal # type: ignore +import numpy +from numpy.testing import assert_array_equal from .basic import parse_bitarray, parse_int2, parse_int4, parse_real8, parse_ascii from .basic import pack_bitarray, pack_int2, pack_int4, pack_real8, pack_ascii From 61d04f89add12bf2f843d6f23a054946686b0451 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 19:48:21 -0700 Subject: [PATCH 06/42] add github link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 65263e1..fd4a4ea 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The goal is to keep this library simple: ### Links - [Source repository](https://mpxd.net/code/jan/klamath) - [PyPI](https://pypi.org/project/klamath) +- [Github mirror](https://github.com/anewusername/klamath) ## Installation From 97527a5948f83f44cb1592511c8213f4711b0402 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 6 Apr 2024 12:20:48 -0700 Subject: [PATCH 07/42] specify signature instead of using astype --- klamath/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/basic.py b/klamath/basic.py index 5a15714..2a837af 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -44,7 +44,7 @@ def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]: exp = (nums >> 56) & 0x7f mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64) mant[neg != 0] *= -1 - return numpy.ldexp(mant, (4 * (exp - 64) - 56).astype(numpy.int64)) + return numpy.ldexp(mant, 4 * (exp - 64) - 56, signature=(float, int, float)) def parse_real8(data: bytes) -> NDArray[numpy.float64]: From 46b33499356a8030e866c180a7f1efb8e5fa9794 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 6 Apr 2024 12:21:50 -0700 Subject: [PATCH 08/42] Allow reading files with invalid dates Notably, KLayout writes invalid dates when told to not write the date --- klamath/basic.py | 11 ++++++++++- klamath/test_basic.py | 14 +++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/klamath/basic.py b/klamath/basic.py index 2a837af..f31451c 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -3,12 +3,16 @@ Functionality for encoding/decoding basic datatypes """ from typing import Sequence, IO import struct +import logging from datetime import datetime import numpy from numpy.typing import NDArray +logger = logging.getLogger(__name__) + + class KlamathError(Exception): pass @@ -70,7 +74,12 @@ def parse_datetime(data: bytes) -> list[datetime]: dts = [] for ii in range(0, len(data), 12): year, *date_parts = parse_int2(data[ii:ii + 12]) - dts.append(datetime(year + 1900, *date_parts)) + try: + dt = datetime(year + 1900, *date_parts) + except ValueError as err: + dt = datetime(1900, 1, 1, 0, 0, 0) + logger.warning(f'Invalid date {[year] + date_parts}, setting {dt} instead') + dts.append(dt) return dts diff --git a/klamath/test_basic.py b/klamath/test_basic.py index f15fe8a..b511cc6 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -2,11 +2,12 @@ import struct import pytest # type: ignore import numpy +from datetime import datetime from numpy.testing import assert_array_equal from .basic import parse_bitarray, parse_int2, parse_int4, parse_real8, parse_ascii from .basic import pack_bitarray, pack_int2, pack_int4, pack_real8, pack_ascii -from .basic import decode_real8, encode_real8 +from .basic import decode_real8, encode_real8, parse_datetime from .basic import KlamathError @@ -117,3 +118,14 @@ def test_pack_real8(): def test_pack_ascii(): assert pack_ascii(b'4321') == b'4321' assert pack_ascii(b'321') == b'321\0' + + +def test_invalid_date(): + default = [datetime(1900, 1, 1, 0, 0, 0)] + assert parse_datetime(pack_int2((0, 0, 0, 0, 0, 0))) == default + assert parse_datetime(pack_int2((0, 1, 32, 0, 0, 0))) == default + assert parse_datetime(pack_int2((0, 2, 30, 0, 0, 0))) == default + assert parse_datetime(pack_int2((0, 1, 1, 24, 0, 0))) == default + assert parse_datetime(pack_int2((0, 1, 1, 25, 0, 0))) == default + assert parse_datetime(pack_int2((0, 1, 1, 0, 61, 0))) == default + assert parse_datetime(pack_int2((0, 1, 1, 0, 0, 61))) == default From 6a0019010fb3b47761a5761535418ab400c43402 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 11 Apr 2024 18:43:33 -0700 Subject: [PATCH 09/42] reduce date normalization log priority from 'warning' to 'info' --- klamath/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/basic.py b/klamath/basic.py index f31451c..9d7bdcb 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -78,7 +78,7 @@ def parse_datetime(data: bytes) -> list[datetime]: dt = datetime(year + 1900, *date_parts) except ValueError as err: dt = datetime(1900, 1, 1, 0, 0, 0) - logger.warning(f'Invalid date {[year] + date_parts}, setting {dt} instead') + logger.info(f'Invalid date {[year] + date_parts}, setting {dt} instead') dts.append(dt) return dts From e94b93d5afbc8638890c2eb8a2bcf0e01345e96d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 22:14:29 -0700 Subject: [PATCH 10/42] replace flake8 with ruff --- .flake8 | 30 ------------------------------ pyproject.toml | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 30 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 1bdbec5..0000000 --- a/.flake8 +++ /dev/null @@ -1,30 +0,0 @@ -[flake8] -ignore = - # E501 line too long - E501, - # W391 newlines at EOF - W391, - # E241 multiple spaces after comma - E241, - # E302 expected 2 newlines - E302, - # W503 line break before binary operator (to be deprecated) - W503, - # E265 block comment should start with '# ' - E265, - # E123 closing bracket does not match indentation of opening bracket's line - E123, - # E124 closing bracket does not match visual indentation - E124, - # E221 multiple spaces before operator - E221, - # E201 whitespace after '[' - E201, -# # E741 ambiguous variable name 'I' -# E741, - - -per-file-ignores = - # F401 import without use - */__init__.py: F401, - __init__.py: F401, diff --git a/pyproject.toml b/pyproject.toml index 11d7918..4044b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,3 +56,37 @@ dependencies = [ [tool.hatch.version] path = "klamath/__init__.py" + + +[tool.ruff] +exclude = [ + ".git", + "dist", + ] +line-length = 145 +indent-width = 4 +lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +lint.select = [ + "NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG", + "C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT", + "ARG", "PL", "R", "TRY", + "G010", "G101", "G201", "G202", + "Q002", "Q003", "Q004", + ] +lint.ignore = [ + #"ANN001", # No annotation + "ANN002", # *args + "ANN003", # **kwargs + "ANN401", # Any + "ANN101", # self: Self + "SIM108", # single-line if / else assignment + "RET504", # x=y+z; return x + "PIE790", # unnecessary pass + "ISC003", # non-implicit string concatenation + "C408", # dict(x=y) instead of {'x': y} + "PLR09", # Too many xxx + "PLR2004", # magic number + "PLC0414", # import x as x + "TRY003", # Long exception message + ] + From dc58159cdf67ffcc083b4b36245beae2140fbd1a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 22:15:26 -0700 Subject: [PATCH 11/42] use redundant imports for re-exported names --- klamath/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/klamath/__init__.py b/klamath/__init__.py index 4aef521..60ce57d 100644 --- a/klamath/__init__.py +++ b/klamath/__init__.py @@ -27,11 +27,13 @@ The goal is to keep this library simple: tools for working with hierarchical design data and supports multiple file formats. """ -from . import basic -from . import record -from . import records -from . import elements -from . import library +from . import ( + basic as basic, + record as record, + records as records, + elements as elements, + library as library, + ) __author__ = 'Jan Petykiewicz' __version__ = '1.3' From e7e42a2ef85f25e2ab6e0d18d5bffe46e88243a1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:04:12 -0700 Subject: [PATCH 12/42] Allow NDArray inputs to pack_* and avoid unnecesary copies --- klamath/basic.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/klamath/basic.py b/klamath/basic.py index 9d7bdcb..6d4d924 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -92,15 +92,15 @@ def pack_bitarray(data: int) -> bytes: return struct.pack('>H', data) -def pack_int2(data: Sequence[int]) -> bytes: - arr = numpy.array(data) +def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: + arr = numpy.array(data, copy=False) if (arr > 32767).any() or (arr < -32768).any(): raise KlamathError(f'int2 data out of range: {arr}') return arr.astype('>i2').tobytes() -def pack_int4(data: Sequence[int]) -> bytes: - arr = numpy.array(data) +def pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: + arr = numpy.array(data, copy=False) if (arr > 2147483647).any() or (arr < -2147483648).any(): raise KlamathError(f'int4 data out of range: {arr}') return arr.astype('>i4').tobytes() @@ -164,8 +164,8 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]: return real8.astype(numpy.uint64, copy=False) -def pack_real8(data: Sequence[float]) -> bytes: - return encode_real8(numpy.array(data)).astype('>u8').tobytes() +def pack_real8(data: NDArray[numpy.floating] | Sequence[float] | float) -> bytes: + return encode_real8(numpy.array(data, copy=False)).astype('>u8').tobytes() def pack_ascii(data: bytes) -> bytes: From 59c94f7c1798f9c5677a57522587b5f13d107954 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:15:11 -0700 Subject: [PATCH 13/42] improve type annotations --- klamath/record.py | 70 +++++++++++++++++++++---------------------- klamath/records.py | 10 +++---- klamath/test_basic.py | 24 +++++++-------- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/klamath/record.py b/klamath/record.py index c0b4a6c..674e55c 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -1,7 +1,8 @@ """ Generic record-level read/write functionality. """ -from typing import Sequence, IO, TypeVar, ClassVar, Type +from typing import IO, ClassVar, Self, Generic, TypeVar +from collections.abc import Sequence import struct import io from datetime import datetime @@ -17,6 +18,8 @@ from .basic import parse_ascii, pack_ascii, read _RECORD_HEADER_FMT = struct.Struct('>HH') +II = TypeVar('II') # Input type +OO = TypeVar('OO') # Output type def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int: @@ -53,30 +56,27 @@ def expect_record(stream: IO[bytes], tag: int) -> int: return data_size -R = TypeVar('R', bound='Record') - - -class Record(metaclass=ABCMeta): +class Record(Generic[II, OO], metaclass=ABCMeta): tag: ClassVar[int] = -1 expected_size: ClassVar[int | None] = None @classmethod - def check_size(cls, size: int): + def check_size(cls: type[Self], size: int) -> None: if cls.expected_size is not None and size != cls.expected_size: raise KlamathError(f'Expected size {cls.expected_size}, got {size}') @classmethod - def check_data(cls, data): + def check_data(cls: type[Self], data: II) -> None: pass @classmethod @abstractmethod - def read_data(cls, stream: IO[bytes], size: int): + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> OO: pass @classmethod @abstractmethod - def pack_data(cls, data) -> bytes: + def pack_data(cls: type[Self], data: II) -> bytes: pass @staticmethod @@ -84,11 +84,11 @@ class Record(metaclass=ABCMeta): return read_record_header(stream) @classmethod - def write_header(cls, stream: IO[bytes], data_size: int) -> int: + def write_header(cls: type[Self], stream: IO[bytes], data_size: int) -> int: return write_record_header(stream, data_size, cls.tag) @classmethod - def skip_past(cls, stream: IO[bytes]) -> bool: + def skip_past(cls: type[Self], stream: IO[bytes]) -> bool: """ Skip to the end of the next occurence of this record. @@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta): return True @classmethod - def skip_and_read(cls, stream: IO[bytes]): + def skip_and_read(cls: type[Self], stream: IO[bytes]) -> OO: size, tag = Record.read_header(stream) while tag != cls.tag: stream.seek(size, io.SEEK_CUR) @@ -119,90 +119,90 @@ class Record(metaclass=ABCMeta): return data @classmethod - def read(cls: Type[R], stream: IO[bytes]): + def read(cls: type[Self], stream: IO[bytes]) -> OO: size = expect_record(stream, cls.tag) data = cls.read_data(stream, size) return data @classmethod - def write(cls, stream: IO[bytes], data) -> int: + def write(cls: type[Self], stream: IO[bytes], data: II) -> int: data_bytes = cls.pack_data(data) b = cls.write_header(stream, len(data_bytes)) b += stream.write(data_bytes) return b -class NoDataRecord(Record): +class NoDataRecord(Record[None, None]): expected_size: ClassVar[int | None] = 0 @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> None: + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> None: stream.read(size) @classmethod - def pack_data(cls, data: None) -> bytes: + def pack_data(cls: type[Self], data: None) -> bytes: if data is not None: raise KlamathError('?? Packing {data} into NoDataRecord??') return b'' -class BitArrayRecord(Record): +class BitArrayRecord(Record[int, int]): expected_size: ClassVar[int | None] = 2 @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> int: + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> int: # noqa: ARG003 size unused return parse_bitarray(read(stream, 2)) @classmethod - def pack_data(cls, data: int) -> bytes: + def pack_data(cls: type[Self], data: int) -> bytes: return pack_bitarray(data) -class Int2Record(Record): +class Int2Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int16]]): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int16]: + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int16]: return parse_int2(read(stream, size)) @classmethod - def pack_data(cls, data: Sequence[int]) -> bytes: + def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: return pack_int2(data) -class Int4Record(Record): +class Int4Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int32]]): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int32]: + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int32]: return parse_int4(read(stream, size)) @classmethod - def pack_data(cls, data: Sequence[int]) -> bytes: + def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: return pack_int4(data) -class Real8Record(Record): +class Real8Record(Record[Sequence[float] | float, NDArray[numpy.float64]]): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.float64]: + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.float64]: return parse_real8(read(stream, size)) @classmethod - def pack_data(cls, data: Sequence[int]) -> bytes: + def pack_data(cls: type[Self], data: Sequence[float] | float) -> bytes: return pack_real8(data) -class ASCIIRecord(Record): +class ASCIIRecord(Record[bytes, bytes]): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> bytes: + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> bytes: return parse_ascii(read(stream, size)) @classmethod - def pack_data(cls, data: bytes) -> bytes: + def pack_data(cls: type[Self], data: bytes) -> bytes: return pack_ascii(data) -class DateTimeRecord(Record): +class DateTimeRecord(Record[Sequence[datetime], list[datetime]]): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> list[datetime]: + def read_data(cls: type[Self], stream: IO[bytes], size: int) -> list[datetime]: return parse_datetime(read(stream, size)) @classmethod - def pack_data(cls, data: Sequence[datetime]) -> bytes: + def pack_data(cls: type[Self], data: Sequence[datetime]) -> bytes: return pack_datetime(data) diff --git a/klamath/records.py b/klamath/records.py index 96fbc7f..9fe296d 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -144,7 +144,7 @@ class REFLIBS(ASCIIRecord): tag = 0x1f06 @classmethod - def check_size(cls, size: int): + def check_size(cls: type[Self], size: int) -> None: if size != 0 and size % 44 != 0: raise Exception(f'Expected size to be multiple of 44, got {size}') @@ -153,7 +153,7 @@ class FONTS(ASCIIRecord): tag = 0x2006 @classmethod - def check_size(cls, size: int): + def check_size(cls: type[Self], size: int) -> None: if size != 0 and size % 44 != 0: raise Exception(f'Expected size to be multiple of 44, got {size}') @@ -168,7 +168,7 @@ class GENERATIONS(Int2Record): expected_size = 2 @classmethod - def check_data(cls, data: Sequence[int]): + def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: if len(data) != 1: raise Exception(f'Expected exactly one integer, got {data}') @@ -177,7 +177,7 @@ class ATTRTABLE(ASCIIRecord): tag = 0x2306 @classmethod - def check_size(cls, size: int): + def check_size(cls: type[Self], size: int) -> None: if size > 44: raise Exception(f'Expected size <= 44, got {size}') @@ -266,7 +266,7 @@ class FORMAT(Int2Record): expected_size = 2 @classmethod - def check_data(cls, data: Sequence[int]): + def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: if len(data) != 1: raise Exception(f'Expected exactly one integer, got {data}') diff --git a/klamath/test_basic.py b/klamath/test_basic.py index b511cc6..4d686d9 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -12,7 +12,7 @@ from .basic import decode_real8, encode_real8, parse_datetime from .basic import KlamathError -def test_parse_bitarray(): +def test_parse_bitarray() -> None: assert parse_bitarray(b'59') == 13625 assert parse_bitarray(b'\0\0') == 0 assert parse_bitarray(b'\xff\xff') == 65535 @@ -26,7 +26,7 @@ def test_parse_bitarray(): parse_bitarray(b'') -def test_parse_int2(): +def test_parse_int2() -> None: assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0)) # odd length @@ -38,7 +38,7 @@ def test_parse_int2(): parse_int2(b'') -def test_parse_int4(): +def test_parse_int4() -> None: assert_array_equal(parse_int4(b'4321'), (875770417,)) # length % 4 != 0 @@ -50,7 +50,7 @@ def test_parse_int4(): parse_int4(b'') -def test_decode_real8(): +def test_decode_real8() -> None: # zeroes assert decode_real8(numpy.array([0x0])) == 0 assert decode_real8(numpy.array([1 << 63])) == 0 # negative @@ -60,7 +60,7 @@ def test_decode_real8(): assert decode_real8(numpy.array([0xC120 << 48])) == -2.0 -def test_parse_real8(): +def test_parse_real8() -> None: packed = struct.pack('>3Q', 0x0, 0x4110_0000_0000_0000, 0xC120_0000_0000_0000) assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0)) @@ -73,7 +73,7 @@ def test_parse_real8(): parse_real8(b'') -def test_parse_ascii(): +def test_parse_ascii() -> None: # # empty data Now allowed! # with pytest.raises(KlamathError): # parse_ascii(b'') @@ -82,40 +82,40 @@ def test_parse_ascii(): assert parse_ascii(b'12345\0') == b'12345' # strips trailing null byte -def test_pack_bitarray(): +def test_pack_bitarray() -> None: packed = pack_bitarray(321) assert len(packed) == 2 assert packed == struct.pack('>H', 321) -def test_pack_int2(): +def test_pack_int2() -> None: packed = pack_int2((3, 2, 1)) assert len(packed) == 3 * 2 assert packed == struct.pack('>3h', 3, 2, 1) assert pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1) -def test_pack_int4(): +def test_pack_int4() -> None: packed = pack_int4((3, 2, 1)) assert len(packed) == 3 * 4 assert packed == struct.pack('>3l', 3, 2, 1) assert pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1) -def test_encode_real8(): +def test_encode_real8() -> None: assert encode_real8(numpy.array([0.0])) == 0 arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12)) assert_array_equal(decode_real8(encode_real8(arr)), arr) -def test_pack_real8(): +def test_pack_real8() -> None: reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12) packed = pack_real8(reals) assert len(packed) == len(reals) * 8 assert_array_equal(parse_real8(packed), reals) -def test_pack_ascii(): +def test_pack_ascii() -> None: assert pack_ascii(b'4321') == b'4321' assert pack_ascii(b'321') == b'321\0' From 438cde513ebfa401f94b36482f5aa37687efe03e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:15:23 -0700 Subject: [PATCH 14/42] whitespace --- klamath/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/elements.py b/klamath/elements.py index ba4098f..fe509b8 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -51,7 +51,7 @@ def read_properties(stream: IO[bytes]) -> dict[int, bytes]: value = PROPVALUE.read(stream) if key in properties: raise KlamathError(f'Duplicate property key: {key!r}') - properties[key] = value + properties[key] = value size, tag = Record.read_header(stream) return properties From f12a1c642169fc982e916530cd32205581eca349 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:15:38 -0700 Subject: [PATCH 15/42] ignore a lint --- klamath/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/elements.py b/klamath/elements.py index fe509b8..a1a10d7 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -211,7 +211,7 @@ class Reference(Element): if self.colrow is not None: if self.xy.size != 6: raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}') - else: + else: # noqa: PLR5501 if self.xy.size != 2: raise KlamathError(f'Expected size-2 xy. Got {self.xy}') From 15af9078f0aa99f9e935d7ca7740ddcda54b6144 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:20:14 -0700 Subject: [PATCH 16/42] modernize type annotations and improve handling of int scalars --- klamath/basic.py | 3 ++- klamath/elements.py | 16 ++++++++-------- klamath/library.py | 8 ++++---- klamath/records.py | 9 ++++++--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/klamath/basic.py b/klamath/basic.py index 6d4d924..95f5551 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -1,7 +1,8 @@ """ Functionality for encoding/decoding basic datatypes """ -from typing import Sequence, IO +from typing import IO +from collections.abc import Sequence import struct import logging from datetime import datetime diff --git a/klamath/elements.py b/klamath/elements.py index a1a10d7..302921c 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -2,7 +2,7 @@ Functionality for reading/writing elements (geometry, text labels, structure references) and associated properties. """ -from typing import Optional, IO, TypeVar, Type, Union +from typing import IO, TypeVar from abc import ABCMeta, abstractmethod from dataclasses import dataclass @@ -78,7 +78,7 @@ class Element(metaclass=ABCMeta): """ @classmethod @abstractmethod - def read(cls: Type[E], stream: IO[bytes]) -> E: + def read(cls: type[E], stream: IO[bytes]) -> E: """ Read from a stream to construct this object. Consumes up to (and including) the ENDEL record. @@ -151,7 +151,7 @@ class Reference(Element): """ Properties associated with this reference. """ @classmethod - def read(cls: Type[R], stream: IO[bytes]) -> R: + def read(cls: type[R], stream: IO[bytes]) -> R: invert_y = False mag = 1 angle_deg = 0 @@ -233,7 +233,7 @@ class Boundary(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[B], stream: IO[bytes]) -> B: + def read(cls: type[B], stream: IO[bytes]) -> B: layer = LAYER.skip_and_read(stream)[0] dtype = DATATYPE.read(stream)[0] xy = XY.read(stream).reshape(-1, 2) @@ -279,7 +279,7 @@ class Path(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[P], stream: IO[bytes]) -> P: + def read(cls: type[P], stream: IO[bytes]) -> P: path_type = 0 width = 0 bgn_ext = 0 @@ -344,7 +344,7 @@ class Box(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[X], stream: IO[bytes]) -> X: + def read(cls: type[X], stream: IO[bytes]) -> X: layer = LAYER.skip_and_read(stream)[0] dtype = BOXTYPE.read(stream)[0] xy = XY.read(stream).reshape(-1, 2) @@ -378,7 +378,7 @@ class Node(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[N], stream: IO[bytes]) -> N: + def read(cls: type[N], stream: IO[bytes]) -> N: layer = LAYER.skip_and_read(stream)[0] dtype = NODETYPE.read(stream)[0] xy = XY.read(stream).reshape(-1, 2) @@ -438,7 +438,7 @@ class Text(Element): """ Properties for the element. """ @classmethod - def read(cls: Type[T], stream: IO[bytes]) -> T: + def read(cls: type[T], stream: IO[bytes]) -> T: path_type = 0 presentation = 0 invert_y = False diff --git a/klamath/library.py b/klamath/library.py index 239609d..4021677 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -1,7 +1,7 @@ """ File-level read/write functionality. """ -from typing import IO, TypeVar, Type, MutableMapping +from typing import IO, Self, TYPE_CHECKING import io from datetime import datetime from dataclasses import dataclass @@ -15,8 +15,8 @@ from .records import BGNSTR, STRNAME, ENDSTR, SNAME, COLROW, ENDEL from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF from .elements import Element, Reference, Text, Box, Boundary, Path, Node - -FH = TypeVar('FH', bound='FileHeader') +if TYPE_CHECKING: + from collections.abc import MutableMapping @dataclass @@ -45,7 +45,7 @@ class FileHeader: """ Last-accessed time """ @classmethod - def read(cls: Type[FH], stream: IO[bytes]) -> FH: + def read(cls: type[Self], stream: IO[bytes]) -> Self: """ Read and construct a header from the provided stream. diff --git a/klamath/records.py b/klamath/records.py index 9fe296d..31db805 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -1,7 +1,10 @@ """ Record type and tag definitions """ -from typing import Sequence +from typing import Self +from collections.abc import Sequence, Sized +import numpy +from numpy.typing import NDArray from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record from .record import ASCIIRecord, DateTimeRecord @@ -169,7 +172,7 @@ class GENERATIONS(Int2Record): @classmethod def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: - if len(data) != 1: + if not isinstance(data, Sized) or len(data) != 1: raise Exception(f'Expected exactly one integer, got {data}') @@ -267,7 +270,7 @@ class FORMAT(Int2Record): @classmethod def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: - if len(data) != 1: + if not isinstance(data, Sized) or len(data) != 1: raise Exception(f'Expected exactly one integer, got {data}') From 2ea9d32984591826033c81ada654666143aa7ed6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:20:25 -0700 Subject: [PATCH 17/42] use KlamathError everywhere --- klamath/records.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/klamath/records.py b/klamath/records.py index 31db805..0fbcb4d 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -6,6 +6,7 @@ from collections.abc import Sequence, Sized import numpy from numpy.typing import NDArray +from .basic import KlamathError from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record from .record import ASCIIRecord, DateTimeRecord @@ -149,7 +150,7 @@ class REFLIBS(ASCIIRecord): @classmethod def check_size(cls: type[Self], size: int) -> None: if size != 0 and size % 44 != 0: - raise Exception(f'Expected size to be multiple of 44, got {size}') + raise KlamathError(f'Expected size to be multiple of 44, got {size}') class FONTS(ASCIIRecord): @@ -158,7 +159,7 @@ class FONTS(ASCIIRecord): @classmethod def check_size(cls: type[Self], size: int) -> None: if size != 0 and size % 44 != 0: - raise Exception(f'Expected size to be multiple of 44, got {size}') + raise KlamathError(f'Expected size to be multiple of 44, got {size}') class PATHTYPE(Int2Record): @@ -173,7 +174,7 @@ class GENERATIONS(Int2Record): @classmethod def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: if not isinstance(data, Sized) or len(data) != 1: - raise Exception(f'Expected exactly one integer, got {data}') + raise KlamathError(f'Expected exactly one integer, got {data}') class ATTRTABLE(ASCIIRecord): @@ -182,7 +183,7 @@ class ATTRTABLE(ASCIIRecord): @classmethod def check_size(cls: type[Self], size: int) -> None: if size > 44: - raise Exception(f'Expected size <= 44, got {size}') + raise KlamathError(f'Expected size <= 44, got {size}') class STYPTABLE(ASCIIRecord): @@ -271,7 +272,7 @@ class FORMAT(Int2Record): @classmethod def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: if not isinstance(data, Sized) or len(data) != 1: - raise Exception(f'Expected exactly one integer, got {data}') + raise KlamathError(f'Expected exactly one integer, got {data}') class MASK(ASCIIRecord): From cae970e65c908f042e03d344b15cd4d8bdd37536 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:20:32 -0700 Subject: [PATCH 18/42] simplify comparisons --- klamath/library.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/klamath/library.py b/klamath/library.py index 4021677..f7b68bd 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -176,9 +176,7 @@ def read_elements(stream: IO[bytes]) -> list[Element]: data.append(Box.read(stream)) elif tag == TEXT.tag: data.append(Text.read(stream)) - elif tag == SREF.tag: - data.append(Reference.read(stream)) - elif tag == AREF.tag: + elif tag in (SREF.tag, AREF.tag): data.append(Reference.read(stream)) else: # don't care, skip From 8061d6cd378931e2dd5c42c4db22ea4ac80a135b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:20:47 -0700 Subject: [PATCH 19/42] note intentionally non-abstract method --- klamath/record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/record.py b/klamath/record.py index 674e55c..48bfe7c 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -65,7 +65,7 @@ class Record(Generic[II, OO], metaclass=ABCMeta): if cls.expected_size is not None and size != cls.expected_size: raise KlamathError(f'Expected size {cls.expected_size}, got {size}') - @classmethod + @classmethod # noqa: B027 Intentionally non-abstract def check_data(cls: type[Self], data: II) -> None: pass From 95976cd6373e3c1b0bf444dd2b883cb5d7dd9f43 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Jul 2024 23:20:57 -0700 Subject: [PATCH 20/42] increase min python version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4044b24..22b6fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ classifiers = [ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", ] -requires-python = ">=3.8" +requires-python = ">=3.11" include = [ "LICENSE.md" ] From 7d6cea1c4a1d890bea938045c886e35c113f5e3e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 29 Jul 2024 03:19:13 -0700 Subject: [PATCH 21/42] numpy.array(..., copy=False) -> numpy.asarray(...) for numpy 2.0 compatibility --- klamath/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klamath/basic.py b/klamath/basic.py index 95f5551..86c9d59 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -94,14 +94,14 @@ def pack_bitarray(data: int) -> bytes: def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: - arr = numpy.array(data, copy=False) + arr = numpy.asarray(data) if (arr > 32767).any() or (arr < -32768).any(): raise KlamathError(f'int2 data out of range: {arr}') return arr.astype('>i2').tobytes() def pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: - arr = numpy.array(data, copy=False) + arr = numpy.asarray(data) if (arr > 2147483647).any() or (arr < -2147483648).any(): raise KlamathError(f'int4 data out of range: {arr}') return arr.astype('>i4').tobytes() @@ -166,7 +166,7 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]: def pack_real8(data: NDArray[numpy.floating] | Sequence[float] | float) -> bytes: - return encode_real8(numpy.array(data, copy=False)).astype('>u8').tobytes() + return encode_real8(numpy.asarray(data)).astype('>u8').tobytes() def pack_ascii(data: bytes) -> bytes: From 65a33d2eca47258195b634d70fb0c1bac90da8ec Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 29 Jul 2024 03:19:56 -0700 Subject: [PATCH 22/42] allow numpy v2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 22b6fc5..c7d7a0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ include = [ ] dynamic = ["version"] dependencies = [ - "numpy~=1.21", + "numpy>=1.26", ] [tool.hatch.version] From 6ad3358665d61655da1c8bf595784085da98aad2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 29 Jul 2024 03:20:27 -0700 Subject: [PATCH 23/42] update reqs in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd4a4ea..3d838ec 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The goal is to keep this library simple: ## Installation Requirements: -* python >= 3.10 (written and tested with 3.11) +* python >= 3.11 * numpy From a50d53b5085c388457f0b33ef98f07689221d53a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 29 Jul 2024 20:53:45 -0700 Subject: [PATCH 24/42] bump version to v1.4 Main change is numpy 2.0 compatibility --- klamath/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/__init__.py b/klamath/__init__.py index 60ce57d..8de1f87 100644 --- a/klamath/__init__.py +++ b/klamath/__init__.py @@ -36,5 +36,5 @@ from . import ( ) __author__ = 'Jan Petykiewicz' -__version__ = '1.3' +__version__ = '1.4' From 4ffb87d361a70e7fdc1e4c9034017969823c5d4d Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 20 Dec 2024 19:52:16 -0800 Subject: [PATCH 25/42] Include repr() of data in error msg --- klamath/record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/record.py b/klamath/record.py index 48bfe7c..f5fce86 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -142,7 +142,7 @@ class NoDataRecord(Record[None, None]): @classmethod def pack_data(cls: type[Self], data: None) -> bytes: if data is not None: - raise KlamathError('?? Packing {data} into NoDataRecord??') + raise KlamathError('?? Packing {data!r} into NoDataRecord??') return b'' From 428e396b9b240703344f8755e5bf32fefb985835 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 21 Apr 2025 19:39:08 -0700 Subject: [PATCH 26/42] Loosen type requirements / guarantees for properties We will still return a dict, but only require a mapping when writing --- klamath/elements.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/klamath/elements.py b/klamath/elements.py index 302921c..8c77162 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -3,6 +3,7 @@ Functionality for reading/writing elements (geometry, text labels, structure references) and associated properties. """ from typing import IO, TypeVar +from collections.abc import Mapping from abc import ABCMeta, abstractmethod from dataclasses import dataclass @@ -56,7 +57,7 @@ def read_properties(stream: IO[bytes]) -> dict[int, bytes]: return properties -def write_properties(stream: IO[bytes], properties: dict[int, bytes]) -> int: +def write_properties(stream: IO[bytes], properties: Mapping[int, bytes]) -> int: """ Write element properties. @@ -147,7 +148,7 @@ class Reference(Element): colrow: tuple[int, int] | NDArray[numpy.int16] | None """ Number of columns and rows (AREF) or None (SREF) """ - properties: dict[int, bytes] + properties: Mapping[int, bytes] """ Properties associated with this reference. """ @classmethod @@ -229,7 +230,7 @@ class Boundary(Element): xy: NDArray[numpy.int32] """ Ordered vertices of the shape. First and last points should be identical. """ - properties: dict[int, bytes] + properties: Mapping[int, bytes] """ Properties for the element. """ @classmethod @@ -275,7 +276,7 @@ class Path(Element): xy: NDArray[numpy.int32] """ Path centerline coordinates """ - properties: dict[int, bytes] + properties: Mapping[int, bytes] """ Properties for the element. """ @classmethod @@ -340,7 +341,7 @@ class Box(Element): xy: NDArray[numpy.int32] """ Box coordinates (5 pairs) """ - properties: dict[int, bytes] + properties: Mapping[int, bytes] """ Properties for the element. """ @classmethod @@ -374,7 +375,7 @@ class Node(Element): xy: NDArray[numpy.int32] """ 1-50 pairs of coordinates. """ - properties: dict[int, bytes] + properties: Mapping[int, bytes] """ Properties for the element. """ @classmethod @@ -434,7 +435,7 @@ class Text(Element): string: bytes """ Text content """ - properties: dict[int, bytes] + properties: Mapping[int, bytes] """ Properties for the element. """ @classmethod From ae9c2d7b5fabd3b6e400d3cfd8638f4375bfc34d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 21 Apr 2025 19:39:16 -0700 Subject: [PATCH 27/42] add type annotation --- klamath/test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/test_basic.py b/klamath/test_basic.py index 4d686d9..287f7a9 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -120,7 +120,7 @@ def test_pack_ascii() -> None: assert pack_ascii(b'321') == b'321\0' -def test_invalid_date(): +def test_invalid_date() -> None: default = [datetime(1900, 1, 1, 0, 0, 0)] assert parse_datetime(pack_int2((0, 0, 0, 0, 0, 0))) == default assert parse_datetime(pack_int2((0, 1, 32, 0, 0, 0))) == default From 0ac4c3b275c0f43e49a5be8f16145544b4518ab1 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 12 Oct 2025 23:36:52 -0700 Subject: [PATCH 28/42] fix path extensions not getting written correctly --- klamath/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/elements.py b/klamath/elements.py index 8c77162..ba72d47 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -316,7 +316,7 @@ class Path(Element): if self.width != 0: b += WIDTH.write(stream, self.width) - if self.path_type < 4: + if self.path_type == 4: bgn_ext, end_ext = self.extension if bgn_ext != 0: b += BGNEXTN.write(stream, bgn_ext) From 74b9672b4a4cb38b5da2ef0aa86aa934c8b55baa Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 12 Oct 2025 23:37:35 -0700 Subject: [PATCH 29/42] bump version to v1.5 --- klamath/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/__init__.py b/klamath/__init__.py index 8de1f87..5a315b4 100644 --- a/klamath/__init__.py +++ b/klamath/__init__.py @@ -36,5 +36,5 @@ from . import ( ) __author__ = 'Jan Petykiewicz' -__version__ = '1.4' +__version__ = '1.5' From 14f795e592c6c2249bde5b931418f705191247e3 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 30 Dec 2025 02:52:40 -0800 Subject: [PATCH 30/42] add file formats tag --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c7d7a0e..afb1d81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ classifiers = [ "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", + "Topic :: File Formats", ] requires-python = ">=3.11" include = [ @@ -54,6 +55,11 @@ dependencies = [ "numpy>=1.26", ] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + [tool.hatch.version] path = "klamath/__init__.py" From 5cd20f975159bbd7ceca261df2b1e74e28ba6ff5 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 8 Mar 2026 20:06:30 -0700 Subject: [PATCH 31/42] [Path] fix handling of non-int extensions (cast) --- klamath/elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klamath/elements.py b/klamath/elements.py index ba72d47..9a12b7d 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -319,9 +319,9 @@ class Path(Element): if self.path_type == 4: bgn_ext, end_ext = self.extension if bgn_ext != 0: - b += BGNEXTN.write(stream, bgn_ext) + b += BGNEXTN.write(stream, int(bgn_ext)) if end_ext != 0: - b += ENDEXTN.write(stream, end_ext) + b += ENDEXTN.write(stream, int(end_ext)) b += XY.write(stream, self.xy) b += write_properties(stream, self.properties) b += ENDEL.write(stream, None) From 2512c83749ee265fa3e71801af1cc831fc6cb95b Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 8 Mar 2026 20:06:45 -0700 Subject: [PATCH 32/42] misc linter cleanup --- klamath/basic.py | 2 +- pyproject.toml | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/klamath/basic.py b/klamath/basic.py index 86c9d59..c1d1d15 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -77,7 +77,7 @@ def parse_datetime(data: bytes) -> list[datetime]: year, *date_parts = parse_int2(data[ii:ii + 12]) try: dt = datetime(year + 1900, *date_parts) - except ValueError as err: + except ValueError: dt = datetime(1900, 1, 1, 0, 0, 0) logger.info(f'Invalid date {[year] + date_parts}, setting {dt} instead') dts.append(dt) diff --git a/pyproject.toml b/pyproject.toml index afb1d81..5ca097d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - [project] name = "klamath" description = "GDSII format reader/writer" @@ -84,7 +80,6 @@ lint.ignore = [ "ANN002", # *args "ANN003", # **kwargs "ANN401", # Any - "ANN101", # self: Self "SIM108", # single-line if / else assignment "RET504", # x=y+z; return x "PIE790", # unnecessary pass From cf4d7f70d4804a4b4959f2f6acb742ad5dcdb3f5 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 8 Mar 2026 21:41:24 -0700 Subject: [PATCH 33/42] [records] fix record id for HARDFENCE --- klamath/records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/records.py b/klamath/records.py index 0fbcb4d..30c8cc3 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -306,7 +306,7 @@ class SOFTFENCE(NoDataRecord): class HARDFENCE(NoDataRecord): - tag = 0x3f00 + tag = 0x3e00 class SOFTWIRE(NoDataRecord): From 49a7ba2209b6c54b606acaf8b6614bb661193556 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 00:15:52 -0700 Subject: [PATCH 34/42] [tests] add a bunch of tests --- klamath/test_elements.py | 110 ++++++++++++++++++++++++++++++++ klamath/test_library.py | 78 +++++++++++++++++++++++ klamath/test_record.py | 134 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+) create mode 100644 klamath/test_elements.py create mode 100644 klamath/test_library.py create mode 100644 klamath/test_record.py diff --git a/klamath/test_elements.py b/klamath/test_elements.py new file mode 100644 index 0000000..c1993c2 --- /dev/null +++ b/klamath/test_elements.py @@ -0,0 +1,110 @@ +import io +import numpy +from numpy.testing import assert_array_equal +from klamath.elements import Boundary, Path, Text, Reference, Box, Node + +def test_boundary_roundtrip() -> None: + xy = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], dtype=numpy.int32) + b = Boundary(layer=(4, 5), xy=xy, properties={1: b'prop1'}) + + stream = io.BytesIO() + b.write(stream) + stream.seek(0) + + b2 = Boundary.read(stream) + assert b2.layer == b.layer + assert_array_equal(b2.xy, b.xy) + assert b2.properties == b.properties + +def test_path_roundtrip() -> None: + xy = numpy.array([[0, 0], [100, 0], [100, 100]], dtype=numpy.int32) + p = Path(layer=(10, 20), xy=xy, properties={2: b'pathprop'}, + path_type=4, width=50, extension=(10, 20)) + + stream = io.BytesIO() + p.write(stream) + stream.seek(0) + + p2 = Path.read(stream) + assert p2.layer == p.layer + assert_array_equal(p2.xy, p.xy) + assert p2.properties == p.properties + assert p2.path_type == p.path_type + assert p2.width == p.width + assert p2.extension == p.extension + +def test_text_roundtrip() -> None: + xy = numpy.array([[50, 50]], dtype=numpy.int32) + t = Text(layer=(1, 1), xy=xy, string=b"HELLO WORLD", properties={}, + presentation=5, path_type=0, width=0, invert_y=True, + mag=2.5, angle_deg=45.0) + + stream = io.BytesIO() + t.write(stream) + stream.seek(0) + + t2 = Text.read(stream) + assert t2.layer == t.layer + assert_array_equal(t2.xy, t.xy) + assert t2.string == t.string + assert t2.presentation == t.presentation + assert t2.invert_y == t.invert_y + assert t2.mag == t.mag + assert t2.angle_deg == t.angle_deg + +def test_reference_sref_roundtrip() -> None: + xy = numpy.array([[100, 200]], dtype=numpy.int32) + r = Reference(struct_name=b"MY_CELL", xy=xy, colrow=None, + properties={5: b'sref'}, invert_y=False, mag=1.0, angle_deg=90.0) + + stream = io.BytesIO() + r.write(stream) + stream.seek(0) + + r2 = Reference.read(stream) + assert r2.struct_name == r.struct_name + assert_array_equal(r2.xy, r.xy) + assert r2.colrow is None + assert r2.properties == r.properties + assert r2.angle_deg == r.angle_deg + +def test_reference_aref_roundtrip() -> None: + xy = numpy.array([[0, 0], [1000, 0], [0, 500]], dtype=numpy.int32) + colrow = (5, 2) + r = Reference(struct_name=b"ARRAY_CELL", xy=xy, colrow=colrow, + properties={}, invert_y=False, mag=1.0, angle_deg=0.0) + + stream = io.BytesIO() + r.write(stream) + stream.seek(0) + + r2 = Reference.read(stream) + assert r2.struct_name == r.struct_name + assert_array_equal(r2.xy, r.xy) + assert r2.colrow is not None + assert list(r2.colrow) == list(colrow) + assert r2.properties == r.properties + +def test_box_roundtrip() -> None: + xy = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], dtype=numpy.int32) + b = Box(layer=(30, 40), xy=xy, properties={}) + + stream = io.BytesIO() + b.write(stream) + stream.seek(0) + + b2 = Box.read(stream) + assert b2.layer == b.layer + assert_array_equal(b2.xy, b.xy) + +def test_node_roundtrip() -> None: + xy = numpy.array([[0, 0], [10, 10]], dtype=numpy.int32) + n = Node(layer=(50, 60), xy=xy, properties={}) + + stream = io.BytesIO() + n.write(stream) + stream.seek(0) + + n2 = Node.read(stream) + assert n2.layer == n.layer + assert_array_equal(n2.xy, n.xy) diff --git a/klamath/test_library.py b/klamath/test_library.py new file mode 100644 index 0000000..27ee41b --- /dev/null +++ b/klamath/test_library.py @@ -0,0 +1,78 @@ +import io +import numpy +from datetime import datetime +from klamath.library import FileHeader, write_struct, try_read_struct, scan_structs, scan_hierarchy, read_elements +from klamath.elements import Boundary +from klamath.records import ENDLIB + +def test_file_header_roundtrip() -> None: + h = FileHeader(name=b"MY_LIB", user_units_per_db_unit=0.001, meters_per_db_unit=1e-9, + mod_time=datetime(2023, 1, 1, 0, 0, 0), acc_time=datetime(2023, 1, 1, 0, 0, 0)) + + stream = io.BytesIO() + h.write(stream) + stream.seek(0) + + h2 = FileHeader.read(stream) + assert h2.name == h.name + assert h2.user_units_per_db_unit == h.user_units_per_db_unit + assert h2.meters_per_db_unit == h.meters_per_db_unit + assert h2.mod_time == h.mod_time + +def test_write_read_struct() -> None: + xy = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], dtype=numpy.int32) + b = Boundary(layer=(1, 1), xy=xy, properties={}) + + stream = io.BytesIO() + # Need a header for some operations, but write_struct works standalone + write_struct(stream, name=b"CELL_A", elements=[b]) + ENDLIB.write(stream, None) + stream.seek(0) + + res = try_read_struct(stream) + assert res is not None + name, elements = res + assert name == b"CELL_A" + assert len(elements) == 1 + assert isinstance(elements[0], Boundary) + +def test_scan_structs() -> None: + stream = io.BytesIO() + write_struct(stream, name=b"CELL_A", elements=[]) + write_struct(stream, name=b"CELL_B", elements=[]) + ENDLIB.write(stream, None) + stream.seek(0) + + positions = scan_structs(stream) + assert b"CELL_A" in positions + assert b"CELL_B" in positions + + # Verify we can seek and read + stream.seek(positions[b"CELL_B"]) + elements = read_elements(stream) + assert len(elements) == 0 + +def test_scan_hierarchy() -> None: + from klamath.elements import Reference + + stream = io.BytesIO() + # Struct A has 2 refs to Struct B + ref_b1 = Reference(struct_name=b"B", xy=numpy.array([[0, 0]], dtype=numpy.int32), colrow=None, properties={}, + invert_y=False, mag=1.0, angle_deg=0.0) + ref_b2 = Reference(struct_name=b"B", xy=numpy.array([[10, 10]], dtype=numpy.int32), colrow=None, properties={}, + invert_y=False, mag=1.0, angle_deg=0.0) + write_struct(stream, name=b"A", elements=[ref_b1, ref_b2]) + + # Struct B has a 3x2 AREF of Struct C + ref_c = Reference(struct_name=b"C", xy=numpy.array([[0, 0], [10, 0], [0, 10]], dtype=numpy.int32), + colrow=(3, 2), properties={}, invert_y=False, mag=1.0, angle_deg=0.0) + write_struct(stream, name=b"B", elements=[ref_c]) + + write_struct(stream, name=b"C", elements=[]) + ENDLIB.write(stream, None) + stream.seek(0) + + hierarchy = scan_hierarchy(stream) + assert hierarchy[b"A"] == {b"B": 2} + assert hierarchy[b"B"] == {b"C": 6} + assert hierarchy[b"C"] == {} diff --git a/klamath/test_record.py b/klamath/test_record.py new file mode 100644 index 0000000..99a5a1f --- /dev/null +++ b/klamath/test_record.py @@ -0,0 +1,134 @@ +import io +import pytest +import struct +from datetime import datetime +from klamath.basic import KlamathError +from klamath.record import ( + write_record_header, read_record_header, expect_record, + BitArrayRecord, Int2Record, ASCIIRecord, DateTimeRecord, NoDataRecord +) +from klamath.records import ENDLIB, HEADER + +def test_write_read_record_header() -> None: + stream = io.BytesIO() + tag = 0x1234 + data_size = 8 + + write_record_header(stream, data_size, tag) + stream.seek(0) + + read_size, read_tag = read_record_header(stream) + assert read_size == data_size + assert read_tag == tag + assert stream.tell() == 4 + +def test_write_record_header_too_big() -> None: + stream = io.BytesIO() + with pytest.raises(KlamathError, match="Record size is too big"): + write_record_header(stream, 0x10000, 0x1234) + +def test_read_record_header_errors() -> None: + # Too small + stream = io.BytesIO(struct.pack('>HH', 2, 0x1234)) + with pytest.raises(KlamathError, match="Record size is too small"): + read_record_header(stream) + + # Odd size + stream = io.BytesIO(struct.pack('>HH', 5, 0x1234)) + with pytest.raises(KlamathError, match="Record size is odd"): + read_record_header(stream) + +def test_expect_record() -> None: + stream = io.BytesIO() + write_record_header(stream, 4, 0x1111) + stream.seek(0) + + # Correct tag + size = expect_record(stream, 0x1111) + assert size == 4 + + # Incorrect tag + stream.seek(0) + with pytest.raises(KlamathError, match="Unexpected record"): + expect_record(stream, 0x2222) + +def test_bitarray_record() -> None: + class TestBit(BitArrayRecord): + tag = 0x9999 + + stream = io.BytesIO() + TestBit.write(stream, 0x8000) + stream.seek(0) + + val = TestBit.read(stream) + assert val == 0x8000 + +def test_int2_record() -> None: + class TestInt2(Int2Record): + tag = 0x8888 + + stream = io.BytesIO() + TestInt2.write(stream, [1, -2, 3]) + stream.seek(0) + + val = TestInt2.read(stream) + assert list(val) == [1, -2, 3] + +def test_ascii_record() -> None: + class TestASCII(ASCIIRecord): + tag = 0x7777 + + stream = io.BytesIO() + TestASCII.write(stream, b"HELLO") + stream.seek(0) + + val = TestASCII.read(stream) + assert val == b"HELLO" + +def test_datetime_record() -> None: + class TestDT(DateTimeRecord): + tag = 0x6666 + + now = datetime(2023, 10, 27, 12, 30, 45) + stream = io.BytesIO() + TestDT.write(stream, [now, now]) + stream.seek(0) + + vals = TestDT.read(stream) + assert vals == [now, now] + +def test_nodata_record() -> None: + class TestNoData(NoDataRecord): + tag = 0x5555 + + stream = io.BytesIO() + TestNoData.write(stream, None) + stream.seek(0) + + # Verify header: 4 bytes total (size=4, tag=0x5555), data_size=0 + header = stream.read(4) + assert header == struct.pack('>HH', 4, 0x5555) + + stream.seek(0) + assert TestNoData.read(stream) is None + +def test_record_skip_past() -> None: + stream = io.BytesIO() + HEADER.write(stream, 600) + ENDLIB.write(stream, None) + + stream.seek(0) + # Skip past HEADER + found = HEADER.skip_past(stream) + assert found is True + assert stream.tell() == 6 # 4 header + 2 data + + # Try to skip past something that doesn't exist before ENDLIB + class NONEXISTENT(NoDataRecord): + tag = 0xFFFF + + stream.seek(0) + found = NONEXISTENT.skip_past(stream) + assert found is False + # Should be at the end of ENDLIB record header/tag read + assert stream.tell() == 10 # 6 (HEADER) + 4 (ENDLIB) From 92492b01d454dabe0471023a0f7e266798cccea4 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 00:16:13 -0700 Subject: [PATCH 35/42] [README] clarify intended use of scan_structs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d838ec..4351171 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ header = klamath.library.FileHeader.read(stream) struct_positions = klamath.library.scan_structs(stream) stream.seek(struct_positions[b'my_struct']) -elements_A = klamath.library.try_read_struct(stream) +elements_A = klamath.library.read_elements(stream) stream.close() From 9cfcad9acd55dcd684c88414293eda5d675adecb Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 01:00:30 -0700 Subject: [PATCH 36/42] [records] fix some expected sizes --- klamath/records.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/klamath/records.py b/klamath/records.py index 30c8cc3..d5ac938 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -18,7 +18,7 @@ class HEADER(Int2Record): class BGNLIB(DateTimeRecord): tag = 0x0102 - expected_size = 6 * 2 + expected_size = 2 * 6 * 2 class LIBNAME(ASCIIRecord): @@ -37,7 +37,7 @@ class ENDLIB(NoDataRecord): class BGNSTR(DateTimeRecord): tag = 0x0502 - expected_size = 6 * 2 + expected_size = 2 * 6 * 2 class STRNAME(ASCIIRecord): @@ -222,7 +222,6 @@ class PROPATTR(Int2Record): class PROPVALUE(ASCIIRecord): tag = 0x2c06 - expected_size = 2 class BOX(NoDataRecord): From 966e350f663a4b6f5ba73e70778407adc432f623 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 01:00:48 -0700 Subject: [PATCH 37/42] [records] early return if we got an int --- klamath/records.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/klamath/records.py b/klamath/records.py index d5ac938..b05bb04 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -173,6 +173,8 @@ class GENERATIONS(Int2Record): @classmethod def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: + if isinstance(data, (int, numpy.integer)): + return if not isinstance(data, Sized) or len(data) != 1: raise KlamathError(f'Expected exactly one integer, got {data}') @@ -270,6 +272,8 @@ class FORMAT(Int2Record): @classmethod def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None: + if isinstance(data, (int, numpy.integer)): + return if not isinstance(data, Sized) or len(data) != 1: raise KlamathError(f'Expected exactly one integer, got {data}') From a67f9036b2aa05fdb766c63db47fb60cd6069497 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 01:01:07 -0700 Subject: [PATCH 38/42] [tests] add some more tests --- klamath/test_elements.py | 46 ++++++++++++++++ klamath/test_library.py | 24 +++++++++ klamath/test_records.py | 110 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 klamath/test_records.py diff --git a/klamath/test_elements.py b/klamath/test_elements.py index c1993c2..ae3df18 100644 --- a/klamath/test_elements.py +++ b/klamath/test_elements.py @@ -108,3 +108,49 @@ def test_node_roundtrip() -> None: n2 = Node.read(stream) assert n2.layer == n.layer assert_array_equal(n2.xy, n.xy) + +def test_reference_check() -> None: + import pytest + from klamath.basic import KlamathError + # SREF with too many points + xy = numpy.array([[0, 0], [10, 10]], dtype=numpy.int32) + r = Reference(struct_name=b"CELL", xy=xy, colrow=None, properties={}, invert_y=False, mag=1.0, angle_deg=0.0) + with pytest.raises(KlamathError, match="Expected size-2 xy"): + r.check() + + # AREF with too few points + xy = numpy.array([[0, 0]], dtype=numpy.int32) + r = Reference(struct_name=b"CELL", xy=xy, colrow=(2, 2), properties={}, invert_y=False, mag=1.0, angle_deg=0.0) + with pytest.raises(KlamathError, match="colrow is not None, so expected size-6 xy"): + r.check() + +def test_read_properties_duplicate() -> None: + import pytest + from klamath.basic import KlamathError + from klamath.records import PROPATTR, PROPVALUE, ENDEL + stream = io.BytesIO() + PROPATTR.write(stream, 1) + PROPVALUE.write(stream, b"val1") + PROPATTR.write(stream, 1) # DUPLICATE + PROPVALUE.write(stream, b"val2") + ENDEL.write(stream, None) + stream.seek(0) + + from klamath.elements import read_properties + with pytest.raises(KlamathError, match="Duplicate property key"): + read_properties(stream) + +def test_element_read_unexpected_tag() -> None: + import pytest + from klamath.basic import KlamathError + from klamath.records import SREF, SNAME, HEADER, XY, ENDEL + stream = io.BytesIO() + SREF.write(stream, None) + SNAME.write(stream, b"CELL") + HEADER.write(stream, 123) # UNEXPECTED TAG for Reference.read + XY.write(stream, [0, 0]) + ENDEL.write(stream, None) + stream.seek(0) + + with pytest.raises(KlamathError, match="Unexpected tag"): + Reference.read(stream) diff --git a/klamath/test_library.py b/klamath/test_library.py index 27ee41b..a27c65c 100644 --- a/klamath/test_library.py +++ b/klamath/test_library.py @@ -76,3 +76,27 @@ def test_scan_hierarchy() -> None: assert hierarchy[b"A"] == {b"B": 2} assert hierarchy[b"B"] == {b"C": 6} assert hierarchy[b"C"] == {} + +def test_scan_structs_duplicate() -> None: + import pytest + from klamath.basic import KlamathError + stream = io.BytesIO() + write_struct(stream, name=b"CELL_A", elements=[]) + write_struct(stream, name=b"CELL_A", elements=[]) + ENDLIB.write(stream, None) + stream.seek(0) + + with pytest.raises(KlamathError, match="Duplicate structure name"): + scan_structs(stream) + +def test_scan_hierarchy_duplicate() -> None: + import pytest + from klamath.basic import KlamathError + stream = io.BytesIO() + write_struct(stream, name=b"CELL_A", elements=[]) + write_struct(stream, name=b"CELL_A", elements=[]) + ENDLIB.write(stream, None) + stream.seek(0) + + with pytest.raises(KlamathError, match="Duplicate structure name"): + scan_hierarchy(stream) diff --git a/klamath/test_records.py b/klamath/test_records.py new file mode 100644 index 0000000..fd07b44 --- /dev/null +++ b/klamath/test_records.py @@ -0,0 +1,110 @@ +import io +import pytest +import numpy +from datetime import datetime +from klamath.basic import KlamathError +from klamath import records + +def test_record_tags() -> None: + assert records.HEADER.tag == 0x0002 + assert records.BGNLIB.tag == 0x0102 + assert records.LIBNAME.tag == 0x0206 + assert records.UNITS.tag == 0x0305 + assert records.ENDLIB.tag == 0x0400 + assert records.BGNSTR.tag == 0x0502 + assert records.STRNAME.tag == 0x0606 + assert records.ENDSTR.tag == 0x0700 + assert records.BOUNDARY.tag == 0x0800 + assert records.PATH.tag == 0x0900 + assert records.SREF.tag == 0x0a00 + assert records.AREF.tag == 0x0b00 + assert records.TEXT.tag == 0x0c00 + assert records.LAYER.tag == 0x0d02 + assert records.DATATYPE.tag == 0x0e02 + assert records.WIDTH.tag == 0x0f03 + assert records.XY.tag == 0x1003 + assert records.ENDEL.tag == 0x1100 + assert records.SNAME.tag == 0x1206 + assert records.COLROW.tag == 0x1302 + assert records.NODE.tag == 0x1500 + assert records.TEXTTYPE.tag == 0x1602 + assert records.PRESENTATION.tag == 0x1701 + assert records.STRING.tag == 0x1906 + assert records.STRANS.tag == 0x1a01 + assert records.MAG.tag == 0x1b05 + assert records.ANGLE.tag == 0x1c05 + assert records.REFLIBS.tag == 0x1f06 + assert records.FONTS.tag == 0x2006 + assert records.PATHTYPE.tag == 0x2102 + assert records.GENERATIONS.tag == 0x2202 + assert records.ATTRTABLE.tag == 0x2306 + assert records.ELFLAGS.tag == 0x2601 + assert records.NODETYPE.tag == 0x2a02 + assert records.PROPATTR.tag == 0x2b02 + assert records.PROPVALUE.tag == 0x2c06 + assert records.BOX.tag == 0x2d00 + assert records.BOXTYPE.tag == 0x2e02 + assert records.PLEX.tag == 0x2f03 + assert records.BGNEXTN.tag == 0x3003 + assert records.ENDEXTN.tag == 0x3103 + assert records.TAPENUM.tag == 0x3202 + assert records.TAPECODE.tag == 0x3302 + assert records.FORMAT.tag == 0x3602 + assert records.MASK.tag == 0x3706 + assert records.ENDMASKS.tag == 0x3800 + assert records.LIBDIRSIZE.tag == 0x3902 + assert records.SRFNAME.tag == 0x3a06 + assert records.LIBSECUR.tag == 0x3b02 + +def test_header_validation() -> None: + # Correct size + records.HEADER.check_size(2) + + # Incorrect size + with pytest.raises(KlamathError, match="Expected size 2, got 4"): + records.HEADER.check_size(4) + +def test_bgnlib_validation() -> None: + now = datetime(2023, 10, 27, 12, 30, 45) + # Correct size (2 datetimes = 24 bytes) + records.BGNLIB.check_size(24) + + # Incorrect size + with pytest.raises(KlamathError, match="Expected size 24, got 12"): + records.BGNLIB.check_size(12) + +def test_reflibs_fonts_validation() -> None: + # REFLIBS must be multiple of 44 + records.REFLIBS.check_size(44) + records.REFLIBS.check_size(88) + records.REFLIBS.check_size(0) + + with pytest.raises(KlamathError, match="Expected size to be multiple of 44"): + records.REFLIBS.check_size(10) + +def test_generations_format_validation() -> None: + # GENERATIONS expects exactly one integer + records.GENERATIONS.check_data(3) + records.GENERATIONS.check_data([1]) + + with pytest.raises(KlamathError, match="Expected exactly one integer"): + records.GENERATIONS.check_data([1, 2]) + +def test_attrtable_validation() -> None: + # ATTRTABLE size <= 44 + records.ATTRTABLE.check_size(44) + records.ATTRTABLE.check_size(10) + + with pytest.raises(KlamathError, match="Expected size <= 44"): + records.ATTRTABLE.check_size(45) + +def test_nodata_records() -> None: + stream = io.BytesIO() + records.ENDLIB.write(stream, None) + stream.seek(0) + assert records.ENDLIB.read(stream) is None + + stream = io.BytesIO() + records.BOUNDARY.write(stream, None) + stream.seek(0) + assert records.BOUNDARY.read(stream) is None From 9e6f5a336578558a2aa9c0414826d4bba594fbaa Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 01:02:06 -0700 Subject: [PATCH 39/42] [decode_real8] avoid passing signature to keep mypy happy --- klamath/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klamath/basic.py b/klamath/basic.py index c1d1d15..f545090 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -49,7 +49,7 @@ def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]: exp = (nums >> 56) & 0x7f mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64) mant[neg != 0] *= -1 - return numpy.ldexp(mant, 4 * (exp - 64) - 56, signature=(float, int, float)) + return numpy.ldexp(mant, 4 * (exp.astype(numpy.int64) - 64) - 56) def parse_real8(data: bytes) -> NDArray[numpy.float64]: From 286f9e194923f8fb98859b114d17b64206d84459 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 01:41:57 -0700 Subject: [PATCH 40/42] [read_properties] skip unrecognized tags --- klamath/elements.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klamath/elements.py b/klamath/elements.py index 9a12b7d..c0d5567 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -2,6 +2,7 @@ Functionality for reading/writing elements (geometry, text labels, structure references) and associated properties. """ +import io from typing import IO, TypeVar from collections.abc import Mapping from abc import ABCMeta, abstractmethod @@ -53,6 +54,8 @@ def read_properties(stream: IO[bytes]) -> dict[int, bytes]: if key in properties: raise KlamathError(f'Duplicate property key: {key!r}') properties[key] = value + else: + stream.seek(size, io.SEEK_CUR) size, tag = Record.read_header(stream) return properties From e72b792f6f15e377e57d8e8652f7554dfc57d141 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 01:45:06 -0700 Subject: [PATCH 41/42] [scan_hierarchy] make sure counts get reset at ref boundaries --- klamath/library.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/klamath/library.py b/klamath/library.py index f7b68bd..8b3ab20 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -220,10 +220,15 @@ def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]: colrow = COLROW.read_data(stream, size) ref_count = colrow[0] * colrow[1] elif tag == ENDEL.tag: - if ref_count is None: - ref_count = 1 - assert ref_name is not None - cur_structure[ref_name] += ref_count + if ref_name is not None: + if ref_count is None: + ref_count = 1 + cur_structure[ref_name] += ref_count + ref_name = None + ref_count = None + elif tag in (SREF.tag, AREF.tag): + ref_name = None + ref_count = None else: stream.seek(size, io.SEEK_CUR) size, tag = Record.read_header(stream) From 56a99c8e58a6cd0cb729454c618fc164815b7427 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 9 Mar 2026 01:52:03 -0700 Subject: [PATCH 42/42] [encode_real8] improve handling of tiny floats --- klamath/basic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/klamath/basic.py b/klamath/basic.py index f545090..e880393 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -149,7 +149,7 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]: gds_exp = exp16 + 64 neg_biased = (gds_exp < 0) - gds_mant[neg_biased] >>= (gds_exp[neg_biased] * 4).astype(numpy.uint16) + gds_mant[neg_biased] >>= (-gds_exp[neg_biased] * 4).astype(numpy.uint16) gds_exp[neg_biased] = 0 too_big = (gds_exp > 0x7f) & ~(zero | subnorm) @@ -160,7 +160,6 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]: real8 = sign | gds_exp_bits | gds_mant real8[zero] = 0 - real8[gds_exp < -14] = 0 # number is too small return real8.astype(numpy.uint64, copy=False)