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