From b9a4783b6fba882234c3c3e2c74968dcfa38596d Mon Sep 17 00:00:00 2001 From: Tyler Ward Date: Sun, 26 Jul 2020 15:50:11 +0100 Subject: [PATCH 01/10] Shorten BOM field names (#121) - Shorten `part_number` to `pn` - Shorten `manufacturer_part_number` to `mpn` - Show `manufacturer` and `mpn` in a single cell of the node - Replace `manufacturer` with `'MPN'`within the node if no manufacturer is specified. - Rearrange order of P/N fields within node `{pn} | {manufacturer}: {mpn}` --- src/wireviz/DataClasses.py | 10 +++---- src/wireviz/Harness.py | 53 +++++++++++++++++++++----------------- src/wireviz/wv_helper.py | 9 ++++++- tutorial/tutorial08.md | 1 + tutorial/tutorial08.yml | 27 +++++++++++++------ 5 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 2287e59..2f7cc14 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -11,8 +11,8 @@ from wireviz import wv_colors class Connector: name: str manufacturer: Optional[str] = None - manufacturer_part_number: Optional[str] = None - internal_part_number: Optional[str] = None + mpn: Optional[str] = None + pn: Optional[str] = None style: Optional[str] = None category: Optional[str] = None type: Optional[str] = None @@ -80,8 +80,8 @@ class Connector: class Cable: name: str manufacturer: Optional[Union[str, List[str]]] = None - manufacturer_part_number: Optional[Union[str, List[str]]] = None - internal_part_number: Optional[Union[str, List[str]]] = None + mpn: Optional[Union[str, List[str]]] = None + pn: Optional[Union[str, List[str]]] = None category: Optional[str] = None type: Optional[str] = None gauge: Optional[float] = None @@ -140,7 +140,7 @@ class Cable: self.wirecount = len(self.colors) # if lists of part numbers are provided check this is a bundle and that it matches the wirecount. - for idfield in [self.manufacturer, self.manufacturer_part_number, self.internal_part_number]: + for idfield in [self.manufacturer, self.mpn, self.pn]: if isinstance(idfield, list): if self.category == "bundle": # check the length diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index a3aa0ce..0c15b49 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -7,7 +7,8 @@ from wireviz import wv_colors, wv_helper from wireviz.wv_colors import get_color_hex from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, \ nested_html_table, flatten2d, index_if_list, html_line_breaks, \ - graphviz_line_breaks, remove_line_breaks, open_file_read, open_file_write + graphviz_line_breaks, remove_line_breaks, open_file_read, open_file_write, \ + manufacturer_info_field from collections import Counter from typing import List from pathlib import Path @@ -88,9 +89,8 @@ class Harness: for key, connector in self.connectors.items(): rows = [[connector.name if connector.show_name else None], - [connector.manufacturer, - f'MPN: {connector.manufacturer_part_number}' if connector.manufacturer_part_number else None, - f'IPN: {connector.internal_part_number}' if connector.internal_part_number else None], + [f'P/N: {connector.pn}' if connector.pn else None, + manufacturer_info_field(connector.manufacturer, connector.mpn)], [html_line_breaks(connector.type), html_line_breaks(connector.subtype), f'{connector.pincount}-pin' if connector.show_pincount else None, @@ -151,9 +151,9 @@ class Harness: elif cable.gauge_unit.upper() == 'AWG': awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' - identification = [cable.manufacturer if not isinstance(cable.manufacturer, list) else '', - f'MPN: {cable.manufacturer_part_number}' if (cable.manufacturer_part_number and not isinstance(cable.manufacturer_part_number, list)) else '', - f'IPN: {cable.internal_part_number}' if (cable.internal_part_number and not isinstance(cable.internal_part_number, list)) else ''] + identification = [f'P/N: {cable.pn}' if (cable.pn and not isinstance(cable.pn, list)) else '', + manufacturer_info_field(cable.manufacturer if not isinstance(cable.manufacturer, list) else None, + cable.mpn if not isinstance(cable.mpn, list) else None)] identification = list(filter(None, identification)) attributes = [html_line_breaks(cable.type) if cable.type else '', @@ -210,12 +210,12 @@ class Harness: if(cable.category == 'bundle'): # for bundles individual wires can have part information # create a list of wire parameters wireidentification = [] - if isinstance(cable.manufacturer, list): - wireidentification.append(cable.manufacturer[i - 1]) - if isinstance(cable.manufacturer_part_number, list): - wireidentification.append(f'MPN: {cable.manufacturer_part_number[i - 1]}') - if isinstance(cable.internal_part_number, list): - wireidentification.append(f'IPN: {cable.internal_part_number[i - 1]}') + if isinstance(cable.pn, list): + wireidentification.append(f'P/N: {cable.pn[i - 1]}') + manufacturer_info = manufacturer_info_field(cable.manufacturer[i - 1] if isinstance(cable.manufacturer, list) else None, + cable.mpn[i - 1] if isinstance(cable.mpn, list) else None) + if manufacturer_info: + wireidentification.append(manufacturer_info) # print parameters into a table row under the wire if(len(wireidentification) > 0): html = f'{html}' @@ -337,7 +337,7 @@ class Harness: bom_cables = [] bom_extra = [] # connectors - connector_group = lambda c: (c.type, c.subtype, c.pincount, c.manufacturer, c.manufacturer_part_number, c.internal_part_number) + connector_group = lambda c: (c.type, c.subtype, c.pincount, c.manufacturer, c.mpn, c.pn) for group in Counter([connector_group(v) for v in self.connectors.values()]): items = {k: v for k, v in self.connectors.items() if connector_group(v) == group} shared = next(iter(items.values())) @@ -349,14 +349,14 @@ class Harness: conn_color = f', {shared.color}' if shared.color else '' name = f'Connector{conn_type}{conn_subtype}{conn_pincount}{conn_color}' item = {'item': name, 'qty': len(designators), 'unit': '', 'designators': designators if shared.show_name else '', - 'manufacturer': shared.manufacturer, 'manufacturer part number': shared.manufacturer_part_number, 'internal part number': shared.internal_part_number} + 'manufacturer': shared.manufacturer, 'mpn': shared.mpn, 'pn': shared.pn} bom_connectors.append(item) bom_connectors = sorted(bom_connectors, key=lambda k: k['item']) # https://stackoverflow.com/a/73050 bom.extend(bom_connectors) # cables # TODO: If category can have other non-empty values than 'bundle', maybe it should be part of item name? # The category needs to be included in cable_group to keep the bundles excluded. - cable_group = lambda c: (c.category, c.type, c.gauge, c.gauge_unit, c.wirecount, c.shield, c.manufacturer, c.manufacturer_part_number, c.internal_part_number) + cable_group = lambda c: (c.category, c.type, c.gauge, c.gauge_unit, c.wirecount, c.shield, c.manufacturer, c.mpn, c.pn) for group in Counter([cable_group(v) for v in self.cables.values() if v.category != 'bundle']): items = {k: v for k, v in self.cables.items() if cable_group(v) == group} shared = next(iter(items.values())) @@ -368,7 +368,7 @@ class Harness: shield_name = ' shielded' if shared.shield else '' name = f'Cable{cable_type}, {shared.wirecount}{gauge_name}{shield_name}' item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators, - 'manufacturer': shared.manufacturer, 'manufacturer part number': shared.manufacturer_part_number, 'internal part number': shared.internal_part_number} + 'manufacturer': shared.manufacturer, 'mpn': shared.mpn, 'pn': shared.pn} bom_cables.append(item) # bundles (ignores wirecount) wirelist = [] @@ -379,10 +379,10 @@ class Harness: for index, color in enumerate(bundle.colors, 0): wirelist.append({'type': bundle.type, 'gauge': bundle.gauge, 'gauge_unit': bundle.gauge_unit, 'length': bundle.length, 'color': color, 'designator': bundle.name, 'manufacturer': index_if_list(bundle.manufacturer, index), - 'manufacturer part number': index_if_list(bundle.manufacturer_part_number, index), - 'internal part number': index_if_list(bundle.internal_part_number, index)}) + 'mpn': index_if_list(bundle.mpn, index), + 'pn': index_if_list(bundle.pn, index)}) # join similar wires from all the bundles to a single BOM item - wire_group = lambda w: (w.get('type', None), w['gauge'], w['gauge_unit'], w['color'], w['manufacturer'], w['manufacturer part number'], w['internal part number']) + wire_group = lambda w: (w.get('type', None), w['gauge'], w['gauge_unit'], w['color'], w['manufacturer'], w['mpn'], w['pn']) for group in Counter([wire_group(v) for v in wirelist]): items = [v for v in wirelist if wire_group(v) == group] shared = items[0] @@ -395,7 +395,7 @@ class Harness: gauge_color = f', {shared["color"]}' if 'color' in shared != '' else '' name = f'Wire{wire_type}{gauge_name}{gauge_color}' item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators, - 'manufacturer': shared['manufacturer'], 'manufacturer part number': shared['manufacturer part number'], 'internal part number': shared['internal part number']} + 'manufacturer': shared['manufacturer'], 'mpn': shared['mpn'], 'pn': shared['pn']} bom_cables.append(item) bom_cables = sorted(bom_cables, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050) bom.extend(bom_cables) @@ -405,7 +405,7 @@ class Harness: if isinstance(item.get('designators', None), List): item['designators'].sort() # sort designators if a list is provided item = {'item': name, 'qty': item.get('qty', None), 'unit': item.get('unit', None), 'designators': item.get('designators', None), - 'manufacturer': item.get('manufacturer', None), 'manufacturer part number': item.get('manufacturer_part_number', None), 'internal part number': item.get('internal_part_number', None)} + 'manufacturer': item.get('manufacturer', None), 'mpn': item.get('mpn', None), 'pn': item.get('pn', None)} bom_extra.append(item) bom_extra = sorted(bom_extra, key=lambda k: k['item']) bom.extend(bom_extra) @@ -414,11 +414,16 @@ class Harness: def bom_list(self): bom = self.bom() keys = ['item', 'qty', 'unit', 'designators'] # these BOM columns will always be included - for fieldname in ['manufacturer', 'manufacturer part number', 'internal part number']: # these optional BOM columns will only be included if at least one BOM item actually uses them + for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM item actually uses them if any(fieldname in x and x.get(fieldname, None) for x in bom): keys.append(fieldname) bom_list = [] - bom_list.append([k.capitalize() for k in keys]) # create header row with keys + # list of staic bom header names, headers not specified here are generated by capitilising the internal name + bom_headings = { + "pn": "P/N", + "mpn": "MPN" + } + bom_list.append([(bom_headings[k] if k in bom_headings else k.capitalize()) for k in keys]) # create header row with keys for item in bom: item_list = [item.get(key, '') for key in keys] # fill missing values with blanks item_list = [', '.join(subitem) if isinstance(subitem, List) else subitem for subitem in item_list] # convert any lists into comma separated strings diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 024c3ef..13b660c 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -117,4 +117,11 @@ def open_file_read(filename): return open(filename, 'r', encoding='UTF-8') def open_file_write(filename): - return open(filename, 'w', encoding='UTF-8') \ No newline at end of file + return open(filename, 'w', encoding='UTF-8') + + +def manufacturer_info_field(manufacturer, mpn): + if manufacturer or mpn: + return f'{manufacturer if manufacturer else "MPN"}{": " + str(mpn) if mpn else ""}' + else: + return None diff --git a/tutorial/tutorial08.md b/tutorial/tutorial08.md index 8369e90..1fc884e 100644 --- a/tutorial/tutorial08.md +++ b/tutorial/tutorial08.md @@ -3,3 +3,4 @@ * Part number information can be added to parts * Only provided fields will be added to the diagram and bom * Bundles can have part information specified by wire +* Additional parts can be added to the bom diff --git a/tutorial/tutorial08.yml b/tutorial/tutorial08.yml index 56a3800..2568f29 100644 --- a/tutorial/tutorial08.yml +++ b/tutorial/tutorial08.yml @@ -3,11 +3,11 @@ connectors: type: Molex KK 254 pincount: 4 subtype: female - manufacturer: Molex - manufacturer_part_number: 22013047 + manufacturer: Molex # set manufacter name + mpn: 22013047 # set manufacturer part number X2: <<: *template1 # reuse template - internal_part_number: CON4 + pn: CON4 # set an internal part number X3: <<: *template1 # reuse template @@ -18,16 +18,16 @@ cables: gauge: 0.25 mm2 color_code: IEC manufacturer: CablesCo - manufacturer_part_number: ABC123 - internal_part_number: CAB1 + mpn: ABC123 + pn: CAB1 W2: category: bundle length: 1 gauge: 0.25 mm2 colors: [YE, BK, BK, RD] - manufacturer: [WiresCo,WiresCo,WiresCo,WiresCo] - manufacturer_part_number: [W1-YE,W1-BK,W1-BK,W1-RD] - internal_part_number: [WIRE1,WIRE2,WIRE2,WIRE3] + manufacturer: [WiresCo,WiresCo,WiresCo,WiresCo] # set a manufacter per wire + mpn: [W1-YE,W1-BK,W1-BK,W1-RD] + pn: [WIRE1,WIRE2,WIRE2,WIRE3] connections: @@ -39,3 +39,14 @@ connections: - X1: [1-4] - W2: [1-4] - X3: [1-4] + +additional_bom_items: + - # define an additional item to add to the bill of materials + description: Label, pinout information + qty: 2 + designators: + - X2 + - X3 + manufacturer: generic company + mpn: Label1 + pn: Label-ID-1 From 1815a13cd6ad6432f20f80b891a8f2acefa5c009 Mon Sep 17 00:00:00 2001 From: KV Date: Sun, 26 Jul 2020 00:47:47 +0200 Subject: [PATCH 02/10] Make each shield wire uniform and allow cable.shield color As the spline shield wires were rendered as tinned wires with black borders, and the shield wires in cable nodes were rendered as a single (bottom) border, they didn't fit well together. Each shield wire is now rendered equally along the spline sections and in cable nodes. If cable.shield is true, they are rendered as thin black wires in the same way as before multi-colors were introduced. The new feature is that cable.shield is allowed to contain a two-letter color code to specify a colored shield wire with black borders. The shield wire thickness is not increased, even if the cable has some multi-colored wires that makes all other wires to increase. This fixes bug #125. --- src/wireviz/Harness.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 0c15b49..5952e0e 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -230,7 +230,14 @@ class Harness: for bla in p: html = html + f'' html = f'{html}' - html = f'{html}' + 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"' + else: + # shield is shown as a thin black wire + attributes = f'height="2" bgcolor="#000000" border="0"' + html = f'{html}' html = f'{html}' # spacer at the end @@ -248,8 +255,8 @@ class Harness: if isinstance(connection_color.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_color.via_port - 1], pad=pad) + ['#000000'])) else: # it's a shield connection - # shield is shown as a thin tinned wire - dot.attr('edge', color=':'.join(['#000000', wv_colors.get_color_hex('SN', pad=False)[0], '#000000'])) + # 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') if connection_color.from_port is not None: # connect to left from_port = f':p{connection_color.from_port}r' if self.connectors[connection_color.from_name].style != 'simple' else '' code_left_1 = f'{connection_color.from_name}{from_port}:e' From 3fa015cabde66b1067acbcaa20069df9b441d140 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Mon, 20 Jul 2020 12:50:07 +0200 Subject: [PATCH 03/10] Refactor build_examples.py - Use `pathlib.Path` instead of `os.path` - Fix order of files while building - Consolidate code for building demos, examples and tutorial - Change argument from `tutorials` to `tutorial` to remain consistent - Add some indentation in console output for better readability --- src/wireviz/build_examples.py | 161 ++++++++++++++++++---------------- src/wireviz/wv_helper.py | 2 + 2 files changed, 85 insertions(+), 78 deletions(-) diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index ee59ded..5633659 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -1,113 +1,118 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- + import argparse -import os import sys -from fnmatch import fnmatch +import os +from pathlib import Path -# noinspection PyUnresolvedReferences -from wv_helper import open_file_write, open_file_read - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +script_path = Path(__file__).absolute() +sys.path.insert(0, str(script_path.parent.parent)) # to find wireviz module from wireviz import wireviz +from wv_helper import open_file_write, open_file_read, open_file_append -examples_path = os.path.join('..','..','examples') -tutorials_path = os.path.join('..','..','tutorial') -demos_path = examples_path + +paths = {} +paths['examples'] = {'path': Path(script_path).parent.parent.parent / 'examples', + 'prefix': 'ex', + 'title': 'Example Gallery'} +paths['tutorial'] = {'path': Path(script_path).parent.parent.parent / 'tutorial', + 'prefix': 'tutorial', + 'title': 'WireViz Tutorial'} +paths['demos'] = {'path': Path(script_path).parent.parent.parent / 'examples', + 'prefix': 'demo'} readme = 'readme.md' -def build_demos(): - for fn in sorted(os.listdir(demos_path)): - if fnmatch(fn, "demo*.yml"): - abspath = os.path.join(demos_path, fn) +def build(dirname, build_readme, include_source, include_readme): + filename_list = [] + path = paths[dirname]['path'] + prefix = paths[dirname]['prefix'] + print(f'Building {path}') + # collect input YAML files + file_iterator = path.iterdir() + for entry in file_iterator: + if entry.is_file() and entry.match(f'{prefix}*.yml'): + filename_list.append(entry) + filename_list = sorted(filename_list) + # build files + if build_readme: + with open_file_write(path / 'readme.md') as out: + out.write(f'# {paths[dirname]["title"]}\n\n') + for yaml_file in filename_list: + print(f' {yaml_file}') + wireviz.parse_file(yaml_file) - print(abspath) - wireviz.parse_file(abspath) + if build_readme: + i = ''.join(filter(str.isdigit, yaml_file.stem)) -def build_examples(): - with open_file_write(os.path.join(examples_path, readme)) as file: - file.write('# Example gallery\n') - for fn in sorted(os.listdir(examples_path)): - if fnmatch(fn, "ex*.yml"): - i = ''.join(filter(str.isdigit, fn)) + if include_readme: + with open_file_append(path / readme) as out: + with open_file_read(path / f'{yaml_file.stem}.md') as info: + for line in info: + out.write(line.replace('## ', '## {} - '.format(i))) + out.write('\n\n') + else: + with open_file_append(path / readme) as out: + out.write(f'## Example {i}\n') - abspath = os.path.join(examples_path, fn) - outfile_name = abspath.split(".yml")[0] + with open_file_append(path / readme) as out: + if include_source: + with open_file_read(yaml_file) as src: + out.write('```yaml\n') + for line in src: + out.write(line) + out.write('```\n') + out.write('\n') + out.write(f'![]({yaml_file.stem}.png)\n\n') + out.write(f'[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n') - print(abspath) - wireviz.parse_file(abspath) - - file.write(f'## Example {i}\n') - file.write(f'![]({outfile_name}.png)\n\n') - file.write(f'[Source]({fn}) - [Bill of Materials]({outfile_name}.bom.tsv)\n\n\n') - -def build_tutorials(): - with open_file_write(os.path.join(tutorials_path, readme)) as file: - file.write('# WireViz Tutorial\n') - for fn in sorted(os.listdir(tutorials_path)): - if fnmatch(fn, "tutorial*.yml"): - i = ''.join(filter(str.isdigit, fn)) - abspath = os.path.join(tutorials_path, fn) - print(abspath) - - wireviz.parse_file(abspath) - - outfile_name = abspath.split(".yml")[0] - - with open_file_read(outfile_name + '.md') as info: - for line in info: - file.write(line.replace('## ', '## {} - '.format(i))) - file.write(f'\n[Source]({fn}):\n\n') - - with open_file_read(abspath) as src: - file.write('```yaml\n') - for line in src: - file.write(line) - file.write('```\n') - file.write('\n') - - file.write('\nOutput:\n\n'.format(i)) - - file.write(f'![](tutorial{outfile_name}.png)\n\n') - - file.write(f'[Bill of Materials](tutorial{outfile_name}.bom.tsv)\n\n\n') def clean_examples(): generated_extensions = ['.gv', '.png', '.svg', '.html', '.bom.tsv'] + for k, v in paths.items(): + filepath = v['path'] + print(f'Cleaning {filepath}') + # collect files to remove + filename_list = [] + file_iterator = filepath.iterdir() + for entry in file_iterator: + for ext in generated_extensions: + if entry.is_file() and entry.match(f'*{ext}'): + filename_list.append(entry) + filename_list.append(filepath / readme) - for filepath in [examples_path, demos_path, tutorials_path]: - print(filepath) - for file in sorted(os.listdir(filepath)): - if os.path.exists(os.path.join(filepath, file)): - if list(filter(file.endswith, generated_extensions)) or file == 'readme.md': - print('rm ' + os.path.join(filepath, file)) - os.remove(os.path.join(filepath, file)) + filename_list = sorted(filename_list) + # remove files + for filename in filename_list: + if filename.is_file(): + print(f' rm {filename}') + os.remove(filename) def parse_args(): - parser = argparse.ArgumentParser( - description='Wireviz Example Manager', - ) + parser = argparse.ArgumentParser(description='Wireviz Example Manager',) parser.add_argument('action', nargs='?', action='store', default='build') - parser.add_argument('-generate', nargs='*', choices=['examples', 'demos', 'tutorials'], default=['examples', 'demos', 'tutorials']) + parser.add_argument('-generate', nargs='*', choices=['examples', 'demos', 'tutorial'], default=['examples', 'demos', 'tutorial']) return parser.parse_args() + + def main(): args = parse_args() if args.action == 'build': - generate_types = { - 'examples': build_examples, - 'demos': build_demos, - 'tutorials': build_tutorials - } for gentype in args.generate: - if gentype in generate_types: - generate_types.get(gentype) () + if gentype == 'demos': + build('demos', build_readme = False, include_source = False, include_readme = False) + if gentype == 'examples': + build('examples', build_readme = True, include_source = False, include_readme = False) + if gentype == 'tutorial': + build('tutorial', build_readme = True, include_source = True, include_readme = True) elif args.action == 'clean': clean_examples() + if __name__ == '__main__': main() diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 13b660c..77309e5 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -119,6 +119,8 @@ def open_file_read(filename): def open_file_write(filename): return open(filename, 'w', encoding='UTF-8') +def open_file_append(filename): + return open(filename, 'a', encoding='UTF-8') def manufacturer_info_field(manufacturer, mpn): if manufacturer or mpn: From 4eedd94164b1aff8ab96312a58e97a4eefa2a3c0 Mon Sep 17 00:00:00 2001 From: KV Date: Mon, 20 Jul 2020 19:06:52 +0200 Subject: [PATCH 04/10] Add actions to compare against and restore from the latest commit Add new actions: - 'compare' action to compare generated files (except those generated by Graphviz) against the latest commit, and - 'restore' action to restore generated files from the latest commit. This is a squash rebase of these commits: - p 9ad3e13 Reduce code duplication by moving common code into a generic function - s d4feae6 Add action to restore generated files from git repository - s 64f6507 Add action to compare generated files against git repository - s 099c202 Simplify code --- src/wireviz/build_examples.py | 73 ++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index 5633659..5790e52 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -23,25 +23,29 @@ paths['tutorial'] = {'path': Path(script_path).parent.parent.parent / 'tutorial' paths['demos'] = {'path': Path(script_path).parent.parent.parent / 'examples', 'prefix': 'demo'} +input_extensions = ['.yml'] +generated_extensions = ['.gv', '.png', '.svg', '.html', '.bom.tsv'] +extensions_not_from_graphviz = [ext for ext in generated_extensions if ext[-1] == 'v'] readme = 'readme.md' +def collect_filenames(description, pathkey, ext_list, extrafile = None): + path = paths[pathkey]['path'] + patterns = [f"{paths[pathkey]['prefix']}*{ext}" for ext in ext_list] + if extrafile is not None: + patterns.append(extrafile) + print(f"{description} {path}") + return sorted([filename for pattern in patterns for filename in path.glob(pattern)]) + + def build(dirname, build_readme, include_source, include_readme): - filename_list = [] - path = paths[dirname]['path'] - prefix = paths[dirname]['prefix'] - print(f'Building {path}') - # collect input YAML files - file_iterator = path.iterdir() - for entry in file_iterator: - if entry.is_file() and entry.match(f'{prefix}*.yml'): - filename_list.append(entry) - filename_list = sorted(filename_list) # build files + path = paths[dirname]['path'] if build_readme: with open_file_write(path / 'readme.md') as out: out.write(f'# {paths[dirname]["title"]}\n\n') - for yaml_file in filename_list: + # collect and iterate input YAML files + for yaml_file in collect_filenames('Building', dirname, input_extensions): print(f' {yaml_file}') wireviz.parse_file(yaml_file) @@ -72,27 +76,38 @@ def build(dirname, build_readme, include_source, include_readme): def clean_examples(): - generated_extensions = ['.gv', '.png', '.svg', '.html', '.bom.tsv'] - for k, v in paths.items(): - filepath = v['path'] - print(f'Cleaning {filepath}') - # collect files to remove - filename_list = [] - file_iterator = filepath.iterdir() - for entry in file_iterator: - for ext in generated_extensions: - if entry.is_file() and entry.match(f'*{ext}'): - filename_list.append(entry) - filename_list.append(filepath / readme) - - filename_list = sorted(filename_list) - # remove files - for filename in filename_list: + for key in paths.keys(): + # collect and remove files + for filename in collect_filenames('Cleaning', key, generated_extensions, readme): if filename.is_file(): print(f' rm {filename}') os.remove(filename) +def compare_generated(include_from_graphviz = False): + compare_extensions = generated_extensions if include_from_graphviz else extensions_not_from_graphviz + for key in paths.keys(): + # collect and compare files + for filename in collect_filenames('Comparing', key, compare_extensions, readme): + cmd = f'git --no-pager diff {filename}' + print(f' {cmd}') + os.system(cmd) + + +def restore_generated(): + for key, value in paths.items(): + # collect input YAML files + 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.append(value['path'] / readme) + # restore files + for filename in filename_list: + cmd = f'git checkout -- {filename}' + print(f' {cmd}') + os.system(cmd) + + def parse_args(): parser = argparse.ArgumentParser(description='Wireviz Example Manager',) parser.add_argument('action', nargs='?', action='store', default='build') @@ -112,6 +127,10 @@ def main(): build('tutorial', build_readme = True, include_source = True, include_readme = True) elif args.action == 'clean': clean_examples() + elif args.action == 'compare': + compare_generated() + elif args.action == 'restore': + restore_generated() if __name__ == '__main__': From 476f85bcf516b1a0bea0bed19ab3df5e5c579a61 Mon Sep 17 00:00:00 2001 From: KV Date: Mon, 20 Jul 2020 20:48:37 +0200 Subject: [PATCH 05/10] Make all actions honor the optional argument -g or --group This make it possible to append '-g' or '--groups' followed by space separated group names to any CLI action command, and the set of generated files affected by the command will be limited to the selected groups ('examples', 'tutorial', and 'demos'). Default is all groups. A simple help text is added for each of the arguments (action and groups) to improve the autogenerated CLI help output. This is a squash rebase of these commits: - p ec29076 Make all actions honor the optional argument -generate - s e3ad11a Move open_file_append() outside the if to avoid re-open - s ba4b900 Avoid including readme in all file groups - s 1ca8bd1 Simplify code - s a9e7337 Rename some variables to better reflect their contents and relations - s 58a54b2 Move test to include readme inside collect_filenames() function - s f2a0db0 Improve status output by adding group name - s d3b299b Rename -generate option to -g/--groups and add argument help --- src/wireviz/build_examples.py | 92 ++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index 5790e52..0560756 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -13,56 +13,59 @@ from wireviz import wireviz from wv_helper import open_file_write, open_file_read, open_file_append -paths = {} -paths['examples'] = {'path': Path(script_path).parent.parent.parent / 'examples', +readme = 'readme.md' +groups = {} +groups['examples'] = {'path': Path(script_path).parent.parent.parent / 'examples', 'prefix': 'ex', + readme: [], # Include no files 'title': 'Example Gallery'} -paths['tutorial'] = {'path': Path(script_path).parent.parent.parent / 'tutorial', +groups['tutorial'] = {'path': Path(script_path).parent.parent.parent / 'tutorial', 'prefix': 'tutorial', + readme: ['md', 'yml'], # Include .md and .yml files 'title': 'WireViz Tutorial'} -paths['demos'] = {'path': Path(script_path).parent.parent.parent / 'examples', +groups['demos'] = {'path': Path(script_path).parent.parent.parent / 'examples', 'prefix': 'demo'} input_extensions = ['.yml'] generated_extensions = ['.gv', '.png', '.svg', '.html', '.bom.tsv'] extensions_not_from_graphviz = [ext for ext in generated_extensions if ext[-1] == 'v'] -readme = 'readme.md' -def collect_filenames(description, pathkey, ext_list, extrafile = None): - path = paths[pathkey]['path'] - patterns = [f"{paths[pathkey]['prefix']}*{ext}" for ext in ext_list] - if extrafile is not None: - patterns.append(extrafile) - print(f"{description} {path}") +def collect_filenames(description, groupkey, ext_list): + 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) + print(f"{description} {groupkey} in {path}") return sorted([filename for pattern in patterns for filename in path.glob(pattern)]) -def build(dirname, build_readme, include_source, include_readme): +def build_generated(groupkey): # build files - path = paths[dirname]['path'] + path = groups[groupkey]['path'] + build_readme = readme in groups[groupkey] if build_readme: - with open_file_write(path / 'readme.md') as out: - out.write(f'# {paths[dirname]["title"]}\n\n') + include_readme = 'md' in groups[groupkey][readme] + include_source = 'yml' in groups[groupkey][readme] + with open_file_write(path / readme) as out: + out.write(f'# {groups[groupkey]["title"]}\n\n') # collect and iterate input YAML files - for yaml_file in collect_filenames('Building', dirname, input_extensions): + for yaml_file in collect_filenames('Building', groupkey, input_extensions): print(f' {yaml_file}') wireviz.parse_file(yaml_file) if build_readme: i = ''.join(filter(str.isdigit, yaml_file.stem)) - if include_readme: - with open_file_append(path / readme) as out: - with open_file_read(path / f'{yaml_file.stem}.md') as info: + with open_file_append(path / readme) as out: + if include_readme: + with open_file_read(yaml_file.with_suffix('.md')) as info: for line in info: - out.write(line.replace('## ', '## {} - '.format(i))) + out.write(line.replace('## ', f'## {i} - ')) out.write('\n\n') - else: - with open_file_append(path / readme) as out: + else: out.write(f'## Example {i}\n') - with open_file_append(path / readme) as out: if include_source: with open_file_read(yaml_file) as src: out.write('```yaml\n') @@ -75,32 +78,33 @@ def build(dirname, build_readme, include_source, include_readme): out.write(f'[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n') -def clean_examples(): - for key in paths.keys(): +def clean_generated(groupkeys): + for key in groupkeys: # collect and remove files - for filename in collect_filenames('Cleaning', key, generated_extensions, readme): + for filename in collect_filenames('Cleaning', key, generated_extensions): if filename.is_file(): print(f' rm {filename}') os.remove(filename) -def compare_generated(include_from_graphviz = False): +def compare_generated(groupkeys, include_from_graphviz = False): compare_extensions = generated_extensions if include_from_graphviz else extensions_not_from_graphviz - for key in paths.keys(): + for key in groupkeys: # collect and compare files - for filename in collect_filenames('Comparing', key, compare_extensions, readme): + for filename in collect_filenames('Comparing', key, compare_extensions): cmd = f'git --no-pager diff {filename}' print(f' {cmd}') os.system(cmd) -def restore_generated(): - for key, value in paths.items(): +def restore_generated(groupkeys): + for key in groupkeys: # collect input YAML files 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.append(value['path'] / readme) + if readme in groups[key]: + filename_list.append(groups[key]['path'] / readme) # restore files for filename in filename_list: cmd = f'git checkout -- {filename}' @@ -110,27 +114,27 @@ def restore_generated(): def parse_args(): parser = argparse.ArgumentParser(description='Wireviz Example Manager',) - parser.add_argument('action', nargs='?', action='store', default='build') - parser.add_argument('-generate', nargs='*', choices=['examples', 'demos', 'tutorial'], default=['examples', 'demos', 'tutorial']) + parser.add_argument('action', nargs='?', action='store', + choices=['build','clean','compare','restore'], default='build', + help='what to do with the generated files (default: build)') + 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': - for gentype in args.generate: - if gentype == 'demos': - build('demos', build_readme = False, include_source = False, include_readme = False) - if gentype == 'examples': - build('examples', build_readme = True, include_source = False, include_readme = False) - if gentype == 'tutorial': - build('tutorial', build_readme = True, include_source = True, include_readme = True) + # TODO: Move this loop into the function for consistency? + for groupkey in args.groups: + build_generated(groupkey) elif args.action == 'clean': - clean_examples() + clean_generated(args.groups) elif args.action == 'compare': - compare_generated() + compare_generated(args.groups) elif args.action == 'restore': - restore_generated() + restore_generated(args.groups) if __name__ == '__main__': From 94ca9cc3588321cb6a67aa313fb2b9c4c6bcc9e8 Mon Sep 17 00:00:00 2001 From: KV Date: Thu, 23 Jul 2020 15:16:22 +0200 Subject: [PATCH 06/10] Restructure the group dict initialization By putting all value entries on separate lines with a trailing comma, it becomes easier to read the diff when later inserting or deleting the first or last value entry in any dict. --- src/wireviz/build_examples.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index 0560756..83c20d6 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -14,17 +14,24 @@ from wv_helper import open_file_write, open_file_read, open_file_append readme = 'readme.md' -groups = {} -groups['examples'] = {'path': Path(script_path).parent.parent.parent / 'examples', - 'prefix': 'ex', - readme: [], # Include no files - 'title': 'Example Gallery'} -groups['tutorial'] = {'path': Path(script_path).parent.parent.parent / 'tutorial', - 'prefix': 'tutorial', - readme: ['md', 'yml'], # Include .md and .yml files - 'title': 'WireViz Tutorial'} -groups['demos'] = {'path': Path(script_path).parent.parent.parent / 'examples', - 'prefix': 'demo'} +groups = { + 'examples': { + 'path': Path(script_path).parent.parent.parent / 'examples', + 'prefix': 'ex', + readme: [], # Include no files + 'title': 'Example Gallery', + }, + 'tutorial' : { + 'path': Path(script_path).parent.parent.parent / 'tutorial', + 'prefix': 'tutorial', + readme: ['md', 'yml'], # Include .md and .yml files + 'title': 'WireViz Tutorial', + }, + 'demos' : { + 'path': Path(script_path).parent.parent.parent / 'examples', + 'prefix': 'demo', + }, +} input_extensions = ['.yml'] generated_extensions = ['.gv', '.png', '.svg', '.html', '.bom.tsv'] From 378e13b9881319134da4d924d3e5156a9dea1bff Mon Sep 17 00:00:00 2001 From: KV Date: Fri, 24 Jul 2020 18:08:50 +0200 Subject: [PATCH 07/10] Move group loop into build_generated() for consistency Now, all action functions are called with a group list as argument. --- src/wireviz/build_examples.py | 69 +++++++++++++++++------------------ 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index 83c20d6..7c3496e 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -47,42 +47,43 @@ def collect_filenames(description, groupkey, ext_list): return sorted([filename for pattern in patterns for filename in path.glob(pattern)]) -def build_generated(groupkey): - # build files - path = groups[groupkey]['path'] - build_readme = readme in groups[groupkey] - if build_readme: - include_readme = 'md' in groups[groupkey][readme] - include_source = 'yml' in groups[groupkey][readme] - with open_file_write(path / readme) as out: - out.write(f'# {groups[groupkey]["title"]}\n\n') - # collect and iterate input YAML files - for yaml_file in collect_filenames('Building', groupkey, input_extensions): - print(f' {yaml_file}') - wireviz.parse_file(yaml_file) - +def build_generated(groupkeys): + for key in groupkeys: + # preparation + path = groups[key]['path'] + build_readme = readme in groups[key] if build_readme: - i = ''.join(filter(str.isdigit, yaml_file.stem)) + 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): + print(f' {yaml_file}') + wireviz.parse_file(yaml_file) - with open_file_append(path / readme) as out: - if include_readme: - 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') - else: - out.write(f'## Example {i}\n') + if build_readme: + i = ''.join(filter(str.isdigit, yaml_file.stem)) - if include_source: - with open_file_read(yaml_file) as src: - out.write('```yaml\n') - for line in src: - out.write(line) - out.write('```\n') - out.write('\n') + with open_file_append(path / readme) as out: + if include_readme: + 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') + else: + out.write(f'## Example {i}\n') - out.write(f'![]({yaml_file.stem}.png)\n\n') - out.write(f'[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n') + if include_source: + with open_file_read(yaml_file) as src: + out.write('```yaml\n') + for line in src: + out.write(line) + out.write('```\n') + out.write('\n') + + out.write(f'![]({yaml_file.stem}.png)\n\n') + out.write(f'[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n') def clean_generated(groupkeys): @@ -133,9 +134,7 @@ def parse_args(): def main(): args = parse_args() if args.action == 'build': - # TODO: Move this loop into the function for consistency? - for groupkey in args.groups: - build_generated(groupkey) + build_generated(args.groups) elif args.action == 'clean': clean_generated(args.groups) elif args.action == 'compare': From 21db0d509ed45162e9a46792a84ea0d094d138ab Mon Sep 17 00:00:00 2001 From: KV Date: Sun, 26 Jul 2020 00:22:19 +0200 Subject: [PATCH 08/10] Add CLI option -c that allows comparing Graphviz output also --- src/wireviz/build_examples.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index 7c3496e..01983c0 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -125,6 +125,8 @@ def parse_args(): parser.add_argument('action', nargs='?', action='store', choices=['build','clean','compare','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('-g', '--groups', nargs='+', choices=groups.keys(), default=groups.keys(), help='the groups of generated files (default: all)') @@ -138,7 +140,7 @@ def main(): elif args.action == 'clean': clean_generated(args.groups) elif args.action == 'compare': - compare_generated(args.groups) + compare_generated(args.groups, args.compare_graphviz_output) elif args.action == 'restore': restore_generated(args.groups) From 99ca1859ebdf957e329a45475d3725b09b8670af Mon Sep 17 00:00:00 2001 From: KV Date: Sun, 26 Jul 2020 20:12:45 +0200 Subject: [PATCH 09/10] Fix change requests from owner as descibed in PR #118 - Add double quotes around path string in `os.system()` call and status output to handle any spaces in the path. - Split the `generated_extensions` list into the two lists `extensions_not_containing_graphviz_output` and `extensions_containing_graphviz_output` for readability. --- src/wireviz/build_examples.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index 01983c0..10a4b1b 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -34,8 +34,9 @@ groups = { } input_extensions = ['.yml'] -generated_extensions = ['.gv', '.png', '.svg', '.html', '.bom.tsv'] -extensions_not_from_graphviz = [ext for ext in generated_extensions if ext[-1] == 'v'] +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): @@ -43,7 +44,7 @@ def collect_filenames(description, groupkey, ext_list): patterns = [f"{groups[groupkey]['prefix']}*{ext}" for ext in ext_list] if ext_list != input_extensions and readme in groups[groupkey]: patterns.append(readme) - print(f"{description} {groupkey} in {path}") + print(f'{description} {groupkey} in "{path}"') return sorted([filename for pattern in patterns for filename in path.glob(pattern)]) @@ -59,7 +60,7 @@ def build_generated(groupkeys): out.write(f'# {groups[key]["title"]}\n\n') # collect and iterate input YAML files for yaml_file in collect_filenames('Building', key, input_extensions): - print(f' {yaml_file}') + print(f' "{yaml_file}"') wireviz.parse_file(yaml_file) if build_readme: @@ -91,16 +92,16 @@ def clean_generated(groupkeys): # collect and remove files for filename in collect_filenames('Cleaning', key, generated_extensions): if filename.is_file(): - print(f' rm {filename}') + print(f' rm "{filename}"') os.remove(filename) -def compare_generated(groupkeys, include_from_graphviz = False): - compare_extensions = generated_extensions if include_from_graphviz else extensions_not_from_graphviz +def compare_generated(groupkeys, include_graphviz_output = False): + 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): - cmd = f'git --no-pager diff {filename}' + cmd = f'git --no-pager diff "{filename}"' print(f' {cmd}') os.system(cmd) @@ -115,7 +116,7 @@ def restore_generated(groupkeys): filename_list.append(groups[key]['path'] / readme) # restore files for filename in filename_list: - cmd = f'git checkout -- {filename}' + cmd = f'git checkout -- "{filename}"' print(f' {cmd}') os.system(cmd) From 23c17e66bf60ba55d33aff05ff12c5dfe24e318b Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Mon, 27 Jul 2020 10:21:00 +0200 Subject: [PATCH 10/10] Change wire padding behavior Addresses #131 --- src/wireviz/Harness.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 5952e0e..9cc6b96 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -139,6 +139,10 @@ class Harness: 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) + for _, cable in self.cables.items(): awg_fmt = '' @@ -187,11 +191,6 @@ class Harness: html = f'{html}
{bla}
 
' # conductor table - # determine if there are double- or triple-colored wires; - # if so, pad single-color wires to make all wires of equal thickness - colorlengths = list(map(len, cable.colors)) - pad = 4 in colorlengths or 6 in colorlengths - for i, connection_color in enumerate(cable.colors, 1): p = [] p.append(f'')