Skip to content

Commit

Permalink
add blkrefs.find_unreferenced_blocks() function
Browse files Browse the repository at this point in the history
  • Loading branch information
mozman committed Oct 26, 2024
1 parent 5a848c9 commit 5bf0de8
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 18 deletions.
14 changes: 12 additions & 2 deletions docs/source/blkrefs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ the document content is changed. This is not very efficient, but it is safe.

.. warning::

And even with all this careful approach, it is always possible to destroy a
DXF document by deleting an absolutely necessary block definition.
The DXF reference does not document all uses of blocks. The INSERT entity is
just one explicit use case, but there are also many indirect block references
and the customizability of DXF allows you to store block names and handles in
many places.

There are some rules for storing names and handles and this module checks all of
these known rules, but there is no guarantee that everyone follows these rules.

Therefore, it is still possible to destroy a DXF document by deleting an
absolutely necessary block definition.

Always remember that `ezdxf` is not intended or suitable as a basis for a CAD
application!
Expand All @@ -42,3 +50,5 @@ application!
.. automethod:: by_handle

.. automethod:: by_name

.. autofunction:: find_unreferenced_blocks
9 changes: 5 additions & 4 deletions notes/pages/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
## Version 1.3.5 - dev
- ((65ed4f6c-edc8-4390-880c-c604a3fa5ec0))
- CHANGE: load `ODAFC` path dynamically from options
- CHANGE: load `OpenSCAD` path dynamically from options
- CHANGE: `BlocksSection.delete_block()` will not delete layout blocks in safe mode.
- REMOVE: `_AbstractLayout.rename()`, unused method without implementation nor documentation
- NEW: `ezdxf.blkrefs.find_unreferenced_blocks()` function returns the names of unused block definitions
- NEW: `Auditor` checks if modelspace and active paperspace exists
- CHANGE: load `ODAFC` path dynamically from `ezdxf.options`
- CHANGE: load `OpenSCAD` path dynamically from `ezdxf.options`
- REMOVE: `_AbstractLayout.rename()`, unused method without implementation and documentation
- BUGFIX: `BlocksSection.delete_block()` will not delete layout blocks in safe mode.
- ## Version 1.3.4 - 2024-10-15
id:: 66d2bce8-c48a-461d-8e5b-534fe26d1f5b
- ((65ed4f6c-edc8-4390-880c-c604a3fa5ec0))
Expand Down
33 changes: 31 additions & 2 deletions src/ezdxf/blkrefs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2021-2022, Manfred Moitzi
# Copyright (c) 2021-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional, Iterator
Expand All @@ -12,7 +12,7 @@
from ezdxf.lldxf.tags import Tags
from ezdxf.entities import DXFEntity, BlockRecord

__all__ = ["BlockDefinitionIndex", "BlockReferenceCounter"]
__all__ = ["BlockDefinitionIndex", "BlockReferenceCounter", "find_unreferenced_blocks"]

"""
Where are block references located:
Expand Down Expand Up @@ -207,3 +207,32 @@ def header_section_handles(doc: "Drawing") -> Iterable[str]:
block = doc.blocks.get(blk_name, None)
if block is not None:
yield block.block_record.dxf.handle


def find_unreferenced_blocks(doc: Drawing) -> set[str]:
"""Returns the names of all block definitions without references.
.. warning::
The DXF reference does not document all uses of blocks. The INSERT entity is
just one explicit use case, but there are also many indirect block references
and the customizability of DXF allows you to store block names and handles in
many places.
There are some rules for storing names and handles and this module checks all of
these known rules, but there is no guarantee that everyone follows these rules.
Therefore, it is still possible to destroy a DXF document by deleting an
absolutely necessary block definition.
"""
ref_counter = BlockReferenceCounter(doc)
unreferenced_blocks: set[str] = set()

for block in doc.blocks:
if not block.is_alive or block.is_any_layout:
continue
count = ref_counter.by_name(block.name)
if count == 0:
unreferenced_blocks.add(block.name)
return unreferenced_blocks
38 changes: 28 additions & 10 deletions tests/test_05_tools/test_524_block_reference_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@
import pytest

import ezdxf
from ezdxf.blkrefs import BlockReferenceCounter
from ezdxf.blkrefs import BlockReferenceCounter, find_unreferenced_blocks


def test_non_exiting_handles_return_0():
doc = ezdxf.new()
ref_counter = BlockReferenceCounter(doc)
assert (
ref_counter.by_handle("xyz") == 0
), "not existing handles should return 0"
assert (
ref_counter.by_name("xyz") == 0
), "not existing block name should return 0"
assert ref_counter.by_handle("xyz") == 0, "not existing handles should return 0"
assert ref_counter.by_name("xyz") == 0, "not existing block name should return 0"


def test_access_interface():
Expand Down Expand Up @@ -53,9 +49,7 @@ def test_count_nested_block_references():
msp.add_blockref("First", (0, 0))
ref_counter = BlockReferenceCounter(doc)
assert ref_counter.by_name("First") == 10
assert (
ref_counter.by_name("Second") == 1
), "referenced only once in block First"
assert ref_counter.by_name("Second") == 1, "referenced only once in block First"


def test_count_references_used_in_xdata():
Expand Down Expand Up @@ -164,5 +158,29 @@ def test_count_references_in_mleader_style():
assert ref_counter.by_handle(block_handle) == 1


class TestFindUnreferencedBlocks:
def test_empty_document_has_no_unreferenced_blocks(self):
doc = ezdxf.new()

assert len(find_unreferenced_blocks(doc)) == 0

def test_add_unreferenced_block(self):
doc = ezdxf.new()
doc.blocks.new("DUMMY")

names = find_unreferenced_blocks(doc)
assert len(names) == 1, "one unreferenced block expected"
assert "DUMMY" in names, "name of unreferenced block expected"

def test_add_referenced_block(self):
doc = ezdxf.new()
doc.blocks.new("DUMMY")
msp = doc.modelspace()
msp.add_blockref("DUMMY", (0, 0))

names = find_unreferenced_blocks(doc)
assert len(names) == 0, "no unreferenced block expected"


if __name__ == "__main__":
pytest.main([__file__])

0 comments on commit 5bf0de8

Please sign in to comment.