relationship.py 4.3 KB

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