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
Requirements:
* python >= 3.10 (written and tested with 3.11)
* python >= 3.11
* 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
file formats.
"""
from . import basic
from . import record
from . import records
from . import elements
from . import library
from . import (
basic as basic,
record as record,
records as records,
elements as elements,
library as library,
)
__author__ = 'Jan Petykiewicz'
__version__ = '1.3'
__version__ = '1.4'

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
"""
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 io
from datetime import datetime
@ -17,6 +18,8 @@ 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:
@ -53,30 +56,27 @@ def expect_record(stream: IO[bytes], tag: int) -> int:
return data_size
R = TypeVar('R', bound='Record')
class Record(metaclass=ABCMeta):
class Record(Generic[II, OO], metaclass=ABCMeta):
tag: ClassVar[int] = -1
expected_size: ClassVar[int | None] = None
@classmethod
def check_size(cls, size: int):
def check_size(cls: type[Self], size: int) -> None:
if cls.expected_size is not None and size != cls.expected_size:
raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
@classmethod
def check_data(cls, data):
@classmethod # noqa: B027 Intentionally non-abstract
def check_data(cls: type[Self], data: II) -> None:
pass
@classmethod
@abstractmethod
def read_data(cls, stream: IO[bytes], size: int):
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> OO:
pass
@classmethod
@abstractmethod
def pack_data(cls, data) -> bytes:
def pack_data(cls: type[Self], data: II) -> bytes:
pass
@staticmethod
@ -84,11 +84,11 @@ class Record(metaclass=ABCMeta):
return read_record_header(stream)
@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)
@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.
@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta):
return True
@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)
while tag != cls.tag:
stream.seek(size, io.SEEK_CUR)
@ -119,90 +119,90 @@ class Record(metaclass=ABCMeta):
return data
@classmethod
def read(cls: Type[R], stream: IO[bytes]):
def read(cls: type[Self], stream: IO[bytes]) -> OO:
size = expect_record(stream, cls.tag)
data = cls.read_data(stream, size)
return data
@classmethod
def write(cls, stream: IO[bytes], data) -> int:
def write(cls: type[Self], stream: IO[bytes], data: II) -> int:
data_bytes = cls.pack_data(data)
b = cls.write_header(stream, len(data_bytes))
b += stream.write(data_bytes)
return b
class NoDataRecord(Record):
class NoDataRecord(Record[None, None]):
expected_size: ClassVar[int | None] = 0
@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)
@classmethod
def pack_data(cls, data: None) -> bytes:
def pack_data(cls: type[Self], data: None) -> bytes:
if data is not None:
raise KlamathError('?? Packing {data} into NoDataRecord??')
return b''
class BitArrayRecord(Record):
class BitArrayRecord(Record[int, int]):
expected_size: ClassVar[int | None] = 2
@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))
@classmethod
def pack_data(cls, data: int) -> bytes:
def pack_data(cls: type[Self], data: int) -> bytes:
return pack_bitarray(data)
class Int2Record(Record):
class Int2Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int16]]):
@classmethod
def read_data(cls, stream: 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))
@classmethod
def pack_data(cls, data: Sequence[int]) -> bytes:
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
return pack_int2(data)
class Int4Record(Record):
class Int4Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int32]]):
@classmethod
def read_data(cls, stream: 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))
@classmethod
def pack_data(cls, data: Sequence[int]) -> bytes:
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
return pack_int4(data)
class Real8Record(Record):
class Real8Record(Record[Sequence[float] | float, NDArray[numpy.float64]]):
@classmethod
def read_data(cls, stream: 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))
@classmethod
def pack_data(cls, data: Sequence[int]) -> bytes:
def pack_data(cls: type[Self], data: Sequence[float] | float) -> bytes:
return pack_real8(data)
class ASCIIRecord(Record):
class ASCIIRecord(Record[bytes, bytes]):
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> bytes:
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> bytes:
return parse_ascii(read(stream, size))
@classmethod
def pack_data(cls, data: bytes) -> bytes:
def pack_data(cls: type[Self], data: bytes) -> bytes:
return pack_ascii(data)
class DateTimeRecord(Record):
class DateTimeRecord(Record[Sequence[datetime], list[datetime]]):
@classmethod
def read_data(cls, stream: 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))
@classmethod
def pack_data(cls, data: Sequence[datetime]) -> bytes:
def pack_data(cls: type[Self], data: Sequence[datetime]) -> bytes:
return pack_datetime(data)

View File

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

View File

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

View File

@ -45,14 +45,48 @@ classifiers = [
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
]
requires-python = ">=3.8"
requires-python = ">=3.11"
include = [
"LICENSE.md"
]
dynamic = ["version"]
dependencies = [
"numpy~=1.21",
"numpy>=1.26",
]
[tool.hatch.version]
path = "klamath/__init__.py"
[tool.ruff]
exclude = [
".git",
"dist",
]
line-length = 145
indent-width = 4
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
lint.select = [
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
"ARG", "PL", "R", "TRY",
"G010", "G101", "G201", "G202",
"Q002", "Q003", "Q004",
]
lint.ignore = [
#"ANN001", # No annotation
"ANN002", # *args
"ANN003", # **kwargs
"ANN401", # Any
"ANN101", # self: Self
"SIM108", # single-line if / else assignment
"RET504", # x=y+z; return x
"PIE790", # unnecessary pass
"ISC003", # non-implicit string concatenation
"C408", # dict(x=y) instead of {'x': y}
"PLR09", # Too many xxx
"PLR2004", # magic number
"PLC0414", # import x as x
"TRY003", # Long exception message
]