# Copyright (c) 2011-2018, Manfred Moitzi # License: MIT License from collections import namedtuple from enum import Enum from typing import Any, Tuple, Iterable, List, Dict, Union, ItemsView, KeysView, TYPE_CHECKING from .const import DXFAttributeError, DXFValueError, DXFInternalEzdxfError, DXFStructureError from .types import dxftag, DXFVertex from .tags import Tags from .extendedtags import ExtendedTags if TYPE_CHECKING: # import forward declarations from ezdxf.eztypes import DXFEntity, TagValue DefSubclass = namedtuple('DefSubclass', 'name attribs') VIRTUAL_TAG = -666 class XType(Enum): """ Extended Attribute Types """ point2d = 1 # 2D points only point3d = 2 # 3D points only any_point = 3 # 2D or 3D points callback = 4 # class DXFAttr: """ Defines a DXF attribute, accessible by DXFEntity.dxf.name. Extended Attribute Types ------------------------ - XType.point2d: 2D points only - XType.point3d: 3D point only - XType.any_point: mixed 2D/3D point - XType.callback: Calls get_value(entity) to get the value of DXF attribute 'name', and calls set_value(entity, value) to set value of DXF attribute 'name'. For example definitions see TestDXFEntity.test_callback() in file test_dxfentity.py """ def __init__(self, code: int, subclass: int = 0, xtype: XType = None, default=None, dxfversion: str = None, getter: str = None, # name of getter method setter: str = None, # name of setter method ): self.name = '' # type: str # set by DXFAttributes._add_subclass_attribs() self.code = code # DXF group code self.subclass = subclass # subclass index self.xtype = xtype # Point2D, Point3D, Point2D/3D, Callback self.default = default # type: TagValue # DXF default value # If dxfversion is None - this attribute is valid for all supported DXF versions, set dxfversion to a specific # DXF version like 'AC1018' and this attribute can only be set by DXF version 'AC1018' or later. self.dxfversion = dxfversion self.getter = getter # DXF entity getter method name for callback attributes self.setter = setter # DXF entity setter method name for callback attributes def get_callback_value(self, entity: 'DXFEntity') -> 'TagValue': """ Executes a callback function in 'entity' to get a DXF value. Callback function is defined by self.getter as string. Args: entity: DXF entity Returns: DXF attribute value """ try: return getattr(entity, self.getter)() except AttributeError: raise DXFAttributeError('DXF attribute {}: invalid getter {}.'.format(self.name, self.getter)) except TypeError: # None DXFAttributeError('DXF attribute {} has no getter.'.format(self.name)) def set_callback_value(self, entity: 'DXFEntity', value: 'TagValue') -> None: """ Executes a callback function in 'entity' to set a DXF value. Callback function is defined by self.setter as string. Args: entity: DXF entity value: DXF attribute value """ try: getattr(entity, self.setter)(value) except AttributeError: raise DXFAttributeError('DXF attribute {}: invalid setter {}.'.format(self.name, self.setter)) except TypeError: # None raise DXFAttributeError('DXF attribute {} has no setter.'.format(self.name)) def get_attrib(self, entity: 'DXFEntity', key: str, default: Any = DXFValueError) -> 'TagValue': """ Return value of DXF attribute 'key'. Args: entity: DXF entity key: attribute name default: default value or DXFValueError for raising an exception if attribute does not exist Returns: value of DXF attribute """ if self.xtype is XType.callback: return self.get_callback_value(entity) try: # No check if attribute is valid for DXF version of drawing, if it is there you get it return self._get_dxf_attrib(entity.tags) except DXFValueError: if default is DXFValueError: # no DXF default values if DXF version is incorrect if self.dxfversion is not None and entity.drawing.dxfversion < self.dxfversion: msg = "DXFAttrib '{0}' not supported by DXF version '{1}', requires at least DXF version '{2}'." raise DXFValueError(msg.format(key, entity.drawing.dxfversion, self.dxfversion)) result = self.default # default value defined by DXF specs if result is not None: return result else: raise DXFValueError("DXFAttrib '%s' does not exist." % key) else: return default def _get_dxf_attrib(self, tags: ExtendedTags) -> 'TagValue': subclass_tags = self._get_dxf_attrib_subclass_tags(tags, self.subclass) if self.xtype is not None: return self._get_extended_type(subclass_tags) else: return subclass_tags.get_first_value(self.code) def _get_extended_type(self, tags: Tags) -> Tuple[float, ...]: value = tags.get_first_value(self.code) if len(value) == 3: if self.xtype is XType.point2d: raise DXFStructureError("expected 2D point but found 3D point") elif self.xtype is XType.point3d: # len(value) == 2 raise DXFStructureError("expected 3D point but found 2D point") return value def _get_dxf_attrib_subclass_tags(self, tags: ExtendedTags, subclass_key: Union[int, str]) -> Tags: try: # fast access subclass by index as int, no subclass is subclass index 0 return tags.subclasses[subclass_key] except IndexError: raise DXFInternalEzdxfError('Subclass index error in {entity} subclass={index}.'.format( entity=str(self), index=subclass_key, )) except TypeError: # slow access subclass by name as string # raises DXFKeyError if subclass does not exist return tags.get_subclass(subclass_key) def set_attrib(self, entity: 'DXFEntity', key: str, value: 'TagValue') -> None: """ Set DXF attribute 'key' to value. Args: entity: DXF entity key: attribute name value: attribute value """ if self.dxfversion is not None: if entity.drawing.dxfversion < self.dxfversion: msg = "DXFAttrib '{0}' not supported by DXF version '{1}', requires at least DXF version '{2}'." raise DXFAttributeError(msg.format(key, entity.drawing.dxfversion, self.dxfversion)) if self.xtype is XType.callback: self.set_callback_value(entity, value) return subclass_tags = self._get_dxf_attrib_subclass_tags(entity.tags, self.subclass) if self.xtype is not None: self._set_extended_type(subclass_tags, value) else: subclass_tags.set_first(dxftag(self.code, value)) def _set_extended_type(self, tags: Tags, value: Iterable) -> None: value = tuple(value) vlen = len(value) if vlen == 3: if self.xtype is XType.point2d: raise DXFValueError('2 axis required') elif vlen == 2: if self.xtype is XType.point3d: raise DXFValueError('3 axis required') else: raise DXFValueError('2 or 3 axis required') tags.set_first(DXFVertex(self.code, value)) def del_attrib(self, entity: 'DXFEntity') -> None: """ Remove tag of DXF attribute in 'entity'. Args: entity: DXF entity """ subclass_tags = self._get_dxf_attrib_subclass_tags(entity.tags, self.subclass) subclass_tags.remove_tags(codes=(self.code,)) class DXFAttributes: def __init__(self, *subclassdefs: DefSubclass): self._subclasses = [] # type: List[DefSubclass] self._attribs = {} # type: Dict[str, DXFAttr] for subclass in subclassdefs: self.add_subclass(subclass) def add_subclass(self, subclass: DefSubclass) -> None: subclass_index = len(self._subclasses) self._subclasses.append(subclass) self._add_subclass_attribs(subclass, subclass_index) def _add_subclass_attribs(self, subclass: DefSubclass, subclass_index: int) -> None: for name, dxfattrib in subclass.attribs.items(): dxfattrib.name = name dxfattrib.subclass = subclass_index self._attribs[name] = dxfattrib def __getitem__(self, name: str) -> DXFAttr: return self._attribs[name] def __contains__(self, name: str) -> bool: return name in self._attribs def get(self, key: str, default: Any = None) -> Any: return self._attribs.get(key, default) def keys(self) -> KeysView[str]: return self._attribs.keys() def items(self) -> ItemsView[str, DXFAttr]: return self._attribs.items() def subclasses(self) -> Iterable[DefSubclass]: return iter(self._subclasses) def build_group_code_items(self, func=lambda x: True): for name, attrib in self.items(): if attrib.code > 0 and func(name): # skip internal tags yield attrib.code, name