Source code for kicadfiles.design_rules

"""Design rules elements for KiCad S-expressions - design rule constraint system."""

from dataclasses import dataclass, field
from typing import List, Optional

from .base_element import KiCadInt, KiCadObject, KiCadStr, ParseStrictness
from .enums import ConstraintType, SeverityLevel
from .sexpr_parser import sexpr_to_str


[docs] @dataclass class DesignRuleConstraint(KiCadObject): """Design rule constraint definition token. The 'constraint' token defines a constraint with optional min/opt/max values in the format:: (constraint CONSTRAINT_TYPE [(min VALUE)] [(opt VALUE)] [(max VALUE)]) (constraint disallow ITEM_TYPE) Args: constraint_type: Type of constraint min_constraint: Minimum value constraint (optional) opt_constraint: Optimal value constraint (optional) max_constraint: Maximum value constraint (optional) disallow_item: Item type to disallow (optional) """ __token_name__ = "constraint" constraint_type: ConstraintType = field( default=ConstraintType.CLEARANCE, metadata={"description": "Type of constraint"} ) min_constraint: Optional[KiCadStr] = field( default_factory=lambda: KiCadStr("min", "0.0", required=False), metadata={"description": "Minimum value constraint", "required": False}, ) opt_constraint: Optional[KiCadStr] = field( default_factory=lambda: KiCadStr("opt", "0.0", required=False), metadata={"description": "Optimal value constraint", "required": False}, ) max_constraint: Optional[KiCadStr] = field( default_factory=lambda: KiCadStr("max", "0.0", required=False), metadata={"description": "Maximum value constraint", "required": False}, ) disallow_item: Optional[str] = field( default=None, metadata={"description": "Item type to disallow", "required": False}, )
[docs] @dataclass class DesignRuleSeverity(KiCadObject): """Design rule severity level token. The 'severity' token defines the severity level for rule violations in the format:: (severity error | warning | ignore) Args: level: Severity level """ __token_name__ = "severity" level: SeverityLevel = field( default=SeverityLevel.ERROR, metadata={"description": "Severity level"} )
[docs] @dataclass class DesignRule(KiCadObject): """Design rule definition token. The 'rule' token defines a complete design rule in the format:: (rule NAME [(severity SEVERITY)] [(layer LAYER_NAME)] [(condition EXPRESSION)] [(priority PRIORITY_NUMBER)] (constraint CONSTRAINT_TYPE [CONSTRAINT_ARGUMENTS]) [(constraint CONSTRAINT_TYPE [CONSTRAINT_ARGUMENTS])]... ) Args: name: Rule name severity: Severity level (optional) layer: Layer specification (optional) condition: Conditional expression (optional) priority: Rule priority (optional) constraints: List of constraint definitions """ __token_name__ = "rule" name: str = field(default="", metadata={"description": "Rule name"}) severity: Optional[DesignRuleSeverity] = field( default=None, metadata={"description": "Severity level", "required": False} ) layer: Optional[KiCadStr] = field( default_factory=lambda: KiCadStr("layer", "", required=False), metadata={"description": "Layer specification", "required": False}, ) condition: Optional[KiCadStr] = field( default_factory=lambda: KiCadStr("condition", "", required=False), metadata={"description": "Conditional expression", "required": False}, ) priority: Optional[KiCadInt] = field( default_factory=lambda: KiCadInt("priority", 0, required=False), metadata={"description": "Rule priority", "required": False}, ) constraints: List[DesignRuleConstraint] = field( default_factory=list, metadata={"description": "List of constraint definitions"}, )
[docs] @dataclass class KiCadDesignRules(KiCadObject): """KiCad design rules file definition. The design rules file contains version information and a list of rules in the format:: (version VERSION) RULES... Note: This is different from other KiCad files - it doesn't have a root token like 'kicad_dru', instead it starts directly with version and rules. Args: version: File format version rules: List of design rules (optional) """ __token_name__ = ( "kicad_dru" # Using this as placeholder, but file format is different ) version: KiCadInt = field( default_factory=lambda: KiCadInt("version", 1), metadata={"description": "File format version"}, ) rules: Optional[List[DesignRule]] = field( default_factory=list, metadata={"description": "List of design rules", "required": False}, )
[docs] @classmethod def from_str( cls, sexpr_string: str, strictness: ParseStrictness = ParseStrictness.STRICT, ) -> "KiCadDesignRules": """Parse from S-expression string - convenience method for design rules. This method automatically handles .kicad_dru format preprocessing. """ # Check if content looks like raw .kicad_dru format (no root token) stripped_content = sexpr_string.strip() if not stripped_content.startswith("(kicad_dru"): # Preprocess as raw .kicad_dru content processed_content = cls._preprocess_dru_content(sexpr_string) else: # Already wrapped, use as-is processed_content = sexpr_string return super().from_str(processed_content, strictness)
[docs] @classmethod def from_file( cls, file_path: str, strictness: ParseStrictness = ParseStrictness.STRICT, encoding: str = "utf-8", ) -> "KiCadDesignRules": """Parse from S-expression file - convenience method for design rules operations. Design rules files (.kicad_dru) have a special format without root token wrapping. This method handles the preprocessing needed for the parser. """ if not file_path.endswith(".kicad_dru"): raise ValueError("Unsupported file extension. Expected: .kicad_dru") with open(file_path, "r", encoding=encoding) as f: raw_content = f.read() return cls.from_str(raw_content, strictness)
@classmethod def _preprocess_dru_content(cls, content: str) -> str: """Preprocess .kicad_dru file content for parsing. Design rules files don't have a root token wrapper, so we need to: 1. Filter out comment lines that might have parsing issues 2. Wrap content in a root token for the parser """ lines = content.split("\n") filtered_lines = [] for line in lines: stripped = line.strip() # Skip empty lines and comment lines if not stripped or stripped.startswith("#"): continue filtered_lines.append(line) # Join the filtered content and wrap in root token filtered_content = "\n".join(filtered_lines) wrapped_content = f"(kicad_dru\n{filtered_content}\n)" return wrapped_content
[docs] def to_dru_str(self) -> str: """Convert to .kicad_dru format string (without root token wrapper). This method produces the native .kicad_dru file format. """ lines = [] # Add version version_sexpr = ["version", self.version.value] lines.append(sexpr_to_str(version_sexpr)) # Add rules if self.rules: for rule in self.rules: rule_sexpr = rule.to_sexpr() lines.append(sexpr_to_str(rule_sexpr)) return "\n".join(lines)
[docs] def save_to_file(self, file_path: str, encoding: str = "utf-8") -> None: """Save to .kicad_dru file format. Args: file_path: Path to write the .kicad_dru file encoding: File encoding (default: utf-8) """ content = self.to_dru_str() with open(file_path, "w", encoding=encoding) as f: f.write(content)