harnice 0.3.0__py3-none-any.whl

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.
Files changed (41) hide show
  1. harnice/__init__.py +0 -0
  2. harnice/__main__.py +4 -0
  3. harnice/cli.py +234 -0
  4. harnice/fileio.py +295 -0
  5. harnice/gui/launcher.py +426 -0
  6. harnice/lists/channel_map.py +182 -0
  7. harnice/lists/circuits_list.py +302 -0
  8. harnice/lists/disconnect_map.py +237 -0
  9. harnice/lists/formboard_graph.py +63 -0
  10. harnice/lists/instances_list.py +280 -0
  11. harnice/lists/library_history.py +40 -0
  12. harnice/lists/manifest.py +93 -0
  13. harnice/lists/post_harness_instances_list.py +66 -0
  14. harnice/lists/rev_history.py +325 -0
  15. harnice/lists/signals_list.py +135 -0
  16. harnice/products/__init__.py +1 -0
  17. harnice/products/cable.py +152 -0
  18. harnice/products/chtype.py +80 -0
  19. harnice/products/device.py +844 -0
  20. harnice/products/disconnect.py +225 -0
  21. harnice/products/flagnote.py +139 -0
  22. harnice/products/harness.py +522 -0
  23. harnice/products/macro.py +10 -0
  24. harnice/products/part.py +640 -0
  25. harnice/products/system.py +125 -0
  26. harnice/products/tblock.py +270 -0
  27. harnice/state.py +57 -0
  28. harnice/utils/appearance.py +51 -0
  29. harnice/utils/circuit_utils.py +326 -0
  30. harnice/utils/feature_tree_utils.py +183 -0
  31. harnice/utils/formboard_utils.py +973 -0
  32. harnice/utils/library_utils.py +333 -0
  33. harnice/utils/note_utils.py +417 -0
  34. harnice/utils/svg_utils.py +819 -0
  35. harnice/utils/system_utils.py +563 -0
  36. harnice-0.3.0.dist-info/METADATA +32 -0
  37. harnice-0.3.0.dist-info/RECORD +41 -0
  38. harnice-0.3.0.dist-info/WHEEL +5 -0
  39. harnice-0.3.0.dist-info/entry_points.txt +3 -0
  40. harnice-0.3.0.dist-info/licenses/LICENSE +19 -0
  41. harnice-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,417 @@
1
+ import ast
2
+ from harnice import fileio
3
+ from harnice.lists import instances_list, rev_history
4
+ from harnice import state
5
+
6
+ note_counter = 1
7
+
8
+
9
+ def new_note(
10
+ note_type,
11
+ note_text,
12
+ note_number=None,
13
+ bubble_text=None,
14
+ shape_mpn=None,
15
+ shape_lib_subpath=None,
16
+ shape_lib_repo="https://github.com/harnice/harnice",
17
+ affectedinstances=None,
18
+ ):
19
+ """
20
+ Creates or updates a note instance.
21
+
22
+ Behavior:
23
+ - If a note with identical (note_type, note_text) already exists:
24
+ * If either existing or new affectedinstances is empty → ERROR.
25
+ * Else → merge affectedinstances into existing note, DO NOT create a new one.
26
+ """
27
+
28
+ if not shape_mpn:
29
+ shape_mpn = note_type
30
+
31
+ global note_counter
32
+
33
+ # ------------------------------------------------------------
34
+ # 1. Search for an existing identical note
35
+ # ------------------------------------------------------------
36
+ existing = None
37
+ for instance in fileio.read_tsv("instances list"):
38
+ if (
39
+ instance.get("item_type") == "note"
40
+ and instance.get("note_type") == note_type
41
+ and instance.get("note_text") == note_text
42
+ ):
43
+ existing = instance
44
+ break
45
+
46
+ # Normalize new affectedinstances
47
+ new_affected_instances = affectedinstances or []
48
+ if isinstance(new_affected_instances, str):
49
+ import ast
50
+
51
+ try:
52
+ new_affected_instances = ast.literal_eval(new_affected_instances)
53
+ except Exception:
54
+ raise ValueError(f"Malformed affectedinstances value: {affectedinstances}")
55
+
56
+ # ------------------------------------------------------------
57
+ # 2. CASE A — Existing note found
58
+ # ------------------------------------------------------------
59
+ if existing:
60
+ # Parse existing list safely
61
+ old_affected_instances_raw = existing.get("note_affected_instances")
62
+ if isinstance(old_affected_instances_raw, str):
63
+ import ast
64
+
65
+ try:
66
+ old_affected_instances = ast.literal_eval(old_affected_instances_raw)
67
+ except Exception:
68
+ old_affected_instances = []
69
+ else:
70
+ old_affected_instances = old_affected_instances_raw or []
71
+
72
+ # A.1 — If either side lacks affected instances → ERROR
73
+ if not old_affected_instances or not new_affected_instances:
74
+ raise ValueError(
75
+ f"Note '{note_type}:{note_text}' already exists but "
76
+ "one of the notes has no affected_instances. "
77
+ "This note has already been assigned."
78
+ )
79
+
80
+ # A.2 — Merge and deduplicate into existing instance
81
+ merged = list(
82
+ dict.fromkeys(old_affected_instances + new_affected_instances)
83
+ ) # keep order, remove dupes
84
+ existing["note_affected_instances"] = merged
85
+
86
+ # Write modification back to instances_list
87
+ instances_list.modify(
88
+ existing.get("instance_name"), {"note_affected_instances": merged}
89
+ )
90
+ return existing
91
+
92
+ # ------------------------------------------------------------
93
+ # 3. CASE B — No existing note, create a new one
94
+ # ------------------------------------------------------------
95
+ instances_list.new_instance(
96
+ f"note-{note_counter}",
97
+ {
98
+ "item_type": "note",
99
+ "note_type": note_type,
100
+ "print_name": bubble_text,
101
+ "note_text": note_text,
102
+ "mpn": shape_mpn,
103
+ "lib_repo": shape_lib_repo,
104
+ "lib_subpath": shape_lib_subpath,
105
+ "note_number": note_number,
106
+ "note_affected_instances": new_affected_instances,
107
+ },
108
+ )
109
+
110
+ note_counter += 1
111
+
112
+
113
+ def assign_buildnote_numbers():
114
+ """
115
+ Assigns sequential numbers to build note instances.
116
+
117
+ Iterates through all note instances in the instances list and assigns sequential
118
+ numbers to build notes. Each build note gets a unique number and that number is
119
+ set as both the `note_number` and `print_name` fields.
120
+ """
121
+ build_note_counter = 0
122
+ for instance in fileio.read_tsv("instances list"):
123
+ if instance.get("note_type") == "build_note":
124
+ if instance.get("item_type") == "note":
125
+ build_note_counter += 1
126
+ instances_list.modify(
127
+ instance.get("instance_name"),
128
+ {"note_number": build_note_counter, "print_name": build_note_counter},
129
+ )
130
+
131
+
132
+ def make_rev_history_notes(rev):
133
+ """
134
+ Creates revision change callout notes based on revision history.
135
+
136
+ Creates a revision change callout note for a given revision, linking it to
137
+ the instances affected by that revision. The note text comes from the
138
+ revision's update description.
139
+
140
+ **Args:**
141
+ - `rev` (dict): Revision dictionary from revision history containing at least
142
+ `'rev'` and `'revisionupdates'` fields.
143
+ """
144
+ affected_instances = rev_history.info(rev=rev.get("rev"), field="affectedinstances")
145
+
146
+ if affected_instances: # safer + more pythonic
147
+ new_note(
148
+ note_type="rev_change_callout",
149
+ note_text=rev.get("revisionupdates"),
150
+ note_number=rev.get("rev"),
151
+ bubble_text=rev.get("rev"),
152
+ shape_mpn="rev_change_callout",
153
+ shape_lib_repo="https://github.com/harnice/harnice",
154
+ affectedinstances=affected_instances,
155
+ )
156
+
157
+
158
+ def make_bom_flagnote(affected_instance, output_csys_name):
159
+ """
160
+ Creates a BOM item flagnote dictionary for an instance.
161
+
162
+ Creates a flagnote configuration dictionary that displays the BOM line number
163
+ for a given instance. The flagnote is positioned at the specified output
164
+ coordinate system of the instance.
165
+
166
+ **Args:**
167
+ - `affected_instance` (dict): Instance dictionary to create a flagnote for.
168
+ - `output_csys_name` (str): Name of the output coordinate system where the
169
+ flagnote should be positioned.
170
+
171
+ **Returns:**
172
+ - `dict`: A flagnote instance dictionary ready to be added to the instances list.
173
+ """
174
+ return {
175
+ "net": state.net,
176
+ "instance_name": f"note-bom_item-{affected_instance.get('instance_name')}",
177
+ "print_name": affected_instance.get("bom_line_number"),
178
+ "mpn": "bom_item",
179
+ "item_type": "flagnote",
180
+ "parent_instance": affected_instance.get("instance_name"),
181
+ "segment_group": affected_instance.get("segment_group"),
182
+ "connector_group": affected_instance.get("connector_group"),
183
+ "parent_csys_instance_name": affected_instance.get("instance_name"),
184
+ "parent_csys_outputcsys_name": output_csys_name,
185
+ "absolute_rotation": 0,
186
+ "note_type": "bom_item",
187
+ "note_affected_instances": [affected_instance.get("instance_name")],
188
+ "lib_repo": "https://github.com/harnice/harnice",
189
+ }
190
+
191
+
192
+ def make_part_name_flagnote(affected_instance, output_csys_name):
193
+ """
194
+ Creates a part name flagnote dictionary for an instance.
195
+
196
+ Creates a flagnote configuration dictionary that displays the print name
197
+ of a given instance. The flagnote is positioned at the specified output
198
+ coordinate system of the instance.
199
+
200
+ **Args:**
201
+ - `affected_instance` (dict): Instance dictionary to create a flagnote for.
202
+ - `output_csys_name` (str): Name of the output coordinate system where the
203
+ flagnote should be positioned.
204
+
205
+ **Returns:**
206
+ - `dict`: A flagnote instance dictionary ready to be added to the instances list.
207
+ """
208
+ return {
209
+ "net": state.net,
210
+ "instance_name": f"note-part_name-{affected_instance.get('instance_name')}",
211
+ "print_name": affected_instance.get("print_name"),
212
+ "mpn": "part_name",
213
+ "item_type": "flagnote",
214
+ "parent_instance": affected_instance.get("instance_name"),
215
+ "segment_group": affected_instance.get("segment_group"),
216
+ "connector_group": affected_instance.get("connector_group"),
217
+ "parent_csys_instance_name": affected_instance.get("instance_name"),
218
+ "parent_csys_outputcsys_name": output_csys_name,
219
+ "absolute_rotation": 0,
220
+ "note_type": "part_name",
221
+ "note_affected_instances": [affected_instance.get("instance_name")],
222
+ "lib_repo": "https://github.com/harnice/harnice",
223
+ }
224
+
225
+
226
+ def make_buildnote_flagnote(note_instance, affected_instance, output_csys_name):
227
+ """
228
+ Creates a build note flagnote dictionary linking a note to an instance.
229
+
230
+ Creates a flagnote configuration dictionary that displays a build note on
231
+ a specific instance. The flagnote shows the build note's print name and is
232
+ positioned at the specified output coordinate system of the affected instance.
233
+
234
+ **Args:**
235
+ - `note_instance` (dict): Build note instance dictionary.
236
+ - `affected_instance` (dict): Instance dictionary to attach the flagnote to.
237
+ - `output_csys_name` (str): Name of the output coordinate system where the
238
+ flagnote should be positioned.
239
+
240
+ **Returns:**
241
+ - `dict`: A flagnote instance dictionary ready to be added to the instances list.
242
+ """
243
+ return {
244
+ "net": state.net,
245
+ "instance_name": f"note-build_note-{note_instance.get('instance_name')}-{affected_instance.get('instance_name')}",
246
+ "print_name": note_instance.get("print_name"),
247
+ "mpn": "build_note",
248
+ "item_type": "flagnote",
249
+ "parent_instance": affected_instance.get("instance_name"),
250
+ "segment_group": affected_instance.get("segment_group"),
251
+ "connector_group": affected_instance.get("connector_group"),
252
+ "parent_csys_instance_name": affected_instance.get("instance_name"),
253
+ "parent_csys_outputcsys_name": output_csys_name,
254
+ "absolute_rotation": 0,
255
+ "note_type": "build_note",
256
+ "note_affected_instances": [affected_instance.get("instance_name")],
257
+ "lib_repo": "https://github.com/harnice/harnice",
258
+ }
259
+
260
+
261
+ def make_rev_change_flagnote(note_instance, affected_instance, output_csys_name):
262
+ """
263
+ Creates a revision change callout flagnote dictionary linking a note to an instance.
264
+
265
+ Creates a flagnote configuration dictionary that displays a revision change
266
+ callout note on a specific instance. The flagnote shows the revision number
267
+ and is positioned at the specified output coordinate system of the affected instance.
268
+
269
+ **Args:**
270
+ - `note_instance` (dict): Revision change callout note instance dictionary.
271
+ - `affected_instance` (dict): Instance dictionary to attach the flagnote to.
272
+ - `output_csys_name` (str): Name of the output coordinate system where the
273
+ flagnote should be positioned.
274
+
275
+ **Returns:**
276
+ - `dict`: A flagnote instance dictionary ready to be added to the instances list.
277
+ """
278
+ return {
279
+ "net": state.net,
280
+ "instance_name": f"note-rev_change_callout-{note_instance.get('instance_name')}-{affected_instance.get('instance_name')}",
281
+ "print_name": note_instance.get("print_name"),
282
+ "mpn": "rev_change_callout",
283
+ "item_type": "flagnote",
284
+ "parent_instance": affected_instance.get("instance_name"),
285
+ "segment_group": affected_instance.get("segment_group"),
286
+ "connector_group": affected_instance.get("connector_group"),
287
+ "parent_csys_instance_name": affected_instance.get("instance_name"),
288
+ "parent_csys_outputcsys_name": output_csys_name,
289
+ "absolute_rotation": 0,
290
+ "note_type": "rev_change_callout",
291
+ "note_affected_instances": [affected_instance.get("instance_name")],
292
+ "lib_repo": "https://github.com/harnice/harnice",
293
+ }
294
+
295
+
296
+ def parse_note_instance(instance):
297
+ """
298
+ Return a full copy of `instance`, but with note_affected_instances
299
+ parsed into a real Python list (or left alone if blank).
300
+ """
301
+ parsed = {}
302
+
303
+ for key, value in instance.items():
304
+ if key == "note_affected_instances":
305
+ if isinstance(value, str) and value.strip():
306
+ try:
307
+ parsed[key] = ast.literal_eval(value)
308
+ except Exception:
309
+ parsed[key] = [] # fallback if malformed
310
+ else:
311
+ parsed[key] = []
312
+ else:
313
+ parsed[key] = value
314
+
315
+ return parsed
316
+
317
+
318
+ def get_lib_build_notes(instance):
319
+ """
320
+ Returns list of build_notes for this instance from the TSV row.
321
+ Safely parses with ast.literal_eval.
322
+ Always returns a Python list.
323
+ """
324
+
325
+ raw = instance.get("lib_build_notes")
326
+
327
+ if not raw or raw in ["", None]:
328
+ return []
329
+
330
+ try:
331
+ # Expecting a string representation of a list
332
+ parsed = ast.literal_eval(raw)
333
+
334
+ # Ensure it's actually a list
335
+ return parsed if isinstance(parsed, list) else []
336
+
337
+ except Exception:
338
+ # Malformed literal → fail safe
339
+ return []
340
+
341
+
342
+ def get_lib_tools(instance):
343
+ """
344
+ Returns list of tools for this instance from the TSV row.
345
+ Safely parses with ast.literal_eval.
346
+ Always returns a Python list.
347
+ """
348
+
349
+ raw = instance.get("lib_tools")
350
+
351
+ if not raw or raw in ["", None]:
352
+ return []
353
+
354
+ try:
355
+ # Expecting a string representation of a list
356
+ parsed = ast.literal_eval(raw)
357
+
358
+ # Ensure it's actually a list
359
+ return parsed if isinstance(parsed, list) else []
360
+
361
+ except Exception:
362
+ # Malformed literal → fail safe
363
+ return []
364
+
365
+
366
+ def combine_notes(keep_note_text, merge_note_texts, note_type=None):
367
+ """
368
+ Combines multiple notes by merging their affected instances into one note.
369
+
370
+ Merges one or more notes into a single note by combining their affected instances
371
+ lists. The note to keep is identified by `keep_note_text`, and all notes matching
372
+ `merge_note_texts` are merged into it and then removed.
373
+
374
+ **Args:**
375
+ - `keep_note_text` (str): The `note_text` value of the note to keep and merge others into.
376
+ - `merge_note_texts` (list): List of `note_text` values to find and merge into the kept note.
377
+ - `note_type` (list, optional): If provided, only notes with `note_type` in this list
378
+ will be considered for merging. If `None`, all notes are considered.
379
+ """
380
+ keep_note_instance = None
381
+ merge_note_instances_raw = []
382
+ for instance in fileio.read_tsv("instances list"):
383
+ if instance.get("note_text") in [None, ""]:
384
+ continue
385
+ if instance.get("note_text") == keep_note_text:
386
+ if note_type:
387
+ if instance.get("note_type") in note_type:
388
+ keep_note_instance = instance
389
+ else:
390
+ keep_note_instance = instance
391
+ if instance.get("note_text") in merge_note_texts:
392
+ if note_type:
393
+ if instance.get("note_type") in note_type:
394
+ merge_note_instances_raw.append(instance)
395
+ else:
396
+ merge_note_instances_raw.append(instance)
397
+
398
+ merge_note_instances_parsed = []
399
+ for instance in merge_note_instances_raw:
400
+ merge_note_instances_parsed.append(parse_note_instance(instance))
401
+
402
+ new_affected_instances = set()
403
+
404
+ for merged_note_instance in merge_note_instances_parsed:
405
+ for affected_instance in merged_note_instance.get("note_affected_instances"):
406
+ new_affected_instances.add(affected_instance)
407
+ instances_list.remove_instance(merged_note_instance)
408
+
409
+ for affected_instance in parse_note_instance(keep_note_instance).get(
410
+ "note_affected_instances"
411
+ ):
412
+ new_affected_instances.add(affected_instance)
413
+
414
+ instances_list.modify(
415
+ keep_note_instance.get("instance_name"),
416
+ {"note_affected_instances": list(new_affected_instances)},
417
+ )