Compare commits

..

15 Commits

10 changed files with 133 additions and 124 deletions

30
.flake8
View File

@ -1,30 +0,0 @@
[flake8]
ignore =
# E501 line too long
E501,
# W391 newlines at EOF
W391,
# E241 multiple spaces after comma
E241,
# E302 expected 2 newlines
E302,
# W503 line break before binary operator (to be deprecated)
W503,
# E265 block comment should start with '# '
E265,
# E123 closing bracket does not match indentation of opening bracket's line
E123,
# E124 closing bracket does not match visual indentation
E124,
# E221 multiple spaces before operator
E221,
# E201 whitespace after '['
E201,
# # E741 ambiguous variable name 'I'
# E741,
per-file-ignores =
# F401 import without use
*/__init__.py: F401,
__init__.py: F401,

View File

@ -50,7 +50,7 @@ The goal is to keep this library simple:
## Installation ## Installation
Requirements: Requirements:
* python >= 3.10 (written and tested with 3.11) * python >= 3.11
* numpy * numpy

View File

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

View File

@ -1,7 +1,8 @@
""" """
Functionality for encoding/decoding basic datatypes Functionality for encoding/decoding basic datatypes
""" """
from typing import Sequence, IO from typing import IO
from collections.abc import Sequence
import struct import struct
import logging import logging
from datetime import datetime from datetime import datetime
@ -92,15 +93,15 @@ def pack_bitarray(data: int) -> bytes:
return struct.pack('>H', data) return struct.pack('>H', data)
def pack_int2(data: Sequence[int]) -> bytes: def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
arr = numpy.array(data) arr = numpy.asarray(data)
if (arr > 32767).any() or (arr < -32768).any(): if (arr > 32767).any() or (arr < -32768).any():
raise KlamathError(f'int2 data out of range: {arr}') raise KlamathError(f'int2 data out of range: {arr}')
return arr.astype('>i2').tobytes() return arr.astype('>i2').tobytes()
def pack_int4(data: Sequence[int]) -> bytes: def pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
arr = numpy.array(data) arr = numpy.asarray(data)
if (arr > 2147483647).any() or (arr < -2147483648).any(): if (arr > 2147483647).any() or (arr < -2147483648).any():
raise KlamathError(f'int4 data out of range: {arr}') raise KlamathError(f'int4 data out of range: {arr}')
return arr.astype('>i4').tobytes() return arr.astype('>i4').tobytes()
@ -164,8 +165,8 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
return real8.astype(numpy.uint64, copy=False) return real8.astype(numpy.uint64, copy=False)
def pack_real8(data: Sequence[float]) -> bytes: def pack_real8(data: NDArray[numpy.floating] | Sequence[float] | float) -> bytes:
return encode_real8(numpy.array(data)).astype('>u8').tobytes() return encode_real8(numpy.asarray(data)).astype('>u8').tobytes()
def pack_ascii(data: bytes) -> bytes: def pack_ascii(data: bytes) -> bytes:

View File

@ -2,7 +2,7 @@
Functionality for reading/writing elements (geometry, text labels, Functionality for reading/writing elements (geometry, text labels,
structure references) and associated properties. structure references) and associated properties.
""" """
from typing import Optional, IO, TypeVar, Type, Union from typing import IO, TypeVar
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
@ -78,7 +78,7 @@ class Element(metaclass=ABCMeta):
""" """
@classmethod @classmethod
@abstractmethod @abstractmethod
def read(cls: Type[E], stream: IO[bytes]) -> E: def read(cls: type[E], stream: IO[bytes]) -> E:
""" """
Read from a stream to construct this object. Read from a stream to construct this object.
Consumes up to (and including) the ENDEL record. Consumes up to (and including) the ENDEL record.
@ -151,7 +151,7 @@ class Reference(Element):
""" Properties associated with this reference. """ """ Properties associated with this reference. """
@classmethod @classmethod
def read(cls: Type[R], stream: IO[bytes]) -> R: def read(cls: type[R], stream: IO[bytes]) -> R:
invert_y = False invert_y = False
mag = 1 mag = 1
angle_deg = 0 angle_deg = 0
@ -211,7 +211,7 @@ class Reference(Element):
if self.colrow is not None: if self.colrow is not None:
if self.xy.size != 6: if self.xy.size != 6:
raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}') raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
else: else: # noqa: PLR5501
if self.xy.size != 2: if self.xy.size != 2:
raise KlamathError(f'Expected size-2 xy. Got {self.xy}') raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
@ -233,7 +233,7 @@ class Boundary(Element):
""" Properties for the element. """ """ Properties for the element. """
@classmethod @classmethod
def read(cls: Type[B], stream: IO[bytes]) -> B: def read(cls: type[B], stream: IO[bytes]) -> B:
layer = LAYER.skip_and_read(stream)[0] layer = LAYER.skip_and_read(stream)[0]
dtype = DATATYPE.read(stream)[0] dtype = DATATYPE.read(stream)[0]
xy = XY.read(stream).reshape(-1, 2) xy = XY.read(stream).reshape(-1, 2)
@ -279,7 +279,7 @@ class Path(Element):
""" Properties for the element. """ """ Properties for the element. """
@classmethod @classmethod
def read(cls: Type[P], stream: IO[bytes]) -> P: def read(cls: type[P], stream: IO[bytes]) -> P:
path_type = 0 path_type = 0
width = 0 width = 0
bgn_ext = 0 bgn_ext = 0
@ -344,7 +344,7 @@ class Box(Element):
""" Properties for the element. """ """ Properties for the element. """
@classmethod @classmethod
def read(cls: Type[X], stream: IO[bytes]) -> X: def read(cls: type[X], stream: IO[bytes]) -> X:
layer = LAYER.skip_and_read(stream)[0] layer = LAYER.skip_and_read(stream)[0]
dtype = BOXTYPE.read(stream)[0] dtype = BOXTYPE.read(stream)[0]
xy = XY.read(stream).reshape(-1, 2) xy = XY.read(stream).reshape(-1, 2)
@ -378,7 +378,7 @@ class Node(Element):
""" Properties for the element. """ """ Properties for the element. """
@classmethod @classmethod
def read(cls: Type[N], stream: IO[bytes]) -> N: def read(cls: type[N], stream: IO[bytes]) -> N:
layer = LAYER.skip_and_read(stream)[0] layer = LAYER.skip_and_read(stream)[0]
dtype = NODETYPE.read(stream)[0] dtype = NODETYPE.read(stream)[0]
xy = XY.read(stream).reshape(-1, 2) xy = XY.read(stream).reshape(-1, 2)
@ -438,7 +438,7 @@ class Text(Element):
""" Properties for the element. """ """ Properties for the element. """
@classmethod @classmethod
def read(cls: Type[T], stream: IO[bytes]) -> T: def read(cls: type[T], stream: IO[bytes]) -> T:
path_type = 0 path_type = 0
presentation = 0 presentation = 0
invert_y = False invert_y = False

View File

@ -1,7 +1,7 @@
""" """
File-level read/write functionality. File-level read/write functionality.
""" """
from typing import IO, TypeVar, Type, MutableMapping from typing import IO, Self, TYPE_CHECKING
import io import io
from datetime import datetime from datetime import datetime
from dataclasses import dataclass from dataclasses import dataclass
@ -15,8 +15,8 @@ from .records import BGNSTR, STRNAME, ENDSTR, SNAME, COLROW, ENDEL
from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
from .elements import Element, Reference, Text, Box, Boundary, Path, Node from .elements import Element, Reference, Text, Box, Boundary, Path, Node
if TYPE_CHECKING:
FH = TypeVar('FH', bound='FileHeader') from collections.abc import MutableMapping
@dataclass @dataclass
@ -45,7 +45,7 @@ class FileHeader:
""" Last-accessed time """ """ Last-accessed time """
@classmethod @classmethod
def read(cls: Type[FH], stream: IO[bytes]) -> FH: def read(cls: type[Self], stream: IO[bytes]) -> Self:
""" """
Read and construct a header from the provided stream. Read and construct a header from the provided stream.
@ -176,9 +176,7 @@ def read_elements(stream: IO[bytes]) -> list[Element]:
data.append(Box.read(stream)) data.append(Box.read(stream))
elif tag == TEXT.tag: elif tag == TEXT.tag:
data.append(Text.read(stream)) data.append(Text.read(stream))
elif tag == SREF.tag: elif tag in (SREF.tag, AREF.tag):
data.append(Reference.read(stream))
elif tag == AREF.tag:
data.append(Reference.read(stream)) data.append(Reference.read(stream))
else: else:
# don't care, skip # don't care, skip

View File

@ -1,7 +1,8 @@
""" """
Generic record-level read/write functionality. Generic record-level read/write functionality.
""" """
from typing import Sequence, IO, TypeVar, ClassVar, Type from typing import IO, ClassVar, Self, Generic, TypeVar
from collections.abc import Sequence
import struct import struct
import io import io
from datetime import datetime from datetime import datetime
@ -17,6 +18,8 @@ from .basic import parse_ascii, pack_ascii, read
_RECORD_HEADER_FMT = struct.Struct('>HH') _RECORD_HEADER_FMT = struct.Struct('>HH')
II = TypeVar('II') # Input type
OO = TypeVar('OO') # Output type
def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int: def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int:
@ -53,30 +56,27 @@ def expect_record(stream: IO[bytes], tag: int) -> int:
return data_size return data_size
R = TypeVar('R', bound='Record') class Record(Generic[II, OO], metaclass=ABCMeta):
class Record(metaclass=ABCMeta):
tag: ClassVar[int] = -1 tag: ClassVar[int] = -1
expected_size: ClassVar[int | None] = None expected_size: ClassVar[int | None] = None
@classmethod @classmethod
def check_size(cls, size: int): def check_size(cls: type[Self], size: int) -> None:
if cls.expected_size is not None and size != cls.expected_size: if cls.expected_size is not None and size != cls.expected_size:
raise KlamathError(f'Expected size {cls.expected_size}, got {size}') raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
@classmethod @classmethod # noqa: B027 Intentionally non-abstract
def check_data(cls, data): def check_data(cls: type[Self], data: II) -> None:
pass pass
@classmethod @classmethod
@abstractmethod @abstractmethod
def read_data(cls, stream: IO[bytes], size: int): def read_data(cls: type[Self], stream: IO[bytes], size: int) -> OO:
pass pass
@classmethod @classmethod
@abstractmethod @abstractmethod
def pack_data(cls, data) -> bytes: def pack_data(cls: type[Self], data: II) -> bytes:
pass pass
@staticmethod @staticmethod
@ -84,11 +84,11 @@ class Record(metaclass=ABCMeta):
return read_record_header(stream) return read_record_header(stream)
@classmethod @classmethod
def write_header(cls, stream: IO[bytes], data_size: int) -> int: def write_header(cls: type[Self], stream: IO[bytes], data_size: int) -> int:
return write_record_header(stream, data_size, cls.tag) return write_record_header(stream, data_size, cls.tag)
@classmethod @classmethod
def skip_past(cls, stream: IO[bytes]) -> bool: def skip_past(cls: type[Self], stream: IO[bytes]) -> bool:
""" """
Skip to the end of the next occurence of this record. Skip to the end of the next occurence of this record.
@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta):
return True return True
@classmethod @classmethod
def skip_and_read(cls, stream: IO[bytes]): def skip_and_read(cls: type[Self], stream: IO[bytes]) -> OO:
size, tag = Record.read_header(stream) size, tag = Record.read_header(stream)
while tag != cls.tag: while tag != cls.tag:
stream.seek(size, io.SEEK_CUR) stream.seek(size, io.SEEK_CUR)
@ -119,90 +119,90 @@ class Record(metaclass=ABCMeta):
return data return data
@classmethod @classmethod
def read(cls: Type[R], stream: IO[bytes]): def read(cls: type[Self], stream: IO[bytes]) -> OO:
size = expect_record(stream, cls.tag) size = expect_record(stream, cls.tag)
data = cls.read_data(stream, size) data = cls.read_data(stream, size)
return data return data
@classmethod @classmethod
def write(cls, stream: IO[bytes], data) -> int: def write(cls: type[Self], stream: IO[bytes], data: II) -> int:
data_bytes = cls.pack_data(data) data_bytes = cls.pack_data(data)
b = cls.write_header(stream, len(data_bytes)) b = cls.write_header(stream, len(data_bytes))
b += stream.write(data_bytes) b += stream.write(data_bytes)
return b return b
class NoDataRecord(Record): class NoDataRecord(Record[None, None]):
expected_size: ClassVar[int | None] = 0 expected_size: ClassVar[int | None] = 0
@classmethod @classmethod
def read_data(cls, stream: IO[bytes], size: int) -> None: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> None:
stream.read(size) stream.read(size)
@classmethod @classmethod
def pack_data(cls, data: None) -> bytes: def pack_data(cls: type[Self], data: None) -> bytes:
if data is not None: if data is not None:
raise KlamathError('?? Packing {data} into NoDataRecord??') raise KlamathError('?? Packing {data} into NoDataRecord??')
return b'' return b''
class BitArrayRecord(Record): class BitArrayRecord(Record[int, int]):
expected_size: ClassVar[int | None] = 2 expected_size: ClassVar[int | None] = 2
@classmethod @classmethod
def read_data(cls, stream: IO[bytes], size: int) -> int: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> int: # noqa: ARG003 size unused
return parse_bitarray(read(stream, 2)) return parse_bitarray(read(stream, 2))
@classmethod @classmethod
def pack_data(cls, data: int) -> bytes: def pack_data(cls: type[Self], data: int) -> bytes:
return pack_bitarray(data) return pack_bitarray(data)
class Int2Record(Record): class Int2Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int16]]):
@classmethod @classmethod
def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int16]: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int16]:
return parse_int2(read(stream, size)) return parse_int2(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: Sequence[int]) -> bytes: def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
return pack_int2(data) return pack_int2(data)
class Int4Record(Record): class Int4Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int32]]):
@classmethod @classmethod
def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int32]: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int32]:
return parse_int4(read(stream, size)) return parse_int4(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: Sequence[int]) -> bytes: def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
return pack_int4(data) return pack_int4(data)
class Real8Record(Record): class Real8Record(Record[Sequence[float] | float, NDArray[numpy.float64]]):
@classmethod @classmethod
def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.float64]: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.float64]:
return parse_real8(read(stream, size)) return parse_real8(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: Sequence[int]) -> bytes: def pack_data(cls: type[Self], data: Sequence[float] | float) -> bytes:
return pack_real8(data) return pack_real8(data)
class ASCIIRecord(Record): class ASCIIRecord(Record[bytes, bytes]):
@classmethod @classmethod
def read_data(cls, stream: IO[bytes], size: int) -> bytes: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> bytes:
return parse_ascii(read(stream, size)) return parse_ascii(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: bytes) -> bytes: def pack_data(cls: type[Self], data: bytes) -> bytes:
return pack_ascii(data) return pack_ascii(data)
class DateTimeRecord(Record): class DateTimeRecord(Record[Sequence[datetime], list[datetime]]):
@classmethod @classmethod
def read_data(cls, stream: IO[bytes], size: int) -> list[datetime]: def read_data(cls: type[Self], stream: IO[bytes], size: int) -> list[datetime]:
return parse_datetime(read(stream, size)) return parse_datetime(read(stream, size))
@classmethod @classmethod
def pack_data(cls, data: Sequence[datetime]) -> bytes: def pack_data(cls: type[Self], data: Sequence[datetime]) -> bytes:
return pack_datetime(data) return pack_datetime(data)

View File

@ -1,8 +1,12 @@
""" """
Record type and tag definitions Record type and tag definitions
""" """
from typing import Sequence from typing import Self
from collections.abc import Sequence, Sized
import numpy
from numpy.typing import NDArray
from .basic import KlamathError
from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record
from .record import ASCIIRecord, DateTimeRecord from .record import ASCIIRecord, DateTimeRecord
@ -144,18 +148,18 @@ class REFLIBS(ASCIIRecord):
tag = 0x1f06 tag = 0x1f06
@classmethod @classmethod
def check_size(cls, size: int): def check_size(cls: type[Self], size: int) -> None:
if size != 0 and size % 44 != 0: if size != 0 and size % 44 != 0:
raise Exception(f'Expected size to be multiple of 44, got {size}') raise KlamathError(f'Expected size to be multiple of 44, got {size}')
class FONTS(ASCIIRecord): class FONTS(ASCIIRecord):
tag = 0x2006 tag = 0x2006
@classmethod @classmethod
def check_size(cls, size: int): def check_size(cls: type[Self], size: int) -> None:
if size != 0 and size % 44 != 0: if size != 0 and size % 44 != 0:
raise Exception(f'Expected size to be multiple of 44, got {size}') raise KlamathError(f'Expected size to be multiple of 44, got {size}')
class PATHTYPE(Int2Record): class PATHTYPE(Int2Record):
@ -168,18 +172,18 @@ class GENERATIONS(Int2Record):
expected_size = 2 expected_size = 2
@classmethod @classmethod
def check_data(cls, data: Sequence[int]): def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
if len(data) != 1: if not isinstance(data, Sized) or len(data) != 1:
raise Exception(f'Expected exactly one integer, got {data}') raise KlamathError(f'Expected exactly one integer, got {data}')
class ATTRTABLE(ASCIIRecord): class ATTRTABLE(ASCIIRecord):
tag = 0x2306 tag = 0x2306
@classmethod @classmethod
def check_size(cls, size: int): def check_size(cls: type[Self], size: int) -> None:
if size > 44: if size > 44:
raise Exception(f'Expected size <= 44, got {size}') raise KlamathError(f'Expected size <= 44, got {size}')
class STYPTABLE(ASCIIRecord): class STYPTABLE(ASCIIRecord):
@ -266,9 +270,9 @@ class FORMAT(Int2Record):
expected_size = 2 expected_size = 2
@classmethod @classmethod
def check_data(cls, data: Sequence[int]): def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
if len(data) != 1: if not isinstance(data, Sized) or len(data) != 1:
raise Exception(f'Expected exactly one integer, got {data}') raise KlamathError(f'Expected exactly one integer, got {data}')
class MASK(ASCIIRecord): class MASK(ASCIIRecord):

View File

@ -12,7 +12,7 @@ from .basic import decode_real8, encode_real8, parse_datetime
from .basic import KlamathError from .basic import KlamathError
def test_parse_bitarray(): def test_parse_bitarray() -> None:
assert parse_bitarray(b'59') == 13625 assert parse_bitarray(b'59') == 13625
assert parse_bitarray(b'\0\0') == 0 assert parse_bitarray(b'\0\0') == 0
assert parse_bitarray(b'\xff\xff') == 65535 assert parse_bitarray(b'\xff\xff') == 65535
@ -26,7 +26,7 @@ def test_parse_bitarray():
parse_bitarray(b'') parse_bitarray(b'')
def test_parse_int2(): def test_parse_int2() -> None:
assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0)) assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
# odd length # odd length
@ -38,7 +38,7 @@ def test_parse_int2():
parse_int2(b'') parse_int2(b'')
def test_parse_int4(): def test_parse_int4() -> None:
assert_array_equal(parse_int4(b'4321'), (875770417,)) assert_array_equal(parse_int4(b'4321'), (875770417,))
# length % 4 != 0 # length % 4 != 0
@ -50,7 +50,7 @@ def test_parse_int4():
parse_int4(b'') parse_int4(b'')
def test_decode_real8(): def test_decode_real8() -> None:
# zeroes # zeroes
assert decode_real8(numpy.array([0x0])) == 0 assert decode_real8(numpy.array([0x0])) == 0
assert decode_real8(numpy.array([1 << 63])) == 0 # negative assert decode_real8(numpy.array([1 << 63])) == 0 # negative
@ -60,7 +60,7 @@ def test_decode_real8():
assert decode_real8(numpy.array([0xC120 << 48])) == -2.0 assert decode_real8(numpy.array([0xC120 << 48])) == -2.0
def test_parse_real8(): def test_parse_real8() -> None:
packed = struct.pack('>3Q', 0x0, 0x4110_0000_0000_0000, 0xC120_0000_0000_0000) packed = struct.pack('>3Q', 0x0, 0x4110_0000_0000_0000, 0xC120_0000_0000_0000)
assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0)) assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0))
@ -73,7 +73,7 @@ def test_parse_real8():
parse_real8(b'') parse_real8(b'')
def test_parse_ascii(): def test_parse_ascii() -> None:
# # empty data Now allowed! # # empty data Now allowed!
# with pytest.raises(KlamathError): # with pytest.raises(KlamathError):
# parse_ascii(b'') # parse_ascii(b'')
@ -82,40 +82,40 @@ def test_parse_ascii():
assert parse_ascii(b'12345\0') == b'12345' # strips trailing null byte assert parse_ascii(b'12345\0') == b'12345' # strips trailing null byte
def test_pack_bitarray(): def test_pack_bitarray() -> None:
packed = pack_bitarray(321) packed = pack_bitarray(321)
assert len(packed) == 2 assert len(packed) == 2
assert packed == struct.pack('>H', 321) assert packed == struct.pack('>H', 321)
def test_pack_int2(): def test_pack_int2() -> None:
packed = pack_int2((3, 2, 1)) packed = pack_int2((3, 2, 1))
assert len(packed) == 3 * 2 assert len(packed) == 3 * 2
assert packed == struct.pack('>3h', 3, 2, 1) assert packed == struct.pack('>3h', 3, 2, 1)
assert pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1) assert pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1)
def test_pack_int4(): def test_pack_int4() -> None:
packed = pack_int4((3, 2, 1)) packed = pack_int4((3, 2, 1))
assert len(packed) == 3 * 4 assert len(packed) == 3 * 4
assert packed == struct.pack('>3l', 3, 2, 1) assert packed == struct.pack('>3l', 3, 2, 1)
assert pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1) assert pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1)
def test_encode_real8(): def test_encode_real8() -> None:
assert encode_real8(numpy.array([0.0])) == 0 assert encode_real8(numpy.array([0.0])) == 0
arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12)) arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12))
assert_array_equal(decode_real8(encode_real8(arr)), arr) assert_array_equal(decode_real8(encode_real8(arr)), arr)
def test_pack_real8(): def test_pack_real8() -> None:
reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12) reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12)
packed = pack_real8(reals) packed = pack_real8(reals)
assert len(packed) == len(reals) * 8 assert len(packed) == len(reals) * 8
assert_array_equal(parse_real8(packed), reals) assert_array_equal(parse_real8(packed), reals)
def test_pack_ascii(): def test_pack_ascii() -> None:
assert pack_ascii(b'4321') == b'4321' assert pack_ascii(b'4321') == b'4321'
assert pack_ascii(b'321') == b'321\0' assert pack_ascii(b'321') == b'321\0'

View File

@ -45,14 +45,48 @@ classifiers = [
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
] ]
requires-python = ">=3.8" requires-python = ">=3.11"
include = [ include = [
"LICENSE.md" "LICENSE.md"
] ]
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
"numpy~=1.21", "numpy>=1.26",
] ]
[tool.hatch.version] [tool.hatch.version]
path = "klamath/__init__.py" 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
]