autoeagle.core

  1import argparse
  2import inspect
  3import itertools
  4import shutil
  5from pathlib import Path
  6from xml.etree import ElementTree
  7
  8from autoeagle import autoeagle_config
  9
 10
 11class EagleFile:
 12    """Base class representing and parsing an Eagle file."""
 13
 14    def __init__(self, file: str = None):
 15        """:param file: Path to an Eagle file.
 16        If None, getting the path from command line args
 17        will be attempted. If that fails, the user will
 18        be prompted for a file path.
 19
 20        A backup of the file will be created during the
 21        initialization."""
 22        if file:
 23            file = Path(file)
 24        else:
 25            args = self.get_args()
 26            file = args.file
 27        self.path = file
 28        self.tree = ElementTree.parse(self.path)
 29        self.root = self.tree.getroot()
 30
 31        self.drawing = self.root.find("drawing")
 32
 33        self.settings = self.drawing.find("settings").findall("setting")
 34        self.grid = self.drawing.find("grid")
 35        self.layers = self.drawing.find("layers").findall("layer")
 36
 37        shutil.copy(self.path, str(self.path.with_stem(f"{self.path.stem}_Bckup")))
 38
 39    def get_parser(self) -> argparse.ArgumentParser:
 40        """Returns an argparse ArgumentParser instance.
 41        This is in a separate function so additional args
 42        can be added in subclass without having to
 43        override the rest of the self.get_args() function.
 44
 45        Only arg added here is for a file path."""
 46        parser = argparse.ArgumentParser()
 47        parser.add_argument(
 48            "file",
 49            type=str,
 50            nargs="?",
 51            default=None,
 52            help=""" Path to an Eagle file. """,
 53        )
 54        return parser
 55
 56    def get_args(self) -> argparse.Namespace:
 57        """Get file path from command line
 58        or prompt user for file path."""
 59        parser = self.get_parser()
 60        args = parser.parse_args()
 61        if not args.file:
 62            args.file = input("Enter path to an Eagle file: ")
 63        args.file = Path(args.file)
 64        return args
 65
 66    def save(self):
 67        self.tree.write(str(self.path), encoding="utf-8")
 68
 69    def get_names(self, elements: list[ElementTree.Element]) -> list[str]:
 70        """Return a list of 'name' attributes for each
 71        ElementTree Element in 'elements' list."""
 72        return self.get_attribute("name", elements)
 73
 74    def get_attribute(
 75        self, attribute: str, elements: list[ElementTree.Element]
 76    ) -> list[str]:
 77        """Return a list of the specified attribute for a list of elements.
 78
 79        >>> schem = autoeagle.Schematic("schem.sch")
 80        >>> part_values = schem.get_attribute("value", schem.parts)
 81        >>> brd = autoeagle.Board("board.brd")
 82        >>> layer_colors = brd.get_attribute("color", brd.layers)"""
 83        return [element.get(attribute) for element in elements]
 84
 85
 86class Schematic(EagleFile):
 87    """Class representing an Eagle schematic file."""
 88
 89    def __init__(self, *args, **kwargs):
 90        super().__init__(*args, **kwargs)
 91
 92        self.schematic = self.drawing.find("schematic")
 93
 94        self.libraries = self.schematic.find("libraries").findall("library")
 95        self.classes = self.schematic.find("classes")
 96        self.parts = self.schematic.find("parts").findall("part")
 97        self.sheets = self.schematic.find("sheets").findall("sheet")
 98
 99
100class Board(EagleFile):
101    """Class representing an Eagle board file."""
102
103    def __init__(self, *args, **kwargs):
104        super().__init__(*args, **kwargs)
105
106        self.board = self.drawing.find("board")
107
108        # This contains the 'wire' elements outlining the board
109        # No idea why Autodesk calls it 'plain'
110        self.plain = self.board.find("plain")
111        self.outline = self.plain
112        self.libraries = self.board.find("libraries").findall("library")
113        self.classes = self.board.find("classes").findall("class")
114        self.designrules = self.board.find("designrules").findall("param")
115        self.autorouter = self.board.find("autorouter")
116        # These are all the actual parts on the board
117        self.elements = self.board.find("elements").findall("element")
118        self.parts = self.elements
119        self.signals = self.board.find("signals").findall("signal")
120
121    def get_packages(self, attribute: str = None) -> list[str | ElementTree.Element]:
122        """Return a list of package elements that are used
123        in the board file.
124
125        :param attribute: If given, the returned list will only
126        contain the given attribute for each element instead of
127        the whole ElementTree.Element ."""
128        packages = [
129            package
130            for package in self.board.findall(".//libraries/library/packages/package")
131        ]
132        return self.get_attribute(attribute, packages) if attribute else packages
133
134    def get_smd_packages(
135        self, attribute: str = None
136    ) -> list[str | ElementTree.Element]:
137        """Return a list of smd packages used in the board file.
138
139        :param attribute: If given, the returned list will only
140        contain the given attribute for each element instead of
141        the whole ElementTree.Element ."""
142        packages = [
143            package
144            for package in self.get_packages()
145            if type(package.find("smd")) is ElementTree.Element
146        ]
147        return self.get_attribute(attribute, packages) if attribute else packages
148
149    def get_smd_parts(self, attribute: str = None) -> list[str | ElementTree.Element]:
150        """Filter self.parts for smd parts.
151
152        :param attribute: If given, the returned list will only
153        contain the given attribute for each element instead of
154        the whole ElementTree.Element ."""
155        smd_packages = self.get_smd_packages("name")
156        parts = [part for part in self.parts if part.get("package") in smd_packages]
157        return self.get_attribute(attribute, parts) if attribute else parts
158
159    def get_bounds(self) -> dict[str, float]:
160        """Return a dictionary representing
161        the minimum and maximum x and y coordinates
162        of the board."""
163        wires = [
164            wire for wire in self.plain.findall("wire") if wire.get("layer") == "20"
165        ]
166        get_coords = lambda n: [
167            float(v)
168            for wire in wires
169            for v in itertools.chain([wire.get(f"{n}1"), wire.get(f"{n}2")])
170        ]
171        x = get_coords("x")
172        y = get_coords("y")
173        return {"x0": min(x), "xf": max(x), "y0": min(y), "yf": max(y)}
174
175    def get_dimensions(self) -> tuple[float, float]:
176        """Returns a two-tuple containing
177        the width and height of the board's
178        bounding box."""
179        bounds = self.get_bounds()
180        return (bounds["xf"] - bounds["x0"], bounds["yf"] - bounds["y0"])
181
182    def get_center(self) -> tuple[float, float]:
183        """Returns a two-tuple containing
184        the x,y center point of the board's
185        bounding box."""
186        bounds = self.get_bounds()
187        midpoint = lambda n: (bounds[f"{n}f"] + bounds[f"{n}0"]) * 0.5
188        return (midpoint("x"), midpoint("y"))
189
190    def get_area(self) -> float:
191        """Returns the area of the board's bounding box.
192        Note: This is not the same as the board's
193        physical area for shapes other than rectangular ones.
194        It also doesn't account for cut-outs."""
195        wxh = self.get_dimensions()
196        return wxh[0] * wxh[1]
197
198    def get_visible_layers(self) -> list[str]:
199        """Returns a list of visible layers."""
200        return [
201            layer.get("name") for layer in self.layers if layer.get("visible") == "yes"
202        ]
203
204
205class ScriptWriter:
206    """Class for writing Eagle script files.
207    These files use the Editor command syntax
208    as documented in the Eagle manual.
209    Scipts generated by this class will be invoked
210    and executed within Eagle after returning
211    from the invoking Ulp file for your Python
212    script.
213
214    The preferred usage of this class is
215    with a context manager. When used with a context
216    manager, you don't need to remember to
217    call the save() method at the end of the script.
218    You also don't need to remember to add 'write;'
219    as the last command, as the save() function adds it for you.
220    Commands can be added with the '+=' operator.
221    The Eagle syntax specifies each command ends with
222    ';'. If you leave it off of an added command,
223    it will be added for you.
224    Some script commands are provided as member functions.
225    For further script options, refer to the Eagle help menu.
226
227    e.x. Moving the center of the board to (0,0)
228    >>> brd = Board()
229    >>> with ScriptWriter(brd.path) as scr:
230    >>>     center = brd.get_center()
231    >>>     scr.group_all() # Adds "group all;" to commands
232    >>>     scr += f"move (>0 0) (-{center[0]} -{center[1]})"""
233
234    def __init__(
235        self,
236        script_name: str = None,
237    ):
238        """The .scr file will be saved to a "scripts" subfolder within the same
239        directory as the eagle_file param.
240
241        :param eagle_file: The path to the Eagle file
242        the script is being written for.
243
244        :param script_name: The name of the script creating the instance.
245        If None, the inspect module will be used to determine the script name."""
246        if not script_name:
247            script_name = (Path(inspect.stack()[1].filename)).stem
248        scriptsdir = Path(autoeagle_config.load_config()["scriptsdir"])
249        self.path = scriptsdir / f"{script_name}.scr"
250        self.commands = []
251
252    def __enter__(self):
253        return self
254
255    def __exit__(self, *args):
256        self.save()
257
258    def __iadd__(self, command: str):
259        if not command.endswith(";"):
260            command += ";"
261        self.commands.append(command)
262        return self
263
264    def save(self):
265        self += "write"
266        self.path.write_text("\n".join(self.commands))
267
268    def group_all(self):
269        """Put everything on a visible layer into a group."""
270        self += "group all"
271
272    def display_all(self):
273        """Make every layer in the design visible."""
274        self += "display all"
275
276    def display_layers(self, layers: list[str]):
277        """Displays only the layers in 'layers'."""
278        self += f"display none {' '.join(layers)}"
279
280    def _coordinate_string(self, coordinates: list[tuple[float, float]]) -> str:
281        """Return command snippet for coordinates to be used with draw commands.
282
283        :param coordinates: A list of two-tuples. A string will
284        be returned specifying the coordinates in the same order
285        as the param."""
286        return " ".join(
287            f"({coordinate[0]} {coordinate[1]})" for coordinate in coordinates
288        )
289
290    def _rectangle_coordinates(
291        self, x0: float, xf: float, y0: float, yf: float
292    ) -> list[tuple[float, float]]:
293        """Return a list of two-tuples that describe
294        a rectangle given the coordinates of the sides.
295
296        :param x0,xf,y0,yf: Coordinates describing
297        the sides of a rectangle."""
298
299        return [(x0, y0), (x0, yf), (xf, yf), (xf, y0), (x0, y0)]
300
301    def draw_group(self, coordinates: list[tuple[float, float]]):
302        """Draw a group bounded by 'coordinates'.
303
304        :param coordinates: A list of two-tuples specifying
305        the path to take when drawing the group."""
306        self += f"group {self._coordinate_string(coordinates)}"
307
308    def draw_rectangle_group(self, x0: float, xf: float, y0: float, yf: float):
309        """Draw a rectangular group whose sides are described by the given coordinates."""
310        self.draw_group(self._rectangle_coordinates(x0, xf, y0, yf))
311
312    def draw(
313        self,
314        coordinates: list[tuple[float, float]],
315        layer: str = None,
316        width: float = None,
317    ):
318        """Draw a line throught the given series of coordinate points.
319
320        :param coordinates: A list of two-tuples specifying
321        the path to take when drawing the line.
322
323        :param layer: The layer to draw the line on.
324        If None, the currently active layer is used.
325
326        :param width: The width of the line.
327        If None, the currently active width is used.
328
329        >>> # Draw a line of width 1 on layer "tplace" from (0 0) to (10 0) to (3 5)
330        >>> draw([(0, 0), (10, 0), (3,5)], "tPlace", 1)"""
331
332        if layer:
333            self += f"layer {layer}"
334        if width:
335            self += f"width {width}"
336        self += f"line {self._coordinate_string(coordinates)}"
337
338    def draw_rectangle(
339        self,
340        x0: float,
341        xf: float,
342        y0: float,
343        yf: float,
344        layer: str = None,
345        width: float = None,
346    ):
347        """Draw a rectangle whose sides are described by the given
348        coordinates: x0, xf, y0, yf.
349
350        :param layer: The layer to draw the line on.
351        If None, the currently active layer is used.
352
353        :param width: The width of the line.
354        If None, the currently active width is used.
355
356        >>> # Draw a 10 x 10 board outline of width 1 centered at (0, 0)
357        >>> draw_rectangle(-5, 5, -5, 5, "Dimension", 1)"""
358        self += self.draw(self._rectangle_coordinates(x0, xf, y0, yf), layer, width)
359
360    def rats_nest(self, ripup_polygons: bool = True):
361        """Run the rats nest command
362
363        :param ripup_polygons: If True, run 'ripup @'
364        after running rats nest. Eagle gets real
365        slow when polygons are visible."""
366
367        self += "ratsnest"
368        if ripup_polygons:
369            self += "ripup @"
class EagleFile:
12class EagleFile:
13    """Base class representing and parsing an Eagle file."""
14
15    def __init__(self, file: str = None):
16        """:param file: Path to an Eagle file.
17        If None, getting the path from command line args
18        will be attempted. If that fails, the user will
19        be prompted for a file path.
20
21        A backup of the file will be created during the
22        initialization."""
23        if file:
24            file = Path(file)
25        else:
26            args = self.get_args()
27            file = args.file
28        self.path = file
29        self.tree = ElementTree.parse(self.path)
30        self.root = self.tree.getroot()
31
32        self.drawing = self.root.find("drawing")
33
34        self.settings = self.drawing.find("settings").findall("setting")
35        self.grid = self.drawing.find("grid")
36        self.layers = self.drawing.find("layers").findall("layer")
37
38        shutil.copy(self.path, str(self.path.with_stem(f"{self.path.stem}_Bckup")))
39
40    def get_parser(self) -> argparse.ArgumentParser:
41        """Returns an argparse ArgumentParser instance.
42        This is in a separate function so additional args
43        can be added in subclass without having to
44        override the rest of the self.get_args() function.
45
46        Only arg added here is for a file path."""
47        parser = argparse.ArgumentParser()
48        parser.add_argument(
49            "file",
50            type=str,
51            nargs="?",
52            default=None,
53            help=""" Path to an Eagle file. """,
54        )
55        return parser
56
57    def get_args(self) -> argparse.Namespace:
58        """Get file path from command line
59        or prompt user for file path."""
60        parser = self.get_parser()
61        args = parser.parse_args()
62        if not args.file:
63            args.file = input("Enter path to an Eagle file: ")
64        args.file = Path(args.file)
65        return args
66
67    def save(self):
68        self.tree.write(str(self.path), encoding="utf-8")
69
70    def get_names(self, elements: list[ElementTree.Element]) -> list[str]:
71        """Return a list of 'name' attributes for each
72        ElementTree Element in 'elements' list."""
73        return self.get_attribute("name", elements)
74
75    def get_attribute(
76        self, attribute: str, elements: list[ElementTree.Element]
77    ) -> list[str]:
78        """Return a list of the specified attribute for a list of elements.
79
80        >>> schem = autoeagle.Schematic("schem.sch")
81        >>> part_values = schem.get_attribute("value", schem.parts)
82        >>> brd = autoeagle.Board("board.brd")
83        >>> layer_colors = brd.get_attribute("color", brd.layers)"""
84        return [element.get(attribute) for element in elements]

Base class representing and parsing an Eagle file.

EagleFile(file: str = None)
15    def __init__(self, file: str = None):
16        """:param file: Path to an Eagle file.
17        If None, getting the path from command line args
18        will be attempted. If that fails, the user will
19        be prompted for a file path.
20
21        A backup of the file will be created during the
22        initialization."""
23        if file:
24            file = Path(file)
25        else:
26            args = self.get_args()
27            file = args.file
28        self.path = file
29        self.tree = ElementTree.parse(self.path)
30        self.root = self.tree.getroot()
31
32        self.drawing = self.root.find("drawing")
33
34        self.settings = self.drawing.find("settings").findall("setting")
35        self.grid = self.drawing.find("grid")
36        self.layers = self.drawing.find("layers").findall("layer")
37
38        shutil.copy(self.path, str(self.path.with_stem(f"{self.path.stem}_Bckup")))
Parameters
  • file: Path to an Eagle file. If None, getting the path from command line args will be attempted. If that fails, the user will be prompted for a file path.

A backup of the file will be created during the initialization.

def get_parser(self) -> argparse.ArgumentParser:
40    def get_parser(self) -> argparse.ArgumentParser:
41        """Returns an argparse ArgumentParser instance.
42        This is in a separate function so additional args
43        can be added in subclass without having to
44        override the rest of the self.get_args() function.
45
46        Only arg added here is for a file path."""
47        parser = argparse.ArgumentParser()
48        parser.add_argument(
49            "file",
50            type=str,
51            nargs="?",
52            default=None,
53            help=""" Path to an Eagle file. """,
54        )
55        return parser

Returns an argparse ArgumentParser instance. This is in a separate function so additional args can be added in subclass without having to override the rest of the self.get_args() function.

Only arg added here is for a file path.

def get_args(self) -> argparse.Namespace:
57    def get_args(self) -> argparse.Namespace:
58        """Get file path from command line
59        or prompt user for file path."""
60        parser = self.get_parser()
61        args = parser.parse_args()
62        if not args.file:
63            args.file = input("Enter path to an Eagle file: ")
64        args.file = Path(args.file)
65        return args

Get file path from command line or prompt user for file path.

def save(self):
67    def save(self):
68        self.tree.write(str(self.path), encoding="utf-8")
def get_names(self, elements: list[xml.etree.ElementTree.Element]) -> list[str]:
70    def get_names(self, elements: list[ElementTree.Element]) -> list[str]:
71        """Return a list of 'name' attributes for each
72        ElementTree Element in 'elements' list."""
73        return self.get_attribute("name", elements)

Return a list of 'name' attributes for each ElementTree Element in 'elements' list.

def get_attribute( self, attribute: str, elements: list[xml.etree.ElementTree.Element]) -> list[str]:
75    def get_attribute(
76        self, attribute: str, elements: list[ElementTree.Element]
77    ) -> list[str]:
78        """Return a list of the specified attribute for a list of elements.
79
80        >>> schem = autoeagle.Schematic("schem.sch")
81        >>> part_values = schem.get_attribute("value", schem.parts)
82        >>> brd = autoeagle.Board("board.brd")
83        >>> layer_colors = brd.get_attribute("color", brd.layers)"""
84        return [element.get(attribute) for element in elements]

Return a list of the specified attribute for a list of elements.

>>> schem = autoeagle.Schematic("schem.sch")
>>> part_values = schem.get_attribute("value", schem.parts)
>>> brd = autoeagle.Board("board.brd")
>>> layer_colors = brd.get_attribute("color", brd.layers)
class Schematic(EagleFile):
87class Schematic(EagleFile):
88    """Class representing an Eagle schematic file."""
89
90    def __init__(self, *args, **kwargs):
91        super().__init__(*args, **kwargs)
92
93        self.schematic = self.drawing.find("schematic")
94
95        self.libraries = self.schematic.find("libraries").findall("library")
96        self.classes = self.schematic.find("classes")
97        self.parts = self.schematic.find("parts").findall("part")
98        self.sheets = self.schematic.find("sheets").findall("sheet")

Class representing an Eagle schematic file.

Schematic(*args, **kwargs)
90    def __init__(self, *args, **kwargs):
91        super().__init__(*args, **kwargs)
92
93        self.schematic = self.drawing.find("schematic")
94
95        self.libraries = self.schematic.find("libraries").findall("library")
96        self.classes = self.schematic.find("classes")
97        self.parts = self.schematic.find("parts").findall("part")
98        self.sheets = self.schematic.find("sheets").findall("sheet")
Parameters
  • file: Path to an Eagle file. If None, getting the path from command line args will be attempted. If that fails, the user will be prompted for a file path.

A backup of the file will be created during the initialization.

class Board(EagleFile):
101class Board(EagleFile):
102    """Class representing an Eagle board file."""
103
104    def __init__(self, *args, **kwargs):
105        super().__init__(*args, **kwargs)
106
107        self.board = self.drawing.find("board")
108
109        # This contains the 'wire' elements outlining the board
110        # No idea why Autodesk calls it 'plain'
111        self.plain = self.board.find("plain")
112        self.outline = self.plain
113        self.libraries = self.board.find("libraries").findall("library")
114        self.classes = self.board.find("classes").findall("class")
115        self.designrules = self.board.find("designrules").findall("param")
116        self.autorouter = self.board.find("autorouter")
117        # These are all the actual parts on the board
118        self.elements = self.board.find("elements").findall("element")
119        self.parts = self.elements
120        self.signals = self.board.find("signals").findall("signal")
121
122    def get_packages(self, attribute: str = None) -> list[str | ElementTree.Element]:
123        """Return a list of package elements that are used
124        in the board file.
125
126        :param attribute: If given, the returned list will only
127        contain the given attribute for each element instead of
128        the whole ElementTree.Element ."""
129        packages = [
130            package
131            for package in self.board.findall(".//libraries/library/packages/package")
132        ]
133        return self.get_attribute(attribute, packages) if attribute else packages
134
135    def get_smd_packages(
136        self, attribute: str = None
137    ) -> list[str | ElementTree.Element]:
138        """Return a list of smd packages used in the board file.
139
140        :param attribute: If given, the returned list will only
141        contain the given attribute for each element instead of
142        the whole ElementTree.Element ."""
143        packages = [
144            package
145            for package in self.get_packages()
146            if type(package.find("smd")) is ElementTree.Element
147        ]
148        return self.get_attribute(attribute, packages) if attribute else packages
149
150    def get_smd_parts(self, attribute: str = None) -> list[str | ElementTree.Element]:
151        """Filter self.parts for smd parts.
152
153        :param attribute: If given, the returned list will only
154        contain the given attribute for each element instead of
155        the whole ElementTree.Element ."""
156        smd_packages = self.get_smd_packages("name")
157        parts = [part for part in self.parts if part.get("package") in smd_packages]
158        return self.get_attribute(attribute, parts) if attribute else parts
159
160    def get_bounds(self) -> dict[str, float]:
161        """Return a dictionary representing
162        the minimum and maximum x and y coordinates
163        of the board."""
164        wires = [
165            wire for wire in self.plain.findall("wire") if wire.get("layer") == "20"
166        ]
167        get_coords = lambda n: [
168            float(v)
169            for wire in wires
170            for v in itertools.chain([wire.get(f"{n}1"), wire.get(f"{n}2")])
171        ]
172        x = get_coords("x")
173        y = get_coords("y")
174        return {"x0": min(x), "xf": max(x), "y0": min(y), "yf": max(y)}
175
176    def get_dimensions(self) -> tuple[float, float]:
177        """Returns a two-tuple containing
178        the width and height of the board's
179        bounding box."""
180        bounds = self.get_bounds()
181        return (bounds["xf"] - bounds["x0"], bounds["yf"] - bounds["y0"])
182
183    def get_center(self) -> tuple[float, float]:
184        """Returns a two-tuple containing
185        the x,y center point of the board's
186        bounding box."""
187        bounds = self.get_bounds()
188        midpoint = lambda n: (bounds[f"{n}f"] + bounds[f"{n}0"]) * 0.5
189        return (midpoint("x"), midpoint("y"))
190
191    def get_area(self) -> float:
192        """Returns the area of the board's bounding box.
193        Note: This is not the same as the board's
194        physical area for shapes other than rectangular ones.
195        It also doesn't account for cut-outs."""
196        wxh = self.get_dimensions()
197        return wxh[0] * wxh[1]
198
199    def get_visible_layers(self) -> list[str]:
200        """Returns a list of visible layers."""
201        return [
202            layer.get("name") for layer in self.layers if layer.get("visible") == "yes"
203        ]

Class representing an Eagle board file.

Board(*args, **kwargs)
104    def __init__(self, *args, **kwargs):
105        super().__init__(*args, **kwargs)
106
107        self.board = self.drawing.find("board")
108
109        # This contains the 'wire' elements outlining the board
110        # No idea why Autodesk calls it 'plain'
111        self.plain = self.board.find("plain")
112        self.outline = self.plain
113        self.libraries = self.board.find("libraries").findall("library")
114        self.classes = self.board.find("classes").findall("class")
115        self.designrules = self.board.find("designrules").findall("param")
116        self.autorouter = self.board.find("autorouter")
117        # These are all the actual parts on the board
118        self.elements = self.board.find("elements").findall("element")
119        self.parts = self.elements
120        self.signals = self.board.find("signals").findall("signal")
Parameters
  • file: Path to an Eagle file. If None, getting the path from command line args will be attempted. If that fails, the user will be prompted for a file path.

A backup of the file will be created during the initialization.

def get_packages(self, attribute: str = None) -> list[str | xml.etree.ElementTree.Element]:
122    def get_packages(self, attribute: str = None) -> list[str | ElementTree.Element]:
123        """Return a list of package elements that are used
124        in the board file.
125
126        :param attribute: If given, the returned list will only
127        contain the given attribute for each element instead of
128        the whole ElementTree.Element ."""
129        packages = [
130            package
131            for package in self.board.findall(".//libraries/library/packages/package")
132        ]
133        return self.get_attribute(attribute, packages) if attribute else packages

Return a list of package elements that are used in the board file.

Parameters
  • attribute: If given, the returned list will only contain the given attribute for each element instead of the whole ElementTree.Element .
def get_smd_packages(self, attribute: str = None) -> list[str | xml.etree.ElementTree.Element]:
135    def get_smd_packages(
136        self, attribute: str = None
137    ) -> list[str | ElementTree.Element]:
138        """Return a list of smd packages used in the board file.
139
140        :param attribute: If given, the returned list will only
141        contain the given attribute for each element instead of
142        the whole ElementTree.Element ."""
143        packages = [
144            package
145            for package in self.get_packages()
146            if type(package.find("smd")) is ElementTree.Element
147        ]
148        return self.get_attribute(attribute, packages) if attribute else packages

Return a list of smd packages used in the board file.

Parameters
  • attribute: If given, the returned list will only contain the given attribute for each element instead of the whole ElementTree.Element .
def get_smd_parts(self, attribute: str = None) -> list[str | xml.etree.ElementTree.Element]:
150    def get_smd_parts(self, attribute: str = None) -> list[str | ElementTree.Element]:
151        """Filter self.parts for smd parts.
152
153        :param attribute: If given, the returned list will only
154        contain the given attribute for each element instead of
155        the whole ElementTree.Element ."""
156        smd_packages = self.get_smd_packages("name")
157        parts = [part for part in self.parts if part.get("package") in smd_packages]
158        return self.get_attribute(attribute, parts) if attribute else parts

Filter self.parts for smd parts.

Parameters
  • attribute: If given, the returned list will only contain the given attribute for each element instead of the whole ElementTree.Element .
def get_bounds(self) -> dict[str, float]:
160    def get_bounds(self) -> dict[str, float]:
161        """Return a dictionary representing
162        the minimum and maximum x and y coordinates
163        of the board."""
164        wires = [
165            wire for wire in self.plain.findall("wire") if wire.get("layer") == "20"
166        ]
167        get_coords = lambda n: [
168            float(v)
169            for wire in wires
170            for v in itertools.chain([wire.get(f"{n}1"), wire.get(f"{n}2")])
171        ]
172        x = get_coords("x")
173        y = get_coords("y")
174        return {"x0": min(x), "xf": max(x), "y0": min(y), "yf": max(y)}

Return a dictionary representing the minimum and maximum x and y coordinates of the board.

def get_dimensions(self) -> tuple[float, float]:
176    def get_dimensions(self) -> tuple[float, float]:
177        """Returns a two-tuple containing
178        the width and height of the board's
179        bounding box."""
180        bounds = self.get_bounds()
181        return (bounds["xf"] - bounds["x0"], bounds["yf"] - bounds["y0"])

Returns a two-tuple containing the width and height of the board's bounding box.

def get_center(self) -> tuple[float, float]:
183    def get_center(self) -> tuple[float, float]:
184        """Returns a two-tuple containing
185        the x,y center point of the board's
186        bounding box."""
187        bounds = self.get_bounds()
188        midpoint = lambda n: (bounds[f"{n}f"] + bounds[f"{n}0"]) * 0.5
189        return (midpoint("x"), midpoint("y"))

Returns a two-tuple containing the x,y center point of the board's bounding box.

def get_area(self) -> float:
191    def get_area(self) -> float:
192        """Returns the area of the board's bounding box.
193        Note: This is not the same as the board's
194        physical area for shapes other than rectangular ones.
195        It also doesn't account for cut-outs."""
196        wxh = self.get_dimensions()
197        return wxh[0] * wxh[1]

Returns the area of the board's bounding box. Note: This is not the same as the board's physical area for shapes other than rectangular ones. It also doesn't account for cut-outs.

def get_visible_layers(self) -> list[str]:
199    def get_visible_layers(self) -> list[str]:
200        """Returns a list of visible layers."""
201        return [
202            layer.get("name") for layer in self.layers if layer.get("visible") == "yes"
203        ]

Returns a list of visible layers.

class ScriptWriter:
206class ScriptWriter:
207    """Class for writing Eagle script files.
208    These files use the Editor command syntax
209    as documented in the Eagle manual.
210    Scipts generated by this class will be invoked
211    and executed within Eagle after returning
212    from the invoking Ulp file for your Python
213    script.
214
215    The preferred usage of this class is
216    with a context manager. When used with a context
217    manager, you don't need to remember to
218    call the save() method at the end of the script.
219    You also don't need to remember to add 'write;'
220    as the last command, as the save() function adds it for you.
221    Commands can be added with the '+=' operator.
222    The Eagle syntax specifies each command ends with
223    ';'. If you leave it off of an added command,
224    it will be added for you.
225    Some script commands are provided as member functions.
226    For further script options, refer to the Eagle help menu.
227
228    e.x. Moving the center of the board to (0,0)
229    >>> brd = Board()
230    >>> with ScriptWriter(brd.path) as scr:
231    >>>     center = brd.get_center()
232    >>>     scr.group_all() # Adds "group all;" to commands
233    >>>     scr += f"move (>0 0) (-{center[0]} -{center[1]})"""
234
235    def __init__(
236        self,
237        script_name: str = None,
238    ):
239        """The .scr file will be saved to a "scripts" subfolder within the same
240        directory as the eagle_file param.
241
242        :param eagle_file: The path to the Eagle file
243        the script is being written for.
244
245        :param script_name: The name of the script creating the instance.
246        If None, the inspect module will be used to determine the script name."""
247        if not script_name:
248            script_name = (Path(inspect.stack()[1].filename)).stem
249        scriptsdir = Path(autoeagle_config.load_config()["scriptsdir"])
250        self.path = scriptsdir / f"{script_name}.scr"
251        self.commands = []
252
253    def __enter__(self):
254        return self
255
256    def __exit__(self, *args):
257        self.save()
258
259    def __iadd__(self, command: str):
260        if not command.endswith(";"):
261            command += ";"
262        self.commands.append(command)
263        return self
264
265    def save(self):
266        self += "write"
267        self.path.write_text("\n".join(self.commands))
268
269    def group_all(self):
270        """Put everything on a visible layer into a group."""
271        self += "group all"
272
273    def display_all(self):
274        """Make every layer in the design visible."""
275        self += "display all"
276
277    def display_layers(self, layers: list[str]):
278        """Displays only the layers in 'layers'."""
279        self += f"display none {' '.join(layers)}"
280
281    def _coordinate_string(self, coordinates: list[tuple[float, float]]) -> str:
282        """Return command snippet for coordinates to be used with draw commands.
283
284        :param coordinates: A list of two-tuples. A string will
285        be returned specifying the coordinates in the same order
286        as the param."""
287        return " ".join(
288            f"({coordinate[0]} {coordinate[1]})" for coordinate in coordinates
289        )
290
291    def _rectangle_coordinates(
292        self, x0: float, xf: float, y0: float, yf: float
293    ) -> list[tuple[float, float]]:
294        """Return a list of two-tuples that describe
295        a rectangle given the coordinates of the sides.
296
297        :param x0,xf,y0,yf: Coordinates describing
298        the sides of a rectangle."""
299
300        return [(x0, y0), (x0, yf), (xf, yf), (xf, y0), (x0, y0)]
301
302    def draw_group(self, coordinates: list[tuple[float, float]]):
303        """Draw a group bounded by 'coordinates'.
304
305        :param coordinates: A list of two-tuples specifying
306        the path to take when drawing the group."""
307        self += f"group {self._coordinate_string(coordinates)}"
308
309    def draw_rectangle_group(self, x0: float, xf: float, y0: float, yf: float):
310        """Draw a rectangular group whose sides are described by the given coordinates."""
311        self.draw_group(self._rectangle_coordinates(x0, xf, y0, yf))
312
313    def draw(
314        self,
315        coordinates: list[tuple[float, float]],
316        layer: str = None,
317        width: float = None,
318    ):
319        """Draw a line throught the given series of coordinate points.
320
321        :param coordinates: A list of two-tuples specifying
322        the path to take when drawing the line.
323
324        :param layer: The layer to draw the line on.
325        If None, the currently active layer is used.
326
327        :param width: The width of the line.
328        If None, the currently active width is used.
329
330        >>> # Draw a line of width 1 on layer "tplace" from (0 0) to (10 0) to (3 5)
331        >>> draw([(0, 0), (10, 0), (3,5)], "tPlace", 1)"""
332
333        if layer:
334            self += f"layer {layer}"
335        if width:
336            self += f"width {width}"
337        self += f"line {self._coordinate_string(coordinates)}"
338
339    def draw_rectangle(
340        self,
341        x0: float,
342        xf: float,
343        y0: float,
344        yf: float,
345        layer: str = None,
346        width: float = None,
347    ):
348        """Draw a rectangle whose sides are described by the given
349        coordinates: x0, xf, y0, yf.
350
351        :param layer: The layer to draw the line on.
352        If None, the currently active layer is used.
353
354        :param width: The width of the line.
355        If None, the currently active width is used.
356
357        >>> # Draw a 10 x 10 board outline of width 1 centered at (0, 0)
358        >>> draw_rectangle(-5, 5, -5, 5, "Dimension", 1)"""
359        self += self.draw(self._rectangle_coordinates(x0, xf, y0, yf), layer, width)
360
361    def rats_nest(self, ripup_polygons: bool = True):
362        """Run the rats nest command
363
364        :param ripup_polygons: If True, run 'ripup @'
365        after running rats nest. Eagle gets real
366        slow when polygons are visible."""
367
368        self += "ratsnest"
369        if ripup_polygons:
370            self += "ripup @"

Class for writing Eagle script files. These files use the Editor command syntax as documented in the Eagle manual. Scipts generated by this class will be invoked and executed within Eagle after returning from the invoking Ulp file for your Python script.

The preferred usage of this class is with a context manager. When used with a context manager, you don't need to remember to call the save() method at the end of the script. You also don't need to remember to add 'write;' as the last command, as the save() function adds it for you. Commands can be added with the '+=' operator. The Eagle syntax specifies each command ends with ';'. If you leave it off of an added command, it will be added for you. Some script commands are provided as member functions. For further script options, refer to the Eagle help menu.

e.x. Moving the center of the board to (0,0)

>>> brd = Board()
>>> with ScriptWriter(brd.path) as scr:
>>>     center = brd.get_center()
>>>     scr.group_all() # Adds "group all;" to commands
>>>     scr += f"move (>0 0) (-{center[0]} -{center[1]})
ScriptWriter(script_name: str = None)
235    def __init__(
236        self,
237        script_name: str = None,
238    ):
239        """The .scr file will be saved to a "scripts" subfolder within the same
240        directory as the eagle_file param.
241
242        :param eagle_file: The path to the Eagle file
243        the script is being written for.
244
245        :param script_name: The name of the script creating the instance.
246        If None, the inspect module will be used to determine the script name."""
247        if not script_name:
248            script_name = (Path(inspect.stack()[1].filename)).stem
249        scriptsdir = Path(autoeagle_config.load_config()["scriptsdir"])
250        self.path = scriptsdir / f"{script_name}.scr"
251        self.commands = []

The .scr file will be saved to a "scripts" subfolder within the same directory as the eagle_file param.

Parameters
  • eagle_file: The path to the Eagle file the script is being written for.

  • script_name: The name of the script creating the instance. If None, the inspect module will be used to determine the script name.

def save(self):
265    def save(self):
266        self += "write"
267        self.path.write_text("\n".join(self.commands))
def group_all(self):
269    def group_all(self):
270        """Put everything on a visible layer into a group."""
271        self += "group all"

Put everything on a visible layer into a group.

def display_all(self):
273    def display_all(self):
274        """Make every layer in the design visible."""
275        self += "display all"

Make every layer in the design visible.

def display_layers(self, layers: list[str]):
277    def display_layers(self, layers: list[str]):
278        """Displays only the layers in 'layers'."""
279        self += f"display none {' '.join(layers)}"

Displays only the layers in 'layers'.

def draw_group(self, coordinates: list[tuple[float, float]]):
302    def draw_group(self, coordinates: list[tuple[float, float]]):
303        """Draw a group bounded by 'coordinates'.
304
305        :param coordinates: A list of two-tuples specifying
306        the path to take when drawing the group."""
307        self += f"group {self._coordinate_string(coordinates)}"

Draw a group bounded by 'coordinates'.

Parameters
  • coordinates: A list of two-tuples specifying the path to take when drawing the group.
def draw_rectangle_group(self, x0: float, xf: float, y0: float, yf: float):
309    def draw_rectangle_group(self, x0: float, xf: float, y0: float, yf: float):
310        """Draw a rectangular group whose sides are described by the given coordinates."""
311        self.draw_group(self._rectangle_coordinates(x0, xf, y0, yf))

Draw a rectangular group whose sides are described by the given coordinates.

def draw( self, coordinates: list[tuple[float, float]], layer: str = None, width: float = None):
313    def draw(
314        self,
315        coordinates: list[tuple[float, float]],
316        layer: str = None,
317        width: float = None,
318    ):
319        """Draw a line throught the given series of coordinate points.
320
321        :param coordinates: A list of two-tuples specifying
322        the path to take when drawing the line.
323
324        :param layer: The layer to draw the line on.
325        If None, the currently active layer is used.
326
327        :param width: The width of the line.
328        If None, the currently active width is used.
329
330        >>> # Draw a line of width 1 on layer "tplace" from (0 0) to (10 0) to (3 5)
331        >>> draw([(0, 0), (10, 0), (3,5)], "tPlace", 1)"""
332
333        if layer:
334            self += f"layer {layer}"
335        if width:
336            self += f"width {width}"
337        self += f"line {self._coordinate_string(coordinates)}"

Draw a line throught the given series of coordinate points.

Parameters
  • coordinates: A list of two-tuples specifying the path to take when drawing the line.

  • layer: The layer to draw the line on. If None, the currently active layer is used.

  • width: The width of the line. If None, the currently active width is used.

>>> # Draw a line of width 1 on layer "tplace" from (0 0) to (10 0) to (3 5)
>>> draw([(0, 0), (10, 0), (3,5)], "tPlace", 1)
def draw_rectangle( self, x0: float, xf: float, y0: float, yf: float, layer: str = None, width: float = None):
339    def draw_rectangle(
340        self,
341        x0: float,
342        xf: float,
343        y0: float,
344        yf: float,
345        layer: str = None,
346        width: float = None,
347    ):
348        """Draw a rectangle whose sides are described by the given
349        coordinates: x0, xf, y0, yf.
350
351        :param layer: The layer to draw the line on.
352        If None, the currently active layer is used.
353
354        :param width: The width of the line.
355        If None, the currently active width is used.
356
357        >>> # Draw a 10 x 10 board outline of width 1 centered at (0, 0)
358        >>> draw_rectangle(-5, 5, -5, 5, "Dimension", 1)"""
359        self += self.draw(self._rectangle_coordinates(x0, xf, y0, yf), layer, width)

Draw a rectangle whose sides are described by the given coordinates: x0, xf, y0, yf.

Parameters
  • layer: The layer to draw the line on. If None, the currently active layer is used.

  • width: The width of the line. If None, the currently active width is used.

>>> # Draw a 10 x 10 board outline of width 1 centered at (0, 0)
>>> draw_rectangle(-5, 5, -5, 5, "Dimension", 1)
def rats_nest(self, ripup_polygons: bool = True):
361    def rats_nest(self, ripup_polygons: bool = True):
362        """Run the rats nest command
363
364        :param ripup_polygons: If True, run 'ripup @'
365        after running rats nest. Eagle gets real
366        slow when polygons are visible."""
367
368        self += "ratsnest"
369        if ripup_polygons:
370            self += "ripup @"

Run the rats nest command

Parameters
  • ripup_polygons: If True, run 'ripup @' after running rats nest. Eagle gets real slow when polygons are visible.