You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
242 lines
8.0 KiB
242 lines
8.0 KiB
# encoding: utf-8
|
|
|
|
"""
|
|
Open Packaging Convention (OPC) objects related to package parts.
|
|
"""
|
|
|
|
from __future__ import (
|
|
absolute_import, division, print_function, unicode_literals
|
|
)
|
|
|
|
from .compat import cls_method_fn
|
|
from .oxml import serialize_part_xml
|
|
from ..oxml import parse_xml
|
|
from .packuri import PackURI
|
|
from .rel import Relationships
|
|
from .shared import lazyproperty
|
|
|
|
|
|
class Part(object):
|
|
"""
|
|
Base class for package parts. Provides common properties and methods, but
|
|
intended to be subclassed in client code to implement specific part
|
|
behaviors.
|
|
"""
|
|
def __init__(self, partname, content_type, blob=None, package=None):
|
|
super(Part, self).__init__()
|
|
self._partname = partname
|
|
self._content_type = content_type
|
|
self._blob = blob
|
|
self._package = package
|
|
|
|
def after_unmarshal(self):
|
|
"""
|
|
Entry point for post-unmarshaling processing, for example to parse
|
|
the part XML. May be overridden by subclasses without forwarding call
|
|
to super.
|
|
"""
|
|
# don't place any code here, just catch call if not overridden by
|
|
# subclass
|
|
pass
|
|
|
|
def before_marshal(self):
|
|
"""
|
|
Entry point for pre-serialization processing, for example to finalize
|
|
part naming if necessary. May be overridden by subclasses without
|
|
forwarding call to super.
|
|
"""
|
|
# don't place any code here, just catch call if not overridden by
|
|
# subclass
|
|
pass
|
|
|
|
@property
|
|
def blob(self):
|
|
"""
|
|
Contents of this package part as a sequence of bytes. May be text or
|
|
binary. Intended to be overridden by subclasses. Default behavior is
|
|
to return load blob.
|
|
"""
|
|
return self._blob
|
|
|
|
@property
|
|
def content_type(self):
|
|
"""
|
|
Content type of this part.
|
|
"""
|
|
return self._content_type
|
|
|
|
def drop_rel(self, rId):
|
|
"""
|
|
Remove the relationship identified by *rId* if its reference count
|
|
is less than 2. Relationships with a reference count of 0 are
|
|
implicit relationships.
|
|
"""
|
|
if self._rel_ref_count(rId) < 2:
|
|
del self.rels[rId]
|
|
|
|
@classmethod
|
|
def load(cls, partname, content_type, blob, package):
|
|
return cls(partname, content_type, blob, package)
|
|
|
|
def load_rel(self, reltype, target, rId, is_external=False):
|
|
"""
|
|
Return newly added |_Relationship| instance of *reltype* between this
|
|
part and *target* with key *rId*. Target mode is set to
|
|
``RTM.EXTERNAL`` if *is_external* is |True|. Intended for use during
|
|
load from a serialized package, where the rId is well-known. Other
|
|
methods exist for adding a new relationship to a part when
|
|
manipulating a part.
|
|
"""
|
|
return self.rels.add_relationship(reltype, target, rId, is_external)
|
|
|
|
@property
|
|
def package(self):
|
|
"""
|
|
|OpcPackage| instance this part belongs to.
|
|
"""
|
|
return self._package
|
|
|
|
@property
|
|
def partname(self):
|
|
"""
|
|
|PackURI| instance holding partname of this part, e.g.
|
|
'/ppt/slides/slide1.xml'
|
|
"""
|
|
return self._partname
|
|
|
|
@partname.setter
|
|
def partname(self, partname):
|
|
if not isinstance(partname, PackURI):
|
|
tmpl = "partname must be instance of PackURI, got '%s'"
|
|
raise TypeError(tmpl % type(partname).__name__)
|
|
self._partname = partname
|
|
|
|
def part_related_by(self, reltype):
|
|
"""
|
|
Return part to which this part has a relationship of *reltype*.
|
|
Raises |KeyError| if no such relationship is found and |ValueError|
|
|
if more than one such relationship is found. Provides ability to
|
|
resolve implicitly related part, such as Slide -> SlideLayout.
|
|
"""
|
|
return self.rels.part_with_reltype(reltype)
|
|
|
|
def relate_to(self, target, reltype, is_external=False):
|
|
"""
|
|
Return rId key of relationship of *reltype* to *target*, from an
|
|
existing relationship if there is one, otherwise a newly created one.
|
|
"""
|
|
if is_external:
|
|
return self.rels.get_or_add_ext_rel(reltype, target)
|
|
else:
|
|
rel = self.rels.get_or_add(reltype, target)
|
|
return rel.rId
|
|
|
|
@property
|
|
def related_parts(self):
|
|
"""
|
|
Dictionary mapping related parts by rId, so child objects can resolve
|
|
explicit relationships present in the part XML, e.g. sldIdLst to a
|
|
specific |Slide| instance.
|
|
"""
|
|
return self.rels.related_parts
|
|
|
|
@lazyproperty
|
|
def rels(self):
|
|
"""
|
|
|Relationships| instance holding the relationships for this part.
|
|
"""
|
|
return Relationships(self._partname.baseURI)
|
|
|
|
def target_ref(self, rId):
|
|
"""
|
|
Return URL contained in target ref of relationship identified by
|
|
*rId*.
|
|
"""
|
|
rel = self.rels[rId]
|
|
return rel.target_ref
|
|
|
|
def _rel_ref_count(self, rId):
|
|
"""
|
|
Return the count of references in this part's XML to the relationship
|
|
identified by *rId*.
|
|
"""
|
|
rIds = self._element.xpath('//@r:id')
|
|
return len([_rId for _rId in rIds if _rId == rId])
|
|
|
|
|
|
class PartFactory(object):
|
|
"""
|
|
Provides a way for client code to specify a subclass of |Part| to be
|
|
constructed by |Unmarshaller| based on its content type and/or a custom
|
|
callable. Setting ``PartFactory.part_class_selector`` to a callable
|
|
object will cause that object to be called with the parameters
|
|
``content_type, reltype``, once for each part in the package. If the
|
|
callable returns an object, it is used as the class for that part. If it
|
|
returns |None|, part class selection falls back to the content type map
|
|
defined in ``PartFactory.part_type_for``. If no class is returned from
|
|
either of these, the class contained in ``PartFactory.default_part_type``
|
|
is used to construct the part, which is by default ``opc.package.Part``.
|
|
"""
|
|
part_class_selector = None
|
|
part_type_for = {}
|
|
default_part_type = Part
|
|
|
|
def __new__(cls, partname, content_type, reltype, blob, package):
|
|
PartClass = None
|
|
if cls.part_class_selector is not None:
|
|
part_class_selector = cls_method_fn(cls, 'part_class_selector')
|
|
PartClass = part_class_selector(content_type, reltype)
|
|
if PartClass is None:
|
|
PartClass = cls._part_cls_for(content_type)
|
|
return PartClass.load(partname, content_type, blob, package)
|
|
|
|
@classmethod
|
|
def _part_cls_for(cls, content_type):
|
|
"""
|
|
Return the custom part class registered for *content_type*, or the
|
|
default part class if no custom class is registered for
|
|
*content_type*.
|
|
"""
|
|
if content_type in cls.part_type_for:
|
|
return cls.part_type_for[content_type]
|
|
return cls.default_part_type
|
|
|
|
|
|
class XmlPart(Part):
|
|
"""
|
|
Base class for package parts containing an XML payload, which is most of
|
|
them. Provides additional methods to the |Part| base class that take care
|
|
of parsing and reserializing the XML payload and managing relationships
|
|
to other parts.
|
|
"""
|
|
def __init__(self, partname, content_type, element, package):
|
|
super(XmlPart, self).__init__(
|
|
partname, content_type, package=package
|
|
)
|
|
self._element = element
|
|
|
|
@property
|
|
def blob(self):
|
|
return serialize_part_xml(self._element)
|
|
|
|
@property
|
|
def element(self):
|
|
"""
|
|
The root XML element of this XML part.
|
|
"""
|
|
return self._element
|
|
|
|
@classmethod
|
|
def load(cls, partname, content_type, blob, package):
|
|
element = parse_xml(blob)
|
|
return cls(partname, content_type, element, package)
|
|
|
|
@property
|
|
def part(self):
|
|
"""
|
|
Part of the parent protocol, "children" of the document will not know
|
|
the part that contains them so must ask their parent object. That
|
|
chain of delegation ends here for child objects.
|
|
"""
|
|
return self
|