diff --git a/.gitignore b/.gitignore index cc1e0d0..03c8891 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.pyc -__pycache__/ +__pycache__ *.idea @@ -7,7 +7,6 @@ build/ dist/ *.egg-info/ .mypy_cache/ -.pytest_cache/ *.swp *.swo diff --git a/README.md b/README.md index d0a8815..516c4b4 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ ### Links - [Source repository](https://mpxd.net/code/jan/g85) - [PyPI](https://pypi.org/project/g85) -- [Github mirror](https://github.com/anewusername/g85) ## Installation Requirements: -* python >= 3.10 (written and tested with 3.11) +* python >= 3.7 (written and tested with 3.9) +* numpy Install with pip: diff --git a/g85/LICENSE.md b/g85/LICENSE.md deleted file mode 120000 index 7eabdb1..0000000 --- a/g85/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ -../LICENSE.md \ No newline at end of file diff --git a/g85/README.md b/g85/README.md deleted file mode 120000 index 32d46ee..0000000 --- a/g85/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/g85/VERSION.py b/g85/VERSION.py new file mode 100644 index 0000000..2c3fbc3 --- /dev/null +++ b/g85/VERSION.py @@ -0,0 +1,4 @@ +""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ +__version__ = ''' +0.1 +'''.strip() diff --git a/g85/__init__.py b/g85/__init__.py index 69c0e4b..8514051 100644 --- a/g85/__init__.py +++ b/g85/__init__.py @@ -1,9 +1,3 @@ -from .main import ( - Map as Map, - Device as Device, - ) -from .read import read as read -from .write import write as write - -__author__ = 'Jan Petykiewicz' -__version__ = '0.7' +from .main import Map, Device +from .read import read +from .write import write diff --git a/g85/main.py b/g85/main.py index 8bbf144..bc0fbc6 100644 --- a/g85/main.py +++ b/g85/main.py @@ -1,3 +1,4 @@ +from typing import Dict, List, Tuple, Union, Optional import datetime from collections import Counter from dataclasses import dataclass, field @@ -7,25 +8,25 @@ from itertools import chain @dataclass class Device: BinType: str - NullBin: str | int - ProductId: str | None = None - LotId: str | None = None - WaferSize: float | None = None - CreateDate: datetime.datetime | None = None - DeviceSizeX: float | None = None - DeviceSizeY: float | None = None - SupplierName: str | None = None - OriginLocation: int | None = None + NullBin: Union[str, int] + ProductId: Optional[str] = None + LotId: Optional[str] = None + WaferSize: Optional[float] = None + CreateDate: Optional[datetime.datetime] = None + DeviceSizeX: Optional[float] = None + DeviceSizeY: Optional[float] = None + SupplierName: Optional[str] = None + OriginLocation: Optional[int] = None MapType: str = 'Array' Orientation: float = 0 - reference_xy: tuple[int, int] | None = None + reference_xy: Optional[Tuple[int, int]] = None - bin_pass: dict[int | str, bool] = field(default_factory=dict) # Is this bin passing? - map: list[list[int]] | list[list[str]] = field(default_factory=list) # The actual map - data_misc: dict[str, str] = field(default_factory=dict) # - supplier_data: dict[str, str] = field(default_factory=dict) # + bin_pass: Dict[Union[int, str], bool] = field(default_factory=dict) # Is this bin passing? + map: Union[List[List[int]], List[List[str]]] = field(default_factory=list) # The actual map + # Map attribs: MapName, MapVersion + # SupplierData attribs: ProductCode, RecipeName - misc: dict[str, str] = field(default_factory=dict) # Any unexpected fields go here + misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here @property def Rows(self) -> int: @@ -45,9 +46,9 @@ class Device: class Map: xmlns: str = 'http://www.semi.org' FormatRevision: str = "SEMI G85 0703" - SubstrateType: str | None = None - SubstrateId: str | None = None + SubstrateType: Optional[str] = None + SubstrateId: Optional[str] = None - devices: list[Device] = field(default_factory=list) - misc: dict[str, str] = field(default_factory=dict) # Any unexpected fields go here + devices: List[Device] = field(default_factory=list) + misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here diff --git a/g85/read.py b/g85/read.py index 8c4a924..8d3db06 100644 --- a/g85/read.py +++ b/g85/read.py @@ -1,4 +1,4 @@ -from typing import TextIO, Any +from typing import List, Union, TextIO, Any import logging import datetime from dataclasses import fields @@ -10,7 +10,7 @@ from .main import Map, Device logger = logging.getLogger(__name__) -def read(stream: TextIO) -> list[Map]: +def read(stream: TextIO) -> List[Map]: tree = ElementTree.parse(stream) el_root = tree.getroot() @@ -21,7 +21,7 @@ def read(stream: TextIO) -> list[Map]: return maps -def read_wmaps(el_root: ElementTree.Element) -> list[Map]: +def read_wmaps(el_root: ElementTree.Element) -> List[Map]: map_fields = [ff.name for ff in fields(Map)] maps = [] for el_map in el_root: @@ -41,7 +41,7 @@ def read_wmaps(el_root: ElementTree.Element) -> list[Map]: return maps -def read_devices(el_map: ElementTree.Element) -> list[Device]: +def read_devices(el_map: ElementTree.Element) -> List[Device]: dev_fields = [ff.name for ff in fields(Device)] devices = [] for el_device in el_map: @@ -50,7 +50,7 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]: continue bin_type = el_device.attrib['BinType'] - null_bin: int | str + null_bin: Union[int, str] if bin_type == 'Decimal': null_bin = int(el_device.attrib['NullBin']) else: @@ -58,24 +58,22 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]: device = Device(BinType=bin_type, NullBin=null_bin) + val: Any for key, val in el_device.attrib.items(): if key in ('BinType', 'NullBin'): continue - parsed_val: Any if key in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'): - parsed_val = float(val) + val = float(val) elif key in ('OriginLocation',): - parsed_val = int(val) + val = int(val) elif key == 'CreateDate': - parsed_val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f') - else: - parsed_val = val + val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f') if key in dev_fields and key[0].isupper(): - setattr(device, key, parsed_val) + setattr(device, key, val) else: - device.misc[key] = parsed_val + device.misc[key] = val for el_entry in el_device: tag = _tag(el_entry) @@ -98,12 +96,7 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]: f'with attributes {el_entry.attrib}') continue - bin_code: int | str - if bin_type == 'Decimal': - bin_code = int(attrib['BinCode']) - else: - bin_code = attrib['BinCode'] - + bin_code = attrib['BinCode'] if bin_code in device.bin_pass: logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!') continue @@ -111,23 +104,17 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]: device.bin_pass[bin_code] = attrib['BinQuality'].lower() == 'pass' elif tag == 'Data': data_strs = [read_row(rr) for rr in el_entry] - data: list[list[str]] | list[list[int]] + data: Union[List[List[str]], List[List[int]]] if device.BinType == 'Decimal': data = [[int(vv) for vv in rr] for rr in data_strs] else: data = data_strs device.map = data - for key, value in attrib.items(): - device.data_misc[key] = value - elif tag == 'SupplierData': - for key, value in attrib.items(): - device.supplier_data[key] = value - devices.append(device) return devices -def read_row(el_row: ElementTree.Element) -> list[str]: +def read_row(el_row: ElementTree.Element) -> List[str]: assert _tag(el_row) == 'Row' row_stripped = (el_row.text or '').strip() @@ -139,7 +126,7 @@ def read_row(el_row: ElementTree.Element) -> list[str]: def _tag(element: ElementTree.Element) -> str: - """ + ''' Get the element's tag, excluding any namespaces. - """ + ''' return element.tag.split('}')[-1] diff --git a/g85/write.py b/g85/write.py index e0c0bd3..b086b39 100644 --- a/g85/write.py +++ b/g85/write.py @@ -1,5 +1,4 @@ -from typing import TextIO, cast -from collections.abc import Sequence +from typing import Sequence, Tuple, List, TextIO, Union import logging import math from dataclasses import fields @@ -11,15 +10,12 @@ from .main import Map, Device logger = logging.getLogger(__name__) -class G85Error(Exception): - pass - - # Hack to directly pass through -def _escape_cdata(text: str) -> str: +def _escape_cdata(text): if text.startswith(''): return text - return _original_escape_cdata(text) + else: + return _original_escape_cdata(text) _original_escape_cdata = ElementTree._escape_cdata # type: ignore @@ -45,11 +41,8 @@ def write_wmap(wmap: Map, el_root: ElementTree.Element) -> None: map_fields = [ff.name for ff in fields(wmap)] for field in map_fields: - if field[0].isupper() or field == 'xmlns': - val = getattr(wmap, field) - if val is None: - continue - el_map.set(field, val) + if field[0].isupper(): + el_map.set(field, getattr(wmap, field)) for key, value in wmap.misc.items(): if key[0].isupper() and key in map_fields: continue @@ -68,7 +61,7 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non # Row data prep if device.map is None: - raise G85Error(f'No _data for device pformat({device})') + raise Exception(f'No _data for device pformat({device})') is_decimal = device.BinType == 'Decimal' row_texts, bin_length = prepare_data(device.map, decimal=is_decimal) @@ -88,9 +81,8 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non el_bin.set('BinQuality', 'Pass' if passed else 'Fail') el_bin.set('BinCount', str(bin_counts[bin_code])) - el_data = ElementTree.SubElement(el_device, 'Data') for row_text in row_texts: - el_row = ElementTree.SubElement(el_data, 'Row') + el_row = ElementTree.SubElement(el_device, 'Row') el_row.text = f'' # Device attribs @@ -98,8 +90,6 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non for field in dev_fields: if field[0].isupper(): val = getattr(device, field) - if val is None: - continue if field in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'): val = f'{val:g}' @@ -107,8 +97,6 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non val = f'{val:d}' elif field == 'CreateDate': val = val.strftime('%Y%m%d%H%M%S%f')[:-3] - elif field == 'NullBin' and device.BinType == 'Decimal': - val = f'{val:d}' el_device.set(field, val) @@ -117,34 +105,27 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non continue el_device.set(key, value) - for key, value in device.data_misc.items(): - el_data.set(key, value) - if device.supplier_data: - el_suppdata = ElementTree.SubElement(el_device, 'SupplierData') - for key, value in device.data_misc.items(): - el_suppdata.set(key, value) - - -def prepare_data(data: list[list[str]] | list[list[int]], decimal: bool) -> tuple[list[str], int]: +def prepare_data(data: List[List[Union[str, int]]], decimal: bool) -> Tuple[List[str], int]: is_char = isinstance(data[0][0], str) - row_texts = [] if is_char: - data = cast(list[list[str]], data) char_len = len(data[0][0]) - for srow in data: - if char_len == 1: - row_text = ''.join(srow) - else: - row_text = ' '.join(srow) + ' ' - row_texts.append(row_text) - return row_texts, char_len - else: # noqa: RET505 - data = cast(list[list[int]], data) + else: max_value = max(max(rr) for rr in data) max_digits = math.ceil(math.log10(max_value)) - for irow in data: - row_text = ' '.join(str(vv).zfill(max_digits) for vv in irow) + ' ' - row_texts.append(row_text) + + row_texts = [] + for row in data: + if is_char and char_len == 1: + row_text = ''.join(row) + elif is_char: + row_text = ' '.join(row) + ' ' + else: + row_text = ' '.join(str(vv).zfill(max_digits) for vv in row) + ' ' + row_texts.append(row_text) + + if is_char: + return row_texts, char_len + else: return row_texts, max_digits diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 91a15d0..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,79 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "g85" -description = "G85 wafer map reader / writer" -readme = "README.md" -license = { file = "LICENSE.md" } -authors = [ - { name="Jan Petykiewicz", email="jan@mpxd.net" }, - ] -homepage = "https://mpxd.net/code/jan/g85" -repository = "https://mpxd.net/code/jan/g85" -keywords = [ - "design", - "CAD", - "EDA", - "electronics", - "photonics", - "IC", - "mask", - "wafer", - "map", - "G85", - "wmap", - ] -classifiers = [ - "Programming Language :: Python :: 3", - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "Intended Audience :: Manufacturing", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", - "Topic :: File Formats", - ] -requires-python = ">=3.10" -dynamic = ["version"] -dependencies = [ - ] - -[tool.hatch.version] -path = "g85/__init__.py" - - -[tool.ruff] -exclude = [ - ".git", - "dist", - ] -line-length = 145 -indent-width = 4 -lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -lint.select = [ - "NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG", - "C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT", - "ARG", "PL", "R", "TRY", - "G010", "G101", "G201", "G202", - "Q002", "Q003", "Q004", - ] -lint.ignore = [ - #"ANN001", # No annotation - "ANN002", # *args - "ANN003", # **kwargs - "ANN401", # Any - "ANN101", # self: Self - "SIM108", # single-line if / else assignment - "RET504", # x=y+z; return x - "PIE790", # unnecessary pass - "ISC003", # non-implicit string concatenation - "C408", # dict(x=y) instead of {'x': y} - "PLR09", # Too many xxx - "PLR2004", # magic number - "PLC0414", # import x as x - "TRY003", # Long exception message - ] - diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1bdf57d --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +from setuptools import setup, find_packages + + +with open('README.md', 'r') as f: + long_description = f.read() + +with open('g85/VERSION.py', 'rt') as f: + version = f.readlines()[2].strip() + +setup( + name='g85', + version=version, + description='G85 wafer map reader / writer', + long_description=long_description, + long_description_content_type='text/markdown', + author='Jan Petykiewicz', + author_email='jan@mpxd.net', + url='https://mpxd.net/code/jan/g85', + packages=find_packages(), + package_data={ + 'g85': ['py.typed'], + }, + install_requires=[ + 'numpy', + ], + classifiers=[ + 'Programming Language :: Python :: 3', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Manufacturing', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', + ], + keywords=[ + 'design', + 'CAD', + 'EDA', + 'electronics', + 'photonics', + 'IC', + 'mask', + 'wafer', + 'map', + 'G85', + 'wmap', + ], + )