From 09b0996cd624f9c457daae3ac8ee79a7d405b7bf Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 20:19:31 +0100 Subject: [PATCH 01/30] Skip assignment and return expression directly --- src/wireviz/wv_bom.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index ac5b071d..3176c2d4 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -115,8 +115,7 @@ def generate_bom(harness): bom = sorted(bom, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050) # add an incrementing id to each bom item - bom = [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] - return bom + return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] def get_bom_index(harness, item, unit, manufacturer, mpn, pn): # Remove linebreaks and clean whitespace of values in search From 300fc5025fcbd893e99c38024be244e8cfa0cb6b Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 20:21:58 +0100 Subject: [PATCH 02/30] Simplify get_bom_index() parameters - Use the actual BOM as first parameter instead of the whole harness. - Use a whole AdditionalComponent as second parameter instead of each attribute separately. --- src/wireviz/wv_bom.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 3176c2d4..84426a5a 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -4,7 +4,7 @@ from typing import List, Union from collections import Counter -from wireviz.DataClasses import Connector, Cable +from wireviz.DataClasses import AdditionalComponent, Connector, Cable from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_helper import clean_whitespace @@ -15,7 +15,7 @@ def get_additional_component_table(harness, component: Union[Connector, Cable]) for extra in component.additional_components: qty = extra.qty * component.get_qty_multiplier(extra.qty_multiplier) if harness.mini_bom_mode: - id = get_bom_index(harness, extra.description, extra.unit, extra.manufacturer, extra.mpn, extra.pn) + id = get_bom_index(harness.bom(), extra) rows.append(component_table_entry(f'#{id} ({extra.type.rstrip()})', qty, extra.unit)) else: rows.append(component_table_entry(extra.description, qty, extra.unit, extra.pn, extra.manufacturer, extra.mpn)) @@ -117,10 +117,10 @@ def generate_bom(harness): # add an incrementing id to each bom item return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] -def get_bom_index(harness, item, unit, manufacturer, mpn, pn): +def get_bom_index(bom: List[dict], extra: AdditionalComponent) -> int: # Remove linebreaks and clean whitespace of values in search - target = tuple(clean_whitespace(v) for v in (item, unit, manufacturer, mpn, pn)) - for entry in harness.bom(): + target = tuple(clean_whitespace(v) for v in (extra.description, extra.unit, extra.manufacturer, extra.mpn, extra.pn)) + for entry in bom: if (entry['item'], entry['unit'], entry['manufacturer'], entry['mpn'], entry['pn']) == target: return entry['id'] return None From a43271c018406676c8b5dc1a096cbff7248ba645 Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 20:51:50 +0100 Subject: [PATCH 03/30] Use the same lambda in get_bom_index() as for deduplicating BOM Move the lambda declaration out of the function scope for common access from two different functions. --- src/wireviz/wv_bom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 84426a5a..d9a1f259 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -36,6 +36,8 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dic }) return(bom_entries) +bom_types_group = lambda bt: (bt['item'], bt['unit'], bt['manufacturer'], bt['mpn'], bt['pn']) + def generate_bom(harness): from wireviz.Harness import Harness # Local import to avoid circular imports bom_entries = [] @@ -97,7 +99,6 @@ def generate_bom(harness): # deduplicate bom bom = [] - bom_types_group = lambda bt: (bt['item'], bt['unit'], bt['manufacturer'], bt['mpn'], bt['pn']) for group in Counter([bom_types_group(v) for v in bom_entries]): group_entries = [v for v in bom_entries if bom_types_group(v) == group] designators = [] @@ -121,7 +122,7 @@ def get_bom_index(bom: List[dict], extra: AdditionalComponent) -> int: # Remove linebreaks and clean whitespace of values in search target = tuple(clean_whitespace(v) for v in (extra.description, extra.unit, extra.manufacturer, extra.mpn, extra.pn)) for entry in bom: - if (entry['item'], entry['unit'], entry['manufacturer'], entry['mpn'], entry['pn']) == target: + if bom_types_group(entry) == target: return entry['id'] return None From f255548df571ea0467c1b098ebfc95f959050dab Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 21:06:22 +0100 Subject: [PATCH 04/30] Convert dataclass object to dict to use the same lambda --- src/wireviz/wv_bom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index d9a1f259..641c434e 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -1,8 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from typing import List, Union from collections import Counter +from dataclasses import asdict +from typing import List, Union from wireviz.DataClasses import AdditionalComponent, Connector, Cable from wireviz.wv_gv_html import html_line_breaks @@ -120,7 +121,7 @@ def generate_bom(harness): def get_bom_index(bom: List[dict], extra: AdditionalComponent) -> int: # Remove linebreaks and clean whitespace of values in search - target = tuple(clean_whitespace(v) for v in (extra.description, extra.unit, extra.manufacturer, extra.mpn, extra.pn)) + target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(extra), 'item': extra.description})) for entry in bom: if bom_types_group(entry) == target: return entry['id'] From 4a77d86fc4faaacf417f7da0d8a04f206a094cf9 Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 21:26:15 +0100 Subject: [PATCH 05/30] Redefine the common lambda to an ordinary function --- src/wireviz/wv_bom.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 641c434e..9e451aea 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -3,7 +3,7 @@ from collections import Counter from dataclasses import asdict -from typing import List, Union +from typing import List, Tuple, Union from wireviz.DataClasses import AdditionalComponent, Connector, Cable from wireviz.wv_gv_html import html_line_breaks @@ -37,7 +37,9 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dic }) return(bom_entries) -bom_types_group = lambda bt: (bt['item'], bt['unit'], bt['manufacturer'], bt['mpn'], bt['pn']) +def bom_types_group(entry: dict) -> Tuple[str, ...]: + """Return a tuple of values from the dict that must be equal to join BOM entries.""" + return tuple(entry.get(key) for key in ('item', 'unit', 'manufacturer', 'mpn', 'pn')) def generate_bom(harness): from wireviz.Harness import Harness # Local import to avoid circular imports From 186cecf7eab835f3c2e87a2b57f01463f0f88bb9 Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 21:28:33 +0100 Subject: [PATCH 06/30] Simplify BOM header row logic --- src/wireviz/wv_bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 9e451aea..e9fc4785 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -140,7 +140,7 @@ def bom_list(bom): "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 + bom_list.append([bom_headings.get(k, 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 From 6984ca181dd5e30650dc461a3f0deebe65676834 Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 21:37:31 +0100 Subject: [PATCH 07/30] Simplify collecting designators for a joined BOM entry Assign input designators once to a temporary variable for easy reusage. --- src/wireviz/wv_bom.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index e9fc4785..c27a2045 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -106,11 +106,8 @@ def generate_bom(harness): group_entries = [v for v in bom_entries if bom_types_group(v) == group] designators = [] for group_entry in group_entries: - if group_entry.get('designators'): - if isinstance(group_entry['designators'], List): - designators.extend(group_entry['designators']) - else: - designators.append(group_entry['designators']) + d = group_entry.get('designators') + designators.extend(d if isinstance(d, List) else [d] if d else []) designators = list(dict.fromkeys(designators)) # remove duplicates designators.sort() total_qty = sum(entry['qty'] for entry in group_entries) From 731add070faf3005d58a32db7171e41d9e26ca5b Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 14 Nov 2020 21:43:57 +0100 Subject: [PATCH 08/30] Simplify deduplication and sorting of collected designators --- src/wireviz/wv_bom.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index c27a2045..c1789633 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -108,10 +108,8 @@ def generate_bom(harness): for group_entry in group_entries: d = group_entry.get('designators') designators.extend(d if isinstance(d, List) else [d] if d else []) - designators = list(dict.fromkeys(designators)) # remove duplicates - designators.sort() total_qty = sum(entry['qty'] for entry in group_entries) - bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': designators}) + bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': sorted(set(designators))}) bom = sorted(bom, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050) From 4e69372469679f9e1b3d36dce1d3d33a2353f592 Mon Sep 17 00:00:00 2001 From: KV Date: Mon, 16 Nov 2020 19:59:52 +0100 Subject: [PATCH 09/30] Remove parentheses around return expressions https://stackoverflow.com/questions/4978567/should-a-return-statement-have-parentheses --- src/wireviz/wv_bom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index c1789633..03286618 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -20,7 +20,7 @@ def get_additional_component_table(harness, component: Union[Connector, Cable]) rows.append(component_table_entry(f'#{id} ({extra.type.rstrip()})', qty, extra.unit)) else: rows.append(component_table_entry(extra.description, qty, extra.unit, extra.pn, extra.manufacturer, extra.mpn)) - return(rows) + return rows def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dict]: bom_entries = [] @@ -35,7 +35,7 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dic 'pn': part.pn, 'designators': component.name if component.show_name else None }) - return(bom_entries) + return bom_entries def bom_types_group(entry: dict) -> Tuple[str, ...]: """Return a tuple of values from the dict that must be equal to join BOM entries.""" From 467fa74388a9e3ada41850e0d53149705b4786f9 Mon Sep 17 00:00:00 2001 From: KV Date: Fri, 27 Nov 2020 00:37:03 +0100 Subject: [PATCH 10/30] Move out code from inner loop into helper functions --- src/wireviz/wv_bom.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 03286618..223de38c 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -3,7 +3,7 @@ from collections import Counter from dataclasses import asdict -from typing import List, Tuple, Union +from typing import Any, List, Tuple, Union from wireviz.DataClasses import AdditionalComponent, Connector, Cable from wireviz.wv_gv_html import html_line_breaks @@ -106,8 +106,7 @@ def generate_bom(harness): group_entries = [v for v in bom_entries if bom_types_group(v) == group] designators = [] for group_entry in group_entries: - d = group_entry.get('designators') - designators.extend(d if isinstance(d, List) else [d] if d else []) + designators.extend(make_list(group_entry.get('designators'))) total_qty = sum(entry['qty'] for entry in group_entries) bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': sorted(set(designators))}) @@ -129,19 +128,13 @@ def bom_list(bom): 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(entry.get(fieldname) for entry in bom): keys.append(fieldname) - bom_list = [] # 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.get(k, 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 - item_list = ['' if subitem is None else subitem for subitem in item_list] # if a field is missing for some (but not all) BOM items - bom_list.append(item_list) - return bom_list + 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, qty, unit=None, pn=None, manufacturer=None, mpn=None): output = f'{qty}' @@ -174,3 +167,11 @@ def manufacturer_info_field(manufacturer, mpn): # Return the value indexed if it is a list, or simply the value otherwise. def index_if_list(value, index): 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)) From 8f6a28e10104891488d8f527a424c22cce4f70f2 Mon Sep 17 00:00:00 2001 From: KV Date: Fri, 27 Nov 2020 01:16:26 +0100 Subject: [PATCH 11/30] Move BOM sorting above grouping to use groupby() - Use one common entry loop to consume iterator only once. - Use same key function for sort() and groupby(), except replace None with empty string when sorting. --- src/wireviz/wv_bom.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 223de38c..d2546767 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from collections import Counter from dataclasses import asdict +from itertools import groupby from typing import Any, List, Tuple, Union from wireviz.DataClasses import AdditionalComponent, Connector, Cable @@ -100,17 +100,20 @@ def generate_bom(harness): # 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] + # Sort entries to prepare grouping on the same key function. + bom_entries.sort(key=lambda entry: tuple(attr or '' for attr in bom_types_group(entry))) + # deduplicate bom bom = [] - for group in Counter([bom_types_group(v) for v in bom_entries]): - group_entries = [v for v in bom_entries if bom_types_group(v) == group] + for _, group in groupby(bom_entries, bom_types_group): + last_entry = None + total_qty = 0 designators = [] - for group_entry in group_entries: + for group_entry in group: designators.extend(make_list(group_entry.get('designators'))) - total_qty = sum(entry['qty'] for entry in group_entries) - bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': sorted(set(designators))}) - - bom = sorted(bom, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050) + total_qty += group_entry['qty'] + last_entry = group_entry + bom.append({**last_entry, 'qty': round(total_qty, 3), 'designators': sorted(set(designators))}) # add an incrementing id to each bom item return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] From f65243a9f0ca7261340032c3bacd13d99dc6593a Mon Sep 17 00:00:00 2001 From: KV Date: Fri, 27 Nov 2020 21:15:59 +0100 Subject: [PATCH 12/30] Make the BOM grouping function return string tuple for sorting --- src/wireviz/wv_bom.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index d2546767..d21b96ef 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -38,8 +38,8 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dic return bom_entries def bom_types_group(entry: dict) -> Tuple[str, ...]: - """Return a tuple of values from the dict that must be equal to join BOM entries.""" - return tuple(entry.get(key) for key in ('item', 'unit', 'manufacturer', 'mpn', 'pn')) + """Return a tuple of string values from the dict that must be equal to join BOM entries.""" + return tuple(make_str(entry.get(key)) for key in ('item', 'unit', 'manufacturer', 'mpn', 'pn')) def generate_bom(harness): from wireviz.Harness import Harness # Local import to avoid circular imports @@ -100,12 +100,9 @@ def generate_bom(harness): # 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] - # Sort entries to prepare grouping on the same key function. - bom_entries.sort(key=lambda entry: tuple(attr or '' for attr in bom_types_group(entry))) - # deduplicate bom bom = [] - for _, group in groupby(bom_entries, bom_types_group): + for _, group in groupby(sorted(bom_entries, key=bom_types_group), key=bom_types_group): last_entry = None total_qty = 0 designators = [] From daced29a7790f2a679d78681cbfd13588502d32c Mon Sep 17 00:00:00 2001 From: KV Date: Fri, 27 Nov 2020 22:20:45 +0100 Subject: [PATCH 13/30] Use a generator expressions and raise exception if failing Seems to be the most popular search alternative: https://stackoverflow.com/questions/8653516/python-list-of-dictionaries-search Raising StopIteration if not found is better than returning None to detect such an internal error more easily. --- src/wireviz/wv_bom.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index d21b96ef..b7dda90c 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -116,12 +116,10 @@ def generate_bom(harness): return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] def get_bom_index(bom: List[dict], extra: AdditionalComponent) -> int: + """Return id of BOM entry or raise StopIteration if not found.""" # Remove linebreaks and clean whitespace of values in search target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(extra), 'item': extra.description})) - for entry in bom: - if bom_types_group(entry) == target: - return entry['id'] - return None + return next(entry['id'] for entry in bom if bom_types_group(entry) == target) def bom_list(bom): keys = ['id', 'item', 'qty', 'unit', 'designators'] # these BOM columns will always be included From 97d537f23fdfe2e4757583910742a5aa27a700c4 Mon Sep 17 00:00:00 2001 From: KV Date: Mon, 30 Nov 2020 18:10:24 +0100 Subject: [PATCH 14/30] Replace accumulation loop with sum expressions Make a list from the group iterator for reusage in sum expressions and to pick first group entry. The expected group sizes are very small, so performance loss by creating a temporary list should be neglectable. Alternativly, itertools.tee(group, 3) could be called to triplicate the iterator, but it was not chosen for readability reasons. --- src/wireviz/wv_bom.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index b7dda90c..dbd5f798 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -103,14 +103,10 @@ def generate_bom(harness): # deduplicate bom bom = [] for _, group in groupby(sorted(bom_entries, key=bom_types_group), key=bom_types_group): - last_entry = None - total_qty = 0 - designators = [] - for group_entry in group: - designators.extend(make_list(group_entry.get('designators'))) - total_qty += group_entry['qty'] - last_entry = group_entry - bom.append({**last_entry, 'qty': round(total_qty, 3), 'designators': sorted(set(designators))}) + group_entries = list(group) + designators = sum((make_list(entry.get('designators')) for entry in group_entries), []) + total_qty = sum(entry['qty'] 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 item return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] From d907bb9089ad4c6cfde1d6c88eed77098d1f3d6a Mon Sep 17 00:00:00 2001 From: KV Date: Mon, 30 Nov 2020 19:31:08 +0100 Subject: [PATCH 15/30] Add function type hints and doc strings --- src/wireviz/wv_bom.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index dbd5f798..66f66454 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -3,13 +3,14 @@ from dataclasses import asdict from itertools import groupby -from typing import Any, List, Tuple, Union +from typing import Any, List, Optional, Tuple, Union from wireviz.DataClasses import AdditionalComponent, Connector, Cable from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_helper import clean_whitespace -def get_additional_component_table(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"]) @@ -23,6 +24,7 @@ def get_additional_component_table(harness, component: Union[Connector, Cable]) return rows def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dict]: + """Return a list of BOM entries with additional components.""" bom_entries = [] for part in component.additional_components: qty = part.qty * component.get_qty_multiplier(part.qty_multiplier) @@ -41,7 +43,8 @@ def bom_types_group(entry: dict) -> Tuple[str, ...]: """Return a tuple of string values from the dict that must be equal to join BOM entries.""" return tuple(make_str(entry.get(key)) for key in ('item', 'unit', 'manufacturer', 'mpn', 'pn')) -def generate_bom(harness): +def generate_bom(harness: "Harness") -> List[dict]: + """Return a list of BOM entries generated from the harness.""" from wireviz.Harness import Harness # Local import to avoid circular imports bom_entries = [] # connectors @@ -117,7 +120,8 @@ def get_bom_index(bom: List[dict], extra: AdditionalComponent) -> int: target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(extra), 'item': extra.description})) return next(entry['id'] for entry in bom if bom_types_group(entry) == target) -def bom_list(bom): +def bom_list(bom: List[dict]) -> List[List[str]]: + """Return list of BOM rows as lists of column strings with headings in top row.""" keys = ['id', 'item', 'qty', 'unit', 'designators'] # these BOM columns will always be included 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(entry.get(fieldname) for entry in bom): @@ -130,7 +134,15 @@ def bom_list(bom): 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, qty, unit=None, pn=None, manufacturer=None, mpn=None): +def component_table_entry( + type: str, + qty: Union[int, float], + unit: Optional[str] = None, + pn: Optional[str] = None, + manufacturer: Optional[str] = None, + mpn: Optional[str] = None, + ) -> str: + """Return a diagram node table row string with an additional component.""" output = f'{qty}' if unit: output += f' {unit}' @@ -152,14 +164,15 @@ def component_table_entry(type, qty, unit=None, pn=None, manufacturer=None, mpn= {output} ''' -def manufacturer_info_field(manufacturer, mpn): +def manufacturer_info_field(manufacturer: Optional[str], mpn: Optional[str]) -> Optional[str]: + """Return the manufacturer and/or the mpn in one single string or None otherwise.""" if manufacturer or mpn: return f'{manufacturer if manufacturer else "MPN"}{": " + str(mpn) if mpn else ""}' else: return None -# Return the value indexed if it is a list, or simply the value otherwise. -def index_if_list(value, index): +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: From b6c7f806a1b94cd205f2d4cc1287486874f56063 Mon Sep 17 00:00:00 2001 From: KV Date: Tue, 1 Dec 2020 16:54:32 +0100 Subject: [PATCH 16/30] Add BOMEntry type alias This type alias describes the possible types of keys and values in the dict representing a BOM entry. --- src/wireviz/wv_bom.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 66f66454..985084fe 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -3,12 +3,14 @@ from dataclasses import asdict from itertools import groupby -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from wireviz.DataClasses import AdditionalComponent, Connector, Cable from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_helper import clean_whitespace +BOMEntry = Dict[str, Union[str, int, float, List[str], None]] + 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 = [] @@ -23,7 +25,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto rows.append(component_table_entry(extra.description, qty, extra.unit, extra.pn, extra.manufacturer, extra.mpn)) return rows -def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dict]: +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: @@ -39,11 +41,11 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dic }) return bom_entries -def bom_types_group(entry: dict) -> Tuple[str, ...]: +def bom_types_group(entry: BOMEntry) -> Tuple[str, ...]: """Return a tuple of string values from the dict that must be equal to join BOM entries.""" return tuple(make_str(entry.get(key)) for key in ('item', 'unit', 'manufacturer', 'mpn', 'pn')) -def generate_bom(harness: "Harness") -> List[dict]: +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 = [] @@ -114,13 +116,13 @@ def generate_bom(harness: "Harness") -> List[dict]: # add an incrementing id to each bom item return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] -def get_bom_index(bom: List[dict], extra: AdditionalComponent) -> int: +def get_bom_index(bom: List[BOMEntry], extra: AdditionalComponent) -> int: """Return id of BOM entry or raise StopIteration if not found.""" # Remove linebreaks and clean whitespace of values in search target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(extra), 'item': extra.description})) return next(entry['id'] for entry in bom if bom_types_group(entry) == target) -def bom_list(bom: List[dict]) -> List[List[str]]: +def bom_list(bom: List[BOMEntry]) -> List[List[str]]: """Return list of BOM rows as lists of column strings with headings in top row.""" keys = ['id', 'item', 'qty', 'unit', 'designators'] # these BOM columns will always be included for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM item actually uses them From 07a6e956eec2c9ce82a4df1737289cb1fba68865 Mon Sep 17 00:00:00 2001 From: KV Date: Sun, 3 Jan 2021 05:29:30 +0100 Subject: [PATCH 17/30] Rename extra variable to part for consistency --- src/wireviz/wv_bom.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 985084fe..17f090a2 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -16,13 +16,13 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto rows = [] if component.additional_components: rows.append(["Additional components"]) - for extra in component.additional_components: - qty = extra.qty * component.get_qty_multiplier(extra.qty_multiplier) + for part in component.additional_components: + qty = part.qty * component.get_qty_multiplier(part.qty_multiplier) if harness.mini_bom_mode: - id = get_bom_index(harness.bom(), extra) - rows.append(component_table_entry(f'#{id} ({extra.type.rstrip()})', qty, extra.unit)) + id = get_bom_index(harness.bom(), part) + rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', qty, part.unit)) else: - rows.append(component_table_entry(extra.description, qty, extra.unit, extra.pn, extra.manufacturer, extra.mpn)) + rows.append(component_table_entry(part.description, qty, part.unit, part.pn, part.manufacturer, part.mpn)) return rows def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]: @@ -116,10 +116,10 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: # add an incrementing id to each bom item return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] -def get_bom_index(bom: List[BOMEntry], extra: AdditionalComponent) -> int: +def get_bom_index(bom: List[BOMEntry], part: AdditionalComponent) -> int: """Return id of BOM entry or raise StopIteration if not found.""" # Remove linebreaks and clean whitespace of values in search - target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(extra), 'item': extra.description})) + target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(part), 'item': part.description})) return next(entry['id'] for entry in bom if bom_types_group(entry) == target) def bom_list(bom: List[BOMEntry]) -> List[List[str]]: From 63fbbe9e6f13bfc30e8e8acc73dba5dfe4664e66 Mon Sep 17 00:00:00 2001 From: KV Date: Sun, 3 Jan 2021 05:51:02 +0100 Subject: [PATCH 18/30] Build output string in one big expression Build output string in component_table_entry() as the similar strings in generate_bom(). Repeating a couple of minor if-expressions is small cost to obtain a more compact and readable main expression. --- src/wireviz/wv_bom.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 17f090a2..cb9a76a9 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -145,25 +145,18 @@ def component_table_entry( mpn: Optional[str] = None, ) -> str: """Return a diagram node table row string with an additional component.""" - output = f'{qty}' - if unit: - output += f' {unit}' - output += f' x {type}' - # print an extra line with part and manufacturer information if provided manufacturer_str = manufacturer_info_field(manufacturer, mpn) - if pn or manufacturer_str: - output += '
' - if pn: - output += f'P/N: {pn}' - if manufacturer_str: - output += ', ' - if manufacturer_str: - output += manufacturer_str - output = html_line_breaks(output) + output = (f'{qty}' + + (f' {unit}' if unit else '') + + f' x {type}' + + ('
' if pn or manufacturer_str else '') + + (f'P/N: {pn}' if pn else '') + + (', ' if pn and manufacturer_str else '') + + (manufacturer_str or '')) # 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''' - +
{output}{html_line_breaks(output)}
''' def manufacturer_info_field(manufacturer: Optional[str], mpn: Optional[str]) -> Optional[str]: From c87161e66060bea0ede012acd181c7de38eab56b Mon Sep 17 00:00:00 2001 From: KV Date: Tue, 5 Jan 2021 04:12:05 +0100 Subject: [PATCH 19/30] Move default qty value=1 to BOM deduplication --- src/wireviz/wv_bom.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index cb9a76a9..56cca29a 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -58,7 +58,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f', {connector.pincount} pins' if connector.show_pincount else '') + (f', {connector.color}' if connector.color else '')) bom_entries.append({ - 'item': description, 'qty': 1, 'unit': None, 'designators': connector.name if connector.show_name else None, + 'item': description, 'designators': connector.name if connector.show_name else None, 'manufacturer': connector.manufacturer, 'mpn': connector.mpn, 'pn': connector.pn }) @@ -96,11 +96,8 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: # add cable/bundles aditional components to bom bom_entries.extend(get_additional_component_bom(cable)) - for item in harness.additional_bom_items: - bom_entries.append({ - 'item': item.get('description', ''), 'qty': item.get('qty', 1), 'unit': item.get('unit'), 'designators': item.get('designators'), - 'manufacturer': item.get('manufacturer'), 'mpn': item.get('mpn'), 'pn': item.get('pn') - }) + # TODO: Simplify this by renaming the 'item' key to 'description' in all BOMEntry dicts. + bom_entries.extend([{k.replace('description', 'item'): v for k, v in entry.items()} for entry in 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] @@ -110,7 +107,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: for _, group in groupby(sorted(bom_entries, key=bom_types_group), key=bom_types_group): group_entries = list(group) designators = sum((make_list(entry.get('designators')) for entry in group_entries), []) - total_qty = sum(entry['qty'] 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 item From b086f2cae0f5db4356408f7913f65d96d2942442 Mon Sep 17 00:00:00 2001 From: KV Date: Tue, 5 Jan 2021 04:20:44 +0100 Subject: [PATCH 20/30] Eliminate local variable --- src/wireviz/wv_bom.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 56cca29a..b1acd587 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -29,10 +29,9 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM """Return a list of BOM entries with additional components.""" bom_entries = [] for part in component.additional_components: - qty = part.qty * component.get_qty_multiplier(part.qty_multiplier) bom_entries.append({ 'item': part.description, - 'qty': qty, + 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), 'unit': part.unit, 'manufacturer': part.manufacturer, 'mpn': part.mpn, From a1fb826e67524933900807c687366c11dd778b92 Mon Sep 17 00:00:00 2001 From: KV Date: Wed, 6 Jan 2021 22:53:33 +0100 Subject: [PATCH 21/30] Rename the 'item' key to 'description' in all BOMEntry dicts This way, both BOM and harness.additional_bom_items uses the same set of keys in their dict entries. This was originally suggested in a #115 review, but had too many issues to be implemented then. --- src/wireviz/wv_bom.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index b1acd587..0be35592 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -30,7 +30,7 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM bom_entries = [] for part in component.additional_components: bom_entries.append({ - 'item': part.description, + 'description': part.description, 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), 'unit': part.unit, 'manufacturer': part.manufacturer, @@ -42,7 +42,7 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM def bom_types_group(entry: BOMEntry) -> Tuple[str, ...]: """Return a tuple of string values from the dict that must be equal to join BOM entries.""" - return tuple(make_str(entry.get(key)) for key in ('item', 'unit', 'manufacturer', 'mpn', 'pn')) + return tuple(make_str(entry.get(key)) for key in ('description', 'unit', 'manufacturer', 'mpn', 'pn')) def generate_bom(harness: "Harness") -> List[BOMEntry]: """Return a list of BOM entries generated from the harness.""" @@ -57,7 +57,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f', {connector.pincount} pins' if connector.show_pincount else '') + (f', {connector.color}' if connector.color else '')) bom_entries.append({ - 'item': description, 'designators': connector.name if connector.show_name else None, + 'description': description, 'designators': connector.name if connector.show_name else None, 'manufacturer': connector.manufacturer, 'mpn': connector.mpn, 'pn': connector.pn }) @@ -65,7 +65,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: bom_entries.extend(get_additional_component_bom(connector)) # cables - # TODO: If category can have other non-empty values than 'bundle', maybe it should be part of item name? + # 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': @@ -76,7 +76,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires') + (' shielded' if cable.shield else '')) bom_entries.append({ - 'item': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, + 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, 'manufacturer': cable.manufacturer, 'mpn': cable.mpn, 'pn': cable.pn }) else: @@ -87,7 +87,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '') + (f', {color}' if color else '')) bom_entries.append({ - 'item': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, + 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, 'manufacturer': index_if_list(cable.manufacturer, index), 'mpn': index_if_list(cable.mpn, index), 'pn': index_if_list(cable.pn, index) }) @@ -95,8 +95,8 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: # add cable/bundles aditional components to bom bom_entries.extend(get_additional_component_bom(cable)) - # TODO: Simplify this by renaming the 'item' key to 'description' in all BOMEntry dicts. - bom_entries.extend([{k.replace('description', 'item'): v for k, v in entry.items()} for entry in harness.additional_bom_items]) + # add harness aditional components to bom directly, as they both are 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] @@ -109,23 +109,24 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: 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 item + # add an incrementing id to each bom entry return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] def get_bom_index(bom: List[BOMEntry], part: AdditionalComponent) -> int: """Return id of BOM entry or raise StopIteration if not found.""" # Remove linebreaks and clean whitespace of values in search - target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(part), 'item': part.description})) + target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(part), 'description': part.description})) return next(entry['id'] for entry in bom if bom_types_group(entry) == 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 = ['id', 'item', 'qty', 'unit', 'designators'] # these BOM columns will always be included - for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM item actually uses them + keys = ['id', 'description', 'qty', 'unit', 'designators'] # these BOM columns will always be included + for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM entry actually uses them if any(entry.get(fieldname) for entry in bom): keys.append(fieldname) # list of staic bom header names, headers not specified here are generated by capitilising the internal name bom_headings = { + "description": "Item", # TODO: Remove this line to use 'Description' in BOM heading. "pn": "P/N", "mpn": "MPN" } From 3b89fd99b7c389b5e7623bee20fd3675739fd0fd Mon Sep 17 00:00:00 2001 From: KV Date: Wed, 6 Jan 2021 23:58:30 +0100 Subject: [PATCH 22/30] Move repeated code into new optional_fields() function --- src/wireviz/wv_bom.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 0be35592..5ba6e094 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -9,7 +9,12 @@ from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_helper import clean_whitespace -BOMEntry = Dict[str, Union[str, int, float, List[str], None]] +BOMColumn = str # = Literal['id', 'description', 'qty', 'unit', 'designators', 'pn', 'manufacturer', 'mpn'] +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.""" + return {'pn': part.pn, 'manufacturer': part.manufacturer, 'mpn': part.mpn} 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.""" @@ -22,7 +27,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto id = get_bom_index(harness.bom(), part) rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', qty, part.unit)) else: - rows.append(component_table_entry(part.description, qty, part.unit, part.pn, part.manufacturer, part.mpn)) + rows.append(component_table_entry(part.description, qty, part.unit, **optional_fields(part))) return rows def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]: @@ -33,10 +38,8 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM 'description': part.description, 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), 'unit': part.unit, - 'manufacturer': part.manufacturer, - 'mpn': part.mpn, - 'pn': part.pn, - 'designators': component.name if component.show_name else None + 'designators': component.name if component.show_name else None, + **optional_fields(part), }) return bom_entries @@ -58,7 +61,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f', {connector.color}' if connector.color else '')) bom_entries.append({ 'description': description, 'designators': connector.name if connector.show_name else None, - 'manufacturer': connector.manufacturer, 'mpn': connector.mpn, 'pn': connector.pn + **optional_fields(connector), }) # add connectors aditional components to bom @@ -77,7 +80,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (' shielded' if cable.shield else '')) bom_entries.append({ 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, - 'manufacturer': cable.manufacturer, 'mpn': cable.mpn, 'pn': cable.pn + **optional_fields(cable), }) else: # add each wire from the bundle to the bom @@ -88,8 +91,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: + (f', {color}' if color else '')) bom_entries.append({ 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, - 'manufacturer': index_if_list(cable.manufacturer, index), - 'mpn': index_if_list(cable.mpn, index), 'pn': index_if_list(cable.pn, index) + **{k: index_if_list(v, index) for k, v in optional_fields(cable).items()}, }) # add cable/bundles aditional components to bom From 51b1136dce6f80d0615f9f40d96ac3ddbd562d70 Mon Sep 17 00:00:00 2001 From: KV Date: Sat, 9 Jan 2021 18:13:47 +0100 Subject: [PATCH 23/30] Group common function arguments into a dict --- src/wireviz/wv_bom.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 5ba6e094..f71abc11 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -22,12 +22,15 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto if component.additional_components: rows.append(["Additional components"]) for part in component.additional_components: - qty = part.qty * component.get_qty_multiplier(part.qty_multiplier) + common_args = { + 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), + 'unit': part.unit, + } if harness.mini_bom_mode: id = get_bom_index(harness.bom(), part) - rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', qty, part.unit)) + rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args)) else: - rows.append(component_table_entry(part.description, qty, part.unit, **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]: From 2c99e83d52316ff86bd16584394661e253dc5e89 Mon Sep 17 00:00:00 2001 From: KV Date: Thu, 25 Mar 2021 20:09:22 +0100 Subject: [PATCH 24/30] Revert "Use a generator expressions and raise exception if failing" This reverts commit 96d393dfb757afc61ffb319c34035d8d2ce7c33d. However, raising an exception if failing the BOM index search is still wanted, so a custom exception is raised instead of returning None. --- src/wireviz/wv_bom.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index f71abc11..99a9fd67 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -118,10 +118,13 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] def get_bom_index(bom: List[BOMEntry], part: AdditionalComponent) -> int: - """Return id of BOM entry or raise StopIteration if not found.""" + """Return id of BOM entry or raise exception if not found.""" # Remove linebreaks and clean whitespace of values in search target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(part), 'description': part.description})) - return next(entry['id'] for entry in bom if bom_types_group(entry) == target) + for entry in bom: + if bom_types_group(entry) == 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.""" From 55760acafc05b6000d1ffbd3e2f74b16ee52e290 Mon Sep 17 00:00:00 2001 From: KV Date: Thu, 25 Mar 2021 21:00:32 +0100 Subject: [PATCH 25/30] Use new BOMKey type alias for get_bom_index() target argument Replace the get_bom_index() part argument with the target key argument to prepare for quering any BOM entry that matches the target key. --- src/wireviz/wv_bom.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 99a9fd67..c1667b55 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -9,6 +9,7 @@ from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_helper import clean_whitespace +BOMKey = Tuple[str, ...] BOMColumn = str # = Literal['id', 'description', 'qty', 'unit', 'designators', 'pn', 'manufacturer', 'mpn'] BOMEntry = Dict[BOMColumn, Union[str, int, float, List[str], None]] @@ -27,7 +28,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto 'unit': part.unit, } if harness.mini_bom_mode: - id = get_bom_index(harness.bom(), part) + id = get_bom_index(harness.bom(), bom_types_group({**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))) @@ -46,9 +47,9 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM }) return bom_entries -def bom_types_group(entry: BOMEntry) -> Tuple[str, ...]: +def bom_types_group(entry: BOMEntry) -> BOMKey: """Return a tuple of string values from the dict that must be equal to join BOM entries.""" - return tuple(make_str(entry.get(key)) for key in ('description', 'unit', 'manufacturer', 'mpn', 'pn')) + return tuple(clean_whitespace(make_str(entry.get(key))) for key in ('description', 'unit', 'manufacturer', 'mpn', 'pn')) def generate_bom(harness: "Harness") -> List[BOMEntry]: """Return a list of BOM entries generated from the harness.""" @@ -117,10 +118,8 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: # add an incrementing id to each bom entry return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] -def get_bom_index(bom: List[BOMEntry], part: AdditionalComponent) -> int: +def get_bom_index(bom: List[BOMEntry], target: BOMKey) -> int: """Return id of BOM entry or raise exception if not found.""" - # Remove linebreaks and clean whitespace of values in search - target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(part), 'description': part.description})) for entry in bom: if bom_types_group(entry) == target: return entry['id'] From f549988a1988fb63bdcadec83722124c36a911c1 Mon Sep 17 00:00:00 2001 From: KV Date: Thu, 25 Mar 2021 23:13:28 +0100 Subject: [PATCH 26/30] Cache the BOM entry key in the entry itself --- src/wireviz/wv_bom.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index c1667b55..d7827789 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -49,7 +49,9 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM def bom_types_group(entry: BOMEntry) -> BOMKey: """Return a tuple of string values from the dict that must be equal to join BOM entries.""" - return tuple(clean_whitespace(make_str(entry.get(key))) for key in ('description', 'unit', 'manufacturer', 'mpn', 'pn')) + if 'key' not in entry: + entry['key'] = tuple(clean_whitespace(make_str(entry.get(k))) for k in ('description', 'unit', 'manufacturer', 'mpn', 'pn')) + return entry['key'] def generate_bom(harness: "Harness") -> List[BOMEntry]: """Return a list of BOM entries generated from the harness.""" From 30a6b137a73f5d5689c93a10ef68629d63bcaeb6 Mon Sep 17 00:00:00 2001 From: KV Date: Fri, 26 Mar 2021 00:46:37 +0100 Subject: [PATCH 27/30] Rename bom_types_group() to bom_entry_key() --- src/wireviz/wv_bom.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index d7827789..3fcdd836 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -28,7 +28,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto 'unit': part.unit, } if harness.mini_bom_mode: - id = get_bom_index(harness.bom(), bom_types_group({**asdict(part), 'description': part.description})) + 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))) @@ -47,7 +47,7 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM }) return bom_entries -def bom_types_group(entry: BOMEntry) -> BOMKey: +def bom_entry_key(entry: BOMEntry) -> BOMKey: """Return a tuple of string values from the dict that must be equal to join BOM entries.""" if 'key' not in entry: entry['key'] = tuple(clean_whitespace(make_str(entry.get(k))) for k in ('description', 'unit', 'manufacturer', 'mpn', 'pn')) @@ -111,7 +111,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: # deduplicate bom bom = [] - for _, group in groupby(sorted(bom_entries, key=bom_types_group), key=bom_types_group): + 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) @@ -123,7 +123,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]: 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_types_group(entry) == target: + if bom_entry_key(entry) == target: return entry['id'] raise Exception('Internal error: No BOM entry found matching: ' + '|'.join(target)) From f1cb7e2b3feb151597f95e3ef0b528c5d294f511 Mon Sep 17 00:00:00 2001 From: KV Date: Fri, 26 Mar 2021 21:24:27 +0100 Subject: [PATCH 28/30] Define tuples of BOM columns as common constants --- src/wireviz/wv_bom.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 3fcdd836..78505aa8 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -9,13 +9,18 @@ from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_helper import clean_whitespace +BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators') +BOM_COLUMNS_OPTIONAL = ('pn', 'manufacturer', 'mpn') +BOM_COLUMNS_IN_KEY = ('description', 'unit') + BOM_COLUMNS_OPTIONAL + BOMKey = Tuple[str, ...] -BOMColumn = str # = Literal['id', 'description', 'qty', 'unit', 'designators', 'pn', 'manufacturer', 'mpn'] +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.""" - return {'pn': part.pn, 'manufacturer': part.manufacturer, 'mpn': part.mpn} + 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]: """Return a list of diagram node table row strings with additional components.""" @@ -50,7 +55,7 @@ def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOM 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(k))) for k in ('description', 'unit', 'manufacturer', 'mpn', 'pn')) + 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]: @@ -129,8 +134,8 @@ def get_bom_index(bom: List[BOMEntry], target: BOMKey) -> int: 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 = ['id', 'description', 'qty', 'unit', 'designators'] # these BOM columns will always be included - for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM entry actually uses them + 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. if any(entry.get(fieldname) for entry in bom): keys.append(fieldname) # list of staic bom header names, headers not specified here are generated by capitilising the internal name From 8de8a69cb3172c107b3beaa5f34ef54e9596e497 Mon Sep 17 00:00:00 2001 From: KV Date: Tue, 6 Apr 2021 20:54:47 +0200 Subject: [PATCH 29/30] Clarify a comment --- src/wireviz/wv_bom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 78505aa8..c1075da3 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -138,7 +138,8 @@ def bom_list(bom: List[BOMEntry]) -> List[List[str]]: 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) - # list of staic bom header names, headers not specified here are generated by capitilising the internal name + # Custom mapping from internal name to BOM column headers. + # Headers not specified here are generated by capitilising the internal name. bom_headings = { "description": "Item", # TODO: Remove this line to use 'Description' in BOM heading. "pn": "P/N", From a1770262ab225045848f268e336d48c80c4c875b Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Sun, 22 Aug 2021 17:52:56 +0200 Subject: [PATCH 30/30] Change BOM heading from `Item` to `Description` Co-authored-by: kvid --- src/wireviz/wv_bom.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index c1075da3..fce42b87 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -141,7 +141,6 @@ def bom_list(bom: List[BOMEntry]) -> List[List[str]]: # Custom mapping from internal name to BOM column headers. # Headers not specified here are generated by capitilising the internal name. bom_headings = { - "description": "Item", # TODO: Remove this line to use 'Description' in BOM heading. "pn": "P/N", "mpn": "MPN" }