diff --git a/.gitignore b/.gitignore index 02ddec7..f0e9ec8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ *.pyc __pycache__ *.idea -.mypy_cache/ build dist diff --git a/MANIFEST.in b/MANIFEST.in index 3d18ec7..c28ab72 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ include README.md include LICENSE.md -include fatamorgana/VERSION diff --git a/README.md b/README.md index fc5f845..92325cd 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 -pip3 install fatamorgana +pip install fatamorgana ``` Install directly from git repository: ```bash -pip3 install git+https://mpxd.net/code/jan/fatamorgana.git@release +pip 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/VERSION b/fatamorgana/VERSION deleted file mode 100644 index aec258d..0000000 --- a/fatamorgana/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.8 diff --git a/fatamorgana/__init__.py b/fatamorgana/__init__.py index 1ffb62a..0e8485b 100644 --- a/fatamorgana/__init__.py +++ b/fatamorgana/__init__.py @@ -16,26 +16,13 @@ Dependencies: - Python 3.5 or later - - numpy (optional, faster but no additional functionality) - - To get started, try: - ```python3 - import fatamorgana - help(fatamorgana.OasisLayout) - ``` + - 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, \ - UnfilledModalError, \ - ReuseRepetition, GridRepetition, ArbitraryRepetition + EOFError, SignedError, InvalidDataError, InvalidRecordError __author__ = 'Jan Petykiewicz' -with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: - __version__ = f.read().strip() -version = __version__ - +version = '0.4' diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py index f5abde3..4db5ba0 100644 --- a/fatamorgana/basic.py +++ b/fatamorgana/basic.py @@ -3,12 +3,11 @@ 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 typing import List, Tuple, Type from enum import Enum import math import struct import io -import warnings try: import numpy @@ -20,9 +19,9 @@ except: ''' Type definitions ''' -real_t = Union[int, float, Fraction] -repetition_t = Union['ReuseRepetition', 'GridRepetition', 'ArbitraryRepetition'] -property_value_t = Union[int, bytes, 'AString', 'NString', 'PropStringReference', float, Fraction] +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 class FatamorganaError(Exception): @@ -59,12 +58,6 @@ class InvalidRecordError(FatamorganaError): """ pass -class UnfilledModalError(FatamorganaError): - """ - Attempted to call .get_var(), but var() was None! - """ - pass - class PathExtensionScheme(Enum): """ @@ -75,10 +68,11 @@ class PathExtensionScheme(Enum): Arbitrary = 3 + ''' Constants ''' -MAGIC_BYTES: bytes = b'%SEMI-OASIS\r\n' +MAGIC_BYTES = b'%SEMI-OASIS\r\n' # type: bytes ''' @@ -89,15 +83,10 @@ 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. - 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. + :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. """ b = stream.read(n) if len(b) != n: @@ -109,11 +98,8 @@ def read_byte(stream: io.BufferedIOBase) -> int: """ Read a single byte and return it. - Args: - stream: Stream to read from. - - Returns: - The byte that was read. + :param stream: Stream to read from. + :return: The byte that was read. """ return _read(stream, 1)[0] @@ -122,90 +108,65 @@ def write_byte(stream: io.BufferedIOBase, n: int) -> int: """ Write a single byte to the stream. - Args: - stream: Stream to read from. - - Returns: - The number of bytes writen (1). + :param stream: Stream to read from. + :return: The number of bytes writen (1). """ 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 _np_read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: + 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). + :param stream: Stream to read from. + :return: 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)) - def _np_write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[Union[bool, int], ...]) -> int: + def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[bool]) -> 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. + :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. """ 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 + return stream.write(numpy.packbits(bits)) else: - read_bool_byte = _py_read_bool_byte - write_bool_byte = _py_write_bool_byte + 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. + + :param stream: Stream to read from. + :return: A list of 8 booleans corresponding to the bits (MSB first). + """ + byte = _read(1)[0] + bits = [(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. + """ + 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))) + def read_uint(stream: io.BufferedIOBase) -> int: """ @@ -216,11 +177,8 @@ 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_. - Args: - stream: Stream to read from. - - Returns: - The integer's value. + :param stream: Stream to read from. + :return: The integer's value. """ result = 0 i = 0 @@ -236,17 +194,12 @@ 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(...). - Args: - stream: Stream to write to. - n: Value to write. - - Returns: - The number of bytes written. - - Raises: - SignedError: if `n` is negative. + :param stream: Stream to write to. + :param n: Value to write. + :return: The number of bytes written. + :raises: SignedError if n is negative. """ if n < 0: raise SignedError('uint must be positive: {}'.format(n)) @@ -273,11 +226,8 @@ def decode_sint(uint: int) -> int: - The LSB is treated as the sign bit - The remainder of the bits encodes the absolute value - Args: - uint: Unsigned integer to decode from. - - Returns: - The decoded signed integer. + :param uint: Unsigned integer to decode from. + :return: The decoded signed integer. """ return (uint >> 1) * (1 - 2 * (0x01 & uint)) @@ -285,13 +235,10 @@ 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. - Args: - int: The signed integer to encode. - - Returns: - Unsigned integer encoding for the input. + :param int: The signed integer to encode. + :return: Unsigned integer encoding for the input. """ return (abs(sint) << 1) | (sint < 0) @@ -299,13 +246,10 @@ 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. - Args: - stream: Stream to read from. - - Returns: - The integer's value. + :param stream: Stream to read from. + :return: The integer's value. """ return decode_sint(read_uint(stream)) @@ -313,14 +257,11 @@ 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. - Args: - stream: Stream to write to. - n: Value to write. - - Returns: - The number of bytes written. + :param stream: Stream to write to. + :param n: Value to write. + :return: The number of bytes written. """ return write_uint(stream, encode_sint(n)) @@ -332,11 +273,8 @@ def read_bstring(stream: io.BufferedIOBase) -> bytes: - length: uint - data: bytes - Args: - stream: Stream to read from. - - Returns: - Bytes containing the binary string. + :param stream: Stream to read from. + :return: Bytes containing the binary string. """ length = read_uint(stream) return _read(stream, length) @@ -345,14 +283,11 @@ 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. - Args: - stream: Stream to write to. - bstring: Binary string to write. - - Returns: - The number of bytes written. + :param stream: Stream to write to. + :param bstring: Binary string to write. + :return: The number of bytes written. """ write_uint(stream, len(bstring)) return stream.write(bstring) @@ -365,11 +300,8 @@ def read_ratio(stream: io.BufferedIOBase) -> Fraction: - numerator: uint - denominator: uint - Args: - stream: Stream to read from. - - Returns: - Fraction object containing the read value. + :param stream: Stream to read from. + :return: Fraction object containing the read value. """ numer = read_uint(stream) denom = read_uint(stream) @@ -379,17 +311,12 @@ 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. - Args: - stream: Stream to write to. - r: Ratio to write (`Fraction` object). - - Returns: - The number of bytes written. - - Raises: - SignedError: if r is negative. + :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. """ if r < 0: raise SignedError('Ratio must be unsigned: {}'.format(r)) @@ -402,11 +329,8 @@ def read_float32(stream: io.BufferedIOBase) -> float: """ Read a 32-bit float from the stream. - Args: - stream: Stream to read from. - - Returns: - The value read. + :param stream: Stream to read from. + :return: The value read. """ b = _read(stream, 4) return struct.unpack(" int: """ Write a 32-bit float to the stream. - Arsg: - stream: Stream to write to. - f: Value to write. - - Returns: - The number of bytes written (4). + :param stream: Stream to write to. + :param f: Value to write. + :return: The number of bytes written (4). """ b = struct.pack(" float: """ Read a 64-bit float from the stream. - Args: - stream: Stream to read from. - - Returns: - The value read. + :param stream: Stream to read from. + :return: The value read. """ b = _read(stream, 8) return struct.unpack(" int: """ Write a 64-bit float to the stream. - Args: - stream: Stream to write to. - f: Value to write. - - Returns: - The number of bytes written (8). + :param stream: Stream to write to. + :param f: Value to write. + :return: The number of bytes written (8). """ b = struct.pack(" real_t: 6: 32-bit float 7: 64-bit float - 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. + :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. """ if real_type is None: @@ -520,14 +430,10 @@ 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). - 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. + :param stream: Stream to write to. + :param r: Value to write. + :param float32: + :return: The number of bytes written. """ size = 0 if isinstance(r, int): @@ -555,16 +461,15 @@ 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: str + _string = None # type: str - def __init__(self, string_or_bytes: Union[bytes, str]): + def __init__(self, string_or_bytes: bytes or str): """ - Args: - string_or_bytes: Content of the `NString`. + :param string_or_bytes: Content of the Nstring. """ if isinstance(string_or_bytes, str): self.string = string_or_bytes @@ -588,7 +493,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 {!r}'.format(bstring)) + raise InvalidDataError('Invalid n-string {}'.format(bstring)) self._string = bstring.decode('ascii') @staticmethod @@ -596,14 +501,9 @@ class NString: """ Create an NString object by reading a bstring from the provided stream. - Args: - stream: Stream to read from. - - Returns: - Resulting NString. - - Raises: - InvalidDataError + :param stream: Stream to read from. + :return: Resulting NString. + :raises: InvalidDataError """ return NString(read_bstring(stream)) @@ -611,37 +511,26 @@ class NString: """ Write this NString to a stream. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. + :param stream: Stream to write to. + :return: Number of bytes written. """ return write_bstring(stream, self.bytes) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: 'NString') -> bool: return isinstance(other, type(self)) and self.string == other.string def __repr__(self) -> str: return '[N]' + self._string - def __str__(self) -> str: - return self._string - 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. - Args: - stream: Stream to read from. - - Returns: - Resulting string. - - Raises: - InvalidDataError + :param stream: Stream to read from. + :return: Resulting string. + :raises: InvalidDataError """ return NString.read(stream).string @@ -649,17 +538,12 @@ 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. - Args: - stream: Stream to write to. - string: String to write. - - Returns: - Number of bytes written. - - Raises: - InvalidDataError + :param stream: Stream to write to. + :param string: String to write. + :return: Number of bytes written. + :raises: InvalidDataError """ return NString(string).write(stream) @@ -669,16 +553,15 @@ 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: str + _string = None # type: str - def __init__(self, string_or_bytes: Union[bytes, str]): + def __init__(self, string_or_bytes: bytes or str): """ - Args: - string_or_bytes: Content of the AString. + :param string_or_bytes: Content of the AString. """ if isinstance(string_or_bytes, str): self.string = string_or_bytes @@ -702,60 +585,44 @@ class AString: @bytes.setter def bytes(self, bstring: bytes): if not all(0x20 <= c <= 0x7e for c in bstring): - raise InvalidDataError('Invalid a-string {!r}'.format(bstring)) + raise InvalidDataError('Invalid a-string {}'.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. - Args: - stream: Stream to read from. - - Returns: - Resulting `AString`. - - Raises: - InvalidDataError + :param stream: Stream to read from. + :return: 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. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. + :param stream: Stream to write to. + :return: Number of bytes written. """ return write_bstring(stream, self.bytes) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: 'AString') -> bool: return isinstance(other, type(self)) and self.string == other.string def __repr__(self) -> str: return '[A]' + self._string - def __str__(self) -> str: - return self._string - 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. - Args: - stream: Stream to read from. - - Returns: - Resulting string. - - Raises: - InvalidDataError + :param stream: Stream to read from. + :return: Resulting string. + :raises: InvalidDataError """ return AString.read(stream).string @@ -765,15 +632,10 @@ def write_astring(stream: io.BufferedIOBase, string: str) -> int: Write an ASCII string to a stream. See AString for constraints on ASCII strings. - Args: - stream: Stream to write to. - string: String to write. - - Returns: - Number of bytes written. - - Raises: - InvalidDataError + :param stream: Stream to write to. + :param string: String to write. + :return: Number of bytes written. + :raises: InvalidDataError """ return AString(string).write(stream) @@ -782,20 +644,19 @@ class ManhattanDelta: """ Class representing an axis-aligned ("Manhattan") vector. - Attributes: - vertical (bool): `True` if aligned along y-axis - value (int): signed length of the vector + Has properties + .vertical (boolean, 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! - Args: - x: x-displacement - y: y-displacement + :param x: x-displacement + :param y: y-displacement """ x = int(x) y = int(y) @@ -812,8 +673,7 @@ class ManhattanDelta: """ Return a list representation of this vector. - Returns: - `[x, y]` + :return: [x, y] """ xy = [0, 0] xy[self.vertical] = self.value @@ -822,10 +682,9 @@ 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. - Returns: - uint encoding of this vector. + :return: uint encoding of this vector. """ return (encode_sint(self.value) << 1) | self.vertical @@ -837,51 +696,42 @@ 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). - Args: - n: Unsigned integer representation of a `ManhattanDelta` vector. - - Returns: - The `ManhattanDelta` object that was encoded by `n`. + :param n: Unsigned integer representation of a ManhattanDelta vector. + :return: The ManhattanDelta object that was encoded by n. """ d = ManhattanDelta(0, 0) d.value = decode_sint(n >> 1) - d.vertical = bool(n & 0x01) + d.vertical = 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 `ManhattanDelta.from_uint()` for format details. + See .from_uint() for format details. - Args: - stream: The stream to read from. - - Returns: - The `ManhattanDelta` object that was read from the stream. + :param stream: The stream to read from. + :return: 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 `ManhattanDelta.from_uint()` for format details. + See .from_uint() for format details. - Args: - stream: The stream to write to. - - Returns: - The number of bytes written. + :param stream: The stream to write to. + :return: The number of bytes written. """ return write_uint(stream, self.as_uint()) - def __eq__(self, other: Any) -> bool: - return hasattr(other, 'as_list') and self.as_list() == other.as_list() + def __eq__(self, other: 'ManhattanDelta') -> bool: + return hasattr(other, as_list) and self.as_list() == other.as_list() def __repr__(self) -> str: return '{}'.format(self.as_list()) @@ -891,9 +741,9 @@ class OctangularDelta: """ Class representing an axis-aligned or 45-degree ("Octangular") vector. - Attributes: - proj_mag (int): projection of the vector onto the x or y axis (non-zero) - octangle (int): bitfield: + Has properties + .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 @@ -906,17 +756,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: int - octangle: int + proj_mag = None # type: int + octangle = None # type: 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! - Args: - x: x-displacement - y: y-displacement + :param x: x-displacement + :param y: y-displacement """ x = int(x) y = int(y) @@ -938,8 +788,7 @@ class OctangularDelta: """ Return a list representation of this vector. - Returns: - `[x, y]` + :return: [x, y] """ if self.octangle < 4: xy = [0, 0] @@ -958,28 +807,24 @@ 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. - Returns: - uint encoding of this vector. + :return: 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. - Args: - n: Unsigned integer representation of an `OctangularDelta` vector. - - Returns: - The `OctangularDelta` object that was encoded by `n`. + :param n: Unsigned integer representation of an OctangularDelta vector. + :return: The OctangularDelta object that was encoded by n. """ d = OctangularDelta(0, 0) d.proj_mag = n >> 3 @@ -989,35 +834,29 @@ 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 `OctangularDelta.from_uint()` for format details. + See .from_uint() for format details. - Args: - stream: The stream to read from. - - Returns: - The `OctangularDelta` object that was read from the stream. + :param stream: The stream to read from. + :return: 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 `OctangularDelta.from_uint()` for format details. + See .from_uint() for format details. - Args: - stream: The stream to write to. - - Returns: - The number of bytes written. + :param stream: The stream to write to. + :return: The number of bytes written. """ return write_uint(stream, self.as_uint()) - def __eq__(self, other: Any) -> bool: - return hasattr(other, 'as_list') and self.as_list() == other.as_list() + def __eq__(self, other: 'OctangularDelta') -> bool: + return hasattr(other, as_list) and self.as_list() == other.as_list() def __repr__(self) -> str: return '{}'.format(self.as_list()) @@ -1027,18 +866,17 @@ class Delta: """ Class representing an arbitrary vector - Attributes - x (int): x-displacement - y (int): y-displacement + Has properties + .x (int) + .y (int) """ - x: int - y: int + x = None # type: int + y = None # type: int def __init__(self, x: int, y: int): """ - Args: - x: x-displacement - y: y-displacement + :param x: x-displacement + :param y: y-displacement """ x = int(x) y = int(y) @@ -1049,29 +887,25 @@ class Delta: """ Return a list representation of this vector. - Returns: - `[x, y]` + :return: [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()). - Args: - stream: The stream to read from. - - Returns: - The `Delta` object that was read from the stream. + :param stream: The stream to read from. + :return: The Delta object that was read from the stream. """ n = read_uint(stream) if (n & 0x01) == 0: @@ -1083,15 +917,12 @@ 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 `Delta.from_uint()` for format details. + See .from_uint() for format details. - Args: - stream: The stream to write to. - - Returns: - The number of bytes written. + :param stream: The stream to write to. + :return: 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) @@ -1100,8 +931,8 @@ class Delta: size += write_uint(stream, encode_sint(self.y)) return size - def __eq__(self, other: Any) -> bool: - return hasattr(other, 'as_list') and self.as_list() == other.as_list() + def __eq__(self, other: 'Delta') -> bool: + return hasattr(other, as_list) and self.as_list() == other.as_list() def __repr__(self) -> str: return '{}'.format(self.as_list()) @@ -1111,14 +942,8 @@ def read_repetition(stream: io.BufferedIOBase) -> repetition_t: """ Read a repetition entry from the given stream. - Args: - stream: Stream to read from. - - Returns: - The repetition entry. - - Raises: - InvalidDataError: if an unexpected repetition type is read + :param stream: Stream to read from. + :return: The repetition entry. """ rtype = read_uint(stream) if rtype == 0: @@ -1127,20 +952,15 @@ 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: """ Write a repetition entry to the given stream. - Args: - stream: Stream to write to. - repetition: The repetition entry to write. - - Returns: - The number of bytes written. + :param stream: Stream to write to. + :param repetition: The repetition entry to write. + :return: The number of bytes written. """ return repetition.write(stream) @@ -1157,7 +977,7 @@ class ReuseRepetition: def write(self, stream: io.BufferedIOBase) -> int: return write_uint(stream, 0) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: 'ReuseRepetition') -> bool: return isinstance(other, ReuseRepetition) def __repr__(self) -> str: @@ -1171,40 +991,37 @@ class GridRepetition: two lattice vectors, and the extent of the grid is stored as the number of elements along each lattice 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`. + 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) """ - a_vector: List[int] - b_vector: Optional[List[int]] = None - a_count: int - b_count: Optional[int] = 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 def __init__(self, a_vector: List[int], a_count: int, - b_vector: Optional[List[int]] = None, - b_count: Optional[int] = None): + b_vector: List[int] = None, + b_count: int = None): """ - 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`. + :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. """ self.a_vector = a_vector self.b_vector = b_vector @@ -1221,7 +1038,8 @@ class GridRepetition: if self.b_count < 2: self.b_count = None self.b_vector = None - warnings.warn('Removed b_count and b_vector since b_count == 1') + print('Warning: removed b_count and b_vector since b_count == 1') + # TODO: warn here if self.a_count < 2: raise InvalidDataError('Repetition has too-small x-count: ' @@ -1234,21 +1052,14 @@ class GridRepetition: @staticmethod def read(stream: io.BufferedIOBase, repetition_type: int) -> 'GridRepetition': """ - Read a `GridRepetition` from a stream. + Read a GridRepetition from a stream. - 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. + :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. """ - nb: Optional[int] - b_vector: Optional[List[int]] if repetition_type == 1: na = read_uint(stream) + 2 nb = read_uint(stream) + 2 @@ -1281,19 +1092,14 @@ 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) - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. - - Raises: - InvalidDataError: if repetition is malformed. + :param stream: Stream to write to. + :return: 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: @@ -1332,20 +1138,12 @@ class GridRepetition: size += Delta(*self.b_vector).write(stream) return size - def __eq__(self, other: Any) -> bool: - 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 __eq__(self, other: 'GridRepetition') -> 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 def __repr__(self) -> str: return 'GridRepetition: ({} : {} | {} : {})'.format(self.a_count, self.a_vector, @@ -1357,21 +1155,20 @@ class ArbitraryRepetition: Class representing a repetition entry denoting a 1D or 2D array of arbitrarily-spaced elements. - Attributes: - x_displacements (List[int]): x-displacements between consecutive elements - y_displacements (List[int]): y-displacements between consecutive elements + Properties: + .x_displacements (List[int], x-displacements between elements) + .y_displacements (List[int], y-displacements between elements) """ - x_displacements: List[int] - y_displacements: List[int] + x_displacements = None # type: List[int] + y_displacements = None # type: List[int] def __init__(self, x_displacements: List[int], y_displacements: List[int]): """ - Args: - x_displacements: x-displacements between consecutive elements - y_displacements: y-displacements between consecutive elements + :param x_displacements: x-displacements between consecutive elements + :param y_displacements: y-displacements between consecutive elements """ self.x_displacements = x_displacements self.y_displacements = y_displacements @@ -1379,18 +1176,13 @@ class ArbitraryRepetition: @staticmethod def read(stream: io.BufferedIOBase, repetition_type: int) -> 'ArbitraryRepetition': """ - Read an `ArbitraryRepetition` from a stream. + Read an ArbitraryRepetition from a stream. - 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. + :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. """ if repetition_type == 4: n = read_uint(stream) + 1 @@ -1433,17 +1225,14 @@ 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. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. + :param stream: Stream to write to. + :return: Number of bytes written. """ def get_gcd(vals: List[int]) -> int: """ @@ -1497,7 +1286,7 @@ class ArbitraryRepetition: return size - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: 'ArbitraryRepetition') -> bool: return isinstance(other, type(self)) and self.x_displacements == other.x_displacements and self.y_displacements == other.y_displacements def __repr__(self) -> str: @@ -1508,14 +1297,8 @@ def read_point_list(stream: io.BufferedIOBase) -> List[List[int]]: """ Read a point list from a stream. - 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. + :param stream: Stream to read from. + :return: Point list of the form [[x0, y0], [x1, y1], ...] """ list_type = read_uint(stream) list_len = read_uint(stream) @@ -1560,42 +1343,43 @@ 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: - points = numpy.cumsum(deltas, axis=0) + delta_x, delta_y = zip(*deltas) + x = numpy.cumsum(delta_x) + y = numpy.cumsum(delta_y) + points = list(zip(x, y)) else: points = [] x = 0 y = 0 - for delta in deltas: - x += delta[0] - y += delta[1] + for _ in range(list_len): + delta = Delta.read(stream) + x += delta.x + y += delta.y points.append([x, y]) else: - raise InvalidDataError('Invalid point list type') + raise Exception('Invalid point list type') return points def write_point_list(stream: io.BufferedIOBase, - points: List[Sequence[int]], + points: List[List[int]], fast: bool = False, implicit_closed: bool = True ) -> int: """ Write a point list to a stream. - 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. + :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. """ # If we're in a hurry, just write the points as arbitrary Deltas if fast: @@ -1636,7 +1420,6 @@ 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] @@ -1677,8 +1460,7 @@ 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) @@ -1699,12 +1481,12 @@ class PropStringReference: """ Reference to a property string. - Attributes: - ref (int): ID of the target - ref_type (Type): Type of the target: `bytes`, `NString`, or `AString` + Properties: + .ref (int, ID of the target) + .ref_type (Type, Type of the target: bytes, NString, or AString) """ - ref: int - reference_type: Type + ref = None # type: int + reference_type = None # type: Type def __init__(self, ref: int, ref_type: Type): """ @@ -1714,7 +1496,7 @@ class PropStringReference: self.ref = ref self.ref_type = ref_type - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: 'PropStringReference') -> bool: return isinstance(other, type(self)) and self.ref == other.ref and self.reference_type == other.reference_type def __repr__(self) -> str: @@ -1732,23 +1514,17 @@ 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 - Args: - stream: Stream to read from. - - Returns: - Value of the property, depending on type. - - Raises: - InvalidDataError: if an invalid type is read. + :param stream: Stream to read from. + :return: Value of the property, depending on type. + :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) @@ -1787,21 +1563,18 @@ 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. - 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. + :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. """ if isinstance(value, int) and not force_real: if force_signed_int or value < 0: @@ -1810,7 +1583,7 @@ def write_property_value(stream: io.BufferedIOBase, else: size = write_uint(stream, 8) size += write_uint(stream, value) - elif isinstance(value, (Fraction, float, int)): + elif isinstance(value, real_t): size = write_real(stream, value, force_float32) elif isinstance(value, AString): size = write_uint(stream, 10) @@ -1834,7 +1607,7 @@ def write_property_value(stream: io.BufferedIOBase, return size -def read_interval(stream: io.BufferedIOBase) -> Tuple[Optional[int], Optional[int]]: +def read_interval(stream: io.BufferedIOBase) -> Tuple[int or None]: """ Read an interval from a stream. These are used for storing layer info. @@ -1847,16 +1620,10 @@ def read_interval(stream: io.BufferedIOBase) -> Tuple[Optional[int], Optional[in type 3: a, a (unsigned integer a) type 4: a, b (unsigned integers a, b) - 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`) - - Raises: - InvalidDataError: On malformed data. + :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) """ interval_type = read_uint(stream) if interval_type == 0: @@ -1870,25 +1637,20 @@ 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, - min_bound: Optional[int] = None, - max_bound: Optional[int] = None + min_bound: int or None = None, + max_bound: int or None = 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. - 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. + :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. """ if min_bound is None: if max_bound is None: @@ -1898,10 +1660,8 @@ 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, 4) + size = write_uint(stream, 3) size += write_uint(stream, min_bound) size += write_uint(stream, max_bound) return size @@ -1911,31 +1671,33 @@ class OffsetEntry: """ Entry for the file's offset table. - 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. + 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.) """ - strict: bool = False - offset: int = 0 + strict = False # type: bool + offset = 0 # type: int def __init__(self, strict: bool = False, offset: int = 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`. + :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. """ self.strict = strict self.offset = offset @@ -1945,11 +1707,8 @@ class OffsetEntry: """ Read an offset entry from a stream. - Args: - stream: Stream to read from. - - Returns: - Offset entry that was read. + :param stream: Stream to read from. + :return: Offset entry that was read. """ entry = OffsetEntry() entry.strict = read_uint(stream) > 0 @@ -1960,11 +1719,8 @@ class OffsetEntry: """ Write this offset entry to a stream. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written + :param stream: Stream to write to. + :return: Number of bytes written """ return write_uint(stream, self.strict) + write_uint(stream, self.offset) @@ -1986,38 +1742,37 @@ class OffsetTable: which are stored in the above order in the file's offset table. - 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 + Proerties: + .cellnames (OffsetEntry) + .textstrings (OffsetEntry) + .propnames (OffsetEntry) + .propstrings (OffsetEntry) + .layernames (OffsetEntry) + .xnames (OffsetEntry) """ - cellnames: OffsetEntry - textstrings: OffsetEntry - propnames: OffsetEntry - propstrings: OffsetEntry - layernames: OffsetEntry - xnames: OffsetEntry + cellnames = None # type: OffsetEntry + textstrings= None # type: OffsetEntry + propnames = None # type: OffsetEntry + propstrings = None # type: OffsetEntry + layernames = None # type: OffsetEntry + xnames = None # type: OffsetEntry def __init__(self, - cellnames: Optional[OffsetEntry] = None, - textstrings: Optional[OffsetEntry] = None, - propnames: Optional[OffsetEntry] = None, - propstrings: Optional[OffsetEntry] = None, - layernames: Optional[OffsetEntry] = None, - xnames: Optional[OffsetEntry] = None): + cellnames: OffsetEntry = None, + textstrings: OffsetEntry = None, + propnames: OffsetEntry = None, + propstrings: OffsetEntry = None, + 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. - 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. + :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. """ if cellnames is None: cellnames = OffsetEntry() @@ -2045,11 +1800,8 @@ class OffsetTable: Read an offset table from a stream. See class docstring for format details. - Args: - stream: Stream to read from. - - Returns: - The offset table that was read. + :param stream: Stream to read from. + :return: The offset table that was read. """ table = OffsetTable() table.cellnames = OffsetEntry.read(stream) @@ -2065,11 +1817,8 @@ class OffsetTable: Write this offset table to a stream. See class docstring for format details. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. + :param stream: Stream to write to. + :return: Number of bytes written. """ size = self.cellnames.write(stream) size += self.textstrings.write(stream) @@ -2088,29 +1837,21 @@ def read_u32(stream: io.BufferedIOBase) -> int: """ Read a 32-bit unsigned integer (little endian) from a stream. - Args: - stream: Stream to read from. - - Returns: - The integer that was read. + :param stream: Stream to read from. + :return: The integer that was read. """ b = _read(stream, 4) - return struct.unpack(' int: """ Write a 32-bit unsigned integer (little endian) to a stream. - Args: - stream: Stream to write to. - n: Integer to write. - - Returns: - The number of bytes written (4). - - Raises: - SignedError: if `n` is negative. + :param stream: Stream to write to. + :param n: Integer to write. + :return: The number of bytes written (4). + :raises: SignedError if n is negative. """ if n < 0: raise SignedError('Negative u32: {}'.format(n)) @@ -2126,22 +1867,20 @@ class Validation: The checksum is calculated using the entire file, excluding the final 4 bytes (the value of the checksum itself). - Attributes: - checksum_type (int): `0` for no checksum, `1` for crc32, `2` for checksum32 - checksum (Optional[int]): value of the checksum + Properties: + .checksum_type (int, 0: No checksum, 1: crc32, 2: checksum32) + .checksum (int or None, value of the checksum) + """ - checksum_type: int - checksum: Optional[int] = None + checksum_type = None # type: int + checksum = None # type: int or None def __init__(self, checksum_type: int, checksum: int = None): """ - 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. + :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. """ if checksum_type < 0 or checksum_type > 2: raise InvalidDataError('Invalid validation type') @@ -2156,14 +1895,9 @@ class Validation: Read a validation entry from a stream. See class docstring for format details. - Args: - stream: Stream to read from. - - Returns: - The validation entry that was read. - - Raises: - InvalidDataError: if an invalid validation type was encountered. + :param stream: Stream to read from. + :return: The validation entry that was read. + :raises: InvalidDataError if an invalid validation type was encountered. """ checksum_type = read_uint(stream) if checksum_type == 0: @@ -2181,27 +1915,15 @@ class Validation: Write this validation entry to a stream. See class docstring for format details. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. - - Raises: - InvalidDataError: if the checksum type can't be handled. + :param stream: Stream to write to. + :return: Number of bytes written. """ 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) @@ -2211,11 +1933,8 @@ def write_magic_bytes(stream: io.BufferedIOBase) -> int: """ Write the magic byte sequence to a stream. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. + :param stream: Stream to write to. + :return: Number of bytes written. """ return stream.write(MAGIC_BYTES) @@ -2223,15 +1942,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. - Args: - stream: Stream to read from. - - Raises: - InvalidDataError: if the sequence was not found. + :param stream: Stream to read from. + :raises: InvalidDataError if the sequence was not found. """ magic = _read(stream, len(MAGIC_BYTES)) if magic != MAGIC_BYTES: raise InvalidDataError('Could not read magic bytes, ' - 'found {!r} : {}'.format(magic, magic.decode())) + 'found {} : {}'.format(magic, magic.decode())) + diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 56e4e41..3cc721c 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -3,12 +3,11 @@ 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, Type import io import logging from . import records -from .records import Modals, Record +from .records import Modals from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validation, \ read_magic_bytes, write_magic_bytes, read_uint, EOFError, \ InvalidDataError, InvalidRecordError @@ -24,16 +23,17 @@ class FileModals: """ File-scoped modal variables """ - 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 = 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 - within_cell: bool = False - within_cblock: bool = False - end_has_offset_table: bool - started: bool = False + within_cell = False # type: bool + within_cblock = False # type: bool + end_has_offset_table = None # type: bool + started = False # type: bool class OasisLayout: @@ -43,51 +43,49 @@ 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 `records.Cell` - record objects). + Cells are stored using Cell objects (different from Cell record objects). - Attributes: - (File properties) - version (AString): Version string ('1.0') - unit (real_t): grid steps per micron - validation (Validation): checksum data + Properties: + File properties: + .version AString: Version string ('1.0') + .unit real number: grid steps per micron + .validation Validation: checksum data - (Names) - cellnames (Dict[int, NString]): Cell names - propnames (Dict[int, NString]): Property names - xnames (Dict[int, XName]): Custom names + Names: + .cellnames Dict[int, NString] + .propnames Dict[int, NString] + .xnames Dict[int, XName] - (Strings) - textstrings (Dict[int, AString]): Text strings - propstrings (Dict[int, AString]): Property strings + Strings: + .textstrings Dict[int, AString] + .propstrings Dict[int, AString] - (Data) - layers (List[records.LayerName]): Layer definitions - properties (List[records.Property]): Property values - cells (List[Cell]): Layout cells + Data: + .layers List[records.LayerName] + .properties List[records.Property] + .cells List[Cell] """ - version: AString - unit: real_t - validation: Validation + version = None # type: AString + unit = None # type: real_t + validation = None # type: Validation - properties: List[records.Property] - cells: List['Cell'] + properties = None # type: List[records.Property] + cells = None # type: List[Cell] - cellnames: Dict[int, NString] - propnames: Dict[int, NString] - xnames: Dict[int, 'XName'] + cellnames = None # type: Dict[int, NString] + propnames = None # type: Dict[int, NString] + xnames = None # type: Dict[int, XName] - textstrings: Dict[int, AString] - propstrings: Dict[int, AString] - layers: List[records.LayerName] + textstrings = None # type: Dict[int, AString] + propstrings = None # type: Dict[int, AString] + layers = None # type: List[records.LayerName] def __init__(self, unit: real_t, validation: Validation = None): """ - 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. + :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. """ if validation is None: validation = Validation(0) @@ -107,17 +105,14 @@ 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. - Args: - stream: Stream to read from. - - Returns: - New `OasisLayout` object. + :param stream: Stream to read from. + :return: New OasisLayout object. """ file_state = FileModals() modals = Modals() - layout = OasisLayout(unit=-1) # dummy unit + layout = OasisLayout(unit=None) read_magic_bytes(stream) @@ -132,20 +127,15 @@ 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. - 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 + :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. """ try: record_id = read_uint(stream) @@ -157,8 +147,6 @@ 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: @@ -316,14 +304,9 @@ class OasisLayout: """ Write this object in OASIS fromat to a stream. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. - - Raises: - InvalidDataError: if contained records are invalid. + :param stream: Stream to write to. + :return: Number of bytes written. + :raises: InvalidDataError if contained records are invalid. """ modals = Modals() @@ -340,7 +323,7 @@ class OasisLayout: for refnum, name in self.propnames.items()) xnames_offset = OffsetEntry(False, size) - size += sum(records.XName(x.attribute, x.bstring, refnum).dedup_write(stream, modals) + size += sum(records.XName(x.attribute, x.string, refnum).dedup_write(stream, modals) for refnum, x in self.xnames.items()) textstrings_offset = OffsetEntry(False, size) @@ -374,24 +357,23 @@ class Cell: """ Representation of an OASIS cell. - Attributes: - name (Union[NString, int]): name or "CellName reference" number + Properties: + .name NString or int (CellName reference number) - properties (List[records.Property]): Properties of this cell - placements (List[records.Placement]): Placement record objects - geometry: (List[records.geometry_t]): Geometry record objectes + .properties List of records.Property + .placements List of records.Placement + .geometry List of geometry record objectes """ - name: Union[NString, int] - properties: List[records.Property] - placements: List[records.Placement] - geometry: List[records.geometry_t] + 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] - def __init__(self, name: Union[NString, str, int]): + def __init__(self, name: NString or int): """ - Args: - name: `NString` or "CellName reference" number + :param name: NString or int (CellName reference number) """ - self.name = name if isinstance(name, (NString, int)) else NString(name) + self.name = name self.properties = [] self.placements = [] self.geometry = [] @@ -401,15 +383,10 @@ class Cell: Write this cell to a stream, using the provided modal variables to deduplicate any repeated data. - 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. + :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. """ size = records.Cell(self.name).dedup_write(stream, modals) size += sum(p.dedup_write(stream, modals) for p in self.properties) @@ -422,17 +399,16 @@ 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: int - bstring: bytes + attribute = None # type: int + bstring = None # type: bytes def __init__(self, attribute: int, bstring: bytes): """ - Args: - attribute: Attribute number. - bstring: Binary data. + :param attribute: Attribute number. + :param bstring: Binary data. """ self.attribute = attribute self.bstring = bstring @@ -440,19 +416,16 @@ 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. - Args: - record: XName record to use. - - Returns: - `XName` object. + :param record: XName record to use. + :return: XName object. """ return XName(record.attribute, record.bstring) # Mapping from record id to record class. -_GEOMETRY: Dict[int, Type] = { +_GEOMETRY = { 19: records.Text, 20: records.Rectangle, 21: records.Polygon, diff --git a/fatamorgana/py.typed b/fatamorgana/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/fatamorgana/records.py b/fatamorgana/records.py index 4741cfb..f76682e 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, TypeVar +from typing import List, Dict, Tuple import copy import math import zlib @@ -24,55 +24,50 @@ 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, UnfilledModalError, PathExtensionScheme, _USE_NUMPY - -if _USE_NUMPY: - import numpy + InvalidDataError, PathExtensionScheme logger = logging.getLogger(__name__) - ''' Type definitions ''' -geometry_t = Union['Text', 'Rectangle', 'Polygon', 'Path', 'Trapezoid', - 'CTrapezoid', 'Circle', 'XElement', 'XGeometry'] -pathextension_t = Tuple['PathExtensionScheme', Optional[int]] -point_list_t = Sequence[Sequence[int]] +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] class Modals: """ - Modal variables, used to store data about previously-written or + Modal variables, used to store data about previously-written ori -read records. """ - 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 + 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 def __init__(self): self.reset() @@ -81,9 +76,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 @@ -102,7 +97,7 @@ class Modals: self.geometry_w = None self.geometry_h = None self.polygon_point_list = None - self.path_half_width = None + self.path_halfwidth = None self.path_point_list = None self.path_extension_start = None self.path_extension_end = None @@ -113,12 +108,6 @@ 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 @@ -135,8 +124,7 @@ 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. - Args: - modals: Modal variables to merge with. + :param modals: Modal variables to merge with. """ pass @@ -149,8 +137,7 @@ class Record(metaclass=ABCMeta): used instead. Update the modal variables using the remaining (unequal) values. - Args: - modals: Modal variables to deduplicate with. + :param modals: Modal variables to deduplicate with. """ pass @@ -161,17 +148,12 @@ class Record(metaclass=ABCMeta): Read a record of this type from a stream. This function does not merge with modal variables. - Args: - stream: Stream to read from. - record_id: Record id of the record to read. The + :param stream: Stream to read from. + :param record_id: Record id of the record to read. The record id is often used to specify which variant of the record is stored. - - Returns: - The record that was read. - - Raises: - InvalidDataError: if the record is malformed. + :return: The record that was read. + :raises: InvalidDataError if the record is malformed. """ pass @@ -181,30 +163,20 @@ class Record(metaclass=ABCMeta): Write this record to a stream as-is. This function does not merge or deduplicate with modal variables. - Args: - stream: Stream to write to. - - Returns: - Number of bytes written. - - Raises: - InvalidDataError: if the record contains invalid data. + :param stream: Stream to write to. + :return: 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. - 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. + :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. """ # TODO logging #print(type(self), stream.tell()) @@ -215,8 +187,7 @@ class Record(metaclass=ABCMeta): """ Perform a deep copy of this record. - Returns: - A deep copy of this record. + :return: A deep copy of this record. """ return copy.deepcopy(self) @@ -224,49 +195,18 @@ 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] - ) -> Union[None, int, NString]: + is_present: bool, + is_reference: bool + ) -> None or int or NString: """ Helper function for reading a possibly-absent, possibly-referenced 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` + :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 """ if not is_present: return None @@ -277,20 +217,17 @@ def read_refname(stream: io.BufferedIOBase, def read_refstring(stream: io.BufferedIOBase, - is_present: Union[bool, int], - is_reference: Union[bool, int], - ) -> Union[None, int, AString]: + is_present: bool, + is_reference: bool + ) -> None or int or AString: """ - Helper function for reading a possibly-absent, possibly-referenced `AString`. + Helper function for reading a possibly-absent, possibly-referenced 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` + :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 """ if not is_present: return None @@ -327,14 +264,14 @@ class XYMode(Record): """ XYMode record (ID 15, 16) - Attributes: - relative (bool): default `False` + Properties: + .relative (bool, default False) """ - relative: bool = False + relative = False # type: bool @property def absolute(self) -> bool: - return not self.relative + return not relative @absolute.setter def absolute(self, b: bool): @@ -342,8 +279,7 @@ class XYMode(Record): def __init__(self, relative: bool): """ - Args: - relative: `True` if the mode is 'relative', `False` if 'absolute'. + :param relative: True if the mode is 'relative', False if 'absolute'. """ self.relative = relative @@ -369,26 +305,25 @@ class Start(Record): """ Start Record (ID 1) - Attributes: - version (AString): "1.0" - unit (real_t): positive real number, grid steps per micron - offset_table (Optional[OffsetTable]): If `None` then table must be - placed in the `End` record) + 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) """ - version: AString - unit: real_t - offset_table: Optional[OffsetTable] = None + version = None # type: AString + unit = None # type: real_t + offset_table = None # type: OffsetTable def __init__(self, unit: real_t, - version: Union[AString, str] = None, - offset_table: Optional[OffsetTable] = None): + version: AString or str = None, + offset_table: OffsetTable = None): """ - 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. + :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. """ if unit <= 0: raise InvalidDataError('Non-positive unit: {}'.format(unit)) @@ -425,7 +360,6 @@ 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: @@ -450,22 +384,21 @@ class End(Record): The end record is always padded to a total length of 256 bytes. - Attributes: - offset_table (Optional[OffsetTable]): `None` if offset table was - written into the `Start` record instead - validation (Validation): object containing checksum + Properties: + .offset_table (OffsetTable or None, None if offset table was + written into the Start record instead) + .validation (Validation object) """ - offset_table: Optional[OffsetTable] = None - validation: Validation + offset_table = None # type: OffsetTable or None + validation = None # type: Validation def __init__(self, validation: Validation, - offset_table: Optional[OffsetTable] = None): + offset_table: OffsetTable = None): """ - Args: - validation: `Validation` object for this file. - offset_table: `OffsetTable`, or `None` if the `Start` record - contained an `OffsetTable`. Default `None`. + :param validation: Validation object for this file. + :param offset_table: OffsetTable, or None if the Start record + contained an OffsetTable. Default None. """ self.validation = validation self.offset_table = offset_table @@ -484,7 +417,7 @@ class End(Record): if record_id != 2: raise InvalidDataError('Invalid record id for End {}'.format(record_id)) if has_offset_table: - offset_table: Optional[OffsetTable] = OffsetTable.read(stream) + offset_table = OffsetTable.read(stream) else: offset_table = None _padding_string = read_bstring(stream) @@ -514,24 +447,23 @@ class CBlock(Record): """ CBlock (Compressed Block) record (ID 34) - Attributes: - compression_type (int): `0` for zlib - decompressed_byte_count (int): size after decompressing - compressed_bytes (bytes): compressed data + Properties: + .compression_type (int, 0 for zlib) + .decompressed_byte_count (int) + .compressed_bytes (bytes) """ - compression_type: int - decompressed_byte_count: int - compressed_bytes: bytes + compression_type = None # type: int + decompressed_byte_count = None # type: int + compressed_bytes = None # type: bytes def __init__(self, compression_type: int, decompressed_byte_count: int, compressed_bytes: bytes): """ - Args: - compression_type: `0` (zlib) - decompressed_byte_count: Number of bytes in the decompressed data. - compressed_bytes: The compressed data. + :param compression_type: 0 (zlib) + :param decompressed_byte_count: Number of bytes in the decompressed data. + :param compressed_bytes: The compressed data. """ if compression_type != 0: raise InvalidDataError('CBlock: Invalid compression scheme ' @@ -574,16 +506,11 @@ class CBlock(Record): """ Create a CBlock record from uncompressed data. - 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`. + :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. """ if compression_args is None: compression_args = {} @@ -603,14 +530,9 @@ class CBlock(Record): """ Decompress the contents of this CBlock. - Args: - decompression_args: Passed as kwargs to `zlib.decompressobj()`. - - Returns: - Decompressed `bytes` object. - - Raises: - InvalidDataError: if data is malformed or compression type is + :param decompression_args: Passed as kwargs to zlib.decompressobj(). + :return: Decompressed bytes object. + :raises: InvalidDataError if data is malformed or compression type is unknonwn. """ if decompression_args is None: @@ -631,21 +553,20 @@ class CellName(Record): """ CellName record (ID 3, 4) - Attributes: - nstring (NString): name - reference_number (Optional[int]): `None` results in implicit assignment + Properties: + .nstring (NString) + .reference_number (int or None) """ - nstring: NString - reference_number: Optional[int] = None + nstring = None # type: NString + reference_number = None # type: int or None def __init__(self, - nstring: Union[NString, str], + nstring: NString or str, reference_number: int = None): """ - Args: - nstring: The contained string. - reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + :param nstring: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(nstring, NString): self.nstring = nstring @@ -666,7 +587,7 @@ class CellName(Record): '{}'.format(record_id)) nstring = NString.read(stream) if record_id == 4: - reference_number: Optional[int] = read_uint(stream) + reference_number = read_uint(stream) else: reference_number = None record = CellName(nstring, reference_number) @@ -685,21 +606,20 @@ class PropName(Record): """ PropName record (ID 7, 8) - Attributes: - nstring (NString): name - reference_number (Optional[int]): `None` results in implicit assignment + Properties: + .nstring (NString) + .reference_number (int or None) """ - nstring: NString - reference_number: Optional[int] = None + nstring = None # type: NString + reference_number = None # type: int or None def __init__(self, - nstring: Union[NString, str], + nstring: NString or str, reference_number: int = None): """ - Args: - nstring: The contained string. - reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + :param nstring: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(nstring, NString): self.nstring = nstring @@ -720,7 +640,7 @@ class PropName(Record): '{}'.format(record_id)) nstring = NString.read(stream) if record_id == 8: - reference_number: Optional[int] = read_uint(stream) + reference_number = read_uint(stream) else: reference_number = None record = PropName(nstring, reference_number) @@ -740,21 +660,20 @@ class TextString(Record): """ TextString record (ID 5, 6) - Attributes: - astring (AString): string data - reference_number (Optional[int]): `None` results in implicit assignment + Properties: + .astring (AString) + .reference_number (int or None) """ - astring: AString - reference_number: Optional[int] = None + astring = None # type: AString + reference_number = None # type: int or None def __init__(self, - string: Union[AString, str], + string: AString or str, reference_number: int = None): """ - Args: - string: The contained string. - reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + :param string: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(string, AString): self.astring = string @@ -775,7 +694,7 @@ class TextString(Record): '{}'.format(record_id)) astring = AString.read(stream) if record_id == 6: - reference_number: Optional[int] = read_uint(stream) + reference_number = read_uint(stream) else: reference_number = None record = TextString(astring, reference_number) @@ -795,21 +714,20 @@ class PropString(Record): """ PropString record (ID 9, 10) - Attributes: - astring (AString): string data - reference_number (Optional[int]): `None` results in implicit assignment + Properties: + .astring (AString) + .reference_number (int or None) """ - astring: AString - reference_number: Optional[int] = None + astring = None # type: AString + reference_number = None # type: int or None def __init__(self, - string: Union[AString, str], + string: AString or str, reference_number: int = None): """ - Args: - string: The contained string. - reference_number: Reference id number for the string. - Default is to use an implicitly-assigned number. + :param string: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. """ if isinstance(string, AString): self.astring = string @@ -830,7 +748,7 @@ class PropString(Record): '{}'.format(record_id)) astring = AString.read(stream) if record_id == 10: - reference_number: Optional[int] = read_uint(stream) + reference_number = read_uint(stream) else: reference_number = None record = PropString(astring, reference_number) @@ -850,30 +768,31 @@ class LayerName(Record): """ LayerName record (ID 11, 12) - 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? + 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) """ - nstring: NString - layer_interval: Tuple - type_interval: Tuple - is_textlayer: bool + nstring = None # type: NString, + layer_interval = None # type: Tuple + type_interval = None # type: Tuple + is_textlayer = None # type: bool def __init__(self, - nstring: Union[NString, str], + nstring: NString or str, layer_interval: Tuple, type_interval: Tuple, is_textlayer: bool): """ - 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. + :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. """ if isinstance(nstring, NString): self.nstring = nstring @@ -915,45 +834,36 @@ class Property(Record): """ LayerName record (ID 28, 29) - 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. + 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) """ - name: Optional[Union[NString, int]] = None - values: Optional[List[property_value_t]] = None - is_standard: Optional[bool] = None + name = None # type: NString or int or None, + values = None # type: List[property_value_t] or None + is_standard = None # type: bool or None def __init__(self, - name: Union[NString, str, int, None] = None, - values: Optional[List[property_value_t]] = None, - is_standard: Optional[bool] = None): + name: NString or str or int = None, + values: List[property_value_t] = None, + is_standard: bool = 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`. + :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. """ - if isinstance(name, (NString, int)) or name is None: - self.name = name - else: + if isinstance(name, str): self.name = NString(name) + else: + self.name = name 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') @@ -986,13 +896,12 @@ class Property(Record): value_count = u else: value_count = read_uint(stream) - values: Optional[List[property_value_t]] = [read_property_value(stream) - for _ in range(value_count)] + values = [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, bool(s)) + record = Property(name, values, s) logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record)) return record @@ -1022,13 +931,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) # type: ignore + size += write_uint(stream, self.name) else: - size += self.name.write(stream) # type: ignore + size += self.name.write(stream) if not v: if u == 0x0f: - size += write_uint(stream, self.name) # type: ignore - size += sum(write_property_value(stream, p) for p in self.values) # type: ignore + size += write_uint(stream, self.name) + size += sum(write_property_value(stream, p) for p in self.values) return size @@ -1036,25 +945,24 @@ class XName(Record): """ XName record (ID 30, 31) - Attributes: - attribute (int): Attribute number - bstring (bytes): XName data - reference_number (Optional[int]): None means to use implicit numbering + Properties: + .attribute (int) + .bstring (bytes) + .reference_number (int or None, None means to use implicity numbering) """ - attribute: int - bstring: bytes - reference_number: Optional[int] = None + attribute = None # type: int + bstring = None # type: bytes + reference_number = None # type: int or None def __init__(self, attribute: int, bstring: bytes, reference_number: int = None): """ - Args: - attribute: Attribute number. - bstring: Binary XName data. - reference_number: Reference number for this `XName`. - Default `None` (implicit). + :param attribute: Attribute number. + :param bstring: Binary XName data. + :param reference_number: Reference number for this XName. + Default None (implicit). """ self.attribute = attribute self.bstring = bstring @@ -1074,7 +982,7 @@ class XName(Record): attribute = read_uint(stream) bstring = read_bstring(stream) if record_id == 31: - reference_number: Optional[int] = read_uint(stream) + reference_number = read_uint(stream) else: reference_number = None record = XName(attribute, bstring, reference_number) @@ -1095,18 +1003,17 @@ class XElement(Record): """ XElement record (ID 32) - Attributes: - attribute (int): Attribute number. - bstring (bytes): XElement data. + Properties: + .attribute (int) + .bstring (bytes) """ - attribute: int - bstring: bytes + attribute = None # type: int + bstring = None # type: bytes def __init__(self, attribute: int, bstring: bytes): """ - Args: - attribute: Attribute number. - bstring: Binary data for this XElement. + :param attribute: Attribute number. + :param bstring: Binary data for this XElement. """ self.attribute = attribute self.bstring = bstring @@ -1135,44 +1042,43 @@ class XElement(Record): return size -class XGeometry(Record, GeometryMixin): +class XGeometry(Record): """ XGeometry record (ID 33) - 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 + 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) """ - 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 + 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 def __init__(self, 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): + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): """ - 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). + :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). """ self.attribute = attribute self.bstring = bstring @@ -1204,7 +1110,7 @@ class XGeometry(Record, GeometryMixin): if z0 or z1 or z2: raise InvalidDataError('Malformed XGeometry header') attribute = read_uint(stream) - optional: Dict[str, Any] = {} + optional = {} if l: optional['layer'] = read_uint(stream) if d: @@ -1232,16 +1138,16 @@ class XGeometry(Record, GeometryMixin): 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) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) size += write_bstring(stream, self.bstring) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size @@ -1249,17 +1155,16 @@ class Cell(Record): """ Cell record (ID 13, 14) - Attributes: - name (Union[int, NString]): int specifies "CellName reference" number + Properties: + .name (NString or int specifying CellName reference number) """ - name: Union[int, NString] + name = None # type: int or NString - def __init__(self, name: Union[int, str, NString]): + def __init__(self, name: int or NString): """ - Args: - name: `NString`, or an int specifying a `CellName` reference number. + :param name: NString, or an int specifying a CellName reference number. """ - self.name = name if isinstance(name, (int, NString)) else NString(name) + self.name = name def merge_with_modals(self, modals: Modals): modals.reset() @@ -1269,7 +1174,6 @@ 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: @@ -1296,43 +1200,44 @@ class Placement(Record): """ Placement record (ID 17, 18) - 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. + 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) """ - 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 + 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 def __init__(self, flip: bool, - name: Union[NString, str, 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): + 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): """ - 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). + :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). """ self.x = x self.y = y @@ -1340,19 +1245,10 @@ class Placement(Record): self.flip = flip self.magnification = magnification self.angle = angle - if isinstance(name, (int, NString)) or name is None: - self.name = name - else: + if isinstance(name, str): self.name = NString(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) + else: + self.name = name def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'placement_x', 'placement_y') @@ -1373,7 +1269,7 @@ class Placement(Record): #CNXYRAAF (17) or CNXYRMAF (18) c, n, x, y, r, ma0, ma1, flip = read_bool_byte(stream) - optional: Dict[str, Any] = {} + optional = {} name = read_refname(stream, c, n) if record_id == 17: aa = (ma0 << 1) | ma1 @@ -1404,9 +1300,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.angle is not None and self.angle % 90 == 0 and \ + self.magnification is None or self.magnification == 1: + aa = int((self.angle / 90) % 4) bools = (c, n, x, y, r, aa & 0b10, aa & 0b01, f) m = False a = False @@ -1421,70 +1317,66 @@ class Placement(Record): size += write_bool_byte(stream, bools) if c: if n: - size += write_uint(stream, self.name) # type: ignore + size += write_uint(stream, self.name) else: - size += self.name.write(stream) # type: ignore + size += self.name.write(self) if m: - size += write_real(stream, self.magnification) # type: ignore + size += write_real(stream, self.magnification) if a: - size += write_real(stream, self.angle) # type: ignore + size += write_real(stream, self.angle) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size -class Text(Record, GeometryMixin): +class Text(Record): """ Text record (ID 19) - 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 + 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) """ - string: Optional[Union[AString, int]] = None - layer: Optional[int] = None - datatype: Optional[int] = None - x: Optional[int] = None - y: Optional[int] = None - repetition: Optional[repetition_t] = None + 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 def __init__(self, - string: Union[AString, str, int, None] = None, - layer: Optional[int] = None, - datatype: Optional[int] = None, - x: Optional[int] = None, - y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + string: AString or str or int = None, + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): """ - 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). + :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). """ self.layer = layer self.datatype = datatype self.x = x self.y = y self.repetition = repetition - if isinstance(string, (AString, int)) or string is None: - self.string = string - else: + if isinstance(string, str): self.string = AString(string) - - def get_string(self) -> Union[AString, int]: - return verify_modal(self.string) # type: ignore + else: + self.string = string def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'text_x', 'text_y') @@ -1510,7 +1402,7 @@ class Text(Record, GeometryMixin): if z0: raise InvalidDataError('Malformed Text header') - optional: Dict[str, Any] = {} + optional = {} string = read_refstring(stream, c, n) if l: optional['layer'] = read_uint(stream) @@ -1540,60 +1432,68 @@ class Text(Record, GeometryMixin): size += write_bool_byte(stream, (0, c, n, x, y, r, d, l)) if c: if n: - size += write_uint(stream, self.string) # type: ignore + size += write_uint(stream, self.string) else: - size += self.string.write(stream) # type: ignore + size += self.string.write(self) if l: - size += write_uint(stream, self.layer) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size -class Rectangle(Record, GeometryMixin): +class Rectangle(Record): """ 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]): 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 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. + 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) """ - 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 + 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 def __init__(self, is_square: bool = False, - 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): + layer: int = None, + datatype: int = None, + width: int = None, + height: int = None, + x: int = None, + y: int = None, + repetition: 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). + """ self.is_square = is_square self.layer = layer self.datatype = datatype @@ -1605,14 +1505,6 @@ class Rectangle(Record, GeometryMixin): 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) @@ -1642,7 +1534,7 @@ class Rectangle(Record, GeometryMixin): '{}'.format(record_id)) is_square, w, h, x, y, r, d, l = read_bool_byte(stream) - optional: Dict[str, Any] = {} + optional = {} if l: optional['layer'] = read_uint(stream) if d: @@ -1674,57 +1566,61 @@ class Rectangle(Record, GeometryMixin): 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) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) if w: - size += write_uint(stream, self.width) # type: ignore + size += write_uint(stream, self.width) if h: - size += write_uint(stream, self.height) # type: ignore + size += write_uint(stream, self.height) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size -class Polygon(Record, GeometryMixin): +class Polygon(Record): """ Polygon record (ID 21) - Attributes: - 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]): 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. + 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) """ - 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 + 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 def __init__(self, - 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): + point_list: List[List[int]] = None, + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: 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). + """ self.layer = layer self.datatype = datatype self.x = x @@ -1736,9 +1632,6 @@ class Polygon(Record, GeometryMixin): 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) @@ -1763,7 +1656,7 @@ class Polygon(Record, GeometryMixin): if z0 or z1: raise InvalidDataError('Invalid polygon header') - optional: Dict[str, Any] = {} + optional = {} if l: optional['layer'] = read_uint(stream) if d: @@ -1791,65 +1684,82 @@ class Polygon(Record, GeometryMixin): 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) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) if p: - size += write_point_list(stream, self.point_list, # type: ignore - implicit_closed=True, fast=fast) + size += write_point_list(stream, self.point_list, implicit_closed=True, fast=fast) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size -class Path(Record, GeometryMixin): +class Path(Record): """ Polygon record (ID 22) - Attributes: - 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]) - 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 + 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) """ - 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 + 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 def __init__(self, - 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): + 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): + """ + :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). + """ self.layer = layer self.datatype = datatype self.x = x @@ -1860,18 +1770,6 @@ class Path(Record, GeometryMixin): 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) @@ -1899,7 +1797,7 @@ class Path(Record, GeometryMixin): '{}'.format(record_id)) e, w, p, x, y, r, d, l = read_bool_byte(stream) - optional: Dict[str, Any] = {} + optional = {} if l: optional['layer'] = read_uint(stream) if d: @@ -1911,7 +1809,7 @@ class Path(Record, GeometryMixin): scheme_end = scheme & 0b11 scheme_start = (scheme >> 2) & 0b11 - def get_pathext(ext_scheme: int) -> Optional[pathextension_t]: + def get_pathext(ext_scheme: int) -> pathextension_t: if ext_scheme == 0: return None elif ext_scheme == 1: @@ -1920,8 +1818,6 @@ class Path(Record, GeometryMixin): 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) @@ -1950,11 +1846,11 @@ class Path(Record, GeometryMixin): 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) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) if w: - size += write_uint(stream, self.half_width) # type: ignore + size += write_uint(stream, self.half_width) if e: scheme = 0 if self.extension_start is not None: @@ -1963,60 +1859,57 @@ class Path(Record, GeometryMixin): scheme += self.extension_end[0].value size += write_uint(stream, scheme) if scheme & 0b1100 == 0b1100: - size += write_sint(stream, self.extension_start[1]) # type: ignore + size += write_sint(stream, self.extension_start[1]) if scheme & 0b0011 == 0b0011: - size += write_sint(stream, self.extension_end[1]) # type: ignore + size += write_sint(stream, self.extension_end[1]) if p: - size += write_point_list(stream, self.point_list, # type: ignore - implicit_closed=False, fast=fast) + size += write_point_list(stream, self.point_list, implicit_closed=False, fast=fast) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size -class Trapezoid(Record, GeometryMixin): +class Trapezoid(Record): """ 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 - 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 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 + 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) """ - 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 + 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 def __init__(self, is_vertical: bool, @@ -2030,8 +1923,23 @@ class Trapezoid(Record, GeometryMixin): y: int = None, repetition: repetition_t = None): """ - Raises: - InvalidDataError: if dimensions are impossible. + :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. """ self.is_vertical = is_vertical self.delta_a = delta_a @@ -2053,21 +1961,6 @@ class Trapezoid(Record, GeometryMixin): 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) @@ -2091,7 +1984,7 @@ class Trapezoid(Record, GeometryMixin): '{}'.format(record_id)) is_vertical, w, h, x, y, r, d, l = read_bool_byte(stream) - optional: Dict[str, Any] = {} + optional = {} if l: optional['layer'] = read_uint(stream) if d: @@ -2133,88 +2026,49 @@ class Trapezoid(Record, GeometryMixin): 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) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) if w: - size += write_uint(stream, self.width) # type: ignore + size += write_uint(stream, self.width) if h: - size += write_uint(stream, self.height) # type: ignore + size += write_uint(stream, self.height) if record_id != 25: - size += write_sint(stream, self.delta_a) # type: ignore + size += write_sint(stream, self.delta_a) if record_id != 24: - size += write_sint(stream, self.delta_b) # type: ignore + size += write_sint(stream, self.delta_b) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size -class CTrapezoid(Record, GeometryMixin): +# TODO: CTrapezoid type descriptions +class CTrapezoid(Record): """ CTrapezoid record (ID 26) - 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 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 + 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) """ - 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 + 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 def __init__(self, ctrapezoid_type: int = None, @@ -2226,8 +2080,16 @@ class CTrapezoid(Record, GeometryMixin): y: int = None, repetition: repetition_t = None): """ - Raises: - InvalidDataError: if dimensions are invalid. + :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. """ self.ctrapezoid_type = ctrapezoid_type self.layer = layer @@ -2238,24 +2100,27 @@ class CTrapezoid(Record, GeometryMixin): self.y = y self.repetition = repetition - 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) + 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)) def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') @@ -2278,8 +2143,6 @@ class CTrapezoid(Record, GeometryMixin): 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) @@ -2303,8 +2166,6 @@ class CTrapezoid(Record, GeometryMixin): 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: @@ -2312,7 +2173,7 @@ class CTrapezoid(Record, GeometryMixin): '{}'.format(record_id)) t, w, h, x, y, r, d, l = read_bool_byte(stream) - optional: Dict[str, Any] = {} + optional = {} if l: optional['layer'] = read_uint(stream) if d: @@ -2346,72 +2207,42 @@ class CTrapezoid(Record, GeometryMixin): 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) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) if t: - size += write_uint(stream, self.ctrapezoid_type) # type: ignore + size += write_uint(stream, self.ctrapezoid_type) if w: - size += write_uint(stream, self.width) # type: ignore + size += write_uint(stream, self.width) if h: - size += write_uint(stream, self.height) # type: ignore + size += write_uint(stream, self.height) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) 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, GeometryMixin): +class Circle(Record): """ Circle record (ID 27) - 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 + 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) """ - 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 + 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 def __init__(self, radius: int = None, @@ -2421,16 +2252,13 @@ class Circle(Record, GeometryMixin): y: int = None, repetition: repetition_t = None): """ - 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. + :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. """ self.radius = radius self.layer = layer @@ -2439,9 +2267,6 @@ class Circle(Record, GeometryMixin): 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) @@ -2466,7 +2291,7 @@ class Circle(Record, GeometryMixin): if z0 or z1: raise InvalidDataError('Malformed circle header') - optional: Dict[str, Any] = {} + optional = {} if l: optional['layer'] = read_uint(stream) if d: @@ -2494,31 +2319,28 @@ class Circle(Record, GeometryMixin): 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) # type: ignore + size += write_uint(stream, self.layer) if d: - size += write_uint(stream, self.datatype) # type: ignore + size += write_uint(stream, self.datatype) if s: - size += write_uint(stream, self.radius) # type: ignore + size += write_uint(stream, self.radius) if x: - size += write_sint(stream, self.x) # type: ignore + size += write_sint(stream, self.x) if y: - size += write_sint(stream, self.y) # type: ignore + size += write_sint(stream, self.y) if r: - size += self.repetition.write(stream) # type: ignore + size += self.repetition.write(stream) return size -def adjust_repetition(record, modals: Modals): +def adjust_repetition(record: Record, modals: Modals): """ Merge the record's repetition entry with the one in 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. + :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. """ if record.repetition is not None: if isinstance(record.repetition, ReuseRepetition): @@ -2530,18 +2352,15 @@ def adjust_repetition(record, modals: Modals): modals.repetition = copy.copy(record.repetition) -def adjust_field(record, r_field: str, modals: Modals, m_field: str): +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 - 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` + :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 """ r = getattr(record, r_field) if r is not None: @@ -2554,52 +2373,48 @@ def adjust_field(record, r_field: str, modals: Modals, m_field: str): raise InvalidDataError('Unfillable field: {}'.format(m_field)) -def adjust_coordinates(record, modals: Modals, mx_field: str, my_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. - 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` + :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 """ if record.x is not None: if modals.xy_relative: record.x += getattr(modals, mx_field) - setattr(modals, mx_field, record.x) + else: + 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) - setattr(modals, my_field, record.y) + else: + setattr(modals, my_field, record.y) else: record.y = getattr(modals, my_field) # TODO: Clarify the docs on the dedup_* functions -def dedup_repetition(record, modals: Modals): +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. - 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. + :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. """ if record.repetition is None: return @@ -2615,32 +2430,21 @@ def dedup_repetition(record, modals: Modals): modals.repetition = record.repetition -def dedup_field(record, r_field: str, modals: Modals, m_field: str): +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. - 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` + :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 """ r = getattr(record, r_field) m = getattr(modals, m_field) if r is not None: - 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 - - if equal: + if m is not None and m == r: setattr(record, r_field, None) else: setattr(modals, m_field, r) @@ -2648,29 +2452,25 @@ def dedup_field(record, r_field: str, modals: Modals, m_field: str): raise InvalidDataError('Unfillable field') -def dedup_coordinates(record, modals: Modals, mx_field: str, my_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. - 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` + :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 """ if record.x is not None: 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 @@ -2681,7 +2481,6 @@ 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 diff --git a/setup.py b/setup.py index f7c40ea..a8ee066 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,13 @@ #!/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=version, + version=fatamorgana.version, description='OASIS layout format parser and writer', long_description=long_description, long_description_content_type='text/markdown', @@ -37,6 +35,7 @@ setup(name='fatamorgana', 'gds', ], classifiers=[ + 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Development Status :: 3 - Alpha', 'Environment :: Other Environment', @@ -49,13 +48,9 @@ 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={ - 'fatamorgana': ['VERSION', - 'py.typed', - ], - }, install_requires=[ 'typing', ],