File: threading.py

package info (click to toggle)
python-flanker 0.9.15-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 17,976 kB
  • sloc: python: 9,308; makefile: 4
file content (204 lines) | stat: -rw-r--r-- 5,587 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
"""
Implements message threading
"""
import six

from flanker import _email


def build_thread(messages):
    """
    Groups given message list by conversation
    returns tree with root linked to all messages
    """
    thread = build_root_set(
        build_table(messages))
    thread.prune_empty()
    return thread


def build_table(messages):
    id_table = {}
    for message in messages:
        map_message(message, id_table)
    return id_table


def build_root_set(table):
    root = Container()
    for container in six.itervalues(table):
        if not container.parent:
            root.add_child(container)
    return root


def map_message(message, table):

    def container(message_id):
        return table.setdefault(message_id, Container())

    w = Wrapper(message)
    this = container(w.message_id)

    # process case when we have two messages
    # with the same id, we should put
    # our current message to another container
    # otherwise the message would be lost
    if this.message:
        fake_id = _email.make_message_id()
        this = container(fake_id)
    this.message = w

    # link message parents together
    prev = None
    for parent_id in w.references:
        parent = container(parent_id)
        if prev and not parent.parent and not introduces_loop(prev, parent):
            prev.add_child(parent)
        prev = parent

    # process case where this message has parent
    # unlink the old parent in this case
    if this.parent:
        this.parent.remove_child(this)

    # link to the cool parent instead
    if prev and not introduces_loop(prev, this):
        prev.add_child(this)


def introduces_loop(parent, child):
    return parent == child or child.has_descendant(parent)


class Container(object):
    def __init__(self, message=None):
        self.message = message
        self.parent = None
        self.child = None
        self.next = None
        self.prev = None

    def __str__(self):
        return self.message.message_id if self.message else "dummy"

    @property
    def is_dummy(self):
        return not self.message

    @property
    def in_root_set(self):
        return self.parent.parent is None

    @property
    def has_children(self):
        return self.child is not None

    @property
    def has_one_child(self):
        return self.child and self.child.next is None

    @property
    def last_child(self):
        child = self.child
        while child and child.next:
            child = child.next
        return child

    def iter_children(self):
        child = self.child
        while child:
            yield child
            child = child.next

    def has_descendant(self, container):
        child = self.child
        while child:
            if child == container or child.has_descendant(container):
                return True
            child = child.next
        return False

    def add_child(self, container):
        """
        Inserts child in front of list of children
        """
        if self.child:
            container.next = self.child
            self.child.prev = container

        self.child = container
        self.child.parent = self
        self.child.prev = None

    def remove_child(self, container):
        """
        """
        if container.parent != self:
            raise Exception("Operation on child when I'm not parent!")
        if not container.prev:
            self.child = container.next
            if self.child:
                self.child.prev = None
        else:
            container.prev.next = container.next
            if container.next:
                container.next.prev = container.prev
        container.parent = None
        container.prev = None
        container.next = None

    def replace_with_its_children(self, container):
        """
        Replaces container with its children.
        """
        if not container.has_children:
            return

        for c in container.iter_children():
            c.parent = self

        if not container.prev:
            self.child = container.child
        else:
            container.prev.next = container.child
            container.child.prev = container.prev

        if container.next:
            last_child = container.last_child
            container.next.prev = last_child
            last_child.next = container.next

    def prune_empty(self):
        """
        Removes child containers
        that don't have messages inside.
        """
        container = self.child
        while container:
            if container.is_dummy and not container.has_children:
                next_ = container.next
                self.remove_child(container)
                container = next_
            elif container.is_dummy \
                 and container.has_children \
                 and (not container.in_root_set or container.has_one_child):
                # remove container from the list
                # replacing it with it's children
                next_ = container.child
                self.replace_with_its_children(container)
                container.parent = None
                container.child = None
                container = next_
            elif container.has_children:
                container.prune_empty()
                container = container.next
            else:
                container = container.next


class Wrapper(object):
    def __init__(self, message):
        self.message = message
        self.message_id = message.message_id or _email.make_message_id()
        self.references = message.references