def test_md5_calculation_opens_file_with_rb(self): # This tests implementation :( . But since the issue this is addressing # could easily come back to bite us if a distracted coder tweaks the # implementation, I'm putting this here anyway. with patch("streamlit.watcher.util.open", mock_open(read_data=b"hello")) as m: md5 = util.calc_md5_with_blocking_retries("foo") m.assert_called_once_with("foo", "rb")
def add_path_change_listener( self, path: str, callback: Callable[[str], None], *, # keyword-only arguments: glob_pattern: Optional[str] = None, allow_nonexistent: bool = False, ) -> None: """Add a path to this object's event filter.""" with self._lock: watched_path = self._watched_paths.get(path, None) if watched_path is None: md5 = util.calc_md5_with_blocking_retries( path, glob_pattern=glob_pattern, allow_nonexistent=allow_nonexistent, ) modification_time = util.path_modification_time( path, allow_nonexistent) watched_path = WatchedPath( md5=md5, modification_time=modification_time, glob_pattern=glob_pattern, allow_nonexistent=allow_nonexistent, ) self._watched_paths[path] = watched_path watched_path.on_changed.connect(callback, weak=False)
def handle_path_change_event(self, event: events.FileSystemEvent) -> None: """Handle when a path (corresponding to a file or dir) is changed. The events that can call this are modification, creation or moved events. """ # Check for both modified and moved files, because many programs write # to a backup file then rename (i.e. move) it. if event.event_type == events.EVENT_TYPE_MODIFIED: changed_path = event.src_path elif event.event_type == events.EVENT_TYPE_MOVED: LOGGER.debug("Move event: src %s; dest %s", event.src_path, event.dest_path) changed_path = event.dest_path # On OSX with VI, on save, the file is deleted, the swap file is # modified and then the original file is created hence why we # capture EVENT_TYPE_CREATED elif event.event_type == events.EVENT_TYPE_CREATED: changed_path = event.src_path else: LOGGER.debug("Don't care about event type %s", event.event_type) return changed_path = os.path.abspath(changed_path) changed_path_info = self._watched_paths.get(changed_path, None) if changed_path_info is None: LOGGER.debug( "Ignoring changed path %s.\nWatched_paths: %s", changed_path, self._watched_paths, ) return modification_time = util.path_modification_time( changed_path, changed_path_info.allow_nonexistent) if modification_time == changed_path_info.modification_time: LOGGER.debug("File/dir timestamp did not change: %s", changed_path) return changed_path_info.modification_time = modification_time new_md5 = util.calc_md5_with_blocking_retries( changed_path, glob_pattern=changed_path_info.glob_pattern, allow_nonexistent=changed_path_info.allow_nonexistent, ) if new_md5 == changed_path_info.md5: LOGGER.debug("File/dir MD5 did not change: %s", changed_path) return LOGGER.debug("File/dir MD5 changed: %s", changed_path) changed_path_info.md5 = new_md5 changed_path_info.on_changed.send(changed_path)
def handle_file_change_event(self, event): """Handle when file is changed. The events that can call this are modification, creation or moved events. Parameters ---------- event : FileSystemEvent The event object representing the file system event. """ if event.is_directory: return # Check for both modified and moved files, because many programs write # to a backup file then rename (i.e. move) it. if event.event_type == events.EVENT_TYPE_MODIFIED: file_path = event.src_path # On OSX with VI, on save, the file is deleted, the swap file is # modified and then the original file is created hence why we # capture EVENT_TYPE_CREATED elif event.event_type == events.EVENT_TYPE_CREATED: file_path = event.src_path elif event.event_type == events.EVENT_TYPE_MOVED: LOGGER.debug("Move event: src %s; dest %s", event.src_path, event.dest_path) file_path = event.dest_path else: LOGGER.debug("Don't care about event type %s", event.event_type), return file_path = os.path.abspath(file_path) file_info = self._watched_files.get(file_path, None) if file_info is None: LOGGER.debug( "Ignoring file %s.\nWatched_files: %s", file_path, self._watched_files ) return modification_time = os.stat(file_path).st_mtime if modification_time == file_info.modification_time: LOGGER.debug("File timestamp did not change: %s", file_path) return file_info.modification_time = modification_time new_md5 = util.calc_md5_with_blocking_retries(file_path) if new_md5 == file_info.md5: LOGGER.debug("File MD5 did not change: %s", file_path) return LOGGER.debug("File MD5 changed: %s", file_path) file_info.md5 = new_md5 file_info.on_file_changed.send(file_path)
def add_file_change_listener(self, file_path, callback): """Add a file to this object's event filter. Parameters ---------- file_path : str callback : callable """ with self._lock: watched_file = self._watched_files.get(file_path, None) if watched_file is None: md5 = util.calc_md5_with_blocking_retries(file_path) modification_time = os.stat(file_path).st_mtime watched_file = WatchedFile(md5=md5, modification_time=modification_time) self._watched_files[file_path] = watched_file watched_file.on_file_changed.connect(callback, weak=False)
def __init__(self, file_path, on_file_changed): """Constructor. Arguments --------- file_path : str Absolute path of the file to watch. on_file_changed : callable Function to call when the file changes. This function should take the changed file's path as a parameter. """ self._file_path = file_path self._on_file_changed = on_file_changed self._active = True self._modification_time = os.stat(self._file_path).st_mtime self._md5 = util.calc_md5_with_blocking_retries(self._file_path) self._schedule()
def _check_if_file_changed(self) -> None: if not self._active: # Don't call self._schedule() return modification_time = os.stat(self._file_path).st_mtime if modification_time <= self._modification_time: self._schedule() return self._modification_time = modification_time md5 = util.calc_md5_with_blocking_retries(self._file_path) if md5 == self._md5: self._schedule() return self._md5 = md5 LOGGER.debug("Change detected: %s", self._file_path) self._on_file_changed(self._file_path) self._schedule()
def __init__(self, file_path: str, on_file_changed: Callable[[str], None]): """Constructor. You do not need to retain a reference to a PollingFileWatcher to prevent it from being garbage collected. (The global _executor object retains references to all active instances.) Arguments --------- file_path Absolute path of the file to watch. on_file_changed Function to call when the file changes. This function should take the changed file's path as a parameter. """ self._file_path = file_path self._on_file_changed = on_file_changed self._active = True self._modification_time = os.stat(self._file_path).st_mtime self._md5 = util.calc_md5_with_blocking_retries(self._file_path) self._schedule()
def test_md5_calculation_succeeds_with_bytes_input(self): with patch("streamlit.watcher.util.open", mock_open(read_data=b"hello")) as m: md5 = util.calc_md5_with_blocking_retries("foo") self.assertEqual(md5, "5d41402abc4b2a76b9719d911017c592")
def test_md5_calculation_allow_nonexistent(self): md5 = util.calc_md5_with_blocking_retries("hello", allow_nonexistent=True) self.assertEqual(md5, "5d41402abc4b2a76b9719d911017c592")
def test_md5_calculation_can_pass_glob(self, mock_stable_dir_identifier): mock_stable_dir_identifier.return_value = "hello" md5 = util.calc_md5_with_blocking_retries("foo", glob_pattern="*.py") mock_stable_dir_identifier.assert_called_once_with("foo", "*.py")
def test_md5_calculation_succeeds_with_dir_input(self, mock_stable_dir_identifier): mock_stable_dir_identifier.return_value = "hello" md5 = util.calc_md5_with_blocking_retries("foo") self.assertEqual(md5, "5d41402abc4b2a76b9719d911017c592") mock_stable_dir_identifier.assert_called_once_with("foo", "*")