custom.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. # Copyright (c) 2010-2024 openpyxl
  2. """Implementation of custom properties see § 22.3 in the specification"""
  3. from warnings import warn
  4. from openpyxl.descriptors import Strict
  5. from openpyxl.descriptors.serialisable import Serialisable
  6. from openpyxl.descriptors.sequence import Sequence
  7. from openpyxl.descriptors import (
  8. Alias,
  9. String,
  10. Integer,
  11. Float,
  12. DateTime,
  13. Bool,
  14. )
  15. from openpyxl.descriptors.nested import (
  16. NestedText,
  17. )
  18. from openpyxl.xml.constants import (
  19. CUSTPROPS_NS,
  20. VTYPES_NS,
  21. CPROPS_FMTID,
  22. )
  23. from .core import NestedDateTime
  24. class NestedBoolText(Bool, NestedText):
  25. """
  26. Descriptor for handling nested elements with the value stored in the text part
  27. """
  28. pass
  29. class _CustomDocumentProperty(Serialisable):
  30. """
  31. Low-level representation of a Custom Document Property.
  32. Not used directly
  33. Must always contain a child element, even if this is empty
  34. """
  35. tagname = "property"
  36. _typ = None
  37. name = String(allow_none=True)
  38. lpwstr = NestedText(expected_type=str, allow_none=True, namespace=VTYPES_NS)
  39. i4 = NestedText(expected_type=int, allow_none=True, namespace=VTYPES_NS)
  40. r8 = NestedText(expected_type=float, allow_none=True, namespace=VTYPES_NS)
  41. filetime = NestedDateTime(allow_none=True, namespace=VTYPES_NS)
  42. bool = NestedBoolText(expected_type=bool, allow_none=True, namespace=VTYPES_NS)
  43. linkTarget = String(expected_type=str, allow_none=True)
  44. fmtid = String()
  45. pid = Integer()
  46. def __init__(self,
  47. name=None,
  48. pid=0,
  49. fmtid=CPROPS_FMTID,
  50. linkTarget=None,
  51. **kw):
  52. self.fmtid = fmtid
  53. self.pid = pid
  54. self.name = name
  55. self._typ = None
  56. self.linkTarget = linkTarget
  57. for k, v in kw.items():
  58. setattr(self, k, v)
  59. setattr(self, "_typ", k) # ugh!
  60. for e in self.__elements__:
  61. if e not in kw:
  62. setattr(self, e, None)
  63. @property
  64. def type(self):
  65. if self._typ is not None:
  66. return self._typ
  67. for a in self.__elements__:
  68. if getattr(self, a) is not None:
  69. return a
  70. if self.linkTarget is not None:
  71. return "linkTarget"
  72. def to_tree(self, tagname=None, idx=None, namespace=None):
  73. child = getattr(self, self._typ, None)
  74. if child is None:
  75. setattr(self, self._typ, "")
  76. return super().to_tree(tagname=None, idx=None, namespace=None)
  77. class _CustomDocumentPropertyList(Serialisable):
  78. """
  79. Parses and seriliases property lists but is not used directly
  80. """
  81. tagname = "Properties"
  82. property = Sequence(expected_type=_CustomDocumentProperty, namespace=CUSTPROPS_NS)
  83. customProps = Alias("property")
  84. def __init__(self, property=()):
  85. self.property = property
  86. def __len__(self):
  87. return len(self.property)
  88. def to_tree(self, tagname=None, idx=None, namespace=None):
  89. for idx, p in enumerate(self.property, 2):
  90. p.pid = idx
  91. tree = super().to_tree(tagname, idx, namespace)
  92. tree.set("xmlns", CUSTPROPS_NS)
  93. return tree
  94. class _TypedProperty(Strict):
  95. name = String()
  96. def __init__(self,
  97. name,
  98. value):
  99. self.name = name
  100. self.value = value
  101. def __eq__(self, other):
  102. return self.name == other.name and self.value == other.value
  103. def __repr__(self):
  104. return f"{self.__class__.__name__}, name={self.name}, value={self.value}"
  105. class IntProperty(_TypedProperty):
  106. value = Integer()
  107. class FloatProperty(_TypedProperty):
  108. value = Float()
  109. class StringProperty(_TypedProperty):
  110. value = String(allow_none=True)
  111. class DateTimeProperty(_TypedProperty):
  112. value = DateTime()
  113. class BoolProperty(_TypedProperty):
  114. value = Bool()
  115. class LinkProperty(_TypedProperty):
  116. value = String()
  117. # from Python
  118. CLASS_MAPPING = {
  119. StringProperty: "lpwstr",
  120. IntProperty: "i4",
  121. FloatProperty: "r8",
  122. DateTimeProperty: "filetime",
  123. BoolProperty: "bool",
  124. LinkProperty: "linkTarget"
  125. }
  126. XML_MAPPING = {v:k for k,v in CLASS_MAPPING.items()}
  127. class CustomPropertyList(Strict):
  128. props = Sequence(expected_type=_TypedProperty)
  129. def __init__(self):
  130. self.props = []
  131. @classmethod
  132. def from_tree(cls, tree):
  133. """
  134. Create list from OOXML element
  135. """
  136. prop_list = _CustomDocumentPropertyList.from_tree(tree)
  137. props = []
  138. for prop in prop_list.property:
  139. attr = prop.type
  140. typ = XML_MAPPING.get(attr, None)
  141. if not typ:
  142. warn(f"Unknown type for {prop.name}")
  143. continue
  144. value = getattr(prop, attr)
  145. link = prop.linkTarget
  146. if link is not None:
  147. typ = LinkProperty
  148. value = prop.linkTarget
  149. new_prop = typ(name=prop.name, value=value)
  150. props.append(new_prop)
  151. new_prop_list = cls()
  152. new_prop_list.props = props
  153. return new_prop_list
  154. def append(self, prop):
  155. if prop.name in self.names:
  156. raise ValueError(f"Property with name {prop.name} already exists")
  157. self.props.append(prop)
  158. def to_tree(self):
  159. props = []
  160. for p in self.props:
  161. attr = CLASS_MAPPING.get(p.__class__, None)
  162. if not attr:
  163. raise TypeError("Unknown adapter for {p}")
  164. np = _CustomDocumentProperty(name=p.name, **{attr:p.value})
  165. if isinstance(p, LinkProperty):
  166. np._typ = "lpwstr"
  167. #np.lpwstr = ""
  168. props.append(np)
  169. prop_list = _CustomDocumentPropertyList(property=props)
  170. return prop_list.to_tree()
  171. def __len__(self):
  172. return len(self.props)
  173. @property
  174. def names(self):
  175. """List of property names"""
  176. return [p.name for p in self.props]
  177. def __getitem__(self, name):
  178. """
  179. Get property by name
  180. """
  181. for p in self.props:
  182. if p.name == name:
  183. return p
  184. raise KeyError(f"Property with name {name} not found")
  185. def __delitem__(self, name):
  186. """
  187. Delete a propery by name
  188. """
  189. for idx, p in enumerate(self.props):
  190. if p.name == name:
  191. self.props.pop(idx)
  192. return
  193. raise KeyError(f"Property with name {name} not found")
  194. def __repr__(self):
  195. return f"{self.__class__.__name__} containing {self.props}"
  196. def __iter__(self):
  197. return iter(self.props)