Source code for openpyxl.packaging.relationship
# Copyright (c) 2010-2024 openpyxl
import posixpath
from warnings import warn
from openpyxl.descriptors import (
String,
Alias,
Sequence,
)
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors.container import ElementList
from openpyxl.xml.constants import REL_NS, PKG_REL_NS
from openpyxl.xml.functions import (
Element,
fromstring,
)
[docs]
class Relationship(Serialisable):
"""Represents many kinds of relationships."""
tagname = "Relationship"
Type = String()
Target = String()
target = Alias("Target")
TargetMode = String(allow_none=True)
Id = String(allow_none=True)
id = Alias("Id")
def __init__(self,
Id=None,
Type=None,
type=None,
Target=None,
TargetMode=None
):
"""
`type` can be used as a shorthand with the default relationships namespace
otherwise the `Type` must be a fully qualified URL
"""
if type is not None:
Type = "{0}/{1}".format(REL_NS, type)
self.Type = Type
self.Target = Target
self.TargetMode = TargetMode
self.Id = Id
[docs]
class RelationshipList(ElementList):
tagname = "Relationships"
expected_type = Relationship
[docs]
def append(self, value):
super().append(value)
if not value.Id:
value.Id = f"rId{len(self)}"
[docs]
def find(self, content_type):
"""
Find relationships by content-type
NB. these content-types namespaced objects and different to the MIME-types
in the package manifest :-(
"""
for r in self:
if r.Type == content_type:
yield r
[docs]
def get(self, key):
for r in self:
if r.Id == key:
return r
raise KeyError("Unknown relationship: {0}".format(key))
[docs]
def to_dict(self):
"""Return a dictionary of relations keyed by id"""
return {r.id:r for r in self}
[docs]
def to_tree(self):
tree = super().to_tree()
tree.set("xmlns", PKG_REL_NS)
return tree
[docs]
def get_rels_path(path):
"""
Convert relative path to absolutes that can be loaded from a zip
archive.
The path to be passed in is that of containing object (workbook,
worksheet, etc.)
"""
folder, obj = posixpath.split(path)
filename = posixpath.join(folder, '_rels', '{0}.rels'.format(obj))
return filename
[docs]
def get_dependents(archive, filename):
"""
Normalise dependency file paths to absolute ones
Relative paths are relative to parent object
"""
src = archive.read(filename)
node = fromstring(src)
try:
rels = RelationshipList.from_tree(node)
except TypeError:
msg = "{0} contains invalid dependency definitions".format(filename)
warn(msg)
rels = RelationshipList()
folder = posixpath.dirname(filename)
parent = posixpath.split(folder)[0]
for r in rels:
if r.TargetMode == "External":
continue
elif r.target.startswith("/"):
r.target = r.target[1:]
else:
pth = posixpath.join(parent, r.target)
r.target = posixpath.normpath(pth)
return rels
[docs]
def get_rel(archive, deps, id=None, cls=None):
"""
Get related object based on id or rel_type
"""
if not any([id, cls]):
raise ValueError("Either the id or the content type are required")
if id is not None:
rel = deps.get(id)
else:
try:
rel = next(deps.find(cls.rel_type))
except StopIteration: # no known dependency
return
path = rel.target
src = archive.read(path)
tree = fromstring(src)
obj = cls.from_tree(tree)
rels_path = get_rels_path(path)
try:
obj.deps = get_dependents(archive, rels_path)
except KeyError:
obj.deps = []
return obj