Compare commits

..

35 commits

Author SHA1 Message Date
jan
93e4075d89 [Placement] fix writing placement 2026-02-17 00:44:56 -08:00
jan
cceabe46ee README update: drop github mirror 2026-02-16 21:50:29 -08:00
c869da03bf [GridRepetition] convert to expected types 2026-02-15 19:21:28 -08:00
5b42cdf609 [basic] cast to the expected types before conversion 2026-02-15 16:25:21 -08:00
7f7431f018 [Polygon / Path] clarify docs 2026-02-15 15:46:45 -08:00
a82c555cda [Polygon] fix overeager warning 2026-02-15 15:45:20 -08:00
0503ceb725 [Path] fix Path writing the wrong record type 2026-02-15 15:11:33 -08:00
jan
12debf0b02 update tags 2025-12-30 02:54:45 -08:00
968392a4a3 bump version number to v0.13
Main changes are numpy 2.0 compatibility and improved type annotations
2024-07-29 18:21:52 -07:00
cc08efcf17 update python version in comment 2024-07-29 18:20:47 -07:00
19324ee147 bump minimum versions and allow numpy 2.0 2024-07-29 18:19:17 -07:00
f4866fb2bb add ruff config 2024-07-29 18:19:17 -07:00
37bac2af0d simplify a comparison 2024-07-29 18:19:17 -07:00
d3ba2ca2af warn at higher stacklevel 2024-07-29 18:19:17 -07:00
3ee1c03f66 improve error handling 2024-07-29 18:19:17 -07:00
1252edb7b5 add missing asserts 2024-07-29 18:19:17 -07:00
8e451c64db improve type annotations 2024-07-29 18:19:17 -07:00
691ce03150 use strict zip 2024-07-29 18:19:17 -07:00
dc9ed8e794 flatten and simplify conditionals 2024-07-29 18:19:17 -07:00
891007054f use pathlibto validate path 2024-07-29 18:19:17 -07:00
5011750637 improve type annotations in tests 2024-07-29 18:19:17 -07:00
f87dc7d771 use ii > 4 instead of 4 < ii 2024-07-29 18:19:17 -07:00
31e52e20d6 use double quotes for docstrings 2024-07-29 18:19:17 -07:00
5ea5e8d8f9 prefix unused variables 2024-07-29 18:19:17 -07:00
d05561af41 comment style 2024-07-29 18:19:17 -07:00
ab8b71b149 fix overflow when using numpy 2.0 2024-07-29 18:19:17 -07:00
c9e894879f use "is" over == for types 2024-07-29 18:19:17 -07:00
d83ef1ce2d update type annotations 2024-07-29 18:19:17 -07:00
bd288d1363 double-up single-letter variable names 2024-07-29 18:19:17 -07:00
931bc599d7 repeat names for re-export 2024-07-29 18:19:17 -07:00
a4c1e52ff8 Modernize type annotations 2024-07-29 18:19:17 -07:00
d61bbc530f Use IO[bytes] everywhere 2024-07-29 18:19:17 -07:00
01b3f9ca3a cleanup based on flake8 output 2024-07-29 18:19:17 -07:00
b2928c8b1c Use IO[bytes] instead of BinaryIO wherever possible 2024-07-29 18:19:17 -07:00
jan
f9d4cfec33 WIP indentation and f-string conversions 2024-07-29 18:18:47 -07:00
21 changed files with 599 additions and 565 deletions

View file

@ -4,7 +4,6 @@
**Homepage:** https://mpxd.net/code/jan/fatamorgana
* [PyPI](https://pypi.org/project/fatamorgana)
* [Github mirror](https://github.com/anewusername/fatamorgana)
**Capabilities:**
* This package is a work-in-progress and is largely untested -- it works for
@ -22,18 +21,13 @@
## Installation
**Dependencies:**
* python >=3.10
* python >=3.11
* (optional) numpy
Install with pip from PyPi (preferred):
```bash
pip3 install fatamorgana
```
Install directly from git repository:
```bash
pip3 install git+https://mpxd.net/code/jan/fatamorgana.git@release
pip install fatamorgana
```
## Documentation
@ -44,7 +38,6 @@ To read the inline help,
import fatamorgana
help(fatamorgana.OasisLayout)
```
The documentation is currently very sparse and I expect to improve it whenever possible!
## Examples

View file

@ -15,7 +15,7 @@
numpy to speed up reading/writing.
Dependencies:
- Python 3.8 or later
- Python 3.11 or later
- numpy (optional, faster but no additional functionality)
To get started, try:
@ -24,17 +24,28 @@
help(fatamorgana.OasisLayout)
```
"""
import pathlib
from .main import OasisLayout, Cell, XName
from .main import (
OasisLayout as OasisLayout,
Cell as Cell,
XName as XName,
)
from .basic import (
NString, AString, Validation, OffsetTable, OffsetEntry,
EOFError, SignedError, InvalidDataError, InvalidRecordError,
UnfilledModalError,
ReuseRepetition, GridRepetition, ArbitraryRepetition
NString as NString,
AString as AString,
Validation as Validation,
OffsetTable as OffsetTable,
OffsetEntry as OffsetEntry,
EOFError as EOFError,
SignedError as SignedError,
InvalidDataError as InvalidDataError,
InvalidRecordError as InvalidRecordError,
UnfilledModalError as UnfilledModalError,
ReuseRepetition as ReuseRepetition,
GridRepetition as GridRepetition,
ArbitraryRepetition as ArbitraryRepetition,
)
__author__ = 'Jan Petykiewicz'
__version__ = '0.12'
__version__ = '0.13'
version = __version__

View file

@ -2,7 +2,8 @@
This module contains all datatypes and parsing/writing functions for
all abstractions below the 'record' or 'block' level.
"""
from typing import Type, Union, Any, Sequence, IO
from typing import Any, IO, Union, TYPE_CHECKING
from collections.abc import Sequence
from fractions import Fraction
from enum import Enum
import math
@ -20,7 +21,7 @@ except ImportError:
'''
Type definitions
'''
real_t = Union[int, float, Fraction]
real_t = int | float | Fraction
repetition_t = Union['ReuseRepetition', 'GridRepetition', 'ArbitraryRepetition']
property_value_t = Union[int, bytes, 'AString', 'NString', 'PropStringReference', float, Fraction]
bytes_t = bytes
@ -184,7 +185,7 @@ if _USE_NUMPY:
byte_arr = _read(stream, 1)
return numpy.unpackbits(numpy.frombuffer(byte_arr, dtype=numpy.uint8))
def _np_write_bool_byte(stream: IO[bytes], bits: tuple[Union[bool, int], ...]) -> int:
def _np_write_bool_byte(stream: IO[bytes], bits: tuple[bool | int, ...]) -> int:
"""
Pack 8 booleans into a byte, and write it to the stream.
@ -249,6 +250,7 @@ def write_uint(stream: IO[bytes], n: int) -> int:
Raises:
SignedError: if `n` is negative.
"""
n = int(n)
if n < 0:
raise SignedError(f'uint must be positive: {n}')
@ -294,6 +296,7 @@ def encode_sint(sint: int) -> int:
Returns:
Unsigned integer encoding for the input.
"""
sint = int(sint)
return (abs(sint) << 1) | (sint < 0)
@ -343,7 +346,7 @@ def read_bstring(stream: IO[bytes]) -> bytes:
return _read(stream, length)
def write_bstring(stream: IO[bytes], bstring: bytes):
def write_bstring(stream: IO[bytes], bstring: bytes) -> int:
"""
Write a binary string to the stream.
See `read_bstring()` for format details.
@ -563,7 +566,7 @@ class NString:
"""
_string: str
def __init__(self, string_or_bytes: Union[bytes, str]) -> None:
def __init__(self, string_or_bytes: bytes | str) -> None:
"""
Args:
string_or_bytes: Content of the `NString`.
@ -677,7 +680,7 @@ class AString:
"""
_string: str
def __init__(self, string_or_bytes: Union[bytes, str]) -> None:
def __init__(self, string_or_bytes: bytes | str) -> None:
"""
Args:
string_or_bytes: Content of the AString.
@ -949,7 +952,7 @@ class OctangularDelta:
sign = self.octangle & 0x02 > 0
xy[axis] = self.proj_mag * (1 - 2 * sign)
return xy
else:
else: # noqa: RET505
yn = (self.octangle & 0x02) > 0
xyn = (self.octangle & 0x01) > 0
ys = 1 - 2 * yn
@ -1096,7 +1099,6 @@ class Delta:
"""
if self.x == 0 or self.y == 0 or abs(self.x) == abs(self.y):
return write_uint(stream, OctangularDelta(self.x, self.y).as_uint() << 1)
else:
size = write_uint(stream, (encode_sint(self.x) << 1) | 0x01)
size += write_uint(stream, encode_sint(self.y))
return size
@ -1124,11 +1126,10 @@ def read_repetition(stream: IO[bytes]) -> repetition_t:
rtype = read_uint(stream)
if rtype == 0:
return ReuseRepetition.read(stream, rtype)
elif rtype in (1, 2, 3, 8, 9):
if rtype in (1, 2, 3, 8, 9):
return GridRepetition.read(stream, rtype)
elif rtype in (4, 5, 6, 7, 10, 11):
if rtype in (4, 5, 6, 7, 10, 11):
return ArbitraryRepetition.read(stream, rtype)
else:
raise InvalidDataError(f'Unexpected repetition type: {rtype}')
@ -1195,7 +1196,8 @@ class GridRepetition:
a_vector: Sequence[int],
a_count: int,
b_vector: Sequence[int] | None = None,
b_count: int | None = None):
b_count: int | None = None,
) -> None:
"""
Args:
a_vector: First lattice vector, of the form `[x, y]`.
@ -1211,6 +1213,10 @@ class GridRepetition:
InvalidDataError: if `b_count` and `b_vector` inputs conflict
with each other or if `a_count < 1`.
"""
a_count = int(a_count)
if b_count is not None:
b_count = int(b_count)
if b_vector is None or b_count is None:
if b_vector is not None or b_count is not None:
raise InvalidDataError('Repetition has only one of'
@ -1221,7 +1227,7 @@ class GridRepetition:
if b_count < 2:
b_count = None
b_vector = None
warnings.warn('Removed b_count and b_vector since b_count == 1')
warnings.warn('Removed b_count and b_vector since b_count == 1', stacklevel=2)
if a_count < 2:
raise InvalidDataError(f'Repetition has too-small a_count: {a_count}')
@ -1310,7 +1316,7 @@ class GridRepetition:
size = write_uint(stream, 9)
size += write_uint(stream, self.a_count - 2)
size += Delta(*self.a_vector).write(stream)
else:
else: # noqa: PLR5501
if self.a_vector[1] == 0 and self.b_vector[0] == 0:
size = write_uint(stream, 1)
size += write_uint(stream, self.a_count - 2)
@ -1342,7 +1348,7 @@ class GridRepetition:
return True
if self.b_vector is None or other.b_vector is None:
return False
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)):
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): # noqa: SIM103
return False
return True
@ -1486,13 +1492,13 @@ class ArbitraryRepetition:
size = write_uint(stream, 10)
size += write_uint(stream, len(self.x_displacements) - 1)
size += sum(Delta(x, y).write(stream)
for x, y in zip(self.x_displacements, self.y_displacements))
for x, y in zip(self.x_displacements, self.y_displacements, strict=True))
else:
size = write_uint(stream, 11)
size += write_uint(stream, len(self.x_displacements) - 1)
size += write_uint(stream, gcd)
size += sum(Delta(x // gcd, y // gcd).write(stream)
for x, y in zip(self.x_displacements, self.y_displacements))
for x, y in zip(self.x_displacements, self.y_displacements, strict=True))
return size
def __eq__(self, other: Any) -> bool:
@ -1580,7 +1586,7 @@ def read_point_list(
assert (dx == 0) or (dy == 0)
close_points = [[-dx, -dy]]
elif list_type == 3:
assert (dx == 0) or (dy == 0) or (dx == dy) or (dx == -dy)
assert 0 in (dx, dy) or dx in (dy, -dy)
close_points = [[-dx, -dy]]
else:
close_points = [[-dx, -dy]]
@ -1595,7 +1601,7 @@ def read_point_list(
def write_point_list(
stream: IO[bytes],
points: list[Sequence[int]],
points: 'list[Sequence[int]] | NDArray',
fast: bool = False,
implicit_closed: bool = True
) -> int:
@ -1618,6 +1624,8 @@ def write_point_list(
Number of bytes written.
"""
# If we're in a hurry, just write the points as arbitrary Deltas
if _USE_NUMPY:
points = numpy.asarray(points, dtype=int)
if fast:
size = write_uint(stream, 4)
size += write_uint(stream, len(points))
@ -1636,8 +1644,7 @@ def write_point_list(
h_first = False
v_first = False
break
else:
if point[1] != previous[1] or point[0] == previous[0]:
elif point[1] != previous[1] or point[0] == previous[0]:
h_first = False
v_first = False
break
@ -1649,7 +1656,7 @@ def write_point_list(
size += write_uint(stream, len(points))
size += sum(write_sint(stream, x + y) for x, y in points)
return size
elif v_first:
if v_first:
size = write_uint(stream, 1)
size += write_uint(stream, len(points))
size += sum(write_sint(stream, x + y) for x, y in points)
@ -1722,19 +1729,20 @@ class PropStringReference:
ref: int
"""ID of the target"""
reference_type: Type
reference_type: type
"""Type of the target: `bytes`, `NString`, or `AString`"""
def __init__(self, ref: int, ref_type: Type) -> None:
def __init__(self, ref: int, ref_type: type) -> None:
"""
:param ref: ID number of the target.
:param ref_type: Type of the target. One of bytes, NString, AString.
Args:
ref: ID number of the target.
ref_type: Type of the target. One of bytes, NString, AString.
"""
self.ref = ref
self.ref_type = ref_type
def __eq__(self, other: Any) -> bool:
return isinstance(other, type(self)) and self.ref == other.ref and self.reference_type == other.reference_type
return isinstance(other, type(self)) and self.ref == other.ref and self.reference_type is other.reference_type
def __repr__(self) -> str:
return f'[{self.ref_type} : {self.ref}]'
@ -1767,33 +1775,32 @@ def read_property_value(stream: IO[bytes]) -> property_value_t:
Raises:
InvalidDataError: if an invalid type is read.
"""
ref_type: Type
ref_type: type
prop_type = read_uint(stream)
if 0 <= prop_type <= 7:
return read_real(stream, prop_type)
elif prop_type == 8:
if prop_type == 8:
return read_uint(stream)
elif prop_type == 9:
if prop_type == 9:
return read_sint(stream)
elif prop_type == 10:
if prop_type == 10:
return AString.read(stream)
elif prop_type == 11:
if prop_type == 11:
return read_bstring(stream)
elif prop_type == 12:
if prop_type == 12:
return NString.read(stream)
elif prop_type == 13:
if prop_type == 13:
ref_type = AString
ref = read_uint(stream)
return PropStringReference(ref, ref_type)
elif prop_type == 14:
if prop_type == 14:
ref_type = bytes
ref = read_uint(stream)
return PropStringReference(ref, ref_type)
elif prop_type == 15:
if prop_type == 15:
ref_type = NString
ref = read_uint(stream)
return PropStringReference(ref, ref_type)
else:
raise InvalidDataError(f'Invalid property type: {prop_type}')
@ -1830,7 +1837,7 @@ def write_property_value(
else:
size = write_uint(stream, 8)
size += write_uint(stream, value)
elif isinstance(value, (Fraction, float, int)):
elif isinstance(value, Fraction | float | int):
size = write_real(stream, value, force_float32)
elif isinstance(value, AString):
size = write_uint(stream, 10)
@ -1842,11 +1849,11 @@ def write_property_value(
size = write_uint(stream, 12)
size += value.write(stream)
elif isinstance(value, PropStringReference):
if value.ref_type == AString:
if value.ref_type is AString:
size = write_uint(stream, 13)
elif value.ref_type == bytes:
elif value.ref_type is bytes:
size = write_uint(stream, 14)
if value.ref_type == AString:
if value.ref_type is AString:
size = write_uint(stream, 15)
size += write_uint(stream, value.ref)
else:
@ -1881,16 +1888,15 @@ def read_interval(stream: IO[bytes]) -> tuple[int | None, int | None]:
interval_type = read_uint(stream)
if interval_type == 0:
return None, None
elif interval_type == 1:
if interval_type == 1:
return None, read_uint(stream)
elif interval_type == 2:
if interval_type == 2:
return read_uint(stream), None
elif interval_type == 3:
if interval_type == 3:
v = read_uint(stream)
return v, v
elif interval_type == 4:
if interval_type == 4:
return read_uint(stream), read_uint(stream)
else:
raise InvalidDataError(f'Unrecognized interval type: {interval_type}')
@ -1914,14 +1920,11 @@ def write_interval(
if min_bound is None:
if max_bound is None:
return write_uint(stream, 0)
else:
return write_uint(stream, 1) + write_uint(stream, max_bound)
else:
if max_bound is None:
return write_uint(stream, 2) + write_uint(stream, min_bound)
elif min_bound == max_bound:
if min_bound == max_bound:
return write_uint(stream, 3) + write_uint(stream, min_bound)
else:
size = write_uint(stream, 4)
size += write_uint(stream, min_bound)
size += write_uint(stream, max_bound)
@ -2188,9 +2191,7 @@ class Validation:
checksum_type = read_uint(stream)
if checksum_type == 0:
checksum = None
elif checksum_type == 1:
checksum = read_u32(stream)
elif checksum_type == 2:
elif checksum_type in (1, 2):
checksum = read_u32(stream)
else:
raise InvalidDataError('Invalid validation type!')
@ -2212,13 +2213,12 @@ class Validation:
"""
if self.checksum_type == 0:
return write_uint(stream, 0)
elif self.checksum is None:
if self.checksum is None:
raise InvalidDataError(f'Checksum is empty but type is {self.checksum_type}')
elif self.checksum_type == 1:
if self.checksum_type == 1:
return write_uint(stream, 1) + write_u32(stream, self.checksum)
elif self.checksum_type == 2:
if self.checksum_type == 2:
return write_uint(stream, 2) + write_u32(stream, self.checksum)
else:
raise InvalidDataError(f'Unrecognized checksum type: {self.checksum_type}')
def __repr__(self) -> str:
@ -2238,7 +2238,7 @@ def write_magic_bytes(stream: IO[bytes]) -> int:
return stream.write(MAGIC_BYTES)
def read_magic_bytes(stream: IO[bytes]):
def read_magic_bytes(stream: IO[bytes]) -> None:
"""
Read the magic byte sequence from a stream.
Raise an `InvalidDataError` if it was not found.

View file

@ -3,7 +3,7 @@ This module contains data structures and functions for reading from and
writing to whole OASIS layout files, and provides a few additional
abstractions for the data contained inside them.
"""
from typing import Type, IO
from typing import IO
import io
import logging
@ -163,11 +163,10 @@ class OasisLayout:
"""
try:
record_id = read_uint(stream)
except EOFError as e:
except EOFError:
if file_state.within_cblock:
return True
else:
raise e
raise
logger.info(f'read_record of type {record_id} at position 0x{stream.tell():x}')
@ -193,7 +192,6 @@ class OasisLayout:
if record_id == 1:
if file_state.started:
raise InvalidRecordError('Duplicate Start record')
else:
file_state.started = True
if record_id == 2 and file_state.within_cblock:
raise InvalidRecordError('End within CBlock')
@ -204,7 +202,7 @@ class OasisLayout:
file_state.within_cell = False
elif record_id in range(15, 28) or record_id in (32, 33):
if not file_state.within_cell:
raise Exception('Geometry outside Cell')
raise InvalidRecordError('Geometry outside Cell')
elif record_id in (13, 14):
file_state.within_cell = True
else:
@ -422,7 +420,7 @@ class Cell:
placements: list[records.Placement] | None = None,
geometry: list[records.geometry_t] | None = None,
) -> None:
self.name = name if isinstance(name, (NString, int)) else NString(name)
self.name = name if isinstance(name, NString | int) else NString(name)
self.properties = [] if properties is None else properties
self.placements = [] if placements is None else placements
self.geometry = [] if geometry is None else geometry
@ -528,7 +526,7 @@ class XName:
# Mapping from record id to record class.
_GEOMETRY: dict[int, Type[records.geometry_t]] = {
_GEOMETRY: dict[int, type[records.geometry_t]] = {
19: records.Text,
20: records.Rectangle,
21: records.Polygon,

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,10 @@
'''
"""
Build files equivalent to the test cases used by KLayout.
'''
"""
from typing import Callable, IO
from typing import IO
from collections.abc import Callable
from pathlib import Path
from . import (
@ -15,8 +17,8 @@ from . import (
def build_file(num: str, func: Callable[[IO[bytes]], IO[bytes]]) -> None:
with open('t' + num + '.oas', 'wb') as f:
func(f)
with Path('t' + num + '.oas').open('wb') as ff:
func(ff)
def write_all_files() -> None:

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -27,8 +27,8 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -24,9 +24,9 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
"""
Single cell with explicit name 'XYZ'
'''
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -49,9 +49,9 @@ def test_file_1() -> None:
def write_file_2(buf: IO[bytes]) -> IO[bytes]:
'''
"""
Two cellnames ('XYZ', 'ABC') and two cells with name references.
'''
"""
buf.write(HEADER)
write_uint(buf, 3) # CELLNAME record (implicit id 0)
@ -86,9 +86,9 @@ def test_file_2() -> None:
def write_file_3(buf: IO[bytes]) -> IO[bytes]:
'''
"""
Invalid file, contains a mix of explicit and implicit cellnames
'''
"""
buf.write(HEADER)
write_uint(buf, 4) # CELLNAME record (explicit id)
@ -113,13 +113,13 @@ def test_file_3() -> None:
buf.seek(0)
with pytest.raises(InvalidRecordError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)
def write_file_4(buf: IO[bytes]) -> IO[bytes]:
'''
"""
Two cells referencing two names with explicit ids (unsorted)
'''
"""
buf.write(HEADER)
write_uint(buf, 4) # CELLNAME record (explicit id)
@ -156,9 +156,9 @@ def test_file_4() -> None:
def write_file_5(buf: IO[bytes]) -> IO[bytes]:
'''
"""
Reference to non-existent cell name.
'''
"""
buf.write(HEADER)
write_uint(buf, 4) # CELLNAME record (explicit id)
@ -197,9 +197,9 @@ def test_file_5() -> None:
def write_file_6(buf: IO[bytes]) -> IO[bytes]:
'''
"""
Cellname with invalid n-string.
'''
"""
buf.write(HEADER)
write_uint(buf, 4) # CELLNAME record (explicit id)
@ -226,7 +226,7 @@ def test_file_6() -> None:
buf.seek(0)
with pytest.raises(InvalidDataError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)
#base_tests(layout)
#assert len(layout.cellnames) == 2
@ -238,9 +238,9 @@ def test_file_6() -> None:
def write_file_7(buf: IO[bytes]) -> IO[bytes]:
'''
"""
Unused cellname.
'''
"""
buf.write(HEADER)
write_uint(buf, 4) # CELLNAME record (explicit id)

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -25,8 +25,8 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -21,8 +21,8 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -61,7 +61,7 @@ def write_file_1(buf: IO[bytes]) -> IO[bytes]:
+ [0b11, 0b10]
)
for t, (x, x_en) in enumerate(zip(wh, wh_en)):
for t, (x, x_en) in enumerate(zip(wh, wh_en, strict=True)):
write_uint(buf, 26) # CTRAPEZOID record
write_byte(buf, 0b1000_1011 | (x_en << 5)) # TWHX_YRDL
write_uint(buf, 1) # layer
@ -135,8 +135,7 @@ def test_file_1() -> None:
assert gg.width == [250, None][is_ctrapz], msg
elif ct_type in range(22, 24) or ct_type == 25:
assert gg.height == [100, None][is_ctrapz], msg
else:
if ct_type < 8 or 16 <= ct_type < 25 or 26 <= ct_type:
elif ct_type < 8 or 16 <= ct_type < 25 or ct_type >= 26:
assert gg.width == 250, msg
assert gg.height == 100, msg
else:
@ -154,8 +153,8 @@ def test_file_1() -> None:
def write_file_2(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)

View file

@ -1,4 +1,3 @@
# type: ignore
from typing import IO
from io import BytesIO
import struct
@ -23,11 +22,11 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File contains one PAD record.
1000 units/micron
Offset table inside START.
'''
"""
buf.write(MAGIC_BYTES)
write_uint(buf, 1) # START record
@ -56,11 +55,11 @@ def test_file_1() -> None:
def write_file_2(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File contains no records.
1/2 unit/micron
Offset table inside START.
'''
"""
buf.write(MAGIC_BYTES)
write_uint(buf, 1) # START record
@ -87,11 +86,11 @@ def test_file_2() -> None:
def write_file_3(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File contains no records.
10/4 unit/micron
Offset table inside START.
'''
"""
buf.write(MAGIC_BYTES)
write_uint(buf, 1) # START record
@ -119,11 +118,11 @@ def test_file_3() -> None:
def write_file_4(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File contains no records.
12.5 unit/micron (float32)
Offset table inside START.
'''
"""
buf.write(MAGIC_BYTES)
write_uint(buf, 1) # START record
@ -150,11 +149,11 @@ def test_file_4() -> None:
def write_file_5(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File contains no records.
12.5 unit/micron (float64)
Offset table inside START.
'''
"""
buf.write(MAGIC_BYTES)
write_uint(buf, 1) # START record

View file

@ -1,5 +1,5 @@
# type: ignore
from typing import Sequence, IO
from typing import IO
from collections.abc import Sequence
from io import BytesIO
@ -27,7 +27,7 @@ def base_tests(layout: OasisLayout) -> None:
assert not layout.cellnames
assert len(layout.cells) == 1
assert layout.cells[0].name.string == 'A'
assert layout.cells[0].name.string == 'A' # type: ignore
assert not layout.cells[0].properties
@ -207,8 +207,8 @@ def elem_test_text(geometry: Sequence) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_names_geom(buf)
@ -237,8 +237,8 @@ def test_file_1() -> None:
def write_file_2(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_names_text(buf)
@ -267,8 +267,8 @@ def test_file_2() -> None:
def write_file_3(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_names_text(buf, prefix=b'T')
write_names_geom(buf, short=True)
@ -283,8 +283,8 @@ def write_file_3(buf: IO[bytes]) -> IO[bytes]:
def write_file_4(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -21,8 +21,8 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -27,8 +27,8 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -182,7 +182,7 @@ def test_file_1() -> None:
else:
assert gg.half_width == 12, msg
assert len(gg.point_list) == 3, msg
assert len(gg.point_list) == 3, msg # type: ignore
assert_equal(gg.point_list, [[150, 0], [0, 50], [-50, 0]], err_msg=msg)
if ii >= 4:

View file

@ -1,5 +1,5 @@
# type: ignore
from typing import Tuple, IO, cast, List
# mypy: disable-error-code="union-attr"
from typing import IO, cast
from io import BytesIO
from numpy.testing import assert_equal
@ -22,7 +22,7 @@ def base_tests(layout: OasisLayout) -> None:
assert not layout.layers
def write_rectangle(buf: IO[bytes], pos: Tuple[int, int] = (300, -400)) -> None:
def write_rectangle(buf: IO[bytes], pos: tuple[int, int] = (300, -400)) -> None:
write_uint(buf, 20) # RECTANGLE record
write_byte(buf, 0b0111_1011) # SWHX_YRDL
write_uint(buf, 1) # layer
@ -34,8 +34,8 @@ def write_rectangle(buf: IO[bytes], pos: Tuple[int, int] = (300, -400)) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -174,7 +174,7 @@ def test_file_1() -> None:
assert not layout.cells[1].properties
assert not layout.cells[1].geometry
geometry = cast(List[Rectangle], layout.cells[0].geometry)
geometry = cast(list[Rectangle], layout.cells[0].geometry)
assert len(geometry) == 1
assert geometry[0].layer == 1
assert geometry[0].datatype == 2
@ -202,7 +202,7 @@ def test_file_1() -> None:
if ii < 3:
assert pp.y == 400 * (ii + 1), msg
elif 7 <= ii:
elif ii >= 7:
assert pp.y == 0, msg
if ii < 4 or ii == 5:
@ -214,7 +214,7 @@ def test_file_1() -> None:
assert pp.angle == 0, msg
elif ii in (5, 6):
assert pp.angle == 90, msg
elif 7 <= ii:
elif ii >= 7:
assert pp.angle == 270, msg
if ii < 7:
@ -250,8 +250,8 @@ def test_file_1() -> None:
def write_file_common(buf: IO[bytes], variant: int) -> IO[bytes]:
'''
'''
"""
"""
assert variant in (2, 3, 5, 7), 'Error in test definition!'
buf.write(HEADER)
@ -516,8 +516,8 @@ def common_tests(layout: OasisLayout, variant: int) -> None:
def write_file_4(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 3) # CELLNAME record (implicit id 0)
@ -595,8 +595,8 @@ def write_file_4(buf: IO[bytes]) -> IO[bytes]:
def write_file_6(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -748,8 +748,8 @@ def test_file_6() -> None:
def write_file_8(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -843,7 +843,7 @@ def test_file_8() -> None:
assert not layout.cells[2].properties
assert not layout.cells[2].placements
geometry = cast(List[Rectangle], layout.cells[2].geometry)
geometry = cast(list[Rectangle], layout.cells[2].geometry)
assert len(geometry) == 1
assert geometry[0].layer == 1
assert geometry[0].datatype == 2

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr, arg-type"
from typing import IO
from io import BytesIO
@ -107,8 +107,8 @@ def common_tests(layout: OasisLayout) -> None:
def write_file_common(buf: IO[bytes], variant: int) -> IO[bytes]:
'''
'''
"""
"""
assert variant in (1, 3), 'Error in test!!'
buf.write(HEADER)
@ -376,8 +376,8 @@ def test_file_1() -> None:
def write_file_2(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -445,7 +445,7 @@ def test_file_3() -> None:
for ii, gg in enumerate(geometry):
msg = f'Fail on polygon {ii}'
assert len(gg.properties) == 1, msg
assert gg.properties[0].name == 0, msg
assert gg.properties[0].name == 0, msg # type: ignore
assert len(gg.properties[0].values) == 1, msg
assert gg.properties[0].values[0] * 5 == 1, msg
assert gg.properties[0].values[0] * 5 == 1, msg # type: ignore

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr, index, arg-type"
from typing import IO
from io import BytesIO
@ -24,11 +24,11 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_common(buf: IO[bytes], variant: int) -> IO[bytes]:
'''
'''
"""
"""
include_repetitions = variant in (2, 5)
def var_byte(buf, byte):
def var_byte(buf: IO[bytes], byte: int) -> None:
if include_repetitions:
byte |= 0b0100
write_byte(buf, byte)
@ -355,8 +355,8 @@ def test_file_5() -> None:
def write_file_3(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 10) # PROPSTRING (explicit id)
@ -580,8 +580,8 @@ def test_file_3() -> None:
def write_file_4_6(buf: IO[bytes], variant: int) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 10) # PROPSTRING (explicit id)
@ -786,8 +786,8 @@ def write_file_4_6(buf: IO[bytes], variant: int) -> IO[bytes]:
def test_file_4() -> None:
'''
'''
"""
"""
buf = write_file_4_6(BytesIO(), 4)
buf.seek(0)
@ -823,7 +823,7 @@ def test_file_4() -> None:
assert pp.x == [-300, 0, 0, 300, 700, 700, 700, 2000, 4000, 6000, 8000, 10000, 12000][ii], msg
assert pp.y == [400, 200, 400, 400, 400, 1400, 2400, 0, 0, 0, 0, 0, 0][ii], msg
if ii == 4 or 6 <= ii:
if ii == 4 or ii >= 6:
assert pp.flip, msg
else:
assert not pp.flip, msg
@ -855,8 +855,8 @@ def test_file_4() -> None:
def test_file_6() -> None:
'''
'''
"""
"""
buf = write_file_4_6(BytesIO(), 6)
buf.seek(0)
@ -892,7 +892,7 @@ def test_file_6() -> None:
assert pp.x == [-300, 0, 0, 300, 700, 700, 700, 2000, 4000, 6000, 8000, 10000, 12000][ii], msg
assert pp.y == [400, 400, 400, 400, 400, 1400, 2400, 0, 0, 0, 0, 0, 0][ii], msg
if ii == 4 or 6 <= ii:
if ii == 4 or ii >= 6:
assert pp.flip, msg
else:
assert not pp.flip, msg
@ -928,8 +928,8 @@ def test_file_6() -> None:
def write_file_7_8_9(buf: IO[bytes], variant: int) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 28) # PROPERTY record
@ -1059,20 +1059,20 @@ def test_file_7() -> None:
def test_file_8() -> None:
'''
'''
"""
"""
buf = write_file_7_8_9(BytesIO(), 8)
buf.seek(0)
with pytest.raises(InvalidDataError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)
def test_file_9() -> None:
'''
'''
"""
"""
buf = write_file_7_8_9(BytesIO(), 9)
buf.seek(0)
with pytest.raises(InvalidDataError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -75,8 +75,8 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_common(buf: IO[bytes], variant: int) -> IO[bytes]:
'''
'''
"""
"""
assert variant in (1, 2), 'Error in test!!'
buf.write(HEADER)
@ -267,7 +267,7 @@ def test_file_2() -> None:
prop = gg.properties[0]
assert prop.name == 0, msg
assert len(prop.values) == 1, msg
assert prop.values[0].numerator == 1, msg
assert prop.values[0].denominator == 5, msg
assert len(prop.values) == 1, msg # type: ignore
assert prop.values[0].numerator == 1, msg # type: ignore
assert prop.values[0].denominator == 5, msg # type: ignore

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr, index"
from typing import IO
from io import BytesIO
@ -30,8 +30,8 @@ def common_tests(layout: OasisLayout) -> None:
geometry = layout.cells[0].geometry
geometry[0].layer == 1
geometry[0].datatype == 2
assert geometry[0].layer == 1
assert geometry[0].datatype == 2
for ii, gg in enumerate(geometry[1:]):
assert gg.layer == 2, f'textstring #{ii + 1}'
assert gg.datatype == 1, f'textstring #{ii + 1}'
@ -100,9 +100,9 @@ def common_tests(layout: OasisLayout) -> None:
def write_file_common(buf: IO[bytes], variant: int) -> IO[bytes]:
'''
"""
Single cell with explicit name 'XYZ'
'''
"""
assert variant in (1, 2, 5, 12), 'Error in test!!'
buf.write(HEADER)
@ -461,10 +461,10 @@ def test_file_12() -> None:
def write_file_3(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with one textstring with explicit id, and one with an implicit id.
Should fail.
'''
"""
buf.write(HEADER)
write_uint(buf, 6) # TEXTSTRING record (explicit id)
@ -494,15 +494,15 @@ def test_file_3() -> None:
buf.seek(0)
with pytest.raises(InvalidRecordError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)
def write_file_4(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with a TEXT record that references a non-existent TEXTSTRING
TODO add an optional check for valid references
'''
"""
buf.write(HEADER)
write_uint(buf, 5) # TEXTSTRING record (implicit id 0)
@ -538,9 +538,9 @@ def test_file_4() -> None:
def write_file_6(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with TEXT record that uses an un-filled modal for the repetition
'''
"""
buf.write(HEADER)
write_uint(buf, 5) # TEXTSTRING record (implicit id 0)
@ -567,13 +567,13 @@ def test_file_6() -> None:
buf.seek(0)
with pytest.raises(InvalidDataError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)
def write_file_7(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with TEXT record that uses an un-filled modal for the layer
'''
"""
buf.write(HEADER)
write_uint(buf, 5) # TEXTSTRING record (implicit id 0)
@ -598,13 +598,13 @@ def test_file_7() -> None:
buf.seek(0)
with pytest.raises(InvalidDataError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)
def write_file_8(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with TEXT record that uses an un-filled modal for the datatype
'''
"""
buf.write(HEADER)
write_uint(buf, 5) # TEXTSTRING record (implicit id 0)
@ -629,13 +629,13 @@ def test_file_8() -> None:
buf.seek(0)
with pytest.raises(InvalidDataError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)
def write_file_9(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with TEXT record that uses a default modal for the x coordinate
'''
"""
buf.write(HEADER)
write_uint(buf, 5) # TEXTSTRING record (implicit id 0)
@ -669,9 +669,9 @@ def test_file_9() -> None:
def write_file_10(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with TEXT record that uses a default modal for the y coordinate
'''
"""
buf.write(HEADER)
write_uint(buf, 5) # TEXTSTRING record (implicit id 0)
@ -705,9 +705,9 @@ def test_file_10() -> None:
def write_file_11(buf: IO[bytes]) -> IO[bytes]:
'''
"""
File with TEXT record that uses an un-filled modal for the text string
'''
"""
buf.write(HEADER)
write_uint(buf, 5) # TEXTSTRING record (implicit id 0)
@ -732,4 +732,4 @@ def test_file_11() -> None:
buf.seek(0)
with pytest.raises(InvalidDataError):
layout = OasisLayout.read(buf)
_layout = OasisLayout.read(buf)

View file

@ -1,4 +1,4 @@
# type: ignore
# mypy: disable-error-code="union-attr"
from typing import IO
from io import BytesIO
@ -25,8 +25,8 @@ def base_tests(layout: OasisLayout) -> None:
def write_file_1(buf: IO[bytes]) -> IO[bytes]:
'''
'''
"""
"""
buf.write(HEADER)
write_uint(buf, 14) # CELL record (explicit)
@ -209,7 +209,7 @@ def test_file_1() -> None:
if ii in (0, 4):
assert gg.delta_a == -20, msg
elif 8 <= ii:
elif ii >= 8:
assert gg.delta_a == 0, msg
else:
assert gg.delta_a == 20, msg

View file

@ -34,17 +34,16 @@ keywords = [
]
classifiers = [
"Programming Language :: Python :: 3",
"Development Status :: 3 - Alpha",
"Environment :: Other Environment",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: Manufacturing",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
"Topic :: File Formats",
]
requires-python = ">=3.10"
requires-python = ">=3.11"
dynamic = ["version"]
dependencies = [
]
@ -53,4 +52,38 @@ dependencies = [
path = "fatamorgana/__init__.py"
[project.optional-dependencies]
numpy = ["numpy~=1.21"]
numpy = ["numpy>=1.26"]
[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
]