Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
ae9c2d7b5f | |||
428e396b9b | |||
4ffb87d361 | |||
a50d53b508 | |||
6ad3358665 | |||
65a33d2eca | |||
7d6cea1c4a | |||
95976cd637 | |||
8061d6cd37 | |||
cae970e65c | |||
2ea9d32984 | |||
15af9078f0 | |||
f12a1c6421 | |||
438cde513e | |||
59c94f7c17 | |||
e7e42a2ef8 | |||
dc58159cdf | |||
e94b93d5af | |||
6a0019010f | |||
46b3349935 | |||
97527a5948 | |||
61d04f89ad | |||
9d14bf27c6 | |||
4d362f8e09 | |||
0bbc8f8e08 | |||
e90e44bd15 | |||
38fc306644 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
__pycache__/
|
||||
|
||||
*.idea
|
||||
|
||||
@ -7,6 +7,7 @@ build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
|
||||
*.swp
|
||||
*.swo
|
||||
|
@ -44,12 +44,13 @@ 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.7 (written and tested with 3.8)
|
||||
* python >= 3.11
|
||||
* numpy
|
||||
|
||||
|
||||
|
1
klamath/LICENSE.md
Symbolic link
1
klamath/LICENSE.md
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE.md
|
1
klamath/README.md
Symbolic link
1
klamath/README.md
Symbolic link
@ -0,0 +1 @@
|
||||
../README.md
|
@ -1,4 +0,0 @@
|
||||
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
|
||||
__version__ = '''
|
||||
1.2
|
||||
'''.strip()
|
@ -27,12 +27,14 @@ The goal is to keep this library simple:
|
||||
tools for working with hierarchical design data and supports multiple
|
||||
file formats.
|
||||
"""
|
||||
from . import basic
|
||||
from . import record
|
||||
from . import records
|
||||
from . import elements
|
||||
from . import library
|
||||
|
||||
from .VERSION import __version__
|
||||
from . import (
|
||||
basic as basic,
|
||||
record as record,
|
||||
records as records,
|
||||
elements as elements,
|
||||
library as library,
|
||||
)
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
__version__ = '1.4'
|
||||
|
||||
|
@ -1,20 +1,26 @@
|
||||
"""
|
||||
Functionality for encoding/decoding basic datatypes
|
||||
"""
|
||||
from typing import Sequence, BinaryIO, List
|
||||
from typing import IO
|
||||
from collections.abc import Sequence
|
||||
import struct
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import numpy # type: ignore
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KlamathError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
Parse functions
|
||||
"""
|
||||
#
|
||||
# Parse functions
|
||||
#
|
||||
def parse_bitarray(data: bytes) -> int:
|
||||
if len(data) != 2:
|
||||
raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.')
|
||||
@ -22,31 +28,31 @@ def parse_bitarray(data: bytes) -> int:
|
||||
return val
|
||||
|
||||
|
||||
def parse_int2(data: bytes) -> numpy.ndarray:
|
||||
def parse_int2(data: bytes) -> NDArray[numpy.int16]:
|
||||
data_len = len(data)
|
||||
if data_len == 0 or (data_len % 2) != 0:
|
||||
raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.')
|
||||
return numpy.frombuffer(data, dtype='>i2', count=data_len // 2)
|
||||
|
||||
|
||||
def parse_int4(data: bytes) -> numpy.ndarray:
|
||||
def parse_int4(data: bytes) -> NDArray[numpy.int32]:
|
||||
data_len = len(data)
|
||||
if data_len == 0 or (data_len % 4) != 0:
|
||||
raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.')
|
||||
return numpy.frombuffer(data, dtype='>i4', count=data_len // 4)
|
||||
|
||||
|
||||
def decode_real8(nums: numpy.ndarray) -> numpy.ndarray:
|
||||
def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]:
|
||||
""" Convert GDS REAL8 data to IEEE float64. """
|
||||
nums = nums.astype(numpy.uint64)
|
||||
neg = nums & 0x8000_0000_0000_0000
|
||||
exp = (nums >> 56) & 0x7f
|
||||
mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
|
||||
mant[neg != 0] *= -1
|
||||
return numpy.ldexp(mant, (4 * (exp - 64) - 56).astype(numpy.int64))
|
||||
return numpy.ldexp(mant, 4 * (exp - 64) - 56, signature=(float, int, float))
|
||||
|
||||
|
||||
def parse_real8(data: bytes) -> numpy.ndarray:
|
||||
def parse_real8(data: bytes) -> NDArray[numpy.float64]:
|
||||
data_len = len(data)
|
||||
if data_len == 0 or (data_len % 8) != 0:
|
||||
raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.')
|
||||
@ -62,41 +68,46 @@ 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])
|
||||
dts.append(datetime(year + 1900, *date_parts))
|
||||
year, *date_parts = parse_int2(data[ii:ii + 12])
|
||||
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
|
||||
|
||||
|
||||
"""
|
||||
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: Sequence[int]) -> bytes:
|
||||
arr = numpy.array(data)
|
||||
def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
arr = numpy.asarray(data)
|
||||
if (arr > 32767).any() or (arr < -32768).any():
|
||||
raise KlamathError(f'int2 data out of range: {arr}')
|
||||
return arr.astype('>i2').tobytes()
|
||||
|
||||
|
||||
def pack_int4(data: Sequence[int]) -> bytes:
|
||||
arr = numpy.array(data)
|
||||
def pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
arr = numpy.asarray(data)
|
||||
if (arr > 2147483647).any() or (arr < -2147483648).any():
|
||||
raise KlamathError(f'int4 data out of range: {arr}')
|
||||
return arr.astype('>i4').tobytes()
|
||||
|
||||
|
||||
def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
|
||||
def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
|
||||
""" Convert from float64 to GDS REAL8 representation. """
|
||||
# Split the ieee float bitfields
|
||||
ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
|
||||
@ -149,13 +160,13 @@ def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
|
||||
|
||||
real8 = sign | gds_exp_bits | gds_mant
|
||||
real8[zero] = 0
|
||||
real8[gds_exp < -14] = 0 # number is too small
|
||||
real8[gds_exp < -14] = 0 # number is too small
|
||||
|
||||
return real8
|
||||
return real8.astype(numpy.uint64, copy=False)
|
||||
|
||||
|
||||
def pack_real8(data: Sequence[float]) -> bytes:
|
||||
return encode_real8(numpy.array(data)).astype('>u8').tobytes()
|
||||
def pack_real8(data: NDArray[numpy.floating] | Sequence[float] | float) -> bytes:
|
||||
return encode_real8(numpy.asarray(data)).astype('>u8').tobytes()
|
||||
|
||||
|
||||
def pack_ascii(data: bytes) -> bytes:
|
||||
@ -172,7 +183,7 @@ def pack_datetime(data: Sequence[datetime]) -> bytes:
|
||||
return pack_int2(parts)
|
||||
|
||||
|
||||
def read(stream: BinaryIO, size: int) -> bytes:
|
||||
def read(stream: IO[bytes], size: int) -> bytes:
|
||||
""" Read and check for failure """
|
||||
data = stream.read(size)
|
||||
if len(data) != size:
|
||||
|
@ -2,11 +2,13 @@
|
||||
Functionality for reading/writing elements (geometry, text labels,
|
||||
structure references) and associated properties.
|
||||
"""
|
||||
from typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type, Union
|
||||
from typing import IO, TypeVar
|
||||
from collections.abc import Mapping
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy # type: ignore
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .basic import KlamathError
|
||||
from .record import Record
|
||||
@ -28,8 +30,7 @@ T = TypeVar('T', bound='Text')
|
||||
X = TypeVar('X', bound='Box')
|
||||
|
||||
|
||||
|
||||
def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
|
||||
def read_properties(stream: IO[bytes]) -> dict[int, bytes]:
|
||||
"""
|
||||
Read element properties.
|
||||
|
||||
@ -51,12 +52,12 @@ def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
|
||||
value = PROPVALUE.read(stream)
|
||||
if key in properties:
|
||||
raise KlamathError(f'Duplicate property key: {key!r}')
|
||||
properties[key] = value
|
||||
properties[key] = value
|
||||
size, tag = Record.read_header(stream)
|
||||
return properties
|
||||
|
||||
|
||||
def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int:
|
||||
def write_properties(stream: IO[bytes], properties: Mapping[int, bytes]) -> int:
|
||||
"""
|
||||
Write element properties.
|
||||
|
||||
@ -78,7 +79,7 @@ class Element(metaclass=ABCMeta):
|
||||
"""
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def read(cls: Type[E], stream: BinaryIO) -> E:
|
||||
def read(cls: type[E], stream: IO[bytes]) -> E:
|
||||
"""
|
||||
Read from a stream to construct this object.
|
||||
Consumes up to (and including) the ENDEL record.
|
||||
@ -92,7 +93,7 @@ class Element(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
"""
|
||||
Write this element to a stream.
|
||||
Finishes with an ENDEL record.
|
||||
@ -131,7 +132,7 @@ class Reference(Element):
|
||||
angle_deg: float
|
||||
""" Rotation (degrees counterclockwise) """
|
||||
|
||||
xy: numpy.ndarray
|
||||
xy: NDArray[numpy.int32]
|
||||
"""
|
||||
(For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
|
||||
(For AREF) 3 locations:
|
||||
@ -144,14 +145,14 @@ class Reference(Element):
|
||||
basis vectors to match it.
|
||||
"""
|
||||
|
||||
colrow: Optional[Union[Tuple[int, int], numpy.ndarray]]
|
||||
colrow: tuple[int, int] | NDArray[numpy.int16] | None
|
||||
""" Number of columns and rows (AREF) or None (SREF) """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
properties: Mapping[int, bytes]
|
||||
""" Properties associated with this reference. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[R], stream: BinaryIO) -> R:
|
||||
def read(cls: type[R], stream: IO[bytes]) -> R:
|
||||
invert_y = False
|
||||
mag = 1
|
||||
angle_deg = 0
|
||||
@ -174,10 +175,17 @@ 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: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
b = 0
|
||||
if self.colrow is None:
|
||||
b += SREF.write(stream, None)
|
||||
@ -189,7 +197,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:
|
||||
@ -204,7 +212,7 @@ class Reference(Element):
|
||||
if self.colrow is not None:
|
||||
if self.xy.size != 6:
|
||||
raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
|
||||
else:
|
||||
else: # noqa: PLR5501
|
||||
if self.xy.size != 2:
|
||||
raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
|
||||
|
||||
@ -216,24 +224,24 @@ class Boundary(Element):
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
layer: tuple[int, int]
|
||||
""" (layer, data_type) tuple """
|
||||
|
||||
xy: numpy.ndarray
|
||||
xy: NDArray[numpy.int32]
|
||||
""" Ordered vertices of the shape. First and last points should be identical. """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
properties: Mapping[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[B], stream: BinaryIO) -> B:
|
||||
def read(cls: type[B], stream: IO[bytes]) -> B:
|
||||
layer = LAYER.skip_and_read(stream)[0]
|
||||
dtype = DATATYPE.read(stream)[0]
|
||||
xy = XY.read(stream).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
b = BOUNDARY.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += DATATYPE.write(stream, self.layer[1])
|
||||
@ -253,7 +261,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
|
||||
@ -262,17 +270,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: numpy.ndarray
|
||||
xy: NDArray[numpy.int32]
|
||||
""" Path centerline coordinates """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
properties: Mapping[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[P], stream: BinaryIO) -> P:
|
||||
def read(cls: type[P], stream: IO[bytes]) -> P:
|
||||
path_type = 0
|
||||
width = 0
|
||||
bgn_ext = 0
|
||||
@ -299,7 +307,7 @@ class Path(Element):
|
||||
properties=properties, extension=(bgn_ext, end_ext),
|
||||
path_type=path_type, width=width)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
b = PATH.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += DATATYPE.write(stream, self.layer[1])
|
||||
@ -327,24 +335,24 @@ class Box(Element):
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
layer: tuple[int, int]
|
||||
""" (layer, box_type) tuple """
|
||||
|
||||
xy: numpy.ndarray
|
||||
xy: NDArray[numpy.int32]
|
||||
""" Box coordinates (5 pairs) """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
properties: Mapping[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[X], stream: BinaryIO) -> X:
|
||||
def read(cls: type[X], stream: IO[bytes]) -> X:
|
||||
layer = LAYER.skip_and_read(stream)[0]
|
||||
dtype = BOXTYPE.read(stream)[0]
|
||||
xy = XY.read(stream).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
b = BOX.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += BOXTYPE.write(stream, self.layer[1])
|
||||
@ -361,24 +369,24 @@ class Node(Element):
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
layer: tuple[int, int]
|
||||
""" (layer, node_type) tuple """
|
||||
|
||||
xy: numpy.ndarray
|
||||
xy: NDArray[numpy.int32]
|
||||
""" 1-50 pairs of coordinates. """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
properties: Mapping[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[N], stream: BinaryIO) -> N:
|
||||
def read(cls: type[N], stream: IO[bytes]) -> N:
|
||||
layer = LAYER.skip_and_read(stream)[0]
|
||||
dtype = NODETYPE.read(stream)[0]
|
||||
xy = XY.read(stream).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
b = NODE.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += NODETYPE.write(stream, self.layer[1])
|
||||
@ -396,7 +404,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
|
||||
@ -421,17 +429,17 @@ class Text(Element):
|
||||
angle_deg: float
|
||||
""" Rotation (ccw). Default 0. """
|
||||
|
||||
xy: numpy.ndarray
|
||||
xy: NDArray[numpy.int32]
|
||||
""" Position (1 pair only) """
|
||||
|
||||
string: bytes
|
||||
""" Text content """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
properties: Mapping[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[T], stream: BinaryIO) -> T:
|
||||
def read(cls: type[T], stream: IO[bytes]) -> T:
|
||||
path_type = 0
|
||||
presentation = 0
|
||||
invert_y = False
|
||||
@ -467,7 +475,7 @@ class Text(Element):
|
||||
string=string, presentation=presentation, path_type=path_type,
|
||||
width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
b = TEXT.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += TEXTTYPE.write(stream, self.layer[1])
|
||||
@ -481,7 +489,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 List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type, MutableMapping
|
||||
from typing import IO, Self, TYPE_CHECKING
|
||||
import io
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
@ -15,8 +15,8 @@ from .records import BGNSTR, STRNAME, ENDSTR, SNAME, COLROW, ENDEL
|
||||
from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
|
||||
from .elements import Element, Reference, Text, Box, Boundary, Path, Node
|
||||
|
||||
|
||||
FH = TypeVar('FH', bound='FileHeader')
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -45,7 +45,7 @@ class FileHeader:
|
||||
""" Last-accessed time """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[FH], stream: BinaryIO) -> FH:
|
||||
def read(cls: type[Self], stream: IO[bytes]) -> Self:
|
||||
"""
|
||||
Read and construct a header from the provided stream.
|
||||
|
||||
@ -55,7 +55,7 @@ class FileHeader:
|
||||
Returns:
|
||||
FileHeader object
|
||||
"""
|
||||
version = HEADER.read(stream)[0]
|
||||
_version = HEADER.read(stream)[0] # noqa: F841 # var is unused
|
||||
mod_time, acc_time = BGNLIB.read(stream)
|
||||
name = LIBNAME.skip_and_read(stream)
|
||||
uu, dbu = UNITS.skip_and_read(stream)
|
||||
@ -63,7 +63,7 @@ class FileHeader:
|
||||
return cls(mod_time=mod_time, acc_time=acc_time, name=name,
|
||||
user_units_per_db_unit=uu, meters_per_db_unit=dbu)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
def write(self, stream: IO[bytes]) -> int:
|
||||
"""
|
||||
Write the header to a stream
|
||||
|
||||
@ -80,7 +80,7 @@ class FileHeader:
|
||||
return b
|
||||
|
||||
|
||||
def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
|
||||
def scan_structs(stream: IO[bytes]) -> dict[bytes, int]:
|
||||
"""
|
||||
Scan through a GDS file, building a table of
|
||||
{b'structure_name': byte_offset}.
|
||||
@ -107,7 +107,7 @@ def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
|
||||
return positions
|
||||
|
||||
|
||||
def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
|
||||
def try_read_struct(stream: IO[bytes]) -> tuple[bytes, list[Element]] | None:
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
def write_struct(
|
||||
stream: IO[bytes],
|
||||
name: bytes,
|
||||
elements: list[Element],
|
||||
cre_time: datetime = datetime(1900, 1, 1),
|
||||
mod_time: datetime = datetime(1900, 1, 1),
|
||||
) -> int:
|
||||
"""
|
||||
Write a structure to the provided stream.
|
||||
|
||||
@ -150,7 +151,7 @@ def write_struct(stream: BinaryIO,
|
||||
return b
|
||||
|
||||
|
||||
def read_elements(stream: BinaryIO) -> List[Element]:
|
||||
def read_elements(stream: IO[bytes]) -> list[Element]:
|
||||
"""
|
||||
Read elements from the stream until an ENDSTR
|
||||
record is encountered. The ENDSTR record is also
|
||||
@ -162,7 +163,7 @@ def read_elements(stream: BinaryIO) -> 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:
|
||||
@ -175,9 +176,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
|
||||
data.append(Box.read(stream))
|
||||
elif tag == TEXT.tag:
|
||||
data.append(Text.read(stream))
|
||||
elif tag == SREF.tag:
|
||||
data.append(Reference.read(stream))
|
||||
elif tag == AREF.tag:
|
||||
elif tag in (SREF.tag, AREF.tag):
|
||||
data.append(Reference.read(stream))
|
||||
else:
|
||||
# don't care, skip
|
||||
@ -186,7 +185,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
|
||||
return data
|
||||
|
||||
|
||||
def scan_hierarchy(stream: BinaryIO) -> Dict[bytes, Dict[bytes, int]]:
|
||||
def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]:
|
||||
"""
|
||||
Scan through a GDS file, building a table of instance counts
|
||||
`{b'structure_name': {b'ref_name': count}}`.
|
||||
@ -223,7 +222,7 @@ def scan_hierarchy(stream: BinaryIO) -> Dict[bytes, Dict[bytes, int]]:
|
||||
elif tag == ENDEL.tag:
|
||||
if ref_count is None:
|
||||
ref_count = 1
|
||||
assert(ref_name is not None)
|
||||
assert ref_name is not None
|
||||
cur_structure[ref_name] += ref_count
|
||||
else:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
|
@ -1,14 +1,15 @@
|
||||
"""
|
||||
Generic record-level read/write functionality.
|
||||
"""
|
||||
from typing import Optional, Sequence, BinaryIO
|
||||
from typing import TypeVar, List, Tuple, ClassVar, Type
|
||||
from typing import IO, ClassVar, Self, Generic, TypeVar
|
||||
from collections.abc import Sequence
|
||||
import struct
|
||||
import io
|
||||
from datetime import datetime
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy # type: ignore
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .basic import KlamathError
|
||||
from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
|
||||
@ -17,9 +18,11 @@ 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: BinaryIO, data_size: int, tag: int) -> int:
|
||||
def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int:
|
||||
record_size = data_size + 4
|
||||
if record_size > 0xFFFF:
|
||||
raise KlamathError(f'Record size is too big: {record_size}')
|
||||
@ -27,7 +30,7 @@ def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int:
|
||||
return stream.write(header)
|
||||
|
||||
|
||||
def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
|
||||
def read_record_header(stream: IO[bytes]) -> tuple[int, int]:
|
||||
"""
|
||||
Read a record's header (size and tag).
|
||||
Args:
|
||||
@ -46,49 +49,46 @@ def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
|
||||
return data_size, tag
|
||||
|
||||
|
||||
def expect_record(stream: BinaryIO, tag: int) -> int:
|
||||
def expect_record(stream: IO[bytes], tag: int) -> int:
|
||||
data_size, actual_tag = read_record_header(stream)
|
||||
if tag != actual_tag:
|
||||
raise KlamathError(f'Unexpected record! Got tag 0x{actual_tag:04x}, expected 0x{tag:04x}')
|
||||
return data_size
|
||||
|
||||
|
||||
R = TypeVar('R', bound='Record')
|
||||
|
||||
|
||||
class Record(metaclass=ABCMeta):
|
||||
class Record(Generic[II, OO], metaclass=ABCMeta):
|
||||
tag: ClassVar[int] = -1
|
||||
expected_size: ClassVar[Optional[int]] = None
|
||||
expected_size: ClassVar[int | None] = None
|
||||
|
||||
@classmethod
|
||||
def check_size(cls, size: int):
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
if cls.expected_size is not None and size != cls.expected_size:
|
||||
raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
|
||||
|
||||
@classmethod
|
||||
def check_data(cls, data):
|
||||
@classmethod # noqa: B027 Intentionally non-abstract
|
||||
def check_data(cls: type[Self], data: II) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int):
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> OO:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def pack_data(cls, data) -> bytes:
|
||||
def pack_data(cls: type[Self], data: II) -> bytes:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def read_header(stream: BinaryIO) -> Tuple[int, int]:
|
||||
def read_header(stream: IO[bytes]) -> tuple[int, int]:
|
||||
return read_record_header(stream)
|
||||
|
||||
@classmethod
|
||||
def write_header(cls, stream: BinaryIO, data_size: int) -> int:
|
||||
def write_header(cls: type[Self], stream: IO[bytes], data_size: int) -> int:
|
||||
return write_record_header(stream, data_size, cls.tag)
|
||||
|
||||
@classmethod
|
||||
def skip_past(cls, stream: BinaryIO) -> bool:
|
||||
def skip_past(cls: type[Self], stream: IO[bytes]) -> bool:
|
||||
"""
|
||||
Skip to the end of the next occurence of this record.
|
||||
|
||||
@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def skip_and_read(cls, stream: BinaryIO):
|
||||
def skip_and_read(cls: type[Self], stream: IO[bytes]) -> OO:
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != cls.tag:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
@ -119,90 +119,90 @@ class Record(metaclass=ABCMeta):
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[R], stream: BinaryIO):
|
||||
def read(cls: type[Self], stream: IO[bytes]) -> OO:
|
||||
size = expect_record(stream, cls.tag)
|
||||
data = cls.read_data(stream, size)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def write(cls, stream: BinaryIO, data) -> int:
|
||||
def write(cls: type[Self], stream: IO[bytes], data: II) -> int:
|
||||
data_bytes = cls.pack_data(data)
|
||||
b = cls.write_header(stream, len(data_bytes))
|
||||
b += stream.write(data_bytes)
|
||||
return b
|
||||
|
||||
|
||||
class NoDataRecord(Record):
|
||||
expected_size: ClassVar[Optional[int]] = 0
|
||||
class NoDataRecord(Record[None, None]):
|
||||
expected_size: ClassVar[int | None] = 0
|
||||
|
||||
@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)
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: None) -> bytes:
|
||||
def pack_data(cls: type[Self], data: None) -> bytes:
|
||||
if data is not None:
|
||||
raise KlamathError('?? Packing {data} into NoDataRecord??')
|
||||
raise KlamathError('?? Packing {data!r} into NoDataRecord??')
|
||||
return b''
|
||||
|
||||
|
||||
class BitArrayRecord(Record):
|
||||
expected_size: ClassVar[Optional[int]] = 2
|
||||
class BitArrayRecord(Record[int, int]):
|
||||
expected_size: ClassVar[int | None] = 2
|
||||
|
||||
@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))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: int) -> bytes:
|
||||
def pack_data(cls: type[Self], data: int) -> bytes:
|
||||
return pack_bitarray(data)
|
||||
|
||||
|
||||
class Int2Record(Record):
|
||||
class Int2Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int16]]):
|
||||
@classmethod
|
||||
def read_data(cls, stream: 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))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
return pack_int2(data)
|
||||
|
||||
|
||||
class Int4Record(Record):
|
||||
class Int4Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int32]]):
|
||||
@classmethod
|
||||
def read_data(cls, stream: 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))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
return pack_int4(data)
|
||||
|
||||
|
||||
class Real8Record(Record):
|
||||
class Real8Record(Record[Sequence[float] | float, NDArray[numpy.float64]]):
|
||||
@classmethod
|
||||
def read_data(cls, stream: 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))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
def pack_data(cls: type[Self], data: Sequence[float] | float) -> bytes:
|
||||
return pack_real8(data)
|
||||
|
||||
|
||||
class ASCIIRecord(Record):
|
||||
class ASCIIRecord(Record[bytes, bytes]):
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> bytes:
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> bytes:
|
||||
return parse_ascii(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: bytes) -> bytes:
|
||||
def pack_data(cls: type[Self], data: bytes) -> bytes:
|
||||
return pack_ascii(data)
|
||||
|
||||
|
||||
class DateTimeRecord(Record):
|
||||
class DateTimeRecord(Record[Sequence[datetime], list[datetime]]):
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]:
|
||||
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> list[datetime]:
|
||||
return parse_datetime(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[datetime]) -> bytes:
|
||||
def pack_data(cls: type[Self], data: Sequence[datetime]) -> bytes:
|
||||
return pack_datetime(data)
|
||||
|
@ -1,8 +1,12 @@
|
||||
"""
|
||||
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 ASCIIRecord, DateTimeRecord
|
||||
|
||||
@ -111,7 +115,7 @@ class PRESENTATION(BitArrayRecord):
|
||||
|
||||
|
||||
class SPACING(Int2Record):
|
||||
tag = 0x1802 #Not sure about 02; Unused
|
||||
tag = 0x1802 # Not sure about 02; Unused
|
||||
|
||||
|
||||
class STRING(ASCIIRecord):
|
||||
@ -133,29 +137,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, size: int):
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
if size != 0 and size % 44 != 0:
|
||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
||||
raise KlamathError(f'Expected size to be multiple of 44, got {size}')
|
||||
|
||||
|
||||
class FONTS(ASCIIRecord):
|
||||
tag = 0x2006
|
||||
|
||||
@classmethod
|
||||
def check_size(cls, size: int):
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
if size != 0 and size % 44 != 0:
|
||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
||||
raise KlamathError(f'Expected size to be multiple of 44, got {size}')
|
||||
|
||||
|
||||
class PATHTYPE(Int2Record):
|
||||
@ -168,26 +172,26 @@ class GENERATIONS(Int2Record):
|
||||
expected_size = 2
|
||||
|
||||
@classmethod
|
||||
def check_data(cls, data: Sequence[int]):
|
||||
if len(data) != 1:
|
||||
raise Exception(f'Expected exactly one integer, got {data}')
|
||||
def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
|
||||
if not isinstance(data, Sized) or len(data) != 1:
|
||||
raise KlamathError(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
||||
class ATTRTABLE(ASCIIRecord):
|
||||
tag = 0x2306
|
||||
|
||||
@classmethod
|
||||
def check_size(cls, size: int):
|
||||
def check_size(cls: type[Self], size: int) -> None:
|
||||
if size > 44:
|
||||
raise Exception(f'Expected size <= 44, got {size}')
|
||||
raise KlamathError(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):
|
||||
@ -266,9 +270,9 @@ class FORMAT(Int2Record):
|
||||
expected_size = 2
|
||||
|
||||
@classmethod
|
||||
def check_data(cls, data: Sequence[int]):
|
||||
if len(data) != 1:
|
||||
raise Exception(f'Expected exactly one integer, got {data}')
|
||||
def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
|
||||
if not isinstance(data, Sized) or len(data) != 1:
|
||||
raise KlamathError(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
||||
class MASK(ASCIIRecord):
|
||||
|
@ -1,20 +1,21 @@
|
||||
import struct
|
||||
|
||||
import pytest # type: ignore
|
||||
import numpy # type: ignore
|
||||
from numpy.testing import assert_array_equal # type: ignore
|
||||
import numpy
|
||||
from datetime import datetime
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
from .basic import parse_bitarray, parse_int2, parse_int4, parse_real8, parse_ascii
|
||||
from .basic import pack_bitarray, pack_int2, pack_int4, pack_real8, pack_ascii
|
||||
from .basic import decode_real8, encode_real8
|
||||
from .basic import decode_real8, encode_real8, parse_datetime
|
||||
|
||||
from .basic import KlamathError
|
||||
|
||||
|
||||
def test_parse_bitarray():
|
||||
assert(parse_bitarray(b'59') == 13625)
|
||||
assert(parse_bitarray(b'\0\0') == 0)
|
||||
assert(parse_bitarray(b'\xff\xff') == 65535)
|
||||
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
|
||||
|
||||
# 4 bytes (too long)
|
||||
with pytest.raises(KlamathError):
|
||||
@ -25,7 +26,7 @@ def test_parse_bitarray():
|
||||
parse_bitarray(b'')
|
||||
|
||||
|
||||
def test_parse_int2():
|
||||
def test_parse_int2() -> None:
|
||||
assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
|
||||
|
||||
# odd length
|
||||
@ -37,7 +38,7 @@ def test_parse_int2():
|
||||
parse_int2(b'')
|
||||
|
||||
|
||||
def test_parse_int4():
|
||||
def test_parse_int4() -> None:
|
||||
assert_array_equal(parse_int4(b'4321'), (875770417,))
|
||||
|
||||
# length % 4 != 0
|
||||
@ -49,17 +50,17 @@ def test_parse_int4():
|
||||
parse_int4(b'')
|
||||
|
||||
|
||||
def test_decode_real8():
|
||||
def test_decode_real8() -> None:
|
||||
# zeroes
|
||||
assert(decode_real8(numpy.array([0x0])) == 0)
|
||||
assert(decode_real8(numpy.array([1<<63])) == 0) # negative
|
||||
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():
|
||||
def test_parse_real8() -> None:
|
||||
packed = struct.pack('>3Q', 0x0, 0x4110_0000_0000_0000, 0xC120_0000_0000_0000)
|
||||
assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0))
|
||||
|
||||
@ -72,48 +73,59 @@ def test_parse_real8():
|
||||
parse_real8(b'')
|
||||
|
||||
|
||||
def test_parse_ascii():
|
||||
# # empty data Now allowed!
|
||||
# with pytest.raises(KlamathError):
|
||||
# parse_ascii(b'')
|
||||
def test_parse_ascii() -> None:
|
||||
# # 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():
|
||||
def test_pack_bitarray() -> None:
|
||||
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():
|
||||
def test_pack_int2() -> None:
|
||||
packed = pack_int2((3, 2, 1))
|
||||
assert(len(packed) == 3*2)
|
||||
assert(packed == struct.pack('>3h', 3, 2, 1))
|
||||
assert(pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1))
|
||||
assert len(packed) == 3 * 2
|
||||
assert packed == struct.pack('>3h', 3, 2, 1)
|
||||
assert pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1)
|
||||
|
||||
|
||||
def test_pack_int4():
|
||||
def test_pack_int4() -> None:
|
||||
packed = pack_int4((3, 2, 1))
|
||||
assert(len(packed) == 3*4)
|
||||
assert(packed == struct.pack('>3l', 3, 2, 1))
|
||||
assert(pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1))
|
||||
assert len(packed) == 3 * 4
|
||||
assert packed == struct.pack('>3l', 3, 2, 1)
|
||||
assert pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1)
|
||||
|
||||
|
||||
def test_encode_real8():
|
||||
assert(encode_real8(numpy.array([0.0])) == 0)
|
||||
def test_encode_real8() -> None:
|
||||
assert encode_real8(numpy.array([0.0])) == 0
|
||||
arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12))
|
||||
assert_array_equal(decode_real8(encode_real8(arr)), arr)
|
||||
|
||||
|
||||
def test_pack_real8():
|
||||
def test_pack_real8() -> None:
|
||||
reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12)
|
||||
packed = pack_real8(reals)
|
||||
assert(len(packed) == len(reals) * 8)
|
||||
assert len(packed) == len(reals) * 8
|
||||
assert_array_equal(parse_real8(packed), reals)
|
||||
|
||||
|
||||
def test_pack_ascii():
|
||||
assert(pack_ascii(b'4321') == b'4321')
|
||||
assert(pack_ascii(b'321') == b'321\0')
|
||||
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
|
||||
|
92
pyproject.toml
Normal file
92
pyproject.toml
Normal 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
|
||||
]
|
||||
|
63
setup.py
63
setup.py
@ -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',
|
||||
],
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user