Development of an internal social media platform with personalised dashboards for students
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.

tree.py 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """
  2. A class for storing a tree graph. Primarily used for filter constructs in the
  3. ORM.
  4. """
  5. import copy
  6. class Node:
  7. """
  8. A single internal node in the tree graph. A Node should be viewed as a
  9. connection (the root) with the children being either leaf nodes or other
  10. Node instances.
  11. """
  12. # Standard connector type. Clients usually won't use this at all and
  13. # subclasses will usually override the value.
  14. default = 'DEFAULT'
  15. def __init__(self, children=None, connector=None, negated=False):
  16. """Construct a new Node. If no connector is given, use the default."""
  17. self.children = children[:] if children else []
  18. self.connector = connector or self.default
  19. self.negated = negated
  20. # Required because django.db.models.query_utils.Q. Q. __init__() is
  21. # problematic, but it is a natural Node subclass in all other respects.
  22. @classmethod
  23. def _new_instance(cls, children=None, connector=None, negated=False):
  24. """
  25. Create a new instance of this class when new Nodes (or subclasses) are
  26. needed in the internal code in this class. Normally, it just shadows
  27. __init__(). However, subclasses with an __init__ signature that aren't
  28. an extension of Node.__init__ might need to implement this method to
  29. allow a Node to create a new instance of them (if they have any extra
  30. setting up to do).
  31. """
  32. obj = Node(children, connector, negated)
  33. obj.__class__ = cls
  34. return obj
  35. def __str__(self):
  36. template = '(NOT (%s: %s))' if self.negated else '(%s: %s)'
  37. return template % (self.connector, ', '.join(str(c) for c in self.children))
  38. def __repr__(self):
  39. return "<%s: %s>" % (self.__class__.__name__, self)
  40. def __deepcopy__(self, memodict):
  41. obj = Node(connector=self.connector, negated=self.negated)
  42. obj.__class__ = self.__class__
  43. obj.children = copy.deepcopy(self.children, memodict)
  44. return obj
  45. def __len__(self):
  46. """Return the number of children this node has."""
  47. return len(self.children)
  48. def __bool__(self):
  49. """Return whether or not this node has children."""
  50. return bool(self.children)
  51. def __contains__(self, other):
  52. """Return True if 'other' is a direct child of this instance."""
  53. return other in self.children
  54. def __eq__(self, other):
  55. return (
  56. self.__class__ == other.__class__ and
  57. (self.connector, self.negated) == (other.connector, other.negated) and
  58. self.children == other.children
  59. )
  60. def __hash__(self):
  61. return hash((self.__class__, self.connector, self.negated) + tuple([
  62. tuple(child) if isinstance(child, list) else child
  63. for child in self.children
  64. ]))
  65. def add(self, data, conn_type, squash=True):
  66. """
  67. Combine this tree and the data represented by data using the
  68. connector conn_type. The combine is done by squashing the node other
  69. away if possible.
  70. This tree (self) will never be pushed to a child node of the
  71. combined tree, nor will the connector or negated properties change.
  72. Return a node which can be used in place of data regardless if the
  73. node other got squashed or not.
  74. If `squash` is False the data is prepared and added as a child to
  75. this tree without further logic.
  76. """
  77. if data in self.children:
  78. return data
  79. if not squash:
  80. self.children.append(data)
  81. return data
  82. if self.connector == conn_type:
  83. # We can reuse self.children to append or squash the node other.
  84. if (isinstance(data, Node) and not data.negated and
  85. (data.connector == conn_type or len(data) == 1)):
  86. # We can squash the other node's children directly into this
  87. # node. We are just doing (AB)(CD) == (ABCD) here, with the
  88. # addition that if the length of the other node is 1 the
  89. # connector doesn't matter. However, for the len(self) == 1
  90. # case we don't want to do the squashing, as it would alter
  91. # self.connector.
  92. self.children.extend(data.children)
  93. return self
  94. else:
  95. # We could use perhaps additional logic here to see if some
  96. # children could be used for pushdown here.
  97. self.children.append(data)
  98. return data
  99. else:
  100. obj = self._new_instance(self.children, self.connector,
  101. self.negated)
  102. self.connector = conn_type
  103. self.children = [obj, data]
  104. return data
  105. def negate(self):
  106. """Negate the sense of the root connector."""
  107. self.negated = not self.negated