From e514ade2b1917dcbce9c11f094cfd3c97681c293 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 28 Sep 2019 11:22:27 -0700 Subject: [PATCH 01/49] Use fatamorgana/VERSION file to single-source version number `import fatamorgana` inside setup.py could break if dependencies weren't satisfied --- MANIFEST.in | 1 + fatamorgana/VERSION | 1 + fatamorgana/__init__.py | 7 ++++++- setup.py | 9 +++++++-- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 fatamorgana/VERSION diff --git a/MANIFEST.in b/MANIFEST.in index c28ab72..3d18ec7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.md include LICENSE.md +include fatamorgana/VERSION diff --git a/fatamorgana/VERSION b/fatamorgana/VERSION new file mode 100644 index 0000000..bd73f47 --- /dev/null +++ b/fatamorgana/VERSION @@ -0,0 +1 @@ +0.4 diff --git a/fatamorgana/__init__.py b/fatamorgana/__init__.py index 0e8485b..9079367 100644 --- a/fatamorgana/__init__.py +++ b/fatamorgana/__init__.py @@ -18,6 +18,8 @@ - Python 3.5 or later - numpy (optional, no additional functionality) """ +import pathlib + from .main import OasisLayout, Cell, XName from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \ EOFError, SignedError, InvalidDataError, InvalidRecordError @@ -25,4 +27,7 @@ from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \ __author__ = 'Jan Petykiewicz' -version = '0.4' +with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: + __version__ = f.read().strip() +version = __version__ + diff --git a/setup.py b/setup.py index a8ee066..9efc3c1 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 from setuptools import setup, find_packages -import fatamorgana with open('README.md', 'r') as f: long_description = f.read() +with open('fatamorgana/VERSION', 'r') as f: + version = f.read().strip() + setup(name='fatamorgana', - version=fatamorgana.version, + version=version, description='OASIS layout format parser and writer', long_description=long_description, long_description_content_type='text/markdown', @@ -51,6 +53,9 @@ setup(name='fatamorgana', 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=find_packages(), + package_data={ + 'fatamorgana': ['VERSION'] + }, install_requires=[ 'typing', ], From e0c29478653faf8c5ef590835362dff7197a9832 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 28 Sep 2019 11:22:43 -0700 Subject: [PATCH 02/49] trim down classifiers --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 9efc3c1..1a1f5c1 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,6 @@ setup(name='fatamorgana', 'gds', ], classifiers=[ - 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Development Status :: 3 - Alpha', 'Environment :: Other Environment', @@ -50,7 +49,6 @@ setup(name='fatamorgana', 'Operating System :: Microsoft :: Windows', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', - 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=find_packages(), package_data={ From deb0fe3bef7f18539f58cc53b00622fa281a461f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 28 Sep 2019 11:23:12 -0700 Subject: [PATCH 03/49] Bump version to 0.5 --- fatamorgana/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/VERSION b/fatamorgana/VERSION index bd73f47..2eb3c4f 100644 --- a/fatamorgana/VERSION +++ b/fatamorgana/VERSION @@ -1 +1 @@ -0.4 +0.5 From b5a7c9a7ad5392dd39479fb443b57f5100e8962a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 16 Apr 2020 22:41:53 -0700 Subject: [PATCH 04/49] fix non-numpy read_bool_byte --- fatamorgana/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 4db5ba0..eb4d706 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -147,7 +147,7 @@ else: :param stream: Stream to read from. :return: A list of 8 booleans corresponding to the bits (MSB first). """ - byte = _read(1)[0] + byte = _read(stream, 1)[0] bits = [(byte >> i) & 0x01 for i in reversed(range(8))] return bits From 58b4f4a40fc52f441a60dbc8d78e54f1b609fcf7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2020 13:51:43 -0700 Subject: [PATCH 05/49] Some minor docstring/readme updates --- README.md | 10 +++++----- fatamorgana/__init__.py | 8 +++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 92325cd..fc5f845 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# fatamorgana +# fatamorgana **fatamorgana** is a Python package for reading and writing OASIS format layout files. @@ -6,7 +6,7 @@ **Capabilities:** * This package is a work-in-progress and is largely untested -- it works for - the tasks I usually use it for, but I can't guarantee I've even + the tasks I usually use it for, but I can't guarantee I've even tried the features you happen to use! Use at your own risk! * Interfaces and datastructures are subject to change! * That said the following work for me: @@ -26,12 +26,12 @@ Install with pip from PyPi (preferred): ```bash -pip install fatamorgana +pip3 install fatamorgana ``` Install directly from git repository: ```bash -pip install git+https://mpxd.net/code/jan/fatamorgana.git@release +pip3 install git+https://mpxd.net/code/jan/fatamorgana.git@release ``` ## Documentation @@ -53,7 +53,7 @@ Read an OASIS file and write it back out: with open('test.oas', 'rb') as f: layout = fatamorgana.OasisLayout.read(f) - + with open('test_write.oas', 'wb') as f: layout.write(f) ``` diff --git a/fatamorgana/__init__.py b/fatamorgana/__init__.py index 9079367..9afbc81 100644 --- a/fatamorgana/__init__.py +++ b/fatamorgana/__init__.py @@ -16,7 +16,13 @@ Dependencies: - Python 3.5 or later - - numpy (optional, no additional functionality) + - numpy (optional, faster but no additional functionality) + + To get started, try: + ```python3 + import fatamorgana + help(fatamorgana.OasisLayout) + ``` """ import pathlib From f4eeb50a6f2f66dc0c9d9e79f115189ed85d13c6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 01:31:36 -0700 Subject: [PATCH 06/49] Fix incorrect calls to .write() --- fatamorgana/records.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index f76682e..4a6e62e 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1319,7 +1319,7 @@ class Placement(Record): if n: size += write_uint(stream, self.name) else: - size += self.name.write(self) + size += self.name.write(stream) if m: size += write_real(stream, self.magnification) if a: @@ -1434,7 +1434,7 @@ class Text(Record): if n: size += write_uint(stream, self.string) else: - size += self.string.write(self) + size += self.string.write(stream) if l: size += write_uint(stream, self.layer) if d: From bb9ebfc8f90a0183de96aa0f047f74414e495359 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 01:37:53 -0700 Subject: [PATCH 07/49] Generate a warning instead of printing --- fatamorgana/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index eb4d706..01ea395 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -8,6 +8,7 @@ from enum import Enum import math import struct import io +import warnings try: import numpy @@ -1038,8 +1039,7 @@ class GridRepetition: if self.b_count < 2: self.b_count = None self.b_vector = None - print('Warning: removed b_count and b_vector since b_count == 1') - # TODO: warn here + warnings.warn('Removed b_count and b_vector since b_count == 1') if self.a_count < 2: raise InvalidDataError('Repetition has too-small x-count: ' From 533c85b249e982412962b5855facb44fd650ad11 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 01:42:14 -0700 Subject: [PATCH 08/49] Fix conditional for writing real-typed properties --- fatamorgana/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 01ea395..76a1d39 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1583,7 +1583,7 @@ def write_property_value(stream: io.BufferedIOBase, else: size = write_uint(stream, 8) size += write_uint(stream, value) - elif isinstance(value, real_t): + elif isinstance(value, (Fraction, float, int)): size = write_real(stream, value, force_float32) elif isinstance(value, AString): size = write_uint(stream, 10) From e046af8ce83f64f661984c78307890e1719c4090 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 02:54:33 -0700 Subject: [PATCH 09/49] Handle more error cases --- fatamorgana/basic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 76a1d39..8a02a38 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -953,6 +953,8 @@ def read_repetition(stream: io.BufferedIOBase) -> repetition_t: return GridRepetition.read(stream, rtype) elif rtype in (4, 5, 6, 7, 10, 11): return ArbitraryRepetition.read(stream, rtype) + else: + raise InvalidDataError('Unexpected repetition type: {}'.format(rtype)) def write_repetition(stream: io.BufferedIOBase, repetition: repetition_t) -> int: @@ -1357,7 +1359,7 @@ def read_point_list(stream: io.BufferedIOBase) -> List[List[int]]: y += delta.y points.append([x, y]) else: - raise Exception('Invalid point list type') + raise InvalidDataError('Invalid point list type') return points From 6f2200c5ed83b1cb02e1e8e107a8d71e10394b17 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 03:00:36 -0700 Subject: [PATCH 10/49] Faster/simpler cumsum approach in read_point_list Reqires a special case for ndarrays in dedup_field() -- probably a good idea anyways if user gives us an ndarray --- fatamorgana/basic.py | 5 +---- fatamorgana/records.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 8a02a38..b65ffea 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1345,10 +1345,7 @@ def read_point_list(stream: io.BufferedIOBase) -> List[List[int]]: elif list_type == 5: deltas = [Delta.read(stream).as_list() for _ in range(list_len)] if _USE_NUMPY: - delta_x, delta_y = zip(*deltas) - x = numpy.cumsum(delta_x) - y = numpy.cumsum(delta_y) - points = list(zip(x, y)) + points = numpy.cumsum(deltas, axis=0) else: points = [] x = 0 diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 4a6e62e..842f201 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -24,7 +24,10 @@ from .basic import AString, NString, repetition_t, property_value_t, real_t, \ read_bstring, read_uint, read_sint, read_real, read_repetition, read_interval, \ write_bstring, write_uint, write_sint, write_real, write_interval, write_point_list, \ write_property_value, read_bool_byte, write_bool_byte, read_byte, write_byte, \ - InvalidDataError, PathExtensionScheme + InvalidDataError, PathExtensionScheme, _USE_NUMPY + +if _USE_NUMPY: + import numpy logger = logging.getLogger(__name__) @@ -2444,7 +2447,12 @@ def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str): r = getattr(record, r_field) m = getattr(modals, m_field) if r is not None: - if m is not None and m == r: + if _USE_NUMPY and m_field in ('polygon_point_list', 'path_point_list'): + equal = numpy.array_equal(m, r) + else: + equal = m is not None and m == r + + if equal: setattr(record, r_field, None) else: setattr(modals, m_field, r) From 62fca39a698e9f6b0f2d9e80e5aeedf8a4ad4f64 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 03:03:35 -0700 Subject: [PATCH 11/49] Modernize comments and type annotations. Ugly commit with a couple code fixes: - modal variable name change: path_halfwidth -> path_half_width - Fixed `hasattr(other, 'as_list')` calls in __eq__() functions --- fatamorgana/basic.py | 1001 +++++++++++++++++++++------------- fatamorgana/main.py | 182 ++++--- fatamorgana/records.py | 1150 +++++++++++++++++++++------------------- 3 files changed, 1335 insertions(+), 998 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index b65ffea..9861397 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -3,7 +3,7 @@ This module contains all datatypes and parsing/writing functions for all abstractions below the 'record' or 'block' level. """ from fractions import Fraction -from typing import List, Tuple, Type +from typing import List, Tuple, Type, Union, Optional, Any, Sequence from enum import Enum import math import struct @@ -20,9 +20,9 @@ except: ''' Type definitions ''' -real_t = int or float or Fraction -repetition_t = 'ReuseRepetition' or 'GridRepetition' or 'ArbitraryRepetition' -property_value_t = int or bytes or 'AString' or 'NString' or' PropStringReference' or float or Fraction +real_t = Union[int, float, Fraction] +repetition_t = Union['ReuseRepetition', 'GridRepetition', 'ArbitraryRepetition'] +property_value_t = Union[int, bytes, 'AString', 'NString', 'PropStringReference', float, Fraction] class FatamorganaError(Exception): @@ -69,11 +69,10 @@ class PathExtensionScheme(Enum): Arbitrary = 3 - ''' Constants ''' -MAGIC_BYTES = b'%SEMI-OASIS\r\n' # type: bytes +MAGIC_BYTES: bytes = b'%SEMI-OASIS\r\n' ''' @@ -84,10 +83,15 @@ def _read(stream: io.BufferedIOBase, n: int) -> bytes: Read n bytes from the stream. Raise an EOFError if there were not enough bytes in the stream. - :param stream: Stream to read from. - :param n: Number of bytes to read. - :return: The bytes that were read. - :raises: EOFError if not enough bytes could be read. + Args: + stream: Stream to read from. + n: Number of bytes to read. + + Returns: + The bytes that were read. + + Raises: + EOFError if not enough bytes could be read. """ b = stream.read(n) if len(b) != n: @@ -99,8 +103,11 @@ def read_byte(stream: io.BufferedIOBase) -> int: """ Read a single byte and return it. - :param stream: Stream to read from. - :return: The byte that was read. + Args: + stream: Stream to read from. + + Returns: + The byte that was read. """ return _read(stream, 1)[0] @@ -109,8 +116,11 @@ def write_byte(stream: io.BufferedIOBase, n: int) -> int: """ Write a single byte to the stream. - :param stream: Stream to read from. - :return: The number of bytes writen (1). + Args: + stream: Stream to read from. + + Returns: + The number of bytes writen (1). """ return stream.write(bytes((n,))) @@ -121,8 +131,11 @@ if _USE_NUMPY: Read a single byte from the stream, and interpret its bits as a list of 8 booleans. - :param stream: Stream to read from. - :return: A list of 8 booleans corresponding to the bits (MSB first). + Args: + stream: Stream to read from. + + Returns: + A list of 8 booleans corresponding to the bits (MSB first). """ byte_arr = _read(stream, 1) return numpy.unpackbits(numpy.frombuffer(byte_arr, dtype=numpy.uint8)) @@ -131,10 +144,15 @@ if _USE_NUMPY: """ Pack 8 booleans into a byte, and write it to the stream. - :param stream: Stream to write to. - :param bits: A list of 8 booleans corresponding to the bits (MSB first). - :return: Number of bytes written (1). - :raises: InvalidDataError if didn't receive 8 bits. + Args: + stream: Stream to write to. + bits: A list of 8 booleans corresponding to the bits (MSB first). + + Returns: + Number of bytes written (1). + + Raises: + InvalidDataError if didn't receive 8 bits. """ if len(bits) != 8: raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) @@ -145,21 +163,29 @@ else: Read a single byte from the stream, and interpret its bits as a list of 8 booleans. - :param stream: Stream to read from. - :return: A list of 8 booleans corresponding to the bits (MSB first). + Args: + stream: Stream to read from. + + Returns: + A list of 8 booleans corresponding to the bits (MSB first). """ byte = _read(stream, 1)[0] - bits = [(byte >> i) & 0x01 for i in reversed(range(8))] + bits = [bool((byte >> i) & 0x01) for i in reversed(range(8))] return bits def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[bool]) -> int: """ Pack 8 booleans into a byte, and write it to the stream. - :param stream: Stream to write to. - :param bits: A list of 8 booleans corresponding to the bits (MSB first). - :return: Number of bytes written (1). - :raises: InvalidDataError if didn't receive 8 bits. + Args: + stream: Stream to write to. + bits: A list of 8 booleans corresponding to the bits (MSB first). + + Returns: + Number of bytes written (1). + + Raises: + InvalidDataError if didn't receive 8 bits. """ if len(bits) != 8: raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) @@ -178,8 +204,11 @@ def read_uint(stream: io.BufferedIOBase) -> int: - Remaining bits of each byte form the binary representation of the integer, but are stored _least significant group first_. - :param stream: Stream to read from. - :return: The integer's value. + Args: + stream: Stream to read from. + + Returns: + The integer's value. """ result = 0 i = 0 @@ -195,12 +224,17 @@ def read_uint(stream: io.BufferedIOBase) -> int: def write_uint(stream: io.BufferedIOBase, n: int) -> int: """ Write an unsigned integer to the stream. - See format details in read_uint(...). + See format details in `read_uint()`. - :param stream: Stream to write to. - :param n: Value to write. - :return: The number of bytes written. - :raises: SignedError if n is negative. + Args: + stream: Stream to write to. + n: Value to write. + + Returns: + The number of bytes written. + + Raises: + SignedError: if `n` is negative. """ if n < 0: raise SignedError('uint must be positive: {}'.format(n)) @@ -227,8 +261,11 @@ def decode_sint(uint: int) -> int: - The LSB is treated as the sign bit - The remainder of the bits encodes the absolute value - :param uint: Unsigned integer to decode from. - :return: The decoded signed integer. + Args: + uint: Unsigned integer to decode from. + + Returns: + The decoded signed integer. """ return (uint >> 1) * (1 - 2 * (0x01 & uint)) @@ -236,10 +273,13 @@ def decode_sint(uint: int) -> int: def encode_sint(sint: int) -> int: """ Encode a signed integer into its corresponding unsigned integer form. - See decode_sint() for format details. + See `decode_sint()` for format details. - :param int: The signed integer to encode. - :return: Unsigned integer encoding for the input. + Args: + int: The signed integer to encode. + + Returns: + Unsigned integer encoding for the input. """ return (abs(sint) << 1) | (sint < 0) @@ -247,10 +287,13 @@ def encode_sint(sint: int) -> int: def read_sint(stream: io.BufferedIOBase) -> int: """ Read a signed integer from the stream. - See decode_sint() for format details. + See `decode_sint()` for format details. - :param stream: Stream to read from. - :return: The integer's value. + Args: + stream: Stream to read from. + + Returns: + The integer's value. """ return decode_sint(read_uint(stream)) @@ -258,11 +301,14 @@ def read_sint(stream: io.BufferedIOBase) -> int: def write_sint(stream: io.BufferedIOBase, n: int) -> int: """ Write a signed integer to the stream. - See decode_sint() for format details. + See `decode_sint()` for format details. - :param stream: Stream to write to. - :param n: Value to write. - :return: The number of bytes written. + Args: + stream: Stream to write to. + n: Value to write. + + Returns: + The number of bytes written. """ return write_uint(stream, encode_sint(n)) @@ -274,8 +320,11 @@ def read_bstring(stream: io.BufferedIOBase) -> bytes: - length: uint - data: bytes - :param stream: Stream to read from. - :return: Bytes containing the binary string. + Args: + stream: Stream to read from. + + Returns: + Bytes containing the binary string. """ length = read_uint(stream) return _read(stream, length) @@ -284,11 +333,14 @@ def read_bstring(stream: io.BufferedIOBase) -> bytes: def write_bstring(stream: io.BufferedIOBase, bstring: bytes): """ Write a binary string to the stream. - See read_bstring() for format details. + See `read_bstring()` for format details. - :param stream: Stream to write to. - :param bstring: Binary string to write. - :return: The number of bytes written. + Args: + stream: Stream to write to. + bstring: Binary string to write. + + Returns: + The number of bytes written. """ write_uint(stream, len(bstring)) return stream.write(bstring) @@ -301,8 +353,11 @@ def read_ratio(stream: io.BufferedIOBase) -> Fraction: - numerator: uint - denominator: uint - :param stream: Stream to read from. - :return: Fraction object containing the read value. + Args: + stream: Stream to read from. + + Returns: + Fraction object containing the read value. """ numer = read_uint(stream) denom = read_uint(stream) @@ -312,12 +367,17 @@ def read_ratio(stream: io.BufferedIOBase) -> Fraction: def write_ratio(stream: io.BufferedIOBase, r: Fraction) -> int: """ Write an unsigned ratio to the stream. - See read_ratio() for format details. + See `read_ratio()` for format details. - :param stream: Stream to write to. - :param r: Ratio to write (Fraction object). - :return: The number of bytes written. - :raises: SignedError if r is negative. + Args: + stream: Stream to write to. + r: Ratio to write (`Fraction` object). + + Returns: + The number of bytes written. + + Raises: + SignedError: if r is negative. """ if r < 0: raise SignedError('Ratio must be unsigned: {}'.format(r)) @@ -330,8 +390,11 @@ def read_float32(stream: io.BufferedIOBase) -> float: """ Read a 32-bit float from the stream. - :param stream: Stream to read from. - :return: The value read. + Args: + stream: Stream to read from. + + Returns: + The value read. """ b = _read(stream, 4) return struct.unpack(" int: """ Write a 32-bit float to the stream. - :param stream: Stream to write to. - :param f: Value to write. - :return: The number of bytes written (4). + Arsg: + stream: Stream to write to. + f: Value to write. + + Returns: + The number of bytes written (4). """ b = struct.pack(" float: """ Read a 64-bit float from the stream. - :param stream: Stream to read from. - :return: The value read. + Args: + stream: Stream to read from. + + Returns: + The value read. """ b = _read(stream, 8) return struct.unpack(" int: """ Write a 64-bit float to the stream. - :param stream: Stream to write to. - :param f: Value to write. - :return: The number of bytes written (8). + Args: + stream: Stream to write to. + f: Value to write. + + Returns: + The number of bytes written (8). """ b = struct.pack(" real_t: 6: 32-bit float 7: 64-bit float - :param stream: Stream to read from. - :param real_type: Type of real number to read. If None (default), - the type is read from the stream. - :return: The value read. - :raises: InvalidDataError if real_type is invalid. + Args: + stream: Stream to read from. + real_type: Type of real number to read. If `None` (default), + the type is read from the stream. + + Returns: + The value read. + + Raises: + InvalidDataError: if real_type is invalid. """ if real_type is None: @@ -431,10 +508,14 @@ def write_real(stream: io.BufferedIOBase, Since python has no 32-bit floats, the force_float32 parameter will perform the cast at write-time if set to True (default False). - :param stream: Stream to write to. - :param r: Value to write. - :param float32: - :return: The number of bytes written. + Args: + stream: Stream to write to. + r: Value to write. + force_float32: If `True`, casts r to float32 when writing. + Default `False`. + + Returns: + The number of bytes written. """ size = 0 if isinstance(r, int): @@ -462,15 +543,16 @@ class NString: Class for handling "name strings", which hold one or more printable ASCII characters (0x21 to 0x7e, inclusive). - __init__ can be called with either a string or bytes object; - subsequent reading/writing should use the .string and - .bytes properties. + `__init__` can be called with either a string or bytes object; + subsequent reading/writing should use the `string` and + `bytes` properties. """ - _string = None # type: str + _string: str - def __init__(self, string_or_bytes: bytes or str): + def __init__(self, string_or_bytes: Union[bytes, str]): """ - :param string_or_bytes: Content of the Nstring. + Args: + string_or_bytes: Content of the `NString`. """ if isinstance(string_or_bytes, str): self.string = string_or_bytes @@ -494,7 +576,7 @@ class NString: @bytes.setter def bytes(self, bstring: bytes): if len(bstring) == 0 or not all(0x21 <= c <= 0x7e for c in bstring): - raise InvalidDataError('Invalid n-string {}'.format(bstring)) + raise InvalidDataError('Invalid n-string {!r}'.format(bstring)) self._string = bstring.decode('ascii') @staticmethod @@ -502,9 +584,14 @@ class NString: """ Create an NString object by reading a bstring from the provided stream. - :param stream: Stream to read from. - :return: Resulting NString. - :raises: InvalidDataError + Args: + stream: Stream to read from. + + Returns: + Resulting NString. + + Raises: + InvalidDataError """ return NString(read_bstring(stream)) @@ -512,12 +599,15 @@ class NString: """ Write this NString to a stream. - :param stream: Stream to write to. - :return: Number of bytes written. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. """ return write_bstring(stream, self.bytes) - def __eq__(self, other: 'NString') -> bool: + def __eq__(self, other: Any) -> bool: return isinstance(other, type(self)) and self.string == other.string def __repr__(self) -> str: @@ -527,11 +617,16 @@ class NString: def read_nstring(stream: io.BufferedIOBase) -> str: """ Read a name string from the provided stream. - See NString for constraints on name strings. + See `NString` for constraints on name strings. - :param stream: Stream to read from. - :return: Resulting string. - :raises: InvalidDataError + Args: + stream: Stream to read from. + + Returns: + Resulting string. + + Raises: + InvalidDataError """ return NString.read(stream).string @@ -539,12 +634,17 @@ def read_nstring(stream: io.BufferedIOBase) -> str: def write_nstring(stream: io.BufferedIOBase, string: str) -> int: """ Write a name string to a stream. - See NString for constraints on name strings. + See `NString` for constraints on name strings. - :param stream: Stream to write to. - :param string: String to write. - :return: Number of bytes written. - :raises: InvalidDataError + Args: + stream: Stream to write to. + string: String to write. + + Returns: + Number of bytes written. + + Raises: + InvalidDataError """ return NString(string).write(stream) @@ -554,15 +654,16 @@ class AString: Class for handling "ascii strings", which hold zero or more ASCII characters (0x20 to 0x7e, inclusive). - __init__ can be called with either a string or bytes object; - subsequent reading/writing should use the .string and - .bytes properties. + `__init__` can be called with either a string or bytes object; + subsequent reading/writing should use the `string` and + `bytes` properties. """ - _string = None # type: str + _string: str - def __init__(self, string_or_bytes: bytes or str): + def __init__(self, string_or_bytes: Union[bytes, str]): """ - :param string_or_bytes: Content of the AString. + Args: + string_or_bytes: Content of the AString. """ if isinstance(string_or_bytes, str): self.string = string_or_bytes @@ -586,30 +687,38 @@ class AString: @bytes.setter def bytes(self, bstring: bytes): if not all(0x20 <= c <= 0x7e for c in bstring): - raise InvalidDataError('Invalid a-string {}'.format(bstring)) + raise InvalidDataError('Invalid a-string {!r}'.format(bstring)) self._string = bstring.decode('ascii') @staticmethod def read(stream: io.BufferedIOBase) -> 'AString': """ - Create an AString object by reading a bstring from the provided stream. + Create an `AString` object by reading a bstring from the provided stream. - :param stream: Stream to read from. - :return: Resulting AString. - :raises: InvalidDataError + Args: + stream: Stream to read from. + + Returns: + Resulting `AString`. + + Raises: + InvalidDataError """ return AString(read_bstring(stream)) def write(self, stream: io.BufferedIOBase) -> int: """ - Write this AString to a stream. + Write this `AString` to a stream. - :param stream: Stream to write to. - :return: Number of bytes written. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. """ return write_bstring(stream, self.bytes) - def __eq__(self, other: 'AString') -> bool: + def __eq__(self, other: Any) -> bool: return isinstance(other, type(self)) and self.string == other.string def __repr__(self) -> str: @@ -619,11 +728,16 @@ class AString: def read_astring(stream: io.BufferedIOBase) -> str: """ Read an ASCII string from the provided stream. - See AString for constraints on ASCII strings. + See `AString` for constraints on ASCII strings. - :param stream: Stream to read from. - :return: Resulting string. - :raises: InvalidDataError + Args: + stream: Stream to read from. + + Returns: + Resulting string. + + Raises: + InvalidDataError """ return AString.read(stream).string @@ -633,10 +747,15 @@ def write_astring(stream: io.BufferedIOBase, string: str) -> int: Write an ASCII string to a stream. See AString for constraints on ASCII strings. - :param stream: Stream to write to. - :param string: String to write. - :return: Number of bytes written. - :raises: InvalidDataError + Args: + stream: Stream to write to. + string: String to write. + + Returns: + Number of bytes written. + + Raises: + InvalidDataError """ return AString(string).write(stream) @@ -645,19 +764,20 @@ class ManhattanDelta: """ Class representing an axis-aligned ("Manhattan") vector. - Has properties - .vertical (boolean, true if aligned along y-axis) - .value (int, signed length of the vector) + Attributes: + vertical (bool): `True` if aligned along y-axis + value (int): signed length of the vector """ vertical = None # type: bool value = None # type: int def __init__(self, x: int, y: int): """ - One of x or y _must_ be zero! + One of `x` or `y` _must_ be zero! - :param x: x-displacement - :param y: y-displacement + Args: + x: x-displacement + y: y-displacement """ x = int(x) y = int(y) @@ -674,7 +794,8 @@ class ManhattanDelta: """ Return a list representation of this vector. - :return: [x, y] + Returns: + `[x, y]` """ xy = [0, 0] xy[self.vertical] = self.value @@ -683,9 +804,10 @@ class ManhattanDelta: def as_uint(self) -> int: """ Return this vector encoded as an unsigned integer. - See ManhattanDelta.from_uint() for format details. + See `ManhattanDelta.from_uint()` for format details. - :return: uint encoding of this vector. + Returns: + uint encoding of this vector. """ return (encode_sint(self.value) << 1) | self.vertical @@ -697,42 +819,51 @@ class ManhattanDelta: The LSB of the encoded object is 1 if the vector is aligned to the y-axis, or 0 if aligned to the x-axis. The remaining bits are used to encode a signed integer containing - the signed length of the vector (see encode_sint() for format details). + the signed length of the vector (see `encode_sint()` for format details). - :param n: Unsigned integer representation of a ManhattanDelta vector. - :return: The ManhattanDelta object that was encoded by n. + Args: + n: Unsigned integer representation of a `ManhattanDelta` vector. + + Returns: + The `ManhattanDelta` object that was encoded by `n`. """ d = ManhattanDelta(0, 0) d.value = decode_sint(n >> 1) - d.vertical = n & 0x01 + d.vertical = bool(n & 0x01) return d @staticmethod def read(stream: io.BufferedIOBase) -> 'ManhattanDelta': """ - Read a ManhattanDelta object from the provided stream. + Read a `ManhattanDelta` object from the provided stream. - See .from_uint() for format details. + See `ManhattanDelta.from_uint()` for format details. - :param stream: The stream to read from. - :return: The ManhattanDelta object that was read from the stream. + Args: + stream: The stream to read from. + + Returns: + The `ManhattanDelta` object that was read from the stream. """ n = read_uint(stream) return ManhattanDelta.from_uint(n) def write(self, stream: io.BufferedIOBase) -> int: """ - Write a ManhattanDelta object to the provided stream. + Write a `ManhattanDelta` object to the provided stream. - See .from_uint() for format details. + See `ManhattanDelta.from_uint()` for format details. - :param stream: The stream to write to. - :return: The number of bytes written. + Args: + stream: The stream to write to. + + Returns: + The number of bytes written. """ return write_uint(stream, self.as_uint()) - def __eq__(self, other: 'ManhattanDelta') -> bool: - return hasattr(other, as_list) and self.as_list() == other.as_list() + def __eq__(self, other: Any) -> bool: + return hasattr(other, 'as_list') and self.as_list() == other.as_list() def __repr__(self) -> str: return '{}'.format(self.as_list()) @@ -742,9 +873,9 @@ class OctangularDelta: """ Class representing an axis-aligned or 45-degree ("Octangular") vector. - Has properties - .proj_mag (int, projection of the vector onto the x or y axis (non-zero)) - .octangle (int, bitfield: + Attributes: + proj_mag (int): projection of the vector onto the x or y axis (non-zero) + octangle (int): bitfield: bit 2: 1 if non-axis-aligned (non-Manhattan) if Manhattan: bit 1: 1 if direction is negative @@ -757,17 +888,17 @@ class OctangularDelta: 0: +x, 1: +y, 2: -x, 3: -y, 4: +x+y, 5: -x+y, 6: +x-y, 7: -x-y - ) """ - proj_mag = None # type: int - octangle = None # type: int + proj_mag: int + octangle: int def __init__(self, x: int, y: int): """ - Either abs(x)==abs(y), x==0, or y==0 _must_ be true! + Either `abs(x)==abs(y)`, `x==0`, or `y==0` _must_ be true! - :param x: x-displacement - :param y: y-displacement + Args: + x: x-displacement + y: y-displacement """ x = int(x) y = int(y) @@ -789,7 +920,8 @@ class OctangularDelta: """ Return a list representation of this vector. - :return: [x, y] + Returns: + `[x, y]` """ if self.octangle < 4: xy = [0, 0] @@ -808,24 +940,28 @@ class OctangularDelta: def as_uint(self) -> int: """ Return this vector encoded as an unsigned integer. - See OctangularDelta.from_uint() for format details. + See `OctangularDelta.from_uint()` for format details. - :return: uint encoding of this vector. + Returns: + uint encoding of this vector. """ return (self.proj_mag << 3) | self.octangle @staticmethod def from_uint(n: int) -> 'OctangularDelta': """ - Construct an OctangularDelta object from its unsigned integer encoding. + Construct an `OctangularDelta` object from its unsigned integer encoding. - The low 3 bits are equal to .proj_mag, as specified in the class + The low 3 bits are equal to `proj_mag`, as specified in the class docstring. The remaining bits are used to encode an unsigned integer containing the length of the vector. - :param n: Unsigned integer representation of an OctangularDelta vector. - :return: The OctangularDelta object that was encoded by n. + Args: + n: Unsigned integer representation of an `OctangularDelta` vector. + + Returns: + The `OctangularDelta` object that was encoded by `n`. """ d = OctangularDelta(0, 0) d.proj_mag = n >> 3 @@ -835,29 +971,35 @@ class OctangularDelta: @staticmethod def read(stream: io.BufferedIOBase) -> 'OctangularDelta': """ - Read an OctangularDelta object from the provided stream. + Read an `OctangularDelta` object from the provided stream. - See .from_uint() for format details. + See `OctangularDelta.from_uint()` for format details. - :param stream: The stream to read from. - :return: The OctangularDelta object that was read from the stream. + Args: + stream: The stream to read from. + + Returns: + The `OctangularDelta` object that was read from the stream. """ n = read_uint(stream) return OctangularDelta.from_uint(n) def write(self, stream: io.BufferedIOBase) -> int: """ - Write an OctangularDelta object to the provided stream. + Write an `OctangularDelta` object to the provided stream. - See .from_uint() for format details. + See `OctangularDelta.from_uint()` for format details. - :param stream: The stream to write to. - :return: The number of bytes written. + Args: + stream: The stream to write to. + + Returns: + The number of bytes written. """ return write_uint(stream, self.as_uint()) - def __eq__(self, other: 'OctangularDelta') -> bool: - return hasattr(other, as_list) and self.as_list() == other.as_list() + def __eq__(self, other: Any) -> bool: + return hasattr(other, 'as_list') and self.as_list() == other.as_list() def __repr__(self) -> str: return '{}'.format(self.as_list()) @@ -867,17 +1009,18 @@ class Delta: """ Class representing an arbitrary vector - Has properties - .x (int) - .y (int) + Attributes + x (int): x-displacement + y (int): y-displacement """ - x = None # type: int - y = None # type: int + x: int + y: int def __init__(self, x: int, y: int): """ - :param x: x-displacement - :param y: y-displacement + Args: + x: x-displacement + y: y-displacement """ x = int(x) y = int(y) @@ -888,25 +1031,29 @@ class Delta: """ Return a list representation of this vector. - :return: [x, y] + Returns: + `[x, y]` """ return [self.x, self.y] @staticmethod def read(stream: io.BufferedIOBase) -> 'Delta': """ - Read a Delta object from the provided stream. + Read a `Delta` object from the provided stream. The format consists of one or two unsigned integers. The LSB of the first integer is 1 if a second integer is present. If two integers are present, the remaining bits of the first - integer are an encoded signed integer (see encode_sint()), and + integer are an encoded signed integer (see `encode_sint()`), and the second integer is an encoded signed_integer. Otherwise, the remaining bits of the first integer are an encoded - OctangularData (see OctangularData.from_uint()). + `OctangularData` (see `OctangularData.from_uint()`). - :param stream: The stream to read from. - :return: The Delta object that was read from the stream. + Args: + stream: The stream to read from. + + Returns: + The `Delta` object that was read from the stream. """ n = read_uint(stream) if (n & 0x01) == 0: @@ -918,12 +1065,15 @@ class Delta: def write(self, stream: io.BufferedIOBase) -> int: """ - Write a Delta object to the provided stream. + Write a `Delta` object to the provided stream. - See .from_uint() for format details. + See `Delta.from_uint()` for format details. - :param stream: The stream to write to. - :return: The number of bytes written. + Args: + stream: The stream to write to. + + Returns: + The number of bytes written. """ 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) @@ -932,8 +1082,8 @@ class Delta: size += write_uint(stream, encode_sint(self.y)) return size - def __eq__(self, other: 'Delta') -> bool: - return hasattr(other, as_list) and self.as_list() == other.as_list() + def __eq__(self, other: Any) -> bool: + return hasattr(other, 'as_list') and self.as_list() == other.as_list() def __repr__(self) -> str: return '{}'.format(self.as_list()) @@ -943,8 +1093,14 @@ def read_repetition(stream: io.BufferedIOBase) -> repetition_t: """ Read a repetition entry from the given stream. - :param stream: Stream to read from. - :return: The repetition entry. + Args: + stream: Stream to read from. + + Returns: + The repetition entry. + + Raises: + InvalidDataError: if an unexpected repetition type is read """ rtype = read_uint(stream) if rtype == 0: @@ -961,9 +1117,12 @@ def write_repetition(stream: io.BufferedIOBase, repetition: repetition_t) -> int """ Write a repetition entry to the given stream. - :param stream: Stream to write to. - :param repetition: The repetition entry to write. - :return: The number of bytes written. + Args: + stream: Stream to write to. + repetition: The repetition entry to write. + + Returns: + The number of bytes written. """ return repetition.write(stream) @@ -980,7 +1139,7 @@ class ReuseRepetition: def write(self, stream: io.BufferedIOBase) -> int: return write_uint(stream, 0) - def __eq__(self, other: 'ReuseRepetition') -> bool: + def __eq__(self, other: Any) -> bool: return isinstance(other, ReuseRepetition) def __repr__(self) -> str: @@ -994,37 +1153,40 @@ class GridRepetition: two lattice vectors, and the extent of the grid is stored as the number of elements along each lattice vector. - This class has properties - .a_vector ([xa: int, ya: int], vector specifying a center-to-center - displacement between adjacent elements in the grid.) - .b_vector ([xb: int, yb: int] or None, a second displacement, present if - a 2D grid is being specified.) - .a_count (int >= 1, number of elements along the grid axis specified by - .a_vector) - .b_count (int >= 1 or None, number of elements along the grid axis - specified by .b_vector) + Attributes: + a_vector (Tuple[int, int]): `(xa, ya)` vector specifying a center-to-center + displacement between adjacent elements in the grid. + b_vector (Optional[Tuple[int, int]]): `(xb, yb)`, a second displacement, + present if a 2D grid is being specified. + a_count (int): number of elements (>=1) along the grid axis specified by + `a_vector`. + b_count (Optional[int]): Number of elements (>=1) along the grid axis + specified by `b_vector`, if `b_vector` is not `None`. """ - a_vector = None # type: List[int] - b_vector = None # type: List[int] or None - a_count = None # type: int - b_count = None # type: int or None + a_vector: List[int] + b_vector: Optional[List[int]] = None + a_count: int + b_count: Optional[int] = None def __init__(self, a_vector: List[int], a_count: int, - b_vector: List[int] = None, - b_count: int = None): + b_vector: Optional[List[int]] = None, + b_count: Optional[int] = None): """ - :param a_vector: First lattice vector, of the form [x, y]. - Specifies center-to-center spacing between adjacent elements. - :param a_count: Number of elements in the a_vector direction. - :param b_vector: Second lattice vector, of the form [x, y]. - Specifies center-to-center spacing between adjacent elements. - Can be omitted when specifying a 1D array. - :param b_count: Number of elements in the b_vector direction. - Should be omitted if b_vector was omitted. - :raises: InvalidDataError if b_* inputs conflict with each other - or a_count < 1. + Args: + a_vector: First lattice vector, of the form `[x, y]`. + Specifies center-to-center spacing between adjacent elements. + a_count: Number of elements in the a_vector direction. + b_vector: Second lattice vector, of the form `[x, y]`. + Specifies center-to-center spacing between adjacent elements. + Can be omitted when specifying a 1D array. + b_count: Number of elements in the `b_vector` direction. + Should be omitted if `b_vector` was omitted. + + Raises: + InvalidDataError: if `b_count` and `b_vector` inputs conflict + with each other or if `a_count < 1`. """ self.a_vector = a_vector self.b_vector = b_vector @@ -1054,14 +1216,21 @@ class GridRepetition: @staticmethod def read(stream: io.BufferedIOBase, repetition_type: int) -> 'GridRepetition': """ - Read a GridRepetition from a stream. + Read a `GridRepetition` from a stream. - :param stream: Stream to read from. - :param repetition_type: Repetition type as defined in OASIS repetition spec. - Valid types are 1, 2, 3, 8, 9. - :return: GridRepetition object read from stream. - :raises InvalidDataError if repetition_type is invalid. + Args: + stream: Stream to read from. + repetition_type: Repetition type as defined in OASIS repetition spec. + Valid types are 1, 2, 3, 8, 9. + + Returns: + `GridRepetition` object read from stream. + + Raises: + InvalidDataError: if `repetition_type` is invalid. """ + nb: Optional[int] + b_vector: Optional[List[int]] if repetition_type == 1: na = read_uint(stream) + 2 nb = read_uint(stream) + 2 @@ -1094,14 +1263,19 @@ class GridRepetition: def write(self, stream: io.BufferedIOBase) -> int: """ - Write the GridRepetition to a stream. + Write the `GridRepetition` to a stream. - A minimal representation is written (e.g., if b_count==1, + A minimal representation is written (e.g., if `b_count==1`, a 1D grid is written) - :param stream: Stream to write to. - :return: Number of bytes written. - :raises: InvalidDataError if repetition is malformed. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. + + Raises: + InvalidDataError: if repetition is malformed. """ if self.b_vector is None or self.b_count is None: if self.b_vector is not None or self.b_count is not None: @@ -1140,7 +1314,7 @@ class GridRepetition: size += Delta(*self.b_vector).write(stream) return size - def __eq__(self, other: 'GridRepetition') -> bool: + def __eq__(self, other: Any) -> bool: return isinstance(other, type(self)) and \ self.a_count == other.a_count and \ self.b_count == other.b_count and \ @@ -1157,20 +1331,21 @@ class ArbitraryRepetition: Class representing a repetition entry denoting a 1D or 2D array of arbitrarily-spaced elements. - Properties: - .x_displacements (List[int], x-displacements between elements) - .y_displacements (List[int], y-displacements between elements) + Attributes: + x_displacements (List[int]): x-displacements between consecutive elements + y_displacements (List[int]): y-displacements between consecutive elements """ - x_displacements = None # type: List[int] - y_displacements = None # type: List[int] + x_displacements: List[int] + y_displacements: List[int] def __init__(self, x_displacements: List[int], y_displacements: List[int]): """ - :param x_displacements: x-displacements between consecutive elements - :param y_displacements: y-displacements between consecutive elements + Args: + x_displacements: x-displacements between consecutive elements + y_displacements: y-displacements between consecutive elements """ self.x_displacements = x_displacements self.y_displacements = y_displacements @@ -1178,13 +1353,18 @@ class ArbitraryRepetition: @staticmethod def read(stream: io.BufferedIOBase, repetition_type: int) -> 'ArbitraryRepetition': """ - Read an ArbitraryRepetition from a stream. + Read an `ArbitraryRepetition` from a stream. - :param stream: Stream to read from. - :param repetition_type: Repetition type as defined in OASIS repetition spec. - Valid types are 4, 5, 6, 7, 10, 11. - :return: ArbitraryRepetition object read from stream. - :raises InvalidDataError if repetition_type is invalid. + Args: + stream: Stream to read from. + repetition_type: Repetition type as defined in OASIS repetition spec. + Valid types are 4, 5, 6, 7, 10, 11. + + Returns: + `ArbitraryRepetition` object read from stream. + + Raises: + InvalidDataError: if `repetition_type` is invalid. """ if repetition_type == 4: n = read_uint(stream) + 1 @@ -1227,14 +1407,17 @@ class ArbitraryRepetition: def write(self, stream: io.BufferedIOBase) -> int: """ - Write the ArbitraryRepetition to a stream. + Write the `ArbitraryRepetition` to a stream. A minimal representation is attempted; common factors in the displacements will be factored out, and lists of zeroes will be omitted. - :param stream: Stream to write to. - :return: Number of bytes written. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. """ def get_gcd(vals: List[int]) -> int: """ @@ -1288,7 +1471,7 @@ class ArbitraryRepetition: return size - def __eq__(self, other: 'ArbitraryRepetition') -> bool: + def __eq__(self, other: Any) -> bool: return isinstance(other, type(self)) and self.x_displacements == other.x_displacements and self.y_displacements == other.y_displacements def __repr__(self) -> str: @@ -1299,8 +1482,14 @@ def read_point_list(stream: io.BufferedIOBase) -> List[List[int]]: """ Read a point list from a stream. - :param stream: Stream to read from. - :return: Point list of the form [[x0, y0], [x1, y1], ...] + Args: + stream: Stream to read from. + + Returns: + Point list of the form `[[x0, y0], [x1, y1], ...]` + + Raises: + InvalidDataError: if an invalid list type is read. """ list_type = read_uint(stream) list_len = read_uint(stream) @@ -1361,24 +1550,27 @@ def read_point_list(stream: io.BufferedIOBase) -> List[List[int]]: def write_point_list(stream: io.BufferedIOBase, - points: List[List[int]], + points: List[Sequence[int]], fast: bool = False, implicit_closed: bool = True ) -> int: """ Write a point list to a stream. - :param stream: Stream to write to. - :param points: List of points, of the form [[x0, y0], [x1, y1], ...] - :param fast: If True, avoid searching for a compact representation for - the point list. - :param implicit_closed: Set to True if the list represents an implicitly - closed polygon, i.e. there is an implied line segment from points[-1] - to points[0]. If False, such segments are ignored, which can result in a - more compact representation for non-closed paths (e.g. a Manhattan - path with non-colinear endpoints). If unsure, use the default. - Default True. - :return: Number of bytes written. + Args: + stream: Stream to write to. + points: List of points, of the form `[[x0, y0], [x1, y1], ...]` + fast: If `True`, avoid searching for a compact representation for + the point list. + implicit_closed: Set to True if the list represents an implicitly + closed polygon, i.e. there is an implied line segment from `points[-1]` + to `points[0]`. If False, such segments are ignored, which can result in + a more compact representation for non-closed paths (e.g. a Manhattan + path with non-colinear endpoints). If unsure, use the default. + Default `True`. + + Returns: + Number of bytes written. """ # If we're in a hurry, just write the points as arbitrary Deltas if fast: @@ -1480,12 +1672,12 @@ class PropStringReference: """ Reference to a property string. - Properties: - .ref (int, ID of the target) - .ref_type (Type, Type of the target: bytes, NString, or AString) + Attributes: + ref (int): ID of the target + ref_type (Type): Type of the target: `bytes`, `NString`, or `AString` """ - ref = None # type: int - reference_type = None # type: Type + ref: int + reference_type: Type def __init__(self, ref: int, ref_type: Type): """ @@ -1495,7 +1687,7 @@ class PropStringReference: self.ref = ref self.ref_type = ref_type - def __eq__(self, other: 'PropStringReference') -> bool: + def __eq__(self, other: Any) -> bool: return isinstance(other, type(self)) and self.ref == other.ref and self.reference_type == other.reference_type def __repr__(self) -> str: @@ -1513,16 +1705,21 @@ def read_property_value(stream: io.BufferedIOBase) -> property_value_t: 0...7: real number; property value type is reused for real number type 8: unsigned integer 9: signed integer - 10: ASCII string (AString) - 11: binary string (bytes) - 12: name string (NString) - 13: PropstringReference to AString - 14: PropstringReference to bstring (i.e., to bytes) - 15: PropstringReference to NString + 10: ASCII string (`AString`) + 11: binary string (`bytes`) + 12: name string (`NString`) + 13: `PropstringReference` to `AString` + 14: `PropstringReference` to `bstring` (i.e., to `bytes`) + 15: `PropstringReference` to `NString` - :param stream: Stream to read from. - :return: Value of the property, depending on type. - :raises: InvalidDataError if an invalid type is read. + Args: + stream: Stream to read from. + + Returns: + Value of the property, depending on type. + + Raises: + InvalidDataError: if an invalid type is read. """ prop_type = read_uint(stream) if 0 <= prop_type <= 7: @@ -1562,18 +1759,21 @@ def write_property_value(stream: io.BufferedIOBase, """ Write a property value to a stream. - See read_property_value() for format details. + See `read_property_value()` for format details. - :param stream: Stream to write to. - :param value: Property value to write. Can be an integer, a real number, - bytes (bstring), NString, AString, or a PropstringReference. - :param force_real: If True and value is an integer, writes an integer- - valued real number instead of a plain integer. Default False. - :param force_signed_int: If True and value is a positive integer, - writes a signed integer. Default false. - :param force_float32: If True and value is a float, writes a 32-bit - float (real number) instead of a 64-bit float. - :return: Number of bytes written. + Args: + stream: Stream to write to. + value: Property value to write. Can be an integer, a real number, + `bytes` (`bstring`), `NString`, `AString`, or a `PropstringReference`. + force_real: If `True` and value is an integer, writes an integer- + valued real number instead of a plain integer. Default `False`. + force_signed_int: If `True` and value is a positive integer, + writes a signed integer. Default `False`. + force_float32: If `True` and value is a float, writes a 32-bit + float (real number) instead of a 64-bit float. + + Returns: + Number of bytes written. """ if isinstance(value, int) and not force_real: if force_signed_int or value < 0: @@ -1606,7 +1806,7 @@ def write_property_value(stream: io.BufferedIOBase, return size -def read_interval(stream: io.BufferedIOBase) -> Tuple[int or None]: +def read_interval(stream: io.BufferedIOBase) -> Tuple[Optional[int], Optional[int]]: """ Read an interval from a stream. These are used for storing layer info. @@ -1619,10 +1819,13 @@ def read_interval(stream: io.BufferedIOBase) -> Tuple[int or None]: type 3: a, a (unsigned integer a) type 4: a, b (unsigned integers a, b) - :param stream: Stream to read from. - :return: (lower, upper), where - lower can be None if there is an implicit lower bound of 0 - upper can be None if there is no upper bound (inf) + Args: + stream: Stream to read from. + + Returns: + `(lower, upper)`, where + `lower` can be `None` if there is an implicit lower bound of `0` + `upper` can be `None` if there is no upper bound (`inf`) """ interval_type = read_uint(stream) if interval_type == 0: @@ -1639,17 +1842,20 @@ def read_interval(stream: io.BufferedIOBase) -> Tuple[int or None]: def write_interval(stream: io.BufferedIOBase, - min_bound: int or None = None, - max_bound: int or None = None + min_bound: Optional[int] = None, + max_bound: Optional[int] = None ) -> int: """ Write an interval to a stream. - Used for layer data; see read_interval for format details. + Used for layer data; see `read_interval()` for format details. - :param stream: Stream to write to. - :param min_bound: Lower bound on the interval, can be None (implicit 0, default) - :param max_bound: Upper bound on the interval, can be None (unbounded, default) - :return: Number of bytes written. + Args: + stream: Stream to write to. + min_bound: Lower bound on the interval, can be None (implicit 0, default) + max_bound: Upper bound on the interval, can be None (unbounded, default) + + Returns: + Number of bytes written. """ if min_bound is None: if max_bound is None: @@ -1670,33 +1876,31 @@ class OffsetEntry: """ Entry for the file's offset table. - Properties: - .strict (bool, If False, the records pointed to by this - offset entry may also appear elsewhere in the file. - If True, all records of the type pointed to by this - offset entry must be present in a contiuous block at - the specified offset [pad records also allowed]. - Additionally: - All references to strict-mode records must be - explicit (using reference_number). - The offset may point to an encapsulating CBlock - record, if the first record in that CBlock is - of the target record type. A strict mode table - cannot begin in the middle of a CBlock. - ) - .offset (int, offset from the start of the file; may be 0 - for records that are not present.) + Attributes: + strict (bool): If `False`, the records pointed to by this + offset entry may also appear elsewhere in the file. If `True`, all + records of the type pointed to by this offset entry must be present + in a contiuous block at the specified offset [pad records also allowed]. + Additionally: + - All references to strict-mode records must be + explicit (using reference_number). + - The offset may point to an encapsulating CBlock record, if the first + record in that CBlock is of the target record type. A strict modei + table cannot begin in the middle of a CBlock. + offset (int): offset from the start of the file; may be 0 + for records that are not present. """ - strict = False # type: bool - offset = 0 # type: int + strict: bool = False + offset: int = 0 def __init__(self, strict: bool = False, offset: int = 0): """ - :param strict: True if the records referenced are written in - strict mode (see class docstring). Default False. - :param offset: Offset from the start of the file for the - referenced records; may be 0 if records are absent. - Default 0. + Args: + strict: `True` if the records referenced are written in + strict mode (see class docstring). Default `False`. + offset: Offset from the start of the file for the + referenced records; may be `0` if records are absent. + Default `0`. """ self.strict = strict self.offset = offset @@ -1706,8 +1910,11 @@ class OffsetEntry: """ Read an offset entry from a stream. - :param stream: Stream to read from. - :return: Offset entry that was read. + Args: + stream: Stream to read from. + + Returns: + Offset entry that was read. """ entry = OffsetEntry() entry.strict = read_uint(stream) > 0 @@ -1718,8 +1925,11 @@ class OffsetEntry: """ Write this offset entry to a stream. - :param stream: Stream to write to. - :return: Number of bytes written + Args: + stream: Stream to write to. + + Returns: + Number of bytes written """ return write_uint(stream, self.strict) + write_uint(stream, self.offset) @@ -1741,20 +1951,20 @@ class OffsetTable: which are stored in the above order in the file's offset table. - Proerties: - .cellnames (OffsetEntry) - .textstrings (OffsetEntry) - .propnames (OffsetEntry) - .propstrings (OffsetEntry) - .layernames (OffsetEntry) - .xnames (OffsetEntry) + Attributes: + cellnames (OffsetEntry): Offset for CellNames + textstrings (OffsetEntry): Offset for TextStrings + propnames (OffsetEntry): Offset for PropNames + propstrings (OffsetEntry): Offset for PropStrings + layernames (OffsetEntry): Offset for LayerNames + xnames (OffsetEntry): Offset for XNames """ - cellnames = None # type: OffsetEntry - textstrings= None # type: OffsetEntry - propnames = None # type: OffsetEntry - propstrings = None # type: OffsetEntry - layernames = None # type: OffsetEntry - xnames = None # type: OffsetEntry + cellnames: OffsetEntry = None + textstrings: OffsetEntry = None + propnames: OffsetEntry = None + propstrings: OffsetEntry = None + layernames: OffsetEntry = None + xnames: OffsetEntry = None def __init__(self, cellnames: OffsetEntry = None, @@ -1764,14 +1974,15 @@ class OffsetTable: layernames: OffsetEntry = None, xnames: OffsetEntry = None): """ - All parameters default to a non-strict entry with offset 0. + All parameters default to a non-strict entry with offset `0`. - :param cellnames: OffsetEntry for CellName records. - :param textstrings: OffsetEntry for TextString records. - :param propnames: OffsetEntry for PropName records. - :param propstrings: OffsetEntry for PropString records. - :param layernames: OffsetEntry for LayerNamerecords. - :param xnames: OffsetEntry for XName records. + Args: + cellnames: `OffsetEntry` for `CellName` records. + textstrings: `OffsetEntry` for `TextString` records. + propnames: `OffsetEntry` for `PropName` records. + propstrings: `OffsetEntry` for `PropString` records. + layernames: `OffsetEntry` for `LayerName` records. + xnames: `OffsetEntry` for `XName` records. """ if cellnames is None: cellnames = OffsetEntry() @@ -1799,8 +2010,11 @@ class OffsetTable: Read an offset table from a stream. See class docstring for format details. - :param stream: Stream to read from. - :return: The offset table that was read. + Args: + stream: Stream to read from. + + Returns: + The offset table that was read. """ table = OffsetTable() table.cellnames = OffsetEntry.read(stream) @@ -1816,8 +2030,11 @@ class OffsetTable: Write this offset table to a stream. See class docstring for format details. - :param stream: Stream to write to. - :return: Number of bytes written. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. """ size = self.cellnames.write(stream) size += self.textstrings.write(stream) @@ -1836,8 +2053,11 @@ def read_u32(stream: io.BufferedIOBase) -> int: """ Read a 32-bit unsigned integer (little endian) from a stream. - :param stream: Stream to read from. - :return: The integer that was read. + Args: + stream: Stream to read from. + + Returns: + The integer that was read. """ b = _read(stream, 4) return struct.unpack(' int: """ Write a 32-bit unsigned integer (little endian) to a stream. - :param stream: Stream to write to. - :param n: Integer to write. - :return: The number of bytes written (4). - :raises: SignedError if n is negative. + Args: + stream: Stream to write to. + n: Integer to write. + + Returns: + The number of bytes written (4). + + Raises: + SignedError: if `n` is negative. """ if n < 0: raise SignedError('Negative u32: {}'.format(n)) @@ -1866,20 +2091,22 @@ class Validation: The checksum is calculated using the entire file, excluding the final 4 bytes (the value of the checksum itself). - Properties: - .checksum_type (int, 0: No checksum, 1: crc32, 2: checksum32) - .checksum (int or None, value of the checksum) - + Attributes: + checksum_type (int): `0` for no checksum, `1` for crc32, `2` for checksum32 + checksum (Optional[int]): value of the checksum """ - checksum_type = None # type: int - checksum = None # type: int or None + checksum_type: int + checksum: Optional[int] = None def __init__(self, checksum_type: int, checksum: int = None): """ - :param checksum_type: 0,1,2 (No checksum, crc32, checksum32) - :param checksum: Value of the checksum, or None. - :raises: InvalidDataError if checksum_type is invalid, or - unexpected checksum is present. + Args: + checksum_type: 0,1,2 (No checksum, crc32, checksum32) + checksum: Value of the checksum, or None. + + Raises: + InvalidDataError: if `checksum_type` is invalid, or + unexpected `checksum` is present. """ if checksum_type < 0 or checksum_type > 2: raise InvalidDataError('Invalid validation type') @@ -1894,9 +2121,14 @@ class Validation: Read a validation entry from a stream. See class docstring for format details. - :param stream: Stream to read from. - :return: The validation entry that was read. - :raises: InvalidDataError if an invalid validation type was encountered. + Args: + stream: Stream to read from. + + Returns: + The validation entry that was read. + + Raises: + InvalidDataError: if an invalid validation type was encountered. """ checksum_type = read_uint(stream) if checksum_type == 0: @@ -1914,8 +2146,11 @@ class Validation: Write this validation entry to a stream. See class docstring for format details. - :param stream: Stream to write to. - :return: Number of bytes written. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. """ if self.checksum_type == 0: return write_uint(stream, 0) @@ -1932,8 +2167,11 @@ def write_magic_bytes(stream: io.BufferedIOBase) -> int: """ Write the magic byte sequence to a stream. - :param stream: Stream to write to. - :return: Number of bytes written. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. """ return stream.write(MAGIC_BYTES) @@ -1941,10 +2179,13 @@ def write_magic_bytes(stream: io.BufferedIOBase) -> int: def read_magic_bytes(stream: io.BufferedIOBase): """ Read the magic byte sequence from a stream. - Raise an InvalidDataError if it was not found. + Raise an `InvalidDataError` if it was not found. - :param stream: Stream to read from. - :raises: InvalidDataError if the sequence was not found. + Args: + stream: Stream to read from. + + Raises: + InvalidDataError: if the sequence was not found. """ magic = _read(stream, len(MAGIC_BYTES)) if magic != MAGIC_BYTES: diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 3cc721c..8fca84e 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -3,6 +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 List, Dict, Union, Optional import io import logging @@ -23,17 +24,17 @@ class FileModals: """ File-scoped modal variables """ - cellname_implicit = None # type: bool or None - propname_implicit = None # type: bool or None - xname_implicit = None # type: bool or None - textstring_implicit = None # type: bool or None - propstring_implicit = None # type: bool or None - cellname_implicit = None # type: bool or None + cellname_implicit: Optional[bool] = None + propname_implicit: Optional[bool] = None + xname_implicit: Optional[bool] = None + textstring_implicit: Optional[bool] = None + propstring_implicit: Optional[bool] = None + cellname_implicit: Optional[bool] = None - within_cell = False # type: bool - within_cblock = False # type: bool - end_has_offset_table = None # type: bool - started = False # type: bool + within_cell: bool = False + within_cblock: bool = False + end_has_offset_table: bool + started: bool = False class OasisLayout: @@ -43,49 +44,51 @@ class OasisLayout: Names and strings are stored in dicts, indexed by reference number. Layer names and properties are stored directly using their associated record objects. - Cells are stored using Cell objects (different from Cell record objects). + Cells are stored using `Cell` objects (different from `records.Cell` + record objects). - Properties: - File properties: - .version AString: Version string ('1.0') - .unit real number: grid steps per micron - .validation Validation: checksum data + Attributes: + (File properties) + version (AString): Version string ('1.0') + unit (real_t): grid steps per micron + validation (Validation): checksum data - Names: - .cellnames Dict[int, NString] - .propnames Dict[int, NString] - .xnames Dict[int, XName] + (Names) + cellnames (Dict[int, NString]): Cell names + propnames (Dict[int, NString]): Property names + xnames (Dict[int, XName]): Custom names - Strings: - .textstrings Dict[int, AString] - .propstrings Dict[int, AString] + (Strings) + textstrings (Dict[int, AString]): Text strings + propstrings (Dict[int, AString]): Property strings - Data: - .layers List[records.LayerName] - .properties List[records.Property] - .cells List[Cell] + (Data) + layers (List[records.LayerName]): Layer definitions + properties (List[records.Property]): Property values + cells (List[Cell]): Layout cells """ - version = None # type: AString - unit = None # type: real_t - validation = None # type: Validation + version: AString + unit: real_t + validation: Validation - properties = None # type: List[records.Property] - cells = None # type: List[Cell] + properties: List[records.Property] + cells: List['Cell'] - cellnames = None # type: Dict[int, NString] - propnames = None # type: Dict[int, NString] - xnames = None # type: Dict[int, XName] + cellnames: Dict[int, NString] + propnames: Dict[int, NString] + xnames: Dict[int, 'XName'] - textstrings = None # type: Dict[int, AString] - propstrings = None # type: Dict[int, AString] - layers = None # type: List[records.LayerName] + textstrings: Dict[int, AString] + propstrings: Dict[int, AString] + layers: List[records.LayerName] def __init__(self, unit: real_t, validation: Validation = None): """ - :param unit: Real number (i.e. int, float, or Fraction), grid steps per micron. - :param validation: Validation object containing checksum data. - Default creates a Validation object of the "no checksum" type. + Args: + unit: Real number (i.e. int, float, or `Fraction`), grid steps per micron. + validation: `Validation` object containing checksum data. + Default creates a `Validation` object of the "no checksum" type. """ if validation is None: validation = Validation(0) @@ -105,10 +108,13 @@ class OasisLayout: @staticmethod def read(stream: io.BufferedIOBase) -> 'OasisLayout': """ - Read an entire .oas file into an OasisLayout object. + Read an entire .oas file into an `OasisLayout` object. - :param stream: Stream to read from. - :return: New OasisLayout object. + Args: + stream: Stream to read from. + + Returns: + New `OasisLayout` object. """ file_state = FileModals() modals = Modals() @@ -127,15 +133,20 @@ class OasisLayout: ) -> bool: """ Read a single record of unspecified type from a stream, adding its - contents into this OasisLayout object. + contents into this `OasisLayout` object. - :param stream: Stream to read from. - :param modals: Modal variable data, used to fill unfilled record - fields and updated using filled record fields. - :param file_state: File status data. - :return: True if EOF was reached without error, False otherwise. - :raises: InvalidRecordError from unexpected records; - InvalidDataError from within record parsers. + Args: + stream: Stream to read from. + modals: Modal variable data, used to fill unfilled record + fields and updated using filled record fields. + file_state: File status data. + + Returns: + `True` if EOF was reached without error, `False` otherwise. + + Raises: + InvalidRecordError: from unexpected records + InvalidDataError: from within record parsers """ try: record_id = read_uint(stream) @@ -304,9 +315,14 @@ class OasisLayout: """ Write this object in OASIS fromat to a stream. - :param stream: Stream to write to. - :return: Number of bytes written. - :raises: InvalidDataError if contained records are invalid. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. + + Raises: + InvalidDataError: if contained records are invalid. """ modals = Modals() @@ -357,21 +373,22 @@ class Cell: """ Representation of an OASIS cell. - Properties: - .name NString or int (CellName reference number) + Attributes: + name (Union[NString, int]): name or "CellName reference" number - .properties List of records.Property - .placements List of records.Placement - .geometry List of geometry record objectes + properties (List[records.Property]): Properties of this cell + placements (List[records.Placement]): Placement record objects + geometry: (List[records.geometry_t]): Geometry record objectes """ - name = None # type: NString or int - properties = None # type: List[records.Property] - placements = None # type: List[records.Placement] - geometry = None # type: List[records.geometry_t] + name: Union[NString, int] + properties: List[records.Property] + placements: List[records.Placement] + geometry: List[records.geometry_t] - def __init__(self, name: NString or int): + def __init__(self, name: Union[NString, int]): """ - :param name: NString or int (CellName reference number) + Args: + name: `NString` or "CellName reference" number """ self.name = name self.properties = [] @@ -383,10 +400,15 @@ class Cell: Write this cell to a stream, using the provided modal variables to deduplicate any repeated data. - :param stream: Stream to write to. - :param modals: Modal variables to use for deduplication. - :return: Number of bytes written. - :raises: InvalidDataError if contained records are invalid. + Args: + stream: Stream to write to. + modals: Modal variables to use for deduplication. + + Returns: + Number of bytes written. + + Raises: + InvalidDataError: if contained records are invalid. """ size = records.Cell(self.name).dedup_write(stream, modals) size += sum(p.dedup_write(stream, modals) for p in self.properties) @@ -399,16 +421,17 @@ class XName: """ Representation of an XName. - This class is effectively a simplified form of a records.XName, + This class is effectively a simplified form of a `records.XName`, with the reference data stripped out. """ - attribute = None # type: int - bstring = None # type: bytes + attribute: int + bstring: bytes def __init__(self, attribute: int, bstring: bytes): """ - :param attribute: Attribute number. - :param bstring: Binary data. + Args: + attribute: Attribute number. + bstring: Binary data. """ self.attribute = attribute self.bstring = bstring @@ -416,10 +439,13 @@ class XName: @staticmethod def from_record(record: records.XName) -> 'XName': """ - Create an XName object from a records.XName record. + Create an `XName` object from a `records.XName` record. - :param record: XName record to use. - :return: XName object. + Args: + record: XName record to use. + + Returns: + `XName` object. """ return XName(record.attribute, record.bstring) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 842f201..3d81f14 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -11,7 +11,7 @@ Higher-level code (e.g. monitoring for combinations of records with in main.py instead. """ from abc import ABCMeta, abstractmethod -from typing import List, Dict, Tuple +from typing import List, Dict, Tuple, Union, Optional, Sequence import copy import math import zlib @@ -35,42 +35,43 @@ logger = logging.getLogger(__name__) ''' Type definitions ''' -geometry_t = 'Text' or 'Rectangle' or 'Polygon' or 'Path' or 'Trapezoid' or \ - 'CTrapezoid' or 'Circle' or 'XElement' or 'XGeometry' -pathextension_t = Tuple['PathExtensionScheme' or int] +geometry_t = Union['Text', 'Rectangle', 'Polygon', 'Path', 'Trapezoid', + 'CTrapezoid', 'Circle', 'XElement', 'XGeometry'] +pathextension_t = Tuple['PathExtensionScheme', Optional[int]] +point_list_t = Sequence[Sequence[int]] class Modals: """ - Modal variables, used to store data about previously-written ori + Modal variables, used to store data about previously-written or -read records. """ - repetition = None # type: repetition_t or None - placement_x = 0 # type: int - placement_y = 0 # type: int - placement_cell = None # type: int or NString or None - layer = None # type: int or None - datatype = None # type: int or None - text_layer = None # type: int or None - text_datatype = None # type: int or None - text_x = 0 # type: int - text_y = 0 # type: int - text_string = None # type: AString or int or None - geometry_x = 0 # type: int - geometry_y = 0 # type: int - xy_relative = False # type: bool - geometry_w = None # type: int or None - geometry_h = None # type: int or None - polygon_point_list = None # type: List[List[int]] or None - path_halfwidth = None # type: int or None - path_point_list = None # type: List[List[int]] or None - path_extension_start = None # type: pathextension_t or None - path_extension_end = None # type: pathextension_t or None - ctrapezoid_type = None # type: int or None - circle_radius = None # type: int or None - property_value_list = None # type: List[property_value_t] or None - property_name = None # type: int or NString or None - property_is_standard = None # type: bool or None + repetition: Optional[repetition_t] = None + placement_x: int = 0 + placement_y: int = 0 + placement_cell: Optional[NString] = None + layer: Optional[int] = None + datatype: Optional[int] = None + text_layer: Optional[int] = None + text_datatype: Optional[int] = None + text_x: int = 0 + text_y: int = 0 + text_string: Union[AString, int, None] = None + geometry_x: int = 0 + geometry_y: int = 0 + xy_relative: bool = False + geometry_w: Optional[int] = None + geometry_h: Optional[int] = None + polygon_point_list: Optional[point_list_t] = None + path_half_width: Optional[int] = None + path_point_list: Optional[point_list_t] = None + path_extension_start: Optional[pathextension_t] = None + path_extension_end: Optional[pathextension_t] = None + ctrapezoid_type: Optional[int] = None + circle_radius: Optional[int] = None + property_value_list: Optional[Sequence[property_value_t]] = None + property_name: Union[int, NString, None] = None + property_is_standard: Optional[bool] = None def __init__(self): self.reset() @@ -79,9 +80,9 @@ class Modals: """ Resets all modal variables to their default values. Default values are: - 0 for placement_{x,y}, text_{x,y}, geometry_{x,y} - False for xy_relative - Undefined (None) for all others + `0` for placement_{x,y}, text_{x,y}, geometry_{x,y} + `False` for xy_relative + Undefined (`None`) for all others """ self.repetition = None self.placement_x = 0 @@ -100,7 +101,7 @@ class Modals: self.geometry_w = None self.geometry_h = None self.polygon_point_list = None - self.path_halfwidth = None + self.path_half_width = None self.path_point_list = None self.path_extension_start = None self.path_extension_end = None @@ -127,7 +128,8 @@ class Record(metaclass=ABCMeta): Copy all defined values from this record into the modal variables. Fill all undefined values in this record from the modal variables. - :param modals: Modal variables to merge with. + Args: + modals: Modal variables to merge with. """ pass @@ -140,7 +142,8 @@ class Record(metaclass=ABCMeta): used instead. Update the modal variables using the remaining (unequal) values. - :param modals: Modal variables to deduplicate with. + Args: + modals: Modal variables to deduplicate with. """ pass @@ -151,12 +154,17 @@ class Record(metaclass=ABCMeta): Read a record of this type from a stream. This function does not merge with modal variables. - :param stream: Stream to read from. - :param record_id: Record id of the record to read. The + Args: + stream: Stream to read from. + record_id: Record id of the record to read. The record id is often used to specify which variant of the record is stored. - :return: The record that was read. - :raises: InvalidDataError if the record is malformed. + + Returns: + The record that was read. + + Raises: + InvalidDataError: if the record is malformed. """ pass @@ -166,20 +174,30 @@ class Record(metaclass=ABCMeta): Write this record to a stream as-is. This function does not merge or deduplicate with modal variables. - :param stream: Stream to write to. - :return: Number of bytes written. - :raises: InvalidDataError if the record contains invalid data. + Args: + stream: Stream to write to. + + Returns: + Number of bytes written. + + Raises: + InvalidDataError: if the record contains invalid data. """ pass def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int: """ - Run .deduplicate_with_modals() and then .write() to the stream. + Run `.deduplicate_with_modals()` and then `.write()` to the stream. - :param stream: Stream to write to. - :param modals: Modal variables to merge with. - :return: Number of bytes written - :raises: InvalidDataError if the record contains invalid data. + Args: + stream: Stream to write to. + modals: Modal variables to merge with. + + Returns: + Number of bytes written. + + Raises: + InvalidDataError: if the record contains invalid data. """ # TODO logging #print(type(self), stream.tell()) @@ -190,7 +208,8 @@ class Record(metaclass=ABCMeta): """ Perform a deep copy of this record. - :return: A deep copy of this record. + Returns: + A deep copy of this record. """ return copy.deepcopy(self) @@ -201,15 +220,18 @@ class Record(metaclass=ABCMeta): def read_refname(stream: io.BufferedIOBase, is_present: bool, is_reference: bool - ) -> None or int or NString: + ) -> Union[None, int, NString]: """ Helper function for reading a possibly-absent, possibly-referenced NString. - :param stream: Stream to read from. - :param is_present: If False, read nothing and return None - :param is_reference: If True, read a uint (reference id), - otherwise read an NString. - :return: None, reference id, or NString + Args: + stream: Stream to read from. + is_present: If `False`, read nothing and return `None` + is_reference: If `True`, read a uint (reference id), + otherwise read an `NString`. + + Returns: + `None`, reference id, or `NString` """ if not is_present: return None @@ -222,15 +244,18 @@ def read_refname(stream: io.BufferedIOBase, def read_refstring(stream: io.BufferedIOBase, is_present: bool, is_reference: bool - ) -> None or int or AString: + ) -> Union[None, int, AString]: """ - Helper function for reading a possibly-absent, possibly-referenced AString. + Helper function for reading a possibly-absent, possibly-referenced `AString`. - :param stream: Stream to read from. - :param is_present: If False, read nothing and return None - :param is_reference: If True, read a uint (reference id), - otherwise read an AString. - :return: None, reference id, or AString + Args: + stream: Stream to read from. + is_present: If `False`, read nothing and return `None` + is_reference: If `True`, read a uint (reference id), + otherwise read an `AString`. + + Returns: + `None`, reference id, or `AString` """ if not is_present: return None @@ -267,10 +292,10 @@ class XYMode(Record): """ XYMode record (ID 15, 16) - Properties: - .relative (bool, default False) + Attributes: + relative (bool): default `False` """ - relative = False # type: bool + relative: bool = False @property def absolute(self) -> bool: @@ -282,7 +307,8 @@ class XYMode(Record): def __init__(self, relative: bool): """ - :param relative: True if the mode is 'relative', False if 'absolute'. + Args: + relative: `True` if the mode is 'relative', `False` if 'absolute'. """ self.relative = relative @@ -308,25 +334,26 @@ class Start(Record): """ Start Record (ID 1) - Properties: - .version (AString, "1.0") - .unit (positive real number, grid steps per micron) - .offset_table (OffsetTable or None, if None then table must be - placed in the End record) + Attributes: + version (AString): "1.0" + unit (real_t): positive real number, grid steps per micron + offset_table (OffsetTable or None): If `None` then table must be + placed in the `End` record) """ - version = None # type: AString - unit = None # type: real_t - offset_table = None # type: OffsetTable + version: AString + unit: real_t + offset_table: OffsetTable = None def __init__(self, unit: real_t, - version: AString or str = None, + version: Union[AString, str] = None, offset_table: OffsetTable = None): """ - :param unit: Grid steps per micron (positive real number) - :param version: Version string, default "1.0" - :param offset_table: OffsetTable for the file, or None to place - it in the End record instead. + Args + unit: Grid steps per micron (positive real number) + version: Version string, default "1.0" + offset_table: `OffsetTable` for the file, or `None` to place + it in the `End` record instead. """ if unit <= 0: raise InvalidDataError('Non-positive unit: {}'.format(unit)) @@ -387,21 +414,22 @@ class End(Record): The end record is always padded to a total length of 256 bytes. - Properties: - .offset_table (OffsetTable or None, None if offset table was - written into the Start record instead) - .validation (Validation object) + Attributes: + offset_table (Optional[OffsetTable]): `None` if offset table was + written into the `Start` record instead + validation (Validation): object containing checksum """ - offset_table = None # type: OffsetTable or None - validation = None # type: Validation + offset_table: Optional[OffsetTable] = None + validation: Validation def __init__(self, validation: Validation, - offset_table: OffsetTable = None): + offset_table: Optional[OffsetTable] = None): """ - :param validation: Validation object for this file. - :param offset_table: OffsetTable, or None if the Start record - contained an OffsetTable. Default None. + Args: + validation: `Validation` object for this file. + offset_table: `OffsetTable`, or `None` if the `Start` record + contained an `OffsetTable`. Default `None`. """ self.validation = validation self.offset_table = offset_table @@ -450,23 +478,24 @@ class CBlock(Record): """ CBlock (Compressed Block) record (ID 34) - Properties: - .compression_type (int, 0 for zlib) - .decompressed_byte_count (int) - .compressed_bytes (bytes) + Attributes: + compression_type (int): `0` for zlib + decompressed_byte_count (int): size after decompressing + compressed_bytes (bytes): compressed data """ - compression_type = None # type: int - decompressed_byte_count = None # type: int - compressed_bytes = None # type: bytes + compression_type: int + decompressed_byte_count: int + compressed_bytes: bytes def __init__(self, compression_type: int, decompressed_byte_count: int, compressed_bytes: bytes): """ - :param compression_type: 0 (zlib) - :param decompressed_byte_count: Number of bytes in the decompressed data. - :param compressed_bytes: The compressed data. + Args: + compression_type: `0` (zlib) + decompressed_byte_count: Number of bytes in the decompressed data. + compressed_bytes: The compressed data. """ if compression_type != 0: raise InvalidDataError('CBlock: Invalid compression scheme ' @@ -509,11 +538,16 @@ class CBlock(Record): """ Create a CBlock record from uncompressed data. - :param decompressed_bytes: Uncompressed data (one or more non-CBlock records) - :param compression_type: Compression type (0: zlib). Default 0 - :param compression_args Passed as kwargs to zlib.compressobj(). Default {}. - :return: CBlock object constructed from the data. - :raises: InvalidDataError if invalid compression_type. + Args: + decompressed_bytes: Uncompressed data (one or more non-CBlock records) + compression_type: Compression type (0: zlib). Default `0` + compression_args: Passed as kwargs to `zlib.compressobj()`. Default `{}`. + + Returns: + CBlock object constructed from the data. + + Raises: + InvalidDataError: if invalid `compression_type`. """ if compression_args is None: compression_args = {} @@ -533,9 +567,14 @@ class CBlock(Record): """ Decompress the contents of this CBlock. - :param decompression_args: Passed as kwargs to zlib.decompressobj(). - :return: Decompressed bytes object. - :raises: InvalidDataError if data is malformed or compression type is + Args: + decompression_args: Passed as kwargs to `zlib.decompressobj()`. + + Returns: + Decompressed `bytes` object. + + Raises: + InvalidDataError: if data is malformed or compression type is unknonwn. """ if decompression_args is None: @@ -556,20 +595,21 @@ class CellName(Record): """ CellName record (ID 3, 4) - Properties: - .nstring (NString) - .reference_number (int or None) + Attributes: + nstring (NString): name + reference_number (Optional[int]): `None` results in implicit assignment """ - nstring = None # type: NString - reference_number = None # type: int or None + nstring: NString + reference_number: Optional[int] = None def __init__(self, - nstring: NString or str, + nstring: Union[NString, str], reference_number: int = None): """ - :param nstring: The contained string. - :param reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + Args: + nstring: The contained string. + reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(nstring, NString): self.nstring = nstring @@ -609,20 +649,21 @@ class PropName(Record): """ PropName record (ID 7, 8) - Properties: - .nstring (NString) - .reference_number (int or None) + Attributes: + nstring (NString): name + reference_number (Optional[int]): `None` results in implicit assignment """ - nstring = None # type: NString - reference_number = None # type: int or None + nstring: NString + reference_number: Optional[int] = None def __init__(self, - nstring: NString or str, + nstring: Union[NString, str], reference_number: int = None): """ - :param nstring: The contained string. - :param reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + Args: + nstring: The contained string. + reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(nstring, NString): self.nstring = nstring @@ -663,20 +704,21 @@ class TextString(Record): """ TextString record (ID 5, 6) - Properties: - .astring (AString) - .reference_number (int or None) + Attributes: + astring (AString): string data + reference_number (Optional[int]): `None` results in implicit assignment """ - astring = None # type: AString - reference_number = None # type: int or None + astring: AString + reference_number: Optional[int] = None def __init__(self, - string: AString or str, + string: Union[AString, str], reference_number: int = None): """ - :param string: The contained string. - :param reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + Args: + string: The contained string. + reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(string, AString): self.astring = string @@ -717,20 +759,21 @@ class PropString(Record): """ PropString record (ID 9, 10) - Properties: - .astring (AString) - .reference_number (int or None) + Attributes: + astring (AString): string data + reference_number (Optional[int]): `None` results in implicit assignment """ - astring = None # type: AString - reference_number = None # type: int or None + astring: AString + reference_number: Optional[int] = None def __init__(self, - string: AString or str, + string: Union[AString, str], reference_number: int = None): """ - :param string: The contained string. - :param reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + Args: + string: The contained string. + reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(string, AString): self.astring = string @@ -771,31 +814,30 @@ class LayerName(Record): """ LayerName record (ID 11, 12) - Properties: - .nstring (NString) - .layer_interval (Tuple, (int or None, int or None), - bounds on the interval) - .type_interval (Tuple, (int or None, int or None), - bounds on the interval) - .is_textlayer (bool) + Attributes: + nstring (NString): name + layer_interval (Tuple[Optional[int], Optional[int]]): bounds on the interval + type_interval (Tuple[Optional[int], Optional[int]]): bounds on the interval + is_textlayer (bool): Is this a text layer? """ - nstring = None # type: NString, - layer_interval = None # type: Tuple - type_interval = None # type: Tuple - is_textlayer = None # type: bool + nstring: NString + layer_interval: Tuple + type_interval: Tuple + is_textlayer: bool def __init__(self, - nstring: NString or str, + nstring: Union[NString, str], layer_interval: Tuple, type_interval: Tuple, is_textlayer: bool): """ - :param nstring: The layer name. - :param layer_interval: Tuple (int or None, int or None) giving bounds - (or lack of thereof) on the layer number. - :param type_interval: Tuple (int or None, int or None) giving bounds - (or lack of thereof) on the type number. - :param is_textlayer: True if the layer is a text layer. + Args: + nstring: The layer name. + layer_interval: Tuple (int or None, int or None) giving bounds + (or lack of thereof) on the layer number. + type_interval: Tuple (int or None, int or None) giving bounds + (or lack of thereof) on the type number. + is_textlayer: `True` if the layer is a text layer. """ if isinstance(nstring, NString): self.nstring = nstring @@ -837,28 +879,28 @@ class Property(Record): """ LayerName record (ID 28, 29) - Properties: - .name (NString or int or None, - int is an explicit reference, - None is a flag to use Modal) - .values (List of property values or None) - .is_standard (bool, whether this is a standard property) + Attributes: + name (Union[NString, int, None]): `int` is an explicit reference, + `None` is a flag to use Modal) + values (Optional[List[property_value_t]]): List of property values. + is_standard (bool): Whether this is a standard property. """ - name = None # type: NString or int or None, - values = None # type: List[property_value_t] or None - is_standard = None # type: bool or None + name: Union[NString, int, None] = None + values: Optional[List[property_value_t]] = None + is_standard: Optional[bool] = None def __init__(self, - name: NString or str or int = None, - values: List[property_value_t] = None, - is_standard: bool = None): + name: Union[NString, str, int] = None, + values: Optional[List[property_value_t]] = None, + is_standard: Optional[bool] = None): """ - :param name: Property name, reference number, or None (i.e. use modal) - Default None. - :param values: List of property values, or None (i.e. use modal) - Default None. - :param is_standard: True if this is a standard property. None to use modal. - Default None. + Args: + name: Property name, reference number, or `None` (i.e. use modal) + Default `None. + values: List of property values, or `None` (i.e. use modal) + Default `None`. + is_standard: `True` if this is a standard property. `None` to use modal. + Default `None`. """ if isinstance(name, str): self.name = NString(name) @@ -948,24 +990,25 @@ class XName(Record): """ XName record (ID 30, 31) - Properties: - .attribute (int) - .bstring (bytes) - .reference_number (int or None, None means to use implicity numbering) + Attributes: + attribute (int): Attribute number + bstring (bytes): XName data + reference_number (Optional[int]): None means to use implicit numbering """ - attribute = None # type: int - bstring = None # type: bytes - reference_number = None # type: int or None + attribute: int + bstring: bytes + reference_number: Optional[int] = None def __init__(self, attribute: int, bstring: bytes, reference_number: int = None): """ - :param attribute: Attribute number. - :param bstring: Binary XName data. - :param reference_number: Reference number for this XName. - Default None (implicit). + Args: + attribute: Attribute number. + bstring: Binary XName data. + reference_number: Reference number for this `XName`. + Default `None` (implicit). """ self.attribute = attribute self.bstring = bstring @@ -1006,17 +1049,18 @@ class XElement(Record): """ XElement record (ID 32) - Properties: - .attribute (int) - .bstring (bytes) + Attributes: + attribute (int): Attribute number. + bstring (bytes): XElement data. """ - attribute = None # type: int - bstring = None # type: bytes + attribute: int + bstring: bytes def __init__(self, attribute: int, bstring: bytes): """ - :param attribute: Attribute number. - :param bstring: Binary data for this XElement. + Args: + attribute: Attribute number. + bstring: Binary data for this XElement. """ self.attribute = attribute self.bstring = bstring @@ -1049,22 +1093,22 @@ class XGeometry(Record): """ XGeometry record (ID 33) - Properties: - .attribute (int) - .bstring (bytes) - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means reuse modal) - .y (int or None, None means reuse modal) - .repetition (reptetition or None) + Attributes: + attribute (int): Attribute number. + bstring (bytes): XGeometry data. + layer (Optional[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): None means reuse modal + y (Optional[int]): None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - attribute = None # type: int - bstring = None # type: bytes - layer = None # type: int or None - datatype = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None + attribute: int + bstring: bytes + layer: Optional[int] = None + datatype: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None def __init__(self, attribute: int, @@ -1075,13 +1119,14 @@ class XGeometry(Record): y: int = None, repetition: repetition_t = None): """ - :param attribute: Attribute number for this XGeometry. - :param bstring: Binary data for this XGeometry. - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). + Args: + attribute: Attribute number for this XGeometry. + bstring: Binary data for this XGeometry. + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). """ self.attribute = attribute self.bstring = bstring @@ -1158,14 +1203,15 @@ class Cell(Record): """ Cell record (ID 13, 14) - Properties: - .name (NString or int specifying CellName reference number) + Attributes: + name (Union[int, NString]): int specifies "CellName reference" number """ - name = None # type: int or NString + name: Union[int, NString] - def __init__(self, name: int or NString): + def __init__(self, name: Union[int, NString]): """ - :param name: NString, or an int specifying a CellName reference number. + Args: + name: `NString`, or an int specifying a `CellName` reference number. """ self.name = name @@ -1203,44 +1249,43 @@ class Placement(Record): """ Placement record (ID 17, 18) - Properties: - .attribute (int) - .name (NString, name or - int, CellName reference number or - None, reuse modal) - .magnification (real) - .angle (real, degrees counterclockwise) - .x (int or None, None means reuse modal) - .y (int or None, None means reuse modal) - .repetition (reptetition or None) - .flip (bool) + Attributes: + name (Union[NString, int, None]): name, "CellName reference" + number, or reuse modal + magnification (real_t): Magnification factor + angle (real_t): Rotation, degrees counterclockwise + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (repetition_t or None): Repetition, if any + flip (bool): Whether to perform reflection about the x-axis. """ - name = None # type: NString or int or None - magnification = None # type: real_t or None - angle = None # type: real_t or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None - flip = None # type: bool + name: Union[NString, int, None] = None + magnification: Optional[real_t] = None + angle: Optional[real_t] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None + flip: bool def __init__(self, flip: bool, - name: NString or str or int = None, - magnification: real_t = None, - angle: real_t = None, - x: int = None, - y: int = None, - repetition: repetition_t = None): + name: Union[NString, str, int] = None, + magnification: Optional[real_t] = None, + angle: Optional[real_t] = None, + x: Optional[int] = None, + y: Optional[int] = None, + repetition: Optional[repetition_t] = None): """ - :param flip: Whether to perform reflection about the x-axis. - :param name: NString, an int specifying a CellName reference number, - or None (reuse modal). - :param magnification: Magnification factor. Default None (use modal). - :param angle: Rotation angle in degrees, counterclockwise. - Default None (reuse modal). - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). + Args: + flip: Whether to perform reflection about the x-axis. + name: `NString`, an int specifying a `CellName` reference number, + or `None` (reuse modal). + magnification: Magnification factor. Default `None` (use modal). + angle: Rotation angle in degrees, counterclockwise. + Default `None` (reuse modal). + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). """ self.x = x self.y = y @@ -1340,36 +1385,37 @@ class Text(Record): """ Text record (ID 19) - Properties: - .string (AString or int or None, None means reuse modal) - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means reuse modal) - .y (int or None, None means reuse modal) - .repetition (reptetition or None) + Attributes: + string (Union[AString, int, None]): None means reuse modal + layer (Optiona[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - string = None # type: AString or int or None - layer = None # type: int or None - datatype = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None + string: Union[AString, int, None] = None + layer: Optional[int] = None + datatype: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None def __init__(self, - string: AString or str or int = None, - layer: int = None, - datatype: int = None, - x: int = None, - y: int = None, - repetition: repetition_t = None): + string: Union[AString, str, int] = None, + layer: Optional[int] = None, + datatype: Optional[int] = None, + x: Optional[int] = None, + y: Optional[int] = None, + repetition: Optional[repetition_t] = None): """ - :param string: Text content, or TextString reference number. - Default None (use modal). - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). + Args: + string: Text content, or `TextString` reference number. + Default `None` (use modal). + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). """ self.layer = layer self.datatype = datatype @@ -1455,47 +1501,48 @@ class Rectangle(Record): """ Rectangle record (ID 20) - Properties: - .is_square (bool, True if this is a square. - If True, height must be None.) - .width (int or None, None means reuse modal) - .height (int or None, Must be None if .is_square is True. - If .is_square is False, None means reuse modal) - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means use modal) - .y (int or None, None means use modal) - .repetition (reptetition or None) + Attributes: + is_square (bool): `True` if this is a square. + If `True`, `height` must be `None`. + width (Optional[int]): `None` means reuse modal. + height (Optional[int]): Must be `None` if `is_square` is `True`. + If `is_square` is `False`, `None` means reuse modal. + layer (Optional[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - layer = None # type: int or None - datatype = None # type: int or None - width = None # type: int or None - height = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None - is_square = None # type: bool + layer: Optional[int] = None + datatype: Optional[int] = None + width: Optional[int] = None + height: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None + is_square: bool = False def __init__(self, is_square: bool = False, - layer: int = None, - datatype: int = None, - width: int = None, - height: int = None, - x: int = None, - y: int = None, - repetition: repetition_t = None): + layer: Optional[int] = None, + datatype: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + x: Optional[int] = None, + y: Optional[int] = None, + repetition: Optional[repetition_t] = None): """ - :param is_square: True if this is a square. If True, height must - be None. Default False. - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param width: X-width. Default None (reuse modal). - :param height: Y-height. Default None (reuse modal, or use width if - square). Must be None if is_square is True. - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). + Args: + is_square: `True` if this is a square. If `True`, `height` must + be `None`. Default `False`. + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + width: X-width. Default `None` (reuse modal). + height: Y-height. Default `None` (reuse modal, or use `width` if + square). Must be `None` if `is_square` is `True`. + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). """ self.is_square = is_square self.layer = layer @@ -1589,40 +1636,40 @@ class Polygon(Record): """ Polygon record (ID 21) - Properties: - .point_list ([[x0, y0], [x1, y1], ...] or None, - list is an implicitly closed path, - vertices are [int, int], - None means reuse modal) - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means reuse modal) - .y (int or None, None means reuse modal) - .repetition (reptetition or None) + Attributes: + point_list (Optional[point_list_t]): `[[x0, y0], [x1, y1], ...]` + The list is an implicitly closed path, vertices are [int, int], + `None` means reuse modal. + layer (Optional[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - layer = None # type: int or None - datatype = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None - point_list = None # type: List[List[int]] or None + layer: Optional[int] = None + datatype: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None + point_list: Optional[point_list_t] = None def __init__(self, - point_list: List[List[int]] = None, - layer: int = None, - datatype: int = None, - x: int = None, - y: int = None, - repetition: repetition_t = None): + point_list: Optional[point_list_t] = None, + layer: Optional[int] = None, + datatype: Optional[int] = None, + x: Optional[int] = None, + y: Optional[int] = None, + repetition: Optional[repetition_t] = None): """ - :param point_list: List of vertices [[x0, y0], [x1, y1], ...]. - List forms an implicitly closed path - Default None (reuse modal). - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). + Args: + point_list: List of vertices `[[x0, y0], [x1, y1], ...]`. + List forms an implicitly closed path. + Default `None` (reuse modal). + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). """ self.layer = layer self.datatype = datatype @@ -1705,63 +1752,60 @@ class Path(Record): """ Polygon record (ID 22) - Properties: - .point_list ([[x0, y0], [x1, y1], ...] or None, - vertices are [int, int], - None means reuse modal) - .half_width (int or None, None means reuse modal) - .extension_start (Tuple or None, - None means reuse modal, - Tuple is of the form - (PathExtensionScheme, int or None) - second value is None unless using - PathExtensionScheme.Arbitrary - Value determines extension past start point. - .extension_end Same form as extension_end. Value determines - extension past end point. - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means use modal) - .y (int or None, None means use modal) - .repetition (reptetition or None) + Attributes: + point_list (Optional[point_list_t]): `[[x0, y0], [x1, y1], ...]` + Vertices are [int, int]; `None` means reuse modal. + half_width (Optional[int]): None means reuse modal + extension_start (Optional[Tuple]): None means reuse modal. + Tuple is of the form (`PathExtensionScheme`, Optional[int]) + Second value is None unless using `PathExtensionScheme.Arbitrary` + Value determines extension past start point. + extension_end (Optional[Tuple]): Same form as `extension_end`. + Value determines extension past end point. + layer (Optional[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - layer = None # type: int or None - datatype = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None - point_list = None # type: List[List[int]] or None - half_width = None # type: int or None - extension_start = None # type: pathextension_t or None - extension_end = None # type: pathextension_t or None + layer: Optional[int] = None + datatype: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None + point_list: Optional[point_list_t] = None + half_width: Optional[int] = None + extension_start: Optional[pathextension_t] = None + extension_end: Optional[pathextension_t] = None def __init__(self, - point_list: List[List[int]] = None, - half_width: int = None, - extension_start: pathextension_t = None, - extension_end: pathextension_t = None, - layer: int = None, - datatype: int = None, - x: int = None, - y: int = None, - repetition: repetition_t = None): + point_list: Optional[point_list_t] = None, + half_width: Optional[int] = None, + extension_start: Optional[pathextension_t] = None, + extension_end: Optional[pathextension_t] = None, + layer: Optional[int] = None, + datatype: Optional[int] = None, + x: Optional[int] = None, + y: Optional[int] = None, + repetition: Optional[repetition_t] = None): """ - :param point_list: List of vertices [[x0, y0], [x1, y1], ...]. - Default None (reuse modal). - :param half_width: Half-width of the path. Default None (reuse modal). - :param extension_start: Specification for path extension at start of path. - None or Tuple: (PathExtensionScheme, int or None). - int is used only for PathExtensionScheme.Arbitrary. - Default None (reuse modal). - :param extension_end: Specification for path extension at end of path. - None or Tuple: (PathExtensionScheme, int or None). - int is used only for PathExtensionScheme.Arbitrary. - Default None (reuse modal). - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). + Args: + point_list: List of vertices `[[x0, y0], [x1, y1], ...]`. + Default `None` (reuse modal). + half_width: Half-width of the path. Default `None` (reuse modal). + extension_start: Specification for path extension at start of path. + `None` or `Tuple`: `(PathExtensionScheme, int or None)`. + int is used only for `PathExtensionScheme.Arbitrary`. + Default `None` (reuse modal). + extension_end: Specification for path extension at end of path. + `None` or `Tuple`: `(PathExtensionScheme, int or None)`. + int is used only for `PathExtensionScheme.Arbitrary`. + Default `None` (reuse modal). + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). """ self.layer = layer self.datatype = datatype @@ -1880,39 +1924,36 @@ class Trapezoid(Record): """ Trapezoid record (ID 23, 24, 25) - Properties: - .delta_a (int or None, - If horizontal, signed x-distance from top left - vertex to bottom left vertex. If vertical, signed - y-distance from bottom left vertex to bottom right - vertex. - None means reuse modal.) - .delta_b (int or None, - If horizontal, signed x-distance from bottom right - vertex to top right vertex. If vertical, signed - y-distance from top right vertex to top left vertex. - None means reuse modal.) - .is_vertical (bool, True if the left and right sides are aligned to - the y-axis. If the trapezoid is a rectangle, either - True or False can be used.) - .width (int or None, Bounding box x-width, None means reuse modal) - .height (int or None, Bounding box y-height, None means reuse modal) - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means se modal) - .y (int or None, None means se modal) - .repetition (reptetition or None) + Attributes: + delta_a (Optional[int]): If horizontal, signed x-distance from top left + vertex to bottom left vertex. If vertical, signed y-distance from + bottom left vertex to bottom right vertex. + None means reuse modal. + delta_b (Optional[int]): If horizontal, signed x-distance from bottom right + vertex to top right vertex. If vertical, signed y-distance from top + right vertex to top left vertex. + None means reuse modal. + is_vertical (bool): `True` if the left and right sides are aligned to + the y-axis. If the trapezoid is a rectangle, either `True` or `False` + can be used. + width (Optional[int]): Bounding box x-width, None means reuse modal. + height (Optional[int]): Bounding box y-height, None means reuse modal. + layer (Optional[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - layer = None # type: int or None - datatype = None # type: int or None - width = None # type: int or None - height = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None - delta_a = None # type: int - delta_b = None # type: int - is_vertical = None # type: bool + layer: Optional[int] = None + datatype: Optional[int] = None + width: Optional[int] = None + height: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None + delta_a: int = 0 + delta_b: int = 0 + is_vertical: bool def __init__(self, is_vertical: bool, @@ -1926,23 +1967,28 @@ class Trapezoid(Record): y: int = None, repetition: repetition_t = None): """ - :param is_vertical: True if both the left and right sides are aligned - to the y-axis. If the trapezoid is a rectangle, either value - is permitted. - :param delta_a: If horizontal, signed x-distance from top-left vertex - to bottom-left vertex. If vertical, signed y-distance from bottom- - left vertex to bottom-right vertex. Default None (reuse modal). - :param delta_b: If horizontal, signed x-distance from bottom-right vertex - to top right vertex. If vertical, signed y-distance from top-right - vertex to top-left vertex. Default None (reuse modal). - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param width: X-width of bounding box. Default None (reuse modal). - :param height: Y-height of bounding box. Default None (reuse modal) - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). - :raises: InvalidDataError if dimensions are impossible. + Args: + is_vertical: `True` if both the left and right sides are aligned + to the y-axis. If the trapezoid is a rectangle, either value + is permitted. + delta_a: If horizontal, signed x-distance from top-left vertex + to bottom-left vertex. If vertical, signed y-distance from bottom- + left vertex to bottom-right vertex. + Default `None` (reuse modal). + delta_b: If horizontal, signed x-distance from bottom-right vertex + to top right vertex. If vertical, signed y-distance from top-right + vertex to top-left vertex. + Default `None` (reuse modal). + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + width: X-width of bounding box. Default `None` (reuse modal). + height: Y-height of bounding box. Default `None` (reuse modal) + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). + + Raises: + InvalidDataError: if dimensions are impossible. """ self.is_vertical = is_vertical self.delta_a = delta_a @@ -2054,24 +2100,24 @@ class CTrapezoid(Record): """ CTrapezoid record (ID 26) - Properties: - .ctrapezoid_type (int or None, see OASIS spec for details, None means reuse modal) - .width (int or None, Bounding box x-width, None means reuse modal) - .height (int or None, Bounding box y-height, None means reuse modal) - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means se modal) - .y (int or None, None means se modal) - .repetition (reptetition or None) + Attributes: + ctrapezoid_type (Optional[int]): see OASIS spec for details, None means reuse modal. + width (Optional[int]): Bounding box x-width, None means reuse modal. + height (Optional[int]): Bounding box y-height, None means reuse modal. + layer (Optional[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - ctrapezoid_type = None # type: int or None - layer = None # type: int or None - datatype = None # type: int or None - width = None # type: int or None - height = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None + ctrapezoid_type: Optional[int] = None + layer: Optional[int] = None + datatype: Optional[int] = None + width: Optional[int] = None + height: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None def __init__(self, ctrapezoid_type: int = None, @@ -2083,16 +2129,19 @@ class CTrapezoid(Record): y: int = None, repetition: repetition_t = None): """ - :param ctrapezoid_type: CTrapezoid type; see OASIS format - documentation. Default None (reuse modal). - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param width: X-width of bounding box. Default None (reuse modal). - :param height: Y-height of bounding box. Default None (reuse modal) - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). - :raises: InvalidDataError if dimensions are invalid. + Args: + ctrapezoid_type: CTrapezoid type; see OASIS format + documentation. Default `None` (reuse modal). + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + width: X-width of bounding box. Default `None` (reuse modal). + height: Y-height of bounding box. Default `None` (reuse modal) + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). + + Raises: + InvalidDataError: if dimensions are invalid. """ self.ctrapezoid_type = ctrapezoid_type self.layer = layer @@ -2232,20 +2281,20 @@ class Circle(Record): """ Circle record (ID 27) - Properties: - .radius (int or None, None means reuse modal) - .layer (int or None, None means reuse modal) - .datatype (int or None, None means reuse modal) - .x (int or None, None means se modal) - .y (int or None, None means se modal) - .repetition (reptetition or None) + Attributes: + radius (Optional[int]): None means reuse modal + layer (Optional[int]): None means reuse modal + datatype (Optional[int]): None means reuse modal + x (Optional[int]): x-offset, None means reuse modal + y (Optional[int]): y-offset, None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any """ - layer = None # type: int or None - datatype = None # type: int or None - x = None # type: int or None - y = None # type: int or None - repetition = None # type: repetition_t or None - radius = None # type: int or None + layer: Optional[int] = None + datatype: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + repetition: Optional[repetition_t] = None + radius: Optional[int] = None def __init__(self, radius: int = None, @@ -2255,13 +2304,16 @@ class Circle(Record): y: int = None, repetition: repetition_t = None): """ - :param radius: Radius. Default None (reuse modal). - :param layer: Layer number. Default None (reuse modal). - :param datatype: Datatype number. Default None (reuse modal). - :param x: X-offset. Default None (use modal). - :param y: Y-offset. Default None (use modal). - :param repetition: Repetition. Default None (no repetition). - :raises: InvalidDataError if dimensions are invalid. + Args: + radius: Radius. Default `None` (reuse modal). + layer: Layer number. Default `None` (reuse modal). + datatype: Datatype number. Default `None` (reuse modal). + x: X-offset. Default `None` (use modal). + y: Y-offset. Default `None` (use modal). + repetition: Repetition. Default `None` (no repetition). + + Raises: + InvalidDataError: if dimensions are invalid. """ self.radius = radius self.layer = layer @@ -2340,10 +2392,13 @@ def adjust_repetition(record: Record, modals: Modals): """ Merge the record's repetition entry with the one in the modals - :param record: Record to read or modify. - :param modals: Modals to read or modify. - :raises: InvalidDataError if a ReuseRepetition can't be filled - from the modals. + Args: + record: Record to read or modify. + modals: Modals to read or modify. + + Raises: + InvalidDataError: if a `ReuseRepetition` can't be filled + from the modals. """ if record.repetition is not None: if isinstance(record.repetition, ReuseRepetition): @@ -2357,13 +2412,16 @@ def adjust_repetition(record: Record, modals: Modals): def adjust_field(record: Record, r_field: str, modals: Modals, m_field: str): """ - Merge record.r_field with modals.m_field + Merge `record.r_field` with `modals.m_field` - :param record: Record to read or modify. - :param r_field: Attr of record to access. - :param modals: Modals to read or modify. - :param m_field: Attr of modals to access. - :raises: InvalidDataError if a both fields are None + Args: + record: `Record` to read or modify. + r_field: Attr of record to access. + modals: `Modals` to read or modify. + m_field: Attr of modals to access. + + Raises: + InvalidDataError: if both fields are `None` """ r = getattr(record, r_field) if r is not None: @@ -2378,18 +2436,21 @@ def adjust_field(record: Record, r_field: str, modals: Modals, m_field: str): def adjust_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str): """ - Merge record.x and record.y with modals.mx_field and modals.my_field, - taking into account the value of modals.xy_relative. + Merge `record.x` and `record.y` with `modals.mx_field` and `modals.my_field`, + taking into account the value of `modals.xy_relative`. - If modals.xy_relative is True and the record has non-None coordinates, - the modal values are added to the record's coordinates. If modals.xy_relative - is False, the coordinates are treated the same way as other fields. + If `modals.xy_relative` is `True` and the record has non-`None` coordinates, + the modal values are added to the record's coordinates. If `modals.xy_relative` + is `False`, the coordinates are treated the same way as other fields. - :param record: Record to read or modify. - :param modals: Modals to read or modify. - :param mx_field: Attr of modals corresponding to record.x - :param my_field: Attr of modals corresponding to record.y - :raises: InvalidDataError if a both fields are None + Args: + record: `Record` to read or modify. + modals: `Modals` to read or modify. + mx_field: Attr of modals corresponding to `record.x` + my_field: Attr of modals corresponding to `record.y` + + Raises: + InvalidDataError: if both fields are `None` """ if record.x is not None: if modals.xy_relative: @@ -2414,10 +2475,13 @@ def dedup_repetition(record: Record, modals: Modals): Deduplicate the record's repetition entry with the one in the modals. Update the one in the modals if they are different. - :param record: Record to read or modify. - :param modals: Modals to read or modify. - :raises: InvalidDataError if a ReuseRepetition can't be filled - from the modals. + Args: + record: `Record` to read or modify. + modals: `Modals` to read or modify. + + Raises: + InvalidDataError: if a `ReuseRepetition` can't be filled + from the modals. """ if record.repetition is None: return @@ -2435,14 +2499,17 @@ def dedup_repetition(record: Record, modals: Modals): def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str): """ - Deduplicate record.r_field using modals.m_field - Update the modals.m_field if they are different. + Deduplicate `record.r_field` using `modals.m_field` + Update the `modals.m_field` if they are different. - :param record: Record to read or modify. - :param r_field: Attr of record to access. - :param modals: Modals to read or modify. - :param m_field: Attr of modals to access. - :raises: InvalidDataError if a both fields are None + Args: + record: `Record` to read or modify. + r_field: Attr of record to access. + modals: `Modals` to read or modify. + m_field: Attr of modals to access. + + Args: + InvalidDataError: if both fields are `None` """ r = getattr(record, r_field) m = getattr(modals, m_field) @@ -2462,18 +2529,21 @@ def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str): def dedup_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str): """ - Deduplicate record.x and record.y using modals.mx_field and modals.my_field, - taking into account the value of modals.xy_relative. + Deduplicate `record.x` and `record.y` using `modals.mx_field` and `modals.my_field`, + taking into account the value of `modals.xy_relative`. - If modals.xy_relative is True and the record has non-None coordinates, - the modal values are subtracted from the record's coordinates. If modals.xy_relative - is False, the coordinates are treated the same way as other fields. + If `modals.xy_relative` is `True` and the record has non-`None` coordinates, + the modal values are subtracted from the record's coordinates. If `modals.xy_relative` + is `False`, the coordinates are treated the same way as other fields. - :param record: Record to read or modify. - :param modals: Modals to read or modify. - :param mx_field: Attr of modals corresponding to record.x - :param my_field: Attr of modals corresponding to record.y - :raises: InvalidDataError if a both fields are None + Args: + record: `Record` to read or modify. + modals: `Modals` to read or modify. + mx_field: Attr of modals corresponding to `record.x` + my_field: Attr of modals corresponding to `record.y` + + Raises: + InvalidDataError: if both fields are `None` """ if record.x is not None: mx = getattr(modals, mx_field) From 06de10062d3e015af2124e2a25ccf42d1790334f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:35:29 -0700 Subject: [PATCH 12/49] Additional error checking --- fatamorgana/basic.py | 14 ++++++++++++++ fatamorgana/records.py | 2 ++ 2 files changed, 16 insertions(+) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 9861397..13cf703 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1826,6 +1826,9 @@ def read_interval(stream: io.BufferedIOBase) -> Tuple[Optional[int], Optional[in `(lower, upper)`, where `lower` can be `None` if there is an implicit lower bound of `0` `upper` can be `None` if there is no upper bound (`inf`) + + Raises: + InvalidDataError: On malformed data. """ interval_type = read_uint(stream) if interval_type == 0: @@ -1839,6 +1842,8 @@ def read_interval(stream: io.BufferedIOBase) -> Tuple[Optional[int], Optional[in return v, v elif interval_type == 4: return read_uint(stream), read_uint(stream) + else: + raise InvalidDataError('Unrecognized interval type: {}'.format(interval_type)) def write_interval(stream: io.BufferedIOBase, @@ -2151,13 +2156,22 @@ class Validation: Returns: Number of bytes written. + + Raises: + InvalidDataError: if the checksum type can't be handled. """ if self.checksum_type == 0: return write_uint(stream, 0) + elif self.checksum is None: + raise InvalidDataError('Checksum is empty but type is ' + '{}'.format(self.checksum_type)) elif self.checksum_type == 1: return write_uint(stream, 1) + write_u32(stream, self.checksum) elif self.checksum_type == 2: return write_uint(stream, 2) + write_u32(stream, self.checksum) + else: + raise InvalidDataError('Unrecognized checksum type: ' + '{}'.format(self.checksum_type)) def __repr__(self) -> str: return 'Validation(type: {} sum: {})'.format(self.checksum_type, self.checksum) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 3d81f14..5d8e613 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1865,6 +1865,8 @@ class Path(Record): return PathExtensionScheme.HalfWidth, None elif ext_scheme == 3: return PathExtensionScheme.Arbitrary, read_sint(stream) + else: + raise InvalidDataError('Invalid ext_scheme: {}'.format(ext_scheme)) optional['extension_start'] = get_pathext(scheme_start) optional['extension_end'] = get_pathext(scheme_end) From e9cf010f54e04f6dae6ecb122020563e9c3a1d12 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:35:42 -0700 Subject: [PATCH 13/49] fix read_u32 --- fatamorgana/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 13cf703..a650203 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -2065,7 +2065,7 @@ def read_u32(stream: io.BufferedIOBase) -> int: The integer that was read. """ b = _read(stream, 4) - return struct.unpack(' int: From 7f0c46525e2bcdea270fa2b5c7399605d2c35e62 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:38:52 -0700 Subject: [PATCH 14/49] improve type annotations --- fatamorgana/basic.py | 32 +++++++++++---------- fatamorgana/main.py | 9 +++--- fatamorgana/records.py | 63 ++++++++++++++++++++++-------------------- 3 files changed, 55 insertions(+), 49 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index a650203..24c266f 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -140,7 +140,7 @@ if _USE_NUMPY: byte_arr = _read(stream, 1) return numpy.unpackbits(numpy.frombuffer(byte_arr, dtype=numpy.uint8)) - def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[bool]) -> int: + def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[Union[bool, int], ...]) -> int: """ Pack 8 booleans into a byte, and write it to the stream. @@ -173,7 +173,7 @@ else: bits = [bool((byte >> i) & 0x01) for i in reversed(range(8))] return bits - def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[bool]) -> int: + def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[Union[bool, int], ...]) -> int: """ Pack 8 booleans into a byte, and write it to the stream. @@ -1611,6 +1611,7 @@ def write_point_list(stream: io.BufferedIOBase, return size # Try writing a bunch of Manhattan or Octangular deltas + deltas: Union[List[ManhattanDelta], List[OctangularDelta], List[Delta]] list_type = None try: deltas = [ManhattanDelta(x, y) for x, y in points] @@ -1721,6 +1722,7 @@ def read_property_value(stream: io.BufferedIOBase) -> property_value_t: Raises: InvalidDataError: if an invalid type is read. """ + ref_type: Type prop_type = read_uint(stream) if 0 <= prop_type <= 7: return read_real(stream, prop_type) @@ -1964,20 +1966,20 @@ class OffsetTable: layernames (OffsetEntry): Offset for LayerNames xnames (OffsetEntry): Offset for XNames """ - cellnames: OffsetEntry = None - textstrings: OffsetEntry = None - propnames: OffsetEntry = None - propstrings: OffsetEntry = None - layernames: OffsetEntry = None - xnames: OffsetEntry = None + cellnames: OffsetEntry + textstrings: OffsetEntry + propnames: OffsetEntry + propstrings: OffsetEntry + layernames: OffsetEntry + xnames: OffsetEntry def __init__(self, - cellnames: OffsetEntry = None, - textstrings: OffsetEntry = None, - propnames: OffsetEntry = None, - propstrings: OffsetEntry = None, - layernames: OffsetEntry = None, - xnames: OffsetEntry = None): + cellnames: Optional[OffsetEntry] = None, + textstrings: Optional[OffsetEntry] = None, + propnames: Optional[OffsetEntry] = None, + propstrings: Optional[OffsetEntry] = None, + layernames: Optional[OffsetEntry] = None, + xnames: Optional[OffsetEntry] = None): """ All parameters default to a non-strict entry with offset `0`. @@ -2204,5 +2206,5 @@ def read_magic_bytes(stream: io.BufferedIOBase): magic = _read(stream, len(MAGIC_BYTES)) if magic != MAGIC_BYTES: raise InvalidDataError('Could not read magic bytes, ' - 'found {} : {}'.format(magic, magic.decode())) + 'found {!r} : {}'.format(magic, magic.decode())) diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 8fca84e..2529ec4 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -3,12 +3,12 @@ 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 List, Dict, Union, Optional +from typing import List, Dict, Union, Optional, Type import io import logging from . import records -from .records import Modals +from .records import Modals, Record from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validation, \ read_magic_bytes, write_magic_bytes, read_uint, EOFError, \ InvalidDataError, InvalidRecordError @@ -29,7 +29,6 @@ class FileModals: xname_implicit: Optional[bool] = None textstring_implicit: Optional[bool] = None propstring_implicit: Optional[bool] = None - cellname_implicit: Optional[bool] = None within_cell: bool = False within_cblock: bool = False @@ -158,6 +157,8 @@ class OasisLayout: logger.info('read_record of type {} at position 0x{:x}'.format(record_id, stream.tell())) + record: Record + # CBlock if record_id == 34: if file_state.within_cblock: @@ -451,7 +452,7 @@ class XName: # Mapping from record id to record class. -_GEOMETRY = { +_GEOMETRY: Dict[int, Type] = { 19: records.Text, 20: records.Rectangle, 21: records.Polygon, diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 5d8e613..c223822 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -11,7 +11,7 @@ Higher-level code (e.g. monitoring for combinations of records with in main.py instead. """ from abc import ABCMeta, abstractmethod -from typing import List, Dict, Tuple, Union, Optional, Sequence +from typing import List, Dict, Tuple, Union, Optional, Sequence, Any import copy import math import zlib @@ -337,17 +337,17 @@ class Start(Record): Attributes: version (AString): "1.0" unit (real_t): positive real number, grid steps per micron - offset_table (OffsetTable or None): If `None` then table must be + offset_table (Optional[OffsetTable]): If `None` then table must be placed in the `End` record) """ version: AString unit: real_t - offset_table: OffsetTable = None + offset_table: Optional[OffsetTable] = None def __init__(self, unit: real_t, version: Union[AString, str] = None, - offset_table: OffsetTable = None): + offset_table: Optional[OffsetTable] = None): """ Args unit: Grid steps per micron (positive real number) @@ -390,6 +390,7 @@ class Start(Record): version = AString.read(stream) unit = read_real(stream) has_offset_table = read_uint(stream) == 0 + offset_table: Optional[OffsetTable] if has_offset_table: offset_table = OffsetTable.read(stream) else: @@ -448,7 +449,7 @@ class End(Record): if record_id != 2: raise InvalidDataError('Invalid record id for End {}'.format(record_id)) if has_offset_table: - offset_table = OffsetTable.read(stream) + offset_table: Optional[OffsetTable] = OffsetTable.read(stream) else: offset_table = None _padding_string = read_bstring(stream) @@ -630,7 +631,7 @@ class CellName(Record): '{}'.format(record_id)) nstring = NString.read(stream) if record_id == 4: - reference_number = read_uint(stream) + reference_number: Optional[int] = read_uint(stream) else: reference_number = None record = CellName(nstring, reference_number) @@ -684,7 +685,7 @@ class PropName(Record): '{}'.format(record_id)) nstring = NString.read(stream) if record_id == 8: - reference_number = read_uint(stream) + reference_number: Optional[int] = read_uint(stream) else: reference_number = None record = PropName(nstring, reference_number) @@ -739,7 +740,7 @@ class TextString(Record): '{}'.format(record_id)) astring = AString.read(stream) if record_id == 6: - reference_number = read_uint(stream) + reference_number: Optional[int] = read_uint(stream) else: reference_number = None record = TextString(astring, reference_number) @@ -794,7 +795,7 @@ class PropString(Record): '{}'.format(record_id)) astring = AString.read(stream) if record_id == 10: - reference_number = read_uint(stream) + reference_number: Optional[int] = read_uint(stream) else: reference_number = None record = PropString(astring, reference_number) @@ -936,6 +937,7 @@ class Property(Record): s = 0x01 & (byte >> 0) name = read_refname(stream, c, n) + values: Optional[List[property_value_t]] if v == 0: if u < 0x0f: value_count = u @@ -1028,7 +1030,7 @@ class XName(Record): attribute = read_uint(stream) bstring = read_bstring(stream) if record_id == 31: - reference_number = read_uint(stream) + reference_number: Optional[int] = read_uint(stream) else: reference_number = None record = XName(attribute, bstring, reference_number) @@ -1113,11 +1115,11 @@ class XGeometry(Record): def __init__(self, attribute: int, bstring: bytes, - layer: int = None, - datatype: int = None, - x: int = None, - y: int = None, - repetition: repetition_t = None): + layer: Optional[int] = None, + datatype: Optional[int] = None, + x: Optional[int] = None, + y: Optional[int] = None, + repetition: Optional[repetition_t] = None): """ Args: attribute: Attribute number for this XGeometry. @@ -1158,7 +1160,7 @@ class XGeometry(Record): if z0 or z1 or z2: raise InvalidDataError('Malformed XGeometry header') attribute = read_uint(stream) - optional = {} + optional: Dict[str, Any] = {} if l: optional['layer'] = read_uint(stream) if d: @@ -1223,6 +1225,7 @@ class Cell(Record): @staticmethod def read(stream: io.BufferedIOBase, record_id: int) -> 'Cell': + name: Union[int, NString] if record_id == 13: name = read_uint(stream) elif record_id == 14: @@ -1317,7 +1320,7 @@ class Placement(Record): #CNXYRAAF (17) or CNXYRMAF (18) c, n, x, y, r, ma0, ma1, flip = read_bool_byte(stream) - optional = {} + optional: Dict[str, Any] = {} name = read_refname(stream, c, n) if record_id == 17: aa = (ma0 << 1) | ma1 @@ -1451,7 +1454,7 @@ class Text(Record): if z0: raise InvalidDataError('Malformed Text header') - optional = {} + optional: Dict[str, Any] = {} string = read_refstring(stream, c, n) if l: optional['layer'] = read_uint(stream) @@ -1584,7 +1587,7 @@ class Rectangle(Record): '{}'.format(record_id)) is_square, w, h, x, y, r, d, l = read_bool_byte(stream) - optional = {} + optional: Dict[str, Any] = {} if l: optional['layer'] = read_uint(stream) if d: @@ -1706,7 +1709,7 @@ class Polygon(Record): if z0 or z1: raise InvalidDataError('Invalid polygon header') - optional = {} + optional: Dict[str, Any] = {} if l: optional['layer'] = read_uint(stream) if d: @@ -1844,7 +1847,7 @@ class Path(Record): '{}'.format(record_id)) e, w, p, x, y, r, d, l = read_bool_byte(stream) - optional = {} + optional: Dict[str, Any] = {} if l: optional['layer'] = read_uint(stream) if d: @@ -2035,7 +2038,7 @@ class Trapezoid(Record): '{}'.format(record_id)) is_vertical, w, h, x, y, r, d, l = read_bool_byte(stream) - optional = {} + optional: Dict[str, Any] = {} if l: optional['layer'] = read_uint(stream) if d: @@ -2227,7 +2230,7 @@ class CTrapezoid(Record): '{}'.format(record_id)) t, w, h, x, y, r, d, l = read_bool_byte(stream) - optional = {} + optional: Dict[str, Any] = {} if l: optional['layer'] = read_uint(stream) if d: @@ -2348,7 +2351,7 @@ class Circle(Record): if z0 or z1: raise InvalidDataError('Malformed circle header') - optional = {} + optional: Dict[str, Any] = {} if l: optional['layer'] = read_uint(stream) if d: @@ -2390,7 +2393,7 @@ class Circle(Record): return size -def adjust_repetition(record: Record, modals: Modals): +def adjust_repetition(record, modals: Modals): """ Merge the record's repetition entry with the one in the modals @@ -2412,7 +2415,7 @@ def adjust_repetition(record: Record, modals: Modals): modals.repetition = copy.copy(record.repetition) -def adjust_field(record: Record, r_field: str, modals: Modals, m_field: str): +def adjust_field(record, r_field: str, modals: Modals, m_field: str): """ Merge `record.r_field` with `modals.m_field` @@ -2436,7 +2439,7 @@ def adjust_field(record: Record, r_field: str, modals: Modals, m_field: str): raise InvalidDataError('Unfillable field: {}'.format(m_field)) -def adjust_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str): +def adjust_coordinates(record, modals: Modals, mx_field: str, my_field: str): """ Merge `record.x` and `record.y` with `modals.mx_field` and `modals.my_field`, taking into account the value of `modals.xy_relative`. @@ -2472,7 +2475,7 @@ def adjust_coordinates(record: Record, modals: Modals, mx_field: str, my_field: # TODO: Clarify the docs on the dedup_* functions -def dedup_repetition(record: Record, modals: Modals): +def dedup_repetition(record, modals: Modals): """ Deduplicate the record's repetition entry with the one in the modals. Update the one in the modals if they are different. @@ -2499,7 +2502,7 @@ def dedup_repetition(record: Record, modals: Modals): modals.repetition = record.repetition -def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str): +def dedup_field(record, r_field: str, modals: Modals, m_field: str): """ Deduplicate `record.r_field` using `modals.m_field` Update the `modals.m_field` if they are different. @@ -2529,7 +2532,7 @@ def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str): raise InvalidDataError('Unfillable field') -def dedup_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str): +def dedup_coordinates(record, modals: Modals, mx_field: str, my_field: str): """ Deduplicate `record.x` and `record.y` using `modals.mx_field` and `modals.my_field`, taking into account the value of `modals.xy_relative`. From cfb3e90d71d99efd1d4a9763b0cf04fa174ef493 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:39:27 -0700 Subject: [PATCH 15/49] Fix XYmode.absolute --- fatamorgana/records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index c223822..53d747e 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -299,7 +299,7 @@ class XYMode(Record): @property def absolute(self) -> bool: - return not relative + return not self.relative @absolute.setter def absolute(self, b: bool): From 8fb2f2b594085f7365601496af22b7b2177b882b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:40:35 -0700 Subject: [PATCH 16/49] fix attempted access to xname.string -> xname.bstring --- fatamorgana/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 2529ec4..947e295 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -340,7 +340,7 @@ class OasisLayout: for refnum, name in self.propnames.items()) xnames_offset = OffsetEntry(False, size) - size += sum(records.XName(x.attribute, x.string, refnum).dedup_write(stream, modals) + size += sum(records.XName(x.attribute, x.bstring, refnum).dedup_write(stream, modals) for refnum, x in self.xnames.items()) textstrings_offset = OffsetEntry(False, size) From 3af40dd1bc6352d0a49d5b4a538602711b42d7fb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:41:44 -0700 Subject: [PATCH 17/49] Use a dummy unit of -1 when reading (to satisfy type checker) --- fatamorgana/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 947e295..4c3e9a3 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -117,7 +117,7 @@ class OasisLayout: """ file_state = FileModals() modals = Modals() - layout = OasisLayout(unit=None) + layout = OasisLayout(unit=-1) # dummy unit read_magic_bytes(stream) From 1a259a1c19b7185b124c57ef3078e7ec8943a321 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:43:25 -0700 Subject: [PATCH 18/49] Improve ctrapezoid validity checking width/height might be None; check them against each other only if they aren't. Also, perform checks after dedup/adjust. --- fatamorgana/records.py | 57 ++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 53d747e..5e1b7dd 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -2157,27 +2157,7 @@ class CTrapezoid(Record): self.y = y self.repetition = repetition - if ctrapezoid_type in (20, 21) and width is not None: - raise InvalidDataError('CTrapezoid has spurious width entry: ' - '{}'.format(width)) - if ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25) and height is not None: - raise InvalidDataError('CTrapezoid has spurious height entry: ' - '{}'.format(height)) - if ctrapezoid_type in range(0, 4) and width < height: - raise InvalidDataError('CTrapezoid has width < height' - ' ({} < {})'.format(width, height)) - if ctrapezoid_type in range(4, 8) and width < 2 * height: - raise InvalidDataError('CTrapezoid has width < 2*height' - ' ({} < 2 * {})'.format(width, height)) - if ctrapezoid_type in range(8, 12) and width > height: - raise InvalidDataError('CTrapezoid has width > height' - ' ({} > {})'.format(width, height)) - if ctrapezoid_type in range(12, 16) and 2 * width > height: - raise InvalidDataError('CTrapezoid has 2*width > height' - ' ({} > 2 * {})'.format(width, height)) - if ctrapezoid_type is not None and ctrapezoid_type not in range(0, 26): - raise InvalidDataError('CTrapezoid has invalid type: ' - '{}'.format(ctrapezoid_type)) + self.check_valid() def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') @@ -2200,6 +2180,8 @@ class CTrapezoid(Record): else: adjust_field(self, 'height', modals, 'geometry_h') + self.check_valid() + def deduplicate_with_modals(self, modals: Modals): dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') dedup_repetition(self, modals) @@ -2223,6 +2205,8 @@ class CTrapezoid(Record): else: dedup_field(self, 'height', modals, 'geometry_h') + self.check_valid() + @staticmethod def read(stream: io.BufferedIOBase, record_id: int) -> 'CTrapezoid': if record_id != 26: @@ -2282,6 +2266,37 @@ class CTrapezoid(Record): return size + def check_valid(self): + ctrapezoid_type = self.ctrapezoid_type + width = self.width + height = self.height + + if ctrapezoid_type in (20, 21) and width is not None: + raise InvalidDataError('CTrapezoid has spurious width entry: ' + '{}'.format(width)) + if ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25) and height is not None: + raise InvalidDataError('CTrapezoid has spurious height entry: ' + '{}'.format(height)) + + if width is not None and height is not None: + if ctrapezoid_type in range(0, 4) and width < height: + raise InvalidDataError('CTrapezoid has width < height' + ' ({} < {})'.format(width, height)) + if ctrapezoid_type in range(4, 8) and width < 2 * height: + raise InvalidDataError('CTrapezoid has width < 2*height' + ' ({} < 2 * {})'.format(width, height)) + if ctrapezoid_type in range(8, 12) and width > height: + raise InvalidDataError('CTrapezoid has width > height' + ' ({} > {})'.format(width, height)) + if ctrapezoid_type in range(12, 16) and 2 * width > height: + raise InvalidDataError('CTrapezoid has 2*width > height' + ' ({} > 2 * {})'.format(width, height)) + + if ctrapezoid_type is not None and ctrapezoid_type not in range(0, 26): + raise InvalidDataError('CTrapezoid has invalid type: ' + '{}'.format(ctrapezoid_type)) + + class Circle(Record): """ Circle record (ID 27) From c9adca61b6a0da9f927c36188183318a0c9eee5f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:44:44 -0700 Subject: [PATCH 19/49] get_pathext may return None --- fatamorgana/records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 5e1b7dd..3b36649 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1859,7 +1859,7 @@ class Path(Record): scheme_end = scheme & 0b11 scheme_start = (scheme >> 2) & 0b11 - def get_pathext(ext_scheme: int) -> pathextension_t: + def get_pathext(ext_scheme: int) -> Optional[pathextension_t]: if ext_scheme == 0: return None elif ext_scheme == 1: From 25b2cecec9b2c5b2eb4b44fb41274e5f492974f6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:50:58 -0700 Subject: [PATCH 20/49] Minor changes to satisfy type checker --- fatamorgana/records.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 3b36649..62bd1da 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -218,8 +218,8 @@ class Record(metaclass=ABCMeta): def read_refname(stream: io.BufferedIOBase, - is_present: bool, - is_reference: bool + is_present: Union[bool, int], + is_reference: Union[bool, int] ) -> Union[None, int, NString]: """ Helper function for reading a possibly-absent, possibly-referenced NString. @@ -242,8 +242,8 @@ def read_refname(stream: io.BufferedIOBase, def read_refstring(stream: io.BufferedIOBase, - is_present: bool, - is_reference: bool + is_present: Union[bool, int], + is_reference: Union[bool, int], ) -> Union[None, int, AString]: """ Helper function for reading a possibly-absent, possibly-referenced `AString`. @@ -937,18 +937,18 @@ class Property(Record): s = 0x01 & (byte >> 0) name = read_refname(stream, c, n) - values: Optional[List[property_value_t]] if v == 0: if u < 0x0f: value_count = u else: value_count = read_uint(stream) - values = [read_property_value(stream) for _ in range(value_count)] + values: Optional[List[property_value_t]] = [read_property_value(stream) + for _ in range(value_count)] else: values = None if u != 0: raise InvalidDataError('Malformed property record header') - record = Property(name, values, s) + record = Property(name, values, bool(s)) logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record)) return record From 4c5f649eec9c6cf24f32d230f967710757773673 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:52:36 -0700 Subject: [PATCH 21/49] Force mypy to ignore a bunch of simple situations it's not smart enough to reason about --- fatamorgana/records.py | 142 +++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 70 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 62bd1da..3ce18f0 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -978,13 +978,13 @@ class Property(Record): size += write_byte(stream, (u << 4) | (v << 3) | (c << 2) | (n << 1) | s) if c: if n: - size += write_uint(stream, self.name) + size += write_uint(stream, self.name) # type: ignore else: - size += self.name.write(stream) + size += self.name.write(stream) # type: ignore if not v: if u == 0x0f: - size += write_uint(stream, self.name) - size += sum(write_property_value(stream, p) for p in self.values) + size += write_uint(stream, self.name) # type: ignore + size += sum(write_property_value(stream, p) for p in self.values) # type: ignore return size @@ -1188,16 +1188,16 @@ class XGeometry(Record): size += write_bool_byte(stream, (0, 0, 0, x, y, r, d, l)) size += write_uint(stream, self.attribute) if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore size += write_bstring(stream, self.bstring) if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -1352,8 +1352,8 @@ class Placement(Record): f = self.flip if self.angle is not None and self.angle % 90 == 0 and \ - self.magnification is None or self.magnification == 1: - aa = int((self.angle / 90) % 4) + self.magnification is None or self.magnification == 1: + aa = int((self.angle / 90) % 4) # type: ignore bools = (c, n, x, y, r, aa & 0b10, aa & 0b01, f) m = False a = False @@ -1368,19 +1368,19 @@ class Placement(Record): size += write_bool_byte(stream, bools) if c: if n: - size += write_uint(stream, self.name) + size += write_uint(stream, self.name) # type: ignore else: - size += self.name.write(stream) + size += self.name.write(stream) # type: ignore if m: - size += write_real(stream, self.magnification) + size += write_real(stream, self.magnification) # type: ignore if a: - size += write_real(stream, self.angle) + size += write_real(stream, self.angle) # type: ignore if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -1484,19 +1484,19 @@ class Text(Record): size += write_bool_byte(stream, (0, c, n, x, y, r, d, l)) if c: if n: - size += write_uint(stream, self.string) + size += write_uint(stream, self.string) # type: ignore else: - size += self.string.write(stream) + size += self.string.write(stream) # type: ignore if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -1619,19 +1619,19 @@ class Rectangle(Record): size = write_uint(stream, 20) size += write_bool_byte(stream, (s, w, h, x, y, r, d, l)) if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore if w: - size += write_uint(stream, self.width) + size += write_uint(stream, self.width) # type: ignore if h: - size += write_uint(stream, self.height) + size += write_uint(stream, self.height) # type: ignore if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -1737,17 +1737,18 @@ class Polygon(Record): size = write_uint(stream, 21) size += write_bool_byte(stream, (0, 0, p, x, y, r, d, l)) if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore if p: - size += write_point_list(stream, self.point_list, implicit_closed=True, fast=fast) + size += write_point_list(stream, self.point_list, # type: ignore + implicit_closed=True, fast=fast) if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -1898,11 +1899,11 @@ class Path(Record): size = write_uint(stream, 21) size += write_bool_byte(stream, (e, w, p, x, y, r, d, l)) if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore if w: - size += write_uint(stream, self.half_width) + size += write_uint(stream, self.half_width) # type: ignore if e: scheme = 0 if self.extension_start is not None: @@ -1911,17 +1912,18 @@ class Path(Record): scheme += self.extension_end[0].value size += write_uint(stream, scheme) if scheme & 0b1100 == 0b1100: - size += write_sint(stream, self.extension_start[1]) + size += write_sint(stream, self.extension_start[1]) # type: ignore if scheme & 0b0011 == 0b0011: - size += write_sint(stream, self.extension_end[1]) + size += write_sint(stream, self.extension_end[1]) # type: ignore if p: - size += write_point_list(stream, self.point_list, implicit_closed=False, fast=fast) + size += write_point_list(stream, self.point_list, # type: ignore + implicit_closed=False, fast=fast) if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -2080,23 +2082,23 @@ class Trapezoid(Record): size = write_uint(stream, record_id) size += write_bool_byte(stream, (v, w, h, x, y, r, d, l)) if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore if w: - size += write_uint(stream, self.width) + size += write_uint(stream, self.width) # type: ignore if h: - size += write_uint(stream, self.height) + size += write_uint(stream, self.height) # type: ignore if record_id != 25: - size += write_sint(stream, self.delta_a) + size += write_sint(stream, self.delta_a) # type: ignore if record_id != 24: - size += write_sint(stream, self.delta_b) + size += write_sint(stream, self.delta_b) # type: ignore if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -2248,21 +2250,21 @@ class CTrapezoid(Record): size = write_uint(stream, 26) size += write_bool_byte(stream, (t, w, h, x, y, r, d, l)) if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore if t: - size += write_uint(stream, self.ctrapezoid_type) + size += write_uint(stream, self.ctrapezoid_type) # type: ignore if w: - size += write_uint(stream, self.width) + size += write_uint(stream, self.width) # type: ignore if h: - size += write_uint(stream, self.height) + size += write_uint(stream, self.height) # type: ignore if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size @@ -2394,17 +2396,17 @@ class Circle(Record): size = write_uint(stream, 27) size += write_bool_byte(stream, (0, 0, s, x, y, r, d, l)) if l: - size += write_uint(stream, self.layer) + size += write_uint(stream, self.layer) # type: ignore if d: - size += write_uint(stream, self.datatype) + size += write_uint(stream, self.datatype) # type: ignore if s: - size += write_uint(stream, self.radius) + size += write_uint(stream, self.radius) # type: ignore if x: - size += write_sint(stream, self.x) + size += write_sint(stream, self.x) # type: ignore if y: - size += write_sint(stream, self.y) + size += write_sint(stream, self.y) # type: ignore if r: - size += self.repetition.write(stream) + size += self.repetition.write(stream) # type: ignore return size From 3b011173291bfd2e610f74250731c2f11b4fbee9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2020 15:55:20 -0700 Subject: [PATCH 22/49] Bump version number to v0.6 --- fatamorgana/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/VERSION b/fatamorgana/VERSION index 2eb3c4f..5a2a580 100644 --- a/fatamorgana/VERSION +++ b/fatamorgana/VERSION @@ -1 +1 @@ -0.5 +0.6 From 3a2d8893609653ad3bf97881e71ada7b07d68398 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 17 May 2020 17:27:11 -0700 Subject: [PATCH 23/49] add mypy_cache to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f0e9ec8..02ddec7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc __pycache__ *.idea +.mypy_cache/ build dist From 73310fe99377bc67d914cb825c98b4e070b17a99 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:16:29 -0700 Subject: [PATCH 24/49] import repetitions at top level --- fatamorgana/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fatamorgana/__init__.py b/fatamorgana/__init__.py index 9afbc81..b56fb99 100644 --- a/fatamorgana/__init__.py +++ b/fatamorgana/__init__.py @@ -28,7 +28,8 @@ import pathlib from .main import OasisLayout, Cell, XName from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \ - EOFError, SignedError, InvalidDataError, InvalidRecordError + EOFError, SignedError, InvalidDataError, InvalidRecordError, \ + ReuseRepetition, GridRepetition, ArbitraryRepetition __author__ = 'Jan Petykiewicz' From f15499030d0550af7a440e497065a62256f53ca8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:17:31 -0700 Subject: [PATCH 25/49] only write the first byte -- probably no different but clearer --- fatamorgana/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 24c266f..a14f888 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -156,7 +156,7 @@ if _USE_NUMPY: """ if len(bits) != 8: raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) - return stream.write(numpy.packbits(bits)) + return stream.write(numpy.packbits(bits)[0]) else: def read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: """ From 411012079db1a7acdfe320e680231671e8d8abad Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:19:59 -0700 Subject: [PATCH 26/49] bifurctae read_bool_byte and write_bool_byte into _np_* and _py_* variants --- fatamorgana/basic.py | 82 ++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index a14f888..c49da9b 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -125,8 +125,45 @@ def write_byte(stream: io.BufferedIOBase, n: int) -> int: return stream.write(bytes((n,))) +def _py_read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: + """ + Read a single byte from the stream, and interpret its bits as + a list of 8 booleans. + + Args: + stream: Stream to read from. + + Returns: + A list of 8 booleans corresponding to the bits (MSB first). + """ + byte = _read(stream, 1)[0] + bits = [bool((byte >> i) & 0x01) for i in reversed(range(8))] + return bits + +def _py_write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[Union[bool, int], ...]) -> int: + """ + Pack 8 booleans into a byte, and write it to the stream. + + Args: + stream: Stream to write to. + bits: A list of 8 booleans corresponding to the bits (MSB first). + + Returns: + Number of bytes written (1). + + Raises: + InvalidDataError if didn't receive 8 bits. + """ + if len(bits) != 8: + raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) + byte = 0 + for i, bit in enumerate(reversed(bits)): + byte |= bit << i + return stream.write(bytes((byte,))) + + if _USE_NUMPY: - def read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: + def _np_read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: """ Read a single byte from the stream, and interpret its bits as a list of 8 booleans. @@ -140,7 +177,7 @@ if _USE_NUMPY: byte_arr = _read(stream, 1) return numpy.unpackbits(numpy.frombuffer(byte_arr, dtype=numpy.uint8)) - def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[Union[bool, int], ...]) -> int: + def _np_write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[Union[bool, int], ...]) -> int: """ Pack 8 booleans into a byte, and write it to the stream. @@ -157,43 +194,12 @@ if _USE_NUMPY: if len(bits) != 8: raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) return stream.write(numpy.packbits(bits)[0]) + + read_bool_byte = _np_read_bool_byte + write_bool_byte = _np_write_bool_byte else: - def read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: - """ - Read a single byte from the stream, and interpret its bits as - a list of 8 booleans. - - Args: - stream: Stream to read from. - - Returns: - A list of 8 booleans corresponding to the bits (MSB first). - """ - byte = _read(stream, 1)[0] - bits = [bool((byte >> i) & 0x01) for i in reversed(range(8))] - return bits - - def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[Union[bool, int], ...]) -> int: - """ - Pack 8 booleans into a byte, and write it to the stream. - - Args: - stream: Stream to write to. - bits: A list of 8 booleans corresponding to the bits (MSB first). - - Returns: - Number of bytes written (1). - - Raises: - InvalidDataError if didn't receive 8 bits. - """ - if len(bits) != 8: - raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) - byte = 0 - for i, bit in enumerate(reversed(bits)): - byte |= bit << i - return stream.write(bytes((byte))) - + read_bool_byte = _py_read_bool_byte + write_bool_byte = _py_write_bool_byte def read_uint(stream: io.BufferedIOBase) -> int: """ From e909aa958dae3765f4cc26595162934c7bc0b7a3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:20:59 -0700 Subject: [PATCH 27/49] fix equality for things which may or may not be numpy arrays --- fatamorgana/basic.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index c49da9b..17de875 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1321,11 +1321,19 @@ class GridRepetition: return size def __eq__(self, other: Any) -> bool: - return isinstance(other, type(self)) and \ - self.a_count == other.a_count and \ - self.b_count == other.b_count and \ - self.a_vector == other.a_vector and \ - self.b_vector == other.b_vector + if not isinstance(other, type(self)): + return False + if self.a_count != other.a_count or self.b_count != other.b_count: + return False + if any(self.a_vector[ii] != other.a_vector[ii] for ii in range(2)): + return False + if self.b_vector is None and other.b_vector is None: + 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)): + return False + return True def __repr__(self) -> str: return 'GridRepetition: ({} : {} | {} : {})'.format(self.a_count, self.a_vector, From d25f3f15986afd31bff8b52ab9d0f6f4a5448a65 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:21:23 -0700 Subject: [PATCH 28/49] enable str() casts on NString and AString --- fatamorgana/basic.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 17de875..e2b9266 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -619,6 +619,9 @@ class NString: def __repr__(self) -> str: return '[N]' + self._string + def __str__(self) -> str: + return self._string + def read_nstring(stream: io.BufferedIOBase) -> str: """ @@ -730,6 +733,9 @@ class AString: def __repr__(self) -> str: return '[A]' + self._string + def __str__(self) -> str: + return self._string + def read_astring(stream: io.BufferedIOBase) -> str: """ From 715fe7ea24e09753d1443c74b3d1d79bdb270127 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:21:50 -0700 Subject: [PATCH 29/49] fix non-numpy write_point_list --- fatamorgana/basic.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index e2b9266..7bde757 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1559,10 +1559,9 @@ def read_point_list(stream: io.BufferedIOBase) -> List[List[int]]: points = [] x = 0 y = 0 - for _ in range(list_len): - delta = Delta.read(stream) - x += delta.x - y += delta.y + for delta in deltas: + x += delta[0] + y += delta[1] points.append([x, y]) else: raise InvalidDataError('Invalid point list type') From e4a62a0f326663d8149830d082f70ddf76bc5aea Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:22:53 -0700 Subject: [PATCH 30/49] fix write_interval for bounded intervals --- fatamorgana/basic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 7bde757..df1513d 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1891,8 +1891,10 @@ def write_interval(stream: io.BufferedIOBase, else: if max_bound is None: return write_uint(stream, 2) + write_uint(stream, min_bound) + elif min_bound == max_bound: + return write_uint(stream, 3) + write_uint(stream, min_bound) else: - size = write_uint(stream, 3) + size = write_uint(stream, 4) size += write_uint(stream, min_bound) size += write_uint(stream, max_bound) return size From 705926d44345d0d9c9e494ddc6864a115a21ea34 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:42:42 -0700 Subject: [PATCH 31/49] Add UnfilledModalError, records.verify_modal(), and .get_*() methods. The .get_*() methods are used to verify that we aren't reading from a pattern with un-filled modals. The GeometryMixin class was also added here and provides some additional convenience methods: get_xy() to get an (x,y) tuple and get_layer_tuple() to get a (layer, datatype) tuple. --- fatamorgana/__init__.py | 1 + fatamorgana/basic.py | 7 ++- fatamorgana/records.py | 135 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 131 insertions(+), 12 deletions(-) diff --git a/fatamorgana/__init__.py b/fatamorgana/__init__.py index b56fb99..1ffb62a 100644 --- a/fatamorgana/__init__.py +++ b/fatamorgana/__init__.py @@ -29,6 +29,7 @@ import pathlib from .main import OasisLayout, Cell, XName from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \ EOFError, SignedError, InvalidDataError, InvalidRecordError, \ + UnfilledModalError, \ ReuseRepetition, GridRepetition, ArbitraryRepetition diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index df1513d..218f80c 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -59,6 +59,12 @@ class InvalidRecordError(FatamorganaError): """ pass +class UnfilledModalError(FatamorganaError): + """ + Attempted to call .get_var(), but var() was None! + """ + pass + class PathExtensionScheme(Enum): """ @@ -2228,4 +2234,3 @@ def read_magic_bytes(stream: io.BufferedIOBase): if magic != MAGIC_BYTES: raise InvalidDataError('Could not read magic bytes, ' 'found {!r} : {}'.format(magic, magic.decode())) - diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 3ce18f0..490b22c 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -11,7 +11,7 @@ Higher-level code (e.g. monitoring for combinations of records with in main.py instead. """ from abc import ABCMeta, abstractmethod -from typing import List, Dict, Tuple, Union, Optional, Sequence, Any +from typing import List, Dict, Tuple, Union, Optional, Sequence, Any, TypeVar import copy import math import zlib @@ -24,7 +24,7 @@ from .basic import AString, NString, repetition_t, property_value_t, real_t, \ read_bstring, read_uint, read_sint, read_real, read_repetition, read_interval, \ write_bstring, write_uint, write_sint, write_real, write_interval, write_point_list, \ write_property_value, read_bool_byte, write_bool_byte, read_byte, write_byte, \ - InvalidDataError, PathExtensionScheme, _USE_NUMPY + InvalidDataError, UnfilledModalError, PathExtensionScheme, _USE_NUMPY if _USE_NUMPY: import numpy @@ -32,6 +32,7 @@ if _USE_NUMPY: logger = logging.getLogger(__name__) + ''' Type definitions ''' @@ -112,6 +113,12 @@ class Modals: self.property_is_standard = None +T = TypeVar('T') +def verify_modal(var: Optional[T]) -> T: + if var is None: + raise UnfilledModalError + return var + ''' Records @@ -217,6 +224,34 @@ class Record(metaclass=ABCMeta): return '{}: {}'.format(self.__class__, pprint.pformat(self.__dict__)) +class GeometryMixin(metaclass=ABCMeta): + """ + Mixin defining common functions for geometry records + """ + x: Optional[int] + y: Optional[int] + layer: Optional[int] + datatype: Optional[int] + + def get_x(self) -> int: + return verify_modal(self.x) + + def get_y(self) -> int: + return verify_modal(self.y) + + def get_xy(self) -> Tuple[int, int]: + return (self.get_x(), self.get_y()) + + def get_layer(self) -> int: + return verify_modal(self.layer) + + def get_datatype(self) -> int: + return verify_modal(self.datatype) + + def get_layer_tuple(self) -> Tuple[int, int]: + return (self.get_layer(), self.get_datatype()) + + def read_refname(stream: io.BufferedIOBase, is_present: Union[bool, int], is_reference: Union[bool, int] @@ -910,6 +945,15 @@ class Property(Record): self.values = values self.is_standard = is_standard + def get_name(self) -> Union[NString, int]: + return verify_modal(self.name) # type: ignore + + def get_values(self) -> List[property_value_t]: + return verify_modal(self.values) + + def get_is_standard(self) -> bool: + return verify_modal(self.is_standard) + def merge_with_modals(self, modals: Modals): adjust_field(self, 'name', modals, 'property_name') adjust_field(self, 'values', modals, 'property_value_list') @@ -1091,7 +1135,7 @@ class XElement(Record): return size -class XGeometry(Record): +class XGeometry(Record, GeometryMixin): """ XGeometry record (ID 33) @@ -1301,6 +1345,15 @@ class Placement(Record): else: self.name = name + def get_name(self) -> Union[NString, int]: + return verify_modal(self.name) # type: ignore + + def get_x(self) -> int: + return verify_modal(self.x) + + def get_y(self) -> int: + return verify_modal(self.y) + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'placement_x', 'placement_y') adjust_repetition(self, modals) @@ -1384,7 +1437,7 @@ class Placement(Record): return size -class Text(Record): +class Text(Record, GeometryMixin): """ Text record (ID 19) @@ -1430,6 +1483,9 @@ class Text(Record): else: self.string = string + def get_string(self) -> Union[AString, int]: + return verify_modal(self.string) # type: ignore + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'text_x', 'text_y') adjust_repetition(self, modals) @@ -1500,7 +1556,7 @@ class Text(Record): return size -class Rectangle(Record): +class Rectangle(Record, GeometryMixin): """ Rectangle record (ID 20) @@ -1558,6 +1614,14 @@ class Rectangle(Record): if is_square and self.height is not None: raise InvalidDataError('Rectangle is square and also has height') + def get_width(self) -> int: + return verify_modal(self.width) + + def get_height(self) -> int: + if self.is_square: + return verify_modal(self.width) + return verify_modal(self.height) + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') adjust_repetition(self, modals) @@ -1635,7 +1699,7 @@ class Rectangle(Record): return size -class Polygon(Record): +class Polygon(Record, GeometryMixin): """ Polygon record (ID 21) @@ -1685,6 +1749,9 @@ class Polygon(Record): if len(point_list) < 3: warn('Polygon with < 3 points') + def get_point_list(self) -> point_list_t: + return verify_modal(self.point_list) + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') adjust_repetition(self, modals) @@ -1752,7 +1819,7 @@ class Polygon(Record): return size -class Path(Record): +class Path(Record, GeometryMixin): """ Polygon record (ID 22) @@ -1821,6 +1888,18 @@ class Path(Record): self.extension_start = extension_start self.extension_end = extension_end + def get_point_list(self) -> point_list_t: + return verify_modal(self.point_list) + + def get_half_width(self) -> int: + return verify_modal(self.half_width) + + def get_extension_start(self) -> pathextension_t: + return verify_modal(self.extension_start) + + def get_extension_end(self) -> pathextension_t: + return verify_modal(self.extension_end) + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') adjust_repetition(self, modals) @@ -1927,7 +2006,7 @@ class Path(Record): return size -class Trapezoid(Record): +class Trapezoid(Record, GeometryMixin): """ Trapezoid record (ID 23, 24, 25) @@ -2017,6 +2096,21 @@ class Trapezoid(Record): raise InvalidDataError('Trapezoid: w < delta_b - delta_a' ' ({} < {} - {})'.format(width, delta_b, delta_a)) + def get_is_vertical(self) -> bool: + return verify_modal(self.is_vertical) + + def get_delta_a(self) -> int: + return verify_modal(self.delta_a) + + def get_delta_b(self) -> int: + return verify_modal(self.delta_b) + + def get_width(self) -> int: + return verify_modal(self.width) + + def get_height(self) -> int: + return verify_modal(self.height) + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') adjust_repetition(self, modals) @@ -2103,7 +2197,7 @@ class Trapezoid(Record): # TODO: CTrapezoid type descriptions -class CTrapezoid(Record): +class CTrapezoid(Record, GeometryMixin): """ CTrapezoid record (ID 26) @@ -2161,6 +2255,23 @@ class CTrapezoid(Record): self.check_valid() + def get_ctrapezoid_type(self) -> int: + return verify_modal(self.ctrapezoid_type) + + def get_height(self) -> int: + if self.ctrapezoid_type is None: + return verify_modal(self.height) + if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25): + return verify_modal(self.width) + return verify_modal(self.height) + + def get_width(self) -> int: + if self.ctrapezoid_type is None: + return verify_modal(self.width) + if self.ctrapezoid_type in (20, 21): + return verify_modal(self.height) + return verify_modal(self.width) + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') adjust_repetition(self, modals) @@ -2267,7 +2378,6 @@ class CTrapezoid(Record): size += self.repetition.write(stream) # type: ignore return size - def check_valid(self): ctrapezoid_type = self.ctrapezoid_type width = self.width @@ -2299,7 +2409,7 @@ class CTrapezoid(Record): '{}'.format(ctrapezoid_type)) -class Circle(Record): +class Circle(Record, GeometryMixin): """ Circle record (ID 27) @@ -2344,6 +2454,9 @@ class Circle(Record): self.y = y self.repetition = repetition + def get_radius(self) -> int: + return verify_modal(self.radius) + def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') adjust_repetition(self, modals) From 99283aaaf00b041f057b801d964b4d011c42ff7c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:47:17 -0700 Subject: [PATCH 32/49] enable passing in str where an AString or NString is needed. --- fatamorgana/main.py | 4 ++-- fatamorgana/records.py | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 4c3e9a3..56e4e41 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -386,12 +386,12 @@ class Cell: placements: List[records.Placement] geometry: List[records.geometry_t] - def __init__(self, name: Union[NString, int]): + def __init__(self, name: Union[NString, str, int]): """ Args: name: `NString` or "CellName reference" number """ - self.name = name + self.name = name if isinstance(name, (NString, int)) else NString(name) self.properties = [] self.placements = [] self.geometry = [] diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 490b22c..7fcc9df 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -921,12 +921,12 @@ class Property(Record): values (Optional[List[property_value_t]]): List of property values. is_standard (bool): Whether this is a standard property. """ - name: Union[NString, int, None] = None + name: Optional[Union[NString, int]] = None values: Optional[List[property_value_t]] = None is_standard: Optional[bool] = None def __init__(self, - name: Union[NString, str, int] = None, + name: Union[NString, str, int, None] = None, values: Optional[List[property_value_t]] = None, is_standard: Optional[bool] = None): """ @@ -938,10 +938,10 @@ class Property(Record): is_standard: `True` if this is a standard property. `None` to use modal. Default `None`. """ - if isinstance(name, str): - self.name = NString(name) - else: + if isinstance(name, (NString, int)) or name is None: self.name = name + else: + self.name = NString(name) self.values = values self.is_standard = is_standard @@ -1254,12 +1254,12 @@ class Cell(Record): """ name: Union[int, NString] - def __init__(self, name: Union[int, NString]): + def __init__(self, name: Union[int, str, NString]): """ Args: name: `NString`, or an int specifying a `CellName` reference number. """ - self.name = name + self.name = name if isinstance(name, (int, NString)) else NString(name) def merge_with_modals(self, modals: Modals): modals.reset() @@ -1316,7 +1316,7 @@ class Placement(Record): def __init__(self, flip: bool, - name: Union[NString, str, int] = None, + name: Union[NString, str, int, None] = None, magnification: Optional[real_t] = None, angle: Optional[real_t] = None, x: Optional[int] = None, @@ -1340,10 +1340,10 @@ class Placement(Record): self.flip = flip self.magnification = magnification self.angle = angle - if isinstance(name, str): - self.name = NString(name) - else: + if isinstance(name, (int, NString)) or name is None: self.name = name + else: + self.name = NString(name) def get_name(self) -> Union[NString, int]: return verify_modal(self.name) # type: ignore @@ -1449,7 +1449,7 @@ class Text(Record, GeometryMixin): y (Optional[int]): y-offset, None means reuse modal repetition (Optional[repetition_t]): Repetition, if any """ - string: Union[AString, int, None] = None + string: Optional[Union[AString, int]] = None layer: Optional[int] = None datatype: Optional[int] = None x: Optional[int] = None @@ -1457,7 +1457,7 @@ class Text(Record, GeometryMixin): repetition: Optional[repetition_t] = None def __init__(self, - string: Union[AString, str, int] = None, + string: Union[AString, str, int, None] = None, layer: Optional[int] = None, datatype: Optional[int] = None, x: Optional[int] = None, @@ -1478,10 +1478,10 @@ class Text(Record, GeometryMixin): self.x = x self.y = y self.repetition = repetition - if isinstance(string, str): - self.string = AString(string) - else: + if isinstance(string, (AString, int)) or string is None: self.string = string + else: + self.string = AString(string) def get_string(self) -> Union[AString, int]: return verify_modal(self.string) # type: ignore From 55638fcde517abe9dd7afe5d81f8c9b30ed8f2aa Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:48:22 -0700 Subject: [PATCH 33/49] fix placement rotation (float modulo int was always returning 0??) --- fatamorgana/records.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 7fcc9df..c8e2d9b 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1404,9 +1404,9 @@ class Placement(Record): r = self.repetition is not None f = self.flip - if self.angle is not None and self.angle % 90 == 0 and \ - self.magnification is None or self.magnification == 1: - aa = int((self.angle / 90) % 4) # type: ignore + if ((self.magnification is None or self.magnification == 1) and + ((self.angle is None or abs(self.angle % 90.0) < 1e-14))): + aa = int((self.angle / 90) % 4.0) # type: ignore bools = (c, n, x, y, r, aa & 0b10, aa & 0b01, f) m = False a = False From aaef1221781f8bb2f52c84c56a0533d31062e484 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:49:42 -0700 Subject: [PATCH 34/49] fixup array equality checking in the case where _USE_NUMPY is false but we somehow still get an ndarray mostly happens during testing --- fatamorgana/records.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index c8e2d9b..8d6db53 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -2649,10 +2649,13 @@ def dedup_field(record, r_field: str, modals: Modals, m_field: str): r = getattr(record, r_field) m = getattr(modals, m_field) if r is not None: - if _USE_NUMPY and m_field in ('polygon_point_list', 'path_point_list'): - equal = numpy.array_equal(m, r) + if m_field in ('polygon_point_list', 'path_point_list'): + if _USE_NUMPY: + equal = numpy.array_equal(m, r) + else: + equal = (m is not None) and all(tuple(mm) == tuple(rr) for mm, rr in zip(m, r)) else: - equal = m is not None and m == r + equal = (m is not None) and m == r if equal: setattr(record, r_field, None) From 492d6416db97bb9a5ce065c175f9c8ca563c9073 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:51:10 -0700 Subject: [PATCH 35/49] Docstring updates: CTrapezoid info, Polygon+Path point_list description improvement, and better description of where x and y point to --- fatamorgana/records.py | 177 +++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 96 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 8d6db53..b908306 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1560,17 +1560,21 @@ class Rectangle(Record, GeometryMixin): """ Rectangle record (ID 20) + (x, y) denotes the lower-left (min-x, min-y) corner of the rectangle. + Attributes: is_square (bool): `True` if this is a square. If `True`, `height` must be `None`. - width (Optional[int]): `None` means reuse modal. - height (Optional[int]): Must be `None` if `is_square` is `True`. + width (Optional[int]): X-width. `None` means reuse modal. + height (Optional[int]): Y-height. Must be `None` if `is_square` is `True`. If `is_square` is `False`, `None` means reuse modal. layer (Optional[int]): None means reuse modal datatype (Optional[int]): None means reuse modal - x (Optional[int]): x-offset, None means reuse modal - y (Optional[int]): y-offset, None means reuse modal - repetition (Optional[repetition_t]): Repetition, if any + x (Optional[int]): x-offset of the rectangle's lower-left (min-x) point. + None means reuse modal. + y (Optional[int]): y-offset of the rectangle's lower-left (min-y) point. + None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1590,19 +1594,6 @@ class Rectangle(Record, GeometryMixin): x: Optional[int] = None, y: Optional[int] = None, repetition: Optional[repetition_t] = None): - """ - Args: - is_square: `True` if this is a square. If `True`, `height` must - be `None`. Default `False`. - layer: Layer number. Default `None` (reuse modal). - datatype: Datatype number. Default `None` (reuse modal). - width: X-width. Default `None` (reuse modal). - height: Y-height. Default `None` (reuse modal, or use `width` if - square). Must be `None` if `is_square` is `True`. - x: X-offset. Default `None` (use modal). - y: Y-offset. Default `None` (use modal). - repetition: Repetition. Default `None` (no repetition). - """ self.is_square = is_square self.layer = layer self.datatype = datatype @@ -1704,14 +1695,21 @@ class Polygon(Record, GeometryMixin): Polygon record (ID 21) Attributes: - point_list (Optional[point_list_t]): `[[x0, y0], [x1, y1], ...]` + point_list (Optional[point_list_t]): List of offsets from the + initial vertex (x, y) to the remaining vertices, + `[[dx0, dy0], [dx1, dy1], ...]`. The list is an implicitly closed path, vertices are [int, int], + The initial vertex is located at (x, y) and is not represented + in `point_list`. `None` means reuse modal. - layer (Optional[int]): None means reuse modal - datatype (Optional[int]): None means reuse modal - x (Optional[int]): x-offset, None means reuse modal - y (Optional[int]): y-offset, None means reuse modal - repetition (Optional[repetition_t]): Repetition, if any + layer (Optional[int]): Layer number. None means reuse modal + datatype (Optional[int]): Datatype number. None means reuse modal + x (Optional[int]): x-offset of the polygon's first point. + None means reuse modal + y (Optional[int]): y-offset of the polygon's first point. + None means reuse modal + repetition (Optional[repetition_t]): Repetition, if any. + Default no repetition. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1727,17 +1725,6 @@ class Polygon(Record, GeometryMixin): x: Optional[int] = None, y: Optional[int] = None, repetition: Optional[repetition_t] = None): - """ - Args: - point_list: List of vertices `[[x0, y0], [x1, y1], ...]`. - List forms an implicitly closed path. - Default `None` (reuse modal). - layer: Layer number. Default `None` (reuse modal). - datatype: Datatype number. Default `None` (reuse modal). - x: X-offset. Default `None` (use modal). - y: Y-offset. Default `None` (use modal). - repetition: Repetition. Default `None` (no repetition). - """ self.layer = layer self.datatype = datatype self.x = x @@ -1824,8 +1811,12 @@ class Path(Record, GeometryMixin): Polygon record (ID 22) Attributes: - point_list (Optional[point_list_t]): `[[x0, y0], [x1, y1], ...]` - Vertices are [int, int]; `None` means reuse modal. + point_list (Optional[point_list_t]): List of offsets from the + initial vertex (x, y) to the remaining vertices, + `[[dx0, dy0], [dx1, dy1], ...]`. + The initial vertex is located at (x, y) and is not represented + in `point_list`. + Offsets are [int, int]; `None` means reuse modal. half_width (Optional[int]): None means reuse modal extension_start (Optional[Tuple]): None means reuse modal. Tuple is of the form (`PathExtensionScheme`, Optional[int]) @@ -1859,25 +1850,6 @@ class Path(Record, GeometryMixin): x: Optional[int] = None, y: Optional[int] = None, repetition: Optional[repetition_t] = None): - """ - Args: - point_list: List of vertices `[[x0, y0], [x1, y1], ...]`. - Default `None` (reuse modal). - half_width: Half-width of the path. Default `None` (reuse modal). - extension_start: Specification for path extension at start of path. - `None` or `Tuple`: `(PathExtensionScheme, int or None)`. - int is used only for `PathExtensionScheme.Arbitrary`. - Default `None` (reuse modal). - extension_end: Specification for path extension at end of path. - `None` or `Tuple`: `(PathExtensionScheme, int or None)`. - int is used only for `PathExtensionScheme.Arbitrary`. - Default `None` (reuse modal). - layer: Layer number. Default `None` (reuse modal). - datatype: Datatype number. Default `None` (reuse modal). - x: X-offset. Default `None` (use modal). - y: Y-offset. Default `None` (use modal). - repetition: Repetition. Default `None` (no repetition). - """ self.layer = layer self.datatype = datatype self.x = x @@ -2010,6 +1982,9 @@ class Trapezoid(Record, GeometryMixin): """ Trapezoid record (ID 23, 24, 25) + Trapezoid with at least two sides parallel to the x- or y-axis. + (x, y) denotes the lower-left (min-x, min-y) corner of the trapezoid's bounding box. + Attributes: delta_a (Optional[int]): If horizontal, signed x-distance from top left vertex to bottom left vertex. If vertical, signed y-distance from @@ -2026,8 +2001,10 @@ class Trapezoid(Record, GeometryMixin): height (Optional[int]): Bounding box y-height, None means reuse modal. layer (Optional[int]): None means reuse modal datatype (Optional[int]): None means reuse modal - x (Optional[int]): x-offset, None means reuse modal - y (Optional[int]): y-offset, None means reuse modal + x (Optional[int]): x-offset to lower-left corner of the trapezoid's bounding box. + None means reuse modal + y (Optional[int]): y-offset to lower-left corner of the trapezoid's bounding box. + None means reuse modal repetition (Optional[repetition_t]): Repetition, if any """ layer: Optional[int] = None @@ -2053,26 +2030,6 @@ class Trapezoid(Record, GeometryMixin): y: int = None, repetition: repetition_t = None): """ - Args: - is_vertical: `True` if both the left and right sides are aligned - to the y-axis. If the trapezoid is a rectangle, either value - is permitted. - delta_a: If horizontal, signed x-distance from top-left vertex - to bottom-left vertex. If vertical, signed y-distance from bottom- - left vertex to bottom-right vertex. - Default `None` (reuse modal). - delta_b: If horizontal, signed x-distance from bottom-right vertex - to top right vertex. If vertical, signed y-distance from top-right - vertex to top-left vertex. - Default `None` (reuse modal). - layer: Layer number. Default `None` (reuse modal). - datatype: Datatype number. Default `None` (reuse modal). - width: X-width of bounding box. Default `None` (reuse modal). - height: Y-height of bounding box. Default `None` (reuse modal) - x: X-offset. Default `None` (use modal). - y: Y-offset. Default `None` (use modal). - repetition: Repetition. Default `None` (no repetition). - Raises: InvalidDataError: if dimensions are impossible. """ @@ -2196,19 +2153,58 @@ class Trapezoid(Record, GeometryMixin): return size -# TODO: CTrapezoid type descriptions class CTrapezoid(Record, GeometryMixin): """ CTrapezoid record (ID 26) - Attributes: - ctrapezoid_type (Optional[int]): see OASIS spec for details, None means reuse modal. - width (Optional[int]): Bounding box x-width, None means reuse modal. - height (Optional[int]): Bounding box y-height, None means reuse modal. + Compact trapezoid formats. + Two sides are assumed to be parallel to the x- or y-axis, and the remaining + sides form 45 or 90 degree angles with them. + + `ctrapezoid_type` is in `range(0, 26)`, with the following shapes: + ____ ____ _____ ______ + | 0 \ / 2 | / 4 \ / 6 / + |_____\ /_____| /_______\ /_____/ + ______ ______ _________ ______ + | 1 / \ 3 | \ 5 / \ 7 \ + |____/ \____| \_____/ \_____\ + w >= h w >= 2h + + ___ ___ |\ /| /| |\ + |\ /| | | | | | \ / | / | | \ + | \ / | |10 | | 11| |12| |13| |14| |15| + | \ / | | / \ | | | | | | | | | + | 8 | | 9 | | / \ | | / \ | | / \ | + |___| |___| |/ \| |/ \| |/ \| + h >= w h >= w h >= 2w h >= 2w + + __________ + |\ /| /\ |\ /| | 24 | (rect) + | \ / | / \ | \ / | |__________| + |16\ /18| / 20 \ |22\ /23| + |___\ /___| /______\ | / \ | + ____ ____ ______ | / \ | + | / \ | \ / |/ \| _____ + |17/ \19| \ 21 / h = 2w | | (sqr) + | / \ | \ / set h = None | 25 | + |/ \| \/ |_____| + w = h w = 2h w = h + set h = None set w = None set h = None + + + Attributes: + ctrapezoid_type (Optional[int]): See above for details. + None means reuse modal. + width (Optional[int]): Bounding box x-width. + None means unnecessary, or reuse modal if necessary. + height (Optional[int]): Bounding box y-height. + None means unnecessary, or reuse modal if necessary. layer (Optional[int]): None means reuse modal datatype (Optional[int]): None means reuse modal - x (Optional[int]): x-offset, None means reuse modal - y (Optional[int]): y-offset, None means reuse modal + x (Optional[int]): x-offset of lower-left (min-x) point of bounding box. + None means reuse modal + y (Optional[int]): y-offset of lower-left (min-y) point of bounding box. + None means reuse modal repetition (Optional[repetition_t]): Repetition, if any """ ctrapezoid_type: Optional[int] = None @@ -2230,17 +2226,6 @@ class CTrapezoid(Record, GeometryMixin): y: int = None, repetition: repetition_t = None): """ - Args: - ctrapezoid_type: CTrapezoid type; see OASIS format - documentation. Default `None` (reuse modal). - layer: Layer number. Default `None` (reuse modal). - datatype: Datatype number. Default `None` (reuse modal). - width: X-width of bounding box. Default `None` (reuse modal). - height: Y-height of bounding box. Default `None` (reuse modal) - x: X-offset. Default `None` (use modal). - y: Y-offset. Default `None` (use modal). - repetition: Repetition. Default `None` (no repetition). - Raises: InvalidDataError: if dimensions are invalid. """ From fab80c8517748694251c3cadc9f7c3e13f318bc7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:51:24 -0700 Subject: [PATCH 36/49] cosmetic change to code --- fatamorgana/basic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 218f80c..f5abde3 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1677,7 +1677,8 @@ def write_point_list(stream: io.BufferedIOBase, previous = [0, 0] diff = [] for point in points: - d = [point[0] - previous[0], point[1] - previous[1]] + d = [point[0] - previous[0], + point[1] - previous[1]] previous = point diff.append(d) From fdf5e9f59805f78cb8691a19842e5e36e3f2e41e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:51:40 -0700 Subject: [PATCH 37/49] enable type checking for downstream --- fatamorgana/py.typed | 0 setup.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 fatamorgana/py.typed diff --git a/fatamorgana/py.typed b/fatamorgana/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 1a1f5c1..f7c40ea 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,9 @@ setup(name='fatamorgana', ], packages=find_packages(), package_data={ - 'fatamorgana': ['VERSION'] + 'fatamorgana': ['VERSION', + 'py.typed', + ], }, install_requires=[ 'typing', From 86c1e4cd3babee8902449ca09969a3b7cb3b30ac Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 19 May 2020 00:52:52 -0700 Subject: [PATCH 38/49] bump version to v0.7 --- fatamorgana/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/VERSION b/fatamorgana/VERSION index 5a2a580..eb49d7c 100644 --- a/fatamorgana/VERSION +++ b/fatamorgana/VERSION @@ -1 +1 @@ -0.6 +0.7 From 94555c1b6ed2130175892dd47d3e168ef9f2c6ee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 20 May 2020 21:07:29 -0700 Subject: [PATCH 39/49] Update modal coordinates even if we are in relative mode --- fatamorgana/records.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index b908306..4741cfb 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -2575,16 +2575,14 @@ def adjust_coordinates(record, modals: Modals, mx_field: str, my_field: str): if record.x is not None: if modals.xy_relative: record.x += getattr(modals, mx_field) - else: - setattr(modals, mx_field, record.x) + setattr(modals, mx_field, record.x) else: record.x = getattr(modals, mx_field) if record.y is not None: if modals.xy_relative: record.y += getattr(modals, my_field) - else: - setattr(modals, my_field, record.y) + setattr(modals, my_field, record.y) else: record.y = getattr(modals, my_field) @@ -2672,6 +2670,7 @@ def dedup_coordinates(record, modals: Modals, mx_field: str, my_field: str): mx = getattr(modals, mx_field) if modals.xy_relative: record.x -= mx + setattr(modals, mx_field, record.x) else: if record.x == mx: record.x = None @@ -2682,6 +2681,7 @@ def dedup_coordinates(record, modals: Modals, mx_field: str, my_field: str): my = getattr(modals, my_field) if modals.xy_relative: record.y -= my + setattr(modals, my_field, record.y) else: if record.y == my: record.y = None From 4b7b6b82c1dcea668428b24d0de66da679360417 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 20 May 2020 21:09:15 -0700 Subject: [PATCH 40/49] bump version to v0.8 --- fatamorgana/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/VERSION b/fatamorgana/VERSION index eb49d7c..aec258d 100644 --- a/fatamorgana/VERSION +++ b/fatamorgana/VERSION @@ -1 +1 @@ -0.7 +0.8 From 5ac774c3866755af5640bd417f7dc19f9c641813 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 3 Jul 2020 13:22:29 -0700 Subject: [PATCH 41/49] drop OS tags from package --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index f7c40ea..66941c3 100644 --- a/setup.py +++ b/setup.py @@ -45,8 +45,6 @@ setup(name='fatamorgana', 'Intended Audience :: Manufacturing', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Operating System :: POSIX :: Linux', - 'Operating System :: Microsoft :: Windows', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', ], From bc15a66ecc0f98c5c190eec3a3a1b1ee7397414a Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 3 Jul 2020 13:25:48 -0700 Subject: [PATCH 42/49] remove extra assignments to *_count and *_vector, and adjust validity checks --- fatamorgana/basic.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index f5abde3..42eefac 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1206,25 +1206,20 @@ class GridRepetition: InvalidDataError: if `b_count` and `b_vector` inputs conflict with each other or if `a_count < 1`. """ - self.a_vector = a_vector - self.b_vector = b_vector - self.a_count = a_count - self.b_count = b_count - - if self.b_vector is None or self.b_count is None: - if self.b_vector is not None or self.b_count is not None: + 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' 'b_vector and b_count') else: - if self.b_count < 1: + if b_count < 1: raise InvalidDataError('Repetition has too-small b_count') - if self.b_count < 2: - self.b_count = None - self.b_vector = None + if b_count < 2: + b_count = None + b_vector = None warnings.warn('Removed b_count and b_vector since b_count == 1') - if self.a_count < 2: - raise InvalidDataError('Repetition has too-small x-count: ' + if a_count < 2: + raise InvalidDataError('Repetition has too-small a_count: ' '{}'.format(a_count)) self.a_vector = a_vector self.b_vector = b_vector From 97f2bb12388ee3845773c04a19b4701c514acaf9 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 3 Jul 2020 13:27:57 -0700 Subject: [PATCH 43/49] add `docs` to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 02ddec7..27d5b19 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ build dist fatamorgana.egg-info +docs From a80ac6199a0b3bcb0b014a9709626754bfae0e8a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Sep 2020 19:54:03 -0700 Subject: [PATCH 44/49] Record type 17 (Placement) should not allow modal angle or magnification --- fatamorgana/records.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 4741cfb..45ab6ef 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1404,9 +1404,9 @@ class Placement(Record): r = self.repetition is not None f = self.flip - if ((self.magnification is None or self.magnification == 1) and - ((self.angle is None or abs(self.angle % 90.0) < 1e-14))): - aa = int((self.angle / 90) % 4.0) # type: ignore + if (self.magnification == 1 and + self.angle is not None and abs(self.angle % 90.0) < 1e-14): + aa = int((self.angle / 90) % 4.0) bools = (c, n, x, y, r, aa & 0b10, aa & 0b01, f) m = False a = False From 2c2013a0fc87416c3080f272f0e2718207487f2f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Sep 2020 19:54:25 -0700 Subject: [PATCH 45/49] move typing imports to top of file --- fatamorgana/basic.py | 2 +- fatamorgana/records.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 42eefac..6342cc2 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -2,8 +2,8 @@ This module contains all datatypes and parsing/writing functions for all abstractions below the 'record' or 'block' level. """ -from fractions import Fraction from typing import List, Tuple, Type, Union, Optional, Any, Sequence +from fractions import Fraction from enum import Enum import math import struct diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 45ab6ef..42bf56e 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -10,8 +10,8 @@ Higher-level code (e.g. monitoring for combinations of records with parse, or code for dealing with nested records in a CBlock) should live in main.py instead. """ -from abc import ABCMeta, abstractmethod from typing import List, Dict, Tuple, Union, Optional, Sequence, Any, TypeVar +from abc import ABCMeta, abstractmethod import copy import math import zlib From 167b16e1c94c38230869b6ae0ecf959964d8a195 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Sep 2020 19:55:03 -0700 Subject: [PATCH 46/49] fix writing of property values --- fatamorgana/records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 42bf56e..e9c30cb 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1027,7 +1027,7 @@ class Property(Record): size += self.name.write(stream) # type: ignore if not v: if u == 0x0f: - size += write_uint(stream, self.name) # type: ignore + size += write_uint(stream, len(self.values)) # type: ignore size += sum(write_property_value(stream, p) for p in self.values) # type: ignore return size From 3627b6365858dd15904c49dba1ba76594c46aad3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Sep 2020 20:03:01 -0700 Subject: [PATCH 47/49] Make records store their associated Property records. Previously, `Property` records were (incorrectly) just associated with the library or cell. This requires a change to how `CellName`s are handled by `OasisLayout`, since they can have associated properties. They now have their own non-record object (like `XName`s did before) which holds the properties. There is also now a `FileModals.property_target` attribute which tracks which record new `Property` rescords should be associated with. --- fatamorgana/main.py | 107 +++++++++++++++++++++++++++++++---------- fatamorgana/records.py | 66 +++++++++++++++++++++---- 2 files changed, 137 insertions(+), 36 deletions(-) diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 56e4e41..4659fd1 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -30,11 +30,16 @@ class FileModals: textstring_implicit: Optional[bool] = None propstring_implicit: Optional[bool] = None + property_target: List[records.Property] + within_cell: bool = False within_cblock: bool = False - end_has_offset_table: bool + end_has_offset_table: bool = False started: bool = False + def __init__(self, property_target = List[records.Property]): + self.property_target = property_target + class OasisLayout: """ @@ -53,7 +58,7 @@ class OasisLayout: validation (Validation): checksum data (Names) - cellnames (Dict[int, NString]): Cell names + cellnames (Dict[int, CellName]): Cell names propnames (Dict[int, NString]): Property names xnames (Dict[int, XName]): Custom names @@ -73,7 +78,7 @@ class OasisLayout: properties: List[records.Property] cells: List['Cell'] - cellnames: Dict[int, NString] + cellnames: Dict[int, 'CellName'] propnames: Dict[int, NString] xnames: Dict[int, 'XName'] @@ -115,9 +120,9 @@ class OasisLayout: Returns: New `OasisLayout` object. """ - file_state = FileModals() - modals = Modals() layout = OasisLayout(unit=-1) # dummy unit + modals = Modals() + file_state = FileModals(layout.properties) read_magic_bytes(stream) @@ -225,7 +230,10 @@ class OasisLayout: key = record.reference_number if key is None: key = len(self.cellnames) - self.cellnames[key] = record.nstring + + cellname = CellName.from_record(record) + self.cellnames[key] = cellname + file_state.property_target = cellname.properties elif record_id in (5, 6): implicit = record_id == 5 if file_state.textstring_implicit is None: @@ -272,10 +280,7 @@ class OasisLayout: elif record_id in (28, 29): record = records.Property.read(stream, record_id) record.merge_with_modals(modals) - if not file_state.within_cell: - self.properties.append(record) - else: - self.cells[-1].properties.append(record) + file_state.property_target.append(record) elif record_id in (30, 31): implicit = record_id == 30 if file_state.xname_implicit is None: @@ -289,6 +294,7 @@ class OasisLayout: if key is None: key = len(self.xnames) self.xnames[key] = XName.from_record(record) + # TODO: do anything with property target? # # Cell and elements @@ -296,7 +302,9 @@ class OasisLayout: elif record_id in (13, 14): record = records.Cell.read(stream, record_id) record.merge_with_modals(modals) - self.cells.append(Cell(record.name)) + cell = Cell(record.name) + self.cells.append(cell) + file_state.property_target = cell.properties elif record_id in (15, 16): record = records.XYMode.read(stream, record_id) record.merge_with_modals(modals) @@ -304,10 +312,12 @@ class OasisLayout: record = records.Placement.read(stream, record_id) record.merge_with_modals(modals) self.cells[-1].placements.append(record) + file_state.property_target = record.properties elif record_id in _GEOMETRY: record = _GEOMETRY[record_id].read(stream, record_id) record.merge_with_modals(modals) self.cells[-1].geometry.append(record) + file_state.property_target = record.properties else: raise InvalidRecordError('Unknown record id: {}'.format(record_id)) return False @@ -330,10 +340,12 @@ class OasisLayout: size = 0 size += write_magic_bytes(stream) size += records.Start(self.unit, self.version).dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in self.properties) cellnames_offset = OffsetEntry(False, size) - size += sum(records.CellName(name, refnum).dedup_write(stream, modals) - for refnum, name in self.cellnames.items()) + for refnum, cn in self.cellnames.items(): + size += records.CellName(cn.nstring, refnum).dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in cn.properties) propnames_offset = OffsetEntry(False, size) size += sum(records.PropName(name, refnum).dedup_write(stream, modals) @@ -354,8 +366,6 @@ class OasisLayout: layernames_offset = OffsetEntry(False, size) size += sum(r.dedup_write(stream, modals) for r in self.layers) - size += sum(p.dedup_write(stream, modals) for p in self.properties) - size += sum(c.dedup_write(stream, modals) for c in self.cells) offset_table = OffsetTable( @@ -386,15 +396,17 @@ class Cell: placements: List[records.Placement] geometry: List[records.geometry_t] - def __init__(self, name: Union[NString, str, int]): - """ - Args: - name: `NString` or "CellName reference" number - """ + def __init__(self, + name: Union[NString, str, int], + *, + properties: Optional[List[records.Property]] = None, + placements: Optional[List[records.Placement]] = None, + geometry: Optional[List[records.geometry_t]] = None, + ): self.name = name if isinstance(name, (NString, int)) else NString(name) - self.properties = [] - self.placements = [] - self.geometry = [] + self.properties = [] if properties is None else properties + self.placements = [] if placements is None else placements + self.geometry = [] if geometry is None else geometry def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int: """ @@ -413,11 +425,54 @@ class Cell: """ size = records.Cell(self.name).dedup_write(stream, modals) size += sum(p.dedup_write(stream, modals) for p in self.properties) - size += sum(p.dedup_write(stream, modals) for p in self.placements) - size += sum(g.dedup_write(stream, modals) for g in self.geometry) + for placement in self.placements: + size += placement.dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in placement.properties) + for shape in self.geometry: + size += shape.dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in shape.properties) return size +class CellName: + """ + Representation of a CellName. + + This class is effectively a simplified form of a `records.CellName`, + with the reference data stripped out. + """ + nstring: NString + properties: List[records.Property] + + def __init__(self, + nstring: Union[NString, str], + properties: Optional[List[records.Property]] = None): + """ + Args: + nstring: The contained string. + properties: Properties which apply to this CellName's cell, but + are placed following the CellName record. + """ + if isinstance(nstring, NString): + self.nstring = nstring + else: + self.nstring = NString(nstring) + self.properties = [] if properties is None else properties + + @staticmethod + def from_record(record: records.CellName) -> 'CellName': + """ + Create an `CellName` object from a `records.CellName` record. + + Args: + record: CellName record to use. + + Returns: + A new `CellName` object. + """ + return CellName(record.nstring) + + class XName: """ Representation of an XName. @@ -446,7 +501,7 @@ class XName: record: XName record to use. Returns: - `XName` object. + a new `XName` object. """ return XName(record.attribute, record.bstring) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index e9c30cb..a261f60 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1101,15 +1101,21 @@ class XElement(Record): """ attribute: int bstring: bytes + properties: List['Property'] - def __init__(self, attribute: int, bstring: bytes): + def __init__(self, + attribute: int, + bstring: bytes, + properties: Optional[List['Property']] = None): """ Args: attribute: Attribute number. bstring: Binary data for this XElement. + properties: List of property records associated with this record. """ self.attribute = attribute self.bstring = bstring + self.properties = [] if properties is None else properties def merge_with_modals(self, modals: Modals): pass @@ -1147,6 +1153,7 @@ class XGeometry(Record, GeometryMixin): x (Optional[int]): None means reuse modal y (Optional[int]): None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ attribute: int bstring: bytes @@ -1155,6 +1162,7 @@ class XGeometry(Record, GeometryMixin): x: Optional[int] = None y: Optional[int] = None repetition: Optional[repetition_t] = None + properties: List['Property'] def __init__(self, attribute: int, @@ -1163,7 +1171,8 @@ class XGeometry(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): """ Args: attribute: Attribute number for this XGeometry. @@ -1173,6 +1182,7 @@ class XGeometry(Record, GeometryMixin): x: X-offset. Default `None` (use modal). y: Y-offset. Default `None` (use modal). repetition: Repetition. Default `None` (no repetition). + properties: List of property records associated with this record. """ self.attribute = attribute self.bstring = bstring @@ -1181,6 +1191,7 @@ class XGeometry(Record, GeometryMixin): self.x = x self.y = y self.repetition = repetition + self.properties = [] if properties is None else properties def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') @@ -1305,6 +1316,7 @@ class Placement(Record): y (Optional[int]): y-offset, None means reuse modal repetition (repetition_t or None): Repetition, if any flip (bool): Whether to perform reflection about the x-axis. + properties (List[Property]): List of property records associate with this record. """ name: Union[NString, int, None] = None magnification: Optional[real_t] = None @@ -1313,6 +1325,7 @@ class Placement(Record): y: Optional[int] = None repetition: Optional[repetition_t] = None flip: bool + properties: List['Property'] def __init__(self, flip: bool, @@ -1321,7 +1334,8 @@ class Placement(Record): angle: Optional[real_t] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): """ Args: flip: Whether to perform reflection about the x-axis. @@ -1333,6 +1347,7 @@ class Placement(Record): x: X-offset. Default `None` (use modal). y: Y-offset. Default `None` (use modal). repetition: Repetition. Default `None` (no repetition). + properties: List of property records associated with this record. """ self.x = x self.y = y @@ -1344,6 +1359,7 @@ class Placement(Record): self.name = name else: self.name = NString(name) + self.properties = [] if properties is None else properties def get_name(self) -> Union[NString, int]: return verify_modal(self.name) # type: ignore @@ -1448,6 +1464,7 @@ class Text(Record, GeometryMixin): x (Optional[int]): x-offset, None means reuse modal y (Optional[int]): y-offset, None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ string: Optional[Union[AString, int]] = None layer: Optional[int] = None @@ -1455,6 +1472,7 @@ class Text(Record, GeometryMixin): x: Optional[int] = None y: Optional[int] = None repetition: Optional[repetition_t] = None + properties: List['Property'] def __init__(self, string: Union[AString, str, int, None] = None, @@ -1462,7 +1480,8 @@ class Text(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): """ Args: string: Text content, or `TextString` reference number. @@ -1472,6 +1491,7 @@ class Text(Record, GeometryMixin): x: X-offset. Default `None` (use modal). y: Y-offset. Default `None` (use modal). repetition: Repetition. Default `None` (no repetition). + properties: List of property records associated with this record. """ self.layer = layer self.datatype = datatype @@ -1482,6 +1502,7 @@ class Text(Record, GeometryMixin): self.string = string else: self.string = AString(string) + self.properties = [] if properties is None else properties def get_string(self) -> Union[AString, int]: return verify_modal(self.string) # type: ignore @@ -1575,6 +1596,7 @@ class Rectangle(Record, GeometryMixin): y (Optional[int]): y-offset of the rectangle's lower-left (min-y) point. None means reuse modal repetition (Optional[repetition_t]): Repetition, if any. + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1584,6 +1606,7 @@ class Rectangle(Record, GeometryMixin): y: Optional[int] = None repetition: Optional[repetition_t] = None is_square: bool = False + properties: List['Property'] def __init__(self, is_square: bool = False, @@ -1593,7 +1616,8 @@ class Rectangle(Record, GeometryMixin): height: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): self.is_square = is_square self.layer = layer self.datatype = datatype @@ -1604,6 +1628,7 @@ class Rectangle(Record, GeometryMixin): self.repetition = repetition if is_square and self.height is not None: raise InvalidDataError('Rectangle is square and also has height') + self.properties = [] if properties is None else properties def get_width(self) -> int: return verify_modal(self.width) @@ -1710,6 +1735,7 @@ class Polygon(Record, GeometryMixin): None means reuse modal repetition (Optional[repetition_t]): Repetition, if any. Default no repetition. + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1717,6 +1743,7 @@ class Polygon(Record, GeometryMixin): y: Optional[int] = None repetition: Optional[repetition_t] = None point_list: Optional[point_list_t] = None + properties: List['Property'] def __init__(self, point_list: Optional[point_list_t] = None, @@ -1724,13 +1751,15 @@ class Polygon(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): self.layer = layer self.datatype = datatype self.x = x self.y = y self.repetition = repetition self.point_list = point_list + self.properties = [] if properties is None else properties if point_list is not None: if len(point_list) < 3: @@ -1829,6 +1858,7 @@ class Path(Record, GeometryMixin): x (Optional[int]): x-offset, None means reuse modal y (Optional[int]): y-offset, None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1839,6 +1869,7 @@ class Path(Record, GeometryMixin): half_width: Optional[int] = None extension_start: Optional[pathextension_t] = None extension_end: Optional[pathextension_t] = None + properties: List['Property'] def __init__(self, point_list: Optional[point_list_t] = None, @@ -1849,7 +1880,8 @@ class Path(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): self.layer = layer self.datatype = datatype self.x = x @@ -1859,6 +1891,7 @@ class Path(Record, GeometryMixin): self.half_width = half_width self.extension_start = extension_start self.extension_end = extension_end + self.properties = [] if properties is None else properties def get_point_list(self) -> point_list_t: return verify_modal(self.point_list) @@ -2006,6 +2039,7 @@ class Trapezoid(Record, GeometryMixin): y (Optional[int]): y-offset to lower-left corner of the trapezoid's bounding box. None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -2017,6 +2051,7 @@ class Trapezoid(Record, GeometryMixin): delta_a: int = 0 delta_b: int = 0 is_vertical: bool + properties: List['Property'] def __init__(self, is_vertical: bool, @@ -2028,7 +2063,8 @@ class Trapezoid(Record, GeometryMixin): height: int = None, x: int = None, y: int = None, - repetition: repetition_t = None): + repetition: repetition_t = None, + properties: Optional[List['Property']] = None): """ Raises: InvalidDataError: if dimensions are impossible. @@ -2043,6 +2079,7 @@ class Trapezoid(Record, GeometryMixin): self.x = x self.y = y self.repetition = repetition + self.properties = [] if properties is None else properties if self.is_vertical: if height is not None and delta_b - delta_a > height: @@ -2206,6 +2243,7 @@ class CTrapezoid(Record, GeometryMixin): y (Optional[int]): y-offset of lower-left (min-y) point of bounding box. None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ ctrapezoid_type: Optional[int] = None layer: Optional[int] = None @@ -2215,6 +2253,7 @@ class CTrapezoid(Record, GeometryMixin): x: Optional[int] = None y: Optional[int] = None repetition: Optional[repetition_t] = None + properties: List['Property'] def __init__(self, ctrapezoid_type: int = None, @@ -2224,7 +2263,8 @@ class CTrapezoid(Record, GeometryMixin): height: int = None, x: int = None, y: int = None, - repetition: repetition_t = None): + repetition: repetition_t = None, + properties: Optional[List['Property']] = None): """ Raises: InvalidDataError: if dimensions are invalid. @@ -2237,6 +2277,7 @@ class CTrapezoid(Record, GeometryMixin): self.x = x self.y = y self.repetition = repetition + self.properties = [] if properties is None else properties self.check_valid() @@ -2405,6 +2446,7 @@ class Circle(Record, GeometryMixin): x (Optional[int]): x-offset, None means reuse modal y (Optional[int]): y-offset, None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -2412,6 +2454,7 @@ class Circle(Record, GeometryMixin): y: Optional[int] = None repetition: Optional[repetition_t] = None radius: Optional[int] = None + properties: List['Property'] def __init__(self, radius: int = None, @@ -2419,7 +2462,8 @@ class Circle(Record, GeometryMixin): datatype: int = None, x: int = None, y: int = None, - repetition: repetition_t = None): + repetition: repetition_t = None, + properties: Optional[List['Property']] = None): """ Args: radius: Radius. Default `None` (reuse modal). @@ -2428,6 +2472,7 @@ class Circle(Record, GeometryMixin): x: X-offset. Default `None` (use modal). y: Y-offset. Default `None` (use modal). repetition: Repetition. Default `None` (no repetition). + properties: List of property records associated with this record. Raises: InvalidDataError: if dimensions are invalid. @@ -2438,6 +2483,7 @@ class Circle(Record, GeometryMixin): self.x = x self.y = y self.repetition = repetition + self.properties = [] if properties is None else properties def get_radius(self) -> int: return verify_modal(self.radius) From 3ca999fa2e2f2d38722ab19217ab1a7ac9912c0d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Sep 2020 20:03:19 -0700 Subject: [PATCH 48/49] documentation updates --- fatamorgana/basic.py | 6 +++--- fatamorgana/main.py | 20 +++++++++++++++--- setup.py | 48 ++++++++++++++++++++++---------------------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index 6342cc2..3d12fe9 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -1730,9 +1730,9 @@ def read_property_value(stream: io.BufferedIOBase) -> property_value_t: 10: ASCII string (`AString`) 11: binary string (`bytes`) 12: name string (`NString`) - 13: `PropstringReference` to `AString` - 14: `PropstringReference` to `bstring` (i.e., to `bytes`) - 15: `PropstringReference` to `NString` + 13: `PropStringReference` to `AString` + 14: `PropStringReference` to `bstring` (i.e., to `bytes`) + 15: `PropStringReference` to `NString` Args: stream: Stream to read from. diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 4659fd1..1df23f4 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -16,6 +16,7 @@ from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validatio __author__ = 'Jan Petykiewicz' +#logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) @@ -86,7 +87,6 @@ class OasisLayout: propstrings: Dict[int, AString] layers: List[records.LayerName] - def __init__(self, unit: real_t, validation: Validation = None): """ Args: @@ -202,16 +202,19 @@ class OasisLayout: raise InvalidRecordError('Unknown record id: {}'.format(record_id)) if record_id == 0: - # Pad + ''' Pad ''' pass elif record_id == 1: + ''' Start ''' record = records.Start.read(stream, record_id) record.merge_with_modals(modals) self.unit = record.unit self.version = record.version file_state.end_has_offset_table = record.offset_table is None + file_state.property_target = self.properties # TODO Offset table strict check elif record_id == 2: + ''' End ''' record = records.End.read(stream, record_id, file_state.end_has_offset_table) record.merge_with_modals(modals) self.validation = record.validation @@ -219,6 +222,7 @@ class OasisLayout: raise InvalidRecordError('Stream continues past End record') return True elif record_id in (3, 4): + ''' CellName ''' implicit = record_id == 3 if file_state.cellname_implicit is None: file_state.cellname_implicit = implicit @@ -235,6 +239,7 @@ class OasisLayout: self.cellnames[key] = cellname file_state.property_target = cellname.properties elif record_id in (5, 6): + ''' TextString ''' implicit = record_id == 5 if file_state.textstring_implicit is None: file_state.textstring_implicit = implicit @@ -248,6 +253,7 @@ class OasisLayout: key = len(self.textstrings) self.textstrings[key] = record.astring elif record_id in (7, 8): + ''' PropName ''' implicit = record_id == 7 if file_state.propname_implicit is None: file_state.propname_implicit = implicit @@ -261,6 +267,7 @@ class OasisLayout: key = len(self.propnames) self.propnames[key] = record.nstring elif record_id in (9, 10): + ''' PropString ''' implicit = record_id == 9 if file_state.propstring_implicit is None: file_state.propstring_implicit = implicit @@ -274,14 +281,17 @@ class OasisLayout: key = len(self.propstrings) self.propstrings[key] = record.astring elif record_id in (11, 12): + ''' LayerName ''' record = records.LayerName.read(stream, record_id) record.merge_with_modals(modals) self.layers.append(record) elif record_id in (28, 29): + ''' Property ''' record = records.Property.read(stream, record_id) record.merge_with_modals(modals) file_state.property_target.append(record) elif record_id in (30, 31): + ''' XName ''' implicit = record_id == 30 if file_state.xname_implicit is None: file_state.xname_implicit = implicit @@ -300,20 +310,24 @@ class OasisLayout: # Cell and elements # elif record_id in (13, 14): + ''' Cell ''' record = records.Cell.read(stream, record_id) record.merge_with_modals(modals) cell = Cell(record.name) self.cells.append(cell) file_state.property_target = cell.properties elif record_id in (15, 16): + ''' XYMode ''' record = records.XYMode.read(stream, record_id) record.merge_with_modals(modals) elif record_id in (17, 18): + ''' Placement ''' record = records.Placement.read(stream, record_id) record.merge_with_modals(modals) self.cells[-1].placements.append(record) file_state.property_target = record.properties elif record_id in _GEOMETRY: + ''' Geometry ''' record = _GEOMETRY[record_id].read(stream, record_id) record.merge_with_modals(modals) self.cells[-1].geometry.append(record) @@ -507,7 +521,7 @@ class XName: # Mapping from record id to record class. -_GEOMETRY: Dict[int, Type] = { +_GEOMETRY: Dict[int, Type[records.geometry_t]] = { 19: records.Text, 20: records.Rectangle, 21: records.Polygon, diff --git a/setup.py b/setup.py index 66941c3..a66adc1 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,30 @@ setup(name='fatamorgana', author='Jan Petykiewicz', author_email='anewusername@gmail.com', url='https://mpxd.net/code/jan/fatamorgana', + packages=find_packages(), + package_data={ + 'fatamorgana': ['VERSION', + 'py.typed', + ], + }, + install_requires=[ + 'typing', + ], + extras_require={ + 'numpy': ['numpy'], + }, + classifiers=[ + 'Programming Language :: Python :: 3', + 'Development Status :: 3 - Alpha', + 'Environment :: Other Environment', + '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)', + ], keywords=[ 'OASIS', 'layout', @@ -36,29 +60,5 @@ setup(name='fatamorgana', 'polygon', 'gds', ], - classifiers=[ - 'Programming Language :: Python :: 3', - 'Development Status :: 3 - Alpha', - 'Environment :: Other Environment', - '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)', - ], - packages=find_packages(), - package_data={ - 'fatamorgana': ['VERSION', - 'py.typed', - ], - }, - install_requires=[ - 'typing', - ], - extras_require={ - 'numpy': ['numpy'], - }, ) From c1b79485a7a8dff979711f2fa4b2cadc0a80a4b8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Sep 2020 20:04:15 -0700 Subject: [PATCH 49/49] Bump version to v0.9 --- fatamorgana/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fatamorgana/VERSION b/fatamorgana/VERSION index aec258d..b63ba69 100644 --- a/fatamorgana/VERSION +++ b/fatamorgana/VERSION @@ -1 +1 @@ -0.8 +0.9