Codebase list python-livereload / 25012f5
New upstream version 2.6.1 Pierre-Elliott Bécue 4 years ago
7 changed file(s) with 214 addition(s) and 25 deletion(s). Raw diff Collapse all Expand all
2020 .coverage
2121 .tox
2222 .env/
23 venv/
2324
2425 docs/_build
2526 example/style.css
11 =========
22
33 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
411
512 Version 2.6.0
613 -------------
77 :license: BSD, see LICENSE for more details.
88 """
99
10 __version__ = '2.6.0'
10 __version__ = '2.6.1'
1111 __author__ = 'Hsiaoming Yang <me@lepture.com>'
1212 __homepage__ = 'https://github.com/lepture/python-livereload'
1313
77 :copyright: (c) 2013 by Hsiaoming Yang
88 :license: BSD, see LICENSE for more details.
99 """
10
10 import datetime
11 import hashlib
1112 import os
13 import stat
1214 import time
1315 import logging
1416 from tornado import web
1517 from tornado import ioloop
1618 from tornado import escape
19 from tornado.log import gen_log
1720 from tornado.websocket import WebSocketHandler
1821 from tornado.util import ObjectDict
1922
132135 LiveReloadHandler.waiters.add(self)
133136
134137
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
135212 class LiveReloadJSHandler(web.RequestHandler):
136213
137214 def get(self):
149226 self.write('ok')
150227
151228
152 class StaticFileHandler(web.StaticFileHandler):
229 class StaticFileHandler(MtimeStaticFileHandler):
153230 def should_return_304(self):
154231 return False
203203 if isinstance(func, string_types):
204204 cmd = func
205205 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)
209207
210208 self.watcher.watch(filepath, func, delay, ignore=ignore)
211209
2222
2323
2424 class Watcher(object):
25 """A file watcher registery."""
25 """A file watcher registry."""
2626 def __init__(self):
2727 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 = {}
2933
3034 # setting changes
3135 self._changes = []
3438 self.filepath = None
3539 self._start = time.time()
3640
37 #list of ignored dirs
41 # list of ignored dirs
3842 self.ignored_dirs = ['.git', '.hg', '.svn', '.cvs']
3943
4044 def ignore_dirs(self, *args):
6468 'func': func,
6569 'delay': delay,
6670 'ignore': ignore,
71 'mtimes': {},
6772 }
6873
6974 def start(self, callback):
7277 return False
7378
7479 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 """
7684 if self._changes:
7785 return self._changes.pop()
7886
8189 delays = set()
8290 for path in self._tasks:
8391 item = self._tasks[path]
92 self._task_mtimes = item['mtimes']
8493 if self.is_changed(path, item['ignore']):
8594 func = item['func']
8695 delay = item['delay']
8796 if delay and isinstance(delay, float):
8897 delays.add(delay)
8998 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))
92104 func()
93105
94106 if delays:
98110 return self.filepath, delay
99111
100112 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
101120 if os.path.isfile(path):
102 return self.is_file_changed(path, ignore)
121 changed = self.is_file_changed(path, ignore)
103122 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
106149
107150 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 """
108156 if not os.path.isfile(path):
109157 return False
110158
116164
117165 mtime = os.path.getmtime(path)
118166
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
121169 self.filepath = path
122170 return mtime > self._start
123171
124 if self._mtimes[path] != mtime:
125 self._mtimes[path] = mtime
172 if self._task_mtimes[path] != mtime:
173 self._new_mtimes[path] = mtime
126174 self.filepath = path
127175 return True
128176
129 self._mtimes[path] = mtime
177 self._new_mtimes[path] = mtime
130178 return False
131179
132180 def is_folder_changed(self, path, ignore=None):
181 """Check if directory path has any changed filepaths."""
133182 for root, dirs, files in os.walk(path, followlinks=True):
134183 for d in self.ignored_dirs:
135184 if d in dirs:
141190 return False
142191
143192 def is_glob_changed(self, path, ignore=None):
193 """Check if glob path has any changed filepaths."""
144194 for f in glob.glob(path):
145195 if self.is_file_changed(f, ignore):
146196 return True
3131 assert watcher.is_changed(tmpdir) is False
3232
3333 # sleep 1 second so that mtime will be different
34 # TODO: This doesn't seem necessary; test passes without it
3435 time.sleep(1)
3536
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:
3740 f.write('')
3841
42 assert watcher.is_changed(tmpdir)
43 assert watcher.is_changed(tmpdir) is False
44
45 os.remove(filepath)
3946 assert watcher.is_changed(tmpdir)
4047 assert watcher.is_changed(tmpdir) is False
4148
4451 watcher.count = 0
4552
4653 # sleep 1 second so that mtime will be different
54 # TODO: This doesn't seem necessary; test passes without it
4755 time.sleep(1)
4856
4957 filepath = os.path.join(tmpdir, 'foo')
5563
5664 watcher.watch(filepath, add_count)
5765 assert watcher.is_changed(filepath)
66 assert watcher.is_changed(filepath) is False
5867
5968 # sleep 1 second so that mtime will be different
69 # TODO: This doesn't seem necessary; test passes without it
6070 time.sleep(1)
6171
6272 with open(filepath, 'w') as f:
6373 f.write('')
6474
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)
6778 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
6884
6985 def test_watch_glob(self):
7086 watcher = Watcher()
8197 with open(filepath, 'w') as f:
8298 f.write('')
8399
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)
86107
87108 def test_watch_ignore(self):
88109 watcher = Watcher()
93114 f.write('')
94115
95116 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)