source: ftputil/stat_cache.py @ 1564:c5b353a1c23d

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