diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 1bdbec5..0000000 --- a/.flake8 +++ /dev/null @@ -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, diff --git a/README.md b/README.md index fd4a4ea..3d838ec 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/klamath/__init__.py b/klamath/__init__.py index 4aef521..8de1f87 100644 --- a/klamath/__init__.py +++ b/klamath/__init__.py @@ -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' diff --git a/klamath/basic.py b/klamath/basic.py index 9d7bdcb..86c9d59 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -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: diff --git a/klamath/elements.py b/klamath/elements.py index ba4098f..302921c 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -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 diff --git a/klamath/library.py b/klamath/library.py index 239609d..f7b68bd 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -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 diff --git a/klamath/record.py b/klamath/record.py index c0b4a6c..48bfe7c 100644 --- a/klamath/record.py +++ b/klamath/record.py @@ -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) diff --git a/klamath/records.py b/klamath/records.py index 96fbc7f..0fbcb4d 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -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): diff --git a/klamath/test_basic.py b/klamath/test_basic.py index b511cc6..4d686d9 100644 --- a/klamath/test_basic.py +++ b/klamath/test_basic.py @@ -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' diff --git a/pyproject.toml b/pyproject.toml index 11d7918..c7d7a0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 + ] +