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

bramp / build-along / 20015271182

08 Dec 2025 03:02AM UTC coverage: 90.402% (+0.1%) from 90.299%
20015271182

push

github

bramp
Minor rename of property in subassembly config.

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

125 existing lines in 14 files now uncovered.

11039 of 12211 relevant lines covered (90.4%)

0.9 hits per line

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

91.67
/src/build_a_long/pdf_extract/classifier/rule_based_classifier.py
1
"""
2
Rule-based classifier implementation.
3
"""
4

5
from __future__ import annotations
1✔
6

7
import logging
1✔
8
from abc import abstractmethod
1✔
9

10
from build_a_long.pdf_extract.classifier.block_filter import (
1✔
11
    find_text_outline_effects,
12
)
13
from build_a_long.pdf_extract.classifier.candidate import Candidate
1✔
14
from build_a_long.pdf_extract.classifier.classification_result import (
1✔
15
    ClassificationResult,
16
)
17
from build_a_long.pdf_extract.classifier.label_classifier import (
1✔
18
    LabelClassifier,
19
)
20
from build_a_long.pdf_extract.classifier.rules import Rule, RuleContext
1✔
21
from build_a_long.pdf_extract.classifier.score import Score, Weight
1✔
22
from build_a_long.pdf_extract.extractor.page_blocks import Text
1✔
23

24
log = logging.getLogger(__name__)
1✔
25

26

27
class RuleScore(Score):
1✔
28
    """Generic score based on rules."""
29

30
    components: dict[str, float]
1✔
31
    total_score: float
1✔
32

33
    def score(self) -> Weight:
1✔
34
        return self.total_score
×
35

36
    def get(self, rule_name: str, default: float = 0.0) -> float:
1✔
37
        """Get the score for a specific rule name."""
UNCOV
38
        return self.components.get(rule_name, default)
×
39

40

41
class RuleBasedClassifier(LabelClassifier):
1✔
42
    """Base class for classifiers that use a list of rules to score candidates."""
43

44
    @property
1✔
45
    @abstractmethod
1✔
46
    def rules(self) -> list[Rule]:
1✔
47
        """Get the list of rules for this classifier."""
UNCOV
48
        pass
×
49

50
    @property
1✔
51
    def min_score(self) -> float:
1✔
52
        """Minimum score threshold for acceptance. Defaults to 0.0."""
UNCOV
53
        return 0.0
×
54

55
    def _score(self, result: ClassificationResult) -> None:
1✔
56
        """Score blocks using rules."""
57
        context = RuleContext(result.page_data, self.config)
1✔
58
        rules = self.rules
1✔
59

60
        for block in result.page_data.blocks:
1✔
61
            components = {}
1✔
62
            weighted_sum = 0.0
1✔
63
            total_weight = 0.0
1✔
64
            failed = False
1✔
65

66
            for rule in rules:
1✔
67
                score = rule.calculate(block, context)
1✔
68

69
                # If rule returns None, it's skipped (not applicable)
70
                if score is None:
1✔
71
                    continue
1✔
72

73
                # If required rule fails (score 0), fail the block immediately
74
                if rule.required and score == 0.0:
1✔
75
                    failed = True
1✔
76
                    break
1✔
77

78
                rule_weight = rule.weight  # Using direct weight from Rule instance
1✔
79

80
                weighted_sum += score * rule_weight
1✔
81
                total_weight += rule_weight
1✔
82
                components[rule.name] = score
1✔
83

84
            if failed:
1✔
85
                continue
1✔
86

87
            # Calculate final score
88
            if total_weight > 0:
1✔
89
                final_score = weighted_sum / total_weight
1✔
90
            else:
UNCOV
91
                final_score = 0.0
×
92

93
            # Check classifier-specific acceptance logic
94
            if not self._should_accept(final_score):
1✔
95
                continue
1✔
96

97
            # Build source blocks list, including text outline effects for Text blocks
98
            source_blocks: list = [block]
1✔
99
            if isinstance(block, Text):
1✔
100
                outline_effects = find_text_outline_effects(
1✔
101
                    block, result.page_data.blocks
102
                )
103
                source_blocks.extend(outline_effects)
1✔
104

105
            # Create candidate
106
            candidate = Candidate(
1✔
107
                bbox=block.bbox,
108
                label=self.output,
109
                score=final_score,
110
                score_details=RuleScore(components=components, total_score=final_score),
111
                source_blocks=source_blocks,
112
            )
113
            result.add_candidate(candidate)
1✔
114

115
    def _should_accept(self, score: float) -> bool:
1✔
116
        """Determine if a score is high enough to be a candidate.
117

118
        Subclasses can override this.
119
        """
120
        return score >= self.min_score
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