Compare commits
No commits in common. "master" and "v1.2" have entirely different histories.
18 changed files with 284 additions and 862 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
|||
*.pyc
|
||||
__pycache__/
|
||||
__pycache__
|
||||
|
||||
*.idea
|
||||
|
||||
|
|
@ -7,7 +7,6 @@ build/
|
|||
dist/
|
||||
*.egg-info/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
|
||||
*.swp
|
||||
*.swo
|
||||
|
|
|
|||
|
|
@ -44,13 +44,12 @@ 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
|
||||
|
||||
Requirements:
|
||||
* python >= 3.11
|
||||
* python >= 3.7 (written and tested with 3.8)
|
||||
* numpy
|
||||
|
||||
|
||||
|
|
@ -197,7 +196,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.read_elements(stream)
|
||||
elements_A = klamath.library.try_read_struct(stream)
|
||||
|
||||
stream.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
../LICENSE.md
|
||||
|
|
@ -1 +0,0 @@
|
|||
../README.md
|
||||
4
klamath/VERSION.py
Normal file
4
klamath/VERSION.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
|
||||
__version__ = '''
|
||||
1.2
|
||||
'''.strip()
|
||||
|
|
@ -27,14 +27,12 @@ The goal is to keep this library simple:
|
|||
tools for working with hierarchical design data and supports multiple
|
||||
file formats.
|
||||
"""
|
||||
from . import (
|
||||
basic as basic,
|
||||
record as record,
|
||||
records as records,
|
||||
elements as elements,
|
||||
library as library,
|
||||
)
|
||||
from . import basic
|
||||
from . import record
|
||||
from . import records
|
||||
from . import elements
|
||||
from . import library
|
||||
|
||||
from .VERSION import __version__
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
__version__ = '1.5'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,20 @@
|
|||
"""
|
||||
Functionality for encoding/decoding basic datatypes
|
||||
"""
|
||||
from typing import IO
|
||||
from collections.abc import Sequence
|
||||
from typing import Sequence, BinaryIO, List
|
||||
import struct
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import numpy # type: ignore
|
||||
|
||||
|
||||
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}.')
|
||||
|
|
@ -28,31 +22,31 @@ def parse_bitarray(data: bytes) -> int:
|
|||
return val
|
||||
|
||||
|
||||
def parse_int2(data: bytes) -> NDArray[numpy.int16]:
|
||||
def parse_int2(data: bytes) -> numpy.ndarray:
|
||||
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) -> NDArray[numpy.int32]:
|
||||
def parse_int4(data: bytes) -> numpy.ndarray:
|
||||
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: NDArray[numpy.uint64]) -> NDArray[numpy.float64]:
|
||||
def decode_real8(nums: numpy.ndarray) -> numpy.ndarray:
|
||||
""" Convert GDS REAL8 data to IEEE float64. """
|
||||
nums = nums.astype(numpy.uint64)
|
||||
neg = nums & 0x8000_0000_0000_0000
|
||||
exp = (nums >> 56) & 0x7f
|
||||
mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
|
||||
mant[neg != 0] *= -1
|
||||
return numpy.ldexp(mant, 4 * (exp.astype(numpy.int64) - 64) - 56)
|
||||
return numpy.ldexp(mant, (4 * (exp - 64) - 56).astype(numpy.int64))
|
||||
|
||||
|
||||
def parse_real8(data: bytes) -> NDArray[numpy.float64]:
|
||||
def parse_real8(data: bytes) -> numpy.ndarray:
|
||||
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}.')
|
||||
|
|
@ -68,46 +62,41 @@ 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}.')
|
||||
dts = []
|
||||
for ii in range(0, len(data), 12):
|
||||
year, *date_parts = parse_int2(data[ii:ii + 12])
|
||||
try:
|
||||
dt = datetime(year + 1900, *date_parts)
|
||||
except ValueError:
|
||||
dt = datetime(1900, 1, 1, 0, 0, 0)
|
||||
logger.info(f'Invalid date {[year] + date_parts}, setting {dt} instead')
|
||||
dts.append(dt)
|
||||
year, *date_parts = parse_int2(data[ii:ii+12])
|
||||
dts.append(datetime(year + 1900, *date_parts))
|
||||
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}')
|
||||
return struct.pack('>H', data)
|
||||
|
||||
|
||||
def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
arr = numpy.asarray(data)
|
||||
def pack_int2(data: Sequence[int]) -> bytes:
|
||||
arr = numpy.array(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.asarray(data)
|
||||
def pack_int4(data: Sequence[int]) -> bytes:
|
||||
arr = numpy.array(data)
|
||||
if (arr > 2147483647).any() or (arr < -2147483648).any():
|
||||
raise KlamathError(f'int4 data out of range: {arr}')
|
||||
return arr.astype('>i4').tobytes()
|
||||
|
||||
|
||||
def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
|
||||
def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
|
||||
""" Convert from float64 to GDS REAL8 representation. """
|
||||
# Split the ieee float bitfields
|
||||
ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
|
||||
|
|
@ -149,7 +138,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,12 +149,13 @@ 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)
|
||||
return real8
|
||||
|
||||
|
||||
def pack_real8(data: NDArray[numpy.floating] | Sequence[float] | float) -> bytes:
|
||||
return encode_real8(numpy.asarray(data)).astype('>u8').tobytes()
|
||||
def pack_real8(data: Sequence[float]) -> bytes:
|
||||
return encode_real8(numpy.array(data)).astype('>u8').tobytes()
|
||||
|
||||
|
||||
def pack_ascii(data: bytes) -> bytes:
|
||||
|
|
@ -182,7 +172,7 @@ def pack_datetime(data: Sequence[datetime]) -> bytes:
|
|||
return pack_int2(parts)
|
||||
|
||||
|
||||
def read(stream: IO[bytes], size: int) -> bytes:
|
||||
def read(stream: BinaryIO, size: int) -> bytes:
|
||||
""" Read and check for failure """
|
||||
data = stream.read(size)
|
||||
if len(data) != size:
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@
|
|||
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 typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type, Union
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
import numpy # type: ignore
|
||||
|
||||
from .basic import KlamathError
|
||||
from .record import Record
|
||||
|
|
@ -31,7 +28,8 @@ T = TypeVar('T', bound='Text')
|
|||
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.
|
||||
|
||||
|
|
@ -53,14 +51,12 @@ 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
|
||||
else:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
properties[key] = value
|
||||
size, tag = Record.read_header(stream)
|
||||
return properties
|
||||
|
||||
|
||||
def write_properties(stream: IO[bytes], properties: Mapping[int, bytes]) -> int:
|
||||
def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int:
|
||||
"""
|
||||
Write element properties.
|
||||
|
||||
|
|
@ -82,7 +78,7 @@ class Element(metaclass=ABCMeta):
|
|||
"""
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def read(cls: type[E], stream: IO[bytes]) -> E:
|
||||
def read(cls: Type[E], stream: BinaryIO) -> E:
|
||||
"""
|
||||
Read from a stream to construct this object.
|
||||
Consumes up to (and including) the ENDEL record.
|
||||
|
|
@ -96,7 +92,7 @@ class Element(metaclass=ABCMeta):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
"""
|
||||
Write this element to a stream.
|
||||
Finishes with an ENDEL record.
|
||||
|
|
@ -135,7 +131,7 @@ class Reference(Element):
|
|||
angle_deg: float
|
||||
""" Rotation (degrees counterclockwise) """
|
||||
|
||||
xy: NDArray[numpy.int32]
|
||||
xy: numpy.ndarray
|
||||
"""
|
||||
(For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
|
||||
(For AREF) 3 locations:
|
||||
|
|
@ -148,14 +144,14 @@ class Reference(Element):
|
|||
basis vectors to match it.
|
||||
"""
|
||||
|
||||
colrow: tuple[int, int] | NDArray[numpy.int16] | None
|
||||
colrow: Optional[Union[Tuple[int, int], numpy.ndarray]]
|
||||
""" Number of columns and rows (AREF) or None (SREF) """
|
||||
|
||||
properties: Mapping[int, bytes]
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties associated with this reference. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[R], stream: IO[bytes]) -> R:
|
||||
def read(cls: Type[R], stream: BinaryIO) -> R:
|
||||
invert_y = False
|
||||
mag = 1
|
||||
angle_deg = 0
|
||||
|
|
@ -178,17 +174,10 @@ 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:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = 0
|
||||
if self.colrow is None:
|
||||
b += SREF.write(stream, None)
|
||||
|
|
@ -200,7 +189,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:
|
||||
|
|
@ -215,7 +204,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: # noqa: PLR5501
|
||||
else:
|
||||
if self.xy.size != 2:
|
||||
raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
|
||||
|
||||
|
|
@ -227,24 +216,24 @@ class Boundary(Element):
|
|||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: tuple[int, int]
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, data_type) tuple """
|
||||
|
||||
xy: NDArray[numpy.int32]
|
||||
xy: numpy.ndarray
|
||||
""" Ordered vertices of the shape. First and last points should be identical. """
|
||||
|
||||
properties: Mapping[int, bytes]
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[B], stream: IO[bytes]) -> B:
|
||||
def read(cls: Type[B], stream: BinaryIO) -> 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: IO[bytes]) -> int:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = BOUNDARY.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += DATATYPE.write(stream, self.layer[1])
|
||||
|
|
@ -264,7 +253,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
|
||||
|
|
@ -273,17 +262,17 @@ class Path(Element):
|
|||
width: int
|
||||
""" Path width """
|
||||
|
||||
extension: tuple[int, int]
|
||||
extension: Tuple[int, int]
|
||||
""" Extension when using path_type=4. Ignored otherwise. """
|
||||
|
||||
xy: NDArray[numpy.int32]
|
||||
xy: numpy.ndarray
|
||||
""" Path centerline coordinates """
|
||||
|
||||
properties: Mapping[int, bytes]
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[P], stream: IO[bytes]) -> P:
|
||||
def read(cls: Type[P], stream: BinaryIO) -> P:
|
||||
path_type = 0
|
||||
width = 0
|
||||
bgn_ext = 0
|
||||
|
|
@ -310,7 +299,7 @@ class Path(Element):
|
|||
properties=properties, extension=(bgn_ext, end_ext),
|
||||
path_type=path_type, width=width)
|
||||
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = PATH.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += DATATYPE.write(stream, self.layer[1])
|
||||
|
|
@ -319,12 +308,12 @@ 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, int(bgn_ext))
|
||||
b += BGNEXTN.write(stream, bgn_ext)
|
||||
if end_ext != 0:
|
||||
b += ENDEXTN.write(stream, int(end_ext))
|
||||
b += ENDEXTN.write(stream, end_ext)
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
|
|
@ -338,24 +327,24 @@ class Box(Element):
|
|||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: tuple[int, int]
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, box_type) tuple """
|
||||
|
||||
xy: NDArray[numpy.int32]
|
||||
xy: numpy.ndarray
|
||||
""" Box coordinates (5 pairs) """
|
||||
|
||||
properties: Mapping[int, bytes]
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[X], stream: IO[bytes]) -> X:
|
||||
def read(cls: Type[X], stream: BinaryIO) -> 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: IO[bytes]) -> int:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = BOX.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += BOXTYPE.write(stream, self.layer[1])
|
||||
|
|
@ -372,24 +361,24 @@ class Node(Element):
|
|||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: tuple[int, int]
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, node_type) tuple """
|
||||
|
||||
xy: NDArray[numpy.int32]
|
||||
xy: numpy.ndarray
|
||||
""" 1-50 pairs of coordinates. """
|
||||
|
||||
properties: Mapping[int, bytes]
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[N], stream: IO[bytes]) -> N:
|
||||
def read(cls: Type[N], stream: BinaryIO) -> 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: IO[bytes]) -> int:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = NODE.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += NODETYPE.write(stream, self.layer[1])
|
||||
|
|
@ -407,7 +396,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
|
||||
|
|
@ -432,17 +421,17 @@ class Text(Element):
|
|||
angle_deg: float
|
||||
""" Rotation (ccw). Default 0. """
|
||||
|
||||
xy: NDArray[numpy.int32]
|
||||
xy: numpy.ndarray
|
||||
""" Position (1 pair only) """
|
||||
|
||||
string: bytes
|
||||
""" Text content """
|
||||
|
||||
properties: Mapping[int, bytes]
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[T], stream: IO[bytes]) -> T:
|
||||
def read(cls: Type[T], stream: BinaryIO) -> T:
|
||||
path_type = 0
|
||||
presentation = 0
|
||||
invert_y = False
|
||||
|
|
@ -478,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: IO[bytes]) -> int:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = TEXT.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += TEXTTYPE.write(stream, self.layer[1])
|
||||
|
|
@ -492,7 +481,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)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
File-level read/write functionality.
|
||||
"""
|
||||
from typing import IO, Self, TYPE_CHECKING
|
||||
from typing import List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type, MutableMapping
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
FH = TypeVar('FH', bound='FileHeader')
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -45,7 +45,7 @@ class FileHeader:
|
|||
""" Last-accessed time """
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[Self], stream: IO[bytes]) -> Self:
|
||||
def read(cls: Type[FH], stream: BinaryIO) -> FH:
|
||||
"""
|
||||
Read and construct a header from the provided stream.
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ class FileHeader:
|
|||
Returns:
|
||||
FileHeader object
|
||||
"""
|
||||
_version = HEADER.read(stream)[0] # noqa: F841 # var is unused
|
||||
version = HEADER.read(stream)[0]
|
||||
mod_time, acc_time = BGNLIB.read(stream)
|
||||
name = LIBNAME.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,
|
||||
user_units_per_db_unit=uu, meters_per_db_unit=dbu)
|
||||
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
"""
|
||||
Write the header to a stream
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ class FileHeader:
|
|||
return b
|
||||
|
||||
|
||||
def scan_structs(stream: IO[bytes]) -> dict[bytes, int]:
|
||||
def scan_structs(stream: BinaryIO) -> 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]) -> tuple[bytes, list[Element]] | None:
|
||||
def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
|
||||
"""
|
||||
Skip to the next structure and attempt to read it.
|
||||
|
||||
|
|
@ -125,13 +125,12 @@ def try_read_struct(stream: IO[bytes]) -> tuple[bytes, list[Element]] | None:
|
|||
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: BinaryIO,
|
||||
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.
|
||||
|
||||
|
|
@ -151,7 +150,7 @@ def write_struct(
|
|||
return b
|
||||
|
||||
|
||||
def read_elements(stream: IO[bytes]) -> list[Element]:
|
||||
def read_elements(stream: BinaryIO) -> List[Element]:
|
||||
"""
|
||||
Read elements from the stream until an ENDSTR
|
||||
record is encountered. The ENDSTR record is also
|
||||
|
|
@ -163,7 +162,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:
|
||||
|
|
@ -176,7 +175,9 @@ def read_elements(stream: IO[bytes]) -> list[Element]:
|
|||
data.append(Box.read(stream))
|
||||
elif tag == TEXT.tag:
|
||||
data.append(Text.read(stream))
|
||||
elif tag in (SREF.tag, AREF.tag):
|
||||
elif tag == SREF.tag:
|
||||
data.append(Reference.read(stream))
|
||||
elif tag == AREF.tag:
|
||||
data.append(Reference.read(stream))
|
||||
else:
|
||||
# don't care, skip
|
||||
|
|
@ -185,7 +186,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: BinaryIO) -> Dict[bytes, Dict[bytes, int]]:
|
||||
"""
|
||||
Scan through a GDS file, building a table of instance counts
|
||||
`{b'structure_name': {b'ref_name': count}}`.
|
||||
|
|
@ -220,15 +221,10 @@ 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_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
|
||||
if ref_count is None:
|
||||
ref_count = 1
|
||||
assert(ref_name is not None)
|
||||
cur_structure[ref_name] += ref_count
|
||||
else:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
size, tag = Record.read_header(stream)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
"""
|
||||
Generic record-level read/write functionality.
|
||||
"""
|
||||
from typing import IO, ClassVar, Self, Generic, TypeVar
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional, Sequence, BinaryIO
|
||||
from typing import TypeVar, List, Tuple, ClassVar, Type
|
||||
import struct
|
||||
import io
|
||||
from datetime import datetime
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
import numpy # type: ignore
|
||||
|
||||
from .basic import KlamathError
|
||||
from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
|
||||
|
|
@ -18,11 +17,9 @@ 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:
|
||||
def write_record_header(stream: BinaryIO, 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}')
|
||||
|
|
@ -30,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: BinaryIO) -> Tuple[int, int]:
|
||||
"""
|
||||
Read a record's header (size and tag).
|
||||
Args:
|
||||
|
|
@ -49,46 +46,49 @@ def read_record_header(stream: IO[bytes]) -> tuple[int, int]:
|
|||
return data_size, tag
|
||||
|
||||
|
||||
def expect_record(stream: IO[bytes], tag: int) -> int:
|
||||
def expect_record(stream: BinaryIO, 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}')
|
||||
return data_size
|
||||
|
||||
|
||||
class Record(Generic[II, OO], metaclass=ABCMeta):
|
||||
R = TypeVar('R', bound='Record')
|
||||
|
||||
|
||||
class Record(metaclass=ABCMeta):
|
||||
tag: ClassVar[int] = -1
|
||||
expected_size: ClassVar[int | None] = None
|
||||
expected_size: ClassVar[Optional[int]] = None
|
||||
|
||||
@classmethod
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
def check_size(cls, size: int):
|
||||
if cls.expected_size is not None and size != cls.expected_size:
|
||||
raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
|
||||
|
||||
@classmethod # noqa: B027 Intentionally non-abstract
|
||||
def check_data(cls: type[Self], data: II) -> None:
|
||||
@classmethod
|
||||
def check_data(cls, data):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> OO:
|
||||
def read_data(cls, stream: BinaryIO, size: int):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def pack_data(cls: type[Self], data: II) -> bytes:
|
||||
def pack_data(cls, data) -> bytes:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def read_header(stream: IO[bytes]) -> tuple[int, int]:
|
||||
def read_header(stream: BinaryIO) -> Tuple[int, int]:
|
||||
return read_record_header(stream)
|
||||
|
||||
@classmethod
|
||||
def write_header(cls: type[Self], stream: IO[bytes], data_size: int) -> int:
|
||||
def write_header(cls, stream: BinaryIO, data_size: int) -> int:
|
||||
return write_record_header(stream, data_size, cls.tag)
|
||||
|
||||
@classmethod
|
||||
def skip_past(cls: type[Self], stream: IO[bytes]) -> bool:
|
||||
def skip_past(cls, stream: BinaryIO) -> bool:
|
||||
"""
|
||||
Skip to the end of the next occurence of this record.
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ class Record(Generic[II, OO], metaclass=ABCMeta):
|
|||
return True
|
||||
|
||||
@classmethod
|
||||
def skip_and_read(cls: type[Self], stream: IO[bytes]) -> OO:
|
||||
def skip_and_read(cls, stream: BinaryIO):
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != cls.tag:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
|
|
@ -119,90 +119,90 @@ class Record(Generic[II, OO], metaclass=ABCMeta):
|
|||
return data
|
||||
|
||||
@classmethod
|
||||
def read(cls: type[Self], stream: IO[bytes]) -> OO:
|
||||
def read(cls: Type[R], stream: BinaryIO):
|
||||
size = expect_record(stream, cls.tag)
|
||||
data = cls.read_data(stream, size)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def write(cls: type[Self], stream: IO[bytes], data: II) -> int:
|
||||
def write(cls, stream: BinaryIO, data) -> 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[None, None]):
|
||||
expected_size: ClassVar[int | None] = 0
|
||||
class NoDataRecord(Record):
|
||||
expected_size: ClassVar[Optional[int]] = 0
|
||||
|
||||
@classmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> None:
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> None:
|
||||
stream.read(size)
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls: type[Self], data: None) -> bytes:
|
||||
def pack_data(cls, data: None) -> bytes:
|
||||
if data is not None:
|
||||
raise KlamathError('?? Packing {data!r} into NoDataRecord??')
|
||||
raise KlamathError('?? Packing {data} into NoDataRecord??')
|
||||
return b''
|
||||
|
||||
|
||||
class BitArrayRecord(Record[int, int]):
|
||||
expected_size: ClassVar[int | None] = 2
|
||||
class BitArrayRecord(Record):
|
||||
expected_size: ClassVar[Optional[int]] = 2
|
||||
|
||||
@classmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> int: # noqa: ARG003 size unused
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> int:
|
||||
return parse_bitarray(read(stream, 2))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls: type[Self], data: int) -> bytes:
|
||||
def pack_data(cls, data: int) -> bytes:
|
||||
return pack_bitarray(data)
|
||||
|
||||
|
||||
class Int2Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int16]]):
|
||||
class Int2Record(Record):
|
||||
@classmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int16]:
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
||||
return parse_int2(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
return pack_int2(data)
|
||||
|
||||
|
||||
class Int4Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int32]]):
|
||||
class Int4Record(Record):
|
||||
@classmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int32]:
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
||||
return parse_int4(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
return pack_int4(data)
|
||||
|
||||
|
||||
class Real8Record(Record[Sequence[float] | float, NDArray[numpy.float64]]):
|
||||
class Real8Record(Record):
|
||||
@classmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.float64]:
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
||||
return parse_real8(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls: type[Self], data: Sequence[float] | float) -> bytes:
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
return pack_real8(data)
|
||||
|
||||
|
||||
class ASCIIRecord(Record[bytes, bytes]):
|
||||
class ASCIIRecord(Record):
|
||||
@classmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> bytes:
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> bytes:
|
||||
return parse_ascii(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls: type[Self], data: bytes) -> bytes:
|
||||
def pack_data(cls, data: bytes) -> bytes:
|
||||
return pack_ascii(data)
|
||||
|
||||
|
||||
class DateTimeRecord(Record[Sequence[datetime], list[datetime]]):
|
||||
class DateTimeRecord(Record):
|
||||
@classmethod
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> list[datetime]:
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]:
|
||||
return parse_datetime(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls: type[Self], data: Sequence[datetime]) -> bytes:
|
||||
def pack_data(cls, data: Sequence[datetime]) -> bytes:
|
||||
return pack_datetime(data)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
"""
|
||||
Record type and tag definitions
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Sized
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
from typing import Sequence
|
||||
|
||||
from .basic import KlamathError
|
||||
from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record
|
||||
from .record import ASCIIRecord, DateTimeRecord
|
||||
|
||||
|
|
@ -18,7 +14,7 @@ class HEADER(Int2Record):
|
|||
|
||||
class BGNLIB(DateTimeRecord):
|
||||
tag = 0x0102
|
||||
expected_size = 2 * 6 * 2
|
||||
expected_size = 6 * 2
|
||||
|
||||
|
||||
class LIBNAME(ASCIIRecord):
|
||||
|
|
@ -37,7 +33,7 @@ class ENDLIB(NoDataRecord):
|
|||
|
||||
class BGNSTR(DateTimeRecord):
|
||||
tag = 0x0502
|
||||
expected_size = 2 * 6 * 2
|
||||
expected_size = 6 * 2
|
||||
|
||||
|
||||
class STRNAME(ASCIIRecord):
|
||||
|
|
@ -115,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):
|
||||
|
|
@ -137,29 +133,29 @@ 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):
|
||||
tag = 0x1f06
|
||||
|
||||
@classmethod
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
def check_size(cls, size: int):
|
||||
if size != 0 and size % 44 != 0:
|
||||
raise KlamathError(f'Expected size to be multiple of 44, got {size}')
|
||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
||||
|
||||
|
||||
class FONTS(ASCIIRecord):
|
||||
tag = 0x2006
|
||||
|
||||
@classmethod
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
def check_size(cls, size: int):
|
||||
if size != 0 and size % 44 != 0:
|
||||
raise KlamathError(f'Expected size to be multiple of 44, got {size}')
|
||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
||||
|
||||
|
||||
class PATHTYPE(Int2Record):
|
||||
|
|
@ -172,28 +168,26 @@ class GENERATIONS(Int2Record):
|
|||
expected_size = 2
|
||||
|
||||
@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}')
|
||||
def check_data(cls, data: Sequence[int]):
|
||||
if len(data) != 1:
|
||||
raise Exception(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
||||
class ATTRTABLE(ASCIIRecord):
|
||||
tag = 0x2306
|
||||
|
||||
@classmethod
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
def check_size(cls, size: int):
|
||||
if size > 44:
|
||||
raise KlamathError(f'Expected size <= 44, got {size}')
|
||||
raise Exception(f'Expected size <= 44, got {size}')
|
||||
|
||||
|
||||
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):
|
||||
|
|
@ -224,6 +218,7 @@ class PROPATTR(Int2Record):
|
|||
|
||||
class PROPVALUE(ASCIIRecord):
|
||||
tag = 0x2c06
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class BOX(NoDataRecord):
|
||||
|
|
@ -271,11 +266,9 @@ class FORMAT(Int2Record):
|
|||
expected_size = 2
|
||||
|
||||
@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}')
|
||||
def check_data(cls, data: Sequence[int]):
|
||||
if len(data) != 1:
|
||||
raise Exception(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
||||
class MASK(ASCIIRecord):
|
||||
|
|
@ -309,7 +302,7 @@ class SOFTFENCE(NoDataRecord):
|
|||
|
||||
|
||||
class HARDFENCE(NoDataRecord):
|
||||
tag = 0x3e00
|
||||
tag = 0x3f00
|
||||
|
||||
|
||||
class SOFTWIRE(NoDataRecord):
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import struct
|
||||
|
||||
import pytest # type: ignore
|
||||
import numpy
|
||||
from datetime import datetime
|
||||
from numpy.testing import assert_array_equal
|
||||
import numpy # type: ignore
|
||||
from numpy.testing import assert_array_equal # type: ignore
|
||||
|
||||
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, parse_datetime
|
||||
from .basic import decode_real8, encode_real8
|
||||
|
||||
from .basic import KlamathError
|
||||
|
||||
|
||||
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
|
||||
def test_parse_bitarray():
|
||||
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):
|
||||
|
|
@ -26,7 +25,7 @@ def test_parse_bitarray() -> None:
|
|||
parse_bitarray(b'')
|
||||
|
||||
|
||||
def test_parse_int2() -> None:
|
||||
def test_parse_int2():
|
||||
assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
|
||||
|
||||
# odd length
|
||||
|
|
@ -38,7 +37,7 @@ def test_parse_int2() -> None:
|
|||
parse_int2(b'')
|
||||
|
||||
|
||||
def test_parse_int4() -> None:
|
||||
def test_parse_int4():
|
||||
assert_array_equal(parse_int4(b'4321'), (875770417,))
|
||||
|
||||
# length % 4 != 0
|
||||
|
|
@ -50,17 +49,17 @@ def test_parse_int4() -> None:
|
|||
parse_int4(b'')
|
||||
|
||||
|
||||
def test_decode_real8() -> None:
|
||||
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() -> None:
|
||||
def test_parse_real8():
|
||||
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,59 +72,48 @@ def test_parse_real8() -> None:
|
|||
parse_real8(b'')
|
||||
|
||||
|
||||
def test_parse_ascii() -> None:
|
||||
# # empty data Now allowed!
|
||||
# with pytest.raises(KlamathError):
|
||||
# parse_ascii(b'')
|
||||
def test_parse_ascii():
|
||||
# # 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() -> None:
|
||||
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() -> None:
|
||||
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() -> None:
|
||||
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() -> None:
|
||||
assert encode_real8(numpy.array([0.0])) == 0
|
||||
def test_encode_real8():
|
||||
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() -> None:
|
||||
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() -> None:
|
||||
assert pack_ascii(b'4321') == b'4321'
|
||||
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
|
||||
def test_pack_ascii():
|
||||
assert(pack_ascii(b'4321') == b'4321')
|
||||
assert(pack_ascii(b'321') == b'321\0')
|
||||
|
|
|
|||
|
|
@ -1,156 +0,0 @@
|
|||
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)
|
||||
|
||||
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)
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
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"] == {}
|
||||
|
||||
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)
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
[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)",
|
||||
"Topic :: File Formats",
|
||||
]
|
||||
requires-python = ">=3.11"
|
||||
include = [
|
||||
"LICENSE.md"
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"numpy>=1.26",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[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
|
||||
"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
|
||||
]
|
||||
|
||||
63
setup.py
Normal file
63
setup.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#!/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',
|
||||
],
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue