kivar 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kivar-0.2.0/PKG-INFO +86 -0
- kivar-0.2.0/README.md +73 -0
- kivar-0.2.0/kivar/__init__.py +1 -0
- kivar-0.2.0/kivar/kivar_backend.py +712 -0
- kivar-0.2.0/kivar/kivar_cli.py +277 -0
- kivar-0.2.0/kivar.egg-info/PKG-INFO +86 -0
- kivar-0.2.0/kivar.egg-info/SOURCES.txt +10 -0
- kivar-0.2.0/kivar.egg-info/dependency_links.txt +1 -0
- kivar-0.2.0/kivar.egg-info/entry_points.txt +3 -0
- kivar-0.2.0/kivar.egg-info/top_level.txt +1 -0
- kivar-0.2.0/setup.cfg +4 -0
- kivar-0.2.0/setup.py +24 -0
kivar-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kivar
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: PCB Assembly Variants for KiCad
|
|
5
|
+
Home-page: https://github.com/markh-de/KiVar
|
|
6
|
+
Author: Mark Hämmerling
|
|
7
|
+
Author-email: dev@markh.de
|
|
8
|
+
License: MIT
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# KiVar − PCB Assembly Variants for KiCad
|
|
13
|
+
|
|
14
|
+
## Introduction
|
|
15
|
+
|
|
16
|
+
KiVar is a tool for **KiCad PCB Assembly Variant selection**, provided as platform-independent
|
|
17
|
+
|
|
18
|
+
* **Command Line Application** (this package) and
|
|
19
|
+
* **KiCad Action Plugin** (available in KiCad PCM).
|
|
20
|
+
|
|
21
|
+
PCB component variation rules are defined in component (i.e. symbol or footprint) fields. This allows for the complete variant configuration to be contained in the schematic and board files without requiring external data from outside the native KiCad design files.
|
|
22
|
+
|
|
23
|
+
The name _KiVar_ (for _KiCad Variants_, obviously) can also be read as an acronym for _**Ki**Cad **V**ariation **a**ssignment **r**ules_.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
KiVar assigns PCB component **values**, **field content** and **attributes** (such as _Do not populate_, _Not in position files_, _Not in BoM_) according to variation rules specified in footprint fields. When applying those rules, components are modified _in place_, allowing for immediate update of the PCB design as well as the 3D view and enabling compatibility with _any_ exporter.
|
|
28
|
+
|
|
29
|
+
Back-propagation of modified component data from the PCB to the schematic can be done in an extra step.
|
|
30
|
+
|
|
31
|
+
## What to Expect
|
|
32
|
+
|
|
33
|
+
Example usage of the **KiVar Command Line Interface app**:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
$ kivar list --selection kivar-demo.kicad_pcb
|
|
37
|
+
BOOT_SRC: [EMMC] JP NAND SD
|
|
38
|
+
EEPROM_ADDR: 0x54 [0x55]
|
|
39
|
+
I_LED_MA: 10 20 30 40 50 60 70 80 90 [100] 110 120 130 140 150 JP
|
|
40
|
+
IOEXP_TYPE/ADDR: 9535/0x20 [9535/0x24] 9539/0x74
|
|
41
|
+
ISL91127: [IRAZ] IRNZ
|
|
42
|
+
UVLO_LO/HI: 2.41V/3.40V [3.15V/3.57V]
|
|
43
|
+
|
|
44
|
+
$ kivar set --assign 'I_LED_MA=60' --assign 'BOOT_SRC=NAND' --verbose kivar-demo.kicad_pcb
|
|
45
|
+
Changes (16):
|
|
46
|
+
Change R9 'Do not populate' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
47
|
+
Change R9 'Exclude from bill of materials' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
48
|
+
Change R9 'Exclude from position files' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
49
|
+
Change R9 field 'ChoiceText' from 'SoM eMMC' to 'SoM NAND' (BOOT_SRC=NAND).
|
|
50
|
+
Change R10 'Do not populate' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
51
|
+
Change R10 'Exclude from bill of materials' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
52
|
+
Change R10 'Exclude from position files' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
53
|
+
Change R11 'Do not populate' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
54
|
+
Change R11 'Exclude from bill of materials' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
55
|
+
Change R11 'Exclude from position files' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
56
|
+
Change R21 'Do not populate' from 'false' to 'true' (I_LED_MA=60).
|
|
57
|
+
Change R21 'Exclude from bill of materials' from 'false' to 'true' (I_LED_MA=60).
|
|
58
|
+
Change R21 'Exclude from position files' from 'false' to 'true' (I_LED_MA=60).
|
|
59
|
+
Change R22 'Do not populate' from 'true' to 'false' (I_LED_MA=60).
|
|
60
|
+
Change R22 'Exclude from bill of materials' from 'true' to 'false' (I_LED_MA=60).
|
|
61
|
+
Change R22 'Exclude from position files' from 'true' to 'false' (I_LED_MA=60).
|
|
62
|
+
Board saved to file "kivar-demo.kicad_pcb".
|
|
63
|
+
|
|
64
|
+
$ kivar list --selection kivar-demo.kicad_pcb
|
|
65
|
+
BOOT_SRC: EMMC JP [NAND] SD
|
|
66
|
+
EEPROM_ADDR: 0x54 [0x55]
|
|
67
|
+
I_LED_MA: 10 20 30 40 50 [60] 70 80 90 100 110 120 130 140 150 JP
|
|
68
|
+
IOEXP_TYPE/ADDR: 9535/0x20 [9535/0x24] 9539/0x74
|
|
69
|
+
ISL91127: [IRAZ] IRNZ
|
|
70
|
+
UVLO_LO/HI: 2.41V/3.40V [3.15V/3.57V]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Concepts
|
|
74
|
+
|
|
75
|
+
Key concepts of KiVar are:
|
|
76
|
+
|
|
77
|
+
* Designs may contain **multiple** independent variation **aspects** (i.e. dimensions or degrees of freedom).
|
|
78
|
+
* Variation rules are **fully contained** in component fields of native design files (no external configuration files) and **portable** (i.e. copying components to another design keeps their variation specification intact).
|
|
79
|
+
* Component values and attributes are modified **in place**, enabling compatibility with all exporters that work on the actual component data.
|
|
80
|
+
* **No external state information** is stored; currently matching variation choices are detected automatically.
|
|
81
|
+
|
|
82
|
+
## Project Page
|
|
83
|
+
|
|
84
|
+
For the usage manual and a demo project check out the [KiVar Project Page](https://github.com/markh-de/KiVar).
|
|
85
|
+
|
|
86
|
+
|
kivar-0.2.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# KiVar − PCB Assembly Variants for KiCad
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
KiVar is a tool for **KiCad PCB Assembly Variant selection**, provided as platform-independent
|
|
6
|
+
|
|
7
|
+
* **Command Line Application** (this package) and
|
|
8
|
+
* **KiCad Action Plugin** (available in KiCad PCM).
|
|
9
|
+
|
|
10
|
+
PCB component variation rules are defined in component (i.e. symbol or footprint) fields. This allows for the complete variant configuration to be contained in the schematic and board files without requiring external data from outside the native KiCad design files.
|
|
11
|
+
|
|
12
|
+
The name _KiVar_ (for _KiCad Variants_, obviously) can also be read as an acronym for _**Ki**Cad **V**ariation **a**ssignment **r**ules_.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
KiVar assigns PCB component **values**, **field content** and **attributes** (such as _Do not populate_, _Not in position files_, _Not in BoM_) according to variation rules specified in footprint fields. When applying those rules, components are modified _in place_, allowing for immediate update of the PCB design as well as the 3D view and enabling compatibility with _any_ exporter.
|
|
17
|
+
|
|
18
|
+
Back-propagation of modified component data from the PCB to the schematic can be done in an extra step.
|
|
19
|
+
|
|
20
|
+
## What to Expect
|
|
21
|
+
|
|
22
|
+
Example usage of the **KiVar Command Line Interface app**:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
$ kivar list --selection kivar-demo.kicad_pcb
|
|
26
|
+
BOOT_SRC: [EMMC] JP NAND SD
|
|
27
|
+
EEPROM_ADDR: 0x54 [0x55]
|
|
28
|
+
I_LED_MA: 10 20 30 40 50 60 70 80 90 [100] 110 120 130 140 150 JP
|
|
29
|
+
IOEXP_TYPE/ADDR: 9535/0x20 [9535/0x24] 9539/0x74
|
|
30
|
+
ISL91127: [IRAZ] IRNZ
|
|
31
|
+
UVLO_LO/HI: 2.41V/3.40V [3.15V/3.57V]
|
|
32
|
+
|
|
33
|
+
$ kivar set --assign 'I_LED_MA=60' --assign 'BOOT_SRC=NAND' --verbose kivar-demo.kicad_pcb
|
|
34
|
+
Changes (16):
|
|
35
|
+
Change R9 'Do not populate' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
36
|
+
Change R9 'Exclude from bill of materials' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
37
|
+
Change R9 'Exclude from position files' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
38
|
+
Change R9 field 'ChoiceText' from 'SoM eMMC' to 'SoM NAND' (BOOT_SRC=NAND).
|
|
39
|
+
Change R10 'Do not populate' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
40
|
+
Change R10 'Exclude from bill of materials' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
41
|
+
Change R10 'Exclude from position files' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
42
|
+
Change R11 'Do not populate' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
43
|
+
Change R11 'Exclude from bill of materials' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
44
|
+
Change R11 'Exclude from position files' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
45
|
+
Change R21 'Do not populate' from 'false' to 'true' (I_LED_MA=60).
|
|
46
|
+
Change R21 'Exclude from bill of materials' from 'false' to 'true' (I_LED_MA=60).
|
|
47
|
+
Change R21 'Exclude from position files' from 'false' to 'true' (I_LED_MA=60).
|
|
48
|
+
Change R22 'Do not populate' from 'true' to 'false' (I_LED_MA=60).
|
|
49
|
+
Change R22 'Exclude from bill of materials' from 'true' to 'false' (I_LED_MA=60).
|
|
50
|
+
Change R22 'Exclude from position files' from 'true' to 'false' (I_LED_MA=60).
|
|
51
|
+
Board saved to file "kivar-demo.kicad_pcb".
|
|
52
|
+
|
|
53
|
+
$ kivar list --selection kivar-demo.kicad_pcb
|
|
54
|
+
BOOT_SRC: EMMC JP [NAND] SD
|
|
55
|
+
EEPROM_ADDR: 0x54 [0x55]
|
|
56
|
+
I_LED_MA: 10 20 30 40 50 [60] 70 80 90 100 110 120 130 140 150 JP
|
|
57
|
+
IOEXP_TYPE/ADDR: 9535/0x20 [9535/0x24] 9539/0x74
|
|
58
|
+
ISL91127: [IRAZ] IRNZ
|
|
59
|
+
UVLO_LO/HI: 2.41V/3.40V [3.15V/3.57V]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Concepts
|
|
63
|
+
|
|
64
|
+
Key concepts of KiVar are:
|
|
65
|
+
|
|
66
|
+
* Designs may contain **multiple** independent variation **aspects** (i.e. dimensions or degrees of freedom).
|
|
67
|
+
* Variation rules are **fully contained** in component fields of native design files (no external configuration files) and **portable** (i.e. copying components to another design keeps their variation specification intact).
|
|
68
|
+
* Component values and attributes are modified **in place**, enabling compatibility with all exporters that work on the actual component data.
|
|
69
|
+
* **No external state information** is stored; currently matching variation choices are detected automatically.
|
|
70
|
+
|
|
71
|
+
## Project Page
|
|
72
|
+
|
|
73
|
+
For the usage manual and a demo project check out the [KiVar Project Page](https://github.com/markh-de/KiVar).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .kivar_cli import main
|
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
import pcbnew
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
|
|
4
|
+
# Note about field case-sensitivity:
|
|
5
|
+
# As long as KiCad can easily be tricked* into having multiple fields whose names only differ in casing, we
|
|
6
|
+
# will not allow case-insensitive field parsing/assignment.
|
|
7
|
+
# * Even though the symbol editor does not allow having "Var" and "VAR" at the same time, you can rename a field
|
|
8
|
+
# to "VAR" and will still get the field name template "Var" presented, which you can fill with a value and KiCad
|
|
9
|
+
# will even save that field to your file.
|
|
10
|
+
|
|
11
|
+
# TODO pre-sort errors and changes before returning them, ready to be used by caller. then remove sorting and
|
|
12
|
+
# requirements (e.g. Key class import) in callers.
|
|
13
|
+
|
|
14
|
+
# TODO clarify rules for Aspect name (forbidden characters: "*" ".")
|
|
15
|
+
|
|
16
|
+
# TODO filter forbidden field names (reference, value, footprint (???)) for set AND get!!
|
|
17
|
+
|
|
18
|
+
# TODO in aux field parser, accept only target fields which are no KiVar fields themselves (avoid recursion!)
|
|
19
|
+
|
|
20
|
+
# TODO wrap the backend with a class.
|
|
21
|
+
|
|
22
|
+
# TODO more testing!
|
|
23
|
+
|
|
24
|
+
# TODO KiCad 8 has different change reporting style, it seems. use this?
|
|
25
|
+
# Remove R29 'Do not place' fabrication attribute.
|
|
26
|
+
# Remove R29 'exclude from BOM' fabrication attribute.
|
|
27
|
+
# Add R22 'exclude from BOM' fabrication attribute.
|
|
28
|
+
# Add R22 'Do not place' fabrication attribute.
|
|
29
|
+
# Update R2 fields.
|
|
30
|
+
# ^^^ this "update fields" message is too generic.
|
|
31
|
+
|
|
32
|
+
def version():
|
|
33
|
+
return '0.2.0'
|
|
34
|
+
|
|
35
|
+
def pcbnew_compatibility_error():
|
|
36
|
+
ver = pcbnew.GetMajorMinorPatchVersion()
|
|
37
|
+
schema = ver.split('.')
|
|
38
|
+
num = int(schema[0]) * 100 + int(schema[1])
|
|
39
|
+
return None if num >= 799 else f'This version of KiVar requires KiCad pcbnew version 7.99 or later.\nYou are using pcbnew version {ver}.'
|
|
40
|
+
|
|
41
|
+
def fp_to_uuid(fp):
|
|
42
|
+
return fp.m_Uuid.AsString()
|
|
43
|
+
|
|
44
|
+
def uuid_to_fp(board, uuid):
|
|
45
|
+
return board.GetItem(pcbnew.KIID(uuid)).Cast()
|
|
46
|
+
|
|
47
|
+
def set_fp_field(fp, field, value):
|
|
48
|
+
if not field.lower() in ['value', 'reference', 'footprint']: fp.SetField(field, value)
|
|
49
|
+
|
|
50
|
+
def legacy_expressions_found(fpdict):
|
|
51
|
+
found = 0
|
|
52
|
+
for uuid in fpdict:
|
|
53
|
+
for field in fpdict[uuid][Key.FIELDS]:
|
|
54
|
+
if field == 'KiVar.Rule': found += 1
|
|
55
|
+
return found
|
|
56
|
+
|
|
57
|
+
def build_fpdict(board):
|
|
58
|
+
fpdict = {}
|
|
59
|
+
for fp in board.GetFootprints():
|
|
60
|
+
uuid = fp_to_uuid(fp)
|
|
61
|
+
# TODO check if UUID exists in dict!
|
|
62
|
+
fpdict[uuid] = {}
|
|
63
|
+
fpdict[uuid][Key.REF] = fp.GetReferenceAsString()
|
|
64
|
+
fpdict[uuid][Key.FIELDS] = fp.GetFieldsText()
|
|
65
|
+
# TODO clean up, we currently store the value twice!
|
|
66
|
+
# the format is the same as for choice branches
|
|
67
|
+
fpdict[uuid][Key.VALUE] = fp.GetValue()
|
|
68
|
+
fpdict[uuid][Key.PROPS] = {}
|
|
69
|
+
fpdict[uuid][Key.PROPS][PropCode.BOM] = not fp.IsExcludedFromBOM()
|
|
70
|
+
fpdict[uuid][Key.PROPS][PropCode.POS] = not fp.IsExcludedFromPosFiles()
|
|
71
|
+
fpdict[uuid][Key.PROPS][PropCode.FIT] = not fp.IsDNP()
|
|
72
|
+
return fpdict
|
|
73
|
+
|
|
74
|
+
def store_fpdict(board, fpdict):
|
|
75
|
+
for uuid in fpdict:
|
|
76
|
+
fp = uuid_to_fp(board, uuid)
|
|
77
|
+
old_fp_value = fp.GetValue()
|
|
78
|
+
new_fp_value = fpdict[uuid][Key.VALUE]
|
|
79
|
+
if old_fp_value != new_fp_value:
|
|
80
|
+
fp.SetValue(new_fp_value)
|
|
81
|
+
for prop_code in fpdict[uuid][Key.PROPS]:
|
|
82
|
+
new_prop_value = fpdict[uuid][Key.PROPS][prop_code]
|
|
83
|
+
if new_prop_value is not None:
|
|
84
|
+
old_prop_value = None
|
|
85
|
+
if prop_code == PropCode.BOM: old_prop_value = not fp.IsExcludedFromBOM()
|
|
86
|
+
elif prop_code == PropCode.POS: old_prop_value = not fp.IsExcludedFromPosFiles()
|
|
87
|
+
elif prop_code == PropCode.FIT: old_prop_value = not fp.IsDNP()
|
|
88
|
+
if old_prop_value is not None:
|
|
89
|
+
if old_prop_value != new_prop_value:
|
|
90
|
+
if prop_code == PropCode.BOM: fp.SetExcludedFromBOM(not new_prop_value)
|
|
91
|
+
elif prop_code == PropCode.POS: fp.SetExcludedFromPosFiles(not new_prop_value)
|
|
92
|
+
elif prop_code == PropCode.FIT: fp.SetDNP(not new_prop_value)
|
|
93
|
+
old_fp_field_values = fp.GetFieldsText()
|
|
94
|
+
for field in fpdict[uuid][Key.FIELDS]:
|
|
95
|
+
old_fp_field_value = old_fp_field_values[field]
|
|
96
|
+
new_fp_field_value = fpdict[uuid][Key.FIELDS][field]
|
|
97
|
+
if old_fp_field_value != new_fp_field_value:
|
|
98
|
+
set_fp_field(fp, field, new_fp_field_value)
|
|
99
|
+
return fpdict
|
|
100
|
+
|
|
101
|
+
# TODO still required if we adopt KiCad 8 reporting style?
|
|
102
|
+
def bool_as_text(value):
|
|
103
|
+
return 'true' if value == True else 'false'
|
|
104
|
+
|
|
105
|
+
def natural_sort_key(string):
|
|
106
|
+
key = []
|
|
107
|
+
part = ''
|
|
108
|
+
for c in string:
|
|
109
|
+
if c.isdigit(): part += c
|
|
110
|
+
else:
|
|
111
|
+
if part:
|
|
112
|
+
key.append((0, int(part), ''))
|
|
113
|
+
part = ''
|
|
114
|
+
key.append((1, 0, c.lower()))
|
|
115
|
+
if part: key.append((0, int(part), ''))
|
|
116
|
+
return key
|
|
117
|
+
|
|
118
|
+
def escape_str(string):
|
|
119
|
+
result = ''
|
|
120
|
+
for c in string:
|
|
121
|
+
if c == '\\' or c == "'" or c == '"': result += '\\'
|
|
122
|
+
result += c
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def quote_str(string):
|
|
126
|
+
# we prefer single-quotes for output
|
|
127
|
+
if string == '': result = "''"
|
|
128
|
+
else:
|
|
129
|
+
if any(c in string for c in ', -\\()="\''):
|
|
130
|
+
q = '"' if string.count("'") > string.count('"') else "'"
|
|
131
|
+
result = q
|
|
132
|
+
for c in string:
|
|
133
|
+
if c == '\\' or c == q: result += '\\'
|
|
134
|
+
result += c
|
|
135
|
+
result += q
|
|
136
|
+
else: result = string
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
# TODO use a cleaner way for keys
|
|
140
|
+
class Key:
|
|
141
|
+
DEFAULT = '*' # relevant for user interface, rest of keys only used internally
|
|
142
|
+
ASPECT = 'a'
|
|
143
|
+
BASE = 'b'
|
|
144
|
+
AUX = 'x'
|
|
145
|
+
VALUE = 'v'
|
|
146
|
+
PROPS = 'p'
|
|
147
|
+
REF = 'R'
|
|
148
|
+
FIELDS = 'F'
|
|
149
|
+
|
|
150
|
+
class PropCode: # all of these must be uppercase
|
|
151
|
+
FIT = 'F'
|
|
152
|
+
BOM = 'B'
|
|
153
|
+
POS = 'P'
|
|
154
|
+
|
|
155
|
+
class FieldID: # case-sensitive
|
|
156
|
+
BASE = 'Var'
|
|
157
|
+
ASPECT = 'Aspect'
|
|
158
|
+
class PropGroup:
|
|
159
|
+
ALL = '!'
|
|
160
|
+
|
|
161
|
+
def base_prop_codes(): return PropCode.FIT + PropCode.BOM + PropCode.POS
|
|
162
|
+
|
|
163
|
+
def supported_prop_codes(): return base_prop_codes() + PropGroup.ALL
|
|
164
|
+
|
|
165
|
+
def prop_state(props, prop):
|
|
166
|
+
return props[prop] if prop in props else None
|
|
167
|
+
|
|
168
|
+
def prop_attrib_name(prop_code):
|
|
169
|
+
if prop_code == PropCode.BOM: name = 'Exclude from bill of materials'
|
|
170
|
+
elif prop_code == PropCode.POS: name = 'Exclude from position files'
|
|
171
|
+
elif prop_code == PropCode.FIT: name = 'Do not populate'
|
|
172
|
+
else: name = '(unknown)'
|
|
173
|
+
return name
|
|
174
|
+
|
|
175
|
+
def prop_abbrev(prop_code):
|
|
176
|
+
if prop_code == PropCode.BOM: name = 'BoM'
|
|
177
|
+
elif prop_code == PropCode.POS: name = 'Pos'
|
|
178
|
+
elif prop_code == PropCode.FIT: name = 'Fit'
|
|
179
|
+
else: name = '(unknown)'
|
|
180
|
+
return name
|
|
181
|
+
|
|
182
|
+
def mismatches_fp_choice(fpdict_branch, vardict_choice_branch):
|
|
183
|
+
# TODO in returned mismatches, add mismatching fp and choice states
|
|
184
|
+
mismatches = []
|
|
185
|
+
choice_value = vardict_choice_branch[Key.VALUE]
|
|
186
|
+
fp_value = fpdict_branch[Key.VALUE]
|
|
187
|
+
if choice_value is not None and fp_value != choice_value:
|
|
188
|
+
mismatches.append('value')
|
|
189
|
+
for prop_code in base_prop_codes():
|
|
190
|
+
choice_prop = prop_state(vardict_choice_branch[Key.PROPS], prop_code)
|
|
191
|
+
fp_prop = prop_state(fpdict_branch[Key.PROPS], prop_code)
|
|
192
|
+
if choice_prop is not None and fp_prop is not None and choice_prop != fp_prop:
|
|
193
|
+
mismatches.append(f"prop '{prop_code}'")
|
|
194
|
+
return mismatches
|
|
195
|
+
|
|
196
|
+
def mismatches_fp_choice_aux(fp_fields, vardict_aux_branch, choice):
|
|
197
|
+
# TODO in returned mismatches, add mismatching fp and choice states
|
|
198
|
+
mismatches = []
|
|
199
|
+
for field in vardict_aux_branch:
|
|
200
|
+
choice_field_value = vardict_aux_branch[field][choice][Key.VALUE]
|
|
201
|
+
if choice_field_value is not None and choice_field_value != fp_fields[field]:
|
|
202
|
+
mismatches.append(f"field '{field}' for choice '{choice}'")
|
|
203
|
+
return mismatches
|
|
204
|
+
|
|
205
|
+
def detect_current_choices(fpdict, vardict):
|
|
206
|
+
# We start with the usual Choice dict filled with all possible Choices per Aspect and then
|
|
207
|
+
# eliminate all Choices whose values do not exactly match the actual FP values, fields or attributes.
|
|
208
|
+
# If exactly one Choice per Aspect remains, then we add this choice to the selection dict.
|
|
209
|
+
choices = get_choice_dict(vardict)
|
|
210
|
+
# Eliminate Choices not matching the actual FP values.
|
|
211
|
+
for uuid in vardict:
|
|
212
|
+
fp_ref = fpdict[uuid][Key.REF]
|
|
213
|
+
aspect = vardict[uuid][Key.ASPECT]
|
|
214
|
+
eliminate_choices = []
|
|
215
|
+
for choice in choices[aspect]:
|
|
216
|
+
eliminate = False
|
|
217
|
+
mismatches = mismatches_fp_choice(fpdict[uuid], vardict[uuid][Key.BASE][choice])
|
|
218
|
+
if mismatches: eliminate = True
|
|
219
|
+
mismatches = mismatches_fp_choice_aux(fpdict[uuid][Key.FIELDS], vardict[uuid][Key.AUX], choice)
|
|
220
|
+
if mismatches: eliminate = True
|
|
221
|
+
# defer elimination until after iteration
|
|
222
|
+
if eliminate: eliminate_choices.append(choice)
|
|
223
|
+
for choice in eliminate_choices: choices[aspect].remove(choice)
|
|
224
|
+
# Create a dict with candidate Choices. Report Choices only if they are unambiguous.
|
|
225
|
+
selection = {}
|
|
226
|
+
for aspect in choices:
|
|
227
|
+
if len(choices[aspect]) == 1: selection[aspect] = choices[aspect][0]
|
|
228
|
+
else: selection[aspect] = None
|
|
229
|
+
return selection
|
|
230
|
+
|
|
231
|
+
def apply_selection(fpdict, vardict, selection, dry_run = False):
|
|
232
|
+
changes = []
|
|
233
|
+
for uuid in vardict:
|
|
234
|
+
ref = fpdict[uuid][Key.REF]
|
|
235
|
+
aspect = vardict[uuid][Key.ASPECT]
|
|
236
|
+
if not aspect in selection: continue
|
|
237
|
+
selected_choice = selection[aspect]
|
|
238
|
+
if selected_choice is None: continue
|
|
239
|
+
choice_text = f'{quote_str(aspect)}={quote_str(selected_choice)}'
|
|
240
|
+
new_value = vardict[uuid][Key.BASE][selected_choice][Key.VALUE]
|
|
241
|
+
if new_value is not None:
|
|
242
|
+
old_value = fpdict[uuid][Key.VALUE]
|
|
243
|
+
if old_value != new_value:
|
|
244
|
+
changes.append([uuid, ref, f"Change {ref} value from '{escape_str(old_value)}' to '{escape_str(new_value)}' ({choice_text})."])
|
|
245
|
+
if not dry_run: fpdict[uuid][Key.VALUE] = new_value
|
|
246
|
+
for prop_code in base_prop_codes():
|
|
247
|
+
new_prop = vardict[uuid][Key.BASE][selected_choice][Key.PROPS][prop_code]
|
|
248
|
+
if new_prop is not None:
|
|
249
|
+
old_prop = fpdict[uuid][Key.PROPS][prop_code]
|
|
250
|
+
if old_prop is not None and old_prop != new_prop:
|
|
251
|
+
changes.append([uuid, ref, f"Change {ref} '{prop_attrib_name(prop_code)}' from '{bool_as_text(not old_prop)}' to '{bool_as_text(not new_prop)}' ({choice_text})."])
|
|
252
|
+
if not dry_run: fpdict[uuid][Key.PROPS][prop_code] = new_prop
|
|
253
|
+
for field in vardict[uuid][Key.AUX]:
|
|
254
|
+
new_field_value = vardict[uuid][Key.AUX][field][selected_choice][Key.VALUE]
|
|
255
|
+
if new_field_value is not None:
|
|
256
|
+
old_field_value = fpdict[uuid][Key.FIELDS][field]
|
|
257
|
+
if old_field_value != new_field_value:
|
|
258
|
+
changes.append([uuid, ref, f"Change {ref} field '{escape_str(field)}' from '{escape_str(old_field_value)}' to '{escape_str(new_field_value)}' ({choice_text})."])
|
|
259
|
+
if not dry_run: fpdict[uuid][Key.FIELDS][field] = new_field_value
|
|
260
|
+
return changes
|
|
261
|
+
|
|
262
|
+
def parse_prop_str(prop_str, old_prop_set={}):
|
|
263
|
+
prop_set = old_prop_set.copy() # TODO as a dict, this is pass-by-reference, so we could simply overwrite the old prop_set and return only errors
|
|
264
|
+
state = None
|
|
265
|
+
expect_prop = False
|
|
266
|
+
for prop_code in prop_str.upper():
|
|
267
|
+
if prop_code in '+-':
|
|
268
|
+
if expect_prop: raise ValueError(f"Property modifier must be followed by property identifier")
|
|
269
|
+
expect_prop = True
|
|
270
|
+
state = prop_code == '+'
|
|
271
|
+
else:
|
|
272
|
+
expect_prop = False
|
|
273
|
+
if state is None: raise ValueError(f"Undefined property modifier for identifier '{prop_code}'") # should never happen
|
|
274
|
+
if not prop_code in supported_prop_codes(): raise ValueError(f"Unsupported property identifier '{prop_code}'")
|
|
275
|
+
# TODO add a '?' symbol, which can be defined by the user
|
|
276
|
+
if prop_code == PropGroup.ALL:
|
|
277
|
+
for c in base_prop_codes(): prop_set[c] = state
|
|
278
|
+
else:
|
|
279
|
+
prop_set[prop_code] = state
|
|
280
|
+
if expect_prop: raise ValueError(f"Property specifier must not end with a modifier, but with an identifier")
|
|
281
|
+
return prop_set
|
|
282
|
+
|
|
283
|
+
def add_choice(vardict, uuid, raw_choice_name, raw_choice_def, field=None, all_aspect_choices=None):
|
|
284
|
+
""" Adds a choice set (base or aux rule definition) to the vardict. """
|
|
285
|
+
# TODO add unique error codes (for aux rules, add an offset), for unit testing (do not compare error strings).
|
|
286
|
+
# If field is passed, this handles aux rules, else base rules.
|
|
287
|
+
is_aux = field is not None
|
|
288
|
+
try:
|
|
289
|
+
raw_names = split_raw_str(raw_choice_name, ',', False)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return [f"Choice identifiers splitter error for identifier list '{raw_choice_name}': {str(e)}"] # TODO cook name?
|
|
292
|
+
try:
|
|
293
|
+
raw_args = split_raw_str(raw_choice_def, ' ', True)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
return [f"Choice arguments splitter error for argument list '{raw_choice_def}': {str(e)}"]
|
|
296
|
+
choices = []
|
|
297
|
+
for choice_name in raw_names:
|
|
298
|
+
cooked_name = cook_raw_string(choice_name)
|
|
299
|
+
if cooked_name == '':
|
|
300
|
+
return ["Empty choice identifier"]
|
|
301
|
+
choices.append(cooked_name)
|
|
302
|
+
errors = []
|
|
303
|
+
values = []
|
|
304
|
+
prop_set = {}
|
|
305
|
+
for raw_arg in raw_args:
|
|
306
|
+
arg = cook_raw_string(raw_arg)
|
|
307
|
+
if raw_arg[0] in '-+': # not supposed to match if arg starts with \-, \+, '+' or '-'
|
|
308
|
+
if is_aux:
|
|
309
|
+
errors.append(f"No property specifier allowed in aux expression")
|
|
310
|
+
continue
|
|
311
|
+
try:
|
|
312
|
+
prop_set = parse_prop_str(arg, prop_set)
|
|
313
|
+
except Exception as error:
|
|
314
|
+
errors.append(f"Property specifier parser error: {str(error)}")
|
|
315
|
+
continue
|
|
316
|
+
else:
|
|
317
|
+
values.append(arg)
|
|
318
|
+
for choice in choices:
|
|
319
|
+
if is_aux:
|
|
320
|
+
if choice != Key.DEFAULT and not choice in all_aspect_choices:
|
|
321
|
+
errors.append(f"Undeclared choice identifier '{choice}'") # TODO print aspect in caller.### (in aspect {quote_str(aspect)})")
|
|
322
|
+
continue
|
|
323
|
+
if not field in vardict[uuid][Key.AUX]: vardict[uuid][Key.AUX][field] = {}
|
|
324
|
+
vardict_branch = vardict[uuid][Key.AUX][field]
|
|
325
|
+
else:
|
|
326
|
+
vardict_branch = vardict[uuid][Key.BASE]
|
|
327
|
+
if not choice in vardict_branch:
|
|
328
|
+
vardict_branch[choice] = {}
|
|
329
|
+
vardict_branch[choice][Key.VALUE] = None
|
|
330
|
+
vardict_branch[choice][Key.PROPS] = {}
|
|
331
|
+
if values:
|
|
332
|
+
value = ' '.join(values)
|
|
333
|
+
if vardict_branch[choice][Key.VALUE] is None:
|
|
334
|
+
vardict_branch[choice][Key.VALUE] = value
|
|
335
|
+
else:
|
|
336
|
+
errors.append(f"Illegal additional content '{value}' assignment for choice '{choice}'")
|
|
337
|
+
for prop_code in prop_set:
|
|
338
|
+
if not prop_code in vardict_branch[choice][Key.PROPS]:
|
|
339
|
+
vardict_branch[choice][Key.PROPS][prop_code] = None
|
|
340
|
+
if vardict_branch[choice][Key.PROPS][prop_code] is None:
|
|
341
|
+
vardict_branch[choice][Key.PROPS][prop_code] = prop_set[prop_code]
|
|
342
|
+
else:
|
|
343
|
+
errors.append(f"Illegal additional '{prop_abbrev(prop_code)}' property assignment for choice '{choice}'")
|
|
344
|
+
return errors
|
|
345
|
+
|
|
346
|
+
def finalize_vardict_branch(vardict_choice_branch, all_aspect_choices, is_aux = False):
|
|
347
|
+
""" Finalizes (flattens) a branch of the vardict (either base rules or aux rules). """
|
|
348
|
+
errors = []
|
|
349
|
+
# Flatten values
|
|
350
|
+
# TODO instead of counting, append (quoted) name of choice to two lists,
|
|
351
|
+
# then print their content (joined with comma) in the error messages!
|
|
352
|
+
choices_with_value_defined = 0
|
|
353
|
+
for choice in all_aspect_choices:
|
|
354
|
+
if not choice in vardict_choice_branch:
|
|
355
|
+
vardict_choice_branch[choice] = {}
|
|
356
|
+
vardict_choice_branch[choice][Key.VALUE] = None
|
|
357
|
+
vardict_choice_branch[choice][Key.PROPS] = {}
|
|
358
|
+
if Key.DEFAULT in vardict_choice_branch:
|
|
359
|
+
if vardict_choice_branch[choice][Key.VALUE] is None:
|
|
360
|
+
vardict_choice_branch[choice][Key.VALUE] = vardict_choice_branch[Key.DEFAULT][Key.VALUE]
|
|
361
|
+
if vardict_choice_branch[choice][Key.VALUE] is not None:
|
|
362
|
+
choices_with_value_defined += 1
|
|
363
|
+
if not (choices_with_value_defined == 0 or choices_with_value_defined == len(all_aspect_choices)):
|
|
364
|
+
errors.append(f"Mixed choices with defined ({choices_with_value_defined}x) and undefined ({len(all_aspect_choices) - choices_with_value_defined}x) content (either all or none must be defined)")
|
|
365
|
+
if not is_aux:
|
|
366
|
+
# Flatten properties
|
|
367
|
+
for prop_code in base_prop_codes():
|
|
368
|
+
default_prop_value = None
|
|
369
|
+
if Key.DEFAULT in vardict_choice_branch and prop_code in vardict_choice_branch[Key.DEFAULT][Key.PROPS] and vardict_choice_branch[Key.DEFAULT][Key.PROPS][prop_code] is not None:
|
|
370
|
+
# defined default prop value available, use it
|
|
371
|
+
default_prop_value = vardict_choice_branch[Key.DEFAULT][Key.PROPS][prop_code]
|
|
372
|
+
else:
|
|
373
|
+
# try to determine an implicit default value
|
|
374
|
+
choices_with_true = 0
|
|
375
|
+
choices_with_false = 0
|
|
376
|
+
for choice in all_aspect_choices:
|
|
377
|
+
if prop_code in vardict_choice_branch[choice][Key.PROPS] and vardict_choice_branch[choice][Key.PROPS][prop_code] is not None:
|
|
378
|
+
if vardict_choice_branch[choice][Key.PROPS][prop_code]: choices_with_true += 1
|
|
379
|
+
else: choices_with_false += 1
|
|
380
|
+
# if only set x-or clear is used in (specific) choices, the implicit default is the opposite.
|
|
381
|
+
if choices_with_false and not choices_with_true: default_prop_value = True
|
|
382
|
+
elif not choices_with_false and choices_with_true: default_prop_value = False
|
|
383
|
+
choices_with_prop_defined = 0
|
|
384
|
+
for choice in all_aspect_choices:
|
|
385
|
+
if not prop_code in vardict_choice_branch[choice][Key.PROPS] or vardict_choice_branch[choice][Key.PROPS][prop_code] is None:
|
|
386
|
+
vardict_choice_branch[choice][Key.PROPS][prop_code] = default_prop_value
|
|
387
|
+
if vardict_choice_branch[choice][Key.PROPS][prop_code] is not None:
|
|
388
|
+
choices_with_prop_defined += 1
|
|
389
|
+
if not (choices_with_prop_defined == 0 or choices_with_prop_defined == len(all_aspect_choices)):
|
|
390
|
+
errors.append(f"Mixed choices with defined ({choices_with_prop_defined}x) and undefined ({len(all_aspect_choices) - choices_with_prop_defined}x) {prop_abbrev(prop_code)} property ('{prop_code}') state (either all or none must be defined)")
|
|
391
|
+
# Remove default choice entries from branch
|
|
392
|
+
vardict_choice_branch.pop(Key.DEFAULT, None)
|
|
393
|
+
return errors
|
|
394
|
+
|
|
395
|
+
def parse_rule_str(rule_str):
|
|
396
|
+
errors = []
|
|
397
|
+
aspects = []
|
|
398
|
+
choice_sets = []
|
|
399
|
+
if rule_str is not None:
|
|
400
|
+
try:
|
|
401
|
+
rule_sections = split_raw_str(rule_str, ' ', True)
|
|
402
|
+
except Exception as error:
|
|
403
|
+
return [f'Combined format splitter: {str(error)}'], None, None
|
|
404
|
+
for section in rule_sections:
|
|
405
|
+
try:
|
|
406
|
+
name_list, content = split_parens(section)
|
|
407
|
+
except Exception as error:
|
|
408
|
+
errors.append(f'Choice expression splitter: {str(error)}')
|
|
409
|
+
continue
|
|
410
|
+
if content is None: # None means: no parens
|
|
411
|
+
# this is an aspect name. cook and store.
|
|
412
|
+
try:
|
|
413
|
+
cooked_name = cook_raw_string(name_list)
|
|
414
|
+
except Exception as error:
|
|
415
|
+
errors.append(f'Aspect identifier parser: {str(error)}')
|
|
416
|
+
continue
|
|
417
|
+
if cooked_name is None or cooked_name == '':
|
|
418
|
+
errors.append('Aspect identifier must not be empty')
|
|
419
|
+
continue
|
|
420
|
+
aspects.append(cooked_name)
|
|
421
|
+
else:
|
|
422
|
+
# this is a choice definition. leave name and content raw and store.
|
|
423
|
+
if name_list is None or name_list == '':
|
|
424
|
+
errors.append('Choice identifier list must not be empty')
|
|
425
|
+
continue
|
|
426
|
+
choice_sets.append([name_list, content])
|
|
427
|
+
return errors, aspects, choice_sets
|
|
428
|
+
|
|
429
|
+
def parse_rule_fields(fpdict_uuid_branch):
|
|
430
|
+
errors = []
|
|
431
|
+
aspect = None
|
|
432
|
+
base_rule_string = None
|
|
433
|
+
base_choice_sets = []
|
|
434
|
+
aux_rule_strings = []
|
|
435
|
+
aux_choice_sets = []
|
|
436
|
+
for fp_field in fpdict_uuid_branch[Key.FIELDS]:
|
|
437
|
+
value = fpdict_uuid_branch[Key.FIELDS][fp_field]
|
|
438
|
+
# field names that are not properly formatted are ignored and do not cause
|
|
439
|
+
# an error message. we don't want to misinterpret user's fields.
|
|
440
|
+
try: parts = split_raw_str(fp_field, '.', False)
|
|
441
|
+
except: continue
|
|
442
|
+
if len(parts) == 2 and parts[0] == FieldID.BASE and parts[1] == FieldID.ASPECT:
|
|
443
|
+
aspect = value
|
|
444
|
+
elif len(parts) == 1 and parts[0] == FieldID.BASE:
|
|
445
|
+
base_rule_string = value
|
|
446
|
+
elif len(parts) > 1 and parts[-1] == FieldID.BASE:
|
|
447
|
+
target_field = '.'.join(parts[0:-1])
|
|
448
|
+
if target_field in fpdict_uuid_branch[Key.FIELDS]:
|
|
449
|
+
aux_rule_strings.append([target_field, value])
|
|
450
|
+
else:
|
|
451
|
+
errors.append(f"Target field '{target_field}' does not exist for combined aux expression") # TODO escape field name?
|
|
452
|
+
continue
|
|
453
|
+
else:
|
|
454
|
+
try: prefix, name_list = split_parens(parts[-1])
|
|
455
|
+
except: continue
|
|
456
|
+
if prefix == FieldID.BASE:
|
|
457
|
+
try: parts_in_parens = split_raw_str(name_list, ' ', True)
|
|
458
|
+
except: continue
|
|
459
|
+
if len(parts_in_parens) > 1:
|
|
460
|
+
errors.append(f"Choice identifier list '{name_list}' contains illegal space character")
|
|
461
|
+
continue
|
|
462
|
+
if len(parts) == 1:
|
|
463
|
+
base_choice_sets.append([name_list, value])
|
|
464
|
+
else:
|
|
465
|
+
target_field = '.'.join(parts[0:-1])
|
|
466
|
+
if target_field in fpdict_uuid_branch[Key.FIELDS]:
|
|
467
|
+
aux_choice_sets.append([target_field, name_list, value])
|
|
468
|
+
else:
|
|
469
|
+
errors.append(f"Target field '{target_field}' does not exist for simple aux expression") # TODO escape field name?
|
|
470
|
+
continue
|
|
471
|
+
return errors, aspect, base_rule_string, base_choice_sets, aux_rule_strings, aux_choice_sets
|
|
472
|
+
|
|
473
|
+
def build_vardict(fpdict):
|
|
474
|
+
vardict = {}
|
|
475
|
+
errors = []
|
|
476
|
+
auxdict = {}
|
|
477
|
+
# Handle base rule
|
|
478
|
+
for uuid in fpdict:
|
|
479
|
+
ref = fpdict[uuid][Key.REF]
|
|
480
|
+
parse_errors, aspect, base_rule_string, base_choice_sets, aux_rule_strings, aux_choice_sets = parse_rule_fields(fpdict[uuid])
|
|
481
|
+
if parse_errors:
|
|
482
|
+
for parse_error in parse_errors: errors.append([uuid, ref, f"{ref}: Field parser: {parse_error}."])
|
|
483
|
+
continue
|
|
484
|
+
parse_errors, aspects, choice_sets = parse_rule_str(base_rule_string)
|
|
485
|
+
if parse_errors:
|
|
486
|
+
for parse_error in parse_errors: errors.append([uuid, ref, f"{ref}: Base scope expression parser: {parse_error}."])
|
|
487
|
+
continue
|
|
488
|
+
choice_sets.extend(base_choice_sets)
|
|
489
|
+
# TODO decide: shall we really use uncooked aspect name? we have the whole field content only for the pure value.
|
|
490
|
+
if len(aspects) > 1:
|
|
491
|
+
errors.append([uuid, ref, f"{ref}: Multiple aspect identifiers specified."])
|
|
492
|
+
continue
|
|
493
|
+
elif len(aspects) == 1:
|
|
494
|
+
# about to use the aspect name from the base rule ...
|
|
495
|
+
if aspect is not None and aspect != '':
|
|
496
|
+
# ... but there is already an aspect set via the aspect field
|
|
497
|
+
errors.append([uuid, ref, f"{ref}: Conflicting aspect identifier specification styles (combined base expression vs. aspect field)."])
|
|
498
|
+
continue
|
|
499
|
+
aspect = aspects[0]
|
|
500
|
+
if aspect is None or aspect == '':
|
|
501
|
+
if choice_sets:
|
|
502
|
+
errors.append([uuid, ref, f"{ref}: Missing aspect identifier for existing choice expressions."])
|
|
503
|
+
continue
|
|
504
|
+
if uuid in vardict:
|
|
505
|
+
errors.append([uuid, ref, f"{ref}: Multiple footprints with same UUID containing base expressions."])
|
|
506
|
+
continue
|
|
507
|
+
vardict[uuid] = {}
|
|
508
|
+
vardict[uuid][Key.ASPECT] = aspect
|
|
509
|
+
vardict[uuid][Key.BASE] = {}
|
|
510
|
+
vardict[uuid][Key.AUX] = {}
|
|
511
|
+
auxdict[uuid] = [aux_rule_strings, aux_choice_sets] # save for aux loop
|
|
512
|
+
for choice_name, choice_content in choice_sets:
|
|
513
|
+
add_errors = add_choice(vardict, uuid, choice_name, choice_content)
|
|
514
|
+
if add_errors:
|
|
515
|
+
for error in add_errors: errors.append([uuid, ref, f"{ref}: When adding aspect '{aspect}' choice list '{choice_name}' in base expression: {error}."])
|
|
516
|
+
break
|
|
517
|
+
# Handle aux rules
|
|
518
|
+
all_choices = get_choice_dict(vardict)
|
|
519
|
+
for uuid in auxdict:
|
|
520
|
+
aux_rule_strings, aux_choice_sets = auxdict[uuid]
|
|
521
|
+
ref = fpdict[uuid][Key.REF]
|
|
522
|
+
aspect = vardict[uuid][Key.ASPECT]
|
|
523
|
+
if aux_rule_strings and aspect is None:
|
|
524
|
+
errors.append([uuid, ref, f"{ref}: Combined aux expression(s) found, but missing base expression(s)."])
|
|
525
|
+
continue
|
|
526
|
+
valid = False
|
|
527
|
+
for field, rule_str in aux_rule_strings:
|
|
528
|
+
if rule_str is None or rule_str == '': continue
|
|
529
|
+
parse_errors, aspects, choice_sets = parse_rule_str(rule_str)
|
|
530
|
+
if parse_errors:
|
|
531
|
+
for parse_error in parse_errors: errors.append([uuid, ref, f"{ref}: Combined aux expression parser for target field '{field}': {parse_error}."])
|
|
532
|
+
continue
|
|
533
|
+
if aspects:
|
|
534
|
+
errors.append([uuid, ref, f"{ref}: Combined aux expression for target field '{field}' contains what looks like an aspect identifier (only allowed in combined base expressions)."])
|
|
535
|
+
continue
|
|
536
|
+
if field in vardict[uuid][Key.AUX]:
|
|
537
|
+
errors.append([uuid, ref, f"{ref}: Multiple aux expressions for target field '{field}'."]) # TODO wording
|
|
538
|
+
continue
|
|
539
|
+
for choice_name, choice_content in choice_sets:
|
|
540
|
+
add_errors = add_choice(vardict, uuid, choice_name, choice_content, field, all_choices[aspect])
|
|
541
|
+
if add_errors:
|
|
542
|
+
for error in add_errors: errors.append([uuid, ref, f"{ref}: Combined aux expression for aspect '{aspect}' choice list '{choice_name}' with target field '{field}': {error}."])
|
|
543
|
+
break
|
|
544
|
+
else:
|
|
545
|
+
valid = True
|
|
546
|
+
if not valid: continue
|
|
547
|
+
if aux_choice_sets and aspect is None:
|
|
548
|
+
errors.append([uuid, ref, f"{ref}: Simple aux expression(s) found, but missing base expression(s)."])
|
|
549
|
+
continue
|
|
550
|
+
valid = False
|
|
551
|
+
for field, choice_name, choice_content in aux_choice_sets:
|
|
552
|
+
add_errors = add_choice(vardict, uuid, choice_name, choice_content, field, all_choices[aspect])
|
|
553
|
+
if add_errors:
|
|
554
|
+
for error in add_errors: errors.append([uuid, ref, f"{ref}: Simple aux expression for aspect '{aspect}' choice list '{choice_name}' with target field '{field}': {error}."])
|
|
555
|
+
break
|
|
556
|
+
else:
|
|
557
|
+
valid = True
|
|
558
|
+
if not valid: continue
|
|
559
|
+
fin_errors = finalize_vardict_branch(vardict[uuid][Key.BASE], all_choices[aspect], is_aux=False)
|
|
560
|
+
if fin_errors:
|
|
561
|
+
# TODO cook and quote names in error message, refine wording
|
|
562
|
+
for e in fin_errors: errors.append([uuid, ref, f"{ref}: In base expression: {e}."])
|
|
563
|
+
continue
|
|
564
|
+
for field in vardict[uuid][Key.AUX]:
|
|
565
|
+
fin_errors = finalize_vardict_branch(vardict[uuid][Key.AUX][field], all_choices[aspect], is_aux=True)
|
|
566
|
+
if fin_errors:
|
|
567
|
+
# TODO cook and quote names in error message, refine wording
|
|
568
|
+
for e in fin_errors: errors.append([uuid, ref, f"{ref}: In aux expression for target field '{field}': {e}."])
|
|
569
|
+
continue
|
|
570
|
+
if not errors:
|
|
571
|
+
# Check for ambiguous choices (only if data is valid so far)
|
|
572
|
+
for aspect in sorted(all_choices, key=natural_sort_key):
|
|
573
|
+
choices = sorted(all_choices[aspect], key=natural_sort_key)
|
|
574
|
+
for choice_a in choices:
|
|
575
|
+
fpdict_a = deepcopy(fpdict)
|
|
576
|
+
apply_selection(fpdict_a, vardict, {aspect: choice_a})
|
|
577
|
+
ambiguous = []
|
|
578
|
+
for choice_b in choices:
|
|
579
|
+
if choice_a == choice_b:
|
|
580
|
+
if ambiguous: break
|
|
581
|
+
else: continue
|
|
582
|
+
fpdict_b = deepcopy(fpdict)
|
|
583
|
+
apply_selection(fpdict_b, vardict, {aspect: choice_b})
|
|
584
|
+
if fpdict_a == fpdict_b: ambiguous.append(f"'{escape_str(choice_b)}'")
|
|
585
|
+
if ambiguous:
|
|
586
|
+
errors.append([None, '0', f"Illegal ambiguity: Aspect '{escape_str(aspect)}' choice '{escape_str(choice_a)}' is identical with choice(s) {', '.join(ambiguous)}."])
|
|
587
|
+
break
|
|
588
|
+
if errors: vardict = {} # make sure an incomplete vardict cannot be used by the caller
|
|
589
|
+
return vardict, errors
|
|
590
|
+
|
|
591
|
+
def get_choice_dict(vardict):
|
|
592
|
+
choices = {}
|
|
593
|
+
for uuid in vardict:
|
|
594
|
+
aspect = vardict[uuid][Key.ASPECT]
|
|
595
|
+
if not aspect in choices: choices[aspect] = []
|
|
596
|
+
for choice in vardict[uuid][Key.BASE]:
|
|
597
|
+
# In case the input dict still contains temporary data (such as default data), ignore it.
|
|
598
|
+
if choice != Key.DEFAULT and not choice in choices[aspect]: choices[aspect].append(choice)
|
|
599
|
+
return choices
|
|
600
|
+
|
|
601
|
+
def split_parens(string):
|
|
602
|
+
item = []
|
|
603
|
+
outside = None
|
|
604
|
+
inside = None
|
|
605
|
+
escaped = False
|
|
606
|
+
quoted_s = False
|
|
607
|
+
quoted_d = False
|
|
608
|
+
parens = 0
|
|
609
|
+
end_expected = False
|
|
610
|
+
for c in string:
|
|
611
|
+
if end_expected: raise ValueError('String extends beyond closing parenthesis')
|
|
612
|
+
elif escaped:
|
|
613
|
+
escaped = False
|
|
614
|
+
item.append(c)
|
|
615
|
+
elif c == '\\':
|
|
616
|
+
escaped = True
|
|
617
|
+
item.append(c)
|
|
618
|
+
elif c == "'" and not quoted_d:
|
|
619
|
+
quoted_s = not quoted_s
|
|
620
|
+
item.append(c)
|
|
621
|
+
elif c == '"' and not quoted_s:
|
|
622
|
+
quoted_d = not quoted_d
|
|
623
|
+
item.append(c)
|
|
624
|
+
elif c == '(' and not (quoted_s or quoted_d):
|
|
625
|
+
parens += 1
|
|
626
|
+
if parens == 1:
|
|
627
|
+
outside = ''.join(item)
|
|
628
|
+
inside = '' # inside: no parens -> None, empty parens -> ''
|
|
629
|
+
item = []
|
|
630
|
+
else:
|
|
631
|
+
item.append(c)
|
|
632
|
+
elif c == ')' and not (quoted_s or quoted_d):
|
|
633
|
+
if parens > 0:
|
|
634
|
+
parens -= 1
|
|
635
|
+
if parens == 0:
|
|
636
|
+
inside = ''.join(item)
|
|
637
|
+
item = []
|
|
638
|
+
end_expected = True
|
|
639
|
+
else:
|
|
640
|
+
item.append(c)
|
|
641
|
+
else: raise ValueError('Unmatched closing parenthesis')
|
|
642
|
+
else:
|
|
643
|
+
item.append(c)
|
|
644
|
+
if parens: raise ValueError('Unmatched opening parenthesis')
|
|
645
|
+
if escaped: raise ValueError('Unterminated escape sequence (\\) at end of string')
|
|
646
|
+
if quoted_s: raise ValueError("Unmatched single-quote (') character in string")
|
|
647
|
+
if quoted_d: raise ValueError('Unmatched double-quote (") character in string')
|
|
648
|
+
if item: outside = ''.join(item)
|
|
649
|
+
return outside, inside
|
|
650
|
+
|
|
651
|
+
def split_raw_str(str, sep, multisep):
|
|
652
|
+
result = []
|
|
653
|
+
item = []
|
|
654
|
+
escaped = False
|
|
655
|
+
quoted_s = False
|
|
656
|
+
quoted_d = False
|
|
657
|
+
parens = 0
|
|
658
|
+
for c in str:
|
|
659
|
+
if escaped:
|
|
660
|
+
escaped = False
|
|
661
|
+
item.append(c)
|
|
662
|
+
elif c == '\\':
|
|
663
|
+
escaped = True
|
|
664
|
+
item.append(c)
|
|
665
|
+
elif c == "'" and not quoted_d:
|
|
666
|
+
quoted_s = not quoted_s
|
|
667
|
+
item.append(c)
|
|
668
|
+
elif c == '"' and not quoted_s:
|
|
669
|
+
quoted_d = not quoted_d
|
|
670
|
+
item.append(c)
|
|
671
|
+
elif c == '(' and not (quoted_s or quoted_d):
|
|
672
|
+
parens += 1
|
|
673
|
+
item.append(c)
|
|
674
|
+
elif c == ')' and not (quoted_s or quoted_d):
|
|
675
|
+
if parens > 0:
|
|
676
|
+
parens -= 1
|
|
677
|
+
else: raise ValueError('Unmatched closing parenthesis')
|
|
678
|
+
item.append(c)
|
|
679
|
+
elif c == sep and not (quoted_s or quoted_d) and parens == 0:
|
|
680
|
+
if not multisep or item:
|
|
681
|
+
result.append(''.join(item))
|
|
682
|
+
item = []
|
|
683
|
+
else:
|
|
684
|
+
item.append(c)
|
|
685
|
+
if parens: raise ValueError('Unmatched opening parenthesis')
|
|
686
|
+
if escaped: raise ValueError('Unterminated escape sequence (\\) at end of string')
|
|
687
|
+
if quoted_s: raise ValueError("Unmatched single-quote (') character in string")
|
|
688
|
+
if quoted_d: raise ValueError('Unmatched double-quote (") character in string')
|
|
689
|
+
if not multisep or item: result.append(''.join(item))
|
|
690
|
+
return result
|
|
691
|
+
|
|
692
|
+
def cook_raw_string(string):
|
|
693
|
+
result = []
|
|
694
|
+
escaped = False
|
|
695
|
+
quoted_s = False
|
|
696
|
+
quoted_d = False
|
|
697
|
+
for c in string:
|
|
698
|
+
if escaped:
|
|
699
|
+
result.append(c)
|
|
700
|
+
escaped = False
|
|
701
|
+
elif c == '\\':
|
|
702
|
+
escaped = True
|
|
703
|
+
elif c == "'" and not quoted_d:
|
|
704
|
+
quoted_s = not quoted_s
|
|
705
|
+
elif c == '"' and not quoted_s:
|
|
706
|
+
quoted_d = not quoted_d
|
|
707
|
+
else:
|
|
708
|
+
result.append(c)
|
|
709
|
+
if escaped: raise ValueError('Unterminated escape sequence (\\) at end of string')
|
|
710
|
+
if quoted_s: raise ValueError("Unmatched single-quote (') character in string")
|
|
711
|
+
if quoted_d: raise ValueError('Unmatched double-quote (") character in string')
|
|
712
|
+
return ''.join(result)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import pcbnew
|
|
7
|
+
try:
|
|
8
|
+
from kivar_backend import Key, version, build_vardict, natural_sort_key, build_fpdict, store_fpdict, detect_current_choices, quote_str, prop_abbrev, base_prop_codes, cook_raw_string, escape_str, get_choice_dict, split_raw_str, apply_selection, pcbnew_compatibility_error
|
|
9
|
+
except ModuleNotFoundError:
|
|
10
|
+
from .kivar_backend import Key, version, build_vardict, natural_sort_key, build_fpdict, store_fpdict, detect_current_choices, quote_str, prop_abbrev, base_prop_codes, cook_raw_string, escape_str, get_choice_dict, split_raw_str, apply_selection, pcbnew_compatibility_error
|
|
11
|
+
|
|
12
|
+
# TODO for list, allow another structure: aspect -> component -> choice (in addition to current aspect -> choice -> component)
|
|
13
|
+
# TODO make use of verbose options, where applicable
|
|
14
|
+
# TODO add option to save to a different file (create a copy)
|
|
15
|
+
# TODO if output to terminal, use colors for highlighting aspects, choices, matches etc.
|
|
16
|
+
# (provide --no-color/-C option (or a different usual option name))
|
|
17
|
+
# also check all TODO notes buried in the code below!
|
|
18
|
+
|
|
19
|
+
# TODO have common help URL for both plugin and CLI? place in module?
|
|
20
|
+
def doc_vcs_ref():
|
|
21
|
+
return f'v{version()}'
|
|
22
|
+
|
|
23
|
+
def doc_base_url():
|
|
24
|
+
return f'https://doc.kivar.markh.de/{doc_vcs_ref()}/README.md'
|
|
25
|
+
|
|
26
|
+
def print_err(*args, file=sys.stderr, **kwargs):
|
|
27
|
+
print(*args, file=file, **kwargs)
|
|
28
|
+
|
|
29
|
+
def load_board(board_file):
|
|
30
|
+
if not os.path.isfile(board_file) or not os.access(board_file, os.R_OK):
|
|
31
|
+
print_err(f'Error: "{board_file}" is not a readable file.')
|
|
32
|
+
return None
|
|
33
|
+
return pcbnew.LoadBoard(board_file)
|
|
34
|
+
|
|
35
|
+
def save_board(board_file, board):
|
|
36
|
+
return pcbnew.SaveBoard(board_file, board)
|
|
37
|
+
|
|
38
|
+
def build_vardict_wrapper(fpdict):
|
|
39
|
+
vardict, errors = build_vardict(fpdict)
|
|
40
|
+
if len(errors) > 0:
|
|
41
|
+
print_err(f'Errors ({len(errors)}):')
|
|
42
|
+
for uuid, order, error in sorted(errors, key=lambda x: natural_sort_key(x[1])):
|
|
43
|
+
print_err(' ' + error)
|
|
44
|
+
return None
|
|
45
|
+
if len(vardict) == 0:
|
|
46
|
+
print_err(f'Error: No rule definitions found.')
|
|
47
|
+
print_err(f' Read {doc_base_url()}#usage for usage instructions.')
|
|
48
|
+
return None
|
|
49
|
+
return vardict
|
|
50
|
+
|
|
51
|
+
def list_command(board=None, long=False, prop_codes=False, detailed=False, selected=False, verbose=False):
|
|
52
|
+
b = load_board(board)
|
|
53
|
+
if b is None: return False
|
|
54
|
+
fpdict = build_fpdict(b)
|
|
55
|
+
vardict = build_vardict_wrapper(fpdict)
|
|
56
|
+
if vardict is None: return False
|
|
57
|
+
if selected: sel = detect_current_choices(fpdict, vardict)
|
|
58
|
+
ndict = {}
|
|
59
|
+
for uuid in vardict:
|
|
60
|
+
aspect = vardict[uuid][Key.ASPECT]
|
|
61
|
+
if not aspect in ndict:
|
|
62
|
+
ndict[aspect] = {}
|
|
63
|
+
for choice in vardict[uuid][Key.BASE]:
|
|
64
|
+
if not choice in ndict[aspect]:
|
|
65
|
+
ndict[aspect][choice] = {}
|
|
66
|
+
if detailed:
|
|
67
|
+
cmp_info = []
|
|
68
|
+
cmp_value = vardict[uuid][Key.BASE][choice][Key.VALUE]
|
|
69
|
+
if cmp_value is not None:
|
|
70
|
+
cmp_info.append(quote_str(cmp_value))
|
|
71
|
+
cmp_props = vardict[uuid][Key.BASE][choice][Key.PROPS]
|
|
72
|
+
for prop_code in base_prop_codes():
|
|
73
|
+
if prop_code in cmp_props and cmp_props[prop_code] is not None:
|
|
74
|
+
# TODO use colored output and use green and red here!
|
|
75
|
+
if prop_codes:
|
|
76
|
+
state_text = '+' if cmp_props[prop_code] else '-'
|
|
77
|
+
cmp_info.append(f'{state_text}{prop_code}')
|
|
78
|
+
else:
|
|
79
|
+
state_text = 'Yes' if cmp_props[prop_code] else 'No'
|
|
80
|
+
cmp_info.append(f'<{prop_abbrev(prop_code)}:{state_text}>')
|
|
81
|
+
ndict[aspect][choice][uuid] = ' '.join(cmp_info)
|
|
82
|
+
for aspect in sorted(ndict, key=natural_sort_key):
|
|
83
|
+
p_aspect = quote_str(aspect)
|
|
84
|
+
if long:
|
|
85
|
+
print(p_aspect)
|
|
86
|
+
else:
|
|
87
|
+
print(f'{p_aspect}:', end='')
|
|
88
|
+
for choice in sorted(ndict[aspect], key=natural_sort_key):
|
|
89
|
+
p_choice = quote_str(choice)
|
|
90
|
+
if selected and sel[aspect] == choice:
|
|
91
|
+
if long:
|
|
92
|
+
print(f' + {p_choice}')
|
|
93
|
+
else:
|
|
94
|
+
print(f' [{p_choice}]', end='')
|
|
95
|
+
else:
|
|
96
|
+
if long:
|
|
97
|
+
print(f' {p_choice}')
|
|
98
|
+
else:
|
|
99
|
+
print(f' {p_choice}', end='')
|
|
100
|
+
if detailed:
|
|
101
|
+
for uuid in sorted(ndict[aspect][choice], key=lambda x: natural_sort_key(fpdict[x][Key.REF])):
|
|
102
|
+
ref = fpdict[uuid][Key.REF]
|
|
103
|
+
print(f' {ref}: {ndict[aspect][choice][uuid]}')
|
|
104
|
+
for field in sorted(vardict[uuid][Key.AUX], key=natural_sort_key):
|
|
105
|
+
f = quote_str(field)
|
|
106
|
+
v = quote_str(vardict[uuid][Key.AUX][field][choice][Key.VALUE])
|
|
107
|
+
print(f" {f}: {v}")
|
|
108
|
+
# Future note: When options for aux rules are allowed, print them here
|
|
109
|
+
print()
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
def state_command(board=None, all=False, query_aspect=None, verbose=False):
|
|
113
|
+
b = load_board(board)
|
|
114
|
+
if b is None: return False
|
|
115
|
+
fpdict = build_fpdict(b)
|
|
116
|
+
vardict = build_vardict_wrapper(fpdict)
|
|
117
|
+
if vardict is None: return False
|
|
118
|
+
sel = detect_current_choices(fpdict, vardict)
|
|
119
|
+
if query_aspect is not None:
|
|
120
|
+
if all:
|
|
121
|
+
print_err('Error: Options "--all" and "--query" are mutually exclusive.')
|
|
122
|
+
return False
|
|
123
|
+
if len(query_aspect) > 1:
|
|
124
|
+
print_err(f'Error: Only one query allowed.')
|
|
125
|
+
return False
|
|
126
|
+
q = cook_raw_string(query_aspect[0])
|
|
127
|
+
if q in sel:
|
|
128
|
+
p_choice = quote_str(sel[q])
|
|
129
|
+
print(p_choice)
|
|
130
|
+
else:
|
|
131
|
+
print_err(f"Error: No such aspect '{escape_str(q)}'.")
|
|
132
|
+
return False
|
|
133
|
+
else:
|
|
134
|
+
for aspect in sorted(sel, key=natural_sort_key):
|
|
135
|
+
choice = sel[aspect]
|
|
136
|
+
if all or choice is not None:
|
|
137
|
+
p_aspect = quote_str(aspect)
|
|
138
|
+
p_c = '' if choice is None else quote_str(choice)
|
|
139
|
+
print(f'{p_aspect}={p_c}')
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
def check_command(board=None, verbose=False):
|
|
143
|
+
b = load_board(board)
|
|
144
|
+
if b is None: return False
|
|
145
|
+
fpdict = build_fpdict(b)
|
|
146
|
+
vardict = build_vardict_wrapper(fpdict)
|
|
147
|
+
if vardict is None: return False
|
|
148
|
+
sel = detect_current_choices(fpdict, vardict)
|
|
149
|
+
failed = []
|
|
150
|
+
for aspect in sorted(sel, key=natural_sort_key):
|
|
151
|
+
choice = sel[aspect]
|
|
152
|
+
# TODO if verbose: print result of each check here!
|
|
153
|
+
if choice is None:
|
|
154
|
+
failed.append(aspect)
|
|
155
|
+
if len(failed) > 0:
|
|
156
|
+
print_err(f'Check failed. No matching choice found for {len(failed)} (of {len(sel)}) aspect(s):')
|
|
157
|
+
for aspect in failed:
|
|
158
|
+
p_aspect = quote_str(aspect)
|
|
159
|
+
print_err(f' {p_aspect}')
|
|
160
|
+
return False
|
|
161
|
+
else:
|
|
162
|
+
print_err(f'Check passed. All {len(sel)} aspect(s) have a matching choice.')
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def set_command(board=None, assign=None, dry_run=False, verbose=False):
|
|
166
|
+
# TODO how to handle errors? ignore failing assignments, or abort?
|
|
167
|
+
# collect errors, then print & return?
|
|
168
|
+
if assign is None:
|
|
169
|
+
print('Error: No assignments passed.')
|
|
170
|
+
return False
|
|
171
|
+
b = load_board(board)
|
|
172
|
+
if b is None: return False
|
|
173
|
+
fpdict = build_fpdict(b)
|
|
174
|
+
vardict = build_vardict_wrapper(fpdict)
|
|
175
|
+
if vardict is None: return False
|
|
176
|
+
sel = detect_current_choices(fpdict, vardict)
|
|
177
|
+
choice_dict = get_choice_dict(vardict)
|
|
178
|
+
for asmt in assign:
|
|
179
|
+
l = split_raw_str(asmt, '=', False) # TODO: test split
|
|
180
|
+
if len(l) == 2:
|
|
181
|
+
aspect = cook_raw_string(l[0])
|
|
182
|
+
choice = cook_raw_string(l[1])
|
|
183
|
+
p_aspect = escape_str(aspect)
|
|
184
|
+
p_choice = escape_str(choice)
|
|
185
|
+
p_asmt = f'{p_aspect}={p_choice}'
|
|
186
|
+
if aspect in sel:
|
|
187
|
+
if choice in choice_dict[aspect]:
|
|
188
|
+
sel[aspect] = choice
|
|
189
|
+
else:
|
|
190
|
+
print(f"Error: Assignment '{p_asmt}' failed: No such choice '{p_choice}' for aspect '{p_aspect}'.")
|
|
191
|
+
else:
|
|
192
|
+
print(f"Error: Assignment '{p_asmt}' failed: No such aspect '{p_aspect}'.")
|
|
193
|
+
else:
|
|
194
|
+
print(f"Error: Assignment '{asmt}' failed: Format error: Wrong number of '=' separators.")
|
|
195
|
+
changes = apply_selection(fpdict, vardict, sel, dry_run=False)
|
|
196
|
+
if len(changes) > 0:
|
|
197
|
+
if dry_run or verbose:
|
|
198
|
+
print_err(f'Changes ({len(changes)}):')
|
|
199
|
+
for uuid, order, change in sorted(changes, key=lambda x: natural_sort_key(x[1])):
|
|
200
|
+
print(' ' + change)
|
|
201
|
+
if not dry_run:
|
|
202
|
+
store_fpdict(b, fpdict)
|
|
203
|
+
if save_board(board, b):
|
|
204
|
+
if verbose:
|
|
205
|
+
print(f'Board saved to file "{board}".')
|
|
206
|
+
else:
|
|
207
|
+
print_err(f'Error: Failed to save board to file "{board}".')
|
|
208
|
+
return False
|
|
209
|
+
else:
|
|
210
|
+
if verbose or dry_run:
|
|
211
|
+
print('No changes.')
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def main():
|
|
215
|
+
parser = argparse.ArgumentParser(description=f"KiVar Command Line Interface ({version()})")
|
|
216
|
+
# parser.add_argument("--verbose", "-v", action="store_true", help="long output style (effect depends on command)")
|
|
217
|
+
parser.add_argument("-V", "--version", action="store_true", help="print version information and exit")
|
|
218
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
219
|
+
|
|
220
|
+
list_parser = subparsers.add_parser("list", help="list all available aspects and choices")
|
|
221
|
+
list_parser.add_argument("-s", "--selection", action="store_true", help="mark currently matching choices")
|
|
222
|
+
list_parser.add_argument("-l", "--long", action="store_true", help="long output style")
|
|
223
|
+
list_parser.add_argument("-d", "--detailed", action="store_true", help="show component assignments (implies --long)")
|
|
224
|
+
list_parser.add_argument("-c", "--prop-codes", action="store_true", help="display property codes (implies --detailed)")
|
|
225
|
+
# list_parser.add_argument("-v", "--verbose", action="store_true", help="verbose output")
|
|
226
|
+
list_parser.add_argument("board", help="KiCad PCB file name")
|
|
227
|
+
|
|
228
|
+
state_parser = subparsers.add_parser("state", help="show currently matching choice for each aspect")
|
|
229
|
+
state_parser.add_argument("-Q", "--query", action="append", help="query aspect for matching choice", metavar="aspect")
|
|
230
|
+
state_parser.add_argument("-a", "--all", action="store_true", help="list all aspects (default: list only aspects with matching choice)")
|
|
231
|
+
# state_parser.add_argument("-v", "--verbose", action="store_true", help="verbose output")
|
|
232
|
+
state_parser.add_argument("board", help="KiCad PCB file name")
|
|
233
|
+
|
|
234
|
+
check_parser = subparsers.add_parser("check", help="check all aspects for matching choices, exit with error if check fails")
|
|
235
|
+
# check_parser.add_argument("--verbose", "-v", action="store_true", help="verbose output")
|
|
236
|
+
# TODO add option to check only for dedicated list of aspects
|
|
237
|
+
check_parser.add_argument("board", help="KiCad PCB file name")
|
|
238
|
+
|
|
239
|
+
set_parser = subparsers.add_parser("set", help="assign choices to aspects")
|
|
240
|
+
set_parser.add_argument("-A", "--assign", action="append", help="assign choice to aspect ('str' format: \"aspect=choice\")", metavar="str")
|
|
241
|
+
set_parser.add_argument("-D", "--dry-run", action="store_true", help="only print assignments, do not really perform/save them")
|
|
242
|
+
set_parser.add_argument("-v", "--verbose", action="store_true", help="verbose output")
|
|
243
|
+
set_parser.add_argument("board", help="KiCad PCB file name")
|
|
244
|
+
|
|
245
|
+
args = parser.parse_args()
|
|
246
|
+
exitcode = 0
|
|
247
|
+
|
|
248
|
+
if args.version:
|
|
249
|
+
print(f"KiVar {version()}")
|
|
250
|
+
exitcode = 0
|
|
251
|
+
else:
|
|
252
|
+
# TODO do this check only prior to executing corresponding commands?
|
|
253
|
+
compatibility_problem = pcbnew_compatibility_error()
|
|
254
|
+
if compatibility_problem is not None:
|
|
255
|
+
print(f'Compatibility error:\n{compatibility_problem}')
|
|
256
|
+
exitcode = 3
|
|
257
|
+
else:
|
|
258
|
+
cmd = args.command
|
|
259
|
+
if cmd == "list":
|
|
260
|
+
if args.prop_codes: args.detailed = True
|
|
261
|
+
if args.detailed: args.long = True
|
|
262
|
+
if not list_command(board=args.board, long=args.long, prop_codes=args.prop_codes, detailed=args.detailed, selected=args.selection): exitcode = 1
|
|
263
|
+
elif cmd == "state":
|
|
264
|
+
if not state_command(board=args.board, all=args.all, query_aspect=args.query): exitcode = 1
|
|
265
|
+
elif cmd == "check":
|
|
266
|
+
if not check_command(board=args.board): exitcode = 1
|
|
267
|
+
elif cmd == "set":
|
|
268
|
+
if not set_command(board=args.board, assign=args.assign, dry_run=args.dry_run, verbose=args.verbose): exitcode = 1
|
|
269
|
+
else:
|
|
270
|
+
print_err(f'This is the KiVar CLI, version {version()}.')
|
|
271
|
+
parser.print_usage()
|
|
272
|
+
exitcode = 2
|
|
273
|
+
|
|
274
|
+
return exitcode
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
sys.exit(main())
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kivar
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: PCB Assembly Variants for KiCad
|
|
5
|
+
Home-page: https://github.com/markh-de/KiVar
|
|
6
|
+
Author: Mark Hämmerling
|
|
7
|
+
Author-email: dev@markh.de
|
|
8
|
+
License: MIT
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# KiVar − PCB Assembly Variants for KiCad
|
|
13
|
+
|
|
14
|
+
## Introduction
|
|
15
|
+
|
|
16
|
+
KiVar is a tool for **KiCad PCB Assembly Variant selection**, provided as platform-independent
|
|
17
|
+
|
|
18
|
+
* **Command Line Application** (this package) and
|
|
19
|
+
* **KiCad Action Plugin** (available in KiCad PCM).
|
|
20
|
+
|
|
21
|
+
PCB component variation rules are defined in component (i.e. symbol or footprint) fields. This allows for the complete variant configuration to be contained in the schematic and board files without requiring external data from outside the native KiCad design files.
|
|
22
|
+
|
|
23
|
+
The name _KiVar_ (for _KiCad Variants_, obviously) can also be read as an acronym for _**Ki**Cad **V**ariation **a**ssignment **r**ules_.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
KiVar assigns PCB component **values**, **field content** and **attributes** (such as _Do not populate_, _Not in position files_, _Not in BoM_) according to variation rules specified in footprint fields. When applying those rules, components are modified _in place_, allowing for immediate update of the PCB design as well as the 3D view and enabling compatibility with _any_ exporter.
|
|
28
|
+
|
|
29
|
+
Back-propagation of modified component data from the PCB to the schematic can be done in an extra step.
|
|
30
|
+
|
|
31
|
+
## What to Expect
|
|
32
|
+
|
|
33
|
+
Example usage of the **KiVar Command Line Interface app**:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
$ kivar list --selection kivar-demo.kicad_pcb
|
|
37
|
+
BOOT_SRC: [EMMC] JP NAND SD
|
|
38
|
+
EEPROM_ADDR: 0x54 [0x55]
|
|
39
|
+
I_LED_MA: 10 20 30 40 50 60 70 80 90 [100] 110 120 130 140 150 JP
|
|
40
|
+
IOEXP_TYPE/ADDR: 9535/0x20 [9535/0x24] 9539/0x74
|
|
41
|
+
ISL91127: [IRAZ] IRNZ
|
|
42
|
+
UVLO_LO/HI: 2.41V/3.40V [3.15V/3.57V]
|
|
43
|
+
|
|
44
|
+
$ kivar set --assign 'I_LED_MA=60' --assign 'BOOT_SRC=NAND' --verbose kivar-demo.kicad_pcb
|
|
45
|
+
Changes (16):
|
|
46
|
+
Change R9 'Do not populate' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
47
|
+
Change R9 'Exclude from bill of materials' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
48
|
+
Change R9 'Exclude from position files' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
49
|
+
Change R9 field 'ChoiceText' from 'SoM eMMC' to 'SoM NAND' (BOOT_SRC=NAND).
|
|
50
|
+
Change R10 'Do not populate' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
51
|
+
Change R10 'Exclude from bill of materials' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
52
|
+
Change R10 'Exclude from position files' from 'true' to 'false' (BOOT_SRC=NAND).
|
|
53
|
+
Change R11 'Do not populate' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
54
|
+
Change R11 'Exclude from bill of materials' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
55
|
+
Change R11 'Exclude from position files' from 'false' to 'true' (BOOT_SRC=NAND).
|
|
56
|
+
Change R21 'Do not populate' from 'false' to 'true' (I_LED_MA=60).
|
|
57
|
+
Change R21 'Exclude from bill of materials' from 'false' to 'true' (I_LED_MA=60).
|
|
58
|
+
Change R21 'Exclude from position files' from 'false' to 'true' (I_LED_MA=60).
|
|
59
|
+
Change R22 'Do not populate' from 'true' to 'false' (I_LED_MA=60).
|
|
60
|
+
Change R22 'Exclude from bill of materials' from 'true' to 'false' (I_LED_MA=60).
|
|
61
|
+
Change R22 'Exclude from position files' from 'true' to 'false' (I_LED_MA=60).
|
|
62
|
+
Board saved to file "kivar-demo.kicad_pcb".
|
|
63
|
+
|
|
64
|
+
$ kivar list --selection kivar-demo.kicad_pcb
|
|
65
|
+
BOOT_SRC: EMMC JP [NAND] SD
|
|
66
|
+
EEPROM_ADDR: 0x54 [0x55]
|
|
67
|
+
I_LED_MA: 10 20 30 40 50 [60] 70 80 90 100 110 120 130 140 150 JP
|
|
68
|
+
IOEXP_TYPE/ADDR: 9535/0x20 [9535/0x24] 9539/0x74
|
|
69
|
+
ISL91127: [IRAZ] IRNZ
|
|
70
|
+
UVLO_LO/HI: 2.41V/3.40V [3.15V/3.57V]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Concepts
|
|
74
|
+
|
|
75
|
+
Key concepts of KiVar are:
|
|
76
|
+
|
|
77
|
+
* Designs may contain **multiple** independent variation **aspects** (i.e. dimensions or degrees of freedom).
|
|
78
|
+
* Variation rules are **fully contained** in component fields of native design files (no external configuration files) and **portable** (i.e. copying components to another design keeps their variation specification intact).
|
|
79
|
+
* Component values and attributes are modified **in place**, enabling compatibility with all exporters that work on the actual component data.
|
|
80
|
+
* **No external state information** is stored; currently matching variation choices are detected automatically.
|
|
81
|
+
|
|
82
|
+
## Project Page
|
|
83
|
+
|
|
84
|
+
For the usage manual and a demo project check out the [KiVar Project Page](https://github.com/markh-de/KiVar).
|
|
85
|
+
|
|
86
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kivar
|
kivar-0.2.0/setup.cfg
ADDED
kivar-0.2.0/setup.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'README.md'), 'r') as f:
|
|
5
|
+
long_descr = f.read()
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name='kivar',
|
|
9
|
+
description='PCB Assembly Variants for KiCad',
|
|
10
|
+
long_description=long_descr,
|
|
11
|
+
long_description_content_type='text/markdown',
|
|
12
|
+
license='MIT',
|
|
13
|
+
url='https://github.com/markh-de/KiVar',
|
|
14
|
+
author='Mark Hämmerling',
|
|
15
|
+
author_email='dev@markh.de',
|
|
16
|
+
version="0.2.0",
|
|
17
|
+
packages=["kivar"],
|
|
18
|
+
install_requires=[],
|
|
19
|
+
entry_points={
|
|
20
|
+
"console_scripts": [
|
|
21
|
+
"kivar = kivar:main"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
)
|