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