relationship.py 3.9 KB

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