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

bramp / build-along / 20398712053

20 Dec 2025 07:00PM UTC coverage: 89.361% (+0.2%) from 89.185%
20398712053

push

github

bramp
Improve circular dependency error to show dependency chain

- Add _find_dependency_cycle() to trace and format the actual circular dependency path
- Update error message to include both affected classifiers and the dependency chain
- Add test case to verify circular dependency detection and error message format

48 of 56 new or added lines in 2 files covered. (85.71%)

145 existing lines in 28 files now uncovered.

13700 of 15331 relevant lines covered (89.36%)

0.89 hits per line

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

96.88
/src/build_a_long/pdf_extract/classifier/steps/step_count_classifier.py
1
"""
2
Step count classifier.
3

4
Purpose
5
-------
6
Detect step-count text like "2x" that appears in substep callout boxes.
7
These are similar to part counts but use a larger font size (typically 16pt),
8
between part count size and step number size.
9

10
Debugging
11
---------
12
Enable DEBUG logs with LOG_LEVEL=DEBUG.
13
"""
14

15
import logging
1✔
16
from collections.abc import Sequence
1✔
17
from typing import ClassVar
1✔
18

19
from build_a_long.pdf_extract.classifier.candidate import Candidate
1✔
20
from build_a_long.pdf_extract.classifier.classification_result import (
1✔
21
    ClassificationResult,
22
)
23
from build_a_long.pdf_extract.classifier.rule_based_classifier import (
1✔
24
    RuleBasedClassifier,
25
)
26
from build_a_long.pdf_extract.classifier.rules import (
1✔
27
    FontSizeRangeRule,
28
    IsInstanceFilter,
29
    PartCountTextRule,
30
    Rule,
31
)
32
from build_a_long.pdf_extract.classifier.rules.scale import LinearScale
1✔
33
from build_a_long.pdf_extract.classifier.text import (
1✔
34
    extract_part_count_value,
35
)
36
from build_a_long.pdf_extract.extractor.lego_page_elements import (
1✔
37
    StepCount,
38
)
39
from build_a_long.pdf_extract.extractor.page_blocks import Text
1✔
40

41
log = logging.getLogger(__name__)
1✔
42

43

44
class StepCountClassifier(RuleBasedClassifier):
1✔
45
    """Classifier for step counts (substep counts like "2x").
46

47
    These are count labels that appear inside substep callout boxes,
48
    indicating how many times to build the sub-assembly.
49
    They use a font size between part counts and step numbers.
50
    """
51

52
    output: ClassVar[str] = "step_count"
1✔
53
    requires: ClassVar[frozenset[str]] = frozenset()
1✔
54

55
    @property
1✔
56
    def min_score(self) -> float:
1✔
57
        return self.config.step_count.min_score
1✔
58

59
    @property
1✔
60
    def rules(self) -> Sequence[Rule]:
1✔
61
        config = self.config
1✔
62
        step_count_config = config.step_count
1✔
63
        hints = config.font_size_hints
1✔
64

65
        return [
1✔
66
            # Must be text
67
            IsInstanceFilter(Text),
68
            # Check if text matches count pattern (e.g., "2x", "4x")
69
            PartCountTextRule(
70
                weight=step_count_config.text_weight,
71
                name="text_score",
72
                required=True,
73
            ),
74
            # Score font size: should be >= part_count_size and <= step_number_size
75
            # 0.7 within tolerance of min, 1.0 above min+tolerance, 0.0 outside range
76
            FontSizeRangeRule(
77
                scale=LinearScale(
78
                    {
79
                        (hints.part_count_size or 10.0) - 1.0: 0.0,
80
                        hints.part_count_size or 10.0: 0.7,
81
                        (hints.part_count_size or 10.0) + 1.0: 1.0,
82
                        (hints.step_number_size or 20.0) + 1.0: 1.0,
83
                        (hints.step_number_size or 20.0) + 2.0: 0.0,
84
                    }
85
                ),
86
                weight=step_count_config.font_size_weight,
87
                name="font_size_score",
88
            ),
89
        ]
90

91
    def build(self, candidate: Candidate, result: ClassificationResult) -> StepCount:
1✔
92
        """Construct a StepCount element from a candidate.
93

94
        The candidate may include additional source blocks (e.g., text outline
95
        effects) beyond the primary Text block.
96
        """
97
        # Get the primary text block (first in source_blocks)
98
        assert len(candidate.source_blocks) >= 1
1✔
99
        block = candidate.source_blocks[0]
1✔
100
        assert isinstance(block, Text)
1✔
101

102
        # Parse the count value
103
        value = extract_part_count_value(block.text)
1✔
104
        if value is None:
1✔
UNCOV
105
            raise ValueError(f"Could not parse step count from text: '{block.text}'")
×
106

107
        # Use candidate.bbox which is the union of all source blocks
108
        return StepCount(count=value, bbox=candidate.bbox)
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

© 2026 Coveralls, Inc