1# Copyright (c) 2010-2024 openpyxl
2
3import posixpath
4from warnings import warn
5
6from openpyxl.descriptors import (
7 String,
8 Alias,
9 Sequence,
10)
11from openpyxl.descriptors.serialisable import Serialisable
12from openpyxl.descriptors.container import ElementList
13
14from openpyxl.xml.constants import REL_NS, PKG_REL_NS
15from openpyxl.xml.functions import (
16 Element,
17 fromstring,
18)
19
20
21class Relationship(Serialisable):
22 """Represents many kinds of relationships."""
23
24 tagname = "Relationship"
25
26 Type = String()
27 Target = String()
28 target = Alias("Target")
29 TargetMode = String(allow_none=True)
30 Id = String(allow_none=True)
31 id = Alias("Id")
32
33
34 def __init__(self,
35 Id=None,
36 Type=None,
37 type=None,
38 Target=None,
39 TargetMode=None
40 ):
41 """
42 `type` can be used as a shorthand with the default relationships namespace
43 otherwise the `Type` must be a fully qualified URL
44 """
45 if type is not None:
46 Type = "{0}/{1}".format(REL_NS, type)
47 self.Type = Type
48 self.Target = Target
49 self.TargetMode = TargetMode
50 self.Id = Id
51
52
53class RelationshipList(ElementList):
54
55 tagname = "Relationships"
56 expected_type = Relationship
57
58
59 def append(self, value):
60 super().append(value)
61 if not value.Id:
62 value.Id = f"rId{len(self)}"
63
64
65 def find(self, content_type):
66 """
67 Find relationships by content-type
68 NB. these content-types namespaced objects and different to the MIME-types
69 in the package manifest :-(
70 """
71 for r in self:
72 if r.Type == content_type:
73 yield r
74
75
76 def get(self, key):
77 for r in self:
78 if r.Id == key:
79 return r
80 raise KeyError("Unknown relationship: {0}".format(key))
81
82
83 def to_dict(self):
84 """Return a dictionary of relations keyed by id"""
85 return {r.id:r for r in self}
86
87
88 def to_tree(self):
89 tree = super().to_tree()
90 tree.set("xmlns", PKG_REL_NS)
91 return tree
92
93
94def get_rels_path(path):
95 """
96 Convert relative path to absolutes that can be loaded from a zip
97 archive.
98 The path to be passed in is that of containing object (workbook,
99 worksheet, etc.)
100 """
101 folder, obj = posixpath.split(path)
102 filename = posixpath.join(folder, '_rels', '{0}.rels'.format(obj))
103 return filename
104
105
106def get_dependents(archive, filename):
107 """
108 Normalise dependency file paths to absolute ones
109
110 Relative paths are relative to parent object
111 """
112 src = archive.read(filename)
113 node = fromstring(src)
114 try:
115 rels = RelationshipList.from_tree(node)
116 except TypeError:
117 msg = "{0} contains invalid dependency definitions".format(filename)
118 warn(msg)
119 rels = RelationshipList()
120 folder = posixpath.dirname(filename)
121 parent = posixpath.split(folder)[0]
122 for r in rels:
123 if r.TargetMode == "External":
124 continue
125 elif r.target.startswith("/"):
126 r.target = r.target[1:]
127 else:
128 pth = posixpath.join(parent, r.target)
129 r.target = posixpath.normpath(pth)
130 return rels
131
132
133def get_rel(archive, deps, id=None, cls=None):
134 """
135 Get related object based on id or rel_type
136 """
137 if not any([id, cls]):
138 raise ValueError("Either the id or the content type are required")
139 if id is not None:
140 rel = deps.get(id)
141 else:
142 try:
143 rel = next(deps.find(cls.rel_type))
144 except StopIteration: # no known dependency
145 return
146
147 path = rel.target
148 src = archive.read(path)
149 tree = fromstring(src)
150 obj = cls.from_tree(tree)
151
152 rels_path = get_rels_path(path)
153 try:
154 obj.deps = get_dependents(archive, rels_path)
155 except KeyError:
156 obj.deps = []
157
158 return obj