Passed
Push — main ( b312d0...29adcf )
by Douglas
01:37
created

pocketutils.misc.mem_cache   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 159
dl 0
loc 207
rs 8.72
c 0
b 0
f 0
wmc 46

28 Methods

Rating   Name   Duplication   Size   Complexity  
A MemCachePolicy.items() 0 3 1
A MemoryLimitingPolicy.__init__() 0 11 1
A MemoryLruPolicy.items() 0 2 1
A MemCache.__repr__() 0 2 1
A MemCache.__getitem__() 0 11 2
A MemCache.__init__() 0 7 3
A MemoryLimitingPolicy.removed() 0 5 1
A MemoryLimitingPolicy.__str__() 0 20 4
A MemCachePolicy.removed() 0 2 1
A MemoryLimitingPolicy.__repr__() 0 13 4
A MemCache.__str__() 0 2 1
A MemCache.__call__() 0 2 1
A MemCache.__delitem__() 0 2 1
A MemCache.__contains__() 0 2 1
A MemCachePolicy.added() 0 2 1
A MemoryLimitingPolicy.accessed() 0 2 1
A MemCachePolicy.accessed() 0 2 1
A MemCachePolicy.should_archive() 0 2 1
A MemoryLimitingPolicy.added() 0 6 1
A MemoryLimitingPolicy.reindex() 0 7 4
A MemoryLimitingPolicy.can_archive() 0 2 1
A MemCache.remove() 0 4 2
A MemCache.clear() 0 9 2
A MemCachePolicy.reindex() 0 2 1
A MemCachePolicy.can_archive() 0 2 1
A MemoryMruPolicy.items() 0 3 1
A MemCache.archive() 0 12 5
A MemoryLimitingPolicy.should_archive() 0 7 1

How to fix   Complexity   

Complexity

Complex classes like pocketutils.misc.mem_cache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import operator
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import sys
3
from abc import ABC
4
from datetime import datetime
5
from typing import Any, Callable, Dict, Generic, Iterator, Mapping, Optional, Sequence, TypeVar
6
7
from psutil import virtual_memory
0 ignored issues
show
introduced by
Unable to import 'psutil'
Loading history...
8
9
# noinspection PyProtectedMember
10
from pocketutils.core._internal import nicesize
11
12
K = TypeVar("K", covariant=True)
0 ignored issues
show
Coding Style Naming introduced by
Class name "K" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
13
V = TypeVar("V", contravariant=True)
0 ignored issues
show
Coding Style Naming introduced by
Class name "V" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
14
15
16
class MemCachePolicy(Generic[K, V]):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
17
    def should_archive(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
18
        raise NotImplementedError()
19
20
    def can_archive(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
21
        raise NotImplementedError()
22
23
    def reindex(self, items: Mapping[K, V]) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
24
        raise NotImplementedError()
25
26
    def items(self) -> Iterator[K]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
27
        # TODO this is not overloaded!!
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
28
        raise NotImplementedError()
29
30
    def accessed(self, key: K) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
31
        pass
32
33
    def added(self, key: K, value: V) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
34
        pass
35
36
    def removed(self, key: K) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
37
        pass
38
39
40
class MemoryLimitingPolicy(MemCachePolicy, ABC):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
41
    def __init__(
42
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
43
        max_memory_bytes: Optional[int] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
44
        max_fraction_available_bytes: Optional[float] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
45
    ):
46
        self._max_memory_bytes = max_memory_bytes
47
        self._max_fraction_available_bytes = max_fraction_available_bytes
48
        self._total_memory_bytes = 0
49
        self._usage_bytes = {}  # type: Dict[K, int]
50
        self._last_accessed = {}  # type: Dict[K, datetime]
51
        self._created = {}  # type: Dict[K, datetime]
52
53
    def can_archive(self) -> bool:
54
        return len(self._last_accessed) > 0
55
56
    def should_archive(self) -> bool:
57
        return (
58
            self._max_memory_bytes is not None and self._total_memory_bytes > self._max_memory_bytes
59
        ) or (
60
            self._max_fraction_available_bytes is not None
61
            and self._total_memory_bytes
62
            > virtual_memory().available * self._max_fraction_available_bytes
63
        )
64
65
    def reindex(self, items: Mapping[K, V]) -> None:
66
        for key in set(self._last_accessed.keys()) - set(items.keys()):
67
            if key not in items.keys():
68
                self.removed(key)
69
        for key, value in items.items():
70
            self._usage_bytes[key] = sys.getsizeof(value)
71
        self._total_memory_bytes = sum(self._usage_bytes.values())
72
73
    def accessed(self, key: K) -> None:
74
        self._last_accessed[key] = datetime.now()
75
76
    def added(self, key: K, value: V) -> None:
77
        now = datetime.now()
78
        self._created[key] = now
79
        self._last_accessed[key] = now
80
        self._usage_bytes[key] = sys.getsizeof(value)
81
        self._total_memory_bytes += self._usage_bytes[key]
82
83
    def removed(self, key: K) -> None:
84
        self._total_memory_bytes -= self._usage_bytes[key]
85
        del self._last_accessed[key]
86
        del self._created[key]
87
        del self._usage_bytes[key]
88
89
    def __str__(self):
90
        available = virtual_memory().available
91
        name = self.__class__.__name__
92
        n_items = len(self._usage_bytes)
93
        real = nicesize(self._total_memory_bytes)
94
        cap_raw = "-" if self._max_memory_bytes is None else nicesize(self._max_memory_bytes)
95
        cap_percent = (
96
            "-"
97
            if self._max_fraction_available_bytes is None
98
            else nicesize(available * self._max_fraction_available_bytes)
99
        )
100
        percent = (
101
            "-"
102
            if self._max_fraction_available_bytes is None
103
            else round(
104
                100 * self._total_memory_bytes / (available * self._max_fraction_available_bytes),
105
                3,
106
            )
107
        )
108
        return f"{name}(n={n_items}, {real}/{cap_raw}, {real}/{cap_percent}={percent}%"
109
110
    def __repr__(self):
111
        ordered = list(self.items())
112
        ss = []
0 ignored issues
show
Coding Style Naming introduced by
Variable name "ss" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
113
        current_day = None
114
        for k in ordered:
115
            dt = self._last_accessed[k]
0 ignored issues
show
Coding Style Naming introduced by
Variable name "dt" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
116
            if current_day is None or current_day.date() != dt.date():
117
                current_day = dt
118
                ss.append("#" + current_day.strftime("%Y-%m-%d") + "...")
119
            nice = nicesize(self._usage_bytes[k])
120
            access = self._last_accessed[k].strftime("%H:%M:%S")
121
            ss.append(f"{k}:{nice}@{access}")
122
        return f"{str(self)}@{hex(id(self))}: [{', '.join(ss)}]"
123
124
125
class MemoryLruPolicy(MemoryLimitingPolicy):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
126
    def items(self) -> Iterator[K]:
127
        return iter([k for k, v in sorted(self._last_accessed.items(), key=operator.itemgetter(1))])
128
129
130
class MemoryMruPolicy(MemoryLimitingPolicy):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
131
    def items(self) -> Iterator[K]:
132
        rev = reversed(sorted(self._last_accessed.items(), key=operator.itemgetter(1)))
133
        return iter([k for k, v in rev])
134
135
136
class MemCache(Generic[K, V]):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
137
    def __init__(
138
        self, loader: Callable[[K], V], policy: MemCachePolicy, *, log: Callable[[str], Any]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
139
    ):
140
        self._loader = loader
141
        self._items = {}  # type: Dict[K, V]
142
        self._policy = policy
143
        self._log = lambda _: None if log is None else log
144
145
    def __getitem__(self, key: K) -> V:
146
        self._policy.accessed(key)
147
        if key in self._items:
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
148
            return self._items[key]
149
        else:
150
            value = self._loader(key)
151
            self._log(f"Loaded {key}")
152
            self._items[key] = value
153
            self._policy.added(key, value)
154
            self.archive()
155
            return value
156
157
    def __call__(self, key: K) -> V:
158
        return self[key]
159
160
    def archive(self, at_least: Optional[int] = None) -> Sequence[K]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
161
        it = self._policy.items()
0 ignored issues
show
Coding Style Naming introduced by
Variable name "it" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
162
        archived = []
163
        while self._policy.can_archive() and (
164
            at_least is not None and len(archived) < at_least or self._policy.should_archive()
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
165
        ):
166
            key = next(it)
167
            self._policy.removed(key)
168
            del self._items[key]
169
            archived.append(key)
170
            self._log(f"Archived {len(archived)} items: {archived}")
171
        return archived
172
173
    def clear(self) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
174
        it = self._policy.items()
0 ignored issues
show
Coding Style Naming introduced by
Variable name "it" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
175
        cleared = []
176
        while self._policy.can_archive():
177
            key = next(it)
178
            self._policy.removed(key)
179
            del self._items[key]
180
            cleared.append(key)
181
        self._log(f"Cleared {len(cleared)} items: {cleared}")
182
183
    def remove(self, key: K) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
184
        if key in self:
185
            self._policy.removed(key)
186
            del self._items[key]
187
188
    def __contains__(self, key: K):
189
        return key in self._items
190
191
    def __delitem__(self, key):
192
        self.remove(key)
193
194
    def __repr__(self):
195
        return str(self) + "@" + hex(id(self))
196
197
    def __str__(self):
198
        return f"{self.__class__.__name__}({repr(self._policy)})"
199
200
201
__all__ = [
202
    "MemCachePolicy",
203
    "MemCache",
204
    "MemoryLimitingPolicy",
205
    "MemoryLruPolicy",
206
    "MemoryMruPolicy",
207
]
208