diff --git a/README.md b/README.md index e65fb24..65263e1 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The goal is to keep this library simple: ## Installation Requirements: -* python >= 3.7 (written and tested with 3.8) +* python >= 3.10 (written and tested with 3.11) * numpy diff --git a/klamath/basic.py b/klamath/basic.py index 3a319e9..5a15714 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -1,20 +1,21 @@ """ Functionality for encoding/decoding basic datatypes """ -from typing import Sequence, IO, List +from typing import Sequence, IO import struct from datetime import datetime -import numpy # type: ignore +import numpy +from numpy.typing import NDArray class KlamathError(Exception): pass -""" -Parse functions -""" +# +# Parse functions +# def parse_bitarray(data: bytes) -> int: if len(data) != 2: raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.') @@ -22,21 +23,21 @@ def parse_bitarray(data: bytes) -> int: return val -def parse_int2(data: bytes) -> numpy.ndarray: +def parse_int2(data: bytes) -> NDArray[numpy.int16]: data_len = len(data) if data_len == 0 or (data_len % 2) != 0: raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.') return numpy.frombuffer(data, dtype='>i2', count=data_len // 2) -def parse_int4(data: bytes) -> numpy.ndarray: +def parse_int4(data: bytes) -> NDArray[numpy.int32]: data_len = len(data) if data_len == 0 or (data_len % 4) != 0: raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.') return numpy.frombuffer(data, dtype='>i4', count=data_len // 4) -def decode_real8(nums: numpy.ndarray) -> numpy.ndarray: +def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]: """ Convert GDS REAL8 data to IEEE float64. """ nums = nums.astype(numpy.uint64) neg = nums & 0x8000_0000_0000_0000 @@ -46,7 +47,7 @@ def decode_real8(nums: numpy.ndarray) -> numpy.ndarray: return numpy.ldexp(mant, (4 * (exp - 64) - 56).astype(numpy.int64)) -def parse_real8(data: bytes) -> numpy.ndarray: +def parse_real8(data: bytes) -> NDArray[numpy.float64]: data_len = len(data) if data_len == 0 or (data_len % 8) != 0: raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.') @@ -62,7 +63,7 @@ def parse_ascii(data: bytes) -> bytes: return data -def parse_datetime(data: bytes) -> List[datetime]: +def parse_datetime(data: bytes) -> list[datetime]: """ Parse date/time data (12 byte blocks) """ if len(data) == 0 or len(data) % 12 != 0: raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.') @@ -73,9 +74,9 @@ def parse_datetime(data: bytes) -> List[datetime]: return dts -""" -Pack functions -""" +# +# Pack functions +# def pack_bitarray(data: int) -> bytes: if data > 65535 or data < 0: raise KlamathError(f'bitarray data out of range: {data}') @@ -96,7 +97,7 @@ def pack_int4(data: Sequence[int]) -> bytes: return arr.astype('>i4').tobytes() -def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray: +def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]: """ Convert from float64 to GDS REAL8 representation. """ # Split the ieee float bitfields ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64)) @@ -151,7 +152,7 @@ def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray: real8[zero] = 0 real8[gds_exp < -14] = 0 # number is too small - return real8 + return real8.astype(numpy.uint64, copy=False) def pack_real8(data: Sequence[float]) -> bytes: diff --git a/klamath/elements.py b/klamath/elements.py index b29cfe2..ba4098f 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -2,11 +2,12 @@ Functionality for reading/writing elements (geometry, text labels, structure references) and associated properties. """ -from typing import Dict, Tuple, Optional, IO, TypeVar, Type, Union +from typing import Optional, IO, TypeVar, Type, Union from abc import ABCMeta, abstractmethod from dataclasses import dataclass -import numpy # type: ignore +import numpy +from numpy.typing import NDArray from .basic import KlamathError from .record import Record @@ -28,7 +29,7 @@ T = TypeVar('T', bound='Text') X = TypeVar('X', bound='Box') -def read_properties(stream: IO[bytes]) -> Dict[int, bytes]: +def read_properties(stream: IO[bytes]) -> dict[int, bytes]: """ Read element properties. @@ -55,7 +56,7 @@ def read_properties(stream: IO[bytes]) -> Dict[int, bytes]: return properties -def write_properties(stream: IO[bytes], properties: Dict[int, bytes]) -> int: +def write_properties(stream: IO[bytes], properties: dict[int, bytes]) -> int: """ Write element properties. @@ -130,7 +131,7 @@ class Reference(Element): angle_deg: float """ Rotation (degrees counterclockwise) """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ (For SREF) Location in the parent structure corresponding to the instance's origin (0, 0). (For AREF) 3 locations: @@ -143,10 +144,10 @@ class Reference(Element): basis vectors to match it. """ - colrow: Optional[Union[Tuple[int, int], numpy.ndarray]] + colrow: tuple[int, int] | NDArray[numpy.int16] | None """ Number of columns and rows (AREF) or None (SREF) """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties associated with this reference. """ @classmethod @@ -173,8 +174,15 @@ class Reference(Element): size, tag = Record.read_header(stream) xy = XY.read_data(stream, size).reshape(-1, 2) properties = read_properties(stream) - return cls(struct_name=struct_name, xy=xy, properties=properties, colrow=colrow, - invert_y=invert_y, mag=mag, angle_deg=angle_deg) + return cls( + struct_name=struct_name, + xy=xy, + properties=properties, + colrow=colrow, + invert_y=invert_y, + mag=mag, + angle_deg=angle_deg, + ) def write(self, stream: IO[bytes]) -> int: b = 0 @@ -215,13 +223,13 @@ class Boundary(Element): """ __slots__ = ('layer', 'xy', 'properties') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, data_type) tuple """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Ordered vertices of the shape. First and last points should be identical. """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -252,7 +260,7 @@ class Path(Element): """ __slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, data_type) tuple """ path_type: int @@ -261,13 +269,13 @@ class Path(Element): width: int """ Path width """ - extension: Tuple[int, int] + extension: tuple[int, int] """ Extension when using path_type=4. Ignored otherwise. """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Path centerline coordinates """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -326,13 +334,13 @@ class Box(Element): """ __slots__ = ('layer', 'xy', 'properties') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, box_type) tuple """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Box coordinates (5 pairs) """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -360,13 +368,13 @@ class Node(Element): """ __slots__ = ('layer', 'xy', 'properties') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, node_type) tuple """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ 1-50 pairs of coordinates. """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod @@ -395,7 +403,7 @@ class Text(Element): __slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type', 'width', 'invert_y', 'mag', 'angle_deg', 'string') - layer: Tuple[int, int] + layer: tuple[int, int] """ (layer, node_type) tuple """ presentation: int @@ -420,13 +428,13 @@ class Text(Element): angle_deg: float """ Rotation (ccw). Default 0. """ - xy: numpy.ndarray + xy: NDArray[numpy.int32] """ Position (1 pair only) """ string: bytes """ Text content """ - properties: Dict[int, bytes] + properties: dict[int, bytes] """ Properties for the element. """ @classmethod diff --git a/klamath/library.py b/klamath/library.py index be9693b..239609d 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -1,7 +1,7 @@ """ File-level read/write functionality. """ -from typing import List, Dict, Tuple, Optional, IO, TypeVar, Type, MutableMapping +from typing import IO, TypeVar, Type, MutableMapping import io from datetime import datetime from dataclasses import dataclass @@ -80,7 +80,7 @@ class FileHeader: return b -def scan_structs(stream: IO[bytes]) -> Dict[bytes, int]: +def scan_structs(stream: IO[bytes]) -> dict[bytes, int]: """ Scan through a GDS file, building a table of {b'structure_name': byte_offset}. @@ -107,7 +107,7 @@ def scan_structs(stream: IO[bytes]) -> Dict[bytes, int]: return positions -def try_read_struct(stream: IO[bytes]) -> Optional[Tuple[bytes, List[Element]]]: +def try_read_struct(stream: IO[bytes]) -> tuple[bytes, list[Element]] | None: """ Skip to the next structure and attempt to read it. @@ -125,12 +125,13 @@ def try_read_struct(stream: IO[bytes]) -> Optional[Tuple[bytes, List[Element]]]: return name, elements -def write_struct(stream: IO[bytes], - name: bytes, - elements: List[Element], - cre_time: datetime = datetime(1900, 1, 1), - mod_time: datetime = datetime(1900, 1, 1), - ) -> int: +def write_struct( + stream: IO[bytes], + name: bytes, + elements: list[Element], + cre_time: datetime = datetime(1900, 1, 1), + mod_time: datetime = datetime(1900, 1, 1), + ) -> int: """ Write a structure to the provided stream. @@ -150,7 +151,7 @@ def write_struct(stream: IO[bytes], return b -def read_elements(stream: IO[bytes]) -> List[Element]: +def read_elements(stream: IO[bytes]) -> list[Element]: """ Read elements from the stream until an ENDSTR record is encountered. The ENDSTR record is also @@ -162,7 +163,7 @@ def read_elements(stream: IO[bytes]) -> List[Element]: Returns: List of element objects. """ - data: List[Element] = [] + data: list[Element] = [] size, tag = Record.read_header(stream) while tag != ENDSTR.tag: if tag == BOUNDARY.tag: @@ -186,7 +187,7 @@ def read_elements(stream: IO[bytes]) -> List[Element]: return data -def scan_hierarchy(stream: IO[bytes]) -> Dict[bytes, Dict[bytes, int]]: +def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]: """ Scan through a GDS file, building a table of instance counts `{b'structure_name': {b'ref_name': count}}`. diff --git a/klamath/record.py b/klamath/record.py index 69769dd..c0b4a6c 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -1,14 +1,14 @@ """ Generic record-level read/write functionality. """ -from typing import Optional, Sequence, IO -from typing import TypeVar, List, Tuple, ClassVar, Type +from typing import Sequence, IO, TypeVar, ClassVar, Type import struct import io from datetime import datetime from abc import ABCMeta, abstractmethod -import numpy # type: ignore +import numpy +from numpy.typing import NDArray from .basic import KlamathError from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray @@ -27,7 +27,7 @@ def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int: return stream.write(header) -def read_record_header(stream: IO[bytes]) -> Tuple[int, int]: +def read_record_header(stream: IO[bytes]) -> tuple[int, int]: """ Read a record's header (size and tag). Args: @@ -58,7 +58,7 @@ R = TypeVar('R', bound='Record') class Record(metaclass=ABCMeta): tag: ClassVar[int] = -1 - expected_size: ClassVar[Optional[int]] = None + expected_size: ClassVar[int | None] = None @classmethod def check_size(cls, size: int): @@ -80,7 +80,7 @@ class Record(metaclass=ABCMeta): pass @staticmethod - def read_header(stream: IO[bytes]) -> Tuple[int, int]: + def read_header(stream: IO[bytes]) -> tuple[int, int]: return read_record_header(stream) @classmethod @@ -133,7 +133,7 @@ class Record(metaclass=ABCMeta): class NoDataRecord(Record): - expected_size: ClassVar[Optional[int]] = 0 + expected_size: ClassVar[int | None] = 0 @classmethod def read_data(cls, stream: IO[bytes], size: int) -> None: @@ -147,7 +147,7 @@ class NoDataRecord(Record): class BitArrayRecord(Record): - expected_size: ClassVar[Optional[int]] = 2 + expected_size: ClassVar[int | None] = 2 @classmethod def read_data(cls, stream: IO[bytes], size: int) -> int: @@ -160,7 +160,7 @@ class BitArrayRecord(Record): class Int2Record(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int16]: return parse_int2(read(stream, size)) @classmethod @@ -170,7 +170,7 @@ class Int2Record(Record): class Int4Record(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int32]: return parse_int4(read(stream, size)) @classmethod @@ -180,7 +180,7 @@ class Int4Record(Record): class Real8Record(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> numpy.ndarray: + def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.float64]: return parse_real8(read(stream, size)) @classmethod @@ -200,7 +200,7 @@ class ASCIIRecord(Record): class DateTimeRecord(Record): @classmethod - def read_data(cls, stream: IO[bytes], size: int) -> List[datetime]: + def read_data(cls, stream: IO[bytes], size: int) -> list[datetime]: return parse_datetime(read(stream, size)) @classmethod diff --git a/klamath/test_basic.py b/klamath/test_basic.py index ae284b6..f15fe8a 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -1,8 +1,8 @@ import struct import pytest # type: ignore -import numpy # type: ignore -from numpy.testing import assert_array_equal # type: ignore +import numpy +from numpy.testing import assert_array_equal from .basic import parse_bitarray, parse_int2, parse_int4, parse_real8, parse_ascii from .basic import pack_bitarray, pack_int2, pack_int4, pack_real8, pack_ascii