source: ftputil/stat_cache.py @ 1745:2402b8a75178

Last change on this file since 1745:2402b8a75178 was 1745:2402b8a75178, checked in by Stefan Schwarzer <sschwarzer@…>, 20 months ago
Unify formatting of XXX-style comments Use consistent format `# XXX: ...` for all FIXME, TODO and XXX comments.
File size: 5.5 KB
Line 
1# Copyright (C) 2006-2018, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# and ftputil contributors (see `doc/contributors.txt`)
3# See the file LICENSE for licensing terms.
4
5"""
6ftp_stat_cache.py - cache for (l)stat data
7"""
8
9import time
10
11import ftputil.error
12import ftputil.lrucache
13
14
15# This module shouldn't be used by clients of the ftputil library.
16__all__ = []
17
18
19class StatCache:
20    """
21    Implement an LRU (least-recently-used) cache.
22
23    `StatCache` objects have an attribute `max_age`. After this
24    duration after _setting_ it a cache entry will expire. For
25    example, if you code
26
27    my_cache = StatCache()
28    my_cache.max_age = 10
29    my_cache["/home"] = ...
30
31    the value my_cache["/home"] can be retrieved for 10 seconds. After
32    that, the entry will be treated as if it had never been in the
33    cache and should be fetched again from the remote host.
34
35    Note that the `__len__` method does no age tests and thus may
36    include some or many already expired entries.
37    """
38
39    # Default number of cache entries
40    _DEFAULT_CACHE_SIZE = 5000
41
42    def __init__(self):
43        # Can be reset with method `resize`
44        self._cache = ftputil.lrucache.LRUCache(self._DEFAULT_CACHE_SIZE)
45        # Never expire
46        self.max_age = None
47        self.enable()
48
49    def enable(self):
50        """Enable storage of stat results."""
51        self._enabled = True
52
53    def disable(self):
54        """
55        Disable the cache. Further storage attempts with `__setitem__`
56        won't have any visible effect.
57
58        Disabling the cache only effects new storage attempts. Values
59        stored before calling `disable` can still be retrieved unless
60        disturbed by a `resize` command or normal cache expiration.
61        """
62        # `_enabled` is set via calling `enable` in the constructor.
63        # pylint: disable=attribute-defined-outside-init
64        self._enabled = False
65
66    def resize(self, new_size):
67        """
68        Set number of cache entries to the integer `new_size`.
69        If the new size is smaller than the current cache size,
70        relatively long-unused elements will be removed.
71        """
72        self._cache.size = new_size
73
74    def _age(self, path):
75        """
76        Return the age of a cache entry for `path` in seconds. If
77        the path isn't in the cache, raise a `CacheMissError`.
78        """
79        try:
80            return time.time() - self._cache.mtime(path)
81        except ftputil.lrucache.CacheKeyError:
82            raise ftputil.error.CacheMissError(
83                    "no entry for path {} in cache".format(path))
84
85    def clear(self):
86        """Clear (invalidate) all cache entries."""
87        self._cache.clear()
88
89    def invalidate(self, path):
90        """
91        Invalidate the cache entry for the absolute `path` if present.
92        After that, the stat result data for `path` can no longer be
93        retrieved, as if it had never been stored.
94
95        If no stat result for `path` is in the cache, do _not_
96        raise an exception.
97        """
98        # XXX: To be 100 % sure, this should be `host.sep`, but I
99        # don't want to introduce a reference to the `FTPHost` object
100        # for only that purpose.
101        assert path.startswith("/"), ("{} must be an absolute path".
102                                      format(path))
103        try:
104            del self._cache[path]
105        except ftputil.lrucache.CacheKeyError:
106            # Ignore errors
107            pass
108
109    def __getitem__(self, path):
110        """
111        Return the stat entry for the `path`. If there's no stored
112        stat entry or the cache is disabled, raise `CacheMissError`.
113        """
114        if not self._enabled:
115            raise ftputil.error.CacheMissError("cache is disabled")
116        # Possibly raise a `CacheMissError` in `_age`
117        if (self.max_age is not None) and (self._age(path) > self.max_age):
118            self.invalidate(path)
119            raise ftputil.error.CacheMissError(
120                    "entry for path {} has expired".format(path))
121        else:
122            # XXX: I don't know if this may raise a `CacheMissError`
123            # in case of race conditions. I prefer robust code.
124            try:
125                return self._cache[path]
126            except ftputil.lrucache.CacheKeyError:
127                raise ftputil.error.CacheMissError(
128                        "entry for path {} not found".format(path))
129
130    def __setitem__(self, path, stat_result):
131        """
132        Put the stat data for the absolute `path` into the cache,
133        unless it's disabled.
134        """
135        assert path.startswith("/")
136        if not self._enabled:
137            return
138        self._cache[path] = stat_result
139
140    def __contains__(self, path):
141        """
142        Support for the `in` operator. Return a true value, if data
143        for `path` is in the cache, else return a false value.
144        """
145        try:
146            # Implicitly do an age test which may raise `CacheMissError`.
147            self[path]
148        except ftputil.error.CacheMissError:
149            return False
150        else:
151            return True
152
153    #
154    # The following methods are only intended for debugging!
155    #
156    def __len__(self):
157        """
158        Return the number of entries in the cache. Note that this
159        may include some (or many) expired entries.
160        """
161        return len(self._cache)
162
163    def __str__(self):
164        """Return a string representation of the cache contents."""
165        lines = []
166        for key in sorted(self._cache):
167            lines.append("{}: {}".format(key, self[key]))
168        return "\n".join(lines)
Note: See TracBrowser for help on using the repository browser.