• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SchrodingersGat / KiBoM / 4891038286

pending completion
4891038286

push

github

Oliver
Check for 'dnp' attribute introduced in kicad v7

4 of 4 new or added lines in 1 file covered. (100.0%)

1245 of 1581 relevant lines covered (78.75%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.58
/kibom/component.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
1✔
3

4
import re
1✔
5
import sys
1✔
6

7
from .columns import ColumnList
1✔
8
from .preferences import BomPref
1✔
9
from . import units
1✔
10
from . import debug
1✔
11
from .sort import natural_sort
1✔
12

13
# String matches for marking a component as "do not fit"
14
DNF = [
1✔
15
    "dnf",
16
    "dnl",
17
    "dnp",
18
    "do not fit",
19
    "do not place",
20
    "do not load",
21
    "nofit",
22
    "nostuff",
23
    "noplace",
24
    "noload",
25
    "not fitted",
26
    "not loaded",
27
    "not placed",
28
    "no stuff",
29
]
30

31
# String matches for marking a component as "do not change" or "fixed"
32
DNC = [
1✔
33
    "dnc",
34
    "do not change",
35
    "no change",
36
    "fixed"
37
]
38

39

40
class Component():
1✔
41
    """Class for a component, aka 'comp' in the xml netlist file.
42
    This component class is implemented by wrapping an xmlElement instance
43
    with accessors.  The xmlElement is held in field 'element'.
44
    """
45

46
    def __init__(self, xml_element, prefs=None):
1✔
47
        self.element = xml_element
1✔
48
        self.libpart = None
1✔
49

50
        if not prefs:
1✔
51
            prefs = BomPref()
×
52

53
        self.prefs = prefs
1✔
54

55
        # Set to true when this component is included in a component group
56
        self.grouped = False
1✔
57

58
    # Compare the value of this part, to the value of another part (see if they match)
59
    def compareValue(self, other):
1✔
60
        # Simple string comparison
61
        if self.getValue().lower() == other.getValue().lower():
1✔
62
            return True
1✔
63

64
        # Otherwise, perform a more complicated value comparison
65
        if units.compareValues(self.getValue(), other.getValue()):
1✔
66
            return True
1✔
67

68
        # Ignore value if both components are connectors
69
        if self.prefs.groupConnectors:
1✔
70
            if 'connector' in self.getLibName().lower() and 'connector' in other.getLibName().lower():
1✔
71
                return True
×
72

73
        # No match, return False
74
        return False
1✔
75

76
    # Determine if two parts have the same name
77
    def comparePartName(self, other):
1✔
78
        pn1 = self.getPartName().lower()
1✔
79
        pn2 = other.getPartName().lower()
1✔
80

81
        # Simple direct match
82
        if pn1 == pn2:
1✔
83
            return True
1✔
84

85
        # Compare part aliases e.g. "c" to "c_small"
86
        for alias in self.prefs.aliases:
1✔
87
            if pn1 in alias and pn2 in alias:
1✔
88
                return True
×
89

90
        return False
1✔
91

92
    def compareField(self, other, field):
1✔
93

94
        this_field = self.getField(field).lower()
1✔
95
        other_field = other.getField(field).lower()
1✔
96

97
        # If blank comparisons are allowed
98
        if this_field == "" or other_field == "":
1✔
99
            if not self.prefs.mergeBlankFields:
×
100
                return False
×
101

102
        if this_field == other_field:
1✔
103
            return True
1✔
104

105
        return False
1✔
106

107
    def __eq__(self, other):
1✔
108
        """
109
        Equivalency operator is used to determine if two parts are 'equal'
110
        """
111
        
112
        # 'fitted' value must be the same for both parts
113
        if self.isFitted() != other.isFitted():
1✔
114
            return False
1✔
115

116
        # 'fixed' value must be the same for both parts
117
        if self.isFixed() != other.isFixed():
1✔
118
            return False
1✔
119

120
        if len(self.prefs.groups) == 0:
1✔
121
            return False
×
122

123
        for c in self.prefs.groups:
1✔
124
            # Perform special matches
125
            if c.lower() == ColumnList.COL_VALUE.lower():
1✔
126
                if not self.compareValue(other):
1✔
127
                    return False
1✔
128
            # Match part name
129
            elif c.lower() == ColumnList.COL_PART.lower():
1✔
130
                if not self.comparePartName(other):
1✔
131
                    return False
1✔
132

133
            # Generic match
134
            elif not self.compareField(other, c):
1✔
135
                return False
1✔
136

137
        return True
1✔
138

139
    def setLibPart(self, part):
1✔
140
        self.libpart = part
1✔
141

142
    def getPrefix(self):
1✔
143
        """
144
        Get the reference prefix
145
        e.g. if this component has a reference U12, will return "U"
146
        """
147
        
148
        prefix = ""
1✔
149

150
        for c in self.getRef():
1✔
151
            if c.isalpha():
1✔
152
                prefix += c
1✔
153
            else:
154
                break
1✔
155

156
        return prefix
1✔
157

158
    def getSuffix(self):
1✔
159
        """
160
        Return the reference suffix #
161
        e.g. if this component has a reference U12, will return "12"
162
        """
163
        
164
        suffix = ""
×
165

166
        for c in self.getRef():
×
167
            if c.isalpha():
×
168
                suffix = ""
×
169
            else:
170
                suffix += c
×
171

172
        return int(suffix)
×
173

174
    def getLibPart(self):
1✔
175
        return self.libpart
1✔
176

177
    def getPartName(self):
1✔
178
        return self.element.get("libsource", "part")
1✔
179

180
    def getLibName(self):
1✔
181
        return self.element.get("libsource", "lib")
1✔
182

183
    def getSheetpathNames(self):
1✔
184
        return self.element.get("sheetpath", "names")
1✔
185

186
    def getDescription(self):
1✔
187
        """Extract the 'description' field for this component"""
188

189
        # Give priority to a user "description" field
190
        ret = self.element.get("field", "name", "description")
1✔
191
        if ret:
1✔
192
            return ret
×
193

194
        ret = self.element.get("field", "name", "Description")
1✔
195
        if ret:
1✔
196
            return ret
×
197

198
        try:
1✔
199
            ret = self.element.get("libsource", "description")
1✔
200
        except:
×
201
            # Compatibility with old KiCad versions (4.x)
202
            ret = self.element.get("field", "name", "description")
×
203

204
        if ret == "":
1✔
205
            try:
×
206
                ret = self.libpart.getDescription()
×
207
            except AttributeError:
×
208
                # Raise a good error description here, so the user knows what the culprit component is.
209
                # (sometimes libpart is None)
210
                raise AttributeError('Could not get description for part {}{}.'.format(self.getPrefix(),
×
211
                                     self.getSuffix()))
212

213
        return ret
1✔
214

215
    def setValue(self, value):
1✔
216
        """Set the value of this component"""
217
        v = self.element.getChild("value")
×
218
        if v:
×
219
            v.setChars(value)
×
220

221
    def getValue(self):
1✔
222
        return self.element.get("value")
1✔
223

224
    # Try to better sort R, L and C components
225
    def getValueSort(self):
1✔
226
        pref = self.getPrefix()
1✔
227
        if pref in 'RLC' or pref == 'RV':
1✔
228
            res = units.compMatch(self.getValue())
1✔
229
            if res:
1✔
230
                value, mult, unit = res
1✔
231
                if pref in "CL":
1✔
232
                    # fempto Farads
233
                    value = "{0:15d}".format(int(value * 1e15 * mult + 0.1))
1✔
234
                else:
235
                    # milli Ohms
236
                    value = "{0:15d}".format(int(value * 1000 * mult + 0.1))
1✔
237
                return value
1✔
238
        return self.element.get("value")
×
239

240
    def setField(self, name, value):
1✔
241
        """ Set the value of the specified field """
242

243
        # Description field
244
        doc = self.element.getChild('libsource')
×
245
        if doc:
×
246
            for att_name, att_value in doc.attributes.items():
×
247
                if att_name.lower() == name.lower():
×
248
                    doc.attributes[att_name] = value
×
249
                    return value
×
250

251
        # Common fields
252
        field = self.element.getChild(name.lower())
×
253
        if field:
×
254
            field.setChars(value)
×
255
            return value
×
256

257
        # Other fields
258
        fields = self.element.getChild('fields')
×
259
        if fields:
×
260
            for field in fields.getChildren():
×
261
                if field.get('field', 'name') == name:
×
262
                    field.setChars(value)
×
263
                    return value
×
264

265
        return None
×
266

267
    def getField(self, name, ignoreCase=True, libraryToo=True):
1✔
268
        """Return the value of a field named name. The component is first
269
        checked for the field, and then the components library part is checked
270
        for the field. If the field doesn't exist in either, an empty string is
271
        returned
272

273
        Keywords:
274
        name -- The name of the field to return the value for
275
        libraryToo --   look in the libpart's fields for the same name if not found
276
                        in component itself
277
        """
278

279
        fp = self.getFootprint().split(":")
1✔
280

281
        if name.lower() == ColumnList.COL_REFERENCE.lower():
1✔
282
            return self.getRef().strip()
1✔
283

284
        elif name.lower() == ColumnList.COL_DESCRIPTION.lower():
1✔
285
            return self.getDescription().strip()
×
286

287
        elif name.lower() == ColumnList.COL_DATASHEET.lower():
1✔
288
            return self.getDatasheet().strip()
×
289

290
        # Footprint library is first element
291
        elif name.lower() == ColumnList.COL_FP_LIB.lower():
1✔
292
            if len(fp) > 1:
1✔
293
                return fp[0].strip()
1✔
294
            else:
295
                # Explicit empty return
296
                return ""
×
297

298
        elif name.lower() == ColumnList.COL_FP.lower():
1✔
299
            if len(fp) > 1:
1✔
300
                return fp[1].strip()
1✔
301
            elif len(fp) == 1:
×
302
                return fp[0]
×
303
            else:
304
                return ""
×
305

306
        elif name.lower() == ColumnList.COL_VALUE.lower():
1✔
307
            return self.getValue().strip()
×
308

309
        elif name.lower() == ColumnList.COL_PART.lower():
1✔
310
            return self.getPartName().strip()
1✔
311

312
        elif name.lower() == ColumnList.COL_PART_LIB.lower():
1✔
313
            return self.getLibName().strip()
1✔
314

315
        elif name.lower() == ColumnList.COL_SHEETPATH.lower():
1✔
316
            return self.getSheetpathNames().strip()
×
317

318
        # Other fields (case insensitive)
319
        for f in self.getFieldNames():
1✔
320
            if f.lower() == name.lower():
1✔
321
                field = self.element.get("field", "name", f)
1✔
322

323
                if field == "" and libraryToo:
1✔
324
                    field = self.libpart.getField(f)
×
325

326
                return field.strip()
1✔
327

328
        # Could not find a matching field
329
        return ""
1✔
330

331
    def getFieldNames(self):
1✔
332
        """Return a list of field names in play for this component.  Mandatory
333
        fields are not included, and they are: Value, Footprint, Datasheet, Ref.
334
        The netlist format only includes fields with non-empty values.  So if a field
335
        is empty, it will not be present in the returned list.
336
        """
337

338
        fieldNames = []
1✔
339
        
340
        fields = self.element.getChild('fields')
1✔
341
        
342
        if fields:
1✔
343
            for f in fields.getChildren():
1✔
344
                fieldNames.append(f.get('field', 'name'))
1✔
345
        
346
        return fieldNames
1✔
347

348
    def getRef(self):
1✔
349
        return self.element.get("comp", "ref")
1✔
350

351
    def isFitted(self):
1✔
352
        """ Determine if a component is FITTED or not """
353

354
        # First, check for the 'dnp' attribute (added in KiCad 7.0)
355
        for child in self.element.getChildren():
1✔
356
            if child.name == 'property':
1✔
357
                if child.attributes.get('name', '').lower() == 'dnp':
×
358
                    return False
×
359

360
        # Check the value field first
361
        if self.getValue().lower() in DNF:
1✔
362
            return False
×
363

364
        check = self.getField(self.prefs.configField).lower()
1✔
365
        # Empty value means part is fitted
366
        if check == "":
1✔
367
            return True
1✔
368

369
        # Also support space separated list (simple cases)
370
        opts = check.split(" ")
1✔
371
        for opt in opts:
1✔
372
            if opt.lower() in DNF:
1✔
373
                return False
1✔
374

375
        # Variants logic
376
        opts = check.split(",")
1✔
377
        # Exclude components that match a -VARIANT
378
        for opt in opts:
1✔
379
            opt = opt.strip()
1✔
380
            # Any option containing a DNF is not fitted
381
            if opt in DNF:
1✔
382
                return False
×
383
            # Options that start with '-' are explicitly removed from certain configurations
384
            if opt.startswith("-") and opt[1:] in self.prefs.pcbConfig:
1✔
385
                return False
×
386
        # Include components that match +VARIANT
387
        exclusive = False
1✔
388
        for opt in opts:
1✔
389
            # Options that start with '+' are fitted only for certain configurations
390
            if opt.startswith("+"):
1✔
391
                exclusive = True
×
392
                if opt[1:] in self.prefs.pcbConfig:
×
393
                    return True
×
394
        # No match
395
        return not exclusive
1✔
396

397
    def isFixed(self):
1✔
398
        """ Determine if a component is FIXED or not.
399
            Fixed components shouldn't be replaced without express authorization """
400

401
        # Check the value field first
402
        if self.getValue().lower() in DNC:
1✔
403
            return True
×
404

405
        check = self.getField(self.prefs.configField).lower()
1✔
406
        # Empty is not fixed
407
        if check == "":
1✔
408
            return False
1✔
409

410
        opts = check.split(" ")
1✔
411
        for opt in opts:
1✔
412
            if opt.lower() in DNC:
1✔
413
                return True
1✔
414

415
        opts = check.split(",")
1✔
416
        for opt in opts:
1✔
417
            if opt.lower() in DNC:
1✔
418
                return True
×
419

420
        return False
1✔
421

422
    # Test if this part should be included, based on any regex expressions provided in the preferences
423
    def testRegExclude(self):
1✔
424

425
        for reg in self.prefs.regExcludes:
1✔
426

427
            if type(reg) == list and len(reg) == 2:
1✔
428
                field_name, regex = reg
1✔
429
                field_value = self.getField(field_name)
1✔
430

431
                # Attempt unicode escaping...
432
                # Filthy hack
433
                try:
1✔
434
                    regex = regex.decode("unicode_escape")
1✔
435
                except:
1✔
436
                    pass
1✔
437

438
                if re.search(regex, field_value, flags=re.IGNORECASE) is not None:
1✔
439
                    debug.info("Excluding '{ref}': Field '{field}' ({value}) matched '{reg}'".format(
×
440
                        ref=self.getRef(),
441
                        field=field_name,
442
                        value=field_value,
443
                        reg=regex).encode('utf-8')
444
                    )
445

446
                    # Found a match
447
                    return True
×
448

449
        # Default, could not find any matches
450
        return False
1✔
451

452
    def testRegInclude(self):
1✔
453

454
        if len(self.prefs.regIncludes) == 0:  # Nothing to match against
1✔
455
            return True
1✔
456

457
        for reg in self.prefs.regIncludes:
×
458

459
            if type(reg) == list and len(reg) == 2:
×
460
                field_name, regex = reg
×
461
                field_value = self.getField(field_name)
×
462

463
                debug.info(field_name, field_value, regex)
×
464

465
                if re.search(regex, field_value, flags=re.IGNORECASE) is not None:
×
466

467
                    # Found a match
468
                    return True
×
469

470
        # Default, could not find a match
471
        return False
×
472

473
    def getFootprint(self, libraryToo=True):
1✔
474
        ret = self.element.get("footprint")
1✔
475
        if ret == "" and libraryToo:
1✔
476
            if self.libpart:
×
477
                ret = self.libpart.getFootprint()
×
478
        return ret
1✔
479

480
    def getDatasheet(self, libraryToo=True):
1✔
481
        ret = self.element.get("datasheet")
1✔
482
        if ret == "" and libraryToo:
1✔
483
            ret = self.libpart.getDatasheet()
×
484
        return ret
1✔
485

486
    def getTimestamp(self):
1✔
487
        return self.element.get("tstamp")
×
488

489

490
class joiner:
1✔
491
    def __init__(self):
1✔
492
        self.stack = []
×
493

494
    def add(self, P, N):
1✔
495
        
496
        if self.stack == []:
×
497
            self.stack.append(((P, N), (P, N)))
×
498
            return
×
499

500
        S, E = self.stack[-1]
×
501

502
        if N == E[1] + 1:
×
503
            self.stack[-1] = (S, (P, N))
×
504
        else:
505
            self.stack.append(((P, N), (P, N)))
×
506

507
    def flush(self, sep, N=None, dash='-'):
1✔
508

509
        refstr = u''
×
510
        c = 0
×
511

512
        for Q in self.stack:
×
513
            if bool(N) and c != 0 and c % N == 0:
×
514
                refstr += u'\n'
×
515
            elif c != 0:
×
516
                refstr += sep + " "
×
517

518
            S, E = Q
×
519

520
            if S == E:
×
521
                refstr += "%s%d" % S
×
522
                c += 1
×
523
            else:
524
                # Do we have space?
525
                if bool(N) and (c + 1) % N == 0:
×
526
                    refstr += u'\n'
×
527
                    c += 1
×
528

529
                refstr += "%s%d%s%s%d" % (S[0], S[1], dash, E[0], E[1])
×
530
                c += 2
×
531
        return refstr
×
532

533

534
class ComponentGroup():
1✔
535

536
    """
537
    Initialize the group with no components, and default fields
538
    """
539
    def __init__(self, prefs=None):
1✔
540
        self.components = []
1✔
541
        self.fields = dict.fromkeys(ColumnList._COLUMNS_DEFAULT)  # Columns loaded from KiCad
1✔
542

543
        if not prefs:
1✔
544
            prefs = BomPref()
×
545

546
        self.prefs = prefs
1✔
547

548
    def getField(self, field):
1✔
549

550
        if field not in self.fields.keys():
1✔
551
            return ""
1✔
552
        
553
        if not self.fields[field]:
1✔
554
            return ""
×
555
        
556
        return u''.join((self.fields[field]))
1✔
557

558
    def getCount(self):
1✔
559
        return len(self.components)
1✔
560

561
    # Test if a given component fits in this group
562
    def matchComponent(self, c):
1✔
563
        if len(self.components) == 0:
1✔
564
            return True
×
565
        if c == self.components[0]:
1✔
566
            return True
1✔
567

568
        return False
1✔
569

570
    def containsComponent(self, c):
1✔
571
        # Test if a given component is already contained in this grop
572
        if not self.matchComponent(c):
1✔
573
            return False
×
574

575
        for comp in self.components:
1✔
576
            if comp.getRef() == c.getRef():
1✔
577
                return True
×
578

579
        return False
1✔
580

581
    def addComponent(self, c):
1✔
582
        # Add a component to the group
583

584
        if len(self.components) == 0:
1✔
585
            self.components.append(c)
1✔
586
        elif self.containsComponent(c):
1✔
587
            return
×
588
        elif self.matchComponent(c):
1✔
589
            self.components.append(c)
1✔
590

591
    def isFitted(self):
1✔
592
        return any([c.isFitted() for c in self.components])
1✔
593

594
    def isFixed(self):
1✔
595
        return any([c.isFixed() for c in self.components])
1✔
596

597
    def getRefs(self):
1✔
598
        # Return a list of the components
599
        separator = self.prefs.refSeparator
1✔
600
        return separator.join([c.getRef() for c in self.components])
1✔
601

602
    def getAltRefs(self):
1✔
603
        S = joiner()
×
604

605
        for n in self.components:
×
606
            P, N = (n.getPrefix(), n.getSuffix())
×
607
            S.add(P, N)
×
608

609
        return S.flush(self.prefs.refSeparator)
×
610

611
    # Sort the components in correct order
612
    def sortComponents(self):
1✔
613
        self.components = sorted(self.components, key=lambda c: natural_sort(c.getRef()))
1✔
614

615
    # Update a given field, based on some rules and such
616
    def updateField(self, field, fieldData):
1✔
617

618
        # Protected fields cannot be overwritten
619
        if field in ColumnList._COLUMNS_PROTECTED:
1✔
620
            return
×
621

622
        if field is None or field == "":
1✔
623
            return
×
624
        elif fieldData == "" or fieldData is None:
1✔
625
            return
×
626

627
        if (field not in self.fields.keys()) or (self.fields[field] is None) or (self.fields[field] == ""):
1✔
628
            self.fields[field] = fieldData
1✔
629
        elif fieldData.lower() in self.fields[field].lower():
×
630
            return
×
631
        else:
632
            debug.warning("Field conflict: ({refs}) [{name}] : '{flds}' <- '{fld}'".format(
×
633
                refs=self.getRefs(),
634
                name=field,
635
                flds=self.fields[field],
636
                fld=fieldData).encode('utf-8'))
637
            self.fields[field] += " " + fieldData
×
638

639
    def updateFields(self, usealt=False, wrapN=None):
1✔
640
        for c in self.components:
1✔
641
            for f in c.getFieldNames():
1✔
642

643
                # These columns are handled explicitly below
644
                if f in ColumnList._COLUMNS_PROTECTED:
1✔
645
                    continue
×
646

647
                self.updateField(f, c.getField(f))
1✔
648

649
        # Update 'global' fields
650
        if usealt:
1✔
651
            self.fields[ColumnList.COL_REFERENCE] = self.getAltRefs()
×
652
        else:
653
            self.fields[ColumnList.COL_REFERENCE] = self.getRefs()
1✔
654

655
        q = self.getCount()
1✔
656
        self.fields[ColumnList.COL_GRP_QUANTITY] = "{n}{dnf}{dnc}".format(
1✔
657
            n=q,
658
            dnf=" (DNF)" if not self.isFitted() else "",
659
            dnc=" (DNC)" if self.isFixed() else "")
660

661
        self.fields[ColumnList.COL_GRP_BUILD_QUANTITY] = str(q * self.prefs.boards) if self.isFitted() else "0"
1✔
662
        self.fields[ColumnList.COL_VALUE] = self.components[0].getValue()
1✔
663
        self.fields[ColumnList.COL_PART] = self.components[0].getPartName()
1✔
664
        self.fields[ColumnList.COL_PART_LIB] = self.components[0].getLibName()
1✔
665
        self.fields[ColumnList.COL_DESCRIPTION] = self.components[0].getDescription()
1✔
666
        self.fields[ColumnList.COL_DATASHEET] = self.components[0].getDatasheet()
1✔
667
        self.fields[ColumnList.COL_SHEETPATH] = self.components[0].getSheetpathNames()
1✔
668

669
        # Footprint field requires special attention
670
        fp = self.components[0].getFootprint().split(":")
1✔
671

672
        if len(fp) >= 2:
1✔
673
            self.fields[ColumnList.COL_FP_LIB] = fp[0]
1✔
674
            self.fields[ColumnList.COL_FP] = fp[1]
1✔
675
        elif len(fp) == 1:
×
676
            self.fields[ColumnList.COL_FP_LIB] = ""
×
677
            self.fields[ColumnList.COL_FP] = fp[0]
×
678
        else:
679
            self.fields[ColumnList.COL_FP_LIB] = ""
×
680
            self.fields[ColumnList.COL_FP] = ""
×
681

682
    # Return a dict of the KiCad data based on the supplied columns
683
    # NOW WITH UNICODE SUPPORT!
684
    def getRow(self, columns):
1✔
685
        row = []
1✔
686
        for key in columns:
1✔
687
            val = self.getField(key)
1✔
688

689
            # Join fields (appending to current value) (#81)
690
            for join_l in self.prefs.join:
1✔
691
                # Each list is "target, source..." so we need at least 2 elements
692
                elements = len(join_l)
×
693
                target = join_l[0]
×
694
                if elements > 1 and target == key:
×
695
                    # Append data from the other fields
696
                    for source in join_l[1:]:
×
697
                        v = self.getField(source)
×
698
                        if v:
×
699
                            val = val + ' ' + v
×
700

701
            if val is None:
1✔
702
                val = ""
×
703
            else:
704
                val = u'' + val
1✔
705
                if sys.version_info[0] < 3:
1✔
706
                    val = val.encode('utf-8')
×
707

708
            row.append(val)
1✔
709

710
        return row
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc