refactor: excel parse
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
# Copyright (c) 2019-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Optional,
|
||||
Iterable,
|
||||
TYPE_CHECKING,
|
||||
Iterator,
|
||||
)
|
||||
from contextlib import contextmanager
|
||||
from ezdxf.tools.handle import HandleGenerator
|
||||
from ezdxf.lldxf.types import is_valid_handle
|
||||
from ezdxf.entities.dxfentity import DXFEntity
|
||||
from ezdxf.entities.dxfobj import DXFObject
|
||||
from ezdxf.audit import AuditError, Auditor
|
||||
from ezdxf.lldxf.const import DXFInternalEzdxfError
|
||||
from ezdxf.entities import factory
|
||||
from ezdxf.query import EntityQuery
|
||||
from ezdxf.entities.copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
DATABASE_EXCLUDE = {
|
||||
"SECTION",
|
||||
"ENDSEC",
|
||||
"EOF",
|
||||
"TABLE",
|
||||
"ENDTAB",
|
||||
"CLASS",
|
||||
"ACDSRECORD",
|
||||
"ACDSSCHEMA",
|
||||
}
|
||||
|
||||
|
||||
class EntityDB:
|
||||
"""A simple key/entity database.
|
||||
|
||||
Every entity/object, except tables and sections, are represented as
|
||||
DXFEntity or inherited types, these entities are stored in the
|
||||
DXF document database, database-key is the `handle` as string.
|
||||
|
||||
"""
|
||||
|
||||
class Trashcan:
|
||||
"""Store handles to entities which should be deleted later."""
|
||||
|
||||
def __init__(self, db: EntityDB):
|
||||
self._database = db._database
|
||||
self._handles: set[str] = set()
|
||||
|
||||
def add(self, handle: str):
|
||||
"""Put handle into trashcan to delete the entity later, this is
|
||||
required for deleting entities while iterating the database.
|
||||
"""
|
||||
self._handles.add(handle)
|
||||
|
||||
def clear(self):
|
||||
"""Remove handles in trashcan from database and destroy entities if
|
||||
still alive.
|
||||
"""
|
||||
db = self._database
|
||||
for handle in self._handles:
|
||||
entity = db.get(handle)
|
||||
if entity and entity.is_alive:
|
||||
entity.destroy()
|
||||
|
||||
if handle in db:
|
||||
del db[handle]
|
||||
|
||||
self._handles.clear()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._database: dict[str, DXFEntity] = {}
|
||||
# DXF handles of entities to delete later:
|
||||
self.handles = HandleGenerator()
|
||||
self.locked: bool = False # used only for debugging
|
||||
|
||||
def __getitem__(self, handle: str) -> DXFEntity:
|
||||
"""Get entity by `handle`, does not filter destroyed entities nor
|
||||
entities in the trashcan.
|
||||
"""
|
||||
return self._database[handle]
|
||||
|
||||
def __setitem__(self, handle: str, entity: DXFEntity) -> None:
|
||||
"""Set `entity` for `handle`."""
|
||||
assert isinstance(handle, str), type(handle)
|
||||
assert isinstance(entity, DXFEntity), type(entity)
|
||||
assert entity.is_alive, "Can not store destroyed entity."
|
||||
if self.locked:
|
||||
raise DXFInternalEzdxfError("Locked entity database.")
|
||||
|
||||
if handle == "0" or not is_valid_handle(handle):
|
||||
raise ValueError(f"Invalid handle {handle}.")
|
||||
self._database[handle] = entity
|
||||
|
||||
def __delitem__(self, handle: str) -> None:
|
||||
"""Delete entity by `handle`. Removes entity only from database, does
|
||||
not destroy the entity.
|
||||
"""
|
||||
if self.locked:
|
||||
raise DXFInternalEzdxfError("Locked entity database.")
|
||||
del self._database[handle]
|
||||
|
||||
def __contains__(self, handle: str) -> bool:
|
||||
"""``True`` if database contains `handle`."""
|
||||
if handle is None:
|
||||
return False
|
||||
assert isinstance(handle, str), type(handle)
|
||||
return handle in self._database
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Count of database items."""
|
||||
return len(self._database)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
"""Iterable of all handles, does filter destroyed entities but not
|
||||
entities in the trashcan.
|
||||
"""
|
||||
return self.keys() # type: ignore
|
||||
|
||||
def get(self, handle: str) -> Optional[DXFEntity]:
|
||||
"""Returns entity for `handle` or ``None`` if no entry exist, does
|
||||
not filter destroyed entities.
|
||||
"""
|
||||
return self._database.get(handle)
|
||||
|
||||
def next_handle(self) -> str:
|
||||
"""Returns next unique handle."""
|
||||
while True:
|
||||
handle = self.handles.next()
|
||||
if handle not in self._database:
|
||||
return handle
|
||||
|
||||
def keys(self) -> Iterable[str]:
|
||||
"""Iterable of all handles, does filter destroyed entities."""
|
||||
return (handle for handle, entity in self.items())
|
||||
|
||||
def values(self) -> Iterable[DXFEntity]:
|
||||
"""Iterable of all entities, does filter destroyed entities."""
|
||||
return (entity for handle, entity in self.items())
|
||||
|
||||
def items(self) -> Iterable[tuple[str, DXFEntity]]:
|
||||
"""Iterable of all (handle, entities) pairs, does filter destroyed
|
||||
entities.
|
||||
"""
|
||||
return (
|
||||
(handle, entity)
|
||||
for handle, entity in self._database.items()
|
||||
if entity.is_alive
|
||||
)
|
||||
|
||||
def add(self, entity: DXFEntity) -> None:
|
||||
"""Add `entity` to database, assigns a new handle to the `entity`
|
||||
if :attr:`entity.dxf.handle` is ``None``. Adding the same entity
|
||||
multiple times is possible and creates only a single database entry.
|
||||
|
||||
"""
|
||||
if entity.dxftype() in DATABASE_EXCLUDE:
|
||||
if entity.dxf.handle is not None:
|
||||
# Mark existing entity handle as used to avoid
|
||||
# reassigning the same handle again.
|
||||
self[entity.dxf.handle] = entity
|
||||
return
|
||||
handle: str = entity.dxf.handle
|
||||
if handle is None:
|
||||
handle = self.next_handle()
|
||||
entity.update_handle(handle)
|
||||
self[handle] = entity
|
||||
|
||||
# Add sub entities ATTRIB, VERTEX and SEQEND to database:
|
||||
# Add linked MTEXT columns to database:
|
||||
if hasattr(entity, "add_sub_entities_to_entitydb"):
|
||||
entity.add_sub_entities_to_entitydb(self)
|
||||
|
||||
def delete_entity(self, entity: DXFEntity) -> None:
|
||||
"""Remove `entity` from database and destroy the `entity`."""
|
||||
if entity.is_alive:
|
||||
del self[entity.dxf.handle]
|
||||
entity.destroy()
|
||||
|
||||
def discard(self, entity: DXFEntity) -> None:
|
||||
"""Discard `entity` from database without destroying the `entity`."""
|
||||
if entity.is_alive:
|
||||
if hasattr(entity, "process_sub_entities"):
|
||||
entity.process_sub_entities(lambda e: self.discard(e))
|
||||
|
||||
handle = entity.dxf.handle
|
||||
try:
|
||||
del self._database[handle]
|
||||
entity.dxf.handle = None
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def duplicate_entity(self, entity: DXFEntity) -> DXFEntity:
|
||||
"""Duplicates `entity` and its sub entities (VERTEX, ATTRIB, SEQEND)
|
||||
and store them with new handles in the entity database.
|
||||
Graphical entities have to be added to a layout by
|
||||
:meth:`~ezdxf.layouts.BaseLayout.add_entity`. DXF objects will
|
||||
automatically added to the OBJECTS section.
|
||||
|
||||
A new owner handle will be set by adding the duplicated entity to a
|
||||
layout.
|
||||
|
||||
Raises:
|
||||
CopyNotSupported: copying of `entity` is not supported
|
||||
|
||||
"""
|
||||
doc = entity.doc
|
||||
assert doc is not None, "valid DXF document required"
|
||||
new_handle = self.next_handle()
|
||||
new_entity: DXFEntity = entity.copy(copy_strategy=default_copy)
|
||||
new_entity.dxf.handle = new_handle
|
||||
factory.bind(new_entity, doc)
|
||||
if isinstance(new_entity, DXFObject):
|
||||
# add DXF objects automatically to the OBJECTS section
|
||||
doc.objects.add_object(new_entity)
|
||||
return new_entity
|
||||
|
||||
def audit(self, auditor: Auditor):
|
||||
"""Restore database integrity:
|
||||
|
||||
- restore database entries with modified handles (key != entity.dxf.handle)
|
||||
- remove entities with invalid handles
|
||||
- empty trashcan - destroy all entities in the trashcan
|
||||
- removes destroyed database entries (purge)
|
||||
|
||||
"""
|
||||
assert self.locked is False, "Database is locked!"
|
||||
add_entities = []
|
||||
|
||||
with self.trashcan() as trash: # type: ignore
|
||||
for handle, entity in self.items():
|
||||
# Destroyed entities are already filtered!
|
||||
if not is_valid_handle(handle):
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_ENTITY_HANDLE,
|
||||
message=f"Removed entity {entity.dxftype()} with invalid "
|
||||
f'handle "{handle}" from entity database.',
|
||||
)
|
||||
trash.add(handle)
|
||||
if handle != entity.dxf.get("handle"):
|
||||
# database handle != stored entity handle
|
||||
# prevent entity from being destroyed:
|
||||
self._database[handle] = None # type: ignore
|
||||
trash.add(handle)
|
||||
add_entities.append(entity)
|
||||
|
||||
# Remove all destroyed entities from database:
|
||||
self.purge()
|
||||
|
||||
for entity in add_entities:
|
||||
handle = entity.dxf.get("handle")
|
||||
if handle is None:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_ENTITY_HANDLE,
|
||||
message=f"Removed entity {entity.dxftype()} without handle "
|
||||
f"from entity database.",
|
||||
)
|
||||
continue
|
||||
if not is_valid_handle(handle) or handle == "0":
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_ENTITY_HANDLE,
|
||||
message=f"Removed entity {entity.dxftype()} with invalid "
|
||||
f'handle "{handle}" from entity database.',
|
||||
)
|
||||
continue
|
||||
self[handle] = entity
|
||||
|
||||
def new_trashcan(self) -> EntityDB.Trashcan:
|
||||
"""Returns a new trashcan, empty trashcan manually by: :
|
||||
func:`Trashcan.clear()`.
|
||||
"""
|
||||
return EntityDB.Trashcan(self)
|
||||
|
||||
@contextmanager # type: ignore
|
||||
def trashcan(self) -> EntityDB.Trashcan: # type: ignore
|
||||
"""Returns a new trashcan in context manager mode, trashcan will be
|
||||
emptied when leaving context.
|
||||
"""
|
||||
trashcan_ = self.new_trashcan()
|
||||
yield trashcan_
|
||||
# try ... finally is not required, in case of an exception the database
|
||||
# is maybe already in an unreliable state.
|
||||
trashcan_.clear()
|
||||
|
||||
def purge(self) -> None:
|
||||
"""Remove all destroyed entities from database, but does not empty the
|
||||
trashcan.
|
||||
"""
|
||||
# Important: operate on underlying data structure:
|
||||
db = self._database
|
||||
dead_handles = [handle for handle, entity in db.items() if not entity.is_alive]
|
||||
for handle in dead_handles:
|
||||
del db[handle]
|
||||
|
||||
def dxf_types_in_use(self) -> set[str]:
|
||||
return set(entity.dxftype() for entity in self.values())
|
||||
|
||||
def reset_handle(self, entity: DXFEntity, handle: str) -> bool:
|
||||
"""Try to reset the entity handle to a certain value.
|
||||
Returns ``True`` if successful and ``False`` otherwise.
|
||||
|
||||
"""
|
||||
if handle in self._database:
|
||||
return False
|
||||
self.discard(entity)
|
||||
entity.dxf.handle = handle
|
||||
self.add(entity)
|
||||
return True
|
||||
|
||||
def query(self, query: str = "*") -> EntityQuery:
|
||||
"""Entity query over all entities in the DXF document.
|
||||
|
||||
Args:
|
||||
query: query string
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`entity query string` and :ref:`entity queries`
|
||||
|
||||
"""
|
||||
return EntityQuery((e for e in self._database.values() if e.is_alive), query)
|
||||
|
||||
|
||||
class EntitySpace:
|
||||
"""
|
||||
An :class:`EntitySpace` is a collection of :class:`~ezdxf.entities.DXFEntity`
|
||||
objects, that stores only references to :class:`DXFEntity` objects.
|
||||
|
||||
The :class:`~ezdxf.layouts.Modelspace`, any :class:`~ezdxf.layouts.Paperspace`
|
||||
layout and :class:`~ezdxf.layouts.BlockLayout` objects have an
|
||||
:class:`EntitySpace` container to store their entities.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, entities: Optional[Iterable[DXFEntity]] = None):
|
||||
self.entities: list[DXFEntity] = (
|
||||
list(e for e in entities if e.is_alive) if entities else []
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[DXFEntity]:
|
||||
"""Iterable of all entities, filters destroyed entities."""
|
||||
return (e for e in self.entities if e.is_alive)
|
||||
|
||||
def __getitem__(self, index) -> DXFEntity:
|
||||
"""Get entity at index `item`
|
||||
|
||||
:class:`EntitySpace` has a standard Python list like interface,
|
||||
therefore `index` can be any valid list indexing or slicing term, like
|
||||
a single index ``layout[-1]`` to get the last entity, or an index slice
|
||||
``layout[:10]`` to get the first 10 or fewer entities as
|
||||
``list[DXFEntity]``. Does not filter destroyed entities.
|
||||
|
||||
"""
|
||||
return self.entities[index]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Count of entities including destroyed entities."""
|
||||
return len(self.entities)
|
||||
|
||||
def has_handle(self, handle: str) -> bool:
|
||||
"""``True`` if `handle` is present, does filter destroyed entities."""
|
||||
assert isinstance(handle, str), type(handle)
|
||||
return any(e.dxf.handle == handle for e in self)
|
||||
|
||||
def purge(self):
|
||||
"""Remove all destroyed entities from entity space."""
|
||||
self.entities = list(self)
|
||||
|
||||
def add(self, entity: DXFEntity) -> None:
|
||||
"""Add `entity`."""
|
||||
assert isinstance(entity, DXFEntity), type(entity)
|
||||
assert entity.is_alive, "Can not store destroyed entities"
|
||||
self.entities.append(entity)
|
||||
|
||||
def extend(self, entities: Iterable[DXFEntity]) -> None:
|
||||
"""Add multiple `entities`."""
|
||||
for entity in entities:
|
||||
self.add(entity)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export all entities into DXF file by `tagwriter`.
|
||||
|
||||
(internal API)
|
||||
"""
|
||||
for entity in iter(self):
|
||||
entity.export_dxf(tagwriter)
|
||||
|
||||
def remove(self, entity: DXFEntity) -> None:
|
||||
"""Remove `entity`."""
|
||||
self.entities.remove(entity)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Remove all entities."""
|
||||
# Do not destroy entities!
|
||||
self.entities = list()
|
||||
|
||||
def pop(self, index: int = -1) -> DXFEntity:
|
||||
return self.entities.pop(index)
|
||||
|
||||
def insert(self, index: int, entity: DXFEntity) -> None:
|
||||
self.entities.insert(index, entity)
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
db_get = auditor.entitydb.get
|
||||
purge: list[DXFEntity] = []
|
||||
# Check if every entity is the entity that is stored for this handle in the
|
||||
# entity database.
|
||||
for entity in self:
|
||||
handle = entity.dxf.handle
|
||||
if entity is not db_get(handle):
|
||||
# A different entity is stored in the database for this handle,
|
||||
# see issues #604 and #833:
|
||||
# - document has entities without handles (invalid for DXF R13+)
|
||||
# - $HANDSEED is not the next usable handle
|
||||
# - entity gets an already used handle
|
||||
# - entity overwrites existing entity or will be overwritten by an entity
|
||||
# loaded afterwards
|
||||
auditor.fixed_error(
|
||||
AuditError.REMOVED_INVALID_DXF_OBJECT,
|
||||
f"Removed entity {entity} with a conflicting handle and without a "
|
||||
f"database entry.",
|
||||
)
|
||||
purge.append(entity)
|
||||
if not purge:
|
||||
return
|
||||
for entity in purge:
|
||||
self.entities.remove(entity)
|
||||
# These are invalid entities do not call destroy() on them, because
|
||||
# this method relies on well-defined entities!
|
||||
entity._silent_kill()
|
||||
Reference in New Issue
Block a user