1# Copyright (c) 2019-2020 Manfred Moitzi
2# License: MIT License
3""" :class:`DXFEntity` is the super class of all DXF entities.
4
5The current entity system uses the features of the latest supported DXF version.
6
7The stored DXF version of the document is used to warn users if they use
8unsupported DXF features of the current DXF version.
9
10The DXF version of the document can be changed at runtime or overridden by
11exporting, but unsupported DXF features are just ignored by exporting.
12
13Ezdxf does no conversion between different DXF versions, this package is
14still not a CAD application.
15
16"""
17from typing import (
18    TYPE_CHECKING, List, Dict, Any, Iterable, Optional, Type, TypeVar, Set,
19    Callable,
20)
21import copy
22import logging
23import uuid
24from ezdxf import options
25from ezdxf.lldxf import const
26from ezdxf.lldxf.tags import Tags
27from ezdxf.lldxf.extendedtags import ExtendedTags
28from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass
29from ezdxf.tools import set_flag_state
30from . import factory
31from .appdata import AppData, Reactors
32from .dxfns import DXFNamespace, SubclassProcessor
33from .xdata import XData, EmbeddedObjects
34from .xdict import ExtensionDict
35
36logger = logging.getLogger('ezdxf')
37
38if TYPE_CHECKING:
39    from ezdxf.eztypes import Auditor, TagWriter, Drawing, DXFAttr
40
41__all__ = ['DXFEntity', 'DXFTagStorage', 'base_class', 'SubclassProcessor']
42
43base_class = DefSubclass(None, {
44    'handle': DXFAttr(5),
45
46    # owner: Soft-pointer ID/handle to owner BLOCK_RECORD object
47    # This tag is not supported by DXF R12, but is used intern to unify entity
48    # handling between DXF R12 and DXF R2000+
49    # Do not write this tag into DXF R12 files!
50    'owner': DXFAttr(330),
51
52    # Application defined data can only appear here:
53    # 102, {APPID ... multiple entries possible DXF R12?
54    # 102, {ACAD_REACTORS ... one entry DXF R2000+, optional
55    # 102, {ACAD_XDICTIONARY  ... one entry DXF R2000+, optional
56})
57
58T = TypeVar('T', bound='DXFEntity')
59
60
61class DXFEntity:
62    """ Common super class for all DXF entities. """
63    DXFTYPE = 'DXFENTITY'  # storing as class var needs less memory
64    DXFATTRIBS = DXFAttributes(base_class)  # DXF attribute definitions
65
66    # Default DXF attributes are set at instantiating a new object, the the
67    # difference to attribute default values is, that this attributes are
68    # really set, this means there is an real object in the dxf namespace
69    # defined, where default attribute values get returned on access without
70    # an existing object in the dxf namespace.
71    DEFAULT_ATTRIBS: Dict = {}
72    MIN_DXF_VERSION_FOR_EXPORT = const.DXF12
73
74    def __init__(self):
75        """ Default constructor. (internal API)"""
76        # Public attributes for package users
77        self.doc: Optional[Drawing] = None
78        self.dxf: DXFNamespace = DXFNamespace(entity=self)
79
80        # None public attributes for package users
81        # create extended data only if needed:
82        self.appdata: Optional[AppData] = None
83        self.reactors: Optional[Reactors] = None
84        self.extension_dict: Optional[ExtensionDict] = None
85        self.xdata: Optional[XData] = None
86        # TODO: remove embedded_objects - no need to waste memory for every entity,
87        #  this is a seldom used feature (ATTRIB, ATTDEF), and this entities have to
88        #  manage the embedded objects by itself at loading stage and DXF export.
89        #  Removing is possible if ATTRIB and ATTDEF have explicit
90        #  support for embedded MTEXT objects
91        self.embedded_objects: Optional[EmbeddedObjects] = None
92        self.proxy_graphic: Optional[bytes] = None
93        # self._uuid  # uuid generated at first request
94
95    @property
96    def uuid(self) -> uuid.UUID:
97        """ Returns an UUID, which allows to distinguish even
98        virtual entities without a handle.
99
100        This UUID will be created at the first request.
101
102        """
103        uuid_ = getattr(self, '_uuid', None)
104        if uuid_ is None:
105            uuid_ = uuid.uuid4()
106            self._uuid = uuid_
107        return uuid_
108
109    @classmethod
110    def new(cls: Type[T], handle: str = None, owner: str = None,
111            dxfattribs: Dict = None, doc: 'Drawing' = None) -> T:
112        """ Constructor for building new entities from scratch by ezdxf.
113
114        NEW process:
115
116        This is a trusted environment where everything is under control of
117        ezdxf respectively the package-user, it is okay to raise exception
118        to show implementation errors in ezdxf or usage errors of the
119        package-user.
120
121        The :attr:`Drawing.is_loading` flag can be checked to distinguish the
122        NEW and the LOAD process.
123
124        Args:
125            handle: unique DXF entity handle or None
126            owner: owner handle if entity has an owner else None or '0'
127            dxfattribs: DXF attributes
128            doc: DXF document
129
130        (internal API)
131        """
132        entity = cls()
133        entity.doc = doc
134        entity.dxf.handle = handle
135        entity.dxf.owner = owner
136        attribs = dict(cls.DEFAULT_ATTRIBS)
137        attribs.update(dxfattribs or {})
138        entity.update_dxf_attribs(attribs)
139        # Only this method triggers the post_new_hook()
140        entity.post_new_hook()
141        return entity
142
143    def post_new_hook(self):
144        """ Post processing and integrity validation after entity creation.
145
146        Called only if created by ezdxf (see :meth:`DXFEntity.new`),
147        not if loaded from an external source.
148
149        (internal API)
150        """
151        pass
152
153    def post_bind_hook(self):
154        """ Post processing and integrity validation after binding entity to a
155        DXF Document. This method is triggered by the :func:`factory.bind`
156        function only when the entity was created by ezdxf.
157
158        If the entity was loaded in the 1st loading stage, the
159        :func:`factory.load` functions also calls the :func:`factory.bind`
160        to bind entities to the loaded document, but not all entities are
161        loaded at this time. To avoid problems this method will not be called
162        when loading content from DXF file, but :meth:`post_load_hook` will be
163        triggered for loaded entities at a later and safer point in time.
164
165        (internal API)
166        """
167        pass
168
169    @classmethod
170    def load(cls: Type[T], tags: ExtendedTags, doc: 'Drawing' = None) -> T:
171        """ Constructor to generate entities loaded from an external source.
172
173        LOAD process:
174
175        This is an untrusted environment where valid structure are not
176        guaranteed and errors should be fixed, because the package-user is not
177        responsible for the problems and also can't fix them, raising
178        exceptions should only be done for unrecoverable issues.
179        Log fixes for debugging!
180
181            Be more like BricsCAD and not as mean as AutoCAD!
182
183        The :attr:`Drawing.is_loading` flag can be checked to distinguish the
184        NEW and the LOAD process.
185
186        Args:
187            tags: DXF tags as :class:`ExtendedTags`
188            doc: DXF Document
189
190        (internal API)
191        """
192        # This method does not trigger the post_new_hook()
193        entity = cls()
194        entity.doc = doc
195        dxfversion = doc.dxfversion if doc else None
196        entity.load_tags(tags, dxfversion=dxfversion)
197        return entity
198
199    def load_tags(self, tags: ExtendedTags, dxfversion: str = None) -> None:
200        """ Generic tag loading interface, called if DXF document is loaded
201        from external sources.
202
203        1. Loading stage which set the basic DXF attributes, additional
204           resources (DXF objects) are not loaded yet. References to these
205           resources have to be stored as handles and can be resolved in the
206        2. Loading stage: :meth:`post_load_hook`.
207
208        (internal API)
209        """
210        if tags:
211            if len(tags.appdata):
212                self.setup_app_data(tags.appdata)
213            if len(tags.xdata):
214                self.xdata = XData(tags.xdata)
215            if tags.embedded_objects:  # TODO: remove
216                self.embedded_objects = EmbeddedObjects(
217                    tags.embedded_objects)
218            processor = SubclassProcessor(tags, dxfversion=dxfversion)
219            self.dxf = self.load_dxf_attribs(processor)
220
221    def load_dxf_attribs(
222            self, processor: SubclassProcessor = None) -> DXFNamespace:
223        """ Load DXF attributes into DXF namespace. """
224        return DXFNamespace(processor, self)
225
226    def post_load_hook(self, doc: 'Drawing') -> Optional[Callable]:
227        """ The 2nd loading stage when loading DXF documents from an external
228        source, for the 1st loading stage see :meth:`load_tags`.
229
230        This stage is meant to convert resource handles into :class:`DXFEntity`
231        objects. This is an untrusted environment where valid structure are not
232        guaranteed, raise exceptions only for unrecoverable structure errors
233        and fix everything else. Log fixes for debugging!
234
235        Some fixes can not be applied at this stage, because some structures
236        like the OBJECTS section are not initialized, in this case return a
237        callable, which will be executed after the DXF document is fully
238        initialized, for an example see :class:`Image`.
239
240        Triggered in method: :meth:`Drawing._2nd_loading_stage`
241
242        Examples for two stage loading:
243        Image, Underlay, DXFGroup, Dictionary, Dimstyle, MText
244
245        """
246        if self.extension_dict is not None:
247            self.extension_dict.load_resources(doc)
248        return None
249
250    @classmethod
251    def from_text(cls: Type[T], text: str, doc: 'Drawing' = None) -> T:
252        """ Load constructor from text for testing. (internal API)"""
253        return cls.load(ExtendedTags.from_text(text), doc)
254
255    @classmethod
256    def shallow_copy(cls: Type[T], other: 'DXFEntity') -> T:
257        """ Copy constructor for type casting e.g. Polyface and Polymesh.
258        (internal API)
259        """
260        entity = cls()
261        entity.doc = other.doc
262        entity.dxf = other.dxf
263        entity.extension_dict = other.extension_dict
264        entity.reactors = other.reactors
265        entity.appdata = other.appdata
266        entity.xdata = other.xdata
267        entity.embedded_objects = other.embedded_objects  # todo: remove
268        entity.proxy_graphic = other.proxy_graphic
269        entity.dxf.rewire(entity)
270        return entity
271
272    def copy(self: T) -> T:
273        """ Returns a copy of `self` but without handle, owner and reactors.
274        This copy is NOT stored in the entity database and does NOT reside
275        in any layout, block, table or objects section! Extension dictionary
276        and reactors are not copied.
277
278        Don't use this function to duplicate DXF entities in drawing,
279        use :meth:`EntityDB.duplicate_entity` instead for this task.
280
281        Copying is not trivial, because of linked resources and the lack of
282        documentation how to handle this linked resources: extension dictionary,
283        handles in appdata, xdata or embedded objects.
284
285        (internal API)
286        """
287        entity = self.__class__()
288        entity.doc = self.doc
289        # copy and bind dxf namespace to new entity
290        entity.dxf = self.dxf.copy(entity)
291        entity.dxf.reset_handles()
292
293        # Do not copy extension dict: if the extension dict should be copied
294        # in the future - a deep copy is maybe required!
295        entity.extension_dict = None
296        # Do not copy reactors:
297        entity.reactors = None
298
299        entity.proxy_graphic = self.proxy_graphic  # immutable bytes
300
301        # if appdata contains handles, they are treated as shared resources
302        entity.appdata = copy.deepcopy(self.appdata)
303
304        # if xdata contains handles, they are treated as shared resources
305        entity.xdata = copy.deepcopy(self.xdata)
306
307        # if embedded objects contains handles, they are treated as shared resources
308        entity.embedded_objects = copy.deepcopy(self.embedded_objects)  # todo: remove
309        self._copy_data(entity)
310        return entity
311
312    def _copy_data(self, entity: 'DXFEntity') -> None:
313        """ Copy entity data like vertices or attribs and store the copies into
314        the entity database.
315        (internal API)
316        """
317        pass
318
319    def __deepcopy__(self, memodict: Dict = None):
320        """ Some entities maybe linked by more than one entity, to be safe use
321        `memodict` for bookkeeping.
322        (internal API)
323        """
324        memodict = memodict or {}
325        try:
326            return memodict[id(self)]
327        except KeyError:
328            copy = self.copy()
329            memodict[id(self)] = copy
330            return copy
331
332    def update_dxf_attribs(self, dxfattribs: Dict) -> None:
333        """ Set DXF attributes by a ``dict`` like :code:`{'layer': 'test',
334        'color': 4}`.
335        """
336        setter = self.dxf.set
337        for key, value in dxfattribs.items():
338            setter(key, value)
339
340    def setup_app_data(self, appdata: List[Tags]) -> None:
341        """ Setup data structures from APP data. (internal API) """
342        for data in appdata:
343            code, appid = data[0]
344            if appid == const.ACAD_REACTORS:
345                self.reactors = Reactors.from_tags(data)
346            elif appid == const.ACAD_XDICTIONARY:
347                self.extension_dict = ExtensionDict.from_tags(data)
348            else:
349                self.set_app_data(appid, data)
350
351    def update_handle(self, handle: str) -> None:
352        """ Update entity handle. (internal API) """
353        self.dxf.handle = handle
354        if self.extension_dict:
355            self.extension_dict.update_owner(handle)
356
357    @property
358    def is_alive(self):
359        """ Returns ``False`` if entity has been deleted. """
360        return hasattr(self, 'dxf')
361
362    @property
363    def is_virtual(self):
364        """ Returns ``True`` if entity is a virtual entity. """
365        return self.doc is None or self.dxf.handle is None
366
367    @property
368    def is_bound(self):
369        """ Returns ``True`` if entity is bound to DXF document. """
370        if self.is_alive and not self.is_virtual:
371            return factory.is_bound(self, self.doc)
372        return False
373
374    def get_dxf_attrib(self, key: str, default: Any = None) -> Any:
375        """ Get DXF attribute `key`, returns `default` if key doesn't exist, or
376        raise :class:`DXFValueError` if `default` is :class:`DXFValueError`
377        and no DXF default value is defined::
378
379            layer = entity.get_dxf_attrib("layer")
380            # same as
381            layer = entity.dxf.layer
382
383        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
384        attribute.
385
386        """
387        return self.dxf.get(key, default)
388
389    def set_dxf_attrib(self, key: str, value: Any) -> None:
390        """ Set new `value` for DXF attribute `key`::
391
392           entity.set_dxf_attrib("layer", "MyLayer")
393           # same as
394           entity.dxf.layer = "MyLayer"
395
396        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
397        attribute.
398
399        """
400        self.dxf.set(key, value)
401
402    def del_dxf_attrib(self, key: str) -> None:
403        """ Delete DXF attribute `key`, does not raise an error if attribute is
404        supported but not present.
405
406        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
407        attribute.
408
409        """
410        self.dxf.discard(key)
411
412    def has_dxf_attrib(self, key: str) -> bool:
413        """ Returns ``True`` if DXF attribute `key` really exist.
414
415        Raises :class:`DXFAttributeError` if `key` is not an supported DXF
416        attribute.
417
418        """
419        return self.dxf.hasattr(key)
420
421    dxf_attrib_exists = has_dxf_attrib
422
423    def is_supported_dxf_attrib(self, key: str) -> bool:
424        """ Returns ``True`` if DXF attrib `key` is supported by this entity.
425        Does not grant that attribute `key` really exist.
426
427        """
428        if key in self.DXFATTRIBS:
429            if self.doc:
430                return self.doc.dxfversion >= self.DXFATTRIBS.get(
431                    key).dxfversion
432            else:
433                return True
434        else:
435            return False
436
437    def dxftype(self) -> str:
438        """ Get DXF type as string, like ``LINE`` for the line entity. """
439        return self.DXFTYPE
440
441    def __str__(self) -> str:
442        """ Returns a simple string representation. """
443        return "{}(#{})".format(self.dxftype(), self.dxf.handle)
444
445    def __repr__(self) -> str:
446        """ Returns a simple string representation including the class. """
447        return str(self.__class__) + " " + str(self)
448
449    def dxfattribs(self, drop: Set[str] = None) -> Dict:
450        """ Returns a ``dict`` with all existing DXF attributes and their
451        values and exclude all DXF attributes listed in set `drop`.
452
453        """
454        all_attribs = self.dxf.all_existing_dxf_attribs()
455        if drop:
456            return {k: v for k, v in all_attribs.items() if k not in drop}
457        else:
458            return all_attribs
459
460    def set_flag_state(self, flag: int, state: bool = True,
461                       name: str = 'flags') -> None:
462        """ Set binary coded `flag` of DXF attribute `name` to ``1`` (on)
463        if `state` is ``True``, set `flag` to ``0`` (off)
464        if `state` is ``False``.
465        """
466        flags = self.dxf.get(name, 0)
467        self.dxf.set(name, set_flag_state(flags, flag, state=state))
468
469    def get_flag_state(self, flag: int, name: str = 'flags') -> bool:
470        """ Returns ``True`` if any `flag` of DXF attribute is ``1`` (on), else
471        ``False``. Always check only one flag state at the time.
472        """
473        return bool(self.dxf.get(name, 0) & flag)
474
475    def remove_dependencies(self, other: 'Drawing' = None):
476        """ Remove all dependencies from current document.
477
478        Intended usage is to remove dependencies from the current document to
479        move or copy the entity to `other` DXF document.
480
481        An error free call of this method does NOT guarantee that this entity
482        can be moved/copied to the `other` document, some entities like
483        DIMENSION have too much dependencies to a document to move or copy
484        them, but to check this is not the domain of this method!
485
486        (internal API)
487        """
488        if self.is_alive:
489            self.dxf.owner = None
490            self.dxf.handle = None
491            self.reactors = None
492            self.extension_dict = None
493            self.appdata = None
494            self.xdata = None
495            self.embedded_objects = None  # todo: remove
496
497    def destroy(self) -> None:
498        """ Delete all data and references. Does not delete entity from
499        structures like layouts or groups.
500
501        Starting with `ezdxf` v0.14 this method could be used to delete
502        entities.
503
504        (internal API)
505
506        """
507        if not self.is_alive:
508            return
509
510        if self.extension_dict is not None:
511            self.extension_dict.destroy()
512            del self.extension_dict
513        del self.appdata
514        del self.reactors
515        del self.xdata
516        del self.embedded_objects  # todo: remove
517        del self.doc
518        del self.dxf  # check mark for is_alive
519
520    def preprocess_export(self, tagwriter: 'TagWriter') -> bool:
521        """ Pre requirement check and pre processing for export.
522
523        Returns False if entity should not be exported at all.
524
525        (internal API)
526        """
527        return True
528
529    def export_dxf(self, tagwriter: 'TagWriter') -> None:
530        """ Export DXF entity by `tagwriter`.
531
532        This is the first key method for exporting DXF entities:
533
534            - has to know the group codes for each attribute
535            - has to add subclass tags in correct order
536            - has to integrate extended data: ExtensionDict, Reactors, AppData
537            - has to maintain the correct tag order (because sometimes order matters)
538
539        (internal API)
540
541        """
542        if tagwriter.dxfversion < self.MIN_DXF_VERSION_FOR_EXPORT:
543            return
544        if not self.preprocess_export(tagwriter):
545            return
546        # ! first step !
547        # write handle, AppData, Reactors, ExtensionDict, owner
548        self.export_base_class(tagwriter)
549
550        # this is the entity specific part
551        self.export_entity(tagwriter)
552
553        # ! Last step !
554        # write xdata, embedded objects
555        self.export_embedded_objects(tagwriter)
556        self.export_xdata(tagwriter)
557
558    def export_base_class(self, tagwriter: 'TagWriter') -> None:
559        """ Export base class DXF attributes and structures. (internal API) """
560        dxftype = self.DXFTYPE
561        _handle_code = 105 if dxftype == 'DIMSTYLE' else 5
562        # 1. tag: (0, DXFTYPE)
563        tagwriter.write_tag2(const.STRUCTURE_MARKER, dxftype)
564
565        if tagwriter.dxfversion >= const.DXF2000:
566            tagwriter.write_tag2(_handle_code, self.dxf.handle)
567            if self.appdata:
568                self.appdata.export_dxf(tagwriter)
569            if self.has_extension_dict:
570                self.extension_dict.export_dxf(tagwriter)
571            if self.reactors:
572                self.reactors.export_dxf(tagwriter)
573            tagwriter.write_tag2(const.OWNER_CODE, self.dxf.owner)
574        else:  # DXF R12
575            if tagwriter.write_handles:
576                tagwriter.write_tag2(_handle_code, self.dxf.handle)
577                # do not write owner handle - not supported by DXF R12
578
579    def export_entity(self, tagwriter: 'TagWriter') -> None:
580        """ Export DXF entity specific data by `tagwriter`.
581
582        This is the second key method for exporting DXF entities:
583
584            - has to know the group codes for each attribute
585            - has to add subclass tags in correct order
586            - has to maintain the correct tag order (because sometimes order matters)
587
588        (internal API)
589        """
590        # base class (handle, appid, reactors, xdict, owner) export is done by parent class
591        pass
592        # xdata and embedded objects  export is also done by parent
593
594    def export_xdata(self, tagwriter: 'TagWriter') -> None:
595        """ Export DXF XDATA by `tagwriter`. (internal API)"""
596        if self.xdata:
597            self.xdata.export_dxf(tagwriter)
598
599    def export_embedded_objects(self, tagwriter: 'TagWriter') -> None:
600        """ Export embedded objects by `tagwriter`. (internal API)"""
601        if self.embedded_objects:  # todo: remove
602            self.embedded_objects.export_dxf(tagwriter)  # todo: remove
603
604    def audit(self, auditor: 'Auditor') -> None:
605        """ Validity check. (internal API) """
606        # Important: do not check owner handle! -> DXFGraphic(), DXFObject()
607        # check app data
608        # check reactors
609        # check extension dict
610        # check XDATA
611
612    @property
613    def has_extension_dict(self) -> bool:
614        """ Returns ``True`` if entity has an attached
615        :class:`~ezdxf.entities.xdict.ExtensionDict`.
616        """
617        xdict = self.extension_dict
618        # Don't use None check: bool(xdict) for an empty extension dict is False
619        if xdict is not None and xdict.is_alive:
620            # Check the associated Dictionary object
621            dictionary = xdict.dictionary
622            if isinstance(dictionary, str):
623                # just a handle string - SUT
624                return True
625            else:
626                return dictionary.is_alive
627        return False
628
629    def get_extension_dict(self) -> 'ExtensionDict':
630        """ Returns the existing :class:`~ezdxf.entities.xdict.ExtensionDict`.
631
632        Raises:
633            AttributeError: extension dict does not exist
634
635        """
636        if self.has_extension_dict:
637            return self.extension_dict
638        else:
639            raise AttributeError('Entity has no extension dictionary.')
640
641    def new_extension_dict(self) -> 'ExtensionDict':
642        self.extension_dict = ExtensionDict.new(self.dxf.handle, self.doc)
643        return self.extension_dict
644
645    def has_app_data(self, appid: str) -> bool:
646        """ Returns ``True`` if application defined data for `appid` exist. """
647        if self.appdata:
648            return appid in self.appdata
649        else:
650            return False
651
652    def get_app_data(self, appid: str) -> Tags:
653        """ Returns application defined data for `appid`.
654
655        Args:
656            appid: application name as defined in the APPID table.
657
658        Raises:
659            DXFValueError: no data for `appid` found
660
661        """
662        if self.appdata:
663            return self.appdata.get(appid)[1:-1]
664        else:
665            raise const.DXFValueError(appid)
666
667    def set_app_data(self, appid: str, tags: Iterable) -> None:
668        """ Set application defined data for `appid` as iterable of tags.
669
670        Args:
671             appid: application name as defined in the APPID table.
672             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`
673
674        """
675        if self.appdata is None:
676            self.appdata = AppData()
677        self.appdata.add(appid, tags)
678
679    def discard_app_data(self, appid: str):
680        """ Discard application defined data for `appid`. Does not raise an
681        exception if no data for `appid` exist.
682        """
683        if self.appdata:
684            self.appdata.discard(appid)
685
686    def has_xdata(self, appid: str) -> bool:
687        """ Returns ``True`` if extended data for `appid` exist. """
688        if self.xdata:
689            return appid in self.xdata
690        else:
691            return False
692
693    def get_xdata(self, appid: str) -> Tags:
694        """ Returns extended data for `appid`.
695
696        Args:
697            appid: application name as defined in the APPID table.
698
699        Raises:
700            DXFValueError: no extended data for `appid` found
701
702        """
703        if self.xdata:
704            return Tags(self.xdata.get(appid)[1:])
705        else:
706            raise const.DXFValueError(appid)
707
708    def set_xdata(self, appid: str, tags: Iterable) -> None:
709        """ Set extended data for `appid` as iterable of tags.
710
711        Args:
712             appid: application name as defined in the APPID table.
713             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`
714
715        """
716        if self.xdata is None:
717            self.xdata = XData()
718        self.xdata.add(appid, tags)
719
720    def discard_xdata(self, appid: str) -> None:
721        """ Discard extended data for `appid`. Does not raise an exception if
722        no extended data for `appid` exist.
723        """
724        if self.xdata:
725            self.xdata.discard(appid)
726
727    def has_xdata_list(self, appid: str, name: str) -> bool:
728        """ Returns ``True`` if a tag list `name` for extended data `appid`
729        exist.
730        """
731        if self.has_xdata(appid):
732            return self.xdata.has_xlist(appid, name)
733        else:
734            return False
735
736    def get_xdata_list(self, appid: str, name: str) -> Tags:
737        """ Returns tag list `name` for extended data `appid`.
738
739        Args:
740            appid: application name as defined in the APPID table.
741            name: extended data list name
742
743        Raises:
744            DXFValueError: no extended data for `appid` found or no data list `name` not found
745
746        """
747        if self.xdata:
748            return Tags(self.xdata.get_xlist(appid, name))
749        else:
750            raise const.DXFValueError(appid)
751
752    def set_xdata_list(self, appid: str, name: str, tags: Iterable) -> None:
753        """ Set tag list `name` for extended data `appid` as iterable of tags.
754
755        Args:
756             appid: application name as defined in the APPID table.
757             name: extended data list name
758             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`
759
760        """
761        if self.xdata is None:
762            self.xdata = XData()
763        self.xdata.set_xlist(appid, name, tags)
764
765    def discard_xdata_list(self, appid: str, name: str) -> None:
766        """ Discard tag list `name` for extended data `appid`. Does not raise
767        an exception if no extended data for `appid` or no tag list `name`
768        exist.
769        """
770        if self.xdata:
771            self.xdata.discard_xlist(appid, name)
772
773    def replace_xdata_list(self, appid: str, name: str, tags: Iterable) -> None:
774        """
775        Replaces tag list `name` for existing extended data `appid` by `tags`.
776        Appends new list if tag list `name` do not exist, but raises
777        :class:`DXFValueError` if extended data `appid` do not exist.
778
779        Args:
780             appid: application name as defined in the APPID table.
781             name: extended data list name
782             tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag`
783
784        Raises:
785            DXFValueError: no extended data for `appid` found
786
787        """
788        self.xdata.replace_xlist(appid, name, tags)
789
790    def has_reactors(self) -> bool:
791        """ Returns ``True`` if entity has reactors. """
792        return bool(self.reactors)
793
794    def get_reactors(self) -> List[str]:
795        """ Returns associated reactors as list of handles. """
796        return self.reactors.get() if self.reactors else []
797
798    def set_reactors(self, handles: Iterable[str]) -> None:
799        """ Set reactors as list of handles. """
800        if self.reactors is None:
801            self.reactors = Reactors()
802        self.reactors.set(handles)
803
804    def append_reactor_handle(self, handle: str) -> None:
805        """ Append `handle` to reactors. """
806        if self.reactors is None:
807            self.reactors = Reactors()
808        self.reactors.add(handle)
809
810    def discard_reactor_handle(self, handle: str) -> None:
811        """ Discard `handle` from reactors. Does not raise an exception if
812        `handle` does not exist.
813        """
814        if self.reactors:
815            self.reactors.discard(handle)
816
817
818@factory.set_default_class
819class DXFTagStorage(DXFEntity):
820    """ Just store all the tags as they are. (internal class) """
821
822    def __init__(self):
823        """ Default constructor """
824        super().__init__()
825        self.xtags: Optional[ExtendedTags] = None
826
827    def copy(self) -> 'DXFEntity':
828        raise const.DXFTypeError(
829            f'Cloning of tag storage {self.dxftype()} not supported.'
830        )
831
832    @property
833    def base_class(self):
834        return self.xtags.subclasses[0]
835
836    @classmethod
837    def load(cls, tags: ExtendedTags, doc: 'Drawing' = None) -> 'DXFTagStorage':
838        assert isinstance(tags, ExtendedTags)
839        entity = cls.new(doc=doc)
840        dxfversion = doc.dxfversion if doc else None
841        entity.load_tags(tags, dxfversion=dxfversion)
842        entity.store_tags(tags)
843        if options.load_proxy_graphics:
844            entity.load_proxy_graphic()
845        return entity
846
847    def load_proxy_graphic(self) -> Optional[bytes]:
848        try:
849            tags = self.xtags.get_subclass('AcDbEntity')
850        except const.DXFKeyError:
851            return
852        binary_data = [tag.value for tag in tags.find_all(310)]
853        if len(binary_data):
854            self.proxy_graphic = b''.join(binary_data)
855
856    def store_tags(self, tags: ExtendedTags) -> None:
857        # store DXFTYPE, overrides class member
858        # 1. tag of 1. subclass is the structure tag (0, DXFTYPE)
859        self.xtags = tags
860        self.DXFTYPE = self.base_class[0].value
861        try:
862            acdb_entity = tags.get_subclass('AcDbEntity')
863            self.dxf.__dict__['paperspace'] = acdb_entity.get_first_value(67, 0)
864        except const.DXFKeyError:
865            # just fake it
866            self.dxf.__dict__['paperspace'] = 0
867
868    def export_entity(self, tagwriter: 'TagWriter') -> None:
869        """ Write subclass tags as they are. """
870        for subclass in self.xtags.subclasses[1:]:
871            tagwriter.write_tags(subclass)
872
873    def destroy(self) -> None:
874        if not self.is_alive:
875            return
876
877        del self.xtags
878        super().destroy()
879