Compare commits

...

27 Commits
v1.2 ... master

Author SHA1 Message Date
ae9c2d7b5f add type annotation 2025-04-21 19:39:23 -07:00
428e396b9b Loosen type requirements / guarantees for properties
We will still return a dict, but only require a mapping when writing
2025-04-21 19:39:23 -07:00
jan
4ffb87d361 Include repr() of data in error msg 2024-12-20 19:52:16 -08:00
a50d53b508 bump version to v1.4
Main change is numpy 2.0 compatibility
2024-07-29 20:53:45 -07:00
6ad3358665 update reqs in readme 2024-07-29 18:35:55 -07:00
65a33d2eca allow numpy v2 2024-07-29 18:35:55 -07:00
7d6cea1c4a numpy.array(..., copy=False) -> numpy.asarray(...)
for numpy 2.0 compatibility
2024-07-29 18:35:55 -07:00
95976cd637 increase min python version 2024-07-29 18:35:55 -07:00
8061d6cd37 note intentionally non-abstract method 2024-07-29 18:35:55 -07:00
cae970e65c simplify comparisons 2024-07-29 18:35:55 -07:00
2ea9d32984 use KlamathError everywhere 2024-07-29 18:35:55 -07:00
15af9078f0 modernize type annotations and improve handling of int scalars 2024-07-29 18:35:55 -07:00
f12a1c6421 ignore a lint 2024-07-29 18:35:55 -07:00
438cde513e whitespace 2024-07-29 18:35:55 -07:00
59c94f7c17 improve type annotations 2024-07-29 18:35:55 -07:00
e7e42a2ef8 Allow NDArray inputs to pack_* and avoid unnecesary copies 2024-07-29 18:35:55 -07:00
dc58159cdf use redundant imports for re-exported names 2024-07-29 18:35:55 -07:00
e94b93d5af replace flake8 with ruff 2024-07-29 18:35:55 -07:00
jan
6a0019010f reduce date normalization log priority from 'warning' to 'info' 2024-04-11 18:43:33 -07:00
jan
46b3349935 Allow reading files with invalid dates
Notably, KLayout writes invalid dates when told to not write the date
2024-04-06 12:22:05 -07:00
jan
97527a5948 specify signature instead of using astype 2024-04-06 12:22:05 -07:00
61d04f89ad add github link 2024-03-30 19:48:21 -07:00
jan
9d14bf27c6 Update type annotations and some formatting 2023-04-14 11:57:31 -07:00
4d362f8e09 do some linting with flake8 2023-01-26 12:40:54 -08:00
0bbc8f8e08 Loosen constraints from BinaryIO to IO[bytes] 2023-01-26 12:32:19 -08:00
e90e44bd15 bump version to v1.3 2022-08-18 23:07:23 -07:00
38fc306644 Move to hatch-based build 2022-08-18 23:06:41 -07:00
14 changed files with 336 additions and 271 deletions

3
.gitignore vendored
View File

@ -1,5 +1,5 @@
*.pyc *.pyc
__pycache__ __pycache__/
*.idea *.idea
@ -7,6 +7,7 @@ build/
dist/ dist/
*.egg-info/ *.egg-info/
.mypy_cache/ .mypy_cache/
.pytest_cache/
*.swp *.swp
*.swo *.swo

View File

@ -44,12 +44,13 @@ The goal is to keep this library simple:
### Links ### Links
- [Source repository](https://mpxd.net/code/jan/klamath) - [Source repository](https://mpxd.net/code/jan/klamath)
- [PyPI](https://pypi.org/project/klamath) - [PyPI](https://pypi.org/project/klamath)
- [Github mirror](https://github.com/anewusername/klamath)
## Installation ## Installation
Requirements: Requirements:
* python >= 3.7 (written and tested with 3.8) * python >= 3.11
* numpy * numpy

1
klamath/LICENSE.md Symbolic link
View File

@ -0,0 +1 @@
../LICENSE.md

1
klamath/README.md Symbolic link
View File

@ -0,0 +1 @@
../README.md

View File

@ -1,4 +0,0 @@
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
__version__ = '''
1.2
'''.strip()

View File

@ -27,12 +27,14 @@ The goal is to keep this library simple:
tools for working with hierarchical design data and supports multiple tools for working with hierarchical design data and supports multiple
file formats. file formats.
""" """
from . import basic from . import (
from . import record basic as basic,
from . import records record as record,
from . import elements records as records,
from . import library elements as elements,
library as library,
from .VERSION import __version__ )
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
__version__ = '1.4'

View File

@ -1,20 +1,26 @@
""" """
Functionality for encoding/decoding basic datatypes Functionality for encoding/decoding basic datatypes
""" """
from typing import Sequence, BinaryIO, List from typing import IO
from collections.abc import Sequence
import struct import struct
import logging
from datetime import datetime from datetime import datetime
import numpy # type: ignore import numpy
from numpy.typing import NDArray
logger = logging.getLogger(__name__)
class KlamathError(Exception): class KlamathError(Exception):
pass pass
""" #
Parse functions # Parse functions
""" #
def parse_bitarray(data: bytes) -> int: def parse_bitarray(data: bytes) -> int:
if len(data) != 2: if len(data) != 2:
raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.') raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.')
@ -22,31 +28,31 @@ def parse_bitarray(data: bytes) -> int:
return val return val
def parse_int2(data: bytes) -> numpy.ndarray: def parse_int2(data: bytes) -> NDArray[numpy.int16]:
data_len = len(data) data_len = len(data)
if data_len == 0 or (data_len % 2) != 0: if data_len == 0 or (data_len % 2) != 0:
raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.') raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.')
return numpy.frombuffer(data, dtype='>i2', count=data_len // 2) 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) data_len = len(data)
if data_len == 0 or (data_len % 4) != 0: if data_len == 0 or (data_len % 4) != 0:
raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.') raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.')
return numpy.frombuffer(data, dtype='>i4', count=data_len // 4) 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. """ """ Convert GDS REAL8 data to IEEE float64. """
nums = nums.astype(numpy.uint64) nums = nums.astype(numpy.uint64)
neg = nums & 0x8000_0000_0000_0000 neg = nums & 0x8000_0000_0000_0000
exp = (nums >> 56) & 0x7f exp = (nums >> 56) & 0x7f
mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64) mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
mant[neg != 0] *= -1 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) -> numpy.ndarray: def parse_real8(data: bytes) -> NDArray[numpy.float64]:
data_len = len(data) data_len = len(data)
if data_len == 0 or (data_len % 8) != 0: if data_len == 0 or (data_len % 8) != 0:
raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.') raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.')
@ -62,41 +68,46 @@ def parse_ascii(data: bytes) -> bytes:
return data return data
def parse_datetime(data: bytes) -> List[datetime]: def parse_datetime(data: bytes) -> list[datetime]:
""" Parse date/time data (12 byte blocks) """ """ Parse date/time data (12 byte blocks) """
if len(data) == 0 or len(data) % 12 != 0: if len(data) == 0 or len(data) % 12 != 0:
raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.') raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.')
dts = [] dts = []
for ii in range(0, len(data), 12): 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)) try:
dt = datetime(year + 1900, *date_parts)
except ValueError as err:
dt = datetime(1900, 1, 1, 0, 0, 0)
logger.info(f'Invalid date {[year] + date_parts}, setting {dt} instead')
dts.append(dt)
return dts return dts
""" #
Pack functions # Pack functions
""" #
def pack_bitarray(data: int) -> bytes: def pack_bitarray(data: int) -> bytes:
if data > 65535 or data < 0: if data > 65535 or data < 0:
raise KlamathError(f'bitarray data out of range: {data}') raise KlamathError(f'bitarray data out of range: {data}')
return struct.pack('>H', data) return struct.pack('>H', data)
def pack_int2(data: Sequence[int]) -> bytes: def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
arr = numpy.array(data) arr = numpy.asarray(data)
if (arr > 32767).any() or (arr < -32768).any(): if (arr > 32767).any() or (arr < -32768).any():
raise KlamathError(f'int2 data out of range: {arr}') raise KlamathError(f'int2 data out of range: {arr}')
return arr.astype('>i2').tobytes() return arr.astype('>i2').tobytes()
def pack_int4(data: Sequence[int]) -> bytes: def pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
arr = numpy.array(data) arr = numpy.asarray(data)
if (arr > 2147483647).any() or (arr < -2147483648).any(): if (arr > 2147483647).any() or (arr < -2147483648).any():
raise KlamathError(f'int4 data out of range: {arr}') raise KlamathError(f'int4 data out of range: {arr}')
return arr.astype('>i4').tobytes() 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. """ """ Convert from float64 to GDS REAL8 representation. """
# Split the ieee float bitfields # Split the ieee float bitfields
ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64)) ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
@ -149,13 +160,13 @@ def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
real8 = sign | gds_exp_bits | gds_mant real8 = sign | gds_exp_bits | gds_mant
real8[zero] = 0 real8[zero] = 0
real8[gds_exp < -14] = 0 # number is too small 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: def pack_real8(data: NDArray[numpy.floating] | Sequence[float] | float) -> bytes:
return encode_real8(numpy.array(data)).astype('>u8').tobytes() return encode_real8(numpy.asarray(data)).astype('>u8').tobytes()
def pack_ascii(data: bytes) -> bytes: def pack_ascii(data: bytes) -> bytes:
@ -172,7 +183,7 @@ def pack_datetime(data: Sequence[datetime]) -> bytes:
return pack_int2(parts) return pack_int2(parts)
def read(stream: BinaryIO, size: int) -> bytes: def read(stream: IO[bytes], size: int) -> bytes:
""" Read and check for failure """ """ Read and check for failure """
data = stream.read(size) data = stream.read(size)
if len(data) != size: if len(data) != size:

View File

@ -2,11 +2,13 @@
Functionality for reading/writing elements (geometry, text labels, Functionality for reading/writing elements (geometry, text labels,
structure references) and associated properties. structure references) and associated properties.
""" """
from typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type, Union from typing import IO, TypeVar
from collections.abc import Mapping
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
import numpy # type: ignore import numpy
from numpy.typing import NDArray
from .basic import KlamathError from .basic import KlamathError
from .record import Record from .record import Record
@ -28,8 +30,7 @@ T = TypeVar('T', bound='Text')
X = TypeVar('X', bound='Box') X = TypeVar('X', bound='Box')
def read_properties(stream: IO[bytes]) -> dict[int, bytes]:
def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
""" """
Read element properties. Read element properties.
@ -51,12 +52,12 @@ def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
value = PROPVALUE.read(stream) value = PROPVALUE.read(stream)
if key in properties: if key in properties:
raise KlamathError(f'Duplicate property key: {key!r}') raise KlamathError(f'Duplicate property key: {key!r}')
properties[key] = value properties[key] = value
size, tag = Record.read_header(stream) size, tag = Record.read_header(stream)
return properties return properties
def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int: def write_properties(stream: IO[bytes], properties: Mapping[int, bytes]) -> int:
""" """
Write element properties. Write element properties.
@ -78,7 +79,7 @@ class Element(metaclass=ABCMeta):
""" """
@classmethod @classmethod
@abstractmethod @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. Read from a stream to construct this object.
Consumes up to (and including) the ENDEL record. Consumes up to (and including) the ENDEL record.
@ -92,7 +93,7 @@ class Element(metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def write(self, stream: BinaryIO) -> int: def write(self, stream: IO[bytes]) -> int:
""" """
Write this element to a stream. Write this element to a stream.
Finishes with an ENDEL record. Finishes with an ENDEL record.
@ -131,7 +132,7 @@ class Reference(Element):
angle_deg: float angle_deg: float
""" Rotation (degrees counterclockwise) """ """ 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 SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
(For AREF) 3 locations: (For AREF) 3 locations:
@ -144,14 +145,14 @@ class Reference(Element):
basis vectors to match it. 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) """ """ Number of columns and rows (AREF) or None (SREF) """
properties: Dict[int, bytes] properties: Mapping[int, bytes]
""" Properties associated with this reference. """ """ Properties associated with this reference. """
@classmethod @classmethod
def read(cls: Type[R], stream: BinaryIO) -> R: def read(cls: type[R], stream: IO[bytes]) -> R:
invert_y = False invert_y = False
mag = 1 mag = 1
angle_deg = 0 angle_deg = 0
@ -174,10 +175,17 @@ class Reference(Element):
size, tag = Record.read_header(stream) size, tag = Record.read_header(stream)
xy = XY.read_data(stream, size).reshape(-1, 2) xy = XY.read_data(stream, size).reshape(-1, 2)
properties = read_properties(stream) properties = read_properties(stream)
return cls(struct_name=struct_name, xy=xy, properties=properties, colrow=colrow, return cls(
invert_y=invert_y, mag=mag, angle_deg=angle_deg) 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 b = 0
if self.colrow is None: if self.colrow is None:
b += SREF.write(stream, None) b += SREF.write(stream, None)
@ -189,7 +197,7 @@ class Reference(Element):
b += STRANS.write(stream, int(self.invert_y) << 15) b += STRANS.write(stream, int(self.invert_y) << 15)
if self.mag != 1: if self.mag != 1:
b += MAG.write(stream, self.mag) b += MAG.write(stream, self.mag)
if self.angle_deg !=0: if self.angle_deg != 0:
b += ANGLE.write(stream, self.angle_deg) b += ANGLE.write(stream, self.angle_deg)
if self.colrow is not None: if self.colrow is not None:
@ -204,7 +212,7 @@ class Reference(Element):
if self.colrow is not None: if self.colrow is not None:
if self.xy.size != 6: if self.xy.size != 6:
raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}') raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
else: else: # noqa: PLR5501
if self.xy.size != 2: if self.xy.size != 2:
raise KlamathError(f'Expected size-2 xy. Got {self.xy}') raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
@ -216,24 +224,24 @@ class Boundary(Element):
""" """
__slots__ = ('layer', 'xy', 'properties') __slots__ = ('layer', 'xy', 'properties')
layer: Tuple[int, int] layer: tuple[int, int]
""" (layer, data_type) tuple """ """ (layer, data_type) tuple """
xy: numpy.ndarray xy: NDArray[numpy.int32]
""" Ordered vertices of the shape. First and last points should be identical. """ """ Ordered vertices of the shape. First and last points should be identical. """
properties: Dict[int, bytes] properties: Mapping[int, bytes]
""" Properties for the element. """ """ Properties for the element. """
@classmethod @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] layer = LAYER.skip_and_read(stream)[0]
dtype = DATATYPE.read(stream)[0] dtype = DATATYPE.read(stream)[0]
xy = XY.read(stream).reshape(-1, 2) xy = XY.read(stream).reshape(-1, 2)
properties = read_properties(stream) properties = read_properties(stream)
return cls(layer=(layer, dtype), xy=xy, properties=properties) 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 = BOUNDARY.write(stream, None)
b += LAYER.write(stream, self.layer[0]) b += LAYER.write(stream, self.layer[0])
b += DATATYPE.write(stream, self.layer[1]) b += DATATYPE.write(stream, self.layer[1])
@ -253,7 +261,7 @@ class Path(Element):
""" """
__slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension') __slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension')
layer: Tuple[int, int] layer: tuple[int, int]
""" (layer, data_type) tuple """ """ (layer, data_type) tuple """
path_type: int path_type: int
@ -262,17 +270,17 @@ class Path(Element):
width: int width: int
""" Path width """ """ Path width """
extension: Tuple[int, int] extension: tuple[int, int]
""" Extension when using path_type=4. Ignored otherwise. """ """ Extension when using path_type=4. Ignored otherwise. """
xy: numpy.ndarray xy: NDArray[numpy.int32]
""" Path centerline coordinates """ """ Path centerline coordinates """
properties: Dict[int, bytes] properties: Mapping[int, bytes]
""" Properties for the element. """ """ Properties for the element. """
@classmethod @classmethod
def read(cls: Type[P], stream: BinaryIO) -> P: def read(cls: type[P], stream: IO[bytes]) -> P:
path_type = 0 path_type = 0
width = 0 width = 0
bgn_ext = 0 bgn_ext = 0
@ -299,7 +307,7 @@ class Path(Element):
properties=properties, extension=(bgn_ext, end_ext), properties=properties, extension=(bgn_ext, end_ext),
path_type=path_type, width=width) 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 = PATH.write(stream, None)
b += LAYER.write(stream, self.layer[0]) b += LAYER.write(stream, self.layer[0])
b += DATATYPE.write(stream, self.layer[1]) b += DATATYPE.write(stream, self.layer[1])
@ -327,24 +335,24 @@ class Box(Element):
""" """
__slots__ = ('layer', 'xy', 'properties') __slots__ = ('layer', 'xy', 'properties')
layer: Tuple[int, int] layer: tuple[int, int]
""" (layer, box_type) tuple """ """ (layer, box_type) tuple """
xy: numpy.ndarray xy: NDArray[numpy.int32]
""" Box coordinates (5 pairs) """ """ Box coordinates (5 pairs) """
properties: Dict[int, bytes] properties: Mapping[int, bytes]
""" Properties for the element. """ """ Properties for the element. """
@classmethod @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] layer = LAYER.skip_and_read(stream)[0]
dtype = BOXTYPE.read(stream)[0] dtype = BOXTYPE.read(stream)[0]
xy = XY.read(stream).reshape(-1, 2) xy = XY.read(stream).reshape(-1, 2)
properties = read_properties(stream) properties = read_properties(stream)
return cls(layer=(layer, dtype), xy=xy, properties=properties) 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 = BOX.write(stream, None)
b += LAYER.write(stream, self.layer[0]) b += LAYER.write(stream, self.layer[0])
b += BOXTYPE.write(stream, self.layer[1]) b += BOXTYPE.write(stream, self.layer[1])
@ -361,24 +369,24 @@ class Node(Element):
""" """
__slots__ = ('layer', 'xy', 'properties') __slots__ = ('layer', 'xy', 'properties')
layer: Tuple[int, int] layer: tuple[int, int]
""" (layer, node_type) tuple """ """ (layer, node_type) tuple """
xy: numpy.ndarray xy: NDArray[numpy.int32]
""" 1-50 pairs of coordinates. """ """ 1-50 pairs of coordinates. """
properties: Dict[int, bytes] properties: Mapping[int, bytes]
""" Properties for the element. """ """ Properties for the element. """
@classmethod @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] layer = LAYER.skip_and_read(stream)[0]
dtype = NODETYPE.read(stream)[0] dtype = NODETYPE.read(stream)[0]
xy = XY.read(stream).reshape(-1, 2) xy = XY.read(stream).reshape(-1, 2)
properties = read_properties(stream) properties = read_properties(stream)
return cls(layer=(layer, dtype), xy=xy, properties=properties) 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 = NODE.write(stream, None)
b += LAYER.write(stream, self.layer[0]) b += LAYER.write(stream, self.layer[0])
b += NODETYPE.write(stream, self.layer[1]) b += NODETYPE.write(stream, self.layer[1])
@ -396,7 +404,7 @@ class Text(Element):
__slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type', __slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type',
'width', 'invert_y', 'mag', 'angle_deg', 'string') 'width', 'invert_y', 'mag', 'angle_deg', 'string')
layer: Tuple[int, int] layer: tuple[int, int]
""" (layer, node_type) tuple """ """ (layer, node_type) tuple """
presentation: int presentation: int
@ -421,17 +429,17 @@ class Text(Element):
angle_deg: float angle_deg: float
""" Rotation (ccw). Default 0. """ """ Rotation (ccw). Default 0. """
xy: numpy.ndarray xy: NDArray[numpy.int32]
""" Position (1 pair only) """ """ Position (1 pair only) """
string: bytes string: bytes
""" Text content """ """ Text content """
properties: Dict[int, bytes] properties: Mapping[int, bytes]
""" Properties for the element. """ """ Properties for the element. """
@classmethod @classmethod
def read(cls: Type[T], stream: BinaryIO) -> T: def read(cls: type[T], stream: IO[bytes]) -> T:
path_type = 0 path_type = 0
presentation = 0 presentation = 0
invert_y = False invert_y = False
@ -467,7 +475,7 @@ class Text(Element):
string=string, presentation=presentation, path_type=path_type, string=string, presentation=presentation, path_type=path_type,
width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg) 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 = TEXT.write(stream, None)
b += LAYER.write(stream, self.layer[0]) b += LAYER.write(stream, self.layer[0])
b += TEXTTYPE.write(stream, self.layer[1]) b += TEXTTYPE.write(stream, self.layer[1])
@ -481,7 +489,7 @@ class Text(Element):
b += STRANS.write(stream, int(self.invert_y) << 15) b += STRANS.write(stream, int(self.invert_y) << 15)
if self.mag != 1: if self.mag != 1:
b += MAG.write(stream, self.mag) b += MAG.write(stream, self.mag)
if self.angle_deg !=0: if self.angle_deg != 0:
b += ANGLE.write(stream, self.angle_deg) b += ANGLE.write(stream, self.angle_deg)
b += XY.write(stream, self.xy) b += XY.write(stream, self.xy)
b += STRING.write(stream, self.string) b += STRING.write(stream, self.string)

View File

@ -1,7 +1,7 @@
""" """
File-level read/write functionality. File-level read/write functionality.
""" """
from typing import List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type, MutableMapping from typing import IO, Self, TYPE_CHECKING
import io import io
from datetime import datetime from datetime import datetime
from dataclasses import dataclass 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 .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
from .elements import Element, Reference, Text, Box, Boundary, Path, Node from .elements import Element, Reference, Text, Box, Boundary, Path, Node
if TYPE_CHECKING:
FH = TypeVar('FH', bound='FileHeader') from collections.abc import MutableMapping
@dataclass @dataclass
@ -45,7 +45,7 @@ class FileHeader:
""" Last-accessed time """ """ Last-accessed time """
@classmethod @classmethod
def read(cls: Type[FH], stream: BinaryIO) -> FH: def read(cls: type[Self], stream: IO[bytes]) -> Self:
""" """
Read and construct a header from the provided stream. Read and construct a header from the provided stream.
@ -55,7 +55,7 @@ class FileHeader:
Returns: Returns:
FileHeader object FileHeader object
""" """
version = HEADER.read(stream)[0] _version = HEADER.read(stream)[0] # noqa: F841 # var is unused
mod_time, acc_time = BGNLIB.read(stream) mod_time, acc_time = BGNLIB.read(stream)
name = LIBNAME.skip_and_read(stream) name = LIBNAME.skip_and_read(stream)
uu, dbu = UNITS.skip_and_read(stream) uu, dbu = UNITS.skip_and_read(stream)
@ -63,7 +63,7 @@ class FileHeader:
return cls(mod_time=mod_time, acc_time=acc_time, name=name, return cls(mod_time=mod_time, acc_time=acc_time, name=name,
user_units_per_db_unit=uu, meters_per_db_unit=dbu) 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 Write the header to a stream
@ -80,7 +80,7 @@ class FileHeader:
return b 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 Scan through a GDS file, building a table of
{b'structure_name': byte_offset}. {b'structure_name': byte_offset}.
@ -107,7 +107,7 @@ def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
return positions return positions
def try_read_struct(stream: BinaryIO) -> 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. Skip to the next structure and attempt to read it.
@ -125,12 +125,13 @@ def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
return name, elements return name, elements
def write_struct(stream: BinaryIO, def write_struct(
name: bytes, stream: IO[bytes],
elements: List[Element], name: bytes,
cre_time: datetime = datetime(1900, 1, 1), elements: list[Element],
mod_time: datetime = datetime(1900, 1, 1), cre_time: datetime = datetime(1900, 1, 1),
) -> int: mod_time: datetime = datetime(1900, 1, 1),
) -> int:
""" """
Write a structure to the provided stream. Write a structure to the provided stream.
@ -150,7 +151,7 @@ def write_struct(stream: BinaryIO,
return b 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 Read elements from the stream until an ENDSTR
record is encountered. The ENDSTR record is also record is encountered. The ENDSTR record is also
@ -162,7 +163,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
Returns: Returns:
List of element objects. List of element objects.
""" """
data: List[Element] = [] data: list[Element] = []
size, tag = Record.read_header(stream) size, tag = Record.read_header(stream)
while tag != ENDSTR.tag: while tag != ENDSTR.tag:
if tag == BOUNDARY.tag: if tag == BOUNDARY.tag:
@ -175,9 +176,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
data.append(Box.read(stream)) data.append(Box.read(stream))
elif tag == TEXT.tag: elif tag == TEXT.tag:
data.append(Text.read(stream)) data.append(Text.read(stream))
elif tag == SREF.tag: elif tag in (SREF.tag, AREF.tag):
data.append(Reference.read(stream))
elif tag == AREF.tag:
data.append(Reference.read(stream)) data.append(Reference.read(stream))
else: else:
# don't care, skip # don't care, skip
@ -186,7 +185,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
return data 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 Scan through a GDS file, building a table of instance counts
`{b'structure_name': {b'ref_name': count}}`. `{b'structure_name': {b'ref_name': count}}`.
@ -223,7 +222,7 @@ def scan_hierarchy(stream: BinaryIO) -> Dict[bytes, Dict[bytes, int]]:
elif tag == ENDEL.tag: elif tag == ENDEL.tag:
if ref_count is None: if ref_count is None:
ref_count = 1 ref_count = 1
assert(ref_name is not None) assert ref_name is not None
cur_structure[ref_name] += ref_count cur_structure[ref_name] += ref_count
else: else:
stream.seek(size, io.SEEK_CUR) stream.seek(size, io.SEEK_CUR)

View File

@ -1,14 +1,15 @@
""" """
Generic record-level read/write functionality. Generic record-level read/write functionality.
""" """
from typing import Optional, Sequence, BinaryIO from typing import IO, ClassVar, Self, Generic, TypeVar
from typing import TypeVar, List, Tuple, ClassVar, Type from collections.abc import Sequence
import struct import struct
import io import io
from datetime import datetime from datetime import datetime
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy # type: ignore import numpy
from numpy.typing import NDArray
from .basic import KlamathError from .basic import KlamathError
from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
@ -17,9 +18,11 @@ from .basic import parse_ascii, pack_ascii, read
_RECORD_HEADER_FMT = struct.Struct('>HH') _RECORD_HEADER_FMT = struct.Struct('>HH')
II = TypeVar('II') # Input type
OO = TypeVar('OO') # Output type
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 record_size = data_size + 4
if record_size > 0xFFFF: if record_size > 0xFFFF:
raise KlamathError(f'Record size is too big: {record_size}') raise KlamathError(f'Record size is too big: {record_size}')
@ -27,7 +30,7 @@ def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int:
return stream.write(header) 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). Read a record's header (size and tag).
Args: Args:
@ -46,49 +49,46 @@ def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
return data_size, tag 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) data_size, actual_tag = read_record_header(stream)
if tag != actual_tag: if tag != actual_tag:
raise KlamathError(f'Unexpected record! Got tag 0x{actual_tag:04x}, expected 0x{tag:04x}') raise KlamathError(f'Unexpected record! Got tag 0x{actual_tag:04x}, expected 0x{tag:04x}')
return data_size return data_size
R = TypeVar('R', bound='Record') class Record(Generic[II, OO], metaclass=ABCMeta):
class Record(metaclass=ABCMeta):
tag: ClassVar[int] = -1 tag: ClassVar[int] = -1
expected_size: ClassVar[Optional[int]] = None expected_size: ClassVar[int | None] = None
@classmethod @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: if cls.expected_size is not None and size != cls.expected_size:
raise KlamathError(f'Expected size {cls.expected_size}, got {size}') raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
@classmethod @classmethod # noqa: B027 Intentionally non-abstract
def check_data(cls, data): def check_data(cls: type[Self], data: II) -> None:
pass pass
@classmethod @classmethod
@abstractmethod @abstractmethod
def read_data(cls, stream: BinaryIO, size: int): def read_data(cls: type[Self], stream: IO[bytes], size: int) -> OO:
pass pass
@classmethod @classmethod
@abstractmethod @abstractmethod
def pack_data(cls, data) -> bytes: def pack_data(cls: type[Self], data: II) -> bytes:
pass pass
@staticmethod @staticmethod
def read_header(stream: BinaryIO) -> Tuple[int, int]: def read_header(stream: IO[bytes]) -> tuple[int, int]:
return read_record_header(stream) return read_record_header(stream)
@classmethod @classmethod
def write_header(cls, stream: BinaryIO, 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) return write_record_header(stream, data_size, cls.tag)
@classmethod @classmethod
def skip_past(cls, stream: BinaryIO) -> bool: def skip_past(cls: type[Self], stream: IO[bytes]) -> bool:
""" """
Skip to the end of the next occurence of this record. Skip to the end of the next occurence of this record.
@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta):
return True return True
@classmethod @classmethod
def skip_and_read(cls, stream: BinaryIO): def skip_and_read(cls: type[Self], stream: IO[bytes]) -> OO:
size, tag = Record.read_header(stream) size, tag = Record.read_header(stream)
while tag != cls.tag: while tag != cls.tag:
stream.seek(size, io.SEEK_CUR) stream.seek(size, io.SEEK_CUR)
@ -119,90 +119,90 @@ class Record(metaclass=ABCMeta):
return data return data
@classmethod @classmethod
def read(cls: Type[R], stream: BinaryIO): def read(cls: type[Self], stream: IO[bytes]) -> OO:
size = expect_record(stream, cls.tag) size = expect_record(stream, cls.tag)
data = cls.read_data(stream, size) data = cls.read_data(stream, size)
return data return data
@classmethod @classmethod
def write(cls, stream: BinaryIO, data) -> int: def write(cls: type[Self], stream: IO[bytes], data: II) -> int:
data_bytes = cls.pack_data(data) data_bytes = cls.pack_data(data)
b = cls.write_header(stream, len(data_bytes)) b = cls.write_header(stream, len(data_bytes))
b += stream.write(data_bytes) b += stream.write(data_bytes)
return b return b
class NoDataRecord(Record): class NoDataRecord(Record[None, None]):
expected_size: ClassVar[Optional[int]] = 0 expected_size: ClassVar[int | None] = 0
@classmethod @classmethod
def read_data(cls, stream: BinaryIO, size: int) -> None: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> None:
stream.read(size) stream.read(size)
@classmethod @classmethod
def pack_data(cls, data: None) -> bytes: def pack_data(cls: type[Self], data: None) -> bytes:
if data is not None: if data is not None:
raise KlamathError('?? Packing {data} into NoDataRecord??') raise KlamathError('?? Packing {data!r} into NoDataRecord??')
return b'' return b''
class BitArrayRecord(Record): class BitArrayRecord(Record[int, int]):
expected_size: ClassVar[Optional[int]] = 2 expected_size: ClassVar[int | None] = 2
@classmethod @classmethod
def read_data(cls, stream: BinaryIO, 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)) return parse_bitarray(read(stream, 2))
@classmethod @classmethod
def pack_data(cls, data: int) -> bytes: def pack_data(cls: type[Self], data: int) -> bytes:
return pack_bitarray(data) return pack_bitarray(data)
class Int2Record(Record): class Int2Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int16]]):
@classmethod @classmethod
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int16]:
return parse_int2(read(stream, size)) return parse_int2(read(stream, size))
@classmethod @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) return pack_int2(data)
class Int4Record(Record): class Int4Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int32]]):
@classmethod @classmethod
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int32]:
return parse_int4(read(stream, size)) return parse_int4(read(stream, size))
@classmethod @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) return pack_int4(data)
class Real8Record(Record): class Real8Record(Record[Sequence[float] | float, NDArray[numpy.float64]]):
@classmethod @classmethod
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.float64]:
return parse_real8(read(stream, size)) return parse_real8(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: Sequence[int]) -> bytes: def pack_data(cls: type[Self], data: Sequence[float] | float) -> bytes:
return pack_real8(data) return pack_real8(data)
class ASCIIRecord(Record): class ASCIIRecord(Record[bytes, bytes]):
@classmethod @classmethod
def read_data(cls, stream: BinaryIO, size: int) -> bytes: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> bytes:
return parse_ascii(read(stream, size)) return parse_ascii(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: bytes) -> bytes: def pack_data(cls: type[Self], data: bytes) -> bytes:
return pack_ascii(data) return pack_ascii(data)
class DateTimeRecord(Record): class DateTimeRecord(Record[Sequence[datetime], list[datetime]]):
@classmethod @classmethod
def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> list[datetime]:
return parse_datetime(read(stream, size)) return parse_datetime(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: Sequence[datetime]) -> bytes: def pack_data(cls: type[Self], data: Sequence[datetime]) -> bytes:
return pack_datetime(data) return pack_datetime(data)

View File

@ -1,8 +1,12 @@
""" """
Record type and tag definitions 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 .basic import KlamathError
from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record
from .record import ASCIIRecord, DateTimeRecord from .record import ASCIIRecord, DateTimeRecord
@ -111,7 +115,7 @@ class PRESENTATION(BitArrayRecord):
class SPACING(Int2Record): class SPACING(Int2Record):
tag = 0x1802 #Not sure about 02; Unused tag = 0x1802 # Not sure about 02; Unused
class STRING(ASCIIRecord): class STRING(ASCIIRecord):
@ -133,29 +137,29 @@ class ANGLE(Real8Record):
class UINTEGER(Int2Record): class UINTEGER(Int2Record):
tag = 0x1d02 #Unused; not sure about 02 tag = 0x1d02 # Unused; not sure about 02
class USTRING(ASCIIRecord): class USTRING(ASCIIRecord):
tag = 0x1e06 #Unused; not sure about 06 tag = 0x1e06 # Unused; not sure about 06
class REFLIBS(ASCIIRecord): class REFLIBS(ASCIIRecord):
tag = 0x1f06 tag = 0x1f06
@classmethod @classmethod
def check_size(cls, size: int): def check_size(cls: type[Self], size: int) -> None:
if size != 0 and size % 44 != 0: 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): class FONTS(ASCIIRecord):
tag = 0x2006 tag = 0x2006
@classmethod @classmethod
def check_size(cls, size: int): def check_size(cls: type[Self], size: int) -> None:
if size != 0 and size % 44 != 0: 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): class PATHTYPE(Int2Record):
@ -168,26 +172,26 @@ class GENERATIONS(Int2Record):
expected_size = 2 expected_size = 2
@classmethod @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: 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): class ATTRTABLE(ASCIIRecord):
tag = 0x2306 tag = 0x2306
@classmethod @classmethod
def check_size(cls, size: int): def check_size(cls: type[Self], size: int) -> None:
if size > 44: if size > 44:
raise Exception(f'Expected size <= 44, got {size}') raise KlamathError(f'Expected size <= 44, got {size}')
class STYPTABLE(ASCIIRecord): class STYPTABLE(ASCIIRecord):
tag = 0x2406 #UNUSED, not sure about 06 tag = 0x2406 # UNUSED, not sure about 06
class STRTYPE(Int2Record): class STRTYPE(Int2Record):
tag = 0x2502 #UNUSED tag = 0x2502 # UNUSED
class ELFLAGS(BitArrayRecord): class ELFLAGS(BitArrayRecord):
@ -266,9 +270,9 @@ class FORMAT(Int2Record):
expected_size = 2 expected_size = 2
@classmethod @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: 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): class MASK(ASCIIRecord):

View File

@ -1,20 +1,21 @@
import struct import struct
import pytest # type: ignore import pytest # type: ignore
import numpy # type: ignore import numpy
from numpy.testing import assert_array_equal # type: ignore 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 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 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 from .basic import KlamathError
def test_parse_bitarray(): def test_parse_bitarray() -> None:
assert(parse_bitarray(b'59') == 13625) assert parse_bitarray(b'59') == 13625
assert(parse_bitarray(b'\0\0') == 0) assert parse_bitarray(b'\0\0') == 0
assert(parse_bitarray(b'\xff\xff') == 65535) assert parse_bitarray(b'\xff\xff') == 65535
# 4 bytes (too long) # 4 bytes (too long)
with pytest.raises(KlamathError): with pytest.raises(KlamathError):
@ -25,7 +26,7 @@ def test_parse_bitarray():
parse_bitarray(b'') 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)) assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
# odd length # odd length
@ -37,7 +38,7 @@ def test_parse_int2():
parse_int2(b'') parse_int2(b'')
def test_parse_int4(): def test_parse_int4() -> None:
assert_array_equal(parse_int4(b'4321'), (875770417,)) assert_array_equal(parse_int4(b'4321'), (875770417,))
# length % 4 != 0 # length % 4 != 0
@ -49,17 +50,17 @@ def test_parse_int4():
parse_int4(b'') parse_int4(b'')
def test_decode_real8(): def test_decode_real8() -> None:
# zeroes # zeroes
assert(decode_real8(numpy.array([0x0])) == 0) assert decode_real8(numpy.array([0x0])) == 0
assert(decode_real8(numpy.array([1<<63])) == 0) # negative assert decode_real8(numpy.array([1 << 63])) == 0 # negative
assert(decode_real8(numpy.array([0xff << 56])) == 0) # denormalized assert decode_real8(numpy.array([0xff << 56])) == 0 # denormalized
assert(decode_real8(numpy.array([0x4110 << 48])) == 1.0) assert decode_real8(numpy.array([0x4110 << 48])) == 1.0
assert(decode_real8(numpy.array([0xC120 << 48])) == -2.0) 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) 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)) assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0))
@ -72,48 +73,59 @@ def test_parse_real8():
parse_real8(b'') parse_real8(b'')
def test_parse_ascii(): def test_parse_ascii() -> None:
# # empty data Now allowed! # # empty data Now allowed!
# with pytest.raises(KlamathError): # with pytest.raises(KlamathError):
# parse_ascii(b'') # parse_ascii(b'')
assert(parse_ascii(b'12345') == b'12345') assert parse_ascii(b'12345') == b'12345'
assert(parse_ascii(b'12345\0') == b'12345') # strips trailing null byte 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) packed = pack_bitarray(321)
assert(len(packed) == 2) assert len(packed) == 2
assert(packed == struct.pack('>H', 321)) assert packed == struct.pack('>H', 321)
def test_pack_int2(): def test_pack_int2() -> None:
packed = pack_int2((3, 2, 1)) packed = pack_int2((3, 2, 1))
assert(len(packed) == 3*2) assert len(packed) == 3 * 2
assert(packed == struct.pack('>3h', 3, 2, 1)) assert packed == struct.pack('>3h', 3, 2, 1)
assert(pack_int2([-3, 2, -1]) == 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)) packed = pack_int4((3, 2, 1))
assert(len(packed) == 3*4) assert len(packed) == 3 * 4
assert(packed == struct.pack('>3l', 3, 2, 1)) assert packed == struct.pack('>3l', 3, 2, 1)
assert(pack_int4([-3, 2, -1]) == 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) assert encode_real8(numpy.array([0.0])) == 0
arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12)) arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12))
assert_array_equal(decode_real8(encode_real8(arr)), arr) 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) reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12)
packed = pack_real8(reals) packed = pack_real8(reals)
assert(len(packed) == len(reals) * 8) assert len(packed) == len(reals) * 8
assert_array_equal(parse_real8(packed), reals) 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'4321') == b'4321'
assert(pack_ascii(b'321') == b'321\0') assert pack_ascii(b'321') == b'321\0'
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
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

92
pyproject.toml Normal file
View File

@ -0,0 +1,92 @@
[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.11"
include = [
"LICENSE.md"
]
dynamic = ["version"]
dependencies = [
"numpy>=1.26",
]
[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
]

View File

@ -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',
],
)