Projet

Général

Profil

0001-utils-add-defer-module-to-run-things-later-31204.patch

Benjamin Dauvergne, 28 juillet 2022 15:33

Télécharger (6,61 ko)

Voir les différences:

Subject: [PATCH 1/3] utils: add defer module to run things later (#31204)

 passerelle/utils/defer.py |  91 +++++++++++++++++++++++++++
 tests/test_utils_defer.py | 126 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 217 insertions(+)
 create mode 100644 passerelle/utils/defer.py
 create mode 100644 tests/test_utils_defer.py
passerelle/utils/defer.py
1
# Copyright (C) 2012  Entr'ouvert
2
#
3
# This program is free software: you can redistribute it and/or modify it
4
# under the terms of the GNU Affero General Public License as published
5
# by the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU Affero General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

  
16

  
17
import functools
18
import logging
19
import threading
20

  
21
import django.db
22

  
23

  
24
class DeferrableScope(threading.local):
25
    @property
26
    def stack(self):
27
        if not hasattr(self, '_stack'):
28
            self._stack = []
29
        return self._stack
30

  
31
    def __push(self):
32
        self.stack.append([])
33

  
34
    def defer(self, func, *args, **kwargs):
35
        if self.should_defer:
36
            self.stack[-1].append((func, args, kwargs))
37
        else:
38
            return func(*args, **kwargs)
39

  
40
    def __pop(self):
41
        return self.stack.pop()
42

  
43
    def __enter__(self):
44
        self.__push()
45
        return self
46

  
47
    def __exit__(self, exc_type, exc_value, exc_tb):
48
        for func, args, kwargs in self.__pop():
49
            try:
50
                func(*args, **kwargs)
51
            except Exception:
52
                logging.exception('failed to run deferrable function %s', func)
53

  
54
    @property
55
    def should_defer(self):
56
        return bool(self.stack)
57

  
58
    def deferrable(self, func=None, predicate=None):
59
        '''Automatically defer a function if dynamic scope is inside a deferrable_scope.'''
60
        if not func:
61
            return functools.partial(self.deferrable, predicate=predicate)
62

  
63
        @functools.wraps(func)
64
        def f(*args, **kwargs):
65
            if not predicate or predicate():
66
                return self.defer(func, *args, **kwargs)
67
            else:
68
                return func(*args, **kwargs)
69

  
70
        return f
71

  
72
    def __call__(self, func):
73
        '''Wraps func in a deferrable_scope scope.'''
74

  
75
        @functools.wraps(func)
76
        def f(*args, **kwargs):
77
            with self:
78
                return func(*args, **kwargs)
79

  
80
        return f
81

  
82

  
83
deferrable_scope = DeferrableScope()
84

  
85

  
86
def is_in_transaction():
87
    return getattr(django.db.connection, 'in_atomic_block', False)
88

  
89

  
90
deferrable = deferrable_scope.deferrable
91
deferrable_if_in_transaction = deferrable_scope.deferrable(predicate=is_in_transaction)
tests/test_utils_defer.py
1
# Copyright (C) 2022  Entr'ouvert
2
#
3
# This program is free software: you can redistribute it and/or modify it
4
# under the terms of the GNU Affero General Public License as published
5
# by the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU Affero General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

  
16
import threading
17

  
18
import pytest
19
from django.db import transaction
20

  
21
from passerelle.utils import defer
22

  
23

  
24
def test_deferrable_scope():
25
    x = []
26

  
27
    def f():
28
        x.append(1)
29

  
30
    assert not defer.deferrable_scope.should_defer
31
    with defer.deferrable_scope:
32
        assert defer.deferrable_scope.should_defer
33
        defer.deferrable_scope.defer(f)
34
        assert x == []
35
    assert not defer.deferrable_scope.should_defer
36
    assert x == [1]
37

  
38

  
39
def test_deferrable_scope_with_threading():
40
    x = []
41

  
42
    def f():
43
        x.append(1)
44

  
45
    assert not defer.deferrable_scope.should_defer
46
    with defer.deferrable_scope:
47
        defer.deferrable_scope.defer(f)
48
        assert x == []
49
        t = threading.Thread(target=defer.deferrable_scope.defer, args=(f,))
50
        assert x == []
51
        t.start()
52
        t.join()
53
        assert x == [1]
54
    assert not defer.deferrable_scope.should_defer
55
    assert x == [1, 1]
56

  
57

  
58
def test_deferrable():
59
    x = []
60

  
61
    @defer.deferrable
62
    def f():
63
        x.append(1)
64

  
65
    f()
66
    assert x == [1]
67

  
68
    assert not defer.deferrable_scope.should_defer
69
    with defer.deferrable_scope:
70
        f()
71
        assert x == [1]
72
    assert not defer.deferrable_scope.should_defer
73
    assert x == [1, 1]
74

  
75

  
76
def test_deferrable_with_threading():
77
    x = []
78

  
79
    @defer.deferrable
80
    def f():
81
        x.append(1)
82

  
83
    f()
84
    assert x == [1]
85

  
86
    assert not defer.deferrable_scope.should_defer
87
    with defer.deferrable_scope:
88
        f()
89
        assert x == [1]
90
        t = threading.Thread(target=f)
91
        t.start()
92
        t.join()
93
        assert x == [1, 1]
94
    assert x == [1, 1, 1]
95
    assert not defer.deferrable_scope.should_defer
96

  
97

  
98
def test_deferrable_if_in_transaction(transactional_db):
99
    assert not defer.is_in_transaction()
100

  
101
    x = []
102

  
103
    @defer.deferrable_if_in_transaction
104
    def f():
105
        x.append(1)
106

  
107
    f()
108
    assert x == [1]
109

  
110
    with transaction.atomic():
111
        assert defer.is_in_transaction()
112
        f()
113
        assert x == [1, 1]
114

  
115
    with pytest.raises(Exception):
116
        with defer.deferrable_scope:
117
            f()
118
            assert x == [1, 1, 1]
119
            try:
120
                with transaction.atomic():
121
                    f()
122
                    assert x == [1, 1, 1]
123
                    raise Exception
124
            finally:
125
                assert x == [1, 1, 1]
126
    assert x == [1, 1, 1, 1]
0
-