Apply black

This commit is contained in:
Daniel Rojas 2021-10-15 17:11:11 +02:00
parent 344615483a
commit f92985a61c
12 changed files with 1474 additions and 744 deletions

View File

@ -7,40 +7,39 @@ from setuptools import find_packages, setup
from src.wireviz import APP_URL, CMD_NAME, __version__ 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( setup(
name=CMD_NAME, name=CMD_NAME,
version=__version__, version=__version__,
author='Daniel Rojas', author="Daniel Rojas",
#author_email='', # author_email='',
description='Easily document cables and wiring harnesses', description="Easily document cables and wiring harnesses",
long_description=open(README_PATH).read(), long_description=open(README_PATH).read(),
long_description_content_type='text/markdown', long_description_content_type="text/markdown",
install_requires=[ install_requires=[
'click', "click",
'pyyaml', "pyyaml",
'pillow', "pillow",
'graphviz', "graphviz",
], ],
license='GPLv3', license="GPLv3",
keywords='cable connector hardware harness wiring wiring-diagram wiring-harness', keywords="cable connector hardware harness wiring wiring-diagram wiring-harness",
url=APP_URL, url=APP_URL,
package_dir={'': 'src'}, package_dir={"": "src"},
packages=find_packages('src'), packages=find_packages("src"),
entry_points={ entry_points={
'console_scripts': [ "console_scripts": [
'wireviz=wireviz.wv_cli:wireviz', "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)',
], ],
},
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)",
],
) )

View File

@ -9,44 +9,55 @@ from wireviz.wv_colors import COLOR_CODES, Color, ColorMode, Colors, ColorScheme
from wireviz.wv_helper import aspect_ratio, int2tuple 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 # 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 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 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 MultilineHypertext = (
Designator = PlainText # Case insensitive unique name of connector or cable 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 # Literal type aliases below are commented to avoid requiring python 3.8
ConnectorMultiplier = PlainText # = Literal['pincount', 'populated'] ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
CableMultiplier = PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length'] CableMultiplier = (
ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both'] PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length']
)
ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both']
# Type combinations # Type combinations
Pin = Union[int, PlainText] # Pin identifier Pin = Union[int, PlainText] # Pin identifier
PinIndex = int # Zero-based pin index PinIndex = int # Zero-based pin index
Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield 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 NoneOrMorePins = Union[
NoneOrMorePinIndices = Union[PinIndex, Tuple[PinIndex, ...], None] # None, one, or a tuple of zero-based pin indices Pin, Tuple[Pin, ...], None
OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires ] # 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. # Metadata can contain whatever is needed by the HTML generation/template.
MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...] MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...]
class Side(Enum): class Side(Enum):
LEFT = auto() LEFT = auto()
RIGHT = auto() RIGHT = auto()
class Metadata(dict): class Metadata(dict):
pass pass
@dataclass @dataclass
class Options: class Options:
fontname: PlainText = 'arial' fontname: PlainText = "arial"
bgcolor: Color = 'WH' bgcolor: Color = "WH"
bgcolor_node: Optional[Color] = 'WH' bgcolor_node: Optional[Color] = "WH"
bgcolor_connector: Optional[Color] = None bgcolor_connector: Optional[Color] = None
bgcolor_cable: Optional[Color] = None bgcolor_cable: Optional[Color] = None
bgcolor_bundle: Optional[Color] = None bgcolor_bundle: Optional[Color] = None
color_mode: ColorMode = 'SHORT' color_mode: ColorMode = "SHORT"
mini_bom_mode: bool = True mini_bom_mode: bool = True
def __post_init__(self): def __post_init__(self):
@ -87,9 +98,13 @@ class Image:
self.fixedsize = (self.width or self.height) and self.scale is None self.fixedsize = (self.width or self.height) and self.scale is None
if self.scale is None: if self.scale is None:
self.scale = "false" if not self.width and not self.height \ self.scale = (
else "both" if self.width and self.height \ "false"
else "true" # When only one dimension is specified. 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 self.fixedsize:
# If only one dimension is specified, compute the other # If only one dimension is specified, compute the other
@ -118,7 +133,9 @@ class AdditionalComponent:
@property @property
def description(self) -> str: 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 @dataclass
@ -158,36 +175,44 @@ class Connector:
self.ports_right = False self.ports_right = False
self.visible_pins = {} self.visible_pins = {}
if self.style == 'simple': if self.style == "simple":
if self.pincount and self.pincount > 1: 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 self.pincount = 1
if not self.pincount: 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: 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 # create default list for pins (sequential) if not specified
if not self.pins: if not self.pins:
self.pins = list(range(1, self.pincount + 1)) self.pins = list(range(1, self.pincount + 1))
if len(self.pins) != len(set(self.pins)): 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: if self.show_name is None:
# hide designators for simple and for auto-generated connectors by default # 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: 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: for loop in self.loops:
# TODO: check that pins to connect actually exist # 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: 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 # TODO: include properties of wire used to create the loop
if len(loop) != 2: 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): for i, item in enumerate(self.additional_components):
if isinstance(item, dict): if isinstance(item, dict):
@ -203,12 +228,14 @@ class Connector:
def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int: def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int:
if not qty_multiplier: if not qty_multiplier:
return 1 return 1
elif qty_multiplier == 'pincount': elif qty_multiplier == "pincount":
return self.pincount return self.pincount
elif qty_multiplier == 'populated': elif qty_multiplier == "populated":
return sum(self.visible_pins.values()) return sum(self.visible_pins.values())
else: else:
raise ValueError(f'invalid qty multiplier parameter for connector {qty_multiplier}') raise ValueError(
f"invalid qty multiplier parameter for connector {qty_multiplier}"
)
@dataclass @dataclass
@ -249,65 +276,79 @@ class Cable:
if isinstance(self.gauge, str): # gauge and unit specified if isinstance(self.gauge, str): # gauge and unit specified
try: try:
g, u = self.gauge.split(' ') g, u = self.gauge.split(" ")
except Exception: 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 self.gauge = g
if self.gauge_unit is not None: 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}') print(
if u.upper() == 'AWG': 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() self.gauge_unit = u.upper()
else: 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 elif self.gauge is not None: # gauge specified, assume mm2
if self.gauge_unit is None: if self.gauge_unit is None:
self.gauge_unit = 'mm\u00B2' self.gauge_unit = "mm\u00B2"
else: else:
pass # gauge not specified pass # gauge not specified
if isinstance(self.length, str): # length and unit specified if isinstance(self.length, str): # length and unit specified
try: try:
L, u = self.length.split(' ') L, u = self.length.split(" ")
L = float(L) L = float(L)
except Exception: 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 self.length = L
if self.length_unit is not None: 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 self.length_unit = u
elif not any(isinstance(self.length, t) for t in [int, float]): 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: elif self.length_unit is None:
self.length_unit = 'm' self.length_unit = "m"
self.connections = [] self.connections = []
if self.wirecount: # number of wires explicitly defined if self.wirecount: # number of wires explicitly defined
if self.colors: # use custom color palette (partly or looped if needed) if self.colors: # use custom color palette (partly or looped if needed)
pass 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: 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] self.colors = COLOR_CODES[self.color_code]
else: # no colors defined, add dummy colors 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 # make color code loop around if more wires than colors
if self.wirecount > len(self.colors): if self.wirecount > len(self.colors):
m = self.wirecount // len(self.colors) + 1 m = self.wirecount // len(self.colors) + 1
self.colors = self.colors * int(m) self.colors = self.colors * int(m)
# cut off excess after looping # 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 else: # wirecount implicit in length of color list
if not self.colors: 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) self.wirecount = len(self.colors)
if self.wirelabels: if self.wirelabels:
if self.shield and 's' in 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.') 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. # 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]: for idfield in [self.manufacturer, self.mpn, self.supplier, self.spn, self.pn]:
@ -315,44 +356,58 @@ class Cable:
if self.category == "bundle": if self.category == "bundle":
# check the length # check the length
if len(idfield) != self.wirecount: if len(idfield) != self.wirecount:
raise Exception('lists of part data must match wirecount') raise Exception("lists of part data must match wirecount")
else: 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: 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: 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): for i, item in enumerate(self.additional_components):
if isinstance(item, dict): if isinstance(item, dict):
self.additional_components[i] = AdditionalComponent(**item) self.additional_components[i] = AdditionalComponent(**item)
# The *_pin arguments accept a tuple, but it seems not in use with the current code. # 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, def connect(
to_name: Optional[Designator], to_pin: NoneOrMorePinIndices) -> None: self,
from_name: Optional[Designator],
from_pin: NoneOrMorePinIndices,
via_wire: OneOrMoreWires,
to_name: Optional[Designator],
to_pin: NoneOrMorePinIndices,
) -> None:
from_pin = int2tuple(from_pin) from_pin = int2tuple(from_pin)
via_wire = int2tuple(via_wire) via_wire = int2tuple(via_wire)
to_pin = int2tuple(to_pin) to_pin = int2tuple(to_pin)
if len(from_pin) != len(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): 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: def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float:
if not qty_multiplier: if not qty_multiplier:
return 1 return 1
elif qty_multiplier == 'wirecount': elif qty_multiplier == "wirecount":
return self.wirecount return self.wirecount
elif qty_multiplier == 'terminations': elif qty_multiplier == "terminations":
return len(self.connections) return len(self.connections)
elif qty_multiplier == 'length': elif qty_multiplier == "length":
return self.length return self.length
elif qty_multiplier == 'total_length': elif qty_multiplier == "total_length":
return self.length * self.wirecount return self.length * self.wirecount
else: else:
raise ValueError(f'invalid qty multiplier parameter for cable {qty_multiplier}') raise ValueError(
f"invalid qty multiplier parameter for cable {qty_multiplier}"
)
@dataclass @dataclass
@ -363,6 +418,7 @@ class Connection:
to_name: Optional[Designator] to_name: Optional[Designator]
to_pin: Optional[Pin] to_pin: Optional[Pin]
@dataclass @dataclass
class MatePin: class MatePin:
from_name: Designator from_name: Designator
@ -371,6 +427,7 @@ class MatePin:
to_pin: Pin to_pin: Pin
shape: str shape: str
@dataclass @dataclass
class MateComponent: class MateComponent:
from_name: Designator from_name: Designator

View File

@ -18,7 +18,7 @@ from wireviz.DataClasses import (
Metadata, Metadata,
Options, Options,
Tweak, Tweak,
Side, Side,
) )
from wireviz.wv_bom import ( from wireviz.wv_bom import (
HEADER_MPN, HEADER_MPN,
@ -83,7 +83,15 @@ class Harness:
def add_bom_item(self, item: dict) -> None: def add_bom_item(self, item: dict) -> None:
self.additional_bom_items.append(item) 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 # check from and to connectors
for (name, pin) in zip([from_name, to_name], [from_pin, to_pin]): for (name, pin) in zip([from_name, to_name], [from_pin, to_pin]):
if name is not None and name in self.connectors: if name is not None and name in self.connectors:
@ -91,19 +99,21 @@ class Harness:
# check if provided name is ambiguous # check if provided name is ambiguous
if pin in connector.pins and pin in connector.pinlabels: if pin in connector.pins and pin in connector.pinlabels:
if connector.pins.index(pin) != connector.pinlabels.index(pin): 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? # TODO: Maybe issue a warning if present in both lists but referencing the same pin?
if pin in connector.pinlabels: if pin in connector.pinlabels:
if connector.pinlabels.count(pin) > 1: 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) 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: if name == from_name:
from_pin = pin from_pin = pin
if name == to_name: if name == to_name:
to_pin = pin to_pin = pin
if not pin in connector.pins: if not pin in connector.pins:
raise Exception(f'{name}:{pin} not found.') raise Exception(f"{name}:{pin} not found.")
# check via cable # check via cable
if via_name in self.cables: if via_name in self.cables:
@ -111,16 +121,26 @@ class Harness:
# check if provided name is ambiguous # check if provided name is ambiguous
if via_wire in cable.colors and via_wire in cable.wirelabels: if via_wire in cable.colors and via_wire in cable.wirelabels:
if cable.colors.index(via_wire) != cable.wirelabels.index(via_wire): 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? # TODO: Maybe issue a warning if present in both lists but referencing the same wire?
if via_wire in cable.colors: if via_wire in cable.colors:
if cable.colors.count(via_wire) > 1: if cable.colors.count(via_wire) > 1:
raise Exception(f'{via_name}:{via_wire} is used for more than one wire.') raise Exception(
via_wire = cable.colors.index(via_wire) + 1 # list index starts at 0, wire IDs start at 1 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: elif via_wire in cable.wirelabels:
if cable.wirelabels.count(via_wire) > 1: if cable.wirelabels.count(via_wire) > 1:
raise Exception(f'{via_name}:{via_wire} is used for more than one wire.') raise Exception(
via_wire = cable.wirelabels.index(via_wire) + 1 # list index starts at 0, wire IDs start at 1 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 # perform the actual connection
self.cables[via_name].connect(from_name, from_pin, via_wire, to_name, to_pin) 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: if to_name in self.connectors:
self.connectors[to_name].activate_pin(to_pin, Side.LEFT) self.connectors[to_name].activate_pin(to_pin, Side.LEFT)
def create_graph(self) -> Graph: def create_graph(self) -> Graph:
dot = Graph() dot = Graph()
dot.body.append(f'// Graph generated by {APP_NAME} {__version__}') dot.body.append(f"// Graph generated by {APP_NAME} {__version__}")
dot.body.append(f'// {APP_URL}') dot.body.append(f"// {APP_URL}")
dot.attr('graph', rankdir='LR', dot.attr(
ranksep='2', "graph",
bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"), rankdir="LR",
nodesep='0.33', ranksep="2",
fontname=self.options.fontname) bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
dot.attr('node', nodesep="0.33",
shape='none', fontname=self.options.fontname,
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label. )
style='filled', dot.attr(
fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"), "node",
fontname=self.options.fontname) shape="none",
dot.attr('edge', style='bold', width="0",
fontname=self.options.fontname) 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(): for connector in self.connectors.values():
@ -156,325 +181,496 @@ class Harness:
html = [] html = []
rows = [[f'{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}' rows = [
if connector.show_name else None], [
[pn_info_string(HEADER_PN, None, remove_links(connector.pn)), f"{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}"
html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)), if connector.show_name
html_line_breaks(pn_info_string(HEADER_SPN, connector.supplier, connector.spn))], else None
[html_line_breaks(connector.type), ],
html_line_breaks(connector.subtype), [
f'{connector.pincount}-pin' if connector.show_pincount else None, pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
translate_color(connector.color, self.options.color_mode) if connector.color else None, html_line_breaks(
html_colorbar(connector.color)], pn_info_string(
'<!-- connector table -->' if connector.style != 'simple' else None, HEADER_MPN, connector.manufacturer, connector.mpn
[html_image(connector.image)], )
[html_caption(connector.image)]] ),
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),
],
"<!-- connector table -->" if connector.style != "simple" else None,
[html_image(connector.image)],
[html_caption(connector.image)],
]
rows.extend(get_additional_component_table(self, connector)) rows.extend(get_additional_component_table(self, connector))
rows.append([html_line_breaks(connector.notes)]) rows.append([html_line_breaks(connector.notes)])
html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor))) html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor)))
if connector.style != 'simple': if connector.style != "simple":
pinhtml = [] pinhtml = []
pinhtml.append('<table border="0" cellspacing="0" cellpadding="3" cellborder="1">') pinhtml.append(
'<table border="0" cellspacing="0" cellpadding="3" cellborder="1">'
)
for pinindex, (pinname, pinlabel, pincolor) in enumerate(zip_longest(connector.pins, connector.pinlabels, connector.pincolors)): for pinindex, (pinname, pinlabel, pincolor) in enumerate(
if connector.hide_disconnected_pins and not connector.visible_pins.get(pinname, False): zip_longest(
connector.pins, connector.pinlabels, connector.pincolors
)
):
if (
connector.hide_disconnected_pins
and not connector.visible_pins.get(pinname, False)
):
continue continue
pinhtml.append(' <tr>') pinhtml.append(" <tr>")
if connector.ports_left: if connector.ports_left:
pinhtml.append(f' <td port="p{pinindex+1}l">{pinname}</td>') pinhtml.append(f' <td port="p{pinindex+1}l">{pinname}</td>')
if pinlabel: if pinlabel:
pinhtml.append(f' <td>{pinlabel}</td>') pinhtml.append(f" <td>{pinlabel}</td>")
if connector.pincolors: if connector.pincolors:
if pincolor in wv_colors._color_hex.keys(): if pincolor in wv_colors._color_hex.keys():
pinhtml.append(f' <td sides="tbl">{translate_color(pincolor, self.options.color_mode)}</td>') pinhtml.append(
pinhtml.append( ' <td sides="tbr">') f' <td sides="tbl">{translate_color(pincolor, self.options.color_mode)}</td>'
pinhtml.append( ' <table border="0" cellborder="1"><tr>') )
pinhtml.append(f' <td bgcolor="{wv_colors.translate_color(pincolor, "HEX")}" width="8" height="8" fixedsize="true"></td>') pinhtml.append(' <td sides="tbr">')
pinhtml.append( ' </tr></table>') pinhtml.append(' <table border="0" cellborder="1"><tr>')
pinhtml.append( ' </td>') pinhtml.append(
f' <td bgcolor="{wv_colors.translate_color(pincolor, "HEX")}" width="8" height="8" fixedsize="true"></td>'
)
pinhtml.append(" </tr></table>")
pinhtml.append(" </td>")
else: else:
pinhtml.append( ' <td colspan="2"></td>') pinhtml.append(' <td colspan="2"></td>')
if connector.ports_right: if connector.ports_right:
pinhtml.append(f' <td port="p{pinindex+1}r">{pinname}</td>') pinhtml.append(f' <td port="p{pinindex+1}r">{pinname}</td>')
pinhtml.append(' </tr>') pinhtml.append(" </tr>")
pinhtml.append(' </table>') pinhtml.append(" </table>")
html = [row.replace('<!-- connector table -->', '\n'.join(pinhtml)) for row in html] html = [
row.replace("<!-- connector table -->", "\n".join(pinhtml))
for row in html
]
html = '\n'.join(html) html = "\n".join(html)
dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled', dot.node(
fillcolor=translate_color(self.options.bgcolor_connector, "HEX")) connector.name,
label=f"<\n{html}\n>",
shape="box",
style="filled",
fillcolor=translate_color(self.options.bgcolor_connector, "HEX"),
)
if len(connector.loops) > 0: if len(connector.loops) > 0:
dot.attr('edge', color='#000000:#ffffff:#000000') dot.attr("edge", color="#000000:#ffffff:#000000")
if connector.ports_left: if connector.ports_left:
loop_side = 'l' loop_side = "l"
loop_dir = 'w' loop_dir = "w"
elif connector.ports_right: elif connector.ports_right:
loop_side = 'r' loop_side = "r"
loop_dir = 'e' loop_dir = "e"
else: else:
raise Exception('No side for loops') raise Exception("No side for loops")
for loop in connector.loops: for loop in connector.loops:
dot.edge(f'{connector.name}:p{loop[0]}{loop_side}:{loop_dir}', dot.edge(
f'{connector.name}:p{loop[1]}{loop_side}:{loop_dir}') 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; # 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 # 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(): for cable in self.cables.values():
html = [] html = []
awg_fmt = '' awg_fmt = ""
if cable.show_equiv: if cable.show_equiv:
# Only convert units we actually know about, i.e. currently # Only convert units we actually know about, i.e. currently
# mm2 and awg --- other units _are_ technically allowed, # mm2 and awg --- other units _are_ technically allowed,
# and passed through as-is. # and passed through as-is.
if cable.gauge_unit =='mm\u00B2': if cable.gauge_unit == "mm\u00B2":
awg_fmt = f' ({awg_equiv(cable.gauge)} AWG)' awg_fmt = f" ({awg_equiv(cable.gauge)} AWG)"
elif cable.gauge_unit.upper() == 'AWG': elif cable.gauge_unit.upper() == "AWG":
awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' awg_fmt = f" ({mm2_equiv(cable.gauge)} mm\u00B2)"
rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}' rows = [
if cable.show_name else None], [
[pn_info_string(HEADER_PN, None, f"{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}"
remove_links(cable.pn)) if not isinstance(cable.pn, list) else None, if cable.show_name
html_line_breaks(pn_info_string(HEADER_MPN, else None
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, pn_info_string(HEADER_PN, None, remove_links(cable.pn))
cable.supplier if not isinstance(cable.supplier, list) else None, if not isinstance(cable.pn, list)
cable.spn if not isinstance(cable.spn, list) else None))], else None,
[html_line_breaks(cable.type), html_line_breaks(
f'{cable.wirecount}x' if cable.show_wirecount else None, pn_info_string(
f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None, HEADER_MPN,
'+ S' if cable.shield else None, cable.manufacturer
f'{cable.length} {cable.length_unit}' if cable.length > 0 else None, if not isinstance(cable.manufacturer, list)
translate_color(cable.color, self.options.color_mode) if cable.color else None, else None,
html_colorbar(cable.color)], cable.mpn if not isinstance(cable.mpn, list) else None,
'<!-- wire table -->', )
[html_image(cable.image)], ),
[html_caption(cable.image)]] 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),
],
"<!-- wire table -->",
[html_image(cable.image)],
[html_caption(cable.image)],
]
rows.extend(get_additional_component_table(self, cable)) rows.extend(get_additional_component_table(self, cable))
rows.append([html_line_breaks(cable.notes)]) rows.append([html_line_breaks(cable.notes)])
html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor))) html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor)))
wirehtml = [] wirehtml = []
wirehtml.append('<table border="0" cellspacing="0" cellborder="0">') # conductor table wirehtml.append(
wirehtml.append(' <tr><td>&nbsp;</td></tr>') '<table border="0" cellspacing="0" cellborder="0">'
) # conductor table
wirehtml.append(" <tr><td>&nbsp;</td></tr>")
for i, (connection_color, wirelabel) in enumerate(zip_longest(cable.colors, cable.wirelabels), 1): for i, (connection_color, wirelabel) in enumerate(
wirehtml.append(' <tr>') zip_longest(cable.colors, cable.wirelabels), 1
wirehtml.append(f' <td><!-- {i}_in --></td>') ):
wirehtml.append(f' <td>') wirehtml.append(" <tr>")
wirehtml.append(f" <td><!-- {i}_in --></td>")
wirehtml.append(f" <td>")
wireinfo = [] wireinfo = []
if cable.show_wirenumbers: if cable.show_wirenumbers:
wireinfo.append(str(i)) 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: if colorstr:
wireinfo.append(colorstr) wireinfo.append(colorstr)
if cable.wirelabels: 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' {":".join(wireinfo)}')
wirehtml.append(f' </td>') wirehtml.append(f" </td>")
wirehtml.append(f' <td><!-- {i}_out --></td>') wirehtml.append(f" <td><!-- {i}_out --></td>")
wirehtml.append(' </tr>') wirehtml.append(" </tr>")
bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000'] bgcolors = (
wirehtml.append(f' <tr>') ["#000000"] + get_color_hex(connection_color, pad=pad) + ["#000000"]
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">') )
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">') wirehtml.append(f" <tr>")
for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors wirehtml.append(
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>') f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">'
wirehtml.append(' </table>') )
wirehtml.append(' </td>') wirehtml.append(
wirehtml.append(' </tr>') ' <table cellspacing="0" cellborder="0" border="0">'
if cable.category == 'bundle': # for bundles individual wires can have part information )
for j, bgcolor in enumerate(
bgcolors[::-1]
): # Reverse to match the curved wires when more than 2 colors
wirehtml.append(
f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>'
)
wirehtml.append(" </table>")
wirehtml.append(" </td>")
wirehtml.append(" </tr>")
if (
cable.category == "bundle"
): # for bundles individual wires can have part information
# create a list of wire parameters # create a list of wire parameters
wireidentification = [] wireidentification = []
if isinstance(cable.pn, list): if isinstance(cable.pn, list):
wireidentification.append(pn_info_string(HEADER_PN, None, remove_links(cable.pn[i - 1]))) wireidentification.append(
manufacturer_info = pn_info_string(HEADER_MPN, pn_info_string(
cable.manufacturer[i - 1] if isinstance(cable.manufacturer, list) else None, HEADER_PN, None, remove_links(cable.pn[i - 1])
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, manufacturer_info = pn_info_string(
cable.spn[i - 1] if isinstance(cable.spn, list) else None) 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: if manufacturer_info:
wireidentification.append(html_line_breaks(manufacturer_info)) wireidentification.append(html_line_breaks(manufacturer_info))
if supplier_info: if supplier_info:
wireidentification.append(html_line_breaks(supplier_info)) wireidentification.append(html_line_breaks(supplier_info))
# print parameters into a table row under the wire # print parameters into a table row under the wire
if len(wireidentification) > 0 : if len(wireidentification) > 0:
wirehtml.append(' <tr><td colspan="3">') wirehtml.append(' <tr><td colspan="3">')
wirehtml.append(' <table border="0" cellspacing="0" cellborder="0"><tr>') wirehtml.append(
' <table border="0" cellspacing="0" cellborder="0"><tr>'
)
for attrib in wireidentification: for attrib in wireidentification:
wirehtml.append(f' <td>{attrib}</td>') wirehtml.append(f" <td>{attrib}</td>")
wirehtml.append(' </tr></table>') wirehtml.append(" </tr></table>")
wirehtml.append(' </td></tr>') wirehtml.append(" </td></tr>")
if cable.shield: if cable.shield:
wirehtml.append(' <tr><td>&nbsp;</td></tr>') # spacer wirehtml.append(" <tr><td>&nbsp;</td></tr>") # spacer
wirehtml.append(' <tr>') wirehtml.append(" <tr>")
wirehtml.append(' <td><!-- s_in --></td>') wirehtml.append(" <td><!-- s_in --></td>")
wirehtml.append(' <td>Shield</td>') wirehtml.append(" <td>Shield</td>")
wirehtml.append(' <td><!-- s_out --></td>') wirehtml.append(" <td><!-- s_out --></td>")
wirehtml.append(' </tr>') wirehtml.append(" </tr>")
if isinstance(cable.shield, str): if isinstance(cable.shield, str):
# shield is shown with specified color and black borders # shield is shown with specified color and black borders
shield_color_hex = wv_colors.get_color_hex(cable.shield)[0] 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: else:
# shield is shown as a thin black wire # shield is shown as a thin black wire
attributes = f'height="2" bgcolor="#000000" border="0"' attributes = f'height="2" bgcolor="#000000" border="0"'
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>') wirehtml.append(
f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>'
)
wirehtml.append(' <tr><td>&nbsp;</td></tr>') wirehtml.append(" <tr><td>&nbsp;</td></tr>")
wirehtml.append(' </table>') wirehtml.append(" </table>")
html = [row.replace('<!-- wire table -->', '\n'.join(wirehtml)) for row in html] html = [
row.replace("<!-- wire table -->", "\n".join(wirehtml)) for row in html
]
# connections # connections
for connection in cable.connections: for connection in cable.connections:
if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield if isinstance(
dot.attr('edge', color=':'.join(['#000000'] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + ['#000000'])) 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 else: # it's a shield connection
# shield is shown with specified color and black borders, or as a thin black wire otherwise # 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 if connection.from_pin is not None: # connect to left
from_connector = self.connectors[connection.from_name] from_connector = self.connectors[connection.from_name]
from_pin_index = from_connector.pins.index(connection.from_pin) 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 '' from_port_str = (
code_left_1 = f'{connection.from_name}{from_port_str}:e' f":p{from_pin_index+1}r"
code_left_2 = f'{cable.name}:w{connection.via_port}:w' 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) dot.edge(code_left_1, code_left_2)
if from_connector.show_name: 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: if from_connector.pinlabels:
pinlabel = from_connector.pinlabels[from_pin_index] pinlabel = from_connector.pinlabels[from_pin_index]
if pinlabel != '': if pinlabel != "":
from_info.append(pinlabel) from_info.append(pinlabel)
from_string = ':'.join(from_info) from_string = ":".join(from_info)
else: else:
from_string = '' from_string = ""
html = [row.replace(f'<!-- {connection.via_port}_in -->', from_string) for row in html] html = [
row.replace(f"<!-- {connection.via_port}_in -->", from_string)
for row in html
]
if connection.to_pin is not None: # connect to right if connection.to_pin is not None: # connect to right
to_connector = self.connectors[connection.to_name] to_connector = self.connectors[connection.to_name]
to_pin_index = to_connector.pins.index(connection.to_pin) 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 '' to_port_str = (
code_right_1 = f'{cable.name}:w{connection.via_port}:e' f":p{to_pin_index+1}l" if to_connector.style != "simple" else ""
code_right_2 = f'{connection.to_name}{to_port_str}:w' )
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) dot.edge(code_right_1, code_right_2)
if to_connector.show_name: if to_connector.show_name:
to_info = [str(connection.to_name), str(connection.to_pin)] to_info = [str(connection.to_name), str(connection.to_pin)]
if to_connector.pinlabels: if to_connector.pinlabels:
pinlabel = to_connector.pinlabels[to_pin_index] pinlabel = to_connector.pinlabels[to_pin_index]
if pinlabel != '': if pinlabel != "":
to_info.append(pinlabel) to_info.append(pinlabel)
to_string = ':'.join(to_info) to_string = ":".join(to_info)
else: else:
to_string = '' to_string = ""
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html] html = [
row.replace(f"<!-- {connection.via_port}_out -->", to_string)
for row in html
]
style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \ style, bgcolor = (
('filled', self.options.bgcolor_cable) ("filled,dashed", self.options.bgcolor_bundle)
html = '\n'.join(html) if cable.category == "bundle"
dot.node(cable.name, label=f'<\n{html}\n>', shape='box', else ("filled", self.options.bgcolor_cable)
style=style, fillcolor=translate_color(bgcolor, "HEX")) )
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: def typecheck(name: str, value: Any, expect: type) -> None:
if not isinstance(value, expect): 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? # TODO?: Differ between override attributes and HTML?
if self.tweak.override is not None: 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(): for k, d in self.tweak.override.items():
typecheck(f'tweak.override.{k} key', k, str) typecheck(f"tweak.override.{k} key", k, str)
typecheck(f'tweak.override.{k} value', d, dict) typecheck(f"tweak.override.{k} value", d, dict)
for a, v in d.items(): for a, v in d.items():
typecheck(f'tweak.override.{k}.{a} key', a, str) 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} value", v, (str, type(None)))
# Override generated attributes of selected entries matching tweak.override. # Override generated attributes of selected entries matching tweak.override.
for i, entry in enumerate(dot.body): for i, entry in enumerate(dot.body):
if isinstance(entry, str): if isinstance(entry, str):
# Find a possibly quoted keyword after leading TAB(s) and followed by [ ]. # 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] keyword = match and match[2]
if keyword in self.tweak.override.keys(): if keyword in self.tweak.override.keys():
for attr, value in self.tweak.override[keyword].items(): for attr, value in self.tweak.override[keyword].items():
if value is None: 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: 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: 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 continue
if len(value) == 0 or ' ' in value: if len(value) == 0 or " " in value:
value = value.replace('"', r'\"') value = value.replace('"', r"\"")
value = f'"{value}"' 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 n_subs < 1:
# If attr not found, then append it # 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: 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 dot.body[i] = entry
if self.tweak.append is not None: if self.tweak.append is not None:
if isinstance(self.tweak.append, list): if isinstance(self.tweak.append, list):
for i, element in enumerate(self.tweak.append, 1): 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) dot.body.extend(self.tweak.append)
else: else:
typecheck('tweak.append', self.tweak.append, str) typecheck("tweak.append", self.tweak.append, str)
dot.body.append(self.tweak.append) dot.body.append(self.tweak.append)
for mate in self.mates: for mate in self.mates:
if mate.shape[0] == '<' and mate.shape[-1] == '>': if mate.shape[0] == "<" and mate.shape[-1] == ">":
dir = 'both' dir = "both"
elif mate.shape[0] == '<': elif mate.shape[0] == "<":
dir = 'back' dir = "back"
elif mate.shape[-1] == '>': elif mate.shape[-1] == ">":
dir = 'forward' dir = "forward"
else: else:
dir = 'none' dir = "none"
if isinstance(mate, MatePin): if isinstance(mate, MatePin):
color = '#000000' color = "#000000"
elif isinstance(mate, MateComponent): elif isinstance(mate, MateComponent):
color = '#000000:#000000' color = "#000000:#000000"
else: else:
raise Exception(f'{mate} is an unknown mate') raise Exception(f"{mate} is an unknown mate")
from_connector = self.connectors[mate.from_name] 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_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' else: # MateComponent or style == 'simple'
from_port_str = '' from_port_str = ""
if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple': if (
isinstance(mate, MatePin)
and self.connectors[mate.to_name].style != "simple"
):
to_pin_index = to_connector.pins.index(mate.to_pin) 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' else: # MateComponent or style == 'simple'
to_port_str = '' to_port_str = ""
code_from = f'{mate.from_name}{from_port_str}:e' code_from = f"{mate.from_name}{from_port_str}:e"
to_connector = self.connectors[mate.to_name] 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) dot.edge(code_from, code_to)
return dot return dot
@ -492,52 +688,64 @@ class Harness:
@property @property
def png(self): def png(self):
from io import BytesIO from io import BytesIO
graph = self.graph graph = self.graph
data = BytesIO() data = BytesIO()
data.write(graph.pipe(format='png')) data.write(graph.pipe(format="png"))
data.seek(0) data.seek(0)
return data.read() return data.read()
@property @property
def svg(self): def svg(self):
from io import BytesIO from io import BytesIO
graph = self.graph graph = self.graph
data = BytesIO() data = BytesIO()
data.write(graph.pipe(format='svg')) data.write(graph.pipe(format="svg"))
data.seek(0) data.seek(0)
return data.read() 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 # graphical output
graph = self.graph 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 # graphical output
for f in fmt: for f in fmt:
if f in ('png', 'svg', 'html'): if f in ("png", "svg", "html"):
if f == 'html': # if HTML format is specified, if f == "html": # if HTML format is specified,
f = 'svg' # generate SVG for embedding into HTML f = "svg" # generate SVG for embedding into HTML
# TODO: prevent rendering SVG twice when both SVG and HTML are specified # TODO: prevent rendering SVG twice when both SVG and HTML are specified
graph.format = f graph.format = f
graph.render(filename=filename, view=view, cleanup=cleanup) graph.render(filename=filename, view=view, cleanup=cleanup)
# GraphViz output # GraphViz output
if 'gv' in fmt: if "gv" in fmt:
graph.save(filename=f'{filename}.gv') graph.save(filename=f"{filename}.gv")
# BOM output # BOM output
bomlist = bom_list(self.bom()) bomlist = bom_list(self.bom())
if 'tsv' in fmt: if "tsv" in fmt:
with open_file_write(f'{filename}.bom.tsv') as file: with open_file_write(f"{filename}.bom.tsv") as file:
file.write(tuplelist2tsv(bomlist)) file.write(tuplelist2tsv(bomlist))
if 'csv' in fmt: if "csv" in fmt:
print('CSV output is not yet supported') # TODO: implement CSV output (preferrably using CSV library) print(
"CSV output is not yet supported"
) # TODO: implement CSV output (preferrably using CSV library)
# HTML output # HTML output
if 'html' in fmt: if "html" in fmt:
generate_html_output(filename, bomlist, self.metadata, self.options) generate_html_output(filename, bomlist, self.metadata, self.options)
# PDF output # PDF output
if 'pdf' in fmt: if "pdf" in fmt:
print('PDF output is not yet supported') # TODO: implement PDF output print("PDF output is not yet supported") # TODO: implement PDF output
# delete SVG if not needed # delete SVG if not needed
if 'html' in fmt and not 'svg' in fmt and not svg_already_exists: if "html" in fmt and not "svg" in fmt and not svg_already_exists:
Path(f'{filename}.svg').unlink() Path(f"{filename}.svg").unlink()
def bom(self): def bom(self):
if not self._bom: if not self._bom:

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Please don't import anything in this file to avoid issues when it is imported in setup.py # 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 CMD_NAME = "wireviz" # Lower case command and module name
APP_NAME = 'WireViz' # Application name in texts meant to be human readable APP_NAME = "WireViz" # Application name in texts meant to be human readable
APP_URL = 'https://github.com/formatc1702/WireViz' APP_URL = "https://github.com/formatc1702/WireViz"

View File

@ -14,34 +14,36 @@ from wv_helper import open_file_append, open_file_read, open_file_write
from wireviz import APP_NAME, __version__, wireviz from wireviz import APP_NAME, __version__, wireviz
dir = script_path.parent.parent.parent dir = script_path.parent.parent.parent
readme = 'readme.md' readme = "readme.md"
groups = { groups = {
'examples': { "examples": {
'path': dir / 'examples', "path": dir / "examples",
'prefix': 'ex', "prefix": "ex",
readme: [], # Include no files readme: [], # Include no files
'title': 'Example Gallery', "title": "Example Gallery",
}, },
'tutorial' : { "tutorial": {
'path': dir / 'tutorial', "path": dir / "tutorial",
'prefix': 'tutorial', "prefix": "tutorial",
readme: ['md', 'yml'], # Include .md and .yml files readme: ["md", "yml"], # Include .md and .yml files
'title': f'{APP_NAME} Tutorial', "title": f"{APP_NAME} Tutorial",
}, },
'demos' : { "demos": {
'path': dir / 'examples', "path": dir / "examples",
'prefix': 'demo', "prefix": "demo",
}, },
} }
input_extensions = ['.yml'] input_extensions = [".yml"]
extensions_not_containing_graphviz_output = ['.gv', '.bom.tsv'] extensions_not_containing_graphviz_output = [".gv", ".bom.tsv"]
extensions_containing_graphviz_output = ['.png', '.svg', '.html'] extensions_containing_graphviz_output = [".png", ".svg", ".html"]
generated_extensions = extensions_not_containing_graphviz_output + extensions_containing_graphviz_output generated_extensions = (
extensions_not_containing_graphviz_output + extensions_containing_graphviz_output
)
def collect_filenames(description, groupkey, ext_list): 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] patterns = [f"{groups[groupkey]['prefix']}*{ext}" for ext in ext_list]
if ext_list != input_extensions and readme in groups[groupkey]: if ext_list != input_extensions and readme in groups[groupkey]:
patterns.append(readme) patterns.append(readme)
@ -52,107 +54,141 @@ def collect_filenames(description, groupkey, ext_list):
def build_generated(groupkeys): def build_generated(groupkeys):
for key in groupkeys: for key in groupkeys:
# preparation # preparation
path = groups[key]['path'] path = groups[key]["path"]
build_readme = readme in groups[key] build_readme = readme in groups[key]
if build_readme: if build_readme:
include_readme = 'md' in groups[key][readme] include_readme = "md" in groups[key][readme]
include_source = 'yml' in groups[key][readme] include_source = "yml" in groups[key][readme]
with open_file_write(path / readme) as out: with open_file_write(path / readme) as out:
out.write(f'# {groups[key]["title"]}\n\n') out.write(f'# {groups[key]["title"]}\n\n')
# collect and iterate input YAML files # 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}"') print(f' "{yaml_file}"')
wireviz.parse_file(yaml_file) wireviz.parse_file(yaml_file)
if build_readme: 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: with open_file_append(path / readme) as out:
if include_readme: 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: for line in info:
out.write(line.replace('## ', f'## {i} - ')) out.write(line.replace("## ", f"## {i} - "))
out.write('\n\n') out.write("\n\n")
else: else:
out.write(f'## Example {i}\n') out.write(f"## Example {i}\n")
if include_source: if include_source:
with open_file_read(yaml_file) as src: with open_file_read(yaml_file) as src:
out.write('```yaml\n') out.write("```yaml\n")
for line in src: for line in src:
out.write(line) 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"![]({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"[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n"
)
def clean_generated(groupkeys): def clean_generated(groupkeys):
for key in groupkeys: for key in groupkeys:
# collect and remove files # 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(): if filename.is_file():
print(f' rm "{filename}"') print(f' rm "{filename}"')
Path(filename).unlink() Path(filename).unlink()
def compare_generated(groupkeys, branch = '', include_graphviz_output = False): def compare_generated(groupkeys, branch="", include_graphviz_output=False):
if branch: if branch:
branch = f' {branch.strip()}' branch = f" {branch.strip()}"
compare_extensions = generated_extensions if include_graphviz_output else extensions_not_containing_graphviz_output compare_extensions = (
generated_extensions
if include_graphviz_output
else extensions_not_containing_graphviz_output
)
for key in groupkeys: for key in groupkeys:
# collect and compare files # 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}"' cmd = f'git --no-pager diff{branch} -- "{filename}"'
print(f' {cmd}') print(f" {cmd}")
os.system(cmd) os.system(cmd)
def restore_generated(groupkeys, branch = ''): def restore_generated(groupkeys, branch=""):
if branch: if branch:
branch = f' {branch.strip()}' branch = f" {branch.strip()}"
for key in groupkeys: for key in groupkeys:
# collect input YAML files # collect input YAML files
filename_list = collect_filenames('Restoring', key, input_extensions) filename_list = collect_filenames("Restoring", key, input_extensions)
# collect files to restore # 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]: if readme in groups[key]:
filename_list.append(groups[key]['path'] / readme) filename_list.append(groups[key]["path"] / readme)
# restore files # restore files
for filename in filename_list: for filename in filename_list:
cmd = f'git checkout{branch} -- "{filename}"' cmd = f'git checkout{branch} -- "{filename}"'
print(f' {cmd}') print(f" {cmd}")
os.system(cmd) os.system(cmd)
def parse_args(): def parse_args():
parser = argparse.ArgumentParser(description=f'{APP_NAME} Example Manager',) parser = argparse.ArgumentParser(
parser.add_argument('-V', '--version', action='version', version=f'%(prog)s - {APP_NAME} {__version__}') description=f"{APP_NAME} Example Manager",
parser.add_argument('action', nargs='?', action='store', )
choices=['build','clean','compare','diff','restore'], default='build', parser.add_argument(
help='what to do with the generated files (default: build)') "-V",
parser.add_argument('-c', '--compare-graphviz-output', action='store_true', "--version",
help='the Graphviz output is also compared (default: False)') action="version",
parser.add_argument('-b', '--branch', action='store', default='', version=f"%(prog)s - {APP_NAME} {__version__}",
help='branch or commit to compare with or restore from') )
parser.add_argument('-g', '--groups', nargs='+', parser.add_argument(
choices=groups.keys(), default=groups.keys(), "action",
help='the groups of generated files (default: all)') 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() return parser.parse_args()
def main(): def main():
args = parse_args() args = parse_args()
if args.action == 'build': if args.action == "build":
build_generated(args.groups) build_generated(args.groups)
elif args.action == 'clean': elif args.action == "clean":
clean_generated(args.groups) 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) compare_generated(args.groups, args.branch, args.compare_graphviz_output)
elif args.action == 'restore': elif args.action == "restore":
restore_generated(args.groups, args.branch) restore_generated(args.groups, args.branch)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -7,7 +7,7 @@ from typing import Any, Dict, List, Tuple
import yaml import yaml
if __name__ == '__main__': if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).parent.parent)) # add src/wireviz to PATH sys.path.insert(0, str(Path(__file__).parent.parent)) # add src/wireviz to PATH
from wireviz.DataClasses import Metadata, Options, Tweak 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 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 - "harness" - will return the `Harness` instance
""" """
yaml_data = yaml.safe_load(yaml_str) 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 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 - "harness" - will return the `Harness` instance
""" """
# define variables ========================================================= # define variables =========================================================
# containers for parsed component data and connection sets # containers for parsed component data and connection sets
template_connectors = {} template_connectors = {}
template_cables = {} template_cables = {}
connection_sets = [] connection_sets = []
# actual harness # actual harness
harness = Harness( harness = Harness(
metadata = Metadata(**yaml_data.get('metadata', {})), metadata=Metadata(**yaml_data.get("metadata", {})),
options = Options(**yaml_data.get('options', {})), options=Options(**yaml_data.get("options", {})),
tweak = Tweak(**yaml_data.get('tweak', {})), tweak=Tweak(**yaml_data.get("tweak", {})),
) )
# others # others
designators_and_templates = {} # store mapping of components to their respective template designators_and_templates = (
autogenerated_designators = {} # keep track of auto-generated designators to avoid duplicates {}
) # 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: if "title" not in harness.metadata:
harness.metadata['title'] = Path(file_out).stem harness.metadata["title"] = Path(file_out).stem
# add items # add items
# parse YAML input file ==================================================== # parse YAML input file ====================================================
sections = ['connectors', 'cables', 'connections'] sections = ["connectors", "cables", "connections"]
types = [dict, dict, list] types = [dict, dict, list]
for sec, ty in zip(sections, types): for sec, ty in zip(sections, types):
if sec in yaml_data and type(yaml_data[sec]) == ty: # section exists 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: if ty == dict:
for key, attribs in yaml_data[sec].items(): for key, attribs in yaml_data[sec].items():
# The Image dataclass might need to open an image file with a relative path. # 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): if isinstance(image, dict):
image_path = image['src'] image_path = image["src"]
if image_path and not Path(image_path).is_absolute(): # resolve relative image path if (
image['src'] = smart_file_resolve(image_path, image_paths) image_path and not Path(image_path).is_absolute()
if sec == 'connectors': ): # resolve relative image path
image["src"] = smart_file_resolve(
image_path, image_paths
)
if sec == "connectors":
template_connectors[key] = attribs template_connectors[key] = attribs
elif sec == 'cables': elif sec == "cables":
template_cables[key] = attribs template_cables[key] = attribs
else: # section exists but is empty else: # section exists but is empty
pass pass
@ -102,24 +128,28 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
elif ty == list: elif ty == list:
yaml_data[sec] = [] yaml_data[sec] = []
connection_sets = yaml_data['connections'] connection_sets = yaml_data["connections"]
# go through connection sets, generate and connect components ============== # 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): def resolve_designator(inp, separator):
if separator in inp: # generate a new instance of an item if separator in inp: # generate a new instance of an item
if inp.count(separator) > 1: 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) template, designator = inp.split(separator)
if designator == '': if designator == "":
autogenerated_designators[template] = autogenerated_designators.get(template, 0) + 1 autogenerated_designators[template] = (
designator = f'__{template}_{autogenerated_designators[template]}' autogenerated_designators.get(template, 0) + 1
)
designator = f"__{template}_{autogenerated_designators[template]}"
# check if redefining existing component to different template # check if redefining existing component to different template
if designator in designators_and_templates: if designator in designators_and_templates:
if designators_and_templates[designator] != template: 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: else:
designators_and_templates[designator] = template designators_and_templates[designator] = template
else: 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 ========== # utilities to check for alternating connectors and cables/arrows ==========
alternating_types = ['connector','cable/arrow'] alternating_types = ["connector", "cable/arrow"]
expected_type = None expected_type = None
def check_type(designator, template, actual_type): 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 expected_type = actual_type
if actual_type != expected_type: # did not alternate 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 def alternate_type(): # flip between connector and cable/arrow
nonlocal expected_type nonlocal expected_type
@ -155,9 +187,11 @@ def parse(yaml_data: Dict, file_out: (str, Path) = None, output_formats: (None,
if isinstance(entry, list): if isinstance(entry, list):
connectioncount.append(len(entry)) connectioncount.append(len(entry))
elif isinstance(entry, dict): 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: else:
pass # strings do not reveal connectioncount pass # strings do not reveal connectioncount
if not any(connectioncount): if not any(connectioncount):
# no item in the list revealed connection count; # no item in the list revealed connection count;
# assume connection count is 1 # 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 # check that all entries are the same length
if len(set(connectioncount)) > 1: 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 # all entries are the same length, connection count is set
connectioncount = connectioncount[0] 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): for index, entry in enumerate(connection_set):
if isinstance(entry, list): if isinstance(entry, list):
for subindex, item in enumerate(entry): 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 connection_set[index][subindex] = designator
elif isinstance(entry, dict): elif isinstance(entry, dict):
key = list(entry.keys())[0] 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 ============================================== # Populate wiring harness ==============================================
expected_type = None # reset check for alternating types expected_type = None # reset check for alternating types
# at the beginning of every connection set # at the beginning of every connection set
# since each set may begin with either type # since each set may begin with either type
# generate components # generate components
for entry in connection_set: 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] template = designators_and_templates[designator]
if designator in harness.connectors: # existing connector instance if designator in harness.connectors: # existing connector instance
check_type(designator, template, 'connector') check_type(designator, template, "connector")
elif template in template_connectors.keys(): # generate new connector instance from template elif (
check_type(designator, template, 'connector') template in template_connectors.keys()
harness.add_connector(name = designator, **template_connectors[template]) ): # 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 elif designator in harness.cables: # existing cable instance
check_type(designator, template, 'cable/arrow') check_type(designator, template, "cable/arrow")
elif template in template_cables.keys(): # generate new cable instance from template elif (
check_type(designator, template, 'cable/arrow') template in template_cables.keys()
harness.add_cable(name = designator, **template_cables[template]) ): # generate new cable instance from template
check_type(designator, template, "cable/arrow")
harness.add_cable(name=designator, **template_cables[template])
elif is_arrow(designator): elif is_arrow(designator):
check_type(designator, template, 'cable/arrow') check_type(designator, template, "cable/arrow")
# arrows do not need to be generated here # arrows do not need to be generated here
else: 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 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] designator = list(item.keys())[0]
if designator in harness.cables: 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) from_name, from_pin = (None, None)
else: 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]) 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) to_name, to_pin = (None, None)
else: else:
to_name, to_pin = get_single_key_and_value(entry[index_item+1]) to_name, to_pin = get_single_key_and_value(
harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) entry[index_item + 1]
)
harness.connect(
from_name, from_pin, via_name, via_pin, to_name, to_pin
)
elif is_arrow(designator): elif is_arrow(designator):
if index_item == 0: # list starts with an arrow 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 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]) from_name, from_pin = get_single_key_and_value(
via_name, via_pin = (designator, None) entry[index_item - 1]
to_name, to_pin = get_single_key_and_value(entry[index_item+1]) )
if '-' in designator: # mate pin by pin via_name, via_pin = (designator, None)
harness.add_mate_pin(from_name, from_pin, to_name, to_pin, designator) to_name, to_pin = get_single_key_and_value(entry[index_item + 1])
elif '=' in designator and index_entry == 0: # mate two connectors as a whole 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.add_mate_component(from_name, to_name, designator)
# harness population completed ============================================= # 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: if return_types is not None:
returns = [] 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 = [return_types]
return_types = [t.lower() for t in return_types] return_types = [t.lower() for t in return_types]
for rt in return_types: for rt in return_types:
if rt == 'png': if rt == "png":
returns.append(harness.png) returns.append(harness.png)
if rt == 'svg': if rt == "svg":
returns.append(harness.svg) returns.append(harness.svg)
if rt == 'harness': if rt == "harness":
returns.append(harness) returns.append(harness)
return tuple(returns) if len(returns) != 1 else returns[0] 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]) 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() main()

View File

@ -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_gv_html import html_bgcolor_attr, html_line_breaks
from wireviz.wv_helper import clean_whitespace from wireviz.wv_helper import clean_whitespace
BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators') BOM_COLUMNS_ALWAYS = ("id", "description", "qty", "unit", "designators")
BOM_COLUMNS_OPTIONAL = ('pn', 'manufacturer', 'mpn', 'supplier', 'spn') BOM_COLUMNS_OPTIONAL = ("pn", "manufacturer", "mpn", "supplier", "spn")
BOM_COLUMNS_IN_KEY = ('description', 'unit') + BOM_COLUMNS_OPTIONAL BOM_COLUMNS_IN_KEY = ("description", "unit") + BOM_COLUMNS_OPTIONAL
HEADER_PN = 'P/N' HEADER_PN = "P/N"
HEADER_MPN = 'MPN' HEADER_MPN = "MPN"
HEADER_SPN = 'SPN' HEADER_SPN = "SPN"
BOMKey = Tuple[str, ...] BOMKey = Tuple[str, ...]
BOMColumn = str # = Literal[*BOM_COLUMNS_ALWAYS, *BOM_COLUMNS_OPTIONAL] BOMColumn = str # = Literal[*BOM_COLUMNS_ALWAYS, *BOM_COLUMNS_OPTIONAL]
BOMEntry = Dict[BOMColumn, Union[str, int, float, List[str], None]] BOMEntry = Dict[BOMColumn, Union[str, int, float, List[str], None]]
def optional_fields(part: Union[Connector, Cable, AdditionalComponent]) -> BOMEntry: def optional_fields(part: Union[Connector, Cable, AdditionalComponent]) -> BOMEntry:
"""Return part field values for the optional BOM columns as a dict.""" """Return part field values for the optional BOM columns as a dict."""
part = asdict(part) part = asdict(part)
return {field: part.get(field) for field in BOM_COLUMNS_OPTIONAL} 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.""" """Return a list of diagram node table row strings with additional components."""
rows = [] rows = []
if component.additional_components: if component.additional_components:
rows.append(["Additional components"]) rows.append(["Additional components"])
for part in component.additional_components: for part in component.additional_components:
common_args = { common_args = {
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), "qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
'unit': part.unit, "unit": part.unit,
'bgcolor': part.bgcolor, "bgcolor": part.bgcolor,
} }
if harness.options.mini_bom_mode: if harness.options.mini_bom_mode:
id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description})) id = get_bom_index(
rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args)) harness.bom(),
bom_entry_key({**asdict(part), "description": part.description}),
)
rows.append(
component_table_entry(
f"#{id} ({part.type.rstrip()})", **common_args
)
)
else: 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 return rows
def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]: def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]:
"""Return a list of BOM entries with additional components.""" """Return a list of BOM entries with additional components."""
bom_entries = [] bom_entries = []
for part in component.additional_components: for part in component.additional_components:
bom_entries.append({ bom_entries.append(
'description': part.description, {
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), "description": part.description,
'unit': part.unit, "qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
'designators': component.name if component.show_name else None, "unit": part.unit,
**optional_fields(part), "designators": component.name if component.show_name else None,
}) **optional_fields(part),
}
)
return bom_entries return bom_entries
def bom_entry_key(entry: BOMEntry) -> BOMKey: def bom_entry_key(entry: BOMEntry) -> BOMKey:
"""Return a tuple of string values from the dict that must be equal to join BOM entries.""" """Return a tuple of string values from the dict that must be equal to join BOM entries."""
if 'key' not in entry: if "key" not in entry:
entry['key'] = tuple(clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY) entry["key"] = tuple(
return entry['key'] clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY
)
return entry["key"]
def generate_bom(harness: "Harness") -> List[BOMEntry]: def generate_bom(harness: "Harness") -> List[BOMEntry]:
"""Return a list of BOM entries generated from the harness.""" """Return a list of BOM entries generated from the harness."""
from wireviz.Harness import Harness # Local import to avoid circular imports from wireviz.Harness import Harness # Local import to avoid circular imports
bom_entries = [] bom_entries = []
# connectors # connectors
for connector in harness.connectors.values(): for connector in harness.connectors.values():
if not connector.ignore_in_bom: if not connector.ignore_in_bom:
description = ('Connector' description = (
+ (f', {connector.type}' if connector.type else '') "Connector"
+ (f', {connector.subtype}' if connector.subtype else '') + (f", {connector.type}" if connector.type else "")
+ (f', {connector.pincount} pins' if connector.show_pincount else '') + (f", {connector.subtype}" if connector.subtype else "")
+ (f', {translate_color(connector.color, harness.options.color_mode)}' if connector.color else '')) + (f", {connector.pincount} pins" if connector.show_pincount else "")
bom_entries.append({ + (
'description': description, 'designators': connector.name if connector.show_name else None, f", {translate_color(connector.color, harness.options.color_mode)}"
**optional_fields(connector), 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 # add connectors aditional components to bom
bom_entries.extend(get_additional_component_bom(connector)) 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? # TODO: If category can have other non-empty values than 'bundle', maybe it should be part of description?
for cable in harness.cables.values(): for cable in harness.cables.values():
if not cable.ignore_in_bom: if not cable.ignore_in_bom:
if cable.category != 'bundle': if cable.category != "bundle":
# process cable as a single entity # process cable as a single entity
description = ('Cable' description = (
+ (f', {cable.type}' if cable.type else '') "Cable"
+ (f', {cable.wirecount}') + (f", {cable.type}" if cable.type else "")
+ (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires') + (f", {cable.wirecount}")
+ ( ' shielded' if cable.shield else '') + (
+ (f', {translate_color(cable.color, harness.options.color_mode)}' if cable.color else '')) f" x {cable.gauge} {cable.gauge_unit}"
bom_entries.append({ if cable.gauge
'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, else " wires"
**optional_fields(cable), )
}) + (" 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: else:
# add each wire from the bundle to the bom # add each wire from the bundle to the bom
for index, color in enumerate(cable.colors): for index, color in enumerate(cable.colors):
description = ('Wire' description = (
+ (f', {cable.type}' if cable.type else '') "Wire"
+ (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '') + (f", {cable.type}" if cable.type else "")
+ (f', {translate_color(color, harness.options.color_mode)}' if color else '')) + (f", {cable.gauge} {cable.gauge_unit}" if cable.gauge else "")
bom_entries.append({ + (
'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, f", {translate_color(color, harness.options.color_mode)}"
**{k: index_if_list(v, index) for k, v in optional_fields(cable).items()}, 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 # add cable/bundles aditional components to bom
bom_entries.extend(get_additional_component_bom(cable)) 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) bom_entries.extend(harness.additional_bom_items)
# remove line breaks if present and cleanup any resulting whitespace issues # 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 # deduplicate bom
bom = [] bom = []
for _, group in groupby(sorted(bom_entries, key=bom_entry_key), key=bom_entry_key): for _, group in groupby(sorted(bom_entries, key=bom_entry_key), key=bom_entry_key):
group_entries = list(group) group_entries = list(group)
designators = sum((make_list(entry.get('designators')) for entry in group_entries), []) designators = sum(
total_qty = sum(entry.get('qty', 1) for entry in group_entries) (make_list(entry.get("designators")) for entry in group_entries), []
bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': sorted(set(designators))}) )
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 # 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: def get_bom_index(bom: List[BOMEntry], target: BOMKey) -> int:
"""Return id of BOM entry or raise exception if not found.""" """Return id of BOM entry or raise exception if not found."""
for entry in bom: for entry in bom:
if bom_entry_key(entry) == target: if bom_entry_key(entry) == target:
return entry['id'] return entry["id"]
raise Exception('Internal error: No BOM entry found matching: ' + '|'.join(target)) raise Exception("Internal error: No BOM entry found matching: " + "|".join(target))
def bom_list(bom: List[BOMEntry]) -> List[List[str]]: def bom_list(bom: List[BOMEntry]) -> List[List[str]]:
"""Return list of BOM rows as lists of column strings with headings in top row.""" """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. 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): if any(entry.get(fieldname) for entry in bom):
keys.append(fieldname) keys.append(fieldname)
# Custom mapping from internal name to BOM column headers. # Custom mapping from internal name to BOM column headers.
# Headers not specified here are generated by capitilising the internal name. # Headers not specified here are generated by capitilising the internal name.
bom_headings = { bom_headings = {
'pn': HEADER_PN, "pn": HEADER_PN,
'mpn': HEADER_MPN, "mpn": HEADER_MPN,
'spn': HEADER_SPN, "spn": HEADER_SPN,
} }
return ([[bom_headings.get(k, k.capitalize()) for k in keys]] + # Create header row with key names return [
[[make_str(entry.get(k)) for k in keys] for entry in bom]) # Create string list for each entry row [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( def component_table_entry(
type: str, type: str,
qty: Union[int, float], qty: Union[int, float],
unit: Optional[str] = None, unit: Optional[str] = None,
bgcolor: Optional[Color] = None, bgcolor: Optional[Color] = None,
pn: Optional[str] = None, pn: Optional[str] = None,
manufacturer: Optional[str] = None, manufacturer: Optional[str] = None,
mpn: Optional[str] = None, mpn: Optional[str] = None,
supplier: Optional[str] = None, supplier: Optional[str] = None,
spn: Optional[str] = None, spn: Optional[str] = None,
) -> str: ) -> str:
"""Return a diagram node table row string with an additional component.""" """Return a diagram node table row string with an additional component."""
part_number_list = [ part_number_list = [
pn_info_string(HEADER_PN, None, pn), pn_info_string(HEADER_PN, None, pn),
pn_info_string(HEADER_MPN, manufacturer, mpn), pn_info_string(HEADER_MPN, manufacturer, mpn),
pn_info_string(HEADER_SPN, supplier, spn), pn_info_string(HEADER_SPN, supplier, spn),
] ]
output = (f'{qty}' output = (
+ (f' {unit}' if unit else '') f"{qty}"
+ f' x {type}' + (f" {unit}" if unit else "")
+ ('<br/>' if any(part_number_list) else '') + f" x {type}"
+ (', '.join([pn for pn in part_number_list if pn]))) + ("<br/>" 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 # 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 # indent is set to two to match the indent in the generated html table
return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{html_bgcolor_attr(bgcolor)}><tr> return f"""<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{html_bgcolor_attr(bgcolor)}><tr>
<td align="left" balign="left">{html_line_breaks(output)}</td> <td align="left" balign="left">{html_line_breaks(output)}</td>
</tr></table>''' </tr></table>"""
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.""" """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: if name or number:
return f'{name if name else header}{": " + number if number else ""}' return f'{name if name else header}{": " + number if number else ""}'
else: else:
return None return None
def index_if_list(value: Any, index: int) -> Any: def index_if_list(value: Any, index: int) -> Any:
"""Return the value indexed if it is a list, or simply the value otherwise.""" """Return the value indexed if it is a list, or simply the value otherwise."""
return value[index] if isinstance(value, list) else value return value[index] if isinstance(value, list) else value
def make_list(value: Any) -> list: def make_list(value: Any) -> list:
"""Return value if a list, empty list if None, or single element list otherwise.""" """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] return value if isinstance(value, list) else [] if value is None else [value]
def make_str(value: Any) -> str: def make_str(value: Any) -> str:
"""Return comma separated elements if a list, empty string if None, or value as a string otherwise.""" """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))

View File

@ -6,30 +6,64 @@ from pathlib import Path
import click import click
if __name__ == '__main__': if __name__ == "__main__":
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import wireviz.wireviz as wv import wireviz.wireviz as wv
from wireviz import APP_NAME, __version__ from wireviz import APP_NAME, __version__
from wireviz.wv_helper import open_file_read 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.command(epilog=epilog, no_args_is_help=True)
@click.argument('file', nargs=-1) @click.argument("file", nargs=-1)
@click.option('-f', '--format', default='hpst', type=str, show_default=True, help='Output formats (see below).') @click.option(
@click.option('-p', '--prepend', default=None, type=Path, help='YAML file to prepend to the input file (optional).') "-f",
@click.option('-o', '--output-file', default=None, type=Path, help='File name (without extension) to use for output, if different from input file name.') "--format",
@click.option('-V', '--version', is_flag=True, default=False, help=f'Output {APP_NAME} version and exit.') 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): def wireviz(file, format, prepend, output_file, version):
""" """
Parses the provided FILE and generates the specified outputs. Parses the provided FILE and generates the specified outputs.
""" """
print() print()
print(f'{APP_NAME} {__version__}') print(f"{APP_NAME} {__version__}")
if version: if version:
return # print version number only and exit return # print version number only and exit
@ -47,35 +81,39 @@ def wireviz(file, format, prepend, output_file, version):
if code in format_codes: if code in format_codes:
output_formats.append(format_codes[code]) output_formats.append(format_codes[code])
else: else:
raise Exception(f'Unknown output format: {code}') raise Exception(f"Unknown output format: {code}")
output_formats = tuple(sorted(set(output_formats))) 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 = [] image_paths = []
# check prepend file # check prepend file
if prepend: if prepend:
prepend = Path(prepend) prepend = Path(prepend)
if not prepend.exists(): if not prepend.exists():
raise Exception(f'File does not exist:\n{prepend}') raise Exception(f"File does not exist:\n{prepend}")
print('Prepend file:', prepend) print("Prepend file:", prepend)
with open_file_read(prepend) as file_handle: with open_file_read(prepend) as file_handle:
prepend_input = file_handle.read() + '\n' prepend_input = file_handle.read() + "\n"
prepend_dir = prepend.parent prepend_dir = prepend.parent
else: else:
prepend_input = '' prepend_input = ""
prepend_dir = None prepend_dir = None
# run WireVIz on each input file # run WireVIz on each input file
for file in filepaths: for file in filepaths:
file = Path(file) file = Path(file)
if not file.exists(): 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("Input file: ", file)
print('Output file: ', f'{file_out}.{output_formats_str}') print("Output file: ", f"{file_out}.{output_formats_str}")
with open_file_read(file) as file_handle: with open_file_read(file) as file_handle:
yaml_input = file_handle.read() yaml_input = file_handle.read()
@ -83,9 +121,15 @@ def wireviz(file, format, prepend, output_file, version):
yaml_input = prepend_input + yaml_input 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() print()
if __name__ == '__main__':
if __name__ == "__main__":
wireviz() wireviz()

View File

@ -3,181 +3,337 @@
from typing import Dict, List from typing import Dict, List
COLOR_CODES = { COLOR_CODES = {
'DIN': ['WH', 'BN', 'GN', 'YE', 'GY', 'PK', 'BU', 'RD', 'BK', 'VT', 'GYPK', 'RDBU', 'WHGN', 'BNGN', 'WHYE', 'YEBN', "DIN": [
'WHGY', 'GYBN', 'WHPK', 'PKBN', 'WHBU', 'BNBU', 'WHRD', 'BNRD', 'WHBK', 'BNBK', 'GYGN', 'YEGY', 'PKGN', "WH",
'YEPK', 'GNBU', 'YEBU', 'GNRD', 'YERD', 'GNBK', 'YEBK', 'GYBU', 'PKBU', 'GYRD', 'PKRD', 'GYBK', 'PKBK', "BN",
'BUBK', 'RDBK', 'WHBNBK', 'YEGNBK', 'GYPKBK', 'RDBUBK', 'WHGNBK', 'BNGNBK', 'WHYEBK', 'YEBNBK', 'WHGYBK', "GN",
'GYBNBK', 'WHPKBK', 'PKBNBK', 'WHBUBK', 'BNBUBK', 'WHRDBK', 'BNRDBK'], "YE",
'IEC': ['BN', 'RD', 'OG', 'YE', 'GN', 'BU', 'VT', 'GY', 'WH', 'BK'], "GY",
'BW': ['BK', 'WH'], "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 # 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). # 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. # Each POTS pair tip (+) had major/minor color, and ring (-) had minor/major color.
'TEL': [ # 25x2: Ring and then tip of each pair "TEL": [ # 25x2: Ring and then tip of each pair
'BUWH', 'WHBU', 'OGWH', 'WHOG', 'GNWH', 'WHGN', 'BNWH', 'WHBN', 'SLWH', 'WHSL', "BUWH",
'BURD', 'RDBU', 'OGRD', 'RDOG', 'GNRD', 'RDGN', 'BNRD', 'RDBN', 'SLRD', 'RDSL', "WHBU",
'BUBK', 'BKBU', 'OGBK', 'BKOG', 'GNBK', 'BKGN', 'BNBK', 'BKBN', 'SLBK', 'BKSL', "OGWH",
'BUYE', 'YEBU', 'OGYE', 'YEOG', 'GNYE', 'YEGN', 'BNYE', 'YEBN', 'SLYE', 'YESL', "WHOG",
'BUVT', 'VTBU', 'OGVT', 'VTOG', 'GNVT', 'VTGN', 'BNVT', 'VTBN', 'SLVT', 'VTSL'], "GNWH",
'TELALT': [ # 25x2: Tip and then ring of each pair "WHGN",
'WHBU', 'BU', 'WHOG', 'OG', 'WHGN', 'GN', 'WHBN', 'BN', 'WHSL', 'SL', "BNWH",
'RDBU', 'BURD', 'RDOG', 'OGRD', 'RDGN', 'GNRD', 'RDBN', 'BNRD', 'RDSL', 'SLRD', "WHBN",
'BKBU', 'BUBK', 'BKOG', 'OGBK', 'BKGN', 'GNBK', 'BKBN', 'BNBK', 'BKSL', 'SLBK', "SLWH",
'YEBU', 'BUYE', 'YEOG', 'OGYE', 'YEGN', 'GNYE', 'YEBN', 'BNYE', 'YESL', 'SLYE', "WHSL",
'VTBU', 'BUVT', 'VTOG', 'OGVT', 'VTGN', 'GNVT', 'VTBN', 'BNVT', 'VTSL', 'SLVT'], "BURD",
'T568A': ['WHGN', 'GN', 'WHOG', 'BU', 'WHBU', 'OG', 'WHBN', 'BN'], "RDBU",
'T568B': ['WHOG', 'OG', 'WHGN', 'BU', 'WHBU', 'GN', 'WHBN', 'BN'], "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 # Convention: Color names should be 2 letters long, to allow for multicolored wires
_color_hex = { _color_hex = {
'BK': '#000000', "BK": "#000000",
'WH': '#ffffff', "WH": "#ffffff",
'GY': '#999999', "GY": "#999999",
'PK': '#ff66cc', "PK": "#ff66cc",
'RD': '#ff0000', "RD": "#ff0000",
'OG': '#ff8000', "OG": "#ff8000",
'YE': '#ffff00', "YE": "#ffff00",
'OL': '#708000', # olive green "OL": "#708000", # olive green
'GN': '#00ff00', "GN": "#00ff00",
'TQ': '#00ffff', "TQ": "#00ffff",
'LB': '#a0dfff', # light blue "LB": "#a0dfff", # light blue
'BU': '#0066ff', "BU": "#0066ff",
'VT': '#8000ff', "VT": "#8000ff",
'BN': '#895956', "BN": "#895956",
'BG': '#ceb673', # beige "BG": "#ceb673", # beige
'IV': '#f5f0d0', # ivory "IV": "#f5f0d0", # ivory
'SL': '#708090', "SL": "#708090",
'CU': '#d6775e', # Faux-copper look, for bare CU wire "CU": "#d6775e", # Faux-copper look, for bare CU wire
'SN': '#aaaaaa', # Silvery look for tinned bare wire "SN": "#aaaaaa", # Silvery look for tinned bare wire
'SR': '#84878c', # Darker silver for silvered wire "SR": "#84878c", # Darker silver for silvered wire
'GD': '#ffcf80', # Golden color for gold "GD": "#ffcf80", # Golden color for gold
} }
_color_full = { _color_full = {
'BK': 'black', "BK": "black",
'WH': 'white', "WH": "white",
'GY': 'grey', "GY": "grey",
'PK': 'pink', "PK": "pink",
'RD': 'red', "RD": "red",
'OG': 'orange', "OG": "orange",
'YE': 'yellow', "YE": "yellow",
'OL': 'olive green', "OL": "olive green",
'GN': 'green', "GN": "green",
'TQ': 'turquoise', "TQ": "turquoise",
'LB': 'light blue', "LB": "light blue",
'BU': 'blue', "BU": "blue",
'VT': 'violet', "VT": "violet",
'BN': 'brown', "BN": "brown",
'BG': 'beige', "BG": "beige",
'IV': 'ivory', "IV": "ivory",
'SL': 'slate', "SL": "slate",
'CU': 'copper', "CU": "copper",
'SN': 'tin', "SN": "tin",
'SR': 'silver', "SR": "silver",
'GD': 'gold', "GD": "gold",
} }
_color_ger = { _color_ger = {
'BK': 'sw', "BK": "sw",
'WH': 'ws', "WH": "ws",
'GY': 'gr', "GY": "gr",
'PK': 'rs', "PK": "rs",
'RD': 'rt', "RD": "rt",
'OG': 'or', "OG": "or",
'YE': 'ge', "YE": "ge",
'OL': 'ol', # olivgrün "OL": "ol", # olivgrün
'GN': 'gn', "GN": "gn",
'TQ': 'tk', "TQ": "tk",
'LB': 'hb', # hellblau "LB": "hb", # hellblau
'BU': 'bl', "BU": "bl",
'VT': 'vi', "VT": "vi",
'BN': 'br', "BN": "br",
'BG': 'bg', # beige "BG": "bg", # beige
'IV': 'eb', # elfenbeinfarben "IV": "eb", # elfenbeinfarben
'SL': 'si', # Schiefer "SL": "si", # Schiefer
'CU': 'ku', # Kupfer "CU": "ku", # Kupfer
'SN': 'vz', # verzinkt "SN": "vz", # verzinkt
'SR': 'ag', # Silber "SR": "ag", # Silber
'GD': 'au', # Gold "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 # Literal type aliases below are commented to avoid requiring python 3.8
Color = str # Two-letter color name = Literal[_color_hex.keys()] Color = str # Two-letter color name = Literal[_color_hex.keys()]
Colors = str # One or more two-letter color names (Color) concatenated into one string 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()] ColorScheme = str # Color scheme name = Literal[COLOR_CODES.keys()]
def get_color_hex(input: Colors, pad: bool = False) -> List[str]: 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.""" """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] return [color_default]
elif input[0] == '#': # Hex color(s) elif input[0] == "#": # Hex color(s)
output = input.split(':') output = input.split(":")
for i, c in enumerate(output): 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: if c != input:
c += f' in input: {input}' c += f" in input: {input}"
print(f'Invalid hex color: {c}') print(f"Invalid hex color: {c}")
output[i] = color_default output[i] = color_default
else: # Color name(s) else: # Color name(s)
def lookup(c: str) -> str: def lookup(c: str) -> str:
try: try:
return _color_hex[c] return _color_hex[c]
except KeyError: except KeyError:
if c != input: if c != input:
c += f' in input: {input}' c += f" in input: {input}"
print(f'Unknown color name: {c}') print(f"Unknown color name: {c}")
return color_default 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. if len(output) == 2: # Give wires with EXACTLY 2 colors that striped look.
output += output[:1] output += output[:1]
elif pad and len(output) == 1: # Hacky style fix: Give single color wires 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 return output
def get_color_translation(translate: Dict[Color, str], input: Colors) -> List[str]: 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.""" """Return list of colors translations from either a string of color names or :-separated hex colors."""
def from_hex(hex_input: str) -> str: def from_hex(hex_input: str) -> str:
for color, hex in _color_hex.items(): for color, hex in _color_hex.items():
if hex == hex_input: if hex == hex_input:
return translate[color] return translate[color]
return f'({",".join(str(int(hex_input[i:i+2], 16)) for i in range(1, 6, 2))})' 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 \ return (
[translate.get(input[i:i+2], '??') for i in range(0, len(input), 2)] [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: def translate_color(input: Colors, color_mode: ColorMode) -> str:
if input == '' or input is None: if input == "" or input is None:
return '' return ""
upper = color_mode.isupper() upper = color_mode.isupper()
if not (color_mode.isupper() or color_mode.islower()): 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() color_mode = color_mode.lower()
if color_mode == 'full': if color_mode == "full":
output = "/".join(get_color_translation(_color_full, input)) output = "/".join(get_color_translation(_color_full, input))
elif color_mode == 'hex': elif color_mode == "hex":
output = ':'.join(get_color_hex(input, pad=False)) output = ":".join(get_color_hex(input, pad=False))
elif color_mode == 'ger': elif color_mode == "ger":
output = "".join(get_color_translation(_color_ger, input)) output = "".join(get_color_translation(_color_ger, input))
elif color_mode == 'short': elif color_mode == "short":
output = input output = input
else: else:
raise Exception('Unknown color mode') raise Exception("Unknown color mode")
if upper: if upper:
return output.upper() return output.upper()
else: else:

View File

@ -8,51 +8,66 @@ from wireviz.wv_colors import translate_color
from wireviz.wv_helper import remove_links 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 # 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 # 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 # purpose: create the appearance of one table, where cell widths are independent between rows
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag # attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
html = [] html = []
html.append(f'<table border="0" cellspacing="0" cellpadding="0"{table_attrs or ""}>') html.append(
f'<table border="0" cellspacing="0" cellpadding="0"{table_attrs or ""}>'
)
num_rows = 0 num_rows = 0
for row in rows: for row in rows:
if isinstance(row, List): if isinstance(row, List):
if len(row) > 0 and any(row): if len(row) > 0 and any(row):
html.append(' <tr><td>') html.append(" <tr><td>")
html.append(' <table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>') html.append(
' <table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>'
)
for cell in row: for cell in row:
if cell is not None: if cell is not None:
# Inject attributes to the preceeding <td> tag where needed # Inject attributes to the preceeding <td> tag where needed
html.append(f' <td balign="left">{cell}</td>'.replace('><tdX', '')) html.append(
html.append(' </tr></table>') f' <td balign="left">{cell}</td>'.replace("><tdX", "")
html.append(' </td></tr>') )
html.append(" </tr></table>")
html.append(" </td></tr>")
num_rows = num_rows + 1 num_rows = num_rows + 1
elif row is not None: elif row is not None:
html.append(' <tr><td>') html.append(" <tr><td>")
html.append(f' {row}') html.append(f" {row}")
html.append(' </td></tr>') html.append(" </td></tr>")
num_rows = num_rows + 1 num_rows = num_rows + 1
if num_rows == 0: # empty table if num_rows == 0: # empty table
html.append('<tr><td></td></tr>') # generate empty cell to avoid GraphViz errors html.append(
html.append('</table>') "<tr><td></td></tr>"
) # generate empty cell to avoid GraphViz errors
html.append("</table>")
return html return html
def html_bgcolor_attr(color: Color) -> str: def html_bgcolor_attr(color: Color) -> str:
"""Return attributes for bgcolor or '' if no color.""" """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 <td> attributes prefix for bgcolor or '' if no color.""" """Return <td> attributes prefix for bgcolor or '' if no color."""
return f'<tdX{html_bgcolor_attr(color)}{_extra_attr}>' if color else '' return f"<tdX{html_bgcolor_attr(color)}{_extra_attr}>" if color else ""
def html_colorbar(color: Color) -> str: def html_colorbar(color: Color) -> str:
"""Return <tdX> attributes prefix for bgcolor and minimum width or None if no color.""" """Return <tdX> attributes prefix for bgcolor and minimum width or None if no color."""
return html_bgcolor(color, ' width="4"') if color else None return html_bgcolor(color, ' width="4"') if color else None
def html_image(image): def html_image(image):
from wireviz.DataClasses import Image from wireviz.DataClasses import Image
if not image: if not image:
return None return None
# The leading attributes belong to the preceeding tag. See where used below. # The leading attributes belong to the preceeding tag. See where used below.
@ -60,25 +75,38 @@ def html_image(image):
if image.fixedsize: if image.fixedsize:
# Close the preceeding tag and enclose the image cell in a table without # 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. # borders to avoid narrow borders when the fixed width < the node width.
html = f'''> html = f""">
<table border="0" cellspacing="0" cellborder="0"><tr> <table border="0" cellspacing="0" cellborder="0"><tr>
<td{html}</td> <td{html}</td>
</tr></table> </tr></table>
''' """
return f'''<tdX{' sides="TLR"' if image.caption else ''}{html_bgcolor_attr(image.bgcolor)}{html}''' return f"""<tdX{' sides="TLR"' if image.caption else ''}{html_bgcolor_attr(image.bgcolor)}{html}"""
def html_caption(image): def html_caption(image):
from wireviz.DataClasses import Image from wireviz.DataClasses import Image
return (f'<tdX sides="BLR"{html_bgcolor_attr(image.bgcolor)}>{html_line_breaks(image.caption)}'
if image and image.caption else None) return (
f'<tdX sides="BLR"{html_bgcolor_attr(image.bgcolor)}>{html_line_breaks(image.caption)}'
if image and image.caption
else None
)
def html_size_attr(image): def html_size_attr(image):
from wireviz.DataClasses import Image from wireviz.DataClasses import Image
# Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object # 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 '') return (
+ (f' height="{image.height}"' if image.height else '') (
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else '' (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): def html_line_breaks(inp):
return remove_links(inp).replace('\n', '<br />') if isinstance(inp, str) else inp return remove_links(inp).replace("\n", "<br />") if isinstance(inp, str) else inp

View File

@ -5,31 +5,33 @@ from pathlib import Path
from typing import Dict, List from typing import Dict, List
awg_equiv_table = { awg_equiv_table = {
'0.09': '28', "0.09": "28",
'0.14': '26', "0.14": "26",
'0.25': '24', "0.25": "24",
'0.34': '22', "0.34": "22",
'0.5': '21', "0.5": "21",
'0.75': '20', "0.75": "20",
'1': '18', "1": "18",
'1.5': '16', "1.5": "16",
'2.5': '14', "2.5": "14",
'4': '12', "4": "12",
'6': '10', "6": "10",
'10': '8', "10": "8",
'16': '6', "16": "6",
'25': '4', "25": "4",
'35': '2', "35": "2",
'50': '1', "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): def awg_equiv(mm2):
return awg_equiv_table.get(str(mm2), 'Unknown') return awg_equiv_table.get(str(mm2), "Unknown")
def mm2_equiv(awg): def mm2_equiv(awg):
return mm2_equiv_table.get(str(awg), 'Unknown') return mm2_equiv_table.get(str(awg), "Unknown")
def expand(yaml_data): def expand(yaml_data):
@ -42,8 +44,8 @@ def expand(yaml_data):
yaml_data = [yaml_data] yaml_data = [yaml_data]
for e in yaml_data: for e in yaml_data:
e = str(e) e = str(e)
if '-' in e: if "-" in e:
a, b = e.split('-', 1) a, b = e.split("-", 1)
try: try:
a = int(a) a = int(a)
b = int(b) b = int(b)
@ -56,7 +58,9 @@ def expand(yaml_data):
else: # a == b else: # a == b
output.append(a) # range of length 1 output.append(a) # range of length 1
except: 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: else:
try: try:
x = int(e) # single int x = int(e) # single int
@ -81,36 +85,46 @@ def int2tuple(inp):
def flatten2d(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): def tuplelist2tsv(inp, header=None):
output = '' output = ""
if header is not None: if header is not None:
inp.insert(0, header) inp.insert(0, header)
inp = flatten2d(inp) inp = flatten2d(inp)
for row in 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 return output
def remove_links(inp): def remove_links(inp):
return re.sub(r'<[aA] [^>]*>([^<]*)</[aA]>', r'\1', inp) if isinstance(inp, str) else inp return (
re.sub(r"<[aA] [^>]*>([^<]*)</[aA]>", r"\1", inp)
if isinstance(inp, str)
else inp
)
def clean_whitespace(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): def open_file_read(filename):
# TODO: Intelligently determine encoding # TODO: Intelligently determine encoding
return open(filename, 'r', encoding='UTF-8') return open(filename, "r", encoding="UTF-8")
def open_file_write(filename): def open_file_write(filename):
return open(filename, 'w', encoding='UTF-8') return open(filename, "w", encoding="UTF-8")
def open_file_append(filename): def open_file_append(filename):
return open(filename, 'a', encoding='UTF-8') return open(filename, "a", encoding="UTF-8")
def is_arrow(inp): def is_arrow(inp):
""" """
@ -122,19 +136,23 @@ def is_arrow(inp):
<==, ==, ==>, <=> <==, ==, ==>, <=>
""" """
# regex by @shiraneyo # regex by @shiraneyo
return bool(re.match(r"^\s*(?P<leftHead><?)(?P<body>-+|=+)(?P<rightHead>>?)\s*$", inp)) return bool(
re.match(r"^\s*(?P<leftHead><?)(?P<body>-+|=+)(?P<rightHead>>?)\s*$", inp)
)
def aspect_ratio(image_src): def aspect_ratio(image_src):
try: try:
from PIL import Image from PIL import Image
image = Image.open(image_src) image = Image.open(image_src)
if image.width > 0 and image.height > 0: if image.width > 0 and image.height > 0:
return image.width / image.height 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. # ModuleNotFoundError and FileNotFoundError are the most expected, but all are handled equally.
except Exception as error: except Exception as error:
print(f'aspect_ratio(): {type(error).__name__}: {error}') print(f"aspect_ratio(): {type(error).__name__}: {error}")
return 1 # Assume 1:1 when unable to read actual image size return 1 # Assume 1:1 when unable to read actual image size
def smart_file_resolve(filename: str, possible_paths: (str, List[str])) -> Path: 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(): if filename.exists():
return filename return filename
else: 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 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: for possible_path in possible_paths:
resolved_path = (possible_path / filename).resolve() resolved_path = (possible_path / filename).resolve()
if resolved_path.exists(): if resolved_path.exists():
return resolved_path return resolved_path
else: else:
raise Exception(f'{filename} was not found in any of the following locations: \n' + raise Exception(
'\n'.join([str(x) for x in possible_paths])) f"{filename} was not found in any of the following locations: \n"
+ "\n".join([str(x) for x in possible_paths])
)

View File

@ -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 # load HTML template
templatename = metadata.get('template',{}).get('name') templatename = metadata.get("template", {}).get("name")
if templatename: if templatename:
# if relative path to template was provided, check directory of YAML file first, fall back to built-in template directory # 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: else:
# fall back to built-in simple template if no template was provided # 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: with open_file_read(templatefile) as file:
html = file.read() html = file.read()
# embed SVG diagram # embed SVG diagram
with open_file_read(f'{filename}.svg') as file: with open_file_read(f"{filename}.svg") as file:
svgdata = re.sub( svgdata = re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>', "^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>",
'<!-- XML and DOCTYPE declarations from SVG file removed -->', "<!-- XML and DOCTYPE declarations from SVG file removed -->",
file.read(), 1) file.read(),
1,
)
# generate BOM table # generate BOM table
bom = flatten2d(bom_list) bom = flatten2d(bom_list)
# generate BOM header (may be at the top or bottom of the table) # generate BOM header (may be at the top or bottom of the table)
bom_header_html = ' <tr>\n' bom_header_html = " <tr>\n"
for item in bom[0]: 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} <th class="{th_class}">{item}</th>\n' bom_header_html = f'{bom_header_html} <th class="{th_class}">{item}</th>\n'
bom_header_html = f'{bom_header_html} </tr>\n' bom_header_html = f"{bom_header_html} </tr>\n"
# generate BOM contents # generate BOM contents
bom_contents = [] bom_contents = []
for row in bom[1:]: for row in bom[1:]:
row_html = ' <tr>\n' row_html = " <tr>\n"
for i, item in enumerate(row): 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} <td class="{td_class}">{item}</td>\n' row_html = f'{row_html} <td class="{td_class}">{item}</td>\n'
row_html = f'{row_html} </tr>\n' row_html = f"{row_html} </tr>\n"
bom_contents.append(row_html) bom_contents.append(row_html)
bom_html = '<table class="bom">\n' + bom_header_html + ''.join(bom_contents) + '</table>\n' bom_html = (
bom_html_reversed = '<table class="bom">\n' + ''.join(list(reversed(bom_contents))) + bom_header_html + '</table>\n' '<table class="bom">\n' + bom_header_html + "".join(bom_contents) + "</table>\n"
)
bom_html_reversed = (
'<table class="bom">\n'
+ "".join(list(reversed(bom_contents)))
+ bom_header_html
+ "</table>\n"
)
# prepare simple replacements # prepare simple replacements
replacements = { replacements = {
'<!-- %generator% -->': f'{APP_NAME} {__version__} - {APP_URL}', "<!-- %generator% -->": f"{APP_NAME} {__version__} - {APP_URL}",
'<!-- %fontname% -->': options.fontname, "<!-- %fontname% -->": options.fontname,
'<!-- %bgcolor% -->': wv_colors.translate_color(options.bgcolor, "hex"), "<!-- %bgcolor% -->": wv_colors.translate_color(options.bgcolor, "hex"),
'<!-- %diagram% -->': svgdata, "<!-- %diagram% -->": svgdata,
'<!-- %bom% -->': bom_html, "<!-- %bom% -->": bom_html,
'<!-- %bom_reversed% -->': bom_html_reversed, "<!-- %bom_reversed% -->": bom_html_reversed,
'<!-- %sheet_current% -->': '1', # TODO: handle multi-page documents "<!-- %sheet_current% -->": "1", # TODO: handle multi-page documents
'<!-- %sheet_total% -->': '1', # TODO: handle multi-page documents "<!-- %sheet_total% -->": "1", # TODO: handle multi-page documents
} }
# prepare metadata replacements # prepare metadata replacements
if metadata: if metadata:
for item, contents in metadata.items(): for item, contents in metadata.items():
if isinstance(contents, (str, int, float)): if isinstance(contents, (str, int, float)):
replacements[f'<!-- %{item}% -->'] = html_line_breaks(str(contents)) replacements[f"<!-- %{item}% -->"] = html_line_breaks(str(contents))
elif isinstance(contents, Dict): # useful for authors, revisions elif isinstance(contents, Dict): # useful for authors, revisions
for index, (category, entry) in enumerate(contents.items()): for index, (category, entry) in enumerate(contents.items()):
if isinstance(entry, Dict): if isinstance(entry, Dict):
replacements[f'<!-- %{item}_{index+1}% -->'] = str(category) replacements[f"<!-- %{item}_{index+1}% -->"] = str(category)
for entry_key, entry_value in entry.items(): for entry_key, entry_value in entry.items():
replacements[f'<!-- %{item}_{index+1}_{entry_key}% -->'] = html_line_breaks(str(entry_value)) replacements[
f"<!-- %{item}_{index+1}_{entry_key}% -->"
] = html_line_breaks(str(entry_value))
replacements['"sheetsize_default"'] = '"{}"'.format(metadata.get('template',{}).get('sheetsize', '')) # include quotes so no replacement happens within <style> definition replacements['"sheetsize_default"'] = '"{}"'.format(
metadata.get("template", {}).get("sheetsize", "")
) # include quotes so no replacement happens within <style> definition
# perform replacements # perform replacements
# regex replacement adapted from: # regex replacement adapted from:
# https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729 # https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729
replacements_sorted = sorted(replacements, key=len, reverse=True) # longer replacements first, just in case replacements_sorted = sorted(
replacements, key=len, reverse=True
) # longer replacements first, just in case
replacements_escaped = map(re.escape, replacements_sorted) replacements_escaped = map(re.escape, replacements_sorted)
pattern = re.compile("|".join(replacements_escaped)) pattern = re.compile("|".join(replacements_escaped))
html = pattern.sub(lambda match: replacements[match.group(0)], html) html = pattern.sub(lambda match: replacements[match.group(0)], html)
with open_file_write(f'{filename}.html') as file: with open_file_write(f"{filename}.html") as file:
file.write(html) file.write(html)