Compare commits

..

13 commits

10 changed files with 535 additions and 33 deletions

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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):

156
klamath/test_elements.py Normal file
View file

@ -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)

102
klamath/test_library.py Normal file
View file

@ -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)

134
klamath/test_record.py Normal file
View file

@ -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)

110
klamath/test_records.py Normal file
View file

@ -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

View file

@ -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