diff --git a/README.md b/README.md index 3d838ec..4351171 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,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.try_read_struct(stream) +elements_A = klamath.library.read_elements(stream) stream.close() diff --git a/klamath/basic.py b/klamath/basic.py index 135398b..e880393 100644 --- a/klamath/basic.py +++ b/klamath/basic.py @@ -17,11 +17,6 @@ logger = logging.getLogger(__name__) class KlamathError(Exception): pass -try: - from klamath_rs_ext import pack_int2, pack_int4 - _USE_RS_EXT = True -except ImportError: - _USE_RS_EXT = False # # Parse functions @@ -54,7 +49,7 @@ def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]: exp = (nums >> 56) & 0x7f mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64) mant[neg != 0] *= -1 - return numpy.ldexp(mant, 4 * (exp - 64) - 56, signature=(float, int, float)) + return numpy.ldexp(mant, 4 * (exp.astype(numpy.int64) - 64) - 56) def parse_real8(data: bytes) -> NDArray[numpy.float64]: @@ -82,7 +77,7 @@ def parse_datetime(data: bytes) -> list[datetime]: year, *date_parts = parse_int2(data[ii:ii + 12]) try: dt = datetime(year + 1900, *date_parts) - except ValueError as err: + except ValueError: dt = datetime(1900, 1, 1, 0, 0, 0) logger.info(f'Invalid date {[year] + date_parts}, setting {dt} instead') dts.append(dt) @@ -98,14 +93,14 @@ def pack_bitarray(data: int) -> bytes: return struct.pack('>H', data) -def _pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: +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: NDArray[numpy.integer] | Sequence[int] | int) -> bytes: +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}') @@ -154,7 +149,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) @@ -165,7 +160,6 @@ 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) @@ -194,9 +188,3 @@ def read(stream: IO[bytes], size: int) -> bytes: if len(data) != size: raise EOFError return data - - -if not _USE_RS_EXT: - pack_int2 = _pack_int2 - pack_int4 = _pack_int4 - diff --git a/klamath/elements.py b/klamath/elements.py index ba72d47..c0d5567 100644 --- a/klamath/elements.py +++ b/klamath/elements.py @@ -2,6 +2,7 @@ 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 abc import ABCMeta, abstractmethod @@ -53,6 +54,8 @@ def read_properties(stream: IO[bytes]) -> dict[int, bytes]: if key in properties: raise KlamathError(f'Duplicate property key: {key!r}') properties[key] = value + else: + stream.seek(size, io.SEEK_CUR) size, tag = Record.read_header(stream) return properties @@ -319,9 +322,9 @@ class Path(Element): if self.path_type == 4: bgn_ext, end_ext = self.extension if bgn_ext != 0: - b += BGNEXTN.write(stream, bgn_ext) + b += BGNEXTN.write(stream, int(bgn_ext)) if end_ext != 0: - b += ENDEXTN.write(stream, end_ext) + b += ENDEXTN.write(stream, int(end_ext)) b += XY.write(stream, self.xy) b += write_properties(stream, self.properties) b += ENDEL.write(stream, None) diff --git a/klamath/library.py b/klamath/library.py index f7b68bd..8b3ab20 100644 --- a/klamath/library.py +++ b/klamath/library.py @@ -220,10 +220,15 @@ 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_count is None: - ref_count = 1 - assert ref_name is not None - cur_structure[ref_name] += ref_count + 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 else: stream.seek(size, io.SEEK_CUR) size, tag = Record.read_header(stream) diff --git a/klamath/records.py b/klamath/records.py index 0fbcb4d..b05bb04 100644 --- a/klamath/records.py +++ b/klamath/records.py @@ -18,7 +18,7 @@ class HEADER(Int2Record): class BGNLIB(DateTimeRecord): tag = 0x0102 - expected_size = 6 * 2 + expected_size = 2 * 6 * 2 class LIBNAME(ASCIIRecord): @@ -37,7 +37,7 @@ class ENDLIB(NoDataRecord): class BGNSTR(DateTimeRecord): tag = 0x0502 - expected_size = 6 * 2 + expected_size = 2 * 6 * 2 class STRNAME(ASCIIRecord): @@ -173,6 +173,8 @@ class GENERATIONS(Int2Record): @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}') @@ -222,7 +224,6 @@ class PROPATTR(Int2Record): class PROPVALUE(ASCIIRecord): tag = 0x2c06 - expected_size = 2 class BOX(NoDataRecord): @@ -271,6 +272,8 @@ class FORMAT(Int2Record): @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}') @@ -306,7 +309,7 @@ class SOFTFENCE(NoDataRecord): class HARDFENCE(NoDataRecord): - tag = 0x3f00 + tag = 0x3e00 class SOFTWIRE(NoDataRecord): diff --git a/klamath/test_elements.py b/klamath/test_elements.py new file mode 100644 index 0000000..ae3df18 --- /dev/null +++ b/klamath/test_elements.py @@ -0,0 +1,156 @@ +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 new file mode 100644 index 0000000..a27c65c --- /dev/null +++ b/klamath/test_library.py @@ -0,0 +1,102 @@ +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 new file mode 100644 index 0000000..99a5a1f --- /dev/null +++ b/klamath/test_record.py @@ -0,0 +1,134 @@ +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 new file mode 100644 index 0000000..fd07b44 --- /dev/null +++ b/klamath/test_records.py @@ -0,0 +1,110 @@ +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 index c7d7a0e..5ca097d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - [project] name = "klamath" description = "GDSII format reader/writer" @@ -44,6 +40,7 @@ classifiers = [ "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 = [ @@ -54,6 +51,11 @@ dependencies = [ "numpy>=1.26", ] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + [tool.hatch.version] path = "klamath/__init__.py" @@ -78,7 +80,6 @@ lint.ignore = [ "ANN002", # *args "ANN003", # **kwargs "ANN401", # Any - "ANN101", # self: Self "SIM108", # single-line if / else assignment "RET504", # x=y+z; return x "PIE790", # unnecessary pass