New upstream version 2.6.1
Pierre-Elliott Bécue
4 years ago
1 | 1 | ========= |
2 | 2 | |
3 | 3 | The full list of changes between each Python LiveReload release. |
4 | ||
5 | Version 2.6.1 | |
6 | ------------- | |
7 | ||
8 | Released on May 7, 2019 | |
9 | ||
10 | 1. Fixed bugs | |
4 | 11 | |
5 | 12 | Version 2.6.0 |
6 | 13 | ------------- |
7 | 7 | :license: BSD, see LICENSE for more details. |
8 | 8 | """ |
9 | 9 | |
10 | __version__ = '2.6.0' | |
10 | __version__ = '2.6.1' | |
11 | 11 | __author__ = 'Hsiaoming Yang <me@lepture.com>' |
12 | 12 | __homepage__ = 'https://github.com/lepture/python-livereload' |
13 | 13 |
7 | 7 | :copyright: (c) 2013 by Hsiaoming Yang |
8 | 8 | :license: BSD, see LICENSE for more details. |
9 | 9 | """ |
10 | ||
10 | import datetime | |
11 | import hashlib | |
11 | 12 | import os |
13 | import stat | |
12 | 14 | import time |
13 | 15 | import logging |
14 | 16 | from tornado import web |
15 | 17 | from tornado import ioloop |
16 | 18 | from tornado import escape |
19 | from tornado.log import gen_log | |
17 | 20 | from tornado.websocket import WebSocketHandler |
18 | 21 | from tornado.util import ObjectDict |
19 | 22 | |
132 | 135 | LiveReloadHandler.waiters.add(self) |
133 | 136 | |
134 | 137 | |
138 | class MtimeStaticFileHandler(web.StaticFileHandler): | |
139 | _static_mtimes = {} # type: typing.Dict | |
140 | ||
141 | @classmethod | |
142 | def get_content_modified_time(cls, abspath): | |
143 | """Returns the time that ``abspath`` was last modified. | |
144 | ||
145 | May be overridden in subclasses. Should return a `~datetime.datetime` | |
146 | object or None. | |
147 | """ | |
148 | stat_result = os.stat(abspath) | |
149 | modified = datetime.datetime.utcfromtimestamp( | |
150 | stat_result[stat.ST_MTIME]) | |
151 | return modified | |
152 | ||
153 | @classmethod | |
154 | def get_content_version(cls, abspath): | |
155 | """Returns a version string for the resource at the given path. | |
156 | ||
157 | This class method may be overridden by subclasses. The | |
158 | default implementation is a hash of the file's contents. | |
159 | ||
160 | .. versionadded:: 3.1 | |
161 | """ | |
162 | data = cls.get_content(abspath) | |
163 | hasher = hashlib.md5() | |
164 | ||
165 | mtime_data = format(cls.get_content_modified_time(abspath), "%Y-%m-%d %H:%M:%S") | |
166 | ||
167 | hasher.update(mtime_data.encode()) | |
168 | ||
169 | if isinstance(data, bytes): | |
170 | hasher.update(data) | |
171 | else: | |
172 | for chunk in data: | |
173 | hasher.update(chunk) | |
174 | return hasher.hexdigest() | |
175 | ||
176 | @classmethod | |
177 | def _get_cached_version(cls, abs_path): | |
178 | def _load_version(abs_path): | |
179 | try: | |
180 | hsh = cls.get_content_version(abs_path) | |
181 | mtm = cls.get_content_modified_time(abs_path) | |
182 | ||
183 | return mtm, hsh | |
184 | except Exception: | |
185 | gen_log.error("Could not open static file %r", abs_path) | |
186 | return None, None | |
187 | ||
188 | with cls._lock: | |
189 | hashes = cls._static_hashes | |
190 | mtimes = cls._static_mtimes | |
191 | ||
192 | if abs_path not in hashes: | |
193 | mtm, hsh = _load_version(abs_path) | |
194 | ||
195 | hashes[abs_path] = mtm | |
196 | mtimes[abs_path] = hsh | |
197 | else: | |
198 | hsh = hashes.get(abs_path) | |
199 | mtm = mtimes.get(abs_path) | |
200 | ||
201 | if mtm != cls.get_content_modified_time(abs_path): | |
202 | mtm, hsh = _load_version(abs_path) | |
203 | ||
204 | hashes[abs_path] = mtm | |
205 | mtimes[abs_path] = hsh | |
206 | ||
207 | if hsh: | |
208 | return hsh | |
209 | return None | |
210 | ||
211 | ||
135 | 212 | class LiveReloadJSHandler(web.RequestHandler): |
136 | 213 | |
137 | 214 | def get(self): |
149 | 226 | self.write('ok') |
150 | 227 | |
151 | 228 | |
152 | class StaticFileHandler(web.StaticFileHandler): | |
229 | class StaticFileHandler(MtimeStaticFileHandler): | |
153 | 230 | def should_return_304(self): |
154 | 231 | return False |
203 | 203 | if isinstance(func, string_types): |
204 | 204 | cmd = func |
205 | 205 | func = shell(func) |
206 | func.repr_str = "shell: {}".format(cmd) | |
207 | elif func: | |
208 | func.repr_str = str(func) | |
206 | func.name = "shell: {}".format(cmd) | |
209 | 207 | |
210 | 208 | self.watcher.watch(filepath, func, delay, ignore=ignore) |
211 | 209 |
22 | 22 | |
23 | 23 | |
24 | 24 | class Watcher(object): |
25 | """A file watcher registery.""" | |
25 | """A file watcher registry.""" | |
26 | 26 | def __init__(self): |
27 | 27 | self._tasks = {} |
28 | self._mtimes = {} | |
28 | ||
29 | # modification time of filepaths for each task, | |
30 | # before and after checking for changes | |
31 | self._task_mtimes = {} | |
32 | self._new_mtimes = {} | |
29 | 33 | |
30 | 34 | # setting changes |
31 | 35 | self._changes = [] |
34 | 38 | self.filepath = None |
35 | 39 | self._start = time.time() |
36 | 40 | |
37 | #list of ignored dirs | |
41 | # list of ignored dirs | |
38 | 42 | self.ignored_dirs = ['.git', '.hg', '.svn', '.cvs'] |
39 | 43 | |
40 | 44 | def ignore_dirs(self, *args): |
64 | 68 | 'func': func, |
65 | 69 | 'delay': delay, |
66 | 70 | 'ignore': ignore, |
71 | 'mtimes': {}, | |
67 | 72 | } |
68 | 73 | |
69 | 74 | def start(self, callback): |
72 | 77 | return False |
73 | 78 | |
74 | 79 | def examine(self): |
75 | """Check if there are changes, if true, run the given task.""" | |
80 | """Check if there are changes. If so, run the given task. | |
81 | ||
82 | Returns a tuple of modified filepath and reload delay. | |
83 | """ | |
76 | 84 | if self._changes: |
77 | 85 | return self._changes.pop() |
78 | 86 | |
81 | 89 | delays = set() |
82 | 90 | for path in self._tasks: |
83 | 91 | item = self._tasks[path] |
92 | self._task_mtimes = item['mtimes'] | |
84 | 93 | if self.is_changed(path, item['ignore']): |
85 | 94 | func = item['func'] |
86 | 95 | delay = item['delay'] |
87 | 96 | if delay and isinstance(delay, float): |
88 | 97 | delays.add(delay) |
89 | 98 | if func: |
90 | logger.info("Running task: {} (delay: {})".format( | |
91 | func.repr_str, delay)) | |
99 | name = getattr(func, 'name', None) | |
100 | if not name: | |
101 | name = getattr(func, '__name__', 'anonymous') | |
102 | logger.info( | |
103 | "Running task: {} (delay: {})".format(name, delay)) | |
92 | 104 | func() |
93 | 105 | |
94 | 106 | if delays: |
98 | 110 | return self.filepath, delay |
99 | 111 | |
100 | 112 | def is_changed(self, path, ignore=None): |
113 | """Check if any filepaths have been added, modified, or removed. | |
114 | ||
115 | Updates filepath modification times in self._task_mtimes. | |
116 | """ | |
117 | self._new_mtimes = {} | |
118 | changed = False | |
119 | ||
101 | 120 | if os.path.isfile(path): |
102 | return self.is_file_changed(path, ignore) | |
121 | changed = self.is_file_changed(path, ignore) | |
103 | 122 | elif os.path.isdir(path): |
104 | return self.is_folder_changed(path, ignore) | |
105 | return self.is_glob_changed(path, ignore) | |
123 | changed = self.is_folder_changed(path, ignore) | |
124 | else: | |
125 | changed = self.is_glob_changed(path, ignore) | |
126 | ||
127 | if not changed: | |
128 | changed = self.is_file_removed() | |
129 | ||
130 | self._task_mtimes.update(self._new_mtimes) | |
131 | return changed | |
132 | ||
133 | def is_file_removed(self): | |
134 | """Check if any filepaths have been removed since last check. | |
135 | ||
136 | Deletes removed paths from self._task_mtimes. | |
137 | Sets self.filepath to one of the removed paths. | |
138 | """ | |
139 | removed_paths = set(self._task_mtimes) - set(self._new_mtimes) | |
140 | if not removed_paths: | |
141 | return False | |
142 | ||
143 | for path in removed_paths: | |
144 | self._task_mtimes.pop(path) | |
145 | # self.filepath seems purely informational, so setting one | |
146 | # of several removed files seems sufficient | |
147 | self.filepath = path | |
148 | return True | |
106 | 149 | |
107 | 150 | def is_file_changed(self, path, ignore=None): |
151 | """Check if filepath has been added or modified since last check. | |
152 | ||
153 | Updates filepath modification times in self._new_mtimes. | |
154 | Sets self.filepath to changed path. | |
155 | """ | |
108 | 156 | if not os.path.isfile(path): |
109 | 157 | return False |
110 | 158 | |
116 | 164 | |
117 | 165 | mtime = os.path.getmtime(path) |
118 | 166 | |
119 | if path not in self._mtimes: | |
120 | self._mtimes[path] = mtime | |
167 | if path not in self._task_mtimes: | |
168 | self._new_mtimes[path] = mtime | |
121 | 169 | self.filepath = path |
122 | 170 | return mtime > self._start |
123 | 171 | |
124 | if self._mtimes[path] != mtime: | |
125 | self._mtimes[path] = mtime | |
172 | if self._task_mtimes[path] != mtime: | |
173 | self._new_mtimes[path] = mtime | |
126 | 174 | self.filepath = path |
127 | 175 | return True |
128 | 176 | |
129 | self._mtimes[path] = mtime | |
177 | self._new_mtimes[path] = mtime | |
130 | 178 | return False |
131 | 179 | |
132 | 180 | def is_folder_changed(self, path, ignore=None): |
181 | """Check if directory path has any changed filepaths.""" | |
133 | 182 | for root, dirs, files in os.walk(path, followlinks=True): |
134 | 183 | for d in self.ignored_dirs: |
135 | 184 | if d in dirs: |
141 | 190 | return False |
142 | 191 | |
143 | 192 | def is_glob_changed(self, path, ignore=None): |
193 | """Check if glob path has any changed filepaths.""" | |
144 | 194 | for f in glob.glob(path): |
145 | 195 | if self.is_file_changed(f, ignore): |
146 | 196 | return True |
31 | 31 | assert watcher.is_changed(tmpdir) is False |
32 | 32 | |
33 | 33 | # sleep 1 second so that mtime will be different |
34 | # TODO: This doesn't seem necessary; test passes without it | |
34 | 35 | time.sleep(1) |
35 | 36 | |
36 | with open(os.path.join(tmpdir, 'foo'), 'w') as f: | |
37 | filepath = os.path.join(tmpdir, 'foo') | |
38 | ||
39 | with open(filepath, 'w') as f: | |
37 | 40 | f.write('') |
38 | 41 | |
42 | assert watcher.is_changed(tmpdir) | |
43 | assert watcher.is_changed(tmpdir) is False | |
44 | ||
45 | os.remove(filepath) | |
39 | 46 | assert watcher.is_changed(tmpdir) |
40 | 47 | assert watcher.is_changed(tmpdir) is False |
41 | 48 | |
44 | 51 | watcher.count = 0 |
45 | 52 | |
46 | 53 | # sleep 1 second so that mtime will be different |
54 | # TODO: This doesn't seem necessary; test passes without it | |
47 | 55 | time.sleep(1) |
48 | 56 | |
49 | 57 | filepath = os.path.join(tmpdir, 'foo') |
55 | 63 | |
56 | 64 | watcher.watch(filepath, add_count) |
57 | 65 | assert watcher.is_changed(filepath) |
66 | assert watcher.is_changed(filepath) is False | |
58 | 67 | |
59 | 68 | # sleep 1 second so that mtime will be different |
69 | # TODO: This doesn't seem necessary; test passes without it | |
60 | 70 | time.sleep(1) |
61 | 71 | |
62 | 72 | with open(filepath, 'w') as f: |
63 | 73 | f.write('') |
64 | 74 | |
65 | rv = watcher.examine() | |
66 | assert rv[0] == os.path.abspath(filepath) | |
75 | abs_filepath = os.path.abspath(filepath) | |
76 | assert watcher.examine() == (abs_filepath, None) | |
77 | assert watcher.examine() == (None, None) | |
67 | 78 | assert watcher.count == 1 |
79 | ||
80 | os.remove(filepath) | |
81 | assert watcher.examine() == (abs_filepath, None) | |
82 | assert watcher.examine() == (None, None) | |
83 | assert watcher.count == 2 | |
68 | 84 | |
69 | 85 | def test_watch_glob(self): |
70 | 86 | watcher = Watcher() |
81 | 97 | with open(filepath, 'w') as f: |
82 | 98 | f.write('') |
83 | 99 | |
84 | rv = watcher.examine() | |
85 | assert rv[0] == os.path.abspath(filepath) | |
100 | abs_filepath = os.path.abspath(filepath) | |
101 | assert watcher.examine() == (abs_filepath, None) | |
102 | assert watcher.examine() == (None, None) | |
103 | ||
104 | os.remove(filepath) | |
105 | assert watcher.examine() == (abs_filepath, None) | |
106 | assert watcher.examine() == (None, None) | |
86 | 107 | |
87 | 108 | def test_watch_ignore(self): |
88 | 109 | watcher = Watcher() |
93 | 114 | f.write('') |
94 | 115 | |
95 | 116 | assert watcher.examine() == (None, None) |
117 | ||
118 | def test_watch_multiple_dirs(self): | |
119 | first_dir = os.path.join(tmpdir, 'first') | |
120 | second_dir = os.path.join(tmpdir, 'second') | |
121 | ||
122 | watcher = Watcher() | |
123 | ||
124 | os.mkdir(first_dir) | |
125 | watcher.watch(first_dir) | |
126 | assert watcher.examine() == (None, None) | |
127 | ||
128 | first_path = os.path.join(first_dir, 'foo') | |
129 | with open(first_path, 'w') as f: | |
130 | f.write('') | |
131 | assert watcher.examine() == (first_path, None) | |
132 | assert watcher.examine() == (None, None) | |
133 | ||
134 | os.mkdir(second_dir) | |
135 | watcher.watch(second_dir) | |
136 | assert watcher.examine() == (None, None) | |
137 | ||
138 | second_path = os.path.join(second_dir, 'bar') | |
139 | with open(second_path, 'w') as f: | |
140 | f.write('') | |
141 | assert watcher.examine() == (second_path, None) | |
142 | assert watcher.examine() == (None, None) | |
143 | ||
144 | with open(first_path, 'a') as f: | |
145 | f.write('foo') | |
146 | assert watcher.examine() == (first_path, None) | |
147 | assert watcher.examine() == (None, None) | |
148 | ||
149 | os.remove(second_path) | |
150 | assert watcher.examine() == (second_path, None) | |
151 | assert watcher.examine() == (None, None) |