diff --git a/.gitignore b/.gitignore index 03c8891..cc1e0d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.pyc -__pycache__ +__pycache__/ *.idea @@ -7,6 +7,7 @@ build/ dist/ *.egg-info/ .mypy_cache/ +.pytest_cache/ *.swp *.swo diff --git a/README.md b/README.md index 516c4b4..d0a8815 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.7 (written and tested with 3.9) -* numpy +* python >= 3.10 (written and tested with 3.11) Install with pip: diff --git a/g85/LICENSE.md b/g85/LICENSE.md new file mode 120000 index 0000000..7eabdb1 --- /dev/null +++ b/g85/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/g85/README.md b/g85/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/g85/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/g85/VERSION.py b/g85/VERSION.py deleted file mode 100644 index 2c3fbc3..0000000 --- a/g85/VERSION.py +++ /dev/null @@ -1,4 +0,0 @@ -""" 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 8514051..69c0e4b 100644 --- a/g85/__init__.py +++ b/g85/__init__.py @@ -1,3 +1,9 @@ -from .main import Map, Device -from .read import read -from .write import write +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' diff --git a/g85/main.py b/g85/main.py index bc0fbc6..8bbf144 100644 --- a/g85/main.py +++ b/g85/main.py @@ -1,4 +1,3 @@ -from typing import Dict, List, Tuple, Union, Optional import datetime from collections import Counter from dataclasses import dataclass, field @@ -8,25 +7,25 @@ from itertools import chain @dataclass class Device: BinType: str - 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 + 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 MapType: str = 'Array' Orientation: float = 0 - reference_xy: Optional[Tuple[int, int]] = None + reference_xy: tuple[int, int] | None = None - 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 + 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) # - 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: @@ -46,9 +45,9 @@ class Device: class Map: xmlns: str = 'http://www.semi.org' FormatRevision: str = "SEMI G85 0703" - SubstrateType: Optional[str] = None - SubstrateId: Optional[str] = None + SubstrateType: str | None = None + SubstrateId: str | None = 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 8d3db06..8c4a924 100644 --- a/g85/read.py +++ b/g85/read.py @@ -1,4 +1,4 @@ -from typing import List, Union, TextIO, Any +from typing import 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: Union[int, str] + null_bin: int | str if bin_type == 'Decimal': null_bin = int(el_device.attrib['NullBin']) else: @@ -58,22 +58,24 @@ 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'): - val = float(val) + parsed_val = float(val) elif key in ('OriginLocation',): - val = int(val) + parsed_val = int(val) elif key == 'CreateDate': - val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f') + parsed_val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f') + else: + parsed_val = val if key in dev_fields and key[0].isupper(): - setattr(device, key, val) + setattr(device, key, parsed_val) else: - device.misc[key] = val + device.misc[key] = parsed_val for el_entry in el_device: tag = _tag(el_entry) @@ -96,7 +98,12 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]: f'with attributes {el_entry.attrib}') continue - bin_code = attrib['BinCode'] + bin_code: int | str + if bin_type == 'Decimal': + bin_code = int(attrib['BinCode']) + else: + bin_code = attrib['BinCode'] + if bin_code in device.bin_pass: logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!') continue @@ -104,17 +111,23 @@ 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: Union[List[List[str]], List[List[int]]] + data: 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() @@ -126,7 +139,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 b086b39..e0c0bd3 100644 --- a/g85/write.py +++ b/g85/write.py @@ -1,4 +1,5 @@ -from typing import Sequence, Tuple, List, TextIO, Union +from typing import TextIO, cast +from collections.abc import Sequence import logging import math from dataclasses import fields @@ -10,12 +11,15 @@ from .main import Map, Device logger = logging.getLogger(__name__) +class G85Error(Exception): + pass + + # Hack to directly pass through -def _escape_cdata(text): +def _escape_cdata(text: str) -> str: if text.startswith(''): return text - else: - return _original_escape_cdata(text) + return _original_escape_cdata(text) _original_escape_cdata = ElementTree._escape_cdata # type: ignore @@ -41,8 +45,11 @@ 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(): - el_map.set(field, getattr(wmap, field)) + if field[0].isupper() or field == 'xmlns': + val = getattr(wmap, field) + if val is None: + continue + el_map.set(field, val) for key, value in wmap.misc.items(): if key[0].isupper() and key in map_fields: continue @@ -61,7 +68,7 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non # Row data prep if device.map is None: - raise Exception(f'No _data for device pformat({device})') + raise G85Error(f'No _data for device pformat({device})') is_decimal = device.BinType == 'Decimal' row_texts, bin_length = prepare_data(device.map, decimal=is_decimal) @@ -81,8 +88,9 @@ 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_device, 'Row') + el_row = ElementTree.SubElement(el_data, 'Row') el_row.text = f'' # Device attribs @@ -90,6 +98,8 @@ 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}' @@ -97,6 +107,8 @@ 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) @@ -105,27 +117,34 @@ 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) -def prepare_data(data: List[List[Union[str, int]]], decimal: bool) -> Tuple[List[str], int]: + 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]: is_char = isinstance(data[0][0], str) + row_texts = [] if is_char: + data = cast(list[list[str]], data) char_len = len(data[0][0]) - else: + 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) max_value = max(max(rr) for rr in data) max_digits = math.ceil(math.log10(max_value)) - - 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: + for irow in data: + row_text = ' '.join(str(vv).zfill(max_digits) for vv in irow) + ' ' + row_texts.append(row_text) return row_texts, max_digits diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..91a15d0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,79 @@ +[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 deleted file mode 100644 index 1bdf57d..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/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', - ], - )