diff --git a/setup.py b/setup.py index 539de26..8cdd486 100644 --- a/setup.py +++ b/setup.py @@ -7,40 +7,39 @@ from setuptools import find_packages, setup from src.wireviz import APP_URL, CMD_NAME, __version__ -README_PATH = Path(__file__).parent / 'docs' / 'README.md' +README_PATH = Path(__file__).parent / "docs" / "README.md" setup( name=CMD_NAME, version=__version__, - author='Daniel Rojas', - #author_email='', - description='Easily document cables and wiring harnesses', + author="Daniel Rojas", + # author_email='', + description="Easily document cables and wiring harnesses", long_description=open(README_PATH).read(), - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", install_requires=[ - 'click', - 'pyyaml', - 'pillow', - 'graphviz', - ], - license='GPLv3', - keywords='cable connector hardware harness wiring wiring-diagram wiring-harness', + "click", + "pyyaml", + "pillow", + "graphviz", + ], + license="GPLv3", + keywords="cable connector hardware harness wiring wiring-diagram wiring-harness", url=APP_URL, - package_dir={'': 'src'}, - packages=find_packages('src'), + package_dir={"": "src"}, + packages=find_packages("src"), entry_points={ - 'console_scripts': [ - 'wireviz=wireviz.wv_cli:wireviz', - ], - }, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Topic :: Utilities', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + "console_scripts": [ + "wireviz=wireviz.wv_cli:wireviz", ], - + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Utilities", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + ], ) diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index c8a39ce..ad7317f 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -9,44 +9,55 @@ from wireviz.wv_colors import COLOR_CODES, Color, ColorMode, Colors, ColorScheme from wireviz.wv_helper import aspect_ratio, int2tuple # Each type alias have their legal values described in comments - validation might be implemented in the future -PlainText = str # Text not containing HTML tags nor newlines -Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output -MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output -Designator = PlainText # Case insensitive unique name of connector or cable +PlainText = str # Text not containing HTML tags nor newlines +Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output +MultilineHypertext = ( + str # Hypertext possibly also including newlines to break lines in diagram output +) + +Designator = PlainText # Case insensitive unique name of connector or cable # Literal type aliases below are commented to avoid requiring python 3.8 -ConnectorMultiplier = PlainText # = Literal['pincount', 'populated'] -CableMultiplier = PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length'] -ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both'] +ConnectorMultiplier = PlainText # = Literal['pincount', 'populated'] +CableMultiplier = ( + PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length'] +) +ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both'] # Type combinations -Pin = Union[int, PlainText] # Pin identifier -PinIndex = int # Zero-based pin index -Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield -NoneOrMorePins = Union[Pin, Tuple[Pin, ...], None] # None, one, or a tuple of pin identifiers -NoneOrMorePinIndices = Union[PinIndex, Tuple[PinIndex, ...], None] # None, one, or a tuple of zero-based pin indices -OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires +Pin = Union[int, PlainText] # Pin identifier +PinIndex = int # Zero-based pin index +Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield +NoneOrMorePins = Union[ + Pin, Tuple[Pin, ...], None +] # None, one, or a tuple of pin identifiers +NoneOrMorePinIndices = Union[ + PinIndex, Tuple[PinIndex, ...], None +] # None, one, or a tuple of zero-based pin indices +OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires # Metadata can contain whatever is needed by the HTML generation/template. MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...] + class Side(Enum): LEFT = auto() RIGHT = auto() + class Metadata(dict): pass @dataclass class Options: - fontname: PlainText = 'arial' - bgcolor: Color = 'WH' - bgcolor_node: Optional[Color] = 'WH' + fontname: PlainText = "arial" + bgcolor: Color = "WH" + bgcolor_node: Optional[Color] = "WH" bgcolor_connector: Optional[Color] = None bgcolor_cable: Optional[Color] = None bgcolor_bundle: Optional[Color] = None - color_mode: ColorMode = 'SHORT' + color_mode: ColorMode = "SHORT" mini_bom_mode: bool = True def __post_init__(self): @@ -87,9 +98,13 @@ class Image: self.fixedsize = (self.width or self.height) and self.scale is None if self.scale is None: - self.scale = "false" if not self.width and not self.height \ - else "both" if self.width and self.height \ - else "true" # When only one dimension is specified. + self.scale = ( + "false" + if not self.width and not self.height + else "both" + if self.width and self.height + else "true" + ) # When only one dimension is specified. if self.fixedsize: # If only one dimension is specified, compute the other @@ -118,7 +133,9 @@ class AdditionalComponent: @property def description(self) -> str: - return self.type.rstrip() + (f', {self.subtype.rstrip()}' if self.subtype else '') + return self.type.rstrip() + ( + f", {self.subtype.rstrip()}" if self.subtype else "" + ) @dataclass @@ -158,36 +175,44 @@ class Connector: self.ports_right = False self.visible_pins = {} - if self.style == 'simple': + if self.style == "simple": if self.pincount and self.pincount > 1: - raise Exception('Connectors with style set to simple may only have one pin') + raise Exception( + "Connectors with style set to simple may only have one pin" + ) self.pincount = 1 if not self.pincount: - self.pincount = max(len(self.pins), len(self.pinlabels), len(self.pincolors)) + self.pincount = max( + len(self.pins), len(self.pinlabels), len(self.pincolors) + ) if not self.pincount: - raise Exception('You need to specify at least one, pincount, pins, pinlabels, or pincolors') + raise Exception( + "You need to specify at least one, pincount, pins, pinlabels, or pincolors" + ) # create default list for pins (sequential) if not specified if not self.pins: self.pins = list(range(1, self.pincount + 1)) if len(self.pins) != len(set(self.pins)): - raise Exception('Pins are not unique') + raise Exception("Pins are not unique") if self.show_name is None: # hide designators for simple and for auto-generated connectors by default - self.show_name = (self.style != 'simple' and self.name[0:2] != '__') + self.show_name = self.style != "simple" and self.name[0:2] != "__" if self.show_pincount is None: - self.show_pincount = self.style != 'simple' # hide pincount for simple (1 pin) connectors by default + self.show_pincount = ( + self.style != "simple" + ) # hide pincount for simple (1 pin) connectors by default for loop in self.loops: # TODO: check that pins to connect actually exist # TODO: allow using pin labels in addition to pin numbers, just like when defining regular connections # TODO: include properties of wire used to create the loop if len(loop) != 2: - raise Exception('Loops must be between exactly two pins!') + raise Exception("Loops must be between exactly two pins!") for i, item in enumerate(self.additional_components): if isinstance(item, dict): @@ -203,12 +228,14 @@ class Connector: def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int: if not qty_multiplier: return 1 - elif qty_multiplier == 'pincount': + elif qty_multiplier == "pincount": return self.pincount - elif qty_multiplier == 'populated': + elif qty_multiplier == "populated": return sum(self.visible_pins.values()) else: - raise ValueError(f'invalid qty multiplier parameter for connector {qty_multiplier}') + raise ValueError( + f"invalid qty multiplier parameter for connector {qty_multiplier}" + ) @dataclass @@ -249,65 +276,79 @@ class Cable: if isinstance(self.gauge, str): # gauge and unit specified try: - g, u = self.gauge.split(' ') + g, u = self.gauge.split(" ") except Exception: - raise Exception(f'Cable {self.name} gauge={self.gauge} - Gauge must be a number, or number and unit separated by a space') + raise Exception( + f"Cable {self.name} gauge={self.gauge} - Gauge must be a number, or number and unit separated by a space" + ) self.gauge = g if self.gauge_unit is not None: - print(f'Warning: Cable {self.name} gauge_unit={self.gauge_unit} is ignored because its gauge contains {u}') - if u.upper() == 'AWG': + print( + f"Warning: Cable {self.name} gauge_unit={self.gauge_unit} is ignored because its gauge contains {u}" + ) + if u.upper() == "AWG": self.gauge_unit = u.upper() else: - self.gauge_unit = u.replace('mm2', 'mm\u00B2') + self.gauge_unit = u.replace("mm2", "mm\u00B2") elif self.gauge is not None: # gauge specified, assume mm2 if self.gauge_unit is None: - self.gauge_unit = 'mm\u00B2' + self.gauge_unit = "mm\u00B2" else: pass # gauge not specified if isinstance(self.length, str): # length and unit specified try: - L, u = self.length.split(' ') + L, u = self.length.split(" ") L = float(L) except Exception: - raise Exception(f'Cable {self.name} length={self.length} - Length must be a number, or number and unit separated by a space') + raise Exception( + f"Cable {self.name} length={self.length} - Length must be a number, or number and unit separated by a space" + ) self.length = L if self.length_unit is not None: - print(f'Warning: Cable {self.name} length_unit={self.length_unit} is ignored because its length contains {u}') + print( + f"Warning: Cable {self.name} length_unit={self.length_unit} is ignored because its length contains {u}" + ) self.length_unit = u elif not any(isinstance(self.length, t) for t in [int, float]): - raise Exception(f'Cable {self.name} length has a non-numeric value') + raise Exception(f"Cable {self.name} length has a non-numeric value") elif self.length_unit is None: - self.length_unit = 'm' + self.length_unit = "m" self.connections = [] if self.wirecount: # number of wires explicitly defined if self.colors: # use custom color palette (partly or looped if needed) pass - elif self.color_code: # use standard color palette (partly or looped if needed) + elif ( + self.color_code + ): # use standard color palette (partly or looped if needed) if self.color_code not in COLOR_CODES: - raise Exception('Unknown color code') + raise Exception("Unknown color code") self.colors = COLOR_CODES[self.color_code] else: # no colors defined, add dummy colors - self.colors = [''] * self.wirecount + self.colors = [""] * self.wirecount # make color code loop around if more wires than colors if self.wirecount > len(self.colors): m = self.wirecount // len(self.colors) + 1 self.colors = self.colors * int(m) # cut off excess after looping - self.colors = self.colors[:self.wirecount] + self.colors = self.colors[: self.wirecount] else: # wirecount implicit in length of color list if not self.colors: - raise Exception('Unknown number of wires. Must specify wirecount or colors (implicit length)') + raise Exception( + "Unknown number of wires. Must specify wirecount or colors (implicit length)" + ) self.wirecount = len(self.colors) if self.wirelabels: - if self.shield and 's' in self.wirelabels: - raise Exception('"s" may not be used as a wire label for a shielded cable.') + if self.shield and "s" in self.wirelabels: + raise Exception( + '"s" may not be used as a wire label for a shielded cable.' + ) # if lists of part numbers are provided check this is a bundle and that it matches the wirecount. for idfield in [self.manufacturer, self.mpn, self.supplier, self.spn, self.pn]: @@ -315,44 +356,58 @@ class Cable: if self.category == "bundle": # check the length if len(idfield) != self.wirecount: - raise Exception('lists of part data must match wirecount') + raise Exception("lists of part data must match wirecount") else: - raise Exception('lists of part data are only supported for bundles') + raise Exception("lists of part data are only supported for bundles") if self.show_name is None: - self.show_name = self.name[0:2] != '__' # hide designators for auto-generated cables by default + self.show_name = ( + self.name[0:2] != "__" + ) # hide designators for auto-generated cables by default if not self.show_wirenumbers: - self.show_wirenumbers = self.category != 'bundle' # by default, show wire numbers for cables, hide for bundles + self.show_wirenumbers = ( + self.category != "bundle" + ) # by default, show wire numbers for cables, hide for bundles for i, item in enumerate(self.additional_components): if isinstance(item, dict): self.additional_components[i] = AdditionalComponent(**item) # The *_pin arguments accept a tuple, but it seems not in use with the current code. - def connect(self, from_name: Optional[Designator], from_pin: NoneOrMorePinIndices, via_wire: OneOrMoreWires, - to_name: Optional[Designator], to_pin: NoneOrMorePinIndices) -> None: + def connect( + self, + from_name: Optional[Designator], + from_pin: NoneOrMorePinIndices, + via_wire: OneOrMoreWires, + to_name: Optional[Designator], + to_pin: NoneOrMorePinIndices, + ) -> None: from_pin = int2tuple(from_pin) via_wire = int2tuple(via_wire) to_pin = int2tuple(to_pin) if len(from_pin) != len(to_pin): - raise Exception('from_pin must have the same number of elements as to_pin') + raise Exception("from_pin must have the same number of elements as to_pin") for i, _ in enumerate(from_pin): - self.connections.append(Connection(from_name, from_pin[i], via_wire[i], to_name, to_pin[i])) + self.connections.append( + Connection(from_name, from_pin[i], via_wire[i], to_name, to_pin[i]) + ) def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float: if not qty_multiplier: return 1 - elif qty_multiplier == 'wirecount': + elif qty_multiplier == "wirecount": return self.wirecount - elif qty_multiplier == 'terminations': + elif qty_multiplier == "terminations": return len(self.connections) - elif qty_multiplier == 'length': + elif qty_multiplier == "length": return self.length - elif qty_multiplier == 'total_length': + elif qty_multiplier == "total_length": return self.length * self.wirecount else: - raise ValueError(f'invalid qty multiplier parameter for cable {qty_multiplier}') + raise ValueError( + f"invalid qty multiplier parameter for cable {qty_multiplier}" + ) @dataclass @@ -363,6 +418,7 @@ class Connection: to_name: Optional[Designator] to_pin: Optional[Pin] + @dataclass class MatePin: from_name: Designator @@ -371,6 +427,7 @@ class MatePin: to_pin: Pin shape: str + @dataclass class MateComponent: from_name: Designator diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index e495b2b..3e7903c 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -18,7 +18,7 @@ from wireviz.DataClasses import ( Metadata, Options, Tweak, - Side, + Side, ) from wireviz.wv_bom import ( HEADER_MPN, @@ -83,7 +83,15 @@ class Harness: def add_bom_item(self, item: dict) -> None: self.additional_bom_items.append(item) - def connect(self, from_name: str, from_pin: (int, str), via_name: str, via_wire: (int, str), to_name: str, to_pin: (int, str)) -> None: + def connect( + self, + from_name: str, + from_pin: (int, str), + via_name: str, + via_wire: (int, str), + to_name: str, + to_pin: (int, str), + ) -> None: # check from and to connectors for (name, pin) in zip([from_name, to_name], [from_pin, to_pin]): if name is not None and name in self.connectors: @@ -91,19 +99,21 @@ class Harness: # check if provided name is ambiguous if pin in connector.pins and pin in connector.pinlabels: if connector.pins.index(pin) != connector.pinlabels.index(pin): - raise Exception(f'{name}:{pin} is defined both in pinlabels and pins, for different pins.') + raise Exception( + f"{name}:{pin} is defined both in pinlabels and pins, for different pins." + ) # TODO: Maybe issue a warning if present in both lists but referencing the same pin? if pin in connector.pinlabels: if connector.pinlabels.count(pin) > 1: - raise Exception(f'{name}:{pin} is defined more than once.') + raise Exception(f"{name}:{pin} is defined more than once.") index = connector.pinlabels.index(pin) - pin = connector.pins[index] # map pin name to pin number + pin = connector.pins[index] # map pin name to pin number if name == from_name: from_pin = pin if name == to_name: to_pin = pin if not pin in connector.pins: - raise Exception(f'{name}:{pin} not found.') + raise Exception(f"{name}:{pin} not found.") # check via cable if via_name in self.cables: @@ -111,16 +121,26 @@ class Harness: # check if provided name is ambiguous if via_wire in cable.colors and via_wire in cable.wirelabels: if cable.colors.index(via_wire) != cable.wirelabels.index(via_wire): - raise Exception(f'{via_name}:{via_wire} is defined both in colors and wirelabels, for different wires.') + raise Exception( + f"{via_name}:{via_wire} is defined both in colors and wirelabels, for different wires." + ) # TODO: Maybe issue a warning if present in both lists but referencing the same wire? if via_wire in cable.colors: if cable.colors.count(via_wire) > 1: - raise Exception(f'{via_name}:{via_wire} is used for more than one wire.') - via_wire = cable.colors.index(via_wire) + 1 # list index starts at 0, wire IDs start at 1 + raise Exception( + f"{via_name}:{via_wire} is used for more than one wire." + ) + via_wire = ( + cable.colors.index(via_wire) + 1 + ) # list index starts at 0, wire IDs start at 1 elif via_wire in cable.wirelabels: if cable.wirelabels.count(via_wire) > 1: - raise Exception(f'{via_name}:{via_wire} is used for more than one wire.') - via_wire = cable.wirelabels.index(via_wire) + 1 # list index starts at 0, wire IDs start at 1 + raise Exception( + f"{via_name}:{via_wire} is used for more than one wire." + ) + via_wire = ( + cable.wirelabels.index(via_wire) + 1 + ) # list index starts at 0, wire IDs start at 1 # perform the actual connection self.cables[via_name].connect(from_name, from_pin, via_wire, to_name, to_pin) @@ -129,24 +149,29 @@ class Harness: if to_name in self.connectors: self.connectors[to_name].activate_pin(to_pin, Side.LEFT) - def create_graph(self) -> Graph: dot = Graph() - dot.body.append(f'// Graph generated by {APP_NAME} {__version__}') - dot.body.append(f'// {APP_URL}') - dot.attr('graph', rankdir='LR', - ranksep='2', - bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"), - nodesep='0.33', - fontname=self.options.fontname) - dot.attr('node', - shape='none', - width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label. - style='filled', - fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"), - fontname=self.options.fontname) - dot.attr('edge', style='bold', - fontname=self.options.fontname) + dot.body.append(f"// Graph generated by {APP_NAME} {__version__}") + dot.body.append(f"// {APP_URL}") + dot.attr( + "graph", + rankdir="LR", + ranksep="2", + bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"), + nodesep="0.33", + fontname=self.options.fontname, + ) + dot.attr( + "node", + shape="none", + width="0", + height="0", + margin="0", # Actual size of the node is entirely determined by the label. + style="filled", + fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"), + fontname=self.options.fontname, + ) + dot.attr("edge", style="bold", fontname=self.options.fontname) for connector in self.connectors.values(): @@ -156,325 +181,496 @@ class Harness: html = [] - rows = [[f'{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}' - if connector.show_name else None], - [pn_info_string(HEADER_PN, None, remove_links(connector.pn)), - html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)), - html_line_breaks(pn_info_string(HEADER_SPN, connector.supplier, connector.spn))], - [html_line_breaks(connector.type), - html_line_breaks(connector.subtype), - f'{connector.pincount}-pin' if connector.show_pincount else None, - translate_color(connector.color, self.options.color_mode) if connector.color else None, - html_colorbar(connector.color)], - '' if connector.style != 'simple' else None, - [html_image(connector.image)], - [html_caption(connector.image)]] + rows = [ + [ + f"{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}" + if connector.show_name + else None + ], + [ + pn_info_string(HEADER_PN, None, remove_links(connector.pn)), + html_line_breaks( + pn_info_string( + HEADER_MPN, connector.manufacturer, connector.mpn + ) + ), + html_line_breaks( + pn_info_string(HEADER_SPN, connector.supplier, connector.spn) + ), + ], + [ + html_line_breaks(connector.type), + html_line_breaks(connector.subtype), + f"{connector.pincount}-pin" if connector.show_pincount else None, + translate_color(connector.color, self.options.color_mode) + if connector.color + else None, + html_colorbar(connector.color), + ], + "" if connector.style != "simple" else None, + [html_image(connector.image)], + [html_caption(connector.image)], + ] rows.extend(get_additional_component_table(self, connector)) rows.append([html_line_breaks(connector.notes)]) html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor))) - if connector.style != 'simple': + if connector.style != "simple": pinhtml = [] - pinhtml.append('') + pinhtml.append( + '
' + ) - for pinindex, (pinname, pinlabel, pincolor) in enumerate(zip_longest(connector.pins, connector.pinlabels, connector.pincolors)): - if connector.hide_disconnected_pins and not connector.visible_pins.get(pinname, False): + for pinindex, (pinname, pinlabel, pincolor) in enumerate( + zip_longest( + connector.pins, connector.pinlabels, connector.pincolors + ) + ): + if ( + connector.hide_disconnected_pins + and not connector.visible_pins.get(pinname, False) + ): continue - pinhtml.append(' ') + pinhtml.append(" ") if connector.ports_left: pinhtml.append(f' ') if pinlabel: - pinhtml.append(f' ') + pinhtml.append(f" ") if connector.pincolors: if pincolor in wv_colors._color_hex.keys(): - pinhtml.append(f' ') - pinhtml.append( ' ') + pinhtml.append( + f' ' + ) + pinhtml.append(' ") else: - pinhtml.append( ' ') + pinhtml.append(' ') if connector.ports_right: pinhtml.append(f' ') - pinhtml.append(' ') + pinhtml.append(" ") - pinhtml.append('
{pinname}{pinlabel}{pinlabel}{translate_color(pincolor, self.options.color_mode)}') - pinhtml.append( ' ') - pinhtml.append(f' ') - pinhtml.append( '
') - pinhtml.append( '
{translate_color(pincolor, self.options.color_mode)}') + pinhtml.append(' ') + pinhtml.append( + f' ' + ) + pinhtml.append("
") + pinhtml.append("
{pinname}
') + pinhtml.append(" ") - html = [row.replace('', '\n'.join(pinhtml)) for row in html] + html = [ + row.replace("", "\n".join(pinhtml)) + for row in html + ] - html = '\n'.join(html) - dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled', - fillcolor=translate_color(self.options.bgcolor_connector, "HEX")) + html = "\n".join(html) + dot.node( + connector.name, + label=f"<\n{html}\n>", + shape="box", + style="filled", + fillcolor=translate_color(self.options.bgcolor_connector, "HEX"), + ) if len(connector.loops) > 0: - dot.attr('edge', color='#000000:#ffffff:#000000') + dot.attr("edge", color="#000000:#ffffff:#000000") if connector.ports_left: - loop_side = 'l' - loop_dir = 'w' + loop_side = "l" + loop_dir = "w" elif connector.ports_right: - loop_side = 'r' - loop_dir = 'e' + loop_side = "r" + loop_dir = "e" else: - raise Exception('No side for loops') + raise Exception("No side for loops") for loop in connector.loops: - dot.edge(f'{connector.name}:p{loop[0]}{loop_side}:{loop_dir}', - f'{connector.name}:p{loop[1]}{loop_side}:{loop_dir}') - + dot.edge( + f"{connector.name}:p{loop[0]}{loop_side}:{loop_dir}", + f"{connector.name}:p{loop[1]}{loop_side}:{loop_dir}", + ) # determine if there are double- or triple-colored wires in the harness; # if so, pad single-color wires to make all wires of equal thickness - pad = any(len(colorstr) > 2 for cable in self.cables.values() for colorstr in cable.colors) + pad = any( + len(colorstr) > 2 + for cable in self.cables.values() + for colorstr in cable.colors + ) for cable in self.cables.values(): html = [] - awg_fmt = '' + awg_fmt = "" if cable.show_equiv: # Only convert units we actually know about, i.e. currently # mm2 and awg --- other units _are_ technically allowed, # and passed through as-is. - if cable.gauge_unit =='mm\u00B2': - awg_fmt = f' ({awg_equiv(cable.gauge)} AWG)' - elif cable.gauge_unit.upper() == 'AWG': - awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' + if cable.gauge_unit == "mm\u00B2": + awg_fmt = f" ({awg_equiv(cable.gauge)} AWG)" + elif cable.gauge_unit.upper() == "AWG": + awg_fmt = f" ({mm2_equiv(cable.gauge)} mm\u00B2)" - rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}' - if cable.show_name else None], - [pn_info_string(HEADER_PN, None, - remove_links(cable.pn)) if not isinstance(cable.pn, list) else None, - html_line_breaks(pn_info_string(HEADER_MPN, - cable.manufacturer if not isinstance(cable.manufacturer, list) else None, - cable.mpn if not isinstance(cable.mpn, list) else None)), - html_line_breaks(pn_info_string(HEADER_SPN, - cable.supplier if not isinstance(cable.supplier, list) else None, - cable.spn if not isinstance(cable.spn, list) else None))], - [html_line_breaks(cable.type), - f'{cable.wirecount}x' if cable.show_wirecount else None, - f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None, - '+ S' if cable.shield else None, - f'{cable.length} {cable.length_unit}' if cable.length > 0 else None, - translate_color(cable.color, self.options.color_mode) if cable.color else None, - html_colorbar(cable.color)], - '', - [html_image(cable.image)], - [html_caption(cable.image)]] + rows = [ + [ + f"{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}" + if cable.show_name + else None + ], + [ + pn_info_string(HEADER_PN, None, remove_links(cable.pn)) + if not isinstance(cable.pn, list) + else None, + html_line_breaks( + pn_info_string( + HEADER_MPN, + cable.manufacturer + if not isinstance(cable.manufacturer, list) + else None, + cable.mpn if not isinstance(cable.mpn, list) else None, + ) + ), + html_line_breaks( + pn_info_string( + HEADER_SPN, + cable.supplier + if not isinstance(cable.supplier, list) + else None, + cable.spn if not isinstance(cable.spn, list) else None, + ) + ), + ], + [ + html_line_breaks(cable.type), + f"{cable.wirecount}x" if cable.show_wirecount else None, + f"{cable.gauge} {cable.gauge_unit}{awg_fmt}" + if cable.gauge + else None, + "+ S" if cable.shield else None, + f"{cable.length} {cable.length_unit}" if cable.length > 0 else None, + translate_color(cable.color, self.options.color_mode) + if cable.color + else None, + html_colorbar(cable.color), + ], + "", + [html_image(cable.image)], + [html_caption(cable.image)], + ] rows.extend(get_additional_component_table(self, cable)) rows.append([html_line_breaks(cable.notes)]) html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor))) wirehtml = [] - wirehtml.append('') # conductor table - wirehtml.append(' ') + wirehtml.append( + '
 
' + ) # conductor table + wirehtml.append(" ") - for i, (connection_color, wirelabel) in enumerate(zip_longest(cable.colors, cable.wirelabels), 1): - wirehtml.append(' ') - wirehtml.append(f' ') - wirehtml.append(f' ") + wirehtml.append(f" ") + wirehtml.append(f" ') - wirehtml.append(f' ') - wirehtml.append(' ') + wirehtml.append(f" ") + wirehtml.append(f" ") + wirehtml.append(" ") - bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000'] - wirehtml.append(f' ') - wirehtml.append(f' ') - wirehtml.append(' ') - if cable.category == 'bundle': # for bundles individual wires can have part information + bgcolors = ( + ["#000000"] + get_color_hex(connection_color, pad=pad) + ["#000000"] + ) + wirehtml.append(f" ") + wirehtml.append( + f' ") + wirehtml.append(" ") + if ( + cable.category == "bundle" + ): # for bundles individual wires can have part information # create a list of wire parameters wireidentification = [] if isinstance(cable.pn, list): - wireidentification.append(pn_info_string(HEADER_PN, None, remove_links(cable.pn[i - 1]))) - manufacturer_info = pn_info_string(HEADER_MPN, - cable.manufacturer[i - 1] if isinstance(cable.manufacturer, list) else None, - cable.mpn[i - 1] if isinstance(cable.mpn, list) else None) - supplier_info = pn_info_string(HEADER_SPN, - cable.supplier[i - 1] if isinstance(cable.supplier, list) else None, - cable.spn[i - 1] if isinstance(cable.spn, list) else None) + wireidentification.append( + pn_info_string( + HEADER_PN, None, remove_links(cable.pn[i - 1]) + ) + ) + manufacturer_info = pn_info_string( + HEADER_MPN, + cable.manufacturer[i - 1] + if isinstance(cable.manufacturer, list) + else None, + cable.mpn[i - 1] if isinstance(cable.mpn, list) else None, + ) + supplier_info = pn_info_string( + HEADER_SPN, + cable.supplier[i - 1] + if isinstance(cable.supplier, list) + else None, + cable.spn[i - 1] if isinstance(cable.spn, list) else None, + ) if manufacturer_info: wireidentification.append(html_line_breaks(manufacturer_info)) if supplier_info: wireidentification.append(html_line_breaks(supplier_info)) # print parameters into a table row under the wire - if len(wireidentification) > 0 : + if len(wireidentification) > 0: wirehtml.append(' ') + wirehtml.append(f" ") + wirehtml.append("
 
') + for i, (connection_color, wirelabel) in enumerate( + zip_longest(cable.colors, cable.wirelabels), 1 + ): + wirehtml.append("
") wireinfo = [] if cable.show_wirenumbers: wireinfo.append(str(i)) - colorstr = wv_colors.translate_color(connection_color, self.options.color_mode) + colorstr = wv_colors.translate_color( + connection_color, self.options.color_mode + ) if colorstr: wireinfo.append(colorstr) if cable.wirelabels: - wireinfo.append(wirelabel if wirelabel is not None else '') + wireinfo.append(wirelabel if wirelabel is not None else "") wirehtml.append(f' {":".join(wireinfo)}') - wirehtml.append(f'
') - wirehtml.append(' ') - for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors - wirehtml.append(f' ') - wirehtml.append('
') - wirehtml.append('
' + ) + wirehtml.append( + ' ' + ) + for j, bgcolor in enumerate( + bgcolors[::-1] + ): # Reverse to match the curved wires when more than 2 colors + wirehtml.append( + f' ' + ) + wirehtml.append("
") + wirehtml.append("
') - wirehtml.append(' ') + wirehtml.append( + '
' + ) for attrib in wireidentification: - wirehtml.append(f' ') - wirehtml.append('
{attrib}
') - wirehtml.append('
{attrib}
") + wirehtml.append(" ") if cable.shield: - wirehtml.append('  ') # spacer - wirehtml.append(' ') - wirehtml.append(' ') - wirehtml.append(' Shield') - wirehtml.append(' ') - wirehtml.append(' ') + wirehtml.append("  ") # spacer + wirehtml.append(" ") + wirehtml.append(" ") + wirehtml.append(" Shield") + wirehtml.append(" ") + wirehtml.append(" ") if isinstance(cable.shield, str): # shield is shown with specified color and black borders shield_color_hex = wv_colors.get_color_hex(cable.shield)[0] - attributes = f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"' + attributes = ( + f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"' + ) else: # shield is shown as a thin black wire attributes = f'height="2" bgcolor="#000000" border="0"' - wirehtml.append(f' ') + wirehtml.append( + f' ' + ) - wirehtml.append('  ') - wirehtml.append(' ') + wirehtml.append("  ") + wirehtml.append(" ") - html = [row.replace('', '\n'.join(wirehtml)) for row in html] + html = [ + row.replace("", "\n".join(wirehtml)) for row in html + ] # connections for connection in cable.connections: - if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield - dot.attr('edge', color=':'.join(['#000000'] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + ['#000000'])) + if isinstance( + connection.via_port, int + ): # check if it's an actual wire and not a shield + dot.attr( + "edge", + color=":".join( + ["#000000"] + + wv_colors.get_color_hex( + cable.colors[connection.via_port - 1], pad=pad + ) + + ["#000000"] + ), + ) else: # it's a shield connection # shield is shown with specified color and black borders, or as a thin black wire otherwise - dot.attr('edge', color=':'.join(['#000000', shield_color_hex, '#000000']) if isinstance(cable.shield, str) else '#000000') + dot.attr( + "edge", + color=":".join(["#000000", shield_color_hex, "#000000"]) + if isinstance(cable.shield, str) + else "#000000", + ) if connection.from_pin is not None: # connect to left from_connector = self.connectors[connection.from_name] from_pin_index = from_connector.pins.index(connection.from_pin) - from_port_str = f':p{from_pin_index+1}r' if from_connector.style != 'simple' else '' - code_left_1 = f'{connection.from_name}{from_port_str}:e' - code_left_2 = f'{cable.name}:w{connection.via_port}:w' + from_port_str = ( + f":p{from_pin_index+1}r" + if from_connector.style != "simple" + else "" + ) + code_left_1 = f"{connection.from_name}{from_port_str}:e" + code_left_2 = f"{cable.name}:w{connection.via_port}:w" dot.edge(code_left_1, code_left_2) if from_connector.show_name: - from_info = [str(connection.from_name), str(connection.from_pin)] + from_info = [ + str(connection.from_name), + str(connection.from_pin), + ] if from_connector.pinlabels: pinlabel = from_connector.pinlabels[from_pin_index] - if pinlabel != '': + if pinlabel != "": from_info.append(pinlabel) - from_string = ':'.join(from_info) + from_string = ":".join(from_info) else: - from_string = '' - html = [row.replace(f'', from_string) for row in html] + from_string = "" + html = [ + row.replace(f"", from_string) + for row in html + ] if connection.to_pin is not None: # connect to right to_connector = self.connectors[connection.to_name] to_pin_index = to_connector.pins.index(connection.to_pin) - to_port_str = f':p{to_pin_index+1}l' if to_connector.style != 'simple' else '' - code_right_1 = f'{cable.name}:w{connection.via_port}:e' - code_right_2 = f'{connection.to_name}{to_port_str}:w' + to_port_str = ( + f":p{to_pin_index+1}l" if to_connector.style != "simple" else "" + ) + code_right_1 = f"{cable.name}:w{connection.via_port}:e" + code_right_2 = f"{connection.to_name}{to_port_str}:w" dot.edge(code_right_1, code_right_2) if to_connector.show_name: to_info = [str(connection.to_name), str(connection.to_pin)] if to_connector.pinlabels: pinlabel = to_connector.pinlabels[to_pin_index] - if pinlabel != '': + if pinlabel != "": to_info.append(pinlabel) - to_string = ':'.join(to_info) + to_string = ":".join(to_info) else: - to_string = '' - html = [row.replace(f'', to_string) for row in html] + to_string = "" + html = [ + row.replace(f"", to_string) + for row in html + ] - style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \ - ('filled', self.options.bgcolor_cable) - html = '\n'.join(html) - dot.node(cable.name, label=f'<\n{html}\n>', shape='box', - style=style, fillcolor=translate_color(bgcolor, "HEX")) + style, bgcolor = ( + ("filled,dashed", self.options.bgcolor_bundle) + if cable.category == "bundle" + else ("filled", self.options.bgcolor_cable) + ) + html = "\n".join(html) + dot.node( + cable.name, + label=f"<\n{html}\n>", + shape="box", + style=style, + fillcolor=translate_color(bgcolor, "HEX"), + ) def typecheck(name: str, value: Any, expect: type) -> None: if not isinstance(value, expect): - raise Exception(f'Unexpected value type of {name}: Expected {expect}, got {type(value)}\n{value}') + raise Exception( + f"Unexpected value type of {name}: Expected {expect}, got {type(value)}\n{value}" + ) # TODO?: Differ between override attributes and HTML? if self.tweak.override is not None: - typecheck('tweak.override', self.tweak.override, dict) + typecheck("tweak.override", self.tweak.override, dict) for k, d in self.tweak.override.items(): - typecheck(f'tweak.override.{k} key', k, str) - typecheck(f'tweak.override.{k} value', d, dict) + typecheck(f"tweak.override.{k} key", k, str) + typecheck(f"tweak.override.{k} value", d, dict) for a, v in d.items(): - typecheck(f'tweak.override.{k}.{a} key', a, str) - typecheck(f'tweak.override.{k}.{a} value', v, (str, type(None))) + typecheck(f"tweak.override.{k}.{a} key", a, str) + typecheck(f"tweak.override.{k}.{a} value", v, (str, type(None))) # Override generated attributes of selected entries matching tweak.override. for i, entry in enumerate(dot.body): if isinstance(entry, str): # Find a possibly quoted keyword after leading TAB(s) and followed by [ ]. - match = re.match(r'^\t*(")?((?(1)[^"]|[^ "])+)(?(1)") \[.*\]$', entry, re.S) + match = re.match( + r'^\t*(")?((?(1)[^"]|[^ "])+)(?(1)") \[.*\]$', entry, re.S + ) keyword = match and match[2] if keyword in self.tweak.override.keys(): for attr, value in self.tweak.override[keyword].items(): if value is None: - entry, n_subs = re.subn(f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', '', entry) + entry, n_subs = re.subn( + f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', "", entry + ) if n_subs < 1: - print(f'Harness.create_graph() warning: {attr} not found in {keyword}!') + print( + f"Harness.create_graph() warning: {attr} not found in {keyword}!" + ) elif n_subs > 1: - print(f'Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!') + print( + f"Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!" + ) continue - if len(value) == 0 or ' ' in value: - value = value.replace('"', r'\"') + if len(value) == 0 or " " in value: + value = value.replace('"', r"\"") value = f'"{value}"' - entry, n_subs = re.subn(f'{attr}=("[^"]*"|[^] ]*)', f'{attr}={value}', entry) + entry, n_subs = re.subn( + f'{attr}=("[^"]*"|[^] ]*)', f"{attr}={value}", entry + ) if n_subs < 1: # If attr not found, then append it - entry = re.sub(r'\]$', f' {attr}={value}]', entry) + entry = re.sub(r"\]$", f" {attr}={value}]", entry) elif n_subs > 1: - print(f'Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!') + print( + f"Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!" + ) dot.body[i] = entry if self.tweak.append is not None: if isinstance(self.tweak.append, list): for i, element in enumerate(self.tweak.append, 1): - typecheck(f'tweak.append[{i}]', element, str) + typecheck(f"tweak.append[{i}]", element, str) dot.body.extend(self.tweak.append) else: - typecheck('tweak.append', self.tweak.append, str) + typecheck("tweak.append", self.tweak.append, str) dot.body.append(self.tweak.append) for mate in self.mates: - if mate.shape[0] == '<' and mate.shape[-1] == '>': - dir = 'both' - elif mate.shape[0] == '<': - dir = 'back' - elif mate.shape[-1] == '>': - dir = 'forward' + if mate.shape[0] == "<" and mate.shape[-1] == ">": + dir = "both" + elif mate.shape[0] == "<": + dir = "back" + elif mate.shape[-1] == ">": + dir = "forward" else: - dir = 'none' + dir = "none" if isinstance(mate, MatePin): - color = '#000000' + color = "#000000" elif isinstance(mate, MateComponent): - color = '#000000:#000000' + color = "#000000:#000000" else: - raise Exception(f'{mate} is an unknown mate') + raise Exception(f"{mate} is an unknown mate") from_connector = self.connectors[mate.from_name] - if isinstance(mate, MatePin) and self.connectors[mate.from_name].style != 'simple': + if ( + isinstance(mate, MatePin) + and self.connectors[mate.from_name].style != "simple" + ): from_pin_index = from_connector.pins.index(mate.from_pin) - from_port_str = f':p{from_pin_index+1}r' + from_port_str = f":p{from_pin_index+1}r" else: # MateComponent or style == 'simple' - from_port_str = '' - if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple': + from_port_str = "" + if ( + isinstance(mate, MatePin) + and self.connectors[mate.to_name].style != "simple" + ): to_pin_index = to_connector.pins.index(mate.to_pin) - to_port_str = f':p{to_pin_index+1}l' if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple' else '' + to_port_str = ( + f":p{to_pin_index+1}l" + if isinstance(mate, MatePin) + and self.connectors[mate.to_name].style != "simple" + else "" + ) else: # MateComponent or style == 'simple' - to_port_str = '' - code_from = f'{mate.from_name}{from_port_str}:e' + to_port_str = "" + code_from = f"{mate.from_name}{from_port_str}:e" to_connector = self.connectors[mate.to_name] - code_to = f'{mate.to_name}{to_port_str}:w' + code_to = f"{mate.to_name}{to_port_str}:w" - dot.attr('edge', color=color, style='dashed', dir=dir) + dot.attr("edge", color=color, style="dashed", dir=dir) dot.edge(code_from, code_to) return dot @@ -492,52 +688,64 @@ class Harness: @property def png(self): from io import BytesIO + graph = self.graph data = BytesIO() - data.write(graph.pipe(format='png')) + data.write(graph.pipe(format="png")) data.seek(0) return data.read() @property def svg(self): from io import BytesIO + graph = self.graph data = BytesIO() - data.write(graph.pipe(format='svg')) + data.write(graph.pipe(format="svg")) data.seek(0) return data.read() - def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('html','png','svg','tsv')) -> None: + def output( + self, + filename: (str, Path), + view: bool = False, + cleanup: bool = True, + fmt: tuple = ("html", "png", "svg", "tsv"), + ) -> None: # graphical output graph = self.graph - svg_already_exists = Path(f'{filename}.svg').exists() # if SVG already exists, do not delete later + svg_already_exists = Path( + f"{filename}.svg" + ).exists() # if SVG already exists, do not delete later # graphical output for f in fmt: - if f in ('png', 'svg', 'html'): - if f == 'html': # if HTML format is specified, - f = 'svg' # generate SVG for embedding into HTML + if f in ("png", "svg", "html"): + if f == "html": # if HTML format is specified, + f = "svg" # generate SVG for embedding into HTML # TODO: prevent rendering SVG twice when both SVG and HTML are specified graph.format = f graph.render(filename=filename, view=view, cleanup=cleanup) # GraphViz output - if 'gv' in fmt: - graph.save(filename=f'{filename}.gv') + if "gv" in fmt: + graph.save(filename=f"{filename}.gv") # BOM output bomlist = bom_list(self.bom()) - if 'tsv' in fmt: - with open_file_write(f'{filename}.bom.tsv') as file: + if "tsv" in fmt: + with open_file_write(f"{filename}.bom.tsv") as file: file.write(tuplelist2tsv(bomlist)) - if 'csv' in fmt: - print('CSV output is not yet supported') # TODO: implement CSV output (preferrably using CSV library) + if "csv" in fmt: + print( + "CSV output is not yet supported" + ) # TODO: implement CSV output (preferrably using CSV library) # HTML output - if 'html' in fmt: + if "html" in fmt: generate_html_output(filename, bomlist, self.metadata, self.options) # PDF output - if 'pdf' in fmt: - print('PDF output is not yet supported') # TODO: implement PDF output + if "pdf" in fmt: + print("PDF output is not yet supported") # TODO: implement PDF output # delete SVG if not needed - if 'html' in fmt and not 'svg' in fmt and not svg_already_exists: - Path(f'{filename}.svg').unlink() + if "html" in fmt and not "svg" in fmt and not svg_already_exists: + Path(f"{filename}.svg").unlink() def bom(self): if not self._bom: diff --git a/src/wireviz/__init__.py b/src/wireviz/__init__.py index 178fbf1..08f7167 100644 --- a/src/wireviz/__init__.py +++ b/src/wireviz/__init__.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # Please don't import anything in this file to avoid issues when it is imported in setup.py -__version__ = '0.4-dev' +__version__ = "0.4-dev" -CMD_NAME = 'wireviz' # Lower case command and module name -APP_NAME = 'WireViz' # Application name in texts meant to be human readable -APP_URL = 'https://github.com/formatc1702/WireViz' +CMD_NAME = "wireviz" # Lower case command and module name +APP_NAME = "WireViz" # Application name in texts meant to be human readable +APP_URL = "https://github.com/formatc1702/WireViz" diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index ee486b3..13c03ca 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -14,34 +14,36 @@ from wv_helper import open_file_append, open_file_read, open_file_write from wireviz import APP_NAME, __version__, wireviz dir = script_path.parent.parent.parent -readme = 'readme.md' +readme = "readme.md" groups = { - 'examples': { - 'path': dir / 'examples', - 'prefix': 'ex', - readme: [], # Include no files - 'title': 'Example Gallery', + "examples": { + "path": dir / "examples", + "prefix": "ex", + readme: [], # Include no files + "title": "Example Gallery", }, - 'tutorial' : { - 'path': dir / 'tutorial', - 'prefix': 'tutorial', - readme: ['md', 'yml'], # Include .md and .yml files - 'title': f'{APP_NAME} Tutorial', + "tutorial": { + "path": dir / "tutorial", + "prefix": "tutorial", + readme: ["md", "yml"], # Include .md and .yml files + "title": f"{APP_NAME} Tutorial", }, - 'demos' : { - 'path': dir / 'examples', - 'prefix': 'demo', + "demos": { + "path": dir / "examples", + "prefix": "demo", }, } -input_extensions = ['.yml'] -extensions_not_containing_graphviz_output = ['.gv', '.bom.tsv'] -extensions_containing_graphviz_output = ['.png', '.svg', '.html'] -generated_extensions = extensions_not_containing_graphviz_output + extensions_containing_graphviz_output +input_extensions = [".yml"] +extensions_not_containing_graphviz_output = [".gv", ".bom.tsv"] +extensions_containing_graphviz_output = [".png", ".svg", ".html"] +generated_extensions = ( + extensions_not_containing_graphviz_output + extensions_containing_graphviz_output +) def collect_filenames(description, groupkey, ext_list): - path = groups[groupkey]['path'] + path = groups[groupkey]["path"] patterns = [f"{groups[groupkey]['prefix']}*{ext}" for ext in ext_list] if ext_list != input_extensions and readme in groups[groupkey]: patterns.append(readme) @@ -52,107 +54,141 @@ def collect_filenames(description, groupkey, ext_list): def build_generated(groupkeys): for key in groupkeys: # preparation - path = groups[key]['path'] + path = groups[key]["path"] build_readme = readme in groups[key] if build_readme: - include_readme = 'md' in groups[key][readme] - include_source = 'yml' in groups[key][readme] + include_readme = "md" in groups[key][readme] + include_source = "yml" in groups[key][readme] with open_file_write(path / readme) as out: out.write(f'# {groups[key]["title"]}\n\n') # collect and iterate input YAML files - for yaml_file in collect_filenames('Building', key, input_extensions): + for yaml_file in collect_filenames("Building", key, input_extensions): print(f' "{yaml_file}"') wireviz.parse_file(yaml_file) if build_readme: - i = ''.join(filter(str.isdigit, yaml_file.stem)) + i = "".join(filter(str.isdigit, yaml_file.stem)) with open_file_append(path / readme) as out: if include_readme: - with open_file_read(yaml_file.with_suffix('.md')) as info: + with open_file_read(yaml_file.with_suffix(".md")) as info: for line in info: - out.write(line.replace('## ', f'## {i} - ')) - out.write('\n\n') + out.write(line.replace("## ", f"## {i} - ")) + out.write("\n\n") else: - out.write(f'## Example {i}\n') + out.write(f"## Example {i}\n") if include_source: with open_file_read(yaml_file) as src: - out.write('```yaml\n') + out.write("```yaml\n") for line in src: out.write(line) - out.write('```\n') - out.write('\n') + out.write("```\n") + out.write("\n") - out.write(f'![]({yaml_file.stem}.png)\n\n') - out.write(f'[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n') + out.write(f"![]({yaml_file.stem}.png)\n\n") + out.write( + f"[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n" + ) def clean_generated(groupkeys): for key in groupkeys: # collect and remove files - for filename in collect_filenames('Cleaning', key, generated_extensions): + for filename in collect_filenames("Cleaning", key, generated_extensions): if filename.is_file(): print(f' rm "{filename}"') Path(filename).unlink() -def compare_generated(groupkeys, branch = '', include_graphviz_output = False): +def compare_generated(groupkeys, branch="", include_graphviz_output=False): if branch: - branch = f' {branch.strip()}' - compare_extensions = generated_extensions if include_graphviz_output else extensions_not_containing_graphviz_output + branch = f" {branch.strip()}" + compare_extensions = ( + generated_extensions + if include_graphviz_output + else extensions_not_containing_graphviz_output + ) for key in groupkeys: # collect and compare files - for filename in collect_filenames('Comparing', key, compare_extensions): + for filename in collect_filenames("Comparing", key, compare_extensions): cmd = f'git --no-pager diff{branch} -- "{filename}"' - print(f' {cmd}') + print(f" {cmd}") os.system(cmd) -def restore_generated(groupkeys, branch = ''): +def restore_generated(groupkeys, branch=""): if branch: - branch = f' {branch.strip()}' + branch = f" {branch.strip()}" for key in groupkeys: # collect input YAML files - filename_list = collect_filenames('Restoring', key, input_extensions) + filename_list = collect_filenames("Restoring", key, input_extensions) # collect files to restore - filename_list = [fn.with_suffix(ext) for fn in filename_list for ext in generated_extensions] + filename_list = [ + fn.with_suffix(ext) for fn in filename_list for ext in generated_extensions + ] if readme in groups[key]: - filename_list.append(groups[key]['path'] / readme) + filename_list.append(groups[key]["path"] / readme) # restore files for filename in filename_list: cmd = f'git checkout{branch} -- "{filename}"' - print(f' {cmd}') + print(f" {cmd}") os.system(cmd) def parse_args(): - parser = argparse.ArgumentParser(description=f'{APP_NAME} Example Manager',) - parser.add_argument('-V', '--version', action='version', version=f'%(prog)s - {APP_NAME} {__version__}') - parser.add_argument('action', nargs='?', action='store', - choices=['build','clean','compare','diff','restore'], default='build', - help='what to do with the generated files (default: build)') - parser.add_argument('-c', '--compare-graphviz-output', action='store_true', - help='the Graphviz output is also compared (default: False)') - parser.add_argument('-b', '--branch', action='store', default='', - help='branch or commit to compare with or restore from') - parser.add_argument('-g', '--groups', nargs='+', - choices=groups.keys(), default=groups.keys(), - help='the groups of generated files (default: all)') + parser = argparse.ArgumentParser( + description=f"{APP_NAME} Example Manager", + ) + parser.add_argument( + "-V", + "--version", + action="version", + version=f"%(prog)s - {APP_NAME} {__version__}", + ) + parser.add_argument( + "action", + nargs="?", + action="store", + choices=["build", "clean", "compare", "diff", "restore"], + default="build", + help="what to do with the generated files (default: build)", + ) + parser.add_argument( + "-c", + "--compare-graphviz-output", + action="store_true", + help="the Graphviz output is also compared (default: False)", + ) + parser.add_argument( + "-b", + "--branch", + action="store", + default="", + help="branch or commit to compare with or restore from", + ) + parser.add_argument( + "-g", + "--groups", + nargs="+", + choices=groups.keys(), + default=groups.keys(), + help="the groups of generated files (default: all)", + ) return parser.parse_args() def main(): args = parse_args() - if args.action == 'build': + if args.action == "build": build_generated(args.groups) - elif args.action == 'clean': + elif args.action == "clean": clean_generated(args.groups) - elif args.action == 'compare' or args.action == 'diff': + elif args.action == "compare" or args.action == "diff": compare_generated(args.groups, args.branch, args.compare_graphviz_output) - elif args.action == 'restore': + elif args.action == "restore": restore_generated(args.groups, args.branch) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index d4cfc0c..5f11736 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Tuple import yaml -if __name__ == '__main__': +if __name__ == "__main__": sys.path.insert(0, str(Path(__file__).parent.parent)) # add src/wireviz to PATH from wireviz.DataClasses import Metadata, Options, Tweak @@ -21,7 +21,13 @@ from wireviz.wv_helper import ( ) -def parse_text(yaml_str: str, file_out: (str, Path) = None, output_formats: (None, str, Tuple[str]) = ('html','png','svg','tsv'), return_types: (None, str, Tuple[str]) = None, image_paths: List = []) -> Any: +def parse_text( + yaml_str: str, + file_out: (str, Path) = None, + output_formats: (None, str, Tuple[str]) = ("html", "png", "svg", "tsv"), + return_types: (None, str, Tuple[str]) = None, + image_paths: List = [], +) -> Any: """ Parses a YAML input string and does the high-level harness conversion @@ -37,9 +43,22 @@ def parse_text(yaml_str: str, file_out: (str, Path) = None, output_formats: (Non - "harness" - will return the `Harness` instance """ yaml_data = yaml.safe_load(yaml_str) - return parse(yaml_data=yaml_data, file_out=file_out, output_formats=output_formats, return_types=return_types, image_paths=image_paths) + return parse( + yaml_data=yaml_data, + file_out=file_out, + output_formats=output_formats, + return_types=return_types, + image_paths=image_paths, + ) -def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, str, Tuple[str]) = ('html','png','svg','tsv'), return_types: (None, str, Tuple[str]) = None, image_paths: List = []) -> Any: + +def parse( + yaml_data: Dict, + file_out: (str, Path) = None, + output_formats: (None, str, Tuple[str]) = ("html", "png", "svg", "tsv"), + return_types: (None, str, Tuple[str]) = None, + image_paths: List = [], +) -> Any: """ Parses a YAML dictionary and does the high-level harness conversion @@ -55,29 +74,32 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, - "harness" - will return the `Harness` instance """ - # define variables ========================================================= # containers for parsed component data and connection sets template_connectors = {} - template_cables = {} - connection_sets = [] + template_cables = {} + connection_sets = [] # actual harness harness = Harness( - metadata = Metadata(**yaml_data.get('metadata', {})), - options = Options(**yaml_data.get('options', {})), - tweak = Tweak(**yaml_data.get('tweak', {})), + metadata=Metadata(**yaml_data.get("metadata", {})), + options=Options(**yaml_data.get("options", {})), + tweak=Tweak(**yaml_data.get("tweak", {})), ) # others - designators_and_templates = {} # store mapping of components to their respective template - autogenerated_designators = {} # keep track of auto-generated designators to avoid duplicates + designators_and_templates = ( + {} + ) # store mapping of components to their respective template + autogenerated_designators = ( + {} + ) # keep track of auto-generated designators to avoid duplicates - if 'title' not in harness.metadata: - harness.metadata['title'] = Path(file_out).stem + if "title" not in harness.metadata: + harness.metadata["title"] = Path(file_out).stem # add items # parse YAML input file ==================================================== - sections = ['connectors', 'cables', 'connections'] + sections = ["connectors", "cables", "connections"] types = [dict, dict, list] for sec, ty in zip(sections, types): if sec in yaml_data and type(yaml_data[sec]) == ty: # section exists @@ -85,14 +107,18 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, if ty == dict: for key, attribs in yaml_data[sec].items(): # The Image dataclass might need to open an image file with a relative path. - image = attribs.get('image') + image = attribs.get("image") if isinstance(image, dict): - image_path = image['src'] - if image_path and not Path(image_path).is_absolute(): # resolve relative image path - image['src'] = smart_file_resolve(image_path, image_paths) - if sec == 'connectors': + image_path = image["src"] + if ( + image_path and not Path(image_path).is_absolute() + ): # resolve relative image path + image["src"] = smart_file_resolve( + image_path, image_paths + ) + if sec == "connectors": template_connectors[key] = attribs - elif sec == 'cables': + elif sec == "cables": template_cables[key] = attribs else: # section exists but is empty pass @@ -102,24 +128,28 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, elif ty == list: yaml_data[sec] = [] - connection_sets = yaml_data['connections'] + connection_sets = yaml_data["connections"] # go through connection sets, generate and connect components ============== - template_separator_char = '.' # TODO: make user-configurable (in case user wants to use `.` as part of their template/component names) + template_separator_char = "." # TODO: make user-configurable (in case user wants to use `.` as part of their template/component names) def resolve_designator(inp, separator): if separator in inp: # generate a new instance of an item if inp.count(separator) > 1: - raise Exception(f'{inp} - Found more than one separator ({separator})') + raise Exception(f"{inp} - Found more than one separator ({separator})") template, designator = inp.split(separator) - if designator == '': - autogenerated_designators[template] = autogenerated_designators.get(template, 0) + 1 - designator = f'__{template}_{autogenerated_designators[template]}' + if designator == "": + autogenerated_designators[template] = ( + autogenerated_designators.get(template, 0) + 1 + ) + designator = f"__{template}_{autogenerated_designators[template]}" # check if redefining existing component to different template if designator in designators_and_templates: if designators_and_templates[designator] != template: - raise Exception(f'Trying to redefine {designator} from {designators_and_templates[designator]} to {template}') + raise Exception( + f"Trying to redefine {designator} from {designators_and_templates[designator]} to {template}" + ) else: designators_and_templates[designator] = template else: @@ -132,7 +162,7 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, # utilities to check for alternating connectors and cables/arrows ========== - alternating_types = ['connector','cable/arrow'] + alternating_types = ["connector", "cable/arrow"] expected_type = None def check_type(designator, template, actual_type): @@ -141,7 +171,9 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, expected_type = actual_type if actual_type != expected_type: # did not alternate - raise Exception(f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}') + raise Exception( + f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}' + ) def alternate_type(): # flip between connector and cable/arrow nonlocal expected_type @@ -155,9 +187,11 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, if isinstance(entry, list): connectioncount.append(len(entry)) elif isinstance(entry, dict): - connectioncount.append(len(expand(list(entry.values())[0]))) # - X1: [1-4,6] yields 5 + connectioncount.append( + len(expand(list(entry.values())[0])) + ) # - X1: [1-4,6] yields 5 else: - pass # strings do not reveal connectioncount + pass # strings do not reveal connectioncount if not any(connectioncount): # no item in the list revealed connection count; # assume connection count is 1 @@ -172,7 +206,9 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, # check that all entries are the same length if len(set(connectioncount)) > 1: - raise Exception('All items in connection set must reference the same number of connections') + raise Exception( + "All items in connection set must reference the same number of connections" + ) # all entries are the same length, connection count is set connectioncount = connectioncount[0] @@ -185,7 +221,9 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, for index, entry in enumerate(connection_set): if isinstance(entry, list): for subindex, item in enumerate(entry): - template, designator = resolve_designator(item, template_separator_char) + template, designator = resolve_designator( + item, template_separator_char + ) connection_set[index][subindex] = designator elif isinstance(entry, dict): key = list(entry.keys())[0] @@ -209,8 +247,8 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, # Populate wiring harness ============================================== expected_type = None # reset check for alternating types - # at the beginning of every connection set - # since each set may begin with either type + # at the beginning of every connection set + # since each set may begin with either type # generate components for entry in connection_set: @@ -219,22 +257,30 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, template = designators_and_templates[designator] if designator in harness.connectors: # existing connector instance - check_type(designator, template, 'connector') - elif template in template_connectors.keys(): # generate new connector instance from template - check_type(designator, template, 'connector') - harness.add_connector(name = designator, **template_connectors[template]) + check_type(designator, template, "connector") + elif ( + template in template_connectors.keys() + ): # generate new connector instance from template + check_type(designator, template, "connector") + harness.add_connector( + name=designator, **template_connectors[template] + ) elif designator in harness.cables: # existing cable instance - check_type(designator, template, 'cable/arrow') - elif template in template_cables.keys(): # generate new cable instance from template - check_type(designator, template, 'cable/arrow') - harness.add_cable(name = designator, **template_cables[template]) + check_type(designator, template, "cable/arrow") + elif ( + template in template_cables.keys() + ): # generate new cable instance from template + check_type(designator, template, "cable/arrow") + harness.add_cable(name=designator, **template_cables[template]) elif is_arrow(designator): - check_type(designator, template, 'cable/arrow') + check_type(designator, template, "cable/arrow") # arrows do not need to be generated here else: - raise Exception(f'{template} is an unknown template/designator/arrow.') + raise Exception( + f"{template} is an unknown template/designator/arrow." + ) alternate_type() # entries in connection set must alternate between connectors and cables/arrows @@ -249,29 +295,49 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, designator = list(item.keys())[0] if designator in harness.cables: - if index_item == 0: # list started with a cable, no connector to join on left side + if ( + index_item == 0 + ): # list started with a cable, no connector to join on left side from_name, from_pin = (None, None) else: - from_name, from_pin = get_single_key_and_value(entry[index_item-1]) + from_name, from_pin = get_single_key_and_value( + entry[index_item - 1] + ) via_name, via_pin = (designator, item[designator]) - if index_item == len(entry) - 1: # list ends with a cable, no connector to join on right side + if ( + index_item == len(entry) - 1 + ): # list ends with a cable, no connector to join on right side to_name, to_pin = (None, None) else: - to_name, to_pin = get_single_key_and_value(entry[index_item+1]) - harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) + to_name, to_pin = get_single_key_and_value( + entry[index_item + 1] + ) + harness.connect( + from_name, from_pin, via_name, via_pin, to_name, to_pin + ) elif is_arrow(designator): if index_item == 0: # list starts with an arrow - raise Exception('An arrow cannot be at the start of a connection set') + raise Exception( + "An arrow cannot be at the start of a connection set" + ) elif index_item == len(entry) - 1: # list ends with an arrow - raise Exception('An arrow cannot be at the end of a connection set') + raise Exception( + "An arrow cannot be at the end of a connection set" + ) - from_name, from_pin = get_single_key_and_value(entry[index_item-1]) - via_name, via_pin = (designator, None) - to_name, to_pin = get_single_key_and_value(entry[index_item+1]) - if '-' in designator: # mate pin by pin - harness.add_mate_pin(from_name, from_pin, to_name, to_pin, designator) - elif '=' in designator and index_entry == 0: # mate two connectors as a whole + from_name, from_pin = get_single_key_and_value( + entry[index_item - 1] + ) + via_name, via_pin = (designator, None) + to_name, to_pin = get_single_key_and_value(entry[index_item + 1]) + if "-" in designator: # mate pin by pin + harness.add_mate_pin( + from_name, from_pin, to_name, to_pin, designator + ) + elif ( + "=" in designator and index_entry == 0 + ): # mate two connectors as a whole harness.add_mate_component(from_name, to_name, designator) # harness population completed ============================================= @@ -285,17 +351,17 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None, if return_types is not None: returns = [] - if isinstance(return_types, str): # only one return type speficied + if isinstance(return_types, str): # only one return type speficied return_types = [return_types] return_types = [t.lower() for t in return_types] for rt in return_types: - if rt == 'png': + if rt == "png": returns.append(harness.png) - if rt == 'svg': + if rt == "svg": returns.append(harness.svg) - if rt == 'harness': + if rt == "harness": returns.append(harness) return tuple(returns) if len(returns) != 1 else returns[0] @@ -314,8 +380,10 @@ def parse_file(yaml_file: str, file_out: (str, Path) = None) -> None: parse_text(yaml_str, file_out=file_out, image_paths=[Path(yaml_file).parent]) -def main(): - print('When running from the command line, please use wv_cli.py instead.') -if __name__ == '__main__': +def main(): + print("When running from the command line, please use wv_cli.py instead.") + + +if __name__ == "__main__": main() diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 56df752..29b49ef 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -9,76 +9,108 @@ from wireviz.wv_colors import translate_color from wireviz.wv_gv_html import html_bgcolor_attr, html_line_breaks from wireviz.wv_helper import clean_whitespace -BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators') -BOM_COLUMNS_OPTIONAL = ('pn', 'manufacturer', 'mpn', 'supplier', 'spn') -BOM_COLUMNS_IN_KEY = ('description', 'unit') + BOM_COLUMNS_OPTIONAL +BOM_COLUMNS_ALWAYS = ("id", "description", "qty", "unit", "designators") +BOM_COLUMNS_OPTIONAL = ("pn", "manufacturer", "mpn", "supplier", "spn") +BOM_COLUMNS_IN_KEY = ("description", "unit") + BOM_COLUMNS_OPTIONAL -HEADER_PN = 'P/N' -HEADER_MPN = 'MPN' -HEADER_SPN = 'SPN' +HEADER_PN = "P/N" +HEADER_MPN = "MPN" +HEADER_SPN = "SPN" BOMKey = Tuple[str, ...] BOMColumn = str # = Literal[*BOM_COLUMNS_ALWAYS, *BOM_COLUMNS_OPTIONAL] BOMEntry = Dict[BOMColumn, Union[str, int, float, List[str], None]] + def optional_fields(part: Union[Connector, Cable, AdditionalComponent]) -> BOMEntry: """Return part field values for the optional BOM columns as a dict.""" part = asdict(part) return {field: part.get(field) for field in BOM_COLUMNS_OPTIONAL} -def get_additional_component_table(harness: "Harness", component: Union[Connector, Cable]) -> List[str]: + +def get_additional_component_table( + harness: "Harness", component: Union[Connector, Cable] +) -> List[str]: """Return a list of diagram node table row strings with additional components.""" rows = [] if component.additional_components: rows.append(["Additional components"]) for part in component.additional_components: common_args = { - 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), - 'unit': part.unit, - 'bgcolor': part.bgcolor, + "qty": part.qty * component.get_qty_multiplier(part.qty_multiplier), + "unit": part.unit, + "bgcolor": part.bgcolor, } if harness.options.mini_bom_mode: - id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description})) - rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args)) + id = get_bom_index( + harness.bom(), + bom_entry_key({**asdict(part), "description": part.description}), + ) + rows.append( + component_table_entry( + f"#{id} ({part.type.rstrip()})", **common_args + ) + ) else: - rows.append(component_table_entry(part.description, **common_args, **optional_fields(part))) + rows.append( + component_table_entry( + part.description, **common_args, **optional_fields(part) + ) + ) return rows + def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]: """Return a list of BOM entries with additional components.""" bom_entries = [] for part in component.additional_components: - bom_entries.append({ - 'description': part.description, - 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), - 'unit': part.unit, - 'designators': component.name if component.show_name else None, - **optional_fields(part), - }) + bom_entries.append( + { + "description": part.description, + "qty": part.qty * component.get_qty_multiplier(part.qty_multiplier), + "unit": part.unit, + "designators": component.name if component.show_name else None, + **optional_fields(part), + } + ) return bom_entries + def bom_entry_key(entry: BOMEntry) -> BOMKey: """Return a tuple of string values from the dict that must be equal to join BOM entries.""" - if 'key' not in entry: - entry['key'] = tuple(clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY) - return entry['key'] + if "key" not in entry: + entry["key"] = tuple( + clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY + ) + return entry["key"] + def generate_bom(harness: "Harness") -> List[BOMEntry]: """Return a list of BOM entries generated from the harness.""" from wireviz.Harness import Harness # Local import to avoid circular imports + bom_entries = [] # connectors for connector in harness.connectors.values(): if not connector.ignore_in_bom: - description = ('Connector' - + (f', {connector.type}' if connector.type else '') - + (f', {connector.subtype}' if connector.subtype else '') - + (f', {connector.pincount} pins' if connector.show_pincount else '') - + (f', {translate_color(connector.color, harness.options.color_mode)}' if connector.color else '')) - bom_entries.append({ - 'description': description, 'designators': connector.name if connector.show_name else None, - **optional_fields(connector), - }) + description = ( + "Connector" + + (f", {connector.type}" if connector.type else "") + + (f", {connector.subtype}" if connector.subtype else "") + + (f", {connector.pincount} pins" if connector.show_pincount else "") + + ( + f", {translate_color(connector.color, harness.options.color_mode)}" + if connector.color + else "" + ) + ) + bom_entries.append( + { + "description": description, + "designators": connector.name if connector.show_name else None, + **optional_fields(connector), + } + ) # add connectors aditional components to bom bom_entries.extend(get_additional_component_bom(connector)) @@ -87,29 +119,58 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: # TODO: If category can have other non-empty values than 'bundle', maybe it should be part of description? for cable in harness.cables.values(): if not cable.ignore_in_bom: - if cable.category != 'bundle': + if cable.category != "bundle": # process cable as a single entity - description = ('Cable' - + (f', {cable.type}' if cable.type else '') - + (f', {cable.wirecount}') - + (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires') - + ( ' shielded' if cable.shield else '') - + (f', {translate_color(cable.color, harness.options.color_mode)}' if cable.color else '')) - bom_entries.append({ - 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, - **optional_fields(cable), - }) + description = ( + "Cable" + + (f", {cable.type}" if cable.type else "") + + (f", {cable.wirecount}") + + ( + f" x {cable.gauge} {cable.gauge_unit}" + if cable.gauge + else " wires" + ) + + (" shielded" if cable.shield else "") + + ( + f", {translate_color(cable.color, harness.options.color_mode)}" + if cable.color + else "" + ) + ) + bom_entries.append( + { + "description": description, + "qty": cable.length, + "unit": cable.length_unit, + "designators": cable.name if cable.show_name else None, + **optional_fields(cable), + } + ) else: # add each wire from the bundle to the bom for index, color in enumerate(cable.colors): - description = ('Wire' - + (f', {cable.type}' if cable.type else '') - + (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '') - + (f', {translate_color(color, harness.options.color_mode)}' if color else '')) - bom_entries.append({ - 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, - **{k: index_if_list(v, index) for k, v in optional_fields(cable).items()}, - }) + description = ( + "Wire" + + (f", {cable.type}" if cable.type else "") + + (f", {cable.gauge} {cable.gauge_unit}" if cable.gauge else "") + + ( + f", {translate_color(color, harness.options.color_mode)}" + if color + else "" + ) + ) + bom_entries.append( + { + "description": description, + "qty": cable.length, + "unit": cable.length_unit, + "designators": cable.name if cable.show_name else None, + **{ + k: index_if_list(v, index) + for k, v in optional_fields(cable).items() + }, + } + ) # add cable/bundles aditional components to bom bom_entries.extend(get_additional_component_bom(cable)) @@ -118,86 +179,114 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: bom_entries.extend(harness.additional_bom_items) # remove line breaks if present and cleanup any resulting whitespace issues - bom_entries = [{k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries] + bom_entries = [ + {k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries + ] # deduplicate bom bom = [] for _, group in groupby(sorted(bom_entries, key=bom_entry_key), key=bom_entry_key): group_entries = list(group) - designators = sum((make_list(entry.get('designators')) for entry in group_entries), []) - total_qty = sum(entry.get('qty', 1) for entry in group_entries) - bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': sorted(set(designators))}) + designators = sum( + (make_list(entry.get("designators")) for entry in group_entries), [] + ) + total_qty = sum(entry.get("qty", 1) for entry in group_entries) + bom.append( + { + **group_entries[0], + "qty": round(total_qty, 3), + "designators": sorted(set(designators)), + } + ) # add an incrementing id to each bom entry - return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] + return [{**entry, "id": index} for index, entry in enumerate(bom, 1)] + def get_bom_index(bom: List[BOMEntry], target: BOMKey) -> int: """Return id of BOM entry or raise exception if not found.""" for entry in bom: if bom_entry_key(entry) == target: - return entry['id'] - raise Exception('Internal error: No BOM entry found matching: ' + '|'.join(target)) + return entry["id"] + raise Exception("Internal error: No BOM entry found matching: " + "|".join(target)) + def bom_list(bom: List[BOMEntry]) -> List[List[str]]: """Return list of BOM rows as lists of column strings with headings in top row.""" keys = list(BOM_COLUMNS_ALWAYS) # Always include this fixed set of BOM columns. - for fieldname in BOM_COLUMNS_OPTIONAL: # Include only those optional BOM columns that are in use. + for ( + fieldname + ) in ( + BOM_COLUMNS_OPTIONAL + ): # Include only those optional BOM columns that are in use. if any(entry.get(fieldname) for entry in bom): keys.append(fieldname) # Custom mapping from internal name to BOM column headers. # Headers not specified here are generated by capitilising the internal name. bom_headings = { - 'pn': HEADER_PN, - 'mpn': HEADER_MPN, - 'spn': HEADER_SPN, + "pn": HEADER_PN, + "mpn": HEADER_MPN, + "spn": HEADER_SPN, } - return ([[bom_headings.get(k, k.capitalize()) for k in keys]] + # Create header row with key names - [[make_str(entry.get(k)) for k in keys] for entry in bom]) # Create string list for each entry row + return [ + [bom_headings.get(k, k.capitalize()) for k in keys] + ] + [ # Create header row with key names + [make_str(entry.get(k)) for k in keys] for entry in bom + ] # Create string list for each entry row + def component_table_entry( - type: str, - qty: Union[int, float], - unit: Optional[str] = None, - bgcolor: Optional[Color] = None, - pn: Optional[str] = None, - manufacturer: Optional[str] = None, - mpn: Optional[str] = None, - supplier: Optional[str] = None, - spn: Optional[str] = None, - ) -> str: + type: str, + qty: Union[int, float], + unit: Optional[str] = None, + bgcolor: Optional[Color] = None, + pn: Optional[str] = None, + manufacturer: Optional[str] = None, + mpn: Optional[str] = None, + supplier: Optional[str] = None, + spn: Optional[str] = None, +) -> str: """Return a diagram node table row string with an additional component.""" part_number_list = [ pn_info_string(HEADER_PN, None, pn), pn_info_string(HEADER_MPN, manufacturer, mpn), pn_info_string(HEADER_SPN, supplier, spn), ] - output = (f'{qty}' - + (f' {unit}' if unit else '') - + f' x {type}' - + ('
' if any(part_number_list) else '') - + (', '.join([pn for pn in part_number_list if pn]))) + output = ( + f"{qty}" + + (f" {unit}" if unit else "") + + f" x {type}" + + ("
" if any(part_number_list) else "") + + (", ".join([pn for pn in part_number_list if pn])) + ) # format the above output as left aligned text in a single visible cell # indent is set to two to match the indent in the generated html table - return f''' + return f"""
-
{html_line_breaks(output)}
''' + """ -def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]: + +def pn_info_string( + header: str, name: Optional[str], number: Optional[str] +) -> Optional[str]: """Return the company name and/or the part number in one single string or None otherwise.""" - number = str(number).strip() if number is not None else '' + number = str(number).strip() if number is not None else "" if name or number: return f'{name if name else header}{": " + number if number else ""}' else: return None + def index_if_list(value: Any, index: int) -> Any: """Return the value indexed if it is a list, or simply the value otherwise.""" return value[index] if isinstance(value, list) else value + def make_list(value: Any) -> list: """Return value if a list, empty list if None, or single element list otherwise.""" return value if isinstance(value, list) else [] if value is None else [value] + def make_str(value: Any) -> str: """Return comma separated elements if a list, empty string if None, or value as a string otherwise.""" - return ', '.join(str(element) for element in make_list(value)) + return ", ".join(str(element) for element in make_list(value)) diff --git a/src/wireviz/wv_cli.py b/src/wireviz/wv_cli.py index d3f80ce..b3f3daf 100644 --- a/src/wireviz/wv_cli.py +++ b/src/wireviz/wv_cli.py @@ -6,30 +6,64 @@ from pathlib import Path import click -if __name__ == '__main__': - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +if __name__ == "__main__": + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import wireviz.wireviz as wv from wireviz import APP_NAME, __version__ from wireviz.wv_helper import open_file_read -format_codes = {'c': 'csv', 'g': 'gv', 'h': 'html', 'p': 'png', 'P': 'pdf', 's': 'svg', 't': 'tsv'} +format_codes = { + "c": "csv", + "g": "gv", + "h": "html", + "p": "png", + "P": "pdf", + "s": "svg", + "t": "tsv", +} + +epilog = "The -f or --format option accepts a string containing one or more of the following characters to specify which file types to output:\n" +epilog += ", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()]) -epilog = 'The -f or --format option accepts a string containing one or more of the following characters to specify which file types to output:\n' -epilog += ', '.join([f'{key} ({value.upper()})' for key, value in format_codes.items()]) @click.command(epilog=epilog, no_args_is_help=True) -@click.argument('file', nargs=-1) -@click.option('-f', '--format', default='hpst', type=str, show_default=True, help='Output formats (see below).') -@click.option('-p', '--prepend', default=None, type=Path, help='YAML file to prepend to the input file (optional).') -@click.option('-o', '--output-file', default=None, type=Path, help='File name (without extension) to use for output, if different from input file name.') -@click.option('-V', '--version', is_flag=True, default=False, help=f'Output {APP_NAME} version and exit.') +@click.argument("file", nargs=-1) +@click.option( + "-f", + "--format", + default="hpst", + type=str, + show_default=True, + help="Output formats (see below).", +) +@click.option( + "-p", + "--prepend", + default=None, + type=Path, + help="YAML file to prepend to the input file (optional).", +) +@click.option( + "-o", + "--output-file", + default=None, + type=Path, + help="File name (without extension) to use for output, if different from input file name.", +) +@click.option( + "-V", + "--version", + is_flag=True, + default=False, + help=f"Output {APP_NAME} version and exit.", +) def wireviz(file, format, prepend, output_file, version): """ Parses the provided FILE and generates the specified outputs. """ print() - print(f'{APP_NAME} {__version__}') + print(f"{APP_NAME} {__version__}") if version: return # print version number only and exit @@ -47,35 +81,39 @@ def wireviz(file, format, prepend, output_file, version): if code in format_codes: output_formats.append(format_codes[code]) else: - raise Exception(f'Unknown output format: {code}') + raise Exception(f"Unknown output format: {code}") output_formats = tuple(sorted(set(output_formats))) - output_formats_str = f'[{"|".join(output_formats)}]' if len(output_formats) > 1 else output_formats[0] + output_formats_str = ( + f'[{"|".join(output_formats)}]' + if len(output_formats) > 1 + else output_formats[0] + ) image_paths = [] # check prepend file if prepend: prepend = Path(prepend) if not prepend.exists(): - raise Exception(f'File does not exist:\n{prepend}') - print('Prepend file:', prepend) + raise Exception(f"File does not exist:\n{prepend}") + print("Prepend file:", prepend) with open_file_read(prepend) as file_handle: - prepend_input = file_handle.read() + '\n' + prepend_input = file_handle.read() + "\n" prepend_dir = prepend.parent else: - prepend_input = '' + prepend_input = "" prepend_dir = None # run WireVIz on each input file for file in filepaths: file = Path(file) if not file.exists(): - raise Exception(f'File does not exist:\n{file}') + raise Exception(f"File does not exist:\n{file}") - file_out = file.with_suffix('') if not output_file else output_file + file_out = file.with_suffix("") if not output_file else output_file - print('Input file: ', file) - print('Output file: ', f'{file_out}.{output_formats_str}') + print("Input file: ", file) + print("Output file: ", f"{file_out}.{output_formats_str}") with open_file_read(file) as file_handle: yaml_input = file_handle.read() @@ -83,9 +121,15 @@ def wireviz(file, format, prepend, output_file, version): yaml_input = prepend_input + yaml_input - wv.parse_text(yaml_input, file_out=file_out, output_formats=output_formats, image_paths=[file_dir, prepend_dir]) + wv.parse_text( + yaml_input, + file_out=file_out, + output_formats=output_formats, + image_paths=[file_dir, prepend_dir], + ) print() -if __name__ == '__main__': + +if __name__ == "__main__": wireviz() diff --git a/src/wireviz/wv_colors.py b/src/wireviz/wv_colors.py index e07ed53..7e9a6b4 100644 --- a/src/wireviz/wv_colors.py +++ b/src/wireviz/wv_colors.py @@ -3,181 +3,337 @@ from typing import Dict, List COLOR_CODES = { - 'DIN': ['WH', 'BN', 'GN', 'YE', 'GY', 'PK', 'BU', 'RD', 'BK', 'VT', 'GYPK', 'RDBU', 'WHGN', 'BNGN', 'WHYE', 'YEBN', - 'WHGY', 'GYBN', 'WHPK', 'PKBN', 'WHBU', 'BNBU', 'WHRD', 'BNRD', 'WHBK', 'BNBK', 'GYGN', 'YEGY', 'PKGN', - 'YEPK', 'GNBU', 'YEBU', 'GNRD', 'YERD', 'GNBK', 'YEBK', 'GYBU', 'PKBU', 'GYRD', 'PKRD', 'GYBK', 'PKBK', - 'BUBK', 'RDBK', 'WHBNBK', 'YEGNBK', 'GYPKBK', 'RDBUBK', 'WHGNBK', 'BNGNBK', 'WHYEBK', 'YEBNBK', 'WHGYBK', - 'GYBNBK', 'WHPKBK', 'PKBNBK', 'WHBUBK', 'BNBUBK', 'WHRDBK', 'BNRDBK'], - 'IEC': ['BN', 'RD', 'OG', 'YE', 'GN', 'BU', 'VT', 'GY', 'WH', 'BK'], - 'BW': ['BK', 'WH'], + "DIN": [ + "WH", + "BN", + "GN", + "YE", + "GY", + "PK", + "BU", + "RD", + "BK", + "VT", + "GYPK", + "RDBU", + "WHGN", + "BNGN", + "WHYE", + "YEBN", + "WHGY", + "GYBN", + "WHPK", + "PKBN", + "WHBU", + "BNBU", + "WHRD", + "BNRD", + "WHBK", + "BNBK", + "GYGN", + "YEGY", + "PKGN", + "YEPK", + "GNBU", + "YEBU", + "GNRD", + "YERD", + "GNBK", + "YEBK", + "GYBU", + "PKBU", + "GYRD", + "PKRD", + "GYBK", + "PKBK", + "BUBK", + "RDBK", + "WHBNBK", + "YEGNBK", + "GYPKBK", + "RDBUBK", + "WHGNBK", + "BNGNBK", + "WHYEBK", + "YEBNBK", + "WHGYBK", + "GYBNBK", + "WHPKBK", + "PKBNBK", + "WHBUBK", + "BNBUBK", + "WHRDBK", + "BNRDBK", + ], + "IEC": ["BN", "RD", "OG", "YE", "GN", "BU", "VT", "GY", "WH", "BK"], + "BW": ["BK", "WH"], # 25-pair color code - see also https://en.wikipedia.org/wiki/25-pair_color_code # 5 major colors (WH,RD,BK,YE,VT) combined with 5 minor colors (BU,OG,GN,BN,SL). # Each POTS pair tip (+) had major/minor color, and ring (-) had minor/major color. - 'TEL': [ # 25x2: Ring and then tip of each pair - 'BUWH', 'WHBU', 'OGWH', 'WHOG', 'GNWH', 'WHGN', 'BNWH', 'WHBN', 'SLWH', 'WHSL', - 'BURD', 'RDBU', 'OGRD', 'RDOG', 'GNRD', 'RDGN', 'BNRD', 'RDBN', 'SLRD', 'RDSL', - 'BUBK', 'BKBU', 'OGBK', 'BKOG', 'GNBK', 'BKGN', 'BNBK', 'BKBN', 'SLBK', 'BKSL', - 'BUYE', 'YEBU', 'OGYE', 'YEOG', 'GNYE', 'YEGN', 'BNYE', 'YEBN', 'SLYE', 'YESL', - 'BUVT', 'VTBU', 'OGVT', 'VTOG', 'GNVT', 'VTGN', 'BNVT', 'VTBN', 'SLVT', 'VTSL'], - 'TELALT': [ # 25x2: Tip and then ring of each pair - 'WHBU', 'BU', 'WHOG', 'OG', 'WHGN', 'GN', 'WHBN', 'BN', 'WHSL', 'SL', - 'RDBU', 'BURD', 'RDOG', 'OGRD', 'RDGN', 'GNRD', 'RDBN', 'BNRD', 'RDSL', 'SLRD', - 'BKBU', 'BUBK', 'BKOG', 'OGBK', 'BKGN', 'GNBK', 'BKBN', 'BNBK', 'BKSL', 'SLBK', - 'YEBU', 'BUYE', 'YEOG', 'OGYE', 'YEGN', 'GNYE', 'YEBN', 'BNYE', 'YESL', 'SLYE', - 'VTBU', 'BUVT', 'VTOG', 'OGVT', 'VTGN', 'GNVT', 'VTBN', 'BNVT', 'VTSL', 'SLVT'], - 'T568A': ['WHGN', 'GN', 'WHOG', 'BU', 'WHBU', 'OG', 'WHBN', 'BN'], - 'T568B': ['WHOG', 'OG', 'WHGN', 'BU', 'WHBU', 'GN', 'WHBN', 'BN'], + "TEL": [ # 25x2: Ring and then tip of each pair + "BUWH", + "WHBU", + "OGWH", + "WHOG", + "GNWH", + "WHGN", + "BNWH", + "WHBN", + "SLWH", + "WHSL", + "BURD", + "RDBU", + "OGRD", + "RDOG", + "GNRD", + "RDGN", + "BNRD", + "RDBN", + "SLRD", + "RDSL", + "BUBK", + "BKBU", + "OGBK", + "BKOG", + "GNBK", + "BKGN", + "BNBK", + "BKBN", + "SLBK", + "BKSL", + "BUYE", + "YEBU", + "OGYE", + "YEOG", + "GNYE", + "YEGN", + "BNYE", + "YEBN", + "SLYE", + "YESL", + "BUVT", + "VTBU", + "OGVT", + "VTOG", + "GNVT", + "VTGN", + "BNVT", + "VTBN", + "SLVT", + "VTSL", + ], + "TELALT": [ # 25x2: Tip and then ring of each pair + "WHBU", + "BU", + "WHOG", + "OG", + "WHGN", + "GN", + "WHBN", + "BN", + "WHSL", + "SL", + "RDBU", + "BURD", + "RDOG", + "OGRD", + "RDGN", + "GNRD", + "RDBN", + "BNRD", + "RDSL", + "SLRD", + "BKBU", + "BUBK", + "BKOG", + "OGBK", + "BKGN", + "GNBK", + "BKBN", + "BNBK", + "BKSL", + "SLBK", + "YEBU", + "BUYE", + "YEOG", + "OGYE", + "YEGN", + "GNYE", + "YEBN", + "BNYE", + "YESL", + "SLYE", + "VTBU", + "BUVT", + "VTOG", + "OGVT", + "VTGN", + "GNVT", + "VTBN", + "BNVT", + "VTSL", + "SLVT", + ], + "T568A": ["WHGN", "GN", "WHOG", "BU", "WHBU", "OG", "WHBN", "BN"], + "T568B": ["WHOG", "OG", "WHGN", "BU", "WHBU", "GN", "WHBN", "BN"], } # Convention: Color names should be 2 letters long, to allow for multicolored wires _color_hex = { - 'BK': '#000000', - 'WH': '#ffffff', - 'GY': '#999999', - 'PK': '#ff66cc', - 'RD': '#ff0000', - 'OG': '#ff8000', - 'YE': '#ffff00', - 'OL': '#708000', # olive green - 'GN': '#00ff00', - 'TQ': '#00ffff', - 'LB': '#a0dfff', # light blue - 'BU': '#0066ff', - 'VT': '#8000ff', - 'BN': '#895956', - 'BG': '#ceb673', # beige - 'IV': '#f5f0d0', # ivory - 'SL': '#708090', - 'CU': '#d6775e', # Faux-copper look, for bare CU wire - 'SN': '#aaaaaa', # Silvery look for tinned bare wire - 'SR': '#84878c', # Darker silver for silvered wire - 'GD': '#ffcf80', # Golden color for gold + "BK": "#000000", + "WH": "#ffffff", + "GY": "#999999", + "PK": "#ff66cc", + "RD": "#ff0000", + "OG": "#ff8000", + "YE": "#ffff00", + "OL": "#708000", # olive green + "GN": "#00ff00", + "TQ": "#00ffff", + "LB": "#a0dfff", # light blue + "BU": "#0066ff", + "VT": "#8000ff", + "BN": "#895956", + "BG": "#ceb673", # beige + "IV": "#f5f0d0", # ivory + "SL": "#708090", + "CU": "#d6775e", # Faux-copper look, for bare CU wire + "SN": "#aaaaaa", # Silvery look for tinned bare wire + "SR": "#84878c", # Darker silver for silvered wire + "GD": "#ffcf80", # Golden color for gold } _color_full = { - 'BK': 'black', - 'WH': 'white', - 'GY': 'grey', - 'PK': 'pink', - 'RD': 'red', - 'OG': 'orange', - 'YE': 'yellow', - 'OL': 'olive green', - 'GN': 'green', - 'TQ': 'turquoise', - 'LB': 'light blue', - 'BU': 'blue', - 'VT': 'violet', - 'BN': 'brown', - 'BG': 'beige', - 'IV': 'ivory', - 'SL': 'slate', - 'CU': 'copper', - 'SN': 'tin', - 'SR': 'silver', - 'GD': 'gold', + "BK": "black", + "WH": "white", + "GY": "grey", + "PK": "pink", + "RD": "red", + "OG": "orange", + "YE": "yellow", + "OL": "olive green", + "GN": "green", + "TQ": "turquoise", + "LB": "light blue", + "BU": "blue", + "VT": "violet", + "BN": "brown", + "BG": "beige", + "IV": "ivory", + "SL": "slate", + "CU": "copper", + "SN": "tin", + "SR": "silver", + "GD": "gold", } _color_ger = { - 'BK': 'sw', - 'WH': 'ws', - 'GY': 'gr', - 'PK': 'rs', - 'RD': 'rt', - 'OG': 'or', - 'YE': 'ge', - 'OL': 'ol', # olivgrün - 'GN': 'gn', - 'TQ': 'tk', - 'LB': 'hb', # hellblau - 'BU': 'bl', - 'VT': 'vi', - 'BN': 'br', - 'BG': 'bg', # beige - 'IV': 'eb', # elfenbeinfarben - 'SL': 'si', # Schiefer - 'CU': 'ku', # Kupfer - 'SN': 'vz', # verzinkt - 'SR': 'ag', # Silber - 'GD': 'au', # Gold + "BK": "sw", + "WH": "ws", + "GY": "gr", + "PK": "rs", + "RD": "rt", + "OG": "or", + "YE": "ge", + "OL": "ol", # olivgrün + "GN": "gn", + "TQ": "tk", + "LB": "hb", # hellblau + "BU": "bl", + "VT": "vi", + "BN": "br", + "BG": "bg", # beige + "IV": "eb", # elfenbeinfarben + "SL": "si", # Schiefer + "CU": "ku", # Kupfer + "SN": "vz", # verzinkt + "SR": "ag", # Silber + "GD": "au", # Gold } -color_default = '#ffffff' +color_default = "#ffffff" -_hex_digits = set('0123456789abcdefABCDEF') +_hex_digits = set("0123456789abcdefABCDEF") # Literal type aliases below are commented to avoid requiring python 3.8 Color = str # Two-letter color name = Literal[_color_hex.keys()] Colors = str # One or more two-letter color names (Color) concatenated into one string -ColorMode = str # = Literal['full', 'FULL', 'hex', 'HEX', 'short', 'SHORT', 'ger', 'GER'] +ColorMode = ( + str # = Literal['full', 'FULL', 'hex', 'HEX', 'short', 'SHORT', 'ger', 'GER'] +) ColorScheme = str # Color scheme name = Literal[COLOR_CODES.keys()] def get_color_hex(input: Colors, pad: bool = False) -> List[str]: """Return list of hex colors from either a string of color names or :-separated hex colors.""" - if input is None or input == '': + if input is None or input == "": return [color_default] - elif input[0] == '#': # Hex color(s) - output = input.split(':') + elif input[0] == "#": # Hex color(s) + output = input.split(":") for i, c in enumerate(output): - if c[0] != '#' or not all(d in _hex_digits for d in c[1:]): + if c[0] != "#" or not all(d in _hex_digits for d in c[1:]): if c != input: - c += f' in input: {input}' - print(f'Invalid hex color: {c}') + c += f" in input: {input}" + print(f"Invalid hex color: {c}") output[i] = color_default else: # Color name(s) + def lookup(c: str) -> str: try: return _color_hex[c] except KeyError: if c != input: - c += f' in input: {input}' - print(f'Unknown color name: {c}') + c += f" in input: {input}" + print(f"Unknown color name: {c}") return color_default - output = [lookup(input[i:i + 2]) for i in range(0, len(input), 2)] + output = [lookup(input[i : i + 2]) for i in range(0, len(input), 2)] if len(output) == 2: # Give wires with EXACTLY 2 colors that striped look. output += output[:1] elif pad and len(output) == 1: # Hacky style fix: Give single color wires - output *= 3 # a triple-up so that wires are the same size. + output *= 3 # a triple-up so that wires are the same size. return output def get_color_translation(translate: Dict[Color, str], input: Colors) -> List[str]: """Return list of colors translations from either a string of color names or :-separated hex colors.""" + def from_hex(hex_input: str) -> str: for color, hex in _color_hex.items(): if hex == hex_input: return translate[color] return f'({",".join(str(int(hex_input[i:i+2], 16)) for i in range(1, 6, 2))})' - return [from_hex(h) for h in input.lower().split(':')] if input[0] == '#' else \ - [translate.get(input[i:i+2], '??') for i in range(0, len(input), 2)] + return ( + [from_hex(h) for h in input.lower().split(":")] + if input[0] == "#" + else [translate.get(input[i : i + 2], "??") for i in range(0, len(input), 2)] + ) def translate_color(input: Colors, color_mode: ColorMode) -> str: - if input == '' or input is None: - return '' + if input == "" or input is None: + return "" upper = color_mode.isupper() if not (color_mode.isupper() or color_mode.islower()): - raise Exception('Unknown color mode capitalization') + raise Exception("Unknown color mode capitalization") color_mode = color_mode.lower() - if color_mode == 'full': + if color_mode == "full": output = "/".join(get_color_translation(_color_full, input)) - elif color_mode == 'hex': - output = ':'.join(get_color_hex(input, pad=False)) - elif color_mode == 'ger': + elif color_mode == "hex": + output = ":".join(get_color_hex(input, pad=False)) + elif color_mode == "ger": output = "".join(get_color_translation(_color_ger, input)) - elif color_mode == 'short': + elif color_mode == "short": output = input else: - raise Exception('Unknown color mode') + raise Exception("Unknown color mode") if upper: return output.upper() else: diff --git a/src/wireviz/wv_gv_html.py b/src/wireviz/wv_gv_html.py index e35084f..ea52c10 100644 --- a/src/wireviz/wv_gv_html.py +++ b/src/wireviz/wv_gv_html.py @@ -8,51 +8,66 @@ from wireviz.wv_colors import translate_color from wireviz.wv_helper import remove_links -def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = '') -> str: +def nested_html_table( + rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = "" +) -> str: # input: list, each item may be scalar or list # output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar # purpose: create the appearance of one table, where cell widths are independent between rows # attributes in any leading inside a list are injected into to the preceeding tag html = [] - html.append(f'') + html.append( + f'
' + ) num_rows = 0 for row in rows: if isinstance(row, List): if len(row) > 0 and any(row): - html.append(' ") num_rows = num_rows + 1 elif row is not None: - html.append(' ') + html.append(" ") num_rows = num_rows + 1 if num_rows == 0: # empty table - html.append('') # generate empty cell to avoid GraphViz errors - html.append('
') - html.append(' ') + html.append(" ') + html.append( + f' '.replace(">
") + html.append( + ' ' + ) for cell in row: if cell is not None: # Inject attributes to the preceeding '.replace('>
tag where needed - html.append(f' {cell}
') - html.append('
{cell}
") + html.append("
') - html.append(f' {row}') - html.append('
") + html.append(f" {row}") + html.append("
') + html.append( + "" + ) # generate empty cell to avoid GraphViz errors + html.append("") return html + def html_bgcolor_attr(color: Color) -> str: """Return attributes for bgcolor or '' if no color.""" - return f' bgcolor="{translate_color(color, "HEX")}"' if color else '' + return f' bgcolor="{translate_color(color, "HEX")}"' if color else "" -def html_bgcolor(color: Color, _extra_attr: str = '') -> str: + +def html_bgcolor(color: Color, _extra_attr: str = "") -> str: """Return attributes prefix for bgcolor or '' if no color.""" - return f'' if color else '' + return f"" if color else "" + def html_colorbar(color: Color) -> str: """Return attributes prefix for bgcolor and minimum width or None if no color.""" return html_bgcolor(color, ' width="4"') if color else None + def html_image(image): from wireviz.DataClasses import Image + if not image: return None # The leading attributes belong to the preceeding tag. See where used below. @@ -60,25 +75,38 @@ def html_image(image): if image.fixedsize: # Close the preceeding tag and enclose the image cell in a table without # borders to avoid narrow borders when the fixed width < the node width. - html = f'''> + html = f""">
- ''' - return f'''{html_line_breaks(image.caption)}' - if image and image.caption else None) + + return ( + f'{html_line_breaks(image.caption)}' + if image and image.caption + else None + ) + def html_size_attr(image): from wireviz.DataClasses import Image # Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object - return ((f' width="{image.width}"' if image.width else '') - + (f' height="{image.height}"' if image.height else '') - + ( ' fixedsize="true"' if image.fixedsize else '')) if image else '' + return ( + ( + (f' width="{image.width}"' if image.width else "") + + (f' height="{image.height}"' if image.height else "") + + (' fixedsize="true"' if image.fixedsize else "") + ) + if image + else "" + ) + def html_line_breaks(inp): - return remove_links(inp).replace('\n', '
') if isinstance(inp, str) else inp + return remove_links(inp).replace("\n", "
") if isinstance(inp, str) else inp diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 844556d..7cbe125 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -5,31 +5,33 @@ from pathlib import Path from typing import Dict, List awg_equiv_table = { - '0.09': '28', - '0.14': '26', - '0.25': '24', - '0.34': '22', - '0.5': '21', - '0.75': '20', - '1': '18', - '1.5': '16', - '2.5': '14', - '4': '12', - '6': '10', - '10': '8', - '16': '6', - '25': '4', - '35': '2', - '50': '1', + "0.09": "28", + "0.14": "26", + "0.25": "24", + "0.34": "22", + "0.5": "21", + "0.75": "20", + "1": "18", + "1.5": "16", + "2.5": "14", + "4": "12", + "6": "10", + "10": "8", + "16": "6", + "25": "4", + "35": "2", + "50": "1", } -mm2_equiv_table = {v:k for k,v in awg_equiv_table.items()} +mm2_equiv_table = {v: k for k, v in awg_equiv_table.items()} + def awg_equiv(mm2): - return awg_equiv_table.get(str(mm2), 'Unknown') + return awg_equiv_table.get(str(mm2), "Unknown") + def mm2_equiv(awg): - return mm2_equiv_table.get(str(awg), 'Unknown') + return mm2_equiv_table.get(str(awg), "Unknown") def expand(yaml_data): @@ -42,8 +44,8 @@ def expand(yaml_data): yaml_data = [yaml_data] for e in yaml_data: e = str(e) - if '-' in e: - a, b = e.split('-', 1) + if "-" in e: + a, b = e.split("-", 1) try: a = int(a) b = int(b) @@ -56,7 +58,9 @@ def expand(yaml_data): else: # a == b output.append(a) # range of length 1 except: - output.append(e) # '-' was not a delimiter between two ints, pass e through unchanged + output.append( + e + ) # '-' was not a delimiter between two ints, pass e through unchanged else: try: x = int(e) # single int @@ -81,36 +85,46 @@ def int2tuple(inp): def flatten2d(inp): - return [[str(item) if not isinstance(item, List) else ', '.join(item) for item in row] for row in inp] + return [ + [str(item) if not isinstance(item, List) else ", ".join(item) for item in row] + for row in inp + ] def tuplelist2tsv(inp, header=None): - output = '' + output = "" if header is not None: inp.insert(0, header) inp = flatten2d(inp) for row in inp: - output = output + '\t'.join(str(remove_links(item)) for item in row) + '\n' + output = output + "\t".join(str(remove_links(item)) for item in row) + "\n" return output def remove_links(inp): - return re.sub(r'<[aA] [^>]*>([^<]*)', r'\1', inp) if isinstance(inp, str) else inp + return ( + re.sub(r"<[aA] [^>]*>([^<]*)", r"\1", inp) + if isinstance(inp, str) + else inp + ) def clean_whitespace(inp): - return ' '.join(inp.split()).replace(' ,', ',') if isinstance(inp, str) else inp + return " ".join(inp.split()).replace(" ,", ",") if isinstance(inp, str) else inp def open_file_read(filename): # TODO: Intelligently determine encoding - return open(filename, 'r', encoding='UTF-8') + return open(filename, "r", encoding="UTF-8") + def open_file_write(filename): - return open(filename, 'w', encoding='UTF-8') + return open(filename, "w", encoding="UTF-8") + def open_file_append(filename): - return open(filename, 'a', encoding='UTF-8') + return open(filename, "a", encoding="UTF-8") + def is_arrow(inp): """ @@ -122,19 +136,23 @@ def is_arrow(inp): <==, ==, ==>, <=> """ # regex by @shiraneyo - return bool(re.match(r"^\s*(?P-+|=+)(?P>?)\s*$", inp)) + return bool( + re.match(r"^\s*(?P-+|=+)(?P>?)\s*$", inp) + ) + def aspect_ratio(image_src): try: from PIL import Image + image = Image.open(image_src) if image.width > 0 and image.height > 0: return image.width / image.height - print(f'aspect_ratio(): Invalid image size {image.width} x {image.height}') + print(f"aspect_ratio(): Invalid image size {image.width} x {image.height}") # ModuleNotFoundError and FileNotFoundError are the most expected, but all are handled equally. except Exception as error: - print(f'aspect_ratio(): {type(error).__name__}: {error}') - return 1 # Assume 1:1 when unable to read actual image size + print(f"aspect_ratio(): {type(error).__name__}: {error}") + return 1 # Assume 1:1 when unable to read actual image size def smart_file_resolve(filename: str, possible_paths: (str, List[str])) -> Path: @@ -145,13 +163,17 @@ def smart_file_resolve(filename: str, possible_paths: (str, List[str])) -> Path: if filename.exists(): return filename else: - raise Exception(f'{filename} does not exist.') + raise Exception(f"{filename} does not exist.") else: # search all possible paths in decreasing order of precedence - possible_paths = [Path(path).resolve() for path in possible_paths if path is not None] + possible_paths = [ + Path(path).resolve() for path in possible_paths if path is not None + ] for possible_path in possible_paths: resolved_path = (possible_path / filename).resolve() if resolved_path.exists(): return resolved_path else: - raise Exception(f'{filename} was not found in any of the following locations: \n' + - '\n'.join([str(x) for x in possible_paths])) + raise Exception( + f"{filename} was not found in any of the following locations: \n" + + "\n".join([str(x) for x in possible_paths]) + ) diff --git a/src/wireviz/wv_html.py b/src/wireviz/wv_html.py index 9a9a968..6389503 100644 --- a/src/wireviz/wv_html.py +++ b/src/wireviz/wv_html.py @@ -15,83 +15,106 @@ from wireviz.wv_helper import ( ) -def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], metadata: Metadata, options: Options): +def generate_html_output( + filename: Union[str, Path], + bom_list: List[List[str]], + metadata: Metadata, + options: Options, +): # load HTML template - templatename = metadata.get('template',{}).get('name') + templatename = metadata.get("template", {}).get("name") if templatename: # if relative path to template was provided, check directory of YAML file first, fall back to built-in template directory - templatefile = smart_file_resolve(f'{templatename}.html', [Path(filename).parent, Path(__file__).parent / 'templates']) + templatefile = smart_file_resolve( + f"{templatename}.html", + [Path(filename).parent, Path(__file__).parent / "templates"], + ) else: # fall back to built-in simple template if no template was provided - templatefile = Path(__file__).parent / 'templates/simple.html' + templatefile = Path(__file__).parent / "templates/simple.html" with open_file_read(templatefile) as file: html = file.read() # embed SVG diagram - with open_file_read(f'{filename}.svg') as file: + with open_file_read(f"{filename}.svg") as file: svgdata = re.sub( - '^<[?]xml [^?>]*[?]>[^<]*]*>', - '', - file.read(), 1) + "^<[?]xml [^?>]*[?]>[^<]*]*>", + "", + file.read(), + 1, + ) # generate BOM table bom = flatten2d(bom_list) # generate BOM header (may be at the top or bottom of the table) - bom_header_html = ' \n' + bom_header_html = " \n" for item in bom[0]: - th_class = f'bom_col_{item.lower()}' + th_class = f"bom_col_{item.lower()}" bom_header_html = f'{bom_header_html} {item}\n' - bom_header_html = f'{bom_header_html} \n' + bom_header_html = f"{bom_header_html} \n" # generate BOM contents bom_contents = [] for row in bom[1:]: - row_html = ' \n' + row_html = " \n" for i, item in enumerate(row): - td_class = f'bom_col_{bom[0][i].lower()}' + td_class = f"bom_col_{bom[0][i].lower()}" row_html = f'{row_html} {item}\n' - row_html = f'{row_html} \n' + row_html = f"{row_html} \n" bom_contents.append(row_html) - bom_html = '\n' + bom_header_html + ''.join(bom_contents) + '
\n' - bom_html_reversed = '\n' + ''.join(list(reversed(bom_contents))) + bom_header_html + '
\n' + bom_html = ( + '\n' + bom_header_html + "".join(bom_contents) + "
\n" + ) + bom_html_reversed = ( + '\n' + + "".join(list(reversed(bom_contents))) + + bom_header_html + + "
\n" + ) # prepare simple replacements replacements = { - '': f'{APP_NAME} {__version__} - {APP_URL}', - '': options.fontname, - '': wv_colors.translate_color(options.bgcolor, "hex"), - '': svgdata, - '': bom_html, - '': bom_html_reversed, - '': '1', # TODO: handle multi-page documents - '': '1', # TODO: handle multi-page documents + "": f"{APP_NAME} {__version__} - {APP_URL}", + "": options.fontname, + "": wv_colors.translate_color(options.bgcolor, "hex"), + "": svgdata, + "": bom_html, + "": bom_html_reversed, + "": "1", # TODO: handle multi-page documents + "": "1", # TODO: handle multi-page documents } # prepare metadata replacements if metadata: for item, contents in metadata.items(): if isinstance(contents, (str, int, float)): - replacements[f''] = html_line_breaks(str(contents)) + replacements[f""] = html_line_breaks(str(contents)) elif isinstance(contents, Dict): # useful for authors, revisions for index, (category, entry) in enumerate(contents.items()): if isinstance(entry, Dict): - replacements[f''] = str(category) + replacements[f""] = str(category) for entry_key, entry_value in entry.items(): - replacements[f''] = html_line_breaks(str(entry_value)) + replacements[ + f"" + ] = html_line_breaks(str(entry_value)) - replacements['"sheetsize_default"'] = '"{}"'.format(metadata.get('template',{}).get('sheetsize', '')) # include quotes so no replacement happens within