diff --git a/.gitignore b/.gitignore index 71e10d7..6e1d4a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.pyc -__pycache__/ +__pycache__ *.idea @@ -7,7 +7,6 @@ build/ dist/ *.egg-info/ .mypy_cache/ -.pytest_cache/ *.swp *.swo diff --git a/README.md b/README.md index 4351171..e65fb24 100644 --- a/README.md +++ b/README.md @@ -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() diff --git a/klamath/LICENSE.md b/klamath/LICENSE.md deleted file mode 120000 index 7eabdb1..0000000 --- a/klamath/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ -../LICENSE.md \ No newline at end of file diff --git a/klamath/README.md b/klamath/README.md deleted file mode 120000 index 32d46ee..0000000 --- a/klamath/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/klamath/VERSION.py b/klamath/VERSION.py new file mode 100644 index 0000000..b99a6d4 --- /dev/null +++ b/klamath/VERSION.py @@ -0,0 +1,4 @@ +""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ +__version__ = ''' +1.2 +'''.strip() diff --git a/klamath/__init__.py b/klamath/__init__.py index 5a315b4..7a8c54e 100644 --- a/klamath/__init__.py +++ b/klamath/__init__.py @@ -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' - diff --git a/klamath/basic.py b/klamath/basic.py index e880393..fd1b417 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -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: diff --git a/klamath/elements.py b/klamath/elements.py index c0d5567..a477642 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -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) diff --git a/klamath/library.py b/klamath/library.py index 8b3ab20..1318854 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -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) diff --git a/klamath/record.py b/klamath/record.py index f5fce86..e6fb5fb 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -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) diff --git a/klamath/records.py b/klamath/records.py index b05bb04..3d3e838 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -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): diff --git a/klamath/test_basic.py b/klamath/test_basic.py index 287f7a9..058c808 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -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') diff --git a/klamath/test_elements.py b/klamath/test_elements.py deleted file mode 100644 index ae3df18..0000000 --- a/klamath/test_elements.py +++ /dev/null @@ -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) diff --git a/klamath/test_library.py b/klamath/test_library.py deleted file mode 100644 index a27c65c..0000000 --- a/klamath/test_library.py +++ /dev/null @@ -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) diff --git a/klamath/test_record.py b/klamath/test_record.py deleted file mode 100644 index 99a5a1f..0000000 --- a/klamath/test_record.py +++ /dev/null @@ -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) diff --git a/klamath/test_records.py b/klamath/test_records.py deleted file mode 100644 index fd07b44..0000000 --- a/klamath/test_records.py +++ /dev/null @@ -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 diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 5ca097d..0000000 --- a/pyproject.toml +++ /dev/null @@ -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 - ] - diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..732b4c9 --- /dev/null +++ b/setup.py @@ -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', + ], + )