syncwave 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: syncwave
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects.
5
5
  Keywords:
6
6
  Author: Lucas Iannitello
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "syncwave"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Lucas Iannitello", email = "lucas.iannitello@protonmail.com" }]
@@ -24,8 +24,12 @@ build-backend = "uv_build"
24
24
 
25
25
  [dependency-groups]
26
26
  dev = [
27
- "ruff>=0.12.7",
27
+ "ruff>=0.12.9",
28
28
  ]
29
29
 
30
30
  [tool.ruff.lint]
31
- extend-select = ["E501"]
31
+ select = ["E", "F"]
32
+
33
+ unfixable = [
34
+ "F401" # disable fix for unused imports
35
+ ]
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.1.3"
2
2
 
3
3
 
4
4
  from .syncwave import SyncStore, Syncwave
@@ -62,15 +62,13 @@ class _IO:
62
62
  timer.start()
63
63
 
64
64
  def _scheduled_write(self, path: Path, data_provider: DataProvider) -> None:
65
- try:
66
- self._atomic_write(path, data_provider())
67
- finally:
68
- with self._lock:
69
- self._debounce_timers.pop(path, None)
70
- self._pending_data_providers.pop(path, None)
65
+ with self._lock:
66
+ self._debounce_timers.pop(path, None)
67
+ self._pending_data_providers.pop(path, None)
68
+ self._atomic_write(path, data_provider())
71
69
 
72
70
  def _atomic_write(self, path: Path, data: JSONData) -> None:
73
- fd, tmp_path = mkstemp(prefix=".tmp_", suffix=".json", dir=path.parent)
71
+ fd, tmp_path = mkstemp(prefix=watcher.TMP_FILE_PREFIX, dir=path.parent)
74
72
  try:
75
73
  with os.fdopen(fd, "w", encoding=self.ENCODING) as tmp_file:
76
74
  json.dump(data, tmp_file, **self.DUMPS_CONFIG)
@@ -81,6 +79,7 @@ class _IO:
81
79
  # Atomically replace the target file
82
80
  watcher.mark_self_write(path)
83
81
  os.replace(tmp_path, path)
82
+ # print(f"Wrote JSON to {path}")
84
83
 
85
84
  except Exception as e:
86
85
  # Attempt to remove temporary file on error
@@ -1,11 +1,11 @@
1
- import json
2
- import threading
3
1
  from collections.abc import Mapping
4
2
  from inspect import isclass
3
+ from json import JSONDecodeError
5
4
  from pathlib import Path
6
- from typing import Any, Callable, Iterator, Optional, Type, Union, get_args
5
+ from threading import RLock
6
+ from typing import Any, Callable, Iterator, Optional, Type, Union, get_args, overload
7
7
 
8
- from pydantic import BaseModel, ValidationError
8
+ from pydantic import BaseModel, ValidationError, create_model, model_validator
9
9
 
10
10
  from .io import io
11
11
  from .watcher import watcher
@@ -33,37 +33,44 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
33
33
  self,
34
34
  syncwave: "Syncwave",
35
35
  cls: Type[BaseModel],
36
- *,
36
+ /,
37
37
  name: Optional[str] = None,
38
- key: str,
38
+ key: Optional[str] = None,
39
+ *,
39
40
  skip_key_validation: bool = False,
40
41
  file_name: Optional[str] = None,
41
42
  sub_dir: Optional[Union[Path, str]] = None,
42
43
  file_path: Optional[Union[Path, str]] = None,
44
+ new_cls_name: Optional[str] = None,
43
45
  # in_memory: bool = True, # TODO: implement in_memory
44
46
  ) -> None:
45
47
  if not isinstance(syncwave, Syncwave):
46
- raise ValueError("The syncwave argument must be an instance of Syncwave.")
48
+ raise TypeError("The first argument must be an instance of Syncwave.")
47
49
  if not (isclass(cls) and issubclass(cls, BaseModel)):
48
- raise ValueError("Only subclasses of BaseModel can be used.")
50
+ raise TypeError("Only subclasses of Pydantic's BaseModel can be used.")
51
+ # `store.cls` is created with `create_model`, with `cls` as its only base class
52
+ # in other words, this checks that `cls` has not been registered already
53
+ if any(store.cls.__bases__[0] is cls for store in syncwave.values()):
54
+ raise ValueError(f"The class '{cls.__name__}' has already been registered.")
55
+ # `name` defaults to the lowercased class name if not provided
49
56
  if (name_ := name or cls.__name__.lower()) in syncwave:
50
- raise ValueError(f"The name '{name_}' is already registered.")
51
- if cls in [store.cls for store in syncwave.values()]:
52
- raise ValueError(f"The class '{cls.__name__}' is already registered.")
53
- if key not in cls.model_fields:
54
- raise ValueError(f"'{cls.__name__}' does not have a field '{key}'.")
57
+ raise ValueError(f"The name '{name_}' has already been registered.")
58
+ if not cls.model_fields:
59
+ raise ValueError(f"Class '{cls.__name__}' has no fields defined.")
60
+ # `key` defaults to the first field in the model if not provided
61
+ if (key_ := key or next(iter(cls.model_fields))) not in cls.model_fields:
62
+ raise ValueError(f"'{cls.__name__}' does not have a field named '{key_}'.")
55
63
  if not skip_key_validation:
56
- _validate_key_type(cls, key)
64
+ _validate_key_type(cls, key_)
57
65
 
58
66
  self.syncwave = syncwave
59
- self.cls = cls
67
+ self.cls = self._patch_cls(cls, new_cls_name or f"Syncwave{cls.__name__}")
60
68
  self.name = name_
61
- self.key = key
69
+ self.key = key_
62
70
  self.path = self._get_path(file_name or name_, sub_dir, file_path)
63
71
 
64
72
  self._ssd: dict[JSONKey, BaseModel] = {} # actual mapping, ssd = SyncStoreDict
65
- self._lock = threading.RLock()
66
- self._patch_cls()
73
+ self._lock = RLock()
67
74
  io.init_json_file(self.path)
68
75
  self.load()
69
76
  watcher.watch(self.path, self.load)
@@ -75,7 +82,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
75
82
 
76
83
  def __iter__(self) -> Iterator[JSONKey]:
77
84
  with self._lock:
78
- # first, convert to a list so the iterator is over a frozen object
85
+ # first convert to a list so the iterator is over a frozen object
79
86
  return iter(list(self._ssd))
80
87
 
81
88
  def __len__(self) -> int:
@@ -91,10 +98,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
91
98
 
92
99
  def to_dict(self) -> dict[JSONKey, dict[str, Any]]:
93
100
  with self._lock:
94
- return {
95
- key: model_instance.model_dump()
96
- for key, model_instance in self._ssd.items()
97
- }
101
+ return {key: instance.model_dump() for key, instance in self._ssd.items()}
98
102
 
99
103
  def delete(self, key: JSONKey) -> None:
100
104
  with self._lock:
@@ -111,9 +115,9 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
111
115
  # no JSONDecodeError or ValidationError at this point, safe to clear
112
116
  self._ssd.clear()
113
117
  self._ssd.update(tmp_ssd)
114
- except (json.JSONDecodeError, ValidationError):
115
- return
116
- else:
118
+ except (FileNotFoundError, JSONDecodeError, ValidationError):
119
+ pass
120
+ finally:
117
121
  io.write_json(self.path, self.to_dict)
118
122
 
119
123
  def _get_path(
@@ -129,41 +133,53 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
129
133
  return (self.syncwave.data_dir / sub_dir / file_name).resolve()
130
134
  return (self.syncwave.data_dir / file_name).resolve()
131
135
 
132
- def _patch_cls(self) -> None:
133
- # preserve original methods
134
- original_init = self.cls.__init__
135
- original_setattr = self.cls.__setattr__
136
- original_delattr = self.cls.__delattr__
137
-
138
- def __init__(model_instance: BaseModel, *args: Any, **kwargs: Any) -> None:
139
- original_init(model_instance, *args, **kwargs)
140
- key_value = getattr(model_instance, self.key)
136
+ def _patch_cls(self, cls: Type[BaseModel], new_cls_name: str) -> Type[BaseModel]:
137
+ # This is a validator that acts as a post-initialization hook.
138
+ # Using a validator instead of `__init__` ensures every instance is tracked.
139
+ # Libraries such as FastAPI may create instances indirectly with methods like
140
+ # `validate_python` or `model_validate` and `__init__` may never be called.
141
+ def init(self_model: BaseModel) -> BaseModel:
142
+ key_value = getattr(self_model, self.key)
141
143
  with self._lock:
142
- self._ssd[key_value] = model_instance
144
+ if key_value in self._ssd and self._ssd[key_value] == self_model:
145
+ return self_model # no changes, no need to write
146
+ self._ssd[key_value] = self_model
143
147
  io.write_json(self.path, self.to_dict)
148
+ return self_model
149
+
150
+ new_cls = create_model(
151
+ new_cls_name,
152
+ __base__=cls,
153
+ __validators__={"__syncwave_init__": model_validator(mode="after")(init)},
154
+ )
144
155
 
145
- def __setattr__(model_instance: BaseModel, attr: str, value: Any) -> None:
156
+ # preserve original methods
157
+ original_setattr = cls.__setattr__
158
+ original_delattr = cls.__delattr__
159
+
160
+ def __setattr__(self_model: BaseModel, attr: str, value: Any) -> None:
146
161
  # TODO check for edge cases
147
- old_value = getattr(model_instance, attr)
148
- original_setattr(model_instance, attr, value)
162
+ old_value = getattr(self_model, attr)
163
+ original_setattr(self_model, attr, value)
149
164
  with self._lock:
150
165
  if attr == self.key:
151
166
  del self._ssd[old_value]
152
- self._ssd[value] = model_instance
167
+ self._ssd[value] = self_model
153
168
  io.write_json(self.path, self.to_dict)
154
169
 
155
- def __delattr__(model_instance: BaseModel, attr: str) -> None:
156
- old_value = getattr(model_instance, attr)
157
- original_delattr(model_instance, attr)
170
+ def __delattr__(self_model: BaseModel, attr: str) -> None:
171
+ old_value = getattr(self_model, attr)
172
+ original_delattr(self_model, attr)
158
173
  with self._lock:
159
174
  if attr == self.key:
160
175
  del self._ssd[old_value]
161
176
  io.write_json(self.path, self.to_dict)
162
177
 
163
- # override original methods
164
- self.cls.__init__ = __init__
165
- self.cls.__setattr__ = __setattr__
166
- self.cls.__delattr__ = __delattr__
178
+ # assign the new methods
179
+ new_cls.__setattr__ = __setattr__
180
+ new_cls.__delattr__ = __delattr__
181
+
182
+ return new_cls
167
183
 
168
184
 
169
185
  class Syncwave(Mapping[str, SyncStore]):
@@ -196,19 +212,33 @@ class Syncwave(Mapping[str, SyncStore]):
196
212
  def stop(self) -> None:
197
213
  watcher.stop()
198
214
 
215
+ @overload
216
+ def register(
217
+ self,
218
+ *,
219
+ name: Optional[str] = None,
220
+ key: Optional[str] = None,
221
+ skip_key_validation: bool = False,
222
+ file_name: Optional[str] = None,
223
+ sub_dir: Optional[Union[Path, str]] = None,
224
+ file_path: Optional[Union[Path, str]] = None,
225
+ ) -> Callable[[Type[BaseModel]], Type[BaseModel]]: ...
226
+
199
227
  def register(
200
228
  self,
229
+ _cls: Optional[Type[BaseModel]] = None,
230
+ /,
201
231
  *,
202
232
  name: Optional[str] = None,
203
- key: str,
233
+ key: Optional[str] = None,
204
234
  skip_key_validation: bool = False,
205
235
  file_name: Optional[str] = None,
206
236
  sub_dir: Optional[Union[Path, str]] = None,
207
237
  file_path: Optional[Union[Path, str]] = None,
208
238
  # in_memory: bool = True, # TODO: implement in_memory
209
- ) -> Callable[[Type[BaseModel]], Type[BaseModel]]:
239
+ ) -> Union[Type[BaseModel], Callable[[Type[BaseModel]], Type[BaseModel]]]:
210
240
  def decorator(cls: Type[BaseModel]) -> Type[BaseModel]:
211
- SyncStore(
241
+ store = SyncStore(
212
242
  self,
213
243
  cls,
214
244
  name=name,
@@ -217,7 +247,12 @@ class Syncwave(Mapping[str, SyncStore]):
217
247
  file_name=file_name,
218
248
  sub_dir=sub_dir,
219
249
  file_path=file_path,
250
+ new_cls_name=cls.__name__,
220
251
  )
221
- return cls
252
+ return store.cls
222
253
 
223
- return decorator
254
+ if _cls is None:
255
+ # called with parenthesis: @syncwave.register(...)
256
+ return decorator
257
+ # called without parenthesis: @syncwave.register
258
+ return decorator(_cls)
@@ -0,0 +1,177 @@
1
+ from pathlib import Path
2
+ from threading import Lock, Timer
3
+ from time import monotonic, sleep
4
+ from typing import Callable
5
+
6
+ from watchdog.events import (
7
+ DirDeletedEvent,
8
+ DirMovedEvent,
9
+ FileSystemEvent,
10
+ FileSystemEventHandler,
11
+ )
12
+ from watchdog.observers import Observer
13
+ from watchdog.observers.api import ObservedWatch
14
+
15
+ DirPath = Path
16
+ FilePath = Path
17
+ Callback = Callable[[], None]
18
+
19
+
20
+ class _Watcher:
21
+ TMP_FILE_PREFIX = ".tmp_"
22
+
23
+ def __init__(self) -> None:
24
+ self._lock = Lock()
25
+ # dir_path -> (watch object, set of file_paths)
26
+ self._watched_dirs: dict[DirPath, tuple[ObservedWatch, set[FilePath]]] = {}
27
+
28
+ self._observer = Observer()
29
+ self._event_handler = _EventHandler(self)
30
+ self._observer.start()
31
+
32
+ def watch(self, file_path: FilePath, callback: Callback) -> None:
33
+ if not file_path.is_file():
34
+ raise ValueError(f"Path '{file_path}' is not a file.")
35
+
36
+ dir_path = file_path.parent
37
+ with self._lock:
38
+ # if the directory is already being watched, just add the file to the set
39
+ if dir_path in self._watched_dirs:
40
+ _, watched_files = self._watched_dirs[dir_path]
41
+ watched_files.add(file_path)
42
+ # otherwise, schedule the directory and create the set of watched files
43
+ else:
44
+ self._watched_dirs[dir_path] = (
45
+ self._observer.schedule(self._event_handler, str(dir_path)),
46
+ {file_path},
47
+ )
48
+ # set or reset the callback in all cases
49
+ self._event_handler.set_callback(file_path, callback)
50
+
51
+ def unwatch(self, file_path: FilePath) -> None:
52
+ # remove the callback
53
+ self._event_handler.unset_callback(file_path)
54
+
55
+ dir_path = file_path.parent
56
+ with self._lock:
57
+ # if the directory is not being watched, do nothing
58
+ if dir_path not in self._watched_dirs:
59
+ return
60
+
61
+ # if the file is in the set, remove it
62
+ watch, watched_files = self._watched_dirs[dir_path]
63
+ if file_path in watched_files:
64
+ watched_files.remove(file_path)
65
+
66
+ # if the set is empty, unschedule the directory
67
+ if not watched_files:
68
+ self._observer.unschedule(watch)
69
+ del self._watched_dirs[dir_path]
70
+
71
+ def mark_self_write(self, file_path: FilePath) -> None:
72
+ self._event_handler.mark_self_write(file_path)
73
+
74
+ def get_watched_dirs(self) -> dict[DirPath, set[FilePath]]:
75
+ with self._lock:
76
+ return {
77
+ dir_path: watched_files.copy()
78
+ for dir_path, (_, watched_files) in self._watched_dirs.items()
79
+ }
80
+
81
+ def restore_watched_dir(self, dir_path: DirPath) -> None:
82
+ with self._lock:
83
+ if dir_path not in self._watched_dirs:
84
+ return
85
+ dir_path.mkdir(parents=True, exist_ok=True)
86
+ watch, watched_files = self._watched_dirs[dir_path]
87
+ self._observer.unschedule(watch)
88
+ self._watched_dirs[dir_path] = (
89
+ self._observer.schedule(self._event_handler, str(dir_path)),
90
+ watched_files,
91
+ )
92
+
93
+ def stop(self) -> None:
94
+ self._observer.stop()
95
+ self._observer.join()
96
+
97
+
98
+ class _EventHandler(FileSystemEventHandler):
99
+ DEBOUNCE_WINDOW = 0.05
100
+ SELF_WRITE_WINDOW = 0.5
101
+
102
+ def __init__(self, watcher: _Watcher) -> None:
103
+ self._watcher = watcher
104
+ self._lock = Lock()
105
+ self._callbacks: dict[FilePath, Callback] = {}
106
+ self._debounce_timers: dict[FilePath, Timer] = {}
107
+ self._self_writes_ts: dict[FilePath, float] = {}
108
+
109
+ def set_callback(self, file_path: FilePath, callback: Callback) -> None:
110
+ with self._lock:
111
+ self._callbacks[file_path] = callback
112
+
113
+ def unset_callback(self, file_path: FilePath) -> None:
114
+ with self._lock:
115
+ self._callbacks.pop(file_path, None)
116
+
117
+ def mark_self_write(self, file_path: FilePath) -> None:
118
+ with self._lock:
119
+ self._self_writes_ts[file_path] = monotonic()
120
+
121
+ def _is_self_write(self, file_path: FilePath) -> bool:
122
+ # not protected by the lock because it's only called internally
123
+ ts = self._self_writes_ts.get(file_path)
124
+ if ts is not None and monotonic() - ts < self.SELF_WRITE_WINDOW:
125
+ return True
126
+ return False
127
+
128
+ def dispatch(self, event: FileSystemEvent) -> None:
129
+ if event.is_directory:
130
+ if not isinstance(event, (DirDeletedEvent, DirMovedEvent)):
131
+ return
132
+ watched_dirs = self._watcher.get_watched_dirs()
133
+ for dir_path in self._paths_from_event(event):
134
+ if dir_path in watched_dirs:
135
+ self._on_dir_deleted(dir_path, watched_dirs[dir_path])
136
+ else:
137
+ for file_path in self._paths_from_event(event):
138
+ self._on_file_modified(file_path)
139
+
140
+ def _on_dir_deleted(self, dir_path: DirPath, watched_files: set[FilePath]) -> None:
141
+ sleep(self.DEBOUNCE_WINDOW) # low-tech debounce
142
+ self._watcher.restore_watched_dir(dir_path)
143
+ for file_path in watched_files:
144
+ self._on_file_modified(file_path)
145
+
146
+ def _on_file_modified(self, file_path: FilePath) -> None:
147
+ with self._lock:
148
+ if file_path not in self._callbacks or self._is_self_write(file_path):
149
+ return
150
+
151
+ if file_path in self._debounce_timers:
152
+ self._debounce_timers[file_path].cancel()
153
+
154
+ timer = Timer(
155
+ self.DEBOUNCE_WINDOW,
156
+ self._scheduled_callback,
157
+ args=(file_path, self._callbacks[file_path]),
158
+ )
159
+ self._debounce_timers[file_path] = timer
160
+ timer.start()
161
+
162
+ def _scheduled_callback(self, file_path: FilePath, callback: Callback) -> None:
163
+ with self._lock:
164
+ self._debounce_timers.pop(file_path, None)
165
+ callback()
166
+
167
+ def _paths_from_event(self, event: FileSystemEvent) -> list[Path]:
168
+ paths = []
169
+ if self._watcher.TMP_FILE_PREFIX not in event.src_path:
170
+ paths.append(Path(event.src_path).resolve())
171
+ dest_path = getattr(event, "dest_path", "")
172
+ if dest_path and self._watcher.TMP_FILE_PREFIX not in dest_path:
173
+ paths.append(Path(dest_path).resolve())
174
+ return paths
175
+
176
+
177
+ watcher = _Watcher()
@@ -1,139 +0,0 @@
1
- from pathlib import Path
2
- from threading import Lock, Timer
3
- from time import monotonic
4
- from typing import Callable, Union
5
-
6
- from watchdog.events import (
7
- DirModifiedEvent,
8
- FileModifiedEvent,
9
- FileSystemEvent,
10
- FileSystemEventHandler,
11
- )
12
- from watchdog.observers import Observer
13
- from watchdog.observers.api import ObservedWatch
14
-
15
- DirPath = Path
16
- FilePath = Path
17
- Callback = Callable[[], None]
18
-
19
-
20
- class _Watcher:
21
- SELF_WRITE_WINDOW = 0.5
22
-
23
- def __init__(self) -> None:
24
- self._lock = Lock()
25
- # mapping dir_path -> (watch object, set of file_paths)
26
- self._watched_dirs: dict[DirPath, tuple[ObservedWatch, set[FilePath]]] = {}
27
- self._self_writes_ts: dict[FilePath, float] = {}
28
- self._callbacks: dict[FilePath, Callback] = {}
29
-
30
- self._observer = Observer()
31
- self._event_handler = _EventHandler(self)
32
- self._observer.start()
33
-
34
- def stop(self) -> None:
35
- self._observer.stop()
36
- self._observer.join()
37
-
38
- def watch(self, file_path: FilePath, callback: Callback) -> None:
39
- dir_path = file_path.parent
40
-
41
- if not file_path.is_file():
42
- raise ValueError(f"Path {file_path} is not a file.")
43
-
44
- with self._lock:
45
- # if the directory is already being watched...
46
- if dir_path in self._watched_dirs:
47
- _, watched_files = self._watched_dirs[dir_path]
48
-
49
- # ...but the file is not in the set, add it
50
- if file_path not in watched_files:
51
- watched_files.add(file_path)
52
-
53
- # and in any case, update the callback before returning
54
- self._callbacks[file_path] = callback
55
- return
56
-
57
- # otherwise, schedule the directory and register the callback
58
- self._watched_dirs[dir_path] = (
59
- self._observer.schedule(self._event_handler, str(dir_path)),
60
- {file_path},
61
- )
62
- self._callbacks[file_path] = callback
63
-
64
- def unwatch(self, file_path: FilePath) -> None:
65
- dir_path = file_path.parent
66
-
67
- with self._lock:
68
- # remove the callback
69
- self._callbacks.pop(file_path, None)
70
-
71
- # if the directory is not being watched, do nothing
72
- if dir_path not in self._watched_dirs:
73
- return
74
-
75
- watch, watched_files = self._watched_dirs[dir_path]
76
-
77
- # if the file is in the set, remove it
78
- if file_path in watched_files:
79
- watched_files.remove(file_path)
80
-
81
- # if the set is empty, unschedule the directory
82
- if not watched_files:
83
- self._observer.unschedule(watch)
84
- del self._watched_dirs[dir_path]
85
-
86
- def mark_self_write(self, file_path: FilePath) -> None:
87
- with self._lock:
88
- self._self_writes_ts[file_path] = monotonic()
89
-
90
- def _is_self_write(self, file_path: FilePath) -> bool:
91
- with self._lock:
92
- ts = self._self_writes_ts.get(file_path)
93
- if ts is not None and monotonic() - ts < self.SELF_WRITE_WINDOW:
94
- return True
95
- return False
96
-
97
-
98
- class _EventHandler(FileSystemEventHandler):
99
- DEBOUNCE_WINDOW = 0.05
100
-
101
- def __init__(self, watcher: _Watcher) -> None:
102
- self.lock = Lock()
103
- self.watcher = watcher
104
- self.debounce_timers: dict[FileSystemEvent, Timer] = {}
105
-
106
- def on_modified(self, event: Union[FileModifiedEvent, DirModifiedEvent]) -> None:
107
- if event.is_directory or isinstance(event, DirModifiedEvent):
108
- return
109
-
110
- file_path = Path(event.src_path).resolve()
111
-
112
- if self.watcher._is_self_write(file_path):
113
- return
114
-
115
- callback = self.watcher._callbacks.get(file_path)
116
- if callback is None:
117
- return
118
-
119
- with self.lock:
120
- if event in self.debounce_timers:
121
- self.debounce_timers[event].cancel()
122
-
123
- timer = Timer(
124
- self.DEBOUNCE_WINDOW,
125
- self.schedule_callback,
126
- args=(event, callback),
127
- )
128
- self.debounce_timers[event] = timer
129
- timer.start()
130
-
131
- def schedule_callback(self, event: FileSystemEvent, callback: Callback) -> None:
132
- try:
133
- callback()
134
- finally:
135
- with self.lock:
136
- self.debounce_timers.pop(event, None)
137
-
138
-
139
- watcher = _Watcher()
File without changes
File without changes
File without changes