attributes.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # Copyright (c) 2011-2018, Manfred Moitzi
  2. # License: MIT License
  3. from collections import namedtuple
  4. from enum import Enum
  5. from typing import Any, Tuple, Iterable, List, Dict, Union, ItemsView, KeysView, TYPE_CHECKING
  6. from .const import DXFAttributeError, DXFValueError, DXFInternalEzdxfError, DXFStructureError
  7. from .types import dxftag, DXFVertex
  8. from .tags import Tags
  9. from .extendedtags import ExtendedTags
  10. if TYPE_CHECKING: # import forward declarations
  11. from ezdxf.eztypes import DXFEntity, TagValue
  12. DefSubclass = namedtuple('DefSubclass', 'name attribs')
  13. VIRTUAL_TAG = -666
  14. class XType(Enum):
  15. """ Extended Attribute Types
  16. """
  17. point2d = 1 # 2D points only
  18. point3d = 2 # 3D points only
  19. any_point = 3 # 2D or 3D points
  20. callback = 4 #
  21. class DXFAttr:
  22. """
  23. Defines a DXF attribute, accessible by DXFEntity.dxf.name.
  24. Extended Attribute Types
  25. ------------------------
  26. - XType.point2d: 2D points only
  27. - XType.point3d: 3D point only
  28. - XType.any_point: mixed 2D/3D point
  29. - XType.callback: Calls get_value(entity) to get the value of DXF attribute 'name', and calls
  30. set_value(entity, value) to set value of DXF attribute 'name'.
  31. For example definitions see TestDXFEntity.test_callback() in file test_dxfentity.py
  32. """
  33. def __init__(self,
  34. code: int,
  35. subclass: int = 0,
  36. xtype: XType = None,
  37. default=None,
  38. dxfversion: str = None,
  39. getter: str = None, # name of getter method
  40. setter: str = None, # name of setter method
  41. ):
  42. self.name = '' # type: str # set by DXFAttributes._add_subclass_attribs()
  43. self.code = code # DXF group code
  44. self.subclass = subclass # subclass index
  45. self.xtype = xtype # Point2D, Point3D, Point2D/3D, Callback
  46. self.default = default # type: TagValue # DXF default value
  47. # If dxfversion is None - this attribute is valid for all supported DXF versions, set dxfversion to a specific
  48. # DXF version like 'AC1018' and this attribute can only be set by DXF version 'AC1018' or later.
  49. self.dxfversion = dxfversion
  50. self.getter = getter # DXF entity getter method name for callback attributes
  51. self.setter = setter # DXF entity setter method name for callback attributes
  52. def get_callback_value(self, entity: 'DXFEntity') -> 'TagValue':
  53. """
  54. Executes a callback function in 'entity' to get a DXF value.
  55. Callback function is defined by self.getter as string.
  56. Args:
  57. entity: DXF entity
  58. Returns: DXF attribute value
  59. """
  60. try:
  61. return getattr(entity, self.getter)()
  62. except AttributeError:
  63. raise DXFAttributeError('DXF attribute {}: invalid getter {}.'.format(self.name, self.getter))
  64. except TypeError: # None
  65. DXFAttributeError('DXF attribute {} has no getter.'.format(self.name))
  66. def set_callback_value(self, entity: 'DXFEntity', value: 'TagValue') -> None:
  67. """
  68. Executes a callback function in 'entity' to set a DXF value.
  69. Callback function is defined by self.setter as string.
  70. Args:
  71. entity: DXF entity
  72. value: DXF attribute value
  73. """
  74. try:
  75. getattr(entity, self.setter)(value)
  76. except AttributeError:
  77. raise DXFAttributeError('DXF attribute {}: invalid setter {}.'.format(self.name, self.setter))
  78. except TypeError: # None
  79. raise DXFAttributeError('DXF attribute {} has no setter.'.format(self.name))
  80. def get_attrib(self, entity: 'DXFEntity', key: str, default: Any = DXFValueError) -> 'TagValue':
  81. """
  82. Return value of DXF attribute 'key'.
  83. Args:
  84. entity: DXF entity
  85. key: attribute name
  86. default: default value or DXFValueError for raising an exception if attribute does not exist
  87. Returns: value of DXF attribute
  88. """
  89. if self.xtype is XType.callback:
  90. return self.get_callback_value(entity)
  91. try: # No check if attribute is valid for DXF version of drawing, if it is there you get it
  92. return self._get_dxf_attrib(entity.tags)
  93. except DXFValueError:
  94. if default is DXFValueError:
  95. # no DXF default values if DXF version is incorrect
  96. if self.dxfversion is not None and entity.drawing.dxfversion < self.dxfversion:
  97. msg = "DXFAttrib '{0}' not supported by DXF version '{1}', requires at least DXF version '{2}'."
  98. raise DXFValueError(msg.format(key, entity.drawing.dxfversion, self.dxfversion))
  99. result = self.default # default value defined by DXF specs
  100. if result is not None:
  101. return result
  102. else:
  103. raise DXFValueError("DXFAttrib '%s' does not exist." % key)
  104. else:
  105. return default
  106. def _get_dxf_attrib(self, tags: ExtendedTags) -> 'TagValue':
  107. subclass_tags = self._get_dxf_attrib_subclass_tags(tags, self.subclass)
  108. if self.xtype is not None:
  109. return self._get_extended_type(subclass_tags)
  110. else:
  111. return subclass_tags.get_first_value(self.code)
  112. def _get_extended_type(self, tags: Tags) -> Tuple[float, ...]:
  113. value = tags.get_first_value(self.code)
  114. if len(value) == 3:
  115. if self.xtype is XType.point2d:
  116. raise DXFStructureError("expected 2D point but found 3D point")
  117. elif self.xtype is XType.point3d: # len(value) == 2
  118. raise DXFStructureError("expected 3D point but found 2D point")
  119. return value
  120. def _get_dxf_attrib_subclass_tags(self, tags: ExtendedTags, subclass_key: Union[int, str]) -> Tags:
  121. try: # fast access subclass by index as int, no subclass is subclass index 0
  122. return tags.subclasses[subclass_key]
  123. except IndexError:
  124. raise DXFInternalEzdxfError('Subclass index error in {entity} subclass={index}.'.format(
  125. entity=str(self),
  126. index=subclass_key,
  127. ))
  128. except TypeError: # slow access subclass by name as string
  129. # raises DXFKeyError if subclass does not exist
  130. return tags.get_subclass(subclass_key)
  131. def set_attrib(self, entity: 'DXFEntity', key: str, value: 'TagValue') -> None:
  132. """
  133. Set DXF attribute 'key' to value.
  134. Args:
  135. entity: DXF entity
  136. key: attribute name
  137. value: attribute value
  138. """
  139. if self.dxfversion is not None:
  140. if entity.drawing.dxfversion < self.dxfversion:
  141. msg = "DXFAttrib '{0}' not supported by DXF version '{1}', requires at least DXF version '{2}'."
  142. raise DXFAttributeError(msg.format(key, entity.drawing.dxfversion, self.dxfversion))
  143. if self.xtype is XType.callback:
  144. self.set_callback_value(entity, value)
  145. return
  146. subclass_tags = self._get_dxf_attrib_subclass_tags(entity.tags, self.subclass)
  147. if self.xtype is not None:
  148. self._set_extended_type(subclass_tags, value)
  149. else:
  150. subclass_tags.set_first(dxftag(self.code, value))
  151. def _set_extended_type(self, tags: Tags, value: Iterable) -> None:
  152. value = tuple(value)
  153. vlen = len(value)
  154. if vlen == 3:
  155. if self.xtype is XType.point2d:
  156. raise DXFValueError('2 axis required')
  157. elif vlen == 2:
  158. if self.xtype is XType.point3d:
  159. raise DXFValueError('3 axis required')
  160. else:
  161. raise DXFValueError('2 or 3 axis required')
  162. tags.set_first(DXFVertex(self.code, value))
  163. def del_attrib(self, entity: 'DXFEntity') -> None:
  164. """
  165. Remove tag of DXF attribute in 'entity'.
  166. Args:
  167. entity: DXF entity
  168. """
  169. subclass_tags = self._get_dxf_attrib_subclass_tags(entity.tags, self.subclass)
  170. subclass_tags.remove_tags(codes=(self.code,))
  171. class DXFAttributes:
  172. def __init__(self, *subclassdefs: DefSubclass):
  173. self._subclasses = [] # type: List[DefSubclass]
  174. self._attribs = {} # type: Dict[str, DXFAttr]
  175. for subclass in subclassdefs:
  176. self.add_subclass(subclass)
  177. def add_subclass(self, subclass: DefSubclass) -> None:
  178. subclass_index = len(self._subclasses)
  179. self._subclasses.append(subclass)
  180. self._add_subclass_attribs(subclass, subclass_index)
  181. def _add_subclass_attribs(self, subclass: DefSubclass, subclass_index: int) -> None:
  182. for name, dxfattrib in subclass.attribs.items():
  183. dxfattrib.name = name
  184. dxfattrib.subclass = subclass_index
  185. self._attribs[name] = dxfattrib
  186. def __getitem__(self, name: str) -> DXFAttr:
  187. return self._attribs[name]
  188. def __contains__(self, name: str) -> bool:
  189. return name in self._attribs
  190. def get(self, key: str, default: Any = None) -> Any:
  191. return self._attribs.get(key, default)
  192. def keys(self) -> KeysView[str]:
  193. return self._attribs.keys()
  194. def items(self) -> ItemsView[str, DXFAttr]:
  195. return self._attribs.items()
  196. def subclasses(self) -> Iterable[DefSubclass]:
  197. return iter(self._subclasses)
  198. def build_group_code_items(self, func=lambda x: True):
  199. for name, attrib in self.items():
  200. if attrib.code > 0 and func(name): # skip internal tags
  201. yield attrib.code, name