Reference
Python API reference
Every public function in the pyacad library, with its exact signature, what it returns, how it fails and whether it needs AutoCAD.
The pyacad library is what your script talks to instead of the raw AutoCAD
.NET API. It is a flat set of small modules, each covering one surface of the drawing or of
pyACAD itself: the active document, the selection, entities, layers, blocks, xrefs, layouts,
plotting, drawing health, AutoLISP, units, and then the dialogs, toasts, the output window,
settings and Excel files. Almost everything is a plain function that takes and returns plain
Python values.
This page is the complete public surface. If a name is not on this page, it is either
private (it starts with an underscore) or it does not exist. Every signature below was taken
from the source, and every pyacad.module.name in an example is checked against
the library each time the site is built.
Importing pyacad#
The package itself exports one name:
import pyacad
pyacad.__version__ # '0.1.0'
pyacad.doc # AttributeError until something imports pyacad.docEverything useful lives in a submodule, and the house style is to import the submodules you need by name:
from pyacad import doc, ents, layers, forms, toastThat reads well at the call site (layers.freeze('A-*'),
toast.success('Done')) and it matters here for a second reason: several functions
share a name with a Python builtin (ents.set, ents.iter,
xrefs.list), so from pyacad.ents import * would quietly replace
set and iter in your script. Never star-import a pyacad module.
pyacad._api, _atomic, _charts,
_datafiles, _history, _log, _markdown,
_repl, _util and _xlsx are internal. They change without
notice and have no compatibility promise; _api loads the AutoCAD assemblies as a
side effect of being imported. Everything they do that a script needs is exposed by a public
module. The private modules at the foot of this page says what
each one is, so you can recognise them in a traceback.
The modules at a glance#
The last column says whether importing the module needs a live AutoCAD session
with pyACAD loaded. The drawing modules import anywhere because they reach the AutoCAD API
only inside each function, through pyacad._api; calling one of their API
functions outside AutoCAD raises ImportError with a plain message. The four
framework modules marked yes load the pyACAD runtime assembly at import time, so they import
only inside AutoCAD (or AutoCAD Core Console under pyacad run).
| Module | What it is for | Import needs AutoCAD |
|---|---|---|
doc |
The active drawing: document, database, editor, transactions, locks, saving, system variables. | No |
sel |
The pickfirst set, interactive picks, and filtered select-all. | No |
ents |
Entities by handle: iterate a space, snapshot, change, delete, measure. | No |
layers |
List, create, rename, delete, merge, freeze, isolate, snapshot and restore layers; layer standards. | No |
blocks |
Definitions, references, attributes, counts, inserting, tables, spreadsheet round trips. | No |
xrefs |
List, reload, unload, detach, bind and repath external references. | No |
layouts |
Paper space tabs: order, current, create, copy, rename, delete, page setups. | No |
plot |
Plot a layout to PDF, list plot devices and paper sizes, name batch output files. | No |
audit |
Purge, AUDIT, zero-length and duplicate geometry, proxies, missing fonts. | No |
lisp |
Call exposed AutoLISP functions, queue an expression, read (pyacad ...) arguments. | No |
units |
INSUNITS names, conversions through metres, length formatting. | No |
forms |
Modal dialogs: alert, confirm, prompts, pickers, cancellable progress, XAML windows. | Yes |
toast |
Small status notes in the corner of the AutoCAD window. | Yes |
banner |
One result on a full-width bar across the bottom of the window. | Yes |
output |
The output window: HTML, markdown, tables, charts, images, progress, object links. | No |
script |
The running command: paths, arguments, toggle state, session variables, per-tool settings and data, a logger. | Yes |
settings |
Per-tool JSON settings files under %APPDATA%\pyACAD\settings. | No |
xl |
Read and write .xlsx workbooks with no Excel and no extra packages. | No |
Three badges appear on every function: Pure means plain Python with no AutoCAD and no pyACAD runtime, so it runs and unit-tests anywhere, including a dev-time CPython; Needs AutoCAD means it reads or changes the drawing, or talks to the pyACAD runtime (a window, a toast, the running command), so it only works inside AutoCAD; Blocks means it opens a modal window or a command-line prompt and your script waits until the user answers.
Conventions that cut across every module#
The modules were written to one set of rules. Read these once and most of the individual entries below become predictable.
Objects are handles, not AutoCAD objects#
Every entity, block reference and table the library hands you is identified by its
handle: the hex string AutoCAD shows in LIST, such as '2F3A'.
Handles survive saving and reopening, so you can store them, write them to Excel and match
them up later. Functions that read return plain dicts, lists and strings, never an AutoCAD
object that would go stale once its transaction closes. The only functions that hand you raw
.NET objects are doc.active(), doc.database(),
doc.editor(), doc.transaction() and doc.lock(), which
exist for the moment you need the API directly.
A handle argument may be upper or lower case and may carry a 0x prefix
(ents.normalize_handle cleans it). An argument that takes "one or many" handles or
names also accepts a single string.
Points are tuples, angles are radians#
Points come back as (x, y, z) tuples of floats. Points you pass in may be
(x, y) or (x, y, z); a 2D point gets z = 0.0. Angles,
such as a block reference's rotation in ents.get or the
rotation you give blocks.insert, are in radians, as AutoCAD stores
them. The one exception is blocks.export_rows, which writes degrees for people to
read in Excel.
One call, one transaction#
Each function opens its own transaction (through doc.transaction()), commits it
when it finishes and aborts it if anything raises. A failed call therefore leaves the drawing
exactly as it was. The cost is that calling ents.set a thousand times opens a
thousand transactions; where a tool naturally works on many objects there is a batch function
that does them all in one (ents.lengths, ents.delete,
blocks.snapshots, blocks.apply_changes, audit.erase,
layers.merge).
Command context and document locks#
A ribbon click, a keyboard chord and a typed command name all run your script as an
AutoCAD command, so the document is already locked and the whole run is one undo step. A
(pyacad ...) call from AutoLISP runs it in document context, inside whatever
command or expression called it. Code that runs later, outside any command, is not:
a hook, a dock pane's button handler, a modeless window's callback. Wrap anything that changes
the drawing there in with doc.lock():. Reading usually works without a lock, but
taking one costs nothing.
Two kinds of wildcard#
Name patterns in layers (and the block name in blocks.references)
use pyacad's own matcher: * is any run of characters, ? is one
character, matching ignores case and covers the whole name, and you may pass a list of patterns.
Nothing else is special, so # and @ are literal.
sel.get(layer=...) is different: it goes straight into an AutoCAD selection
filter, so it uses AutoCAD's own wildcard syntax (the WCMATCH rules, where #,
@, ~ and commas mean something).
Naming a space#
Functions that take a space argument read it the same way
(ents.resolve_space): None, '' or 'model'
(any case) is model space, 'BLOCK:Door' is the definition of block Door, and
anything else is a paper space layout's name. Watch the default: in ents,
space=None means model space only; in blocks.references and
blocks.counts, space=None means model space and every layout.
Progress and cancel callbacks#
Long-running functions take two optional callables, progress and
cancelled. Every doc.STEP items (200), starting with the first, the
function calls progress(index, total) and then cancelled(); when
cancelled() returns true it stops. Most stop where they are and return what they
have so far (and commit it, for the ones that write); layers.merge instead raises
RuntimeError and rolls everything back. The cookbook shows
how to wire these to a progress window.
How things fail#
| Exception | Means |
|---|---|
ValueError | Bad input, or a name or handle that does not exist:
'No layer is named X.', '2F3A is not a block reference.'. The message
is written for a person, so it is fine to show it in a toast. |
RuntimeError | The drawing is in the wrong state: no drawing open, never saved, read-only, a plot already running, a cancelled merge. |
ImportError | An AutoCAD function was called outside AutoCAD. |
| AutoCAD's own exception | Something the library does not check first, most often
writing to an entity on a locked layer (eOnLockedLayer). It surfaces as
Autodesk.AutoCAD.Runtime.Exception, and the transaction is still rolled back. |
doc: the active drawing#
Every other drawing module reaches the drawing through this one, and
doc.transaction() is the single way into the database. The module imports
anywhere; its AutoCAD functions import the API when called. Everything here works in Core
Console too (none of it needs the AutoCAD UI assembly). The last four functions are pure
helpers the other modules share, and you are welcome to use them.
doc.active#
Needs AutoCAD The active Document (AutoCAD's
DocumentManager.MdiActiveDocument). Raises RuntimeError('No drawing is
open.') when there is none, which makes it the natural first line of a tool that must
have a drawing.
from pyacad import doc
document = doc.active()
print(document.Name)doc.database#
Needs AutoCAD The active drawing's Database. Raises RuntimeError like
doc.active().
db = doc.database()
print(db.Filename)doc.editor#
Needs AutoCAD The active drawing's Editor, for prompts and messages the library does
not wrap.
doc.editor().WriteMessage('\nHello from pyACAD\n')doc.path#
Needs AutoCAD The drawing's full path as a string, or the literal 'untitled' when it
has never been saved. Compare against 'untitled', not against
None.
if doc.path() == 'untitled':
toast.warning('Save the drawing first')doc.is_saved#
Needs AutoCAD True when the drawing has a file and no unsaved changes (the DBMOD system
variable is 0). Always False for an untitled drawing.
if not doc.is_saved():
forms.alert('Save your changes before running a batch plot.')doc.is_read_only#
Needs AutoCAD True when the drawing was opened read-only.
if doc.is_read_only():
toast.error('This drawing is read-only')doc.transaction#
Needs AutoCAD A context manager over one database transaction. with hands you the
AutoCAD Transaction; it commits when the block ends normally and aborts when the
block raises (the exception still propagates), then disposes it either way. Use it when you need
the raw API inside one transaction; the rest of the library calls it for you.
from pyacad import doc
db = doc.database() # loads the AutoCAD assemblies on first use
from Autodesk.AutoCAD.DatabaseServices import OpenMode
with doc.transaction() as tr:
table = tr.GetObject(db.LayerTableId, OpenMode.ForRead)
print(table.Has('A-WALL'))The Autodesk.AutoCAD namespaces import once the assemblies are loaded, and
the first pyacad call that touches the drawing loads them. That is why the example calls
doc.database() before the from Autodesk... line.
doc.lock#
Needs AutoCAD A context manager that locks the active document and unlocks it on exit. Commands are already locked; you need this in code that runs outside a command: hooks, dock pane handlers, modeless window callbacks. It does not start a transaction.
def on_click(sender, args): # a dock pane button
with doc.lock():
layers.freeze('Z-*')doc.save#
Needs AutoCAD Saves the drawing to its own path in the current DWG format, taking a document lock
itself, and returns the path. Raises RuntimeError when the drawing has never been
saved (there is no path to save to) or is read-only.
saved_to = doc.save()
toast.success('Saved', saved_to)doc.units#
Needs AutoCAD The drawing's insertion units (INSUNITS) as AutoCAD's enum name, such as
'Millimeters', 'Inches' or 'Undefined'. These names
are exactly what the units module takes.
print(doc.units()) # 'Millimeters'doc.open_documents#
Needs AutoCAD A list with one entry per open drawing: the full path for saved drawings, the document
name (such as 'Drawing1.dwg') for untitled ones.
for name in doc.open_documents():
print(name)doc.send_command#
Needs AutoCAD Queues text plus a newline on the active drawing's command line. It does
not run now: it runs after your script returns, so you get nothing back and the next line of
your script runs before it. Prefix command names with _. so they work in any
language and are not redefined.
doc.send_command('_.ZOOM _E')doc.sysvar#
Needs AutoCAD A system variable's value, as AutoCAD returns it (an int, float, string or point
object). Wrap it in int() or str() when you compare it, as the
library does.
if int(doc.sysvar('PSLTSCALE')) != 1:
toast.warning('PSLTSCALE is not 1')doc.set_sysvar#
Needs AutoCAD Sets a system variable. Booleans go in as 1 or 0. Integers are tried as 16-bit first (most integer variables are) and then as 32-bit, floats go in as doubles, anything else as it is. AutoCAD raises its own exception for an unknown or read-only variable.
doc.set_sysvar('LTSCALE', 1.0)
doc.set_sysvar('PSLTSCALE', True)
doc.set_sysvar('CLAYER', 'A-WALL')doc.as_point#
Pure (x, y) or (x, y, z) as a tuple of three floats; a 2D point gets
z = 0.0. Raises ValueError for anything else.
doc.as_point((10, 20)) # (10.0, 20.0, 0.0)doc.as_names#
Pure One name or an iterable of names as a list of strings; None gives
[]. This is why "one or many" arguments accept a bare string.
doc.as_names('A-WALL') # ['A-WALL']
doc.as_names(('A-WALL', 'A-DOOR'))doc.step#
Pure The progress and cancel hook every batch function uses. On every
STEPth index (0, 200, 400, ...) it calls progress(index, total) when
given, then returns True when cancelled is given and returns true.
On other indices it returns False without calling anything. Use it in your own
loops to behave like the library.
for index, handle in enumerate(handles):
if doc.step(index, len(handles), report, user_cancelled):
breakdoc.STEP#
Pure How many items pass between progress reports and cancel checks in every batch function.
sel: selection#
The pickfirst set (what is selected when your tool starts), interactive picks on the command
line, and filtered select-all. Everything is in handles. build_filter and
value_kind are pure; the rest import the API when called. The pick_*
prompts need an interactive editor, so under Core Console they come back as None or
[].
sel.current#
Needs AutoCAD Handles of the objects that were selected when the command started (the pickfirst
set), or []. pyACAD's commands accept a pickfirst set, so selecting first and
then clicking a button works.
from pyacad import sel, toast
picked = sel.current()
if not picked:
toast.info('Select something first')sel.set_current#
Needs AutoCAD Makes these handles the pickfirst set, so they show as selected (grips) when your
script ends. One handle or a list. Raises ValueError for a handle that does not
exist.
sel.set_current(sel.get(type='CIRCLE'))sel.clear#
Needs AutoCAD Empties the pickfirst set.
sel.clear()sel.pick_one#
Needs AutoCAD Blocks Prompts on the command line for one object and returns its handle, or
None when the user presses Esc or picks nothing.
handle = sel.pick_one('Select a block: ')
if handle and ents.type_of(handle) == 'INSERT':
print(blocks.attributes(handle))sel.pick_many#
Needs AutoCAD Blocks Prompts for a selection (window, crossing, any method) and returns a list of
handles, [] on cancel.
handles = sel.pick_many('Select lines to measure: ')sel.pick_point#
Needs AutoCAD Blocks Prompts for a point (object snaps work) and returns (x, y, z) in
world coordinates, or None on cancel.
point = sel.pick_point('Insertion point: ')
if point is not None:
blocks.insert('NORTH', point)sel.get#
Needs AutoCAD Handles of every entity in the drawing that passes the filter, using AutoCAD's
select-all with a selection filter (see sel.build_filter for the arguments). With
no arguments it returns every entity in model space and all layouts. Entities inside block
definitions are not included, because select-all never sees them.
walls = sel.get(type='LWPOLYLINE', layer='A-WALL*')
texts = sel.get(type=['TEXT', 'MTEXT'], space='model')
red = sel.get(c62=1) # colour index 1 set on the entitysel.build_filter#
Pure The (code, value) pairs sel.get turns into a selection
filter. type is one DXF name or a list (any case, joined with commas as AutoCAD
expects); layer is a layer name with AutoCAD wildcards; space is a
layout name, with 'model' (any case) meaning model space. Any other DXF group code
goes in as a keyword c<code>=value, such as c62=1 for colour 1
or c40=2.5 for a radius; the value is converted to what that code takes (see
sel.value_kind) and extra pairs are sorted by code. Raises ValueError
for a keyword that is not shaped like c62.
sel.build_filter(type=['line', 'arc'], layer='M-*', c62=3)
# [(0, 'LINE,ARC'), (8, 'M-*'), (62, 3)]sel.value_kind#
Pure What a DXF group code takes in a filter: 'int16',
'int32', 'double', 'point' or 'text'
(anything not in a numeric or point range).
sel.value_kind(62) # 'int16'
sel.value_kind(40) # 'double'
sel.value_kind(8) # 'text'ents: entities by handle#
Iterate a space, take a snapshot of an entity as a plain dict, change a few common
properties, delete, and measure. ents.get returns a dict, so a script never holds
an AutoCAD object past its transaction. resolve_space,
normalize_handle, check_props and length_by_layer are
pure. Two functions here are named like builtins, iter and set:
always call them as ents.iter and ents.set.
ents.iter#
Needs AutoCAD A generator over the handle of every entity in model space (the default), a layout
(its name) or a block definition ('BLOCK:Name'). The handles are read in one
transaction before the first one is yielded, so changing the drawing while you loop is safe.
Raises ValueError for an unknown layout or block.
from pyacad import ents
for handle in ents.iter():
print(handle, ents.type_of(handle))
for handle in ents.iter('BLOCK:Door'):
pass # the entities that make up the Door definitionents.get#
Needs AutoCAD A snapshot of one entity as a plain dict. Every entity carries
handle, type (the DXF name), layer, color
(the colour index: 256 is ByLayer, 0 ByBlock), linetype, lineweight
(an int, see ents.LINEWEIGHTS), visible and space
('Model', a layout name, or 'BLOCK:Name'). Some types add geometry:
| Type | Extra keys |
|---|---|
LINE | start, end |
CIRCLE | center, radius |
ARC | center, radius, start_angle, end_angle (radians) |
LWPOLYLINE | points (a list of (x, y)), closed |
TEXT | text, height, position |
MTEXT | text (the raw contents, formatting codes included), height, position |
INSERT | name (a dynamic block's real name, not *U12), position, rotation (radians), scale (x, y, z), attributes {tag: text} |
Raises ValueError when no object has that handle.
snap = ents.get('2F3A')
if snap['type'] == 'LINE':
print(snap['start'], snap['end'])ents.set#
Needs AutoCAD Changes properties of one entity. Any entity takes layer,
color (an int 0 to 256), linetype, lineweight (one of
ents.LINEWEIGHTS) and visible; TEXT and
MTEXT also take text and height; INSERT
takes attributes (a dict of tag to text, matched to the tags exactly as the
drawing spells them). Everything is validated by ents.check_props before anything
changes, and the layer and linetype must exist ('ByLayer' and
'ByBlock' are always allowed), else ValueError. Returns
None. An entity on a locked layer raises AutoCAD's
eOnLockedLayer.
ents.set('2F3A', layer='A-WALL', color=256)
ents.set('2F40', text='LEVEL 2', height=3.5)
ents.set('2F41', attributes={'ROOM_NO': '2.14'})ents.delete#
Needs AutoCAD Erases the entities (one handle or a list) in one transaction and returns how many
were erased. A handle that does not exist raises ValueError and nothing is
erased; an entity on a locked layer raises AutoCAD's exception and nothing is erased. Stopping
through cancelled keeps what was erased so far. For a forgiving delete that skips
locked layers and missing handles, use audit.erase.
erased = ents.delete(sel.get(type='POINT'))ents.count#
Needs AutoCAD How many entities a space holds (model space by default; a layout name or
'BLOCK:Name' otherwise).
print(ents.count(), 'in model space')ents.bounds#
Needs AutoCAD The entity's extents as ((minx, miny, minz), (maxx, maxy, maxz)).
AutoCAD raises its own exception for an entity with no extents (an empty text, a ray).
low, high = ents.bounds(handle)
width = high[0] - low[0]ents.type_of#
Needs AutoCAD The entity's DXF type name, such as 'LINE',
'LWPOLYLINE' or 'INSERT', without opening a transaction. Raises
ValueError for an unknown handle.
if ents.type_of(handle) == 'INSERT':
print(blocks.attributes(handle))ents.length_of#
Needs AutoCAD The length along a curve in drawing units: lines, arcs, circles (the circumference),
polylines of every kind, splines and ellipses. Raises ValueError for anything
without a length.
print('%.1f' % ents.length_of(handle))ents.lengths#
Needs AutoCAD Measures many curves in one transaction. Returns a list of
{'handle', 'type', 'layer', 'length'} for each handle whose DXF type is in
types (by default ents.LENGTH_TYPES: lines, arcs, both kinds of
polyline and splines). Other entities and handles that no longer exist are left out silently;
a curve AutoCAD cannot measure gets length None. Feed the result to
ents.length_by_layer for totals.
rows = ents.lengths(sel.get(layer='M-DUCT*'))
total = sum(r['length'] for r in rows if r['length'] is not None)ents.length_by_layer#
Pure Totals per layer from dicts that carry at least 'layer' and
'length' (what ents.lengths returns): a list of
{'layer', 'count', 'length'} sorted by layer name ignoring case. A dict whose length
is None is left out of both the count and the total.
ents.length_by_layer([{'layer': 'A', 'length': 2.0}, {'layer': 'A', 'length': 3.0}])
# [{'layer': 'A', 'count': 2, 'length': 5.0}]ents.resolve_space#
Pure How a space argument is read: ('model', 'Model') for
None, '' or 'model' in any case;
('block', name) for 'BLOCK:name'; ('layout', text) for
anything else. Raises ValueError for 'BLOCK:' with no name.
ents.resolve_space('block:Door') # ('block', 'Door')
ents.resolve_space('A1 Plan') # ('layout', 'A1 Plan')ents.normalize_handle#
Pure A handle as uppercase hex text, with surrounding spaces and a 0x prefix
removed. Raises ValueError when the text is not hex. Use it before comparing
handles that came from a spreadsheet or a file.
ents.normalize_handle(' 0x2f3a ') # '2F3A'ents.check_props#
Pure The validation ents.set runs: raises ValueError when a key
is not allowed for that DXF type, when color is not an int from 0 to 256, when
lineweight is not in ents.LINEWEIGHTS, when height is
not above zero, or when attributes is not a dict. Returns None
otherwise. Handy for checking user input before a batch.
ents.check_props('LINE', {'color': 1}) # fine
ents.check_props('LINE', {'text': 'x'}) # ValueError: LINE cannot set 'text'ents.LINEWEIGHTS#
Pure The lineweights ents.set accepts, in hundredths of a millimetre:
-3 is Default, -2 ByBlock, -1 ByLayer, and the rest are
AutoCAD's standard weights from 0 to 2.11 mm. The same numbers come back in
ents.get and layers.get.
ents.LENGTH_TYPES#
Pure The DXF types ents.lengths measures by default. POLYLINE
covers both the old 2D polyline and the 3D polyline.
layers: layers and layer standards#
Read, create, rename, delete and merge layers; freeze, thaw, turn off and on, lock and
unlock by pattern; isolate; and save and restore layer states. Pattern arguments take
* and ?, ignore case, and may be a list of patterns. The
freeze-style functions return how many layers actually changed and regenerate the drawing.
The second half of the module is pure: matching, a filter box, and the engine behind a
layer-standard mapping (a CSV of old name to new name).
layers.names#
Needs AutoCAD Every layer name in the drawing, in layer table order (xref layers included, as
Xref|Layer).
from pyacad import layers
for name in layers.names():
print(name)layers.current#
Needs AutoCAD The current layer's name (CLAYER).
print(layers.current())layers.set_current#
Needs AutoCAD Makes the named layer current, thawing it and turning it on first so AutoCAD accepts
it. Raises ValueError when there is no such layer.
layers.set_current('A-ANNO')layers.exists#
Needs AutoCAD True when the drawing has a layer of that name (AutoCAD compares layer
names ignoring case).
if not layers.exists('A-ANNO'):
layers.create('A-ANNO', color=2)layers.get#
Needs AutoCAD One layer as a dict: name, color (the colour index),
linetype (its name), lineweight (an int, see
ents.LINEWEIGHTS), is_off, is_frozen,
is_locked, is_plottable. Raises ValueError for an
unknown layer.
state = layers.get('A-WALL')
if state['is_frozen']:
layers.thaw('A-WALL')layers.create#
Needs AutoCAD Adds a layer and returns its name. color is a colour index from 1 to
255 (not 0 or 256, which mean ByBlock and ByLayer); linetype must already be
loaded in the drawing. Raises ValueError when the layer exists, the name is not a
valid symbol name, the colour is out of range or the linetype is missing.
layers.create('M-DUCT-SUPPLY', color=4, linetype='Continuous')layers.rename#
Needs AutoCAD Renames a layer. Raises ValueError for layer 0 and Defpoints, an unknown
old name, an invalid new name, or a new name another layer already has. A change of case only
is allowed.
layers.rename('WALLS', 'A-WALL')layers.delete#
Needs AutoCAD Erases a layer. Raises ValueError when it is layer 0 or Defpoints, the
current layer, an xref layer, or still in use (AutoCAD's purge check says something references
it). To empty a layer into another one first, use layers.merge.
try:
layers.delete('OLD-TEMP')
except ValueError as error:
toast.warning('Not deleted', str(error))layers.merge#
Needs AutoCAD Moves every entity on the source layers (one name or a list) to
target, then deletes the sources, all in one transaction, and returns how many
objects moved. It covers model space, every layout and every block definition in this
drawing (not xrefs), including the attributes on block references, and it moves entities on
locked layers too. If a source is the current layer, the target becomes current. A source
equal to the target is ignored. Raises ValueError for an unknown layer or a
source that cannot be deleted afterwards; when cancelled fires it raises
RuntimeError('Merge cancelled; nothing was changed.') and rolls everything
back.
moved = layers.merge(['WALL', 'WALLS', 'Wall-old'], 'A-WALL')
toast.success('Merged 3 layers', '%d objects moved' % moved)layers.freeze, thaw, turn_off, turn_on, lock, unlock#
Needs AutoCAD Change one flag on every layer whose name matches pattern (a pattern or a
list) and return how many layers actually changed; layers already in that state do not count.
freeze never freezes the current layer (AutoCAD would refuse) and skips it
silently. turn_off does turn off the current layer, as AutoCAD allows. All but
lock and unlock regenerate the drawing afterwards. These change the
layer itself, so the effect shows in model space and every layout; they do not touch
viewport overrides.
frozen = layers.freeze(['*-ANNO*', 'Z-*'])
layers.lock('X-*')
layers.turn_on('*')layers.isolate#
Needs AutoCAD Thaws and turns on the named layers (exact names, one or a list) and freezes every
other layer. If the current layer is not among them, the first named layer becomes current.
Returns how many layers were frozen. Raises ValueError for an empty list or an
unknown name. Take a layers.snapshot() first if the user will want the old state
back.
before = layers.snapshot()
layers.isolate(['A-WALL', 'A-DOOR'])layers.snapshot#
Needs AutoCAD {name: layers.get(name)} for every layer, read in one transaction. The
result is plain data, so it can be stored with script.store_data or written to
JSON.
script.store_data('before', layers.snapshot(), per_document=True)layers.restore#
Needs AutoCAD Puts back is_off, is_frozen and is_locked from
a snapshot() (colour and linetype are not restored). Layers that are no longer in
the drawing are skipped, the current layer is never frozen, and the return value is how many
layers changed. Regenerates afterwards.
saved = script.load_data('before', per_document=True)
if saved:
layers.restore(saved)layers.matches#
Pure True when the whole of name fits pattern:
* is any run of characters, ? exactly one, case is ignored, and
every other character is literal.
layers.matches('A-WALL-FULL', 'a-wall*') # True
layers.matches('A-WALL', 'A-WAL?') # Truelayers.matches_any#
Pure True when name fits any of patterns (one
string or a list).
layers.matches_any('Z-TEMP', ['X-*', 'Z-*']) # Truelayers.filter_names#
Pure The names that fit what someone typed in a filter box, in their original order, as a
new list. Blank text keeps everything. Each comma-separated term is a wildcard pattern when it
holds * or ?, otherwise a case-insensitive substring; a name stays
when any term fits.
layers.filter_names(['A-WALL', 'A-DOOR', 'M-DUCT'], 'wall, M-*')
# ['A-WALL', 'M-DUCT']layers.parse_mapping#
Pure Reads a layer-standard CSV into (old, new, color, linetype) tuples.
Columns are old,new[,color][,linetype], and anything after the fourth (a notes
column) is ignored. old may use wildcards. color is 1 to 255 or one
of red, yellow, green, cyan, blue, magenta, white (black also reads as 7); colour and linetype
are None when blank. Cells split on commas, semicolons or tabs, whichever the first
row uses most, and may be quoted. Blank lines, a byte order mark and a header row
(old,new, from,to, layer,standard and the like) are
skipped. Raises ValueError naming the line for a row without an old or new name or
with a bad colour.
rows = layers.parse_mapping('old,new,color\nWALL*,A-WALL,1\nDOOR,A-DOOR,red\n')
# [('WALL*', 'A-WALL', 1, None), ('DOOR', 'A-DOOR', 1, None)]layers.plan_mapping#
Pure What applying a parsed standard to a drawing would do, as a list of steps, without
touching anything. Each existing layer, in order, meets the first row whose old pattern fits
it and becomes ('rename', old, new) when no layer is called new yet, or
('merge', old, target) when one is (counting what earlier steps renamed or merged
away). Layer 0, Defpoints, xref layers and invalid new names become
('skip', name, reason); a layer already called new gives nothing. Every new name
still missing at the end becomes ('create', new, color, linetype) once. Run the
steps yourself with layers.rename, layers.merge and
layers.create.
plan = layers.plan_mapping(layers.names(), rows)
for step in plan:
print(layers.describe_action(step))layers.describe_action#
Pure One plan_mapping step in words: 'Rename A to B',
'Merge A into B', 'Create B (color 3, DASHED)',
'Skip A: reason'.
layers.describe_action(('merge', 'WALLS', 'A-WALL')) # 'Merge WALLS into A-WALL'layers.snapshot_choices#
Pure Picker entries for saved snapshot slots. slots maps a slot name to
{'saved': 'YYYY-MM-DD HH:MM', 'layers': snapshot()}; the result is
(label, name) pairs, newest first then by name, with labels such as
'Before issue (saved 2026-10-12 14:05, 42 layers)'. Slots with no time, or that
are not dicts, come last. The pairs go straight into forms.select_from_list.
slots = script.load_data('slots', {}, per_document=True)
name = forms.select_from_list(layers.snapshot_choices(slots), title='Restore layers')blocks: definitions, references and attributes#
Block definitions and references, attributes, counts, inserting blocks and AutoCAD tables,
and the pure engine behind the spreadsheet round trip (export to Excel, edit, write back).
Dynamic block references always report their definition's real name, never the anonymous
*U copy. Name arguments take * and ? as in
layers.matches. The pure half (attribute_tags,
export_rows, diff_rows, schedule_rows,
replace_in_attributes, tag_audit, column_widths) works on
the plain dicts blocks.snapshots returns.
blocks.definitions#
Needs AutoCAD Names of the drawing's own blocks, sorted ignoring case: layouts, anonymous blocks, xrefs and blocks that belong to xrefs are left out.
from pyacad import blocks
names = blocks.definitions()blocks.references#
Needs AutoCAD Handles of block references. name is None for all of them, or
a pattern (or list of patterns) matched against the block name. space is
None for model space and every layout, or one space as in ents.iter.
Xref attachments are references too, so they are included when their name matches.
doors = blocks.references('DOOR*')
in_model = blocks.references(space='model')blocks.attributes#
Needs AutoCAD {tag: text} of one block reference's attributes. Raises
ValueError when the handle does not exist or is not a block reference.
values = blocks.attributes(handle)
print(values.get('ROOM_NO'))blocks.set_attributes#
Needs AutoCAD Writes {tag: text} into one block reference and returns how many values
actually changed. Tags are matched exactly as the drawing spells them (usually upper case);
tags the reference does not have are ignored. Raises ValueError when
values is not a dict or the handle is not a block reference.
changed = blocks.set_attributes(handle, {'ROOM_NO': '2.14', 'ROOM_NAME': 'Plant'})blocks.counts#
Needs AutoCAD {block name: number of references} in a space, or in model space and every
layout when space is None. Xref attachments are counted under the xref
name.
for name, number in sorted(blocks.counts().items()):
print(name, number)blocks.insert#
Needs AutoCAD Inserts a reference to a block already defined in the drawing into the current space
(model space, or the paper space layout that is current) and returns its handle.
rotation is in radians; scale is one number or (x, y, z);
layer must exist. Every non-constant attribute is added, with the value from
attributes when its tag is there and the definition's default otherwise. Raises
ValueError for an unknown block or layer.
import math
handle = blocks.insert('TAG-ROOM', (1200, 3400), rotation=math.radians(90),
layer='A-ANNO', attributes={'ROOM_NO': '2.14'})blocks.definition_info#
Needs AutoCAD One block definition as a dict: name, has_attributes,
is_dynamic, origin (the base point) and comments (its
description). Raises ValueError for an unknown block.
info = blocks.definition_info('TAG-ROOM')
if not info['has_attributes']:
toast.warning('TAG-ROOM has no attributes')blocks.snapshots#
Needs AutoCAD Snapshot dicts of block references, read in one transaction: every reference in model
space and the layouts when handles is None, else those of the given
handles that are block references (anything else, and handles that no longer exist, are left
out). Each dict is what ents.get gives for an INSERT, plus
tags: the attribute tags in the order the drawing holds them. Xref attachments are
never included. This is the input to every pure function below.
picked = sel.current()
refs = blocks.snapshots(picked if picked else None)blocks.apply_changes#
Needs AutoCAD Writes changes into many block references in one transaction. changes
maps a handle to {'attributes': {tag: text}, 'layer': name}, either part optional,
exactly as blocks.diff_rows produces. Returns
{'updated': references changed, 'values': attribute values changed, 'failed': {handle:
reason}}. A reference that fails (gone, not a block reference, unknown or locked layer)
is left as it was and reported; the rest still go in. Raises ValueError up front
when changes is not shaped right.
result = blocks.apply_changes({'2F3A': {'attributes': {'ROOM_NO': '2.15'}}})
if result['failed']:
output.print_table(sorted(result['failed'].items()), ['Handle', 'Reason'])blocks.insert_table#
Needs AutoCAD Inserts an AutoCAD table with its top left corner at point, in the current
space and the drawing's current table style: an optional title row, a header row, then one row
per item. Every cell is written as text (whole floats lose their .0), short rows are
padded, and columns are sized to their longest text using the style's text height. Returns the
table's handle. Raises ValueError for no headers or a row longer than the
headers.
headers, rows = blocks.schedule_rows(blocks.snapshots())
point = sel.pick_point('Table corner: ')
if point:
blocks.insert_table(point, headers, rows, title='Block schedule')blocks.attribute_tags#
Pure Every attribute tag across the references, once each, in the order first met (a
reference's tags order when it has one, else its tags sorted). Tags that differ only
in case count once, spelled as first met.
blocks.attribute_tags([{'tags': ['NO', 'NAME'], 'attributes': {'NO': '1', 'NAME': 'A'}},
{'attributes': {'AREA': '12'}}])
# ['NO', 'NAME', 'AREA']blocks.export_rows#
Pure (headers, rows) for a spreadsheet of block references: the
blocks.EXPORT_COLUMNS, then one column per attribute tag. One row per reference,
sorted by block name then handle. Rotation is in degrees (0 up to 360); Scale is one number when
uniform, else 'x, y, z' text; a tag the reference lacks is an empty string.
headers, rows = blocks.export_rows(blocks.snapshots())
xl.write(path, rows, headers=headers, sheet='Blocks')blocks.diff_rows#
Pure Compares a sheet written from export_rows (header row first, as
xl.read returns it) with the references in the drawing now. Columns are found by
header name, so a reordered sheet still reads. Only attribute values and Layer go back: a tag
cell is a change when its text differs from the reference's value (an emptied cell clears the
value), a Layer cell when it is filled in and differs. Returns (changes, skipped):
changes is ready for blocks.apply_changes, skipped lists
the sheet's handles that are no longer block references. Raises ValueError for an
empty sheet or one with no Handle column.
changes, skipped = blocks.diff_rows(blocks.snapshots(), xl.read(path))
result = blocks.apply_changes(changes)blocks.schedule_rows#
Pure (headers, rows) counting references by block name, or by name and layer:
headers ['Block', 'Count'] or ['Block', 'Layer', 'Count'], rows sorted
ignoring case, counts as ints.
headers, rows = blocks.schedule_rows(refs, by_layer=True)
output.print_table(rows, headers)blocks.replace_in_attributes#
Pure {handle: {tag: new text}} for every attribute value that changes when
find becomes replace (plain text, never a pattern). Only changed values
are listed; tag limits it to one tag (any case). Raises ValueError when
find is empty. Turn it into apply_changes input with a dict
comprehension.
found = blocks.replace_in_attributes(refs, 'LVL', 'LEVEL', case_sensitive=False)
blocks.apply_changes(dict((h, {'attributes': v}) for h, v in found.items()))blocks.tag_audit#
Pure Audits one attribute tag across references. Returns
{'duplicates': {value: [handles]}, 'blanks': [handles], 'missing': [handles], 'tagged':
count}: values two or more references share, references whose value is empty or spaces,
references without that attribute, and how many have a value. Values are compared trimmed and,
unless ignore_case is False, ignoring case. Raises
ValueError for an empty tag.
report = blocks.tag_audit(blocks.snapshots(blocks.references('TAG-ROOM')), 'ROOM_NO')
for value, handles in report['duplicates'].items():
print(value, len(handles))blocks.column_widths#
Pure The column widths insert_table uses: each column's longest text (at least
3 characters) times text_height times 0.9, plus a margin each side (a third of the
text height unless given). With a title, the columns share any extra width the
title needs. Raises ValueError when text_height is not above zero.
blocks.column_widths(['Block', 'Count'], [['DOOR', 12]], 2.5)blocks.EXPORT_COLUMNS#
Pure The fixed columns at the front of every export_rows sheet, before the
attribute tag columns.
xrefs: external references#
List, reload, unload, detach, bind and repath xrefs. Reload, unload, detach and bind must
run outside any open transaction, as AutoCAD requires, so call them from your script's top
level (a command) or inside doc.lock(), never inside
doc.transaction(). Names are xref names as they appear in the External References
palette, matched as AutoCAD matches block names. The path helpers and the planning and reporting
helpers in the second half are pure and use Windows path rules on every platform.
xrefs.list shares its name with the builtin; call it as
xrefs.list().
xrefs.list#
Needs AutoCAD One dict per xref, nested ones included:
| Key | Value |
|---|---|
name | The xref name. |
path | The saved path, exactly as stored (relative or absolute). |
found_at | Where AutoCAD actually loaded it from; '' when it is not loaded. |
status | 'loaded', 'unloaded', 'not-found', 'unresolved' or 'unreferenced'. |
is_overlay | True for an overlay, False for an attachment. |
nested | True for an xref inside another xref. |
from pyacad import xrefs
broken = [x for x in xrefs.list() if x['status'] == 'not-found']xrefs.reload#
Needs AutoCAD Reloads the named xrefs (one name or a list), or every top-level xref when
names is None, and returns how many it asked AutoCAD to reload.
AutoCAD does not complain when a file still cannot be found, so check the status afterwards
(xrefs.check_loaded does exactly that). Raises ValueError for an
unknown name or a name that is a plain block.
xrefs.reload()
xrefs.reload('Site')xrefs.unload#
Needs AutoCAD Unloads the named xrefs and returns how many. Unloaded xrefs stay attached and keep their path.
xrefs.unload(['Site', 'Survey'])xrefs.detach#
Needs AutoCAD Detaches one xref. Returns None. Pass a single name: given a list it
detaches only the first.
xrefs.detach('Old Survey')xrefs.bind#
Needs AutoCAD Binds the named xrefs into the drawing and returns how many. With
insert=False (Bind) the xref's layers and styles come in with a prefix, so
A-WALL from xref Site becomes Site$0$A-WALL; with
insert=True (Insert) they merge into the drawing's own names.
xrefs.bind('Site', insert=True)xrefs.repath#
Needs AutoCAD Points one xref at new_path and reloads it. With
reload=False only the saved path changes, which keeps an unloaded xref unloaded
until its next reload. Raises ValueError for an unknown name.
xrefs.repath('Site', r'..\Xref\site-v2.dwg')xrefs.find_replace#
Needs AutoCAD Replaces find with replace (plain text, ignoring case) in
every top-level xref path, reloads the ones that changed and returns how many. Nested xrefs are
left alone: they follow the xref that holds them. Raises ValueError when
find is empty.
count = xrefs.find_replace(r'\\oldserver\projects', r'\\newserver\projects')xrefs.to_relative#
Needs AutoCAD Makes every top-level xref path relative to the drawing's folder and returns how many
changed. Paths on another drive or share cannot be relative and are left alone. Raises
RuntimeError when the drawing has never been saved.
xrefs.to_relative()xrefs.to_absolute#
Needs AutoCAD Makes every top-level xref path absolute, resolved against the drawing's folder, and
returns how many changed. Like to_relative it raises RuntimeError for a
drawing that has never been saved.
xrefs.to_absolute()xrefs.status_name#
Pure The xrefs.list status for an AutoCAD XrefStatus name:
'Resolved' is 'loaded', 'FileNotFound' is
'not-found', and so on; anything unknown comes back lower-cased.
xrefs.status_name('FileNotFound') # 'not-found'xrefs.status_label#
Pure A status as people read it: 'not-found' becomes 'not found',
and an empty status 'unknown'.
xrefs.status_label('not-found') # 'not found'xrefs.apply_replace#
Pure path with every occurrence of find replaced, ignoring case.
Raises ValueError when find is empty.
xrefs.apply_replace(r'P:\Old\site.dwg', 'p:\\old', r'Q:\New') # 'Q:\\New\\site.dwg'xrefs.make_relative#
Pure path relative to host_dir, written the way AutoCAD writes
relative xref paths (.\x.dwg, ..\a\x.dwg). Relative paths, paths on
another drive or share, and an empty host_dir come back unchanged.
xrefs.make_relative(r'C:\Job\Xref\site.dwg', r'C:\Job\Sheets') # '..\\Xref\\site.dwg'xrefs.make_absolute#
Pure A relative path resolved against host_dir and normalised; an absolute
path comes back normalised; a relative path with no host_dir comes back
unchanged.
xrefs.make_absolute(r'..\Xref\site.dwg', r'C:\Job\Sheets') # 'C:\\Job\\Xref\\site.dwg'xrefs.describe#
Pure One picker label per xrefs.list() row, in order, such as
'Site [loaded, overlay] ..\Xref\site.dwg'. A row with no path shows
(no path).
rows = xrefs.list()
picked = forms.select_from_list(list(zip(xrefs.describe(rows), [r['name'] for r in rows])),
title='Reload xrefs', multiselect=True)xrefs.plan_repath#
Pure What a repath would do, worked out before anything changes: one
(old_path, new_path, note) per row. mode is
'relative' or 'absolute' (both need host_dir, the
drawing's folder), 'replace' (find becomes replace,
ignoring case) or 'folder' (each xref points at
folder\<its file name>, but only where exists(path) is true;
exists defaults to os.path.isfile, so this mode reads the disk unless
you pass your own). note is '' for a row that changes and says why not
otherwise ('already relative', 'no match',
'not found in the folder', ...), with new_path equal to
old_path. Nested xrefs never change. Raises ValueError for an unknown
mode or a missing argument the mode needs.
rows = xrefs.list()
plan = xrefs.plan_repath(rows, 'folder', folder=r'\\server\job\xref')xrefs.format_plan#
Pure A plan_repath result as preview text: each change as its name with from
and to lines, then every row left alone with its note. limit caps each list, the
rest becoming ...and N more.
if forms.confirm(xrefs.format_plan(rows, plan, limit=20)):
for row, (old, new, note) in zip(rows, plan):
if not note:
xrefs.repath(row['name'], new)xrefs.headline#
Pure One line for a toast from (name, error) pairs, where a falsy error is a
success: 'Reloaded 3 xrefs', or 'Reloaded 2 of 3 xrefs' when some
failed; 'No xrefs to update' for none.
xrefs.headline([('Site', None), ('Survey', 'not found')], 'Reloaded')
# 'Reloaded 1 of 2 xrefs'xrefs.summarize#
Pure The headline as a sentence, then every failure as name: error under
Not done:; limit caps the failure list. Text for
forms.alert.
forms.alert(xrefs.summarize(results, 'Reloaded', limit=15))xrefs.check_loaded#
Pure results with each success whose xref did not end up loaded turned into a
failure such as 'not found at ..\b.dwg'. Pass the xrefs.list() rows
read after the action. Names no longer in the rows (detached, bound) and earlier errors are kept
as they were.
results = []
for name in names:
try:
xrefs.reload(name)
results.append((name, None))
except ValueError as error:
results.append((name, str(error)))
results = xrefs.check_loaded(results, xrefs.list())
toast.info(xrefs.headline(results, 'Reloaded'))layouts: paper space tabs#
Paper space layouts in tab order, the current one, create, copy, rename, delete, reorder,
and the drawing's named page setups. Changes go through AutoCAD's LayoutManager,
which acts on the active drawing. is_model, reorder and
unique_name are pure.
layouts.names#
Needs AutoCAD Paper space layout names in tab order. The Model tab is not included.
from pyacad import layouts
for name in layouts.names():
print(name)layouts.current#
Needs AutoCAD The current layout's name, 'Model' on the model tab.
if layouts.is_model(layouts.current()):
toast.info('You are in model space')layouts.set_current#
Needs AutoCAD Switches to the named layout ('Model', any case, for the model tab).
Raises ValueError for an unknown layout.
layouts.set_current('A1 - Plan')layouts.exists#
Needs AutoCAD True when the drawing has a layout of that name.
layouts.exists('A1 - Plan')layouts.create#
Needs AutoCAD Adds an empty paper space layout and returns its name. Raises
ValueError when it already exists.
layouts.create('A1 - Sections')layouts.delete#
Needs AutoCAD Deletes a paper space layout. Raises ValueError for the model tab or an
unknown layout.
layouts.delete('Layout1')layouts.rename#
Needs AutoCAD Renames a paper space layout; a change of case only is allowed. Raises
ValueError when either name is the model tab, the old one does not exist or the new
one is taken.
layouts.rename('Layout1', 'A1 - Plan')layouts.copy#
Needs AutoCAD Copies a paper space layout, with its content and plot settings, to a new layout and
returns the new name. Raises ValueError when either is the model tab, the new name
is blank or taken, or the source does not exist. Use layouts.unique_name to pick a
free name.
new = layouts.copy('A1 - Plan', layouts.unique_name(layouts.names(), 'A1 - Plan'))layouts.set_order#
Needs AutoCAD Puts the paper space layouts in this tab order. ordered must name every
layout exactly once (any case), as layouts.reorder returns it; otherwise
ValueError lists the layouts.
layouts.set_order(sorted(layouts.names(), key=str.lower))layouts.page_setups#
Needs AutoCAD Names of the drawing's named page setups, sorted ignoring case. With
paper_only=True the ones made for model space, which a layout cannot take, are left
out.
setups = layouts.page_setups(paper_only=True)layouts.apply_page_setup#
Needs AutoCAD Makes a named paper space page setup the plot settings of each named layout and returns
how many. All or nothing: an unknown setup, a model space setup, the model tab or an unknown
layout raises ValueError before any layout changes.
layouts.apply_page_setup('A1 PDF', layouts.names())layouts.is_model#
Pure True for the model tab's name in any case, surrounding spaces
ignored.
layouts.is_model(' MODEL ') # Truelayouts.reorder#
Pure names with name moved to new_index (0 is first),
as a new list. The index is clamped to the list; name matches in any case. Raises
ValueError when it is not in names.
order = layouts.reorder(layouts.names(), 'Cover', 0)
layouts.set_order(order)layouts.unique_name#
Pure wanted, or wanted with ' (2)',
' (3)' ... when a layout in existing (any case) or the model tab
already has that name. A counter wanted already carries is replaced, not stacked.
Raises ValueError for a blank name.
layouts.unique_name(['A1', 'A1 (2)'], 'A1 (2)') # 'A1 (3)'plot: PDF output#
Plot a layout to a PDF (or any file a plot device writes), list plot devices and their paper
sizes, and name the files of a batch. to_pdf plots in the foreground: it sets
BACKGROUNDPLOT to 0 for the duration, puts it back afterwards, and returns once the file is
written. Everything here also runs in Core Console. output_name,
batch_names and scale_type are pure.
plot.to_pdf#
Needs AutoCAD Plots one layout to path and returns path. The settings start
from page_setup (a named page setup) when given, else from the layout's own plot
settings; the device is set to device; paper is a canonical paper
name the device offers (see plot.papers), else the layout's current paper is kept
when the device has it. The plot is always the whole layout, centred, unrotated, at
scale ('fit' or a ratio such as '1:50', see
plot.scale_type). Raises RuntimeError when another plot is running and
ValueError for an unknown layout, page setup, paper or scale. It does not create the
folder.
plot.to_pdf('A1 - Plan', r'C:\Out\A1 - Plan.pdf', paper='ISO_full_bleed_A1_(841.00_x_594.00_MM)')plot.devices#
Needs AutoCAD Names of every plot device AutoCAD knows: its .pc3 files and the Windows printers.
if 'DWG To PDF.pc3' not in plot.devices():
toast.error('DWG To PDF.pc3 is missing')plot.papers#
Needs AutoCAD The canonical paper size names a device offers, the names to_pdf's
paper takes (not the friendly names in the Plot dialog).
a1 = [p for p in plot.papers('DWG To PDF.pc3') if '_A1_' in p]plot.output_name#
Pure pattern with {drawing} (the drawing's file name without folder
or extension) and {layout} filled in. Characters Windows forbids in file names
become _.
plot.output_name('{drawing} - {layout}.pdf', r'C:\Job\GA-100.dwg', 'A1/Plan')
# 'GA-100 - A1_Plan.pdf'plot.batch_names#
Pure (layout, path) for each layout, in order: output_name of
pattern (plot.DEFAULT_PATTERN when None) inside
folder, with .pdf added when missing. Two layouts never get the same
path in any case: a repeat gains ' (2)', ' (3)' ... before the
extension, never a name another layout already plots to.
for layout, target in plot.batch_names(doc.path(), layouts.names(), r'C:\Out'):
plot.to_pdf(layout, target)plot.scale_type#
Pure The AutoCAD StdScaleType member name for 'fit' or a ratio:
'1:1', '1:2', '1:4', '1:5',
'1:8', '1:10', '1:16', '1:20',
'1:30', '1:40', '1:50', '1:100',
'2:1', '4:1', '8:1', '10:1',
'100:1', '1000:1'. Spaces and case are ignored; anything else raises
ValueError.
plot.scale_type('1:50') # 'StdScale1To50'
plot.scale_type('Fit') # 'ScaleToFit'plot.DEFAULT_PATTERN#
Pure The file name pattern batch_names uses when you give none.
audit: drawing health#
Purge, AUDIT, zero-length and duplicate geometry, colours to ByLayer, proxy objects, and
text styles whose font files cannot be found. Scans cover model space and every layout;
proxies and proxy_details also look inside block definitions. The
pure half plans and words the results, and is what the shipped Drawing Cleanup and Font and
Proxy Report tools are built on. audit.purge has a parameter called
all; pass it by keyword.
audit.purge#
Needs AutoCAD Erases unreferenced blocks, layers, linetypes, text styles, dimension styles,
registered applications, multiline styles, table styles and multileader styles, and returns how
many it erased. With all=True it repeats until a pass finds nothing, which catches
blocks nested in blocks; all=False does one pass. One transaction.
from pyacad import audit
removed = audit.purge()audit.audit#
Needs AutoCAD Queues the AUDIT command, answering Yes to fixing errors when fix is true.
Like doc.send_command it runs after your script returns, so nothing comes back;
AUDIT reports on the command line.
audit.audit(fix=True)audit.zero_length#
Needs AutoCAD Handles of LINE and LWPOLYLINE entities with no length, in
model space and every layout.
erased, skipped = audit.erase(audit.zero_length())audit.duplicates#
Needs AutoCAD Groups of handles (two or more each) of entities in the same space with the same type,
layer and geometry, as audit.geometry_key defines it, with numbers compared to
tolerance. Only the types geometry_key knows are checked: lines,
circles, arcs, lightweight polylines, text, mtext and block references.
groups = audit.duplicates()
audit.erase(audit.duplicate_extras(groups)) # keeps the first of each groupaudit.erase#
Needs AutoCAD Erases entities by handle in one transaction, leaving any on a locked layer alone, and
returns (erased, skipped). Handles that no longer resolve are passed over without
counting. This is the forgiving counterpart of ents.delete.
erased, skipped = audit.erase(handles)
toast.success('%d erased' % erased, '%d on locked layers left alone' % skipped)audit.colors_to_bylayer#
Needs AutoCAD Sets the colour of every entity in model space and every layout, and of the attributes
on block references, to ByLayer, and returns (changed, skipped), where skipped is
on locked layers. Viewports and the contents of block definitions are left alone.
changed, skipped = audit.colors_to_bylayer()audit.proxies#
Needs AutoCAD Handles of proxy entities (DXF name starting ACAD_PROXY) in every space
and block definition of the drawing.
if audit.proxies():
toast.warning('This drawing has proxy entities')audit.proxy_details#
Needs AutoCAD Every proxy as a dict: handle, kind ('entity' or
'object'), class (the original class name, '' when the
proxy does not say), application (what made it) and where (the space or
'BLOCK:Name' for an entity, 'Named objects' for an object). Objects are
found in the named object dictionary and the dictionaries nested in it.
for cls, app, count in audit.proxy_summary(audit.proxy_details()):
print(cls, app, count)audit.text_styles_missing_fonts#
Needs AutoCAD Names of the drawing's own text styles whose font file AutoCAD cannot find (the support paths, the drawing's folder, and the Windows fonts for TrueType). Xref styles are skipped.
missing = audit.text_styles_missing_fonts()audit.text_style_fonts#
Needs AutoCAD Every font file the drawing's own text styles name, as (style_name, font_file,
role) rows: role 'font' for a style's font, 'big font' for its
Asian big font, 'shape' for a shape file loaded for complex linetypes. Feed it to
audit.missing_fonts.
missing = audit.missing_fonts(audit.text_style_fonts(), audit.font_finder())audit.font_finder#
Needs AutoCAD A callable that takes a font file name and returns True when AutoCAD can
find it: the available argument of audit.missing_fonts.
found = audit.font_finder()
print(found('romans.shx'))audit.set_text_style_font#
Needs AutoCAD Points a text style at another font file, such as simplex.shx or
arial.ttf, clearing any TrueType typeface so the file is what counts. Text already
drawn shows the new font after a REGEN. Raises ValueError for an empty file name,
an unknown style, or a style that holds a shape file.
for kind, default, styles in audit.remap_plan(missing):
for style in styles:
audit.set_text_style_font(style, default)
doc.send_command('_.REGEN')audit.geometry_key#
Pure A hashable key for an ents.get snapshot: type, layer (ignoring case) and
geometry, with every number snapped to tolerance. A line and its reverse share a
key; a block reference's attribute values are part of its key. Returns None for
types without a rule. Raises ValueError when tolerance is not above
zero.
a = {'type': 'LINE', 'layer': 'A', 'start': (0, 0, 0), 'end': (1, 0, 0)}
b = {'type': 'LINE', 'layer': 'a', 'start': (1, 0, 0), 'end': (0, 0, 0)}
audit.geometry_key(a) == audit.geometry_key(b) # Trueaudit.duplicate_extras#
Pure The handles to delete from duplicates groups: every one but the first of
each group, so one copy always survives.
audit.duplicate_extras([['1A', '1B', '1C'], ['2A', '2B']]) # ['1B', '1C', '2B']audit.step_label#
Pure The checklist label of a cleanup step key from audit.STEPS. Raises
ValueError for an unknown key.
audit.step_label('zero_length') # 'Delete zero-length lines and polylines'audit.cleanup_plan#
Pure The cleanup steps to run, as keys in STEPS order. choices is
a list of keys (exactly those run), a dict of key to true or false (overriding the defaults one
step at a time), or None (the defaults run). defaults is a list of
keys; None means the steps STEPS ticks by default. Unknown and
repeated keys are dropped; case and spaces are ignored.
audit.cleanup_plan({'bylayer': True, 'zoom': False})
# ['purge', 'audit', 'zero_length', 'duplicates', 'bylayer']audit.deleting_steps#
Pure The steps in a plan that erase something (purge, zero-length, duplicates), in run order: the ones to confirm with the user first.
audit.deleting_steps(['zoom', 'purge']) # ['purge']audit.format_report#
Pure The Drawing Cleanup report, one line per step, from a list of (key,
result). A result is a count, None for a step queued to run later (audit,
zoom), or a dict with any of count, skipped, groups,
error, cancelled, not_run. Returns
'No steps ran.' for an empty list.
text = audit.format_report([('purge', 14), ('audit', None),
('duplicates', {'count': 3, 'groups': 2})])
# Purge: 14 unused items removed
# Audit: queued; it runs when this tool finishes and reports on the command line
# Duplicates: 3 extra copies deleted (2 groups)audit.summary#
Pure One line for the toast after a cleanup, such as '14 purged, 3 duplicates
deleted', from the same (key, result) list; 'Nothing needed
cleaning' when nothing changed.
toast.success(audit.summary(steps), audit.format_report(steps))audit.font_kind#
Pure 'ttf' for a TrueType or OpenType file (.ttf, .ttc, .otf), else
'shx'.
audit.font_kind('Arial.TTF') # 'ttf'audit.missing_fonts#
Pure The rows of styles whose font file (column 2) is not available, in order;
extra columns ride along, and rows with no font file are skipped. available is
either a collection of font file names (compared on the bare file name ignoring case, a name
with no extension meaning .shx) or a callable such as audit.font_finder(), asked
once per distinct file.
audit.missing_fonts([('Notes', 'romans.shx'), ('Title', 'corp.ttf')], ['ROMANS'])
# [('Title', 'corp.ttf')]audit.remap_plan#
Pure How to remap missing fonts: [(kind, default, style_names)], SHX first
('simplex.shx') then TrueType ('arial.ttf'). Only a style's own font
is remapped; big fonts and shape files are left out.
audit.remap_plan([('Title', 'corp.ttf', 'font')]) # [('ttf', 'arial.ttf', ['Title'])]audit.proxy_summary#
Pure [(class, application, count)] from proxy_details rows, most
common first, then by class name. A proxy with no class counts as
'(unknown class)'.
rows = audit.proxy_summary(audit.proxy_details())
output.print_table(rows, ['Class', 'Application', 'Count'])audit.format_font_report#
Pure The Font and Proxy Report text: proxies grouped by class, then the missing font rows.
Each list stops after limit lines with ...and N more.
forms.alert(audit.format_font_report(details, missing), 'Font and proxy report')audit.font_report_rows#
Pure Spreadsheet rows under audit.FONT_REPORT_HEADERS: one per proxy, then one
per missing font row.
xl.write(path, audit.font_report_rows(details, missing), headers=audit.FONT_REPORT_HEADERS)audit.STEPS#
Pure The Drawing Cleanup steps as (key, checklist label, ticked by default), in
the order they run: purge, audit, zero_length,
duplicates, bylayer (off by default), zoom.
audit.FONT_REPORT_HEADERS#
Pure The column headers for font_report_rows.
lisp: AutoLISP from Python#
Call a LISP function, queue an expression, and read the arguments a
(pyacad ...) call passed in. call goes through AutoCAD's
acedInvoke, which reaches only functions LISP has exposed: commands defined as
C:NAME and functions registered with (vl-acad-defun 'name). Arbitrary
expressions cannot be evaluated synchronously from .NET, so evaluate queues one on
the command line instead. Python functions cannot be registered as LISP functions; a script is
reachable from LISP as (pyacad "PYNAME" args...), which hands it the arguments
and returns its __result__ (see the Commands page).
encode and decode are pure.
lisp.call#
Needs AutoCAD Calls an exposed LISP function by name with Python arguments (encoded by
lisp.encode) and returns its result as Python values: a single value bare, several
as a list, nil as None, T as True, points as tuples, entity names as
handles. Returns None when the function returns nothing. AutoCAD raises its own
exception when the function is not exposed.
# LISP side, loaded at startup:
# (defun pa-scale (sheet) (if (= sheet "A1") 50 100))
# (vl-acad-defun 'pa-scale)
factor = lisp.call('pa-scale', 'A1') # 50lisp.evaluate#
Needs AutoCAD Queues a LISP expression on the command line (wrapping it in parentheses when you
left them off). It runs after your script returns, so this always returns
None.
lisp.evaluate('(setvar "OSMODE" 4133)')lisp.args#
Needs AutoCAD The arguments of the (pyacad "PYNAME" args...) call, or of the
PYACAD command line, that ran this script, as a list; [] when it got
none. The same as script.get_args() and the __args__ global. From
LISP, strings arrive as str, integers as int, reals as
float, nil as None and T as True; from the
PYACAD command line every argument is a string.
args = lisp.args()
pattern = args[0] if args else '*'lisp.encode#
Pure A Python value as (type code, value) pairs: None and
False are nil, True is T, strings are text, ints are 32-bit integers
(reals when they do not fit), floats are reals, lists and tuples are lists, and a dict is a list
of (key value) pairs. Anything else becomes its str().
lisp.encode(['A', 2])
# [(5016, None), (5005, 'A'), (5010, 2), (5017, None)]lisp.decode#
Pure (type code, value) pairs back into Python values: a list with one item
per top-level LISP value. Lists nest, a dotted pair closes like a list, nil is
None, T is True.
lisp.decode([(5016, None), (5005, 'A'), (5010, 2), (5017, None)]) # [['A', 2]]lisp type codes#
Pure AutoCAD's LISP data type codes (LispDataType), as plain ints, for reading
and writing the pairs above.
units: drawing units#
The INSUNITS names, conversions through metres, and length formatting, with feet and inches
for Inches and Feet drawings. Everything but current is pure. Unit names are
AutoCAD's UnitsValue names ('Millimeters', 'Feet', ...)
in any case, exactly what doc.units() returns.
units.current#
Needs AutoCAD The active drawing's insertion units name; the same as doc.units().
from pyacad import units
name = units.current() # 'Millimeters'units.name_of#
Pure The unit name for an INSUNITS number. Raises ValueError for a number that
is not one.
units.name_of(4) # 'Millimeters'units.to_metres#
Pure value in the named unit, as metres. Raises ValueError for an
unknown name or for 'Undefined', which has no length.
units.to_metres(2500, 'Millimeters') # 2.5units.from_metres#
Pure value in metres, as the named unit. Fails like
to_metres.
units.from_metres(1.0, 'Feet') # 3.2808...units.format_length#
Pure A length for display: feet and inches such as 2'-6.50" for Inches and
Feet drawings, the number and short label such as '12.35 mm' for the others, and
the bare number for Undefined. precision is decimal places.
units.format_length(30.5, 'Inches') # 2'-6.50"
units.format_length(12345.6, 'Millimeters') # '12345.60 mm'
units.format_length(12.5, 'Feet', 0) # 12'-6"units.UNITS#
Pure INSUNITS number to (name, metres per unit, short label) for all 25 values
AutoCAD defines, from 0 (Undefined, no metres) to 24 (USSurveyMile).
forms: dialogs#
Modal dialogs in pyACAD's own style (rounded corners, dark title bar in dark mode, the
Windows accent colour): an alert, a yes or no question, text, number and file prompts, a
searchable list picker, a one-click option picker, a folder picker, a cancellable progress
window, and custom windows loaded from XAML. None of them touch the drawing. Every one
blocks your script until the user answers, and cancelling always returns None,
never raises. Under pyacad run, where nobody can answer, alert
becomes a line in the transcript and the others raise InvalidOperationException
(check script.is_runner() first).
forms.alert#
Needs AutoCAD Blocks Shows a message with an OK button. copy adds a Copy button that puts
that text on the clipboard without closing the dialog. Returns None.
from pyacad import forms
forms.alert('3 xrefs could not be found.', 'Xref check', copy=report_text)forms.confirm#
Needs AutoCAD Blocks A Yes and No question. Returns True on Yes, False on No
or when the window is closed.
if forms.confirm('Delete 14 duplicate entities?'):
audit.erase(extras)forms.ask_string#
Needs AutoCAD Blocks A single-line text prompt. Returns what was typed (possibly an empty string), or
None on cancel.
prefix = forms.ask_string('Freeze layers starting with:', default='Z-')
if prefix:
layers.freeze(prefix + '*')forms.ask_number#
Needs AutoCAD Blocks A number prompt that validates as the user types (and against
min_value and max_value when given). Returns a float, or
None on cancel.
height = forms.ask_number('Text height', default=2.5, min_value=0.1)forms.ask_options#
Needs AutoCAD Blocks One click between a few options, shown as buttons: clicking one is
the answer, with no OK. Returns the option you passed (the object itself, not its text), or
None on Esc. With prompt set to None or
'' the window drops its title bar and question and a click anywhere outside it
cancels, for a quick switch.
scope = forms.ask_options('Export which blocks?', ['Selected', 'Whole drawing'])forms.select_from_list#
Needs AutoCAD Blocks A searchable list picker. items are strings, or (label,
value) pairs where the label is shown and the value returned. With
multiselect the user ticks several and you get a list of values; otherwise you get
one value. checked lists values to open already ticked (or highlighted, single
select). Returns None on cancel.
names = forms.select_from_list(layers.names(), title='Freeze layers', multiselect=True)
if names:
layers.freeze(names)forms.save_file#
Needs AutoCAD Blocks The Windows Save dialog (which asks before overwriting). filter is a
Windows filter string. Returns the chosen path, or None on cancel.
path = forms.save_file('Excel workbook (*.xlsx)|*.xlsx', 'Blocks.xlsx', 'Export blocks')forms.open_file#
Needs AutoCAD Blocks The Windows Open dialog. Returns the chosen path, or None on
cancel.
path = forms.open_file('CSV files (*.csv)|*.csv', 'Choose a layer standard')forms.pick_folder#
Needs AutoCAD Blocks A folder picker, starting at initial when given. Returns the path, or
None on cancel.
folder = forms.pick_folder('Save PDFs to')forms.progress#
Needs AutoCAD A context manager that shows a progress window with a Cancel button and closes it when
the block ends, however it ends. with hands you an object with two methods:
update(fraction, label=None)moves the bar (0.0 to 1.0) and, when given, changes the label. It returnsFalseonce the user has clicked Cancel. Leavelabelout after a cancel so the window's own "Cancelling..." stays.check()lets the window process clicks and raisesforms.Cancelledonce the user has clicked Cancel. A loop that never calls either method cannot be cancelled, because your script holds the UI thread.
handles = list(ents.iter())
with forms.progress('Checking entities') as bar:
for index, handle in enumerate(handles):
bar.check()
bar.update(float(index) / len(handles), handle)forms.Cancelled#
Needs AutoCAD The exception check() raises after a Cancel click. It is an ordinary
Python exception, but it lives in forms, which imports only inside AutoCAD. Catch
it to stop quietly.
try:
with forms.progress('Exporting') as bar:
for index, item in enumerate(items):
bar.check()
work(item)
except forms.Cancelled:
toast.info('Cancelled')forms.WPFWindow#
Needs AutoCAD Blocks A WPF window loaded from a XAML file. A relative xaml_file is looked
up in the running bundle's folder; a missing file raises IOError. The loaded
Window is .window. win['Name'] returns the element with
that x:Name (or raises KeyError), find(name) returns it
or None. show_dialog() applies pyACAD's window chrome, shows the
window modally over AutoCAD and returns its DialogResult (it raises under
pyacad run). close(result) sets DialogResult when
result is given, then closes.
win = forms.WPFWindow('rename.xaml')
win['Ok'].Click += lambda sender, args: win.close(True)
if win.show_dialog():
layers.rename(old, win['NewName'].Text)toast: status notes#
A toast is a small card in the corner of the AutoCAD window: green for success, red for an
error, blue for information, amber for a warning. It never takes focus and dismisses itself
(after 4 seconds, 5 for a warning, 7 for an error), so a tool can say what it did without
opening the output window. This is where one-line statuses go, never print(),
which opens an output window for one line. A toast never raises: if one cannot be shown, the
failure goes to the log. Under pyacad run it becomes a transcript line.
toast.show#
Needs AutoCAD Shows a toast. level is 'success', 'error',
'info' or 'warning' (anything else shows as success);
detail is an optional second, smaller line.
from pyacad import toast
toast.show('warning', 'Layer A-ANNO is locked', 'Unlock it and run this again')toast.success, error, info, warning#
Needs AutoCAD toast.show with the level filled in.
toast.success('Exported 42 blocks', path)
toast.error('Could not write the workbook', 'Is it open in Excel?')banner: a result bar#
Where a toast is a note in the corner, a banner is a readout: a bar across the full width of the bottom of the AutoCAD window, filled in the level colour, with the message in large type. Use it for the one value a tool exists to produce, such as a measurement, and for an instruction the tool is waiting on. There is only ever one banner; a new one replaces the last. It never takes focus, a click dismisses it, and by default it goes after 8 seconds (10 for a warning, 12 for an error).
banner.show#
Needs AutoCAD Shows the banner. level is as for toasts. seconds is how long
it stays: None for the level's own time, 0 to stay until
banner.clear(), another banner, a click or Reload.
from pyacad import banner
banner.show('success', 'Total duct run: 148.20 m', '37 segments on M-DUCT*', seconds=20)banner.success, error, info, warning#
Needs AutoCAD banner.show with the level filled in and the level's own duration.
banner.info('Area: 42.6 m2')banner.prompt#
Needs AutoCAD An instruction the tool is waiting on: an info banner that stays until replaced or
cleared. Pair it with banner.clear() once the user has answered.
banner.prompt('Pick the insertion points', 'Press Enter or Esc to finish')
try:
point = sel.pick_point('Point: ')
finally:
banner.clear()banner.clear#
Needs AutoCAD Removes the banner, if one is showing.
banner.clear()output: the output window#
The output window is where a report goes: HTML, markdown, tables, charts, images and links
that select objects in the drawing. Each command run gets its own window, titled after the
tool, which opens the first time you write to it (a script that writes nothing gets none). When
the WebView2 runtime is missing it falls back to plain text, and under pyacad run
everything goes to the transcript; your script never needs to care. table_html and
format_table are pure, so the module imports anywhere.
output.print_md#
Needs AutoCAD Renders markdown: #, ## and ### headings,
**bold**, *italic*, `code`, fenced code blocks,
- lists, [text](url) links and paragraphs. Everything is escaped
first, so HTML in the text shows as text.
from pyacad import output
output.print_md('## Xref check\n\n- **3** loaded\n- **1** not found')output.print_html#
Needs AutoCAD Renders an HTML fragment as it is. Escape anything that came from the drawing yourself.
output.print_html('<p>Largest: <b>%d</b> objects</p>' % biggest)output.print_table#
Needs AutoCAD A styled table. Note the order: rows first, then headers.
Rows are lists or tuples of any values (each is shown with str()); short rows are
padded. A column whose every filled cell is a number is right-aligned.
output.print_table([['A-WALL', 120], ['A-DOOR', 14]], ['Layer', 'Objects'])output.print_code#
Needs AutoCAD A monospace block, escaped, scrolling sideways rather than wrapping.
output.print_code(xrefs.format_plan(rows, plan))output.print_image#
Needs AutoCAD Embeds a .png, .jpg, .jpeg, .gif or .svg file, with an optional caption. Raises
ValueError for any other extension.
output.print_image(r'C:\Out\preview.png', 'Sheet A1 preview')output.chart_bar#
Needs AutoCAD A bar chart card, drawn as SVG that follows the window's light or dark theme.
labels and values pair up by position.
counts = blocks.counts()
names = sorted(counts, key=counts.get, reverse=True)[:10]
output.chart_bar(names, [counts[n] for n in names], 'Top 10 blocks')output.chart_line#
Needs AutoCAD A line chart. series is {name: [values]}, every list sharing
the labels axis.
output.chart_line(['L1', 'L2', 'L3'], {'Supply': [40, 52, 47], 'Return': [38, 50, 44]},
'Duct length by level (m)')output.chart_pie#
Needs AutoCAD A pie chart card.
output.chart_pie(['Lines', 'Polylines', 'Arcs'], [120, 48, 9], 'Entity mix')output.chart_doughnut#
Needs AutoCAD The pie chart with a hole in the middle.
output.chart_doughnut(['Loaded', 'Not found'], [5, 2], 'Xrefs')output.element_link#
Needs AutoCAD HTML for a link that, when clicked, selects the object in the drawing (it becomes the
pickfirst set, with grips). item is a handle string; the label is escaped by the
window. Use the result inside print_html or a table cell. With no output window
(plain text fallback) you get the label back.
for handle in audit.zero_length():
output.print_html('<div>%s</div>' % output.element_link(handle, 'Zero-length ' + handle))output.progress#
Needs AutoCAD Shows or moves a progress bar inside the output window; fraction runs from
0 to 1, and 1.0 completes it. For a progress window the user can cancel, use
forms.progress.
output.progress(0.5, 'Half way')output.set_title#
Needs AutoCAD Retitles the output window (the default title is pyACAD - plus the tool's
title).
output.set_title('Xref report: ' + doc.path())output.save#
Needs AutoCAD Writes everything printed so far to path as a standalone HTML file.
output.save(r'C:\Out\layer-report.html')output.table_html#
Pure The HTML print_table renders, as a string: escaped cells, numeric columns
marked for right alignment.
html = output.table_html([['A', 1]], ['Layer', 'Count'])output.format_table#
Pure Rows as an aligned plain-text table with a dashed rule under the headers, for a log file, the clipboard or an alert.
text = output.format_table([['A-WALL', 120], ['A-DOOR', 14]], ['Layer', 'Objects'])
script.clipboard_copy(text)script: the running command#
What the running command is (its script, folder, title, arguments), the pyACAD session (toggle state, shared variables, theme, Reload), per-tool settings and data files, a logger, and a few Windows conveniences. It needs only the pyACAD runtime, never the AutoCAD API, so every function works in a hook or a dock pane as well as a button.
The runtime records the command's identity when a button or command starts and keeps
answering with it until the next one starts. A hook, startup.py, a smartbutton's
ribbon-build run and a dock pane's script.py never set it, so inside them
get_command_path, get_config, store_data and friends
answer for whichever button the user clicked last. In those places use
pyacad.settings with a tool key you choose. Code that runs after a button's
script.py returns (a modeless window's handlers) should capture the identity first
with script.bind().
script.get_host#
Needs AutoCAD The runtime's host object, the same object scripts receive as the
__pyacad__ global. Everything else in this module reads from it.
from pyacad import script
host = script.get_host()script.get_script_path#
Needs AutoCAD Full path of the running command's script.py.
print(script.get_script_path())script.get_command_path#
Needs AutoCAD The running command's bundle folder, where its icons and any files you ship beside
script.py live.
import os
template = os.path.join(script.get_command_path(), 'template.xlsx')script.get_bundle_file#
Needs AutoCAD Full path of a file inside the running command's bundle folder, or None
when it does not exist.
csv_path = script.get_bundle_file('standard.csv')
if csv_path is None:
toast.error('standard.csv is missing from the bundle')script.get_title#
Needs AutoCAD The running command's title, as shown on the ribbon.
forms.alert('Nothing to do.', script.get_title())script.get_document_path#
Needs AutoCAD Full path of the active drawing, 'untitled' when it was never saved,
None when no drawing is open. Unlike doc.path() it never raises, so it
is the cheap way to ask "is there a drawing at all?".
if script.get_document_path() is None:
toast.warning('Open a drawing first')script.get_args#
Needs AutoCAD The running script's __args__ as a list: what the PYACAD
command line or a (pyacad "PYNAME" args...) call passed; [] for a
ribbon click. It reads the global from the calling script, so it also works from a helper
module the script imported.
args = script.get_args()script.get_host_version#
Needs AutoCAD pyACAD's product version string, such as '0.1.0'. The build stamps the
runtime from pyacad.__version__, so the two agree.
script.get_logger().info('pyACAD ' + script.get_host_version())script.is_dark_theme#
Needs AutoCAD True when the AutoCAD UI is dark. Use it to theme your own XAML
windows.
background = '#1E1E1E' if script.is_dark_theme() else '#FFFFFF'script.is_runner#
Needs AutoCAD True when the script runs under pyacad run (Core Console, no
ribbon, no windows). Skip prompts when it is; the __runner__ global says the
same.
folder = r'C:\Out' if script.is_runner() else forms.pick_folder('Save PDFs to')script.reload_pyacad#
Needs AutoCAD Rescans the extension folders and rebuilds the pyACAD ribbon, as the Reload button does. No restart needed.
script.reload_pyacad()script.get_toggle_state#
Needs AutoCAD True when this *.toggle bundle is on. The state lives for the
AutoCAD session only and starts off.
on = not script.get_toggle_state()script.set_toggle_state#
Needs AutoCAD Sets this toggle's state and swaps its ribbon icon to icon.on or
icon.off. It only works from the toggle's own script: the state is keyed on the
running command, which a hook or startup.py never sets. A toggle that must survive
a restart should save its value with pyacad.settings (the
cookbook shows how).
script.set_toggle_state(on)script.get_envvar#
Needs AutoCAD A session variable shared by every pyACAD script in this AutoCAD session, or
default when it is unset. Names ignore case. Nothing is written to disk.
last_folder = script.get_envvar('pdf_folder', r'C:\Out')script.set_envvar#
Needs AutoCAD Stores a small value for other scripts this session; None removes
it.
script.set_envvar('pdf_folder', folder)script.get_config#
Needs AutoCAD This tool's saved settings merged over defaults (see
settings.load): keys missing from the file take the default, keys not in
defaults are dropped. The file is
%APPDATA%\pyACAD\settings\cfg_<bundle key>.json, so a tool's
config.py (Shift+Click) and its script.py share it. Leave
_own alone; it is how script.bind() pins the key.
cfg = script.get_config({'prefix': 'Z-', 'regen': True})script.save_config#
Needs AutoCAD Saves this tool's settings, replacing the file, so only the keys you pass survive. Written atomically.
cfg['prefix'] = answer
script.save_config(cfg)script.reset_config#
Needs AutoCAD Deletes this tool's settings file, if there is one, so the defaults apply again.
if forms.confirm('Forget your saved settings?'):
script.reset_config()script.store_data#
Needs AutoCAD Saves a JSON-serialisable value under key in this tool's data file,
%APPDATA%\pyACAD\data\cfg_<bundle key>.json. With
per_document=True the file is per drawing (its name carries the drawing's name and
a hash of its full path); that raises RuntimeError when no drawing is open, and every
untitled drawing shares one file. Other keys in the file are kept, and a file that will not
parse is kept beside it as .corrupt before being replaced.
script.store_data('snapshot', layers.snapshot(), per_document=True)script.load_data#
Needs AutoCAD A value saved with store_data, or default when it is not
there or the file is missing or corrupt.
saved = script.load_data('snapshot', per_document=True)script.data_exists#
Needs AutoCAD True when key is in this tool's data file.
if not script.data_exists('snapshot', per_document=True):
toast.info('No snapshot saved for this drawing yet')script.get_data_file#
Needs AutoCAD The path of this tool's shared data file with the given extension, for when you want to write the file yourself (a CSV log, say). The folder may not exist yet.
log_path = script.get_data_file('csv')script.bind#
Needs AutoCAD Pins this command's identity for code that runs after script.py has
returned, and returns a BoundScript. Call it at the top of the script, while the
identity is still right.
me = script.bind()
def on_closed(sender, args): # fires long after script.py returned
me.save_config({'width': window.Width})script.BoundScript#
Needs AutoCAD What script.bind() returns: the settings and data functions above, fixed
to the tool that was running when it was made. You never construct one yourself.
me = script.bind()
cfg = me.get_config({'folder': ''})script.get_logger#
Needs AutoCAD A logger for this script with debug, info,
success, warning and error methods (each takes one
message) and set_level(level), where level is 'debug',
'info' (the default), 'warning' or 'error'. Lines go to
the pyACAD log labelled with the tool's title (or hook <name> in a hook);
warnings and errors also appear in the output window, but only when one is already open, so
logging never pops up a window. One logger per label, cached.
log = script.get_logger()
log.info('Starting export of %d blocks' % len(refs))
log.warning('3 references were on locked layers')script.clipboard_copy#
Needs AutoCAD Puts text on the Windows clipboard.
script.clipboard_copy('\n'.join(layers.names()))script.clipboard_text#
Needs AutoCAD The clipboard's text, or '' when it holds none or another program has it
locked. Never raises.
handles = script.clipboard_text().split()script.open_url#
Needs AutoCAD Opens a URL in the default browser.
script.open_url('https://help.autodesk.com/view/OARX/2026/ENU/')script.show_in_explorer#
Needs AutoCAD Opens Explorer with the file selected, or at the folder.
script.show_in_explorer(path)settings: per-tool settings files#
One JSON file per tool key under %APPDATA%\pyACAD\settings. Pure standard
library with no AutoCAD and no runtime, so it works in any engine, in hooks and panes, and
outside AutoCAD. script.get_config is built on it with the running tool's key; use
this module directly when you want a key you choose, which is the only reliable choice in a
hook, a dock pane or startup.py, and the way two tools share settings.
settings.load#
Pure The tool's saved values merged over defaults (settings.merge).
A missing, unreadable or non-object file gives a fresh copy of the defaults; it never
raises.
from pyacad import settings
cfg = settings.load('save_log', {'on': False, 'folder': ''})settings.save#
Pure Writes values as the tool's settings file (sorted, indented JSON),
creating the folder when needed. The write is atomic: a crash leaves the old file intact.
values must be JSON-serialisable.
settings.save('save_log', {'on': True, 'folder': r'C:\Logs'})settings.merge#
Pure defaults overlaid with stored: only keys in
defaults survive, so stale keys from an older version of a tool never leak
back.
settings.merge({'a': 1, 'b': 2}, {'b': 3, 'old': 9}) # {'a': 1, 'b': 3}settings.path_for#
Pure The settings file path for a tool key: %APPDATA%\pyACAD\settings\<tool>.json.
print(settings.path_for('save_log'))xl: Excel workbooks#
Reads and writes .xlsx files in pure Python, with no Excel and no third-party package, so it runs the same under IronPython and CPython, inside or outside AutoCAD. It writes a minimal valid workbook (inline strings, a bold header row, approximate column widths) and reads both its own files and workbooks Excel saved, shared strings included. Formulas, styles and charts are not its business.
xl.write#
Pure Writes rows (lists of str, int,
float, bool or None) to the sheet sheet of
the workbook at path, with headers as a bold first row when given.
Creates the folder when needed. If the file already holds a workbook, the named sheet is
replaced in place (or added at the end) and the other sheets are kept, but only as values: they
lose their formatting, a bold header row included, and their ints come back as floats. The file
is written to a temporary file first and moved into place, so a failure leaves the old workbook
intact. A file open in Excel raises IOError or OSError.
from pyacad import xl
xl.write(r'C:\Out\blocks.xlsx', [['DOOR', 12], ['WINDOW', 30]], headers=['Block', 'Count'])xl.read#
Pure The rows of the named sheet, or the first sheet when sheet is
None. Numbers come back as float, booleans as True or
False, everything else as str. A row is as long as its last used
column, with gaps as ''; blank rows come back as [], so row positions
match the sheet. Raises ValueError for a missing sheet.
rows = xl.read(path)
headers, body = rows[0], rows[1:]xl.sheets#
Pure The workbook's sheet names, in order.
if 'Blocks' in xl.sheets(path):
rows = xl.read(path, sheet='Blocks')The private modules#
These turn up in tracebacks. Do not import them.
| Module | What it is |
|---|---|
_api | Loads the AutoCAD .NET assemblies and exposes their namespaces to the other modules. Importing it outside AutoCAD raises the ImportError the drawing functions pass on. |
_atomic | Writes a text file through a temporary file and a rename, so a crash never leaves a half-written settings or data file. |
_charts | Draws the bar, line, pie and doughnut SVGs for output.chart_*, coloured from the output window's theme variables. |
_datafiles | The file naming and JSON storage behind script.store_data. |
_history | The pyACAD console's command history (the up and down arrows). |
_log | The level-filtered logger script.get_logger returns. |
_markdown | The small markdown converter behind output.print_md, and HTML escaping. |
_repl | Runs a line typed in the pyACAD console and echoes a trailing expression's value. |
_util | Tree flattening helpers. |
_xlsx | The OpenXML building and parsing behind xl. |