Source code for openpyxl.packaging.custom

# Copyright (c) 2010-2023 openpyxl

"""Implementation of custom properties see ยง 22.3 in the specification"""


from warnings import warn

from openpyxl.descriptors import Strict
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors.sequence import Sequence
from openpyxl.descriptors import (
    Alias,
    String,
    Integer,
    Float,
    DateTime,
    Bool,
)
from openpyxl.descriptors.nested import (
    NestedText,
)

from openpyxl.xml.constants import (
    CUSTPROPS_NS,
    VTYPES_NS,
    CPROPS_FMTID,
)

from .core import NestedDateTime


[docs]class NestedBoolText(Bool, NestedText): """ Descriptor for handling nested elements with the value stored in the text part """ pass
class _CustomDocumentProperty(Serialisable): """ Low-level representation of a Custom Document Property. Not used directly Must always contain a child element, even if this is empty """ tagname = "property" _typ = None name = String(allow_none=True) lpwstr = NestedText(expected_type=str, allow_none=True, namespace=VTYPES_NS) i4 = NestedText(expected_type=int, allow_none=True, namespace=VTYPES_NS) r8 = NestedText(expected_type=float, allow_none=True, namespace=VTYPES_NS) filetime = NestedDateTime(allow_none=True, namespace=VTYPES_NS) bool = NestedBoolText(expected_type=bool, allow_none=True, namespace=VTYPES_NS) linkTarget = String(expected_type=str, allow_none=True) fmtid = String() pid = Integer() def __init__(self, name=None, pid=0, fmtid=CPROPS_FMTID, linkTarget=None, **kw): self.fmtid = fmtid self.pid = pid self.name = name self._typ = None self.linkTarget = linkTarget for k, v in kw.items(): setattr(self, k, v) setattr(self, "_typ", k) # ugh! for e in self.__elements__: if e not in kw: setattr(self, e, None) @property def type(self): if self._typ is not None: return self._typ for a in self.__elements__: if getattr(self, a) is not None: return a if self.linkTarget is not None: return "linkTarget" def to_tree(self, tagname=None, idx=None, namespace=None): child = getattr(self, self._typ, None) if child is None: setattr(self, self._typ, "") return super().to_tree(tagname=None, idx=None, namespace=None) class _CustomDocumentPropertyList(Serialisable): """ Parses and seriliases property lists but is not used directly """ tagname = "Properties" property = Sequence(expected_type=_CustomDocumentProperty, namespace=CUSTPROPS_NS) customProps = Alias("property") def __init__(self, property=()): self.property = property def __len__(self): return len(self.property) def to_tree(self, tagname=None, idx=None, namespace=None): for idx, p in enumerate(self.property, 2): p.pid = idx tree = super().to_tree(tagname, idx, namespace) tree.set("xmlns", CUSTPROPS_NS) return tree class _TypedProperty(Strict): name = String() def __init__(self, name, value): self.name = name self.value = value def __eq__(self, other): return self.name == other.name and self.value == other.value def __repr__(self): return f"{self.__class__.__name__}, name={self.name}, value={self.value}"
[docs]class IntProperty(_TypedProperty): value = Integer()
[docs]class FloatProperty(_TypedProperty): value = Float()
[docs]class StringProperty(_TypedProperty): value = String(allow_none=True)
[docs]class DateTimeProperty(_TypedProperty): value = DateTime()
[docs]class BoolProperty(_TypedProperty): value = Bool()
[docs]class LinkProperty(_TypedProperty): value = String()
# from Python CLASS_MAPPING = { StringProperty: "lpwstr", IntProperty: "i4", FloatProperty: "r8", DateTimeProperty: "filetime", BoolProperty: "bool", LinkProperty: "linkTarget" } XML_MAPPING = {v:k for k,v in CLASS_MAPPING.items()}
[docs]class CustomPropertyList(Strict): props = Sequence(expected_type=_TypedProperty) def __init__(self): self.props = []
[docs] @classmethod def from_tree(cls, tree): """ Create list from OOXML element """ prop_list = _CustomDocumentPropertyList.from_tree(tree) new_props = cls() for prop in prop_list.property: attr = prop.type typ = XML_MAPPING.get(attr, None) if not typ: warn(f"Unknown type for {prop.name}") continue value = getattr(prop, attr) link = prop.linkTarget if link is not None: typ = LinkProperty value = prop.linkTarget new_prop = typ(name=prop.name, value=value) new_props.append(new_prop) return new_props
[docs] def append(self, prop): if prop.name in self.names: raise ValueError(f"Property with name {prop.name} already exists") props = self.props props.append(prop) self.props = props
[docs] def to_tree(self): props = [] for p in self.props: attr = CLASS_MAPPING.get(p.__class__, None) if not attr: raise TypeError("Unknown adapter for {p}") np = _CustomDocumentProperty(name=p.name, **{attr:p.value}) if isinstance(p, LinkProperty): np._typ = "lpwstr" #np.lpwstr = "" props.append(np) prop_list = _CustomDocumentPropertyList(property=props) return prop_list.to_tree()
def __len__(self): return len(self.props) @property def names(self): """List of property names""" return [p.name for p in self.props] def __getitem__(self, name): """ Get property by name """ for p in self.props: if p.name == name: return p raise KeyError(f"Property with name {name} not found") def __delitem__(self, name): """ Delete a propery by name """ for idx, p in enumerate(self.props): if p.name == name: self.props.pop(idx) return raise KeyError(f"Property with name {name} not found") def __repr__(self): return f"{self.__class__.__name__} containing {self.props}" def __iter__(self): return iter(self.props)