"""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)