cleepcli 1.44.0__tar.gz → 1.44.2__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.
Files changed (33) hide show
  1. {cleepcli-1.44.0/cleepcli.egg-info → cleepcli-1.44.2}/PKG-INFO +1 -1
  2. {cleepcli-1.44.0 → cleepcli-1.44.2}/bin/ccli +15 -4
  3. {cleepcli-1.44.0 → cleepcli-1.44.2}/bin/cleep-cli +15 -4
  4. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/file.py +61 -34
  5. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/version.py +1 -1
  6. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/watch.py +141 -49
  7. {cleepcli-1.44.0 → cleepcli-1.44.2/cleepcli.egg-info}/PKG-INFO +1 -1
  8. cleepcli-1.44.2/tests/test_watch.py +314 -0
  9. cleepcli-1.44.0/tests/test_watch.py +0 -208
  10. {cleepcli-1.44.0 → cleepcli-1.44.2}/LICENSE +0 -0
  11. {cleepcli-1.44.0 → cleepcli-1.44.2}/MANIFEST.in +0 -0
  12. {cleepcli-1.44.0 → cleepcli-1.44.2}/README.md +0 -0
  13. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/__init__.py +0 -0
  14. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/check.py +0 -0
  15. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/ci.py +0 -0
  16. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/cleepapi.py +0 -0
  17. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/config.py +0 -0
  18. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/console.py +0 -0
  19. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/docs.py +0 -0
  20. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/git.py +0 -0
  21. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/module.py +0 -0
  22. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/package.py +0 -0
  23. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/test.py +0 -0
  24. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/tools.py +0 -0
  25. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/SOURCES.txt +0 -0
  26. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/dependency_links.txt +0 -0
  27. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/requires.txt +0 -0
  28. {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/top_level.txt +0 -0
  29. {cleepcli-1.44.0 → cleepcli-1.44.2}/pyproject.toml +0 -0
  30. {cleepcli-1.44.0 → cleepcli-1.44.2}/requirements.txt +0 -0
  31. {cleepcli-1.44.0 → cleepcli-1.44.2}/setup.cfg +0 -0
  32. {cleepcli-1.44.0 → cleepcli-1.44.2}/setup.py +0 -0
  33. {cleepcli-1.44.0 → cleepcli-1.44.2}/tests/test_console.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.44.0
3
+ Version: 1.44.2
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -291,11 +291,22 @@ def modssync(run_scripts):
291
291
  f = File()
292
292
 
293
293
  for module in config.MODULES_REPO_URL.keys():
294
- res = f.module_sync(module)
295
- if not res:
296
- sys.exit(1)
297
294
  if run_scripts:
298
- res = f.module_run_install_scripts(module)
295
+ # Same order as package install: scripts + preinst, then files, then postinst
296
+ res = f.module_sync(module, backend=False, frontend=False, scripts=True)
297
+ if not res:
298
+ sys.exit(1)
299
+ res = f.module_run_install_script(module, "preinst.sh")
300
+ if not res:
301
+ sys.exit(1)
302
+ res = f.module_sync(module, backend=True, frontend=True, scripts=True)
303
+ if not res:
304
+ sys.exit(1)
305
+ res = f.module_run_install_script(module, "postinst.sh")
306
+ if not res:
307
+ sys.exit(1)
308
+ else:
309
+ res = f.module_sync(module)
299
310
  if not res:
300
311
  sys.exit(1)
301
312
 
@@ -291,11 +291,22 @@ def modssync(run_scripts):
291
291
  f = File()
292
292
 
293
293
  for module in config.MODULES_REPO_URL.keys():
294
- res = f.module_sync(module)
295
- if not res:
296
- sys.exit(1)
297
294
  if run_scripts:
298
- res = f.module_run_install_scripts(module)
295
+ # Same order as package install: scripts + preinst, then files, then postinst
296
+ res = f.module_sync(module, backend=False, frontend=False, scripts=True)
297
+ if not res:
298
+ sys.exit(1)
299
+ res = f.module_run_install_script(module, "preinst.sh")
300
+ if not res:
301
+ sys.exit(1)
302
+ res = f.module_sync(module, backend=True, frontend=True, scripts=True)
303
+ if not res:
304
+ sys.exit(1)
305
+ res = f.module_run_install_script(module, "postinst.sh")
306
+ if not res:
307
+ sys.exit(1)
308
+ else:
309
+ res = f.module_sync(module)
299
310
  if not res:
300
311
  sys.exit(1)
301
312
 
@@ -31,12 +31,19 @@ class File():
31
31
  'MEDIA_DST': config.MEDIA_DST,
32
32
  'CONFIG_DIR': config.CONFIG_DIR,
33
33
  }
34
+ variables['MODULES_DST'] = config.MODULES_DST
34
35
  cmd = """
35
36
  set -e
36
37
  mkdir -p "%(HTML_DST)s/"
37
- mkdir -p "%(CORE_DST)s/modules"
38
+ mkdir -p "%(CORE_DST)s"
39
+ mkdir -p "%(MODULES_DST)s"
38
40
  mkdir -p "%(MEDIA_DST)s/modules"
39
41
  mkdir -p "%(CONFIG_DIR)s"
42
+ # Python imports cleep.modules from CORE_DST/modules; it must point at the live apps tree
43
+ if [ -e "%(CORE_DST)s/modules" ] && [ ! -L "%(CORE_DST)s/modules" ]; then
44
+ rm -rf "%(CORE_DST)s/modules"
45
+ fi
46
+ ln -sfn "%(MODULES_DST)s" "%(CORE_DST)s/modules"
40
47
  rsync -av "%(REPO_DIR)s/cleep/" "%(CORE_DST)s/" --exclude "/tests/" --exclude "modules" --exclude "*__pycache__*" --delete --exclude "*.pyc" --keep-dirlinks
41
48
  rsync -av "%(REPO_DIR)s/html/" "%(HTML_DST)s/" --delete --exclude "js/modules/" --exclude "*node_modules*"
42
49
  rsync -av "%(REPO_DIR)s/bin/cleep" "%(BIN_DST)s/cleep"
@@ -53,16 +60,19 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
53
60
 
54
61
  return True
55
62
 
56
- def module_sync(self, module_name):
57
- """
63
+ def module_sync(self, module_name, backend=True, frontend=True, scripts=True):
64
+ """
58
65
  Copy specified module content to valid execution location
66
+
67
+ Args:
68
+ module_name (str): module folder name under MODULES_SRC
69
+ backend (bool): sync backend/ when True
70
+ frontend (bool): sync frontend/ when True
71
+ scripts (bool): sync scripts/ when True
59
72
  """
60
73
  if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
61
74
  self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
62
75
  return False
63
-
64
- if not os.path.exists(os.path.join(config.MODULES_DST, module_name)):
65
- os.makedirs(os.path.join(config.MODULES_DST, module_name))
66
76
 
67
77
  mod_backend_src = os.path.join(config.MODULES_SRC, module_name, 'backend/')
68
78
  mod_backend_dst = os.path.join(config.MODULES_DST, module_name)
@@ -71,26 +81,34 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
71
81
  mod_scripts_src = os.path.join(config.MODULES_SRC, module_name, 'scripts/')
72
82
  mod_scripts_dst = os.path.join(config.MODULES_SCRIPTS_DST, module_name)
73
83
 
74
- c = Console()
75
- cmd = """
76
- set -e
84
+ parts = []
85
+ if backend:
86
+ parts.append("""
77
87
  if [ -d "%(BACKEND_SRC)s" ]; then
78
88
  /bin/mkdir -p "%(BACKEND_DST)s"
79
89
  rsync -av "%(BACKEND_SRC)s" "%(BACKEND_DST)s" --delete --exclude "*.pyc" --exclude "*__pycache__*"
80
90
  fi
91
+ """ % {'BACKEND_SRC': mod_backend_src, 'BACKEND_DST': mod_backend_dst})
92
+ if frontend:
93
+ parts.append("""
81
94
  if [ -d "%(FRONTEND_SRC)s" ]; then
82
95
  /bin/mkdir -p "%(FRONTEND_DST)s"
83
96
  rsync -av "%(FRONTEND_SRC)s" "%(FRONTEND_DST)s" --delete --exclude "*node_modules*"
84
97
  fi
98
+ """ % {'FRONTEND_SRC': mod_frontend_src, 'FRONTEND_DST': mod_frontend_dst})
99
+ if scripts:
100
+ parts.append("""
85
101
  if [ -d "%(SCRIPTS_SRC)s" ]; then
86
102
  /bin/mkdir -p "%(SCRIPTS_DST)s"
87
103
  rsync -av "%(SCRIPTS_SRC)s" "%(SCRIPTS_DST)s" --delete --exclude "*__pycache__*"
88
104
  fi
89
- """ % {
90
- 'BACKEND_SRC': mod_backend_src, 'BACKEND_DST': mod_backend_dst,
91
- 'FRONTEND_SRC': mod_frontend_src, 'FRONTEND_DST': mod_frontend_dst,
92
- 'SCRIPTS_SRC': mod_scripts_src, 'SCRIPTS_DST': mod_scripts_dst
93
- }
105
+ """ % {'SCRIPTS_SRC': mod_scripts_src, 'SCRIPTS_DST': mod_scripts_dst})
106
+
107
+ if not parts:
108
+ return True
109
+
110
+ c = Console()
111
+ cmd = "set -e\n" + "\n".join(parts)
94
112
  self.logger.debug('Modsync cmd: %s' % cmd)
95
113
  resp = c.command(cmd, 120)
96
114
 
@@ -103,30 +121,39 @@ fi
103
121
 
104
122
  return True
105
123
 
106
- def module_run_install_scripts(self, module_name):
124
+ def module_run_install_script(self, module_name, script_name):
107
125
  """
108
- Run module installation scripts
126
+ Run one module installation script from the synced scripts directory
127
+
128
+ Args:
129
+ module_name (str): module name
130
+ script_name (str): script filename (preinst.sh, postinst.sh, ...)
109
131
  """
110
132
  if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
111
133
  self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
112
134
  return False
113
135
 
114
- preinst_script = os.path.join(config.MODULES_SCRIPTS_DST, module_name, "preinst.sh")
115
- postinst_script = os.path.join(config.MODULES_SCRIPTS_DST, module_name, "postinst.sh")
116
- console = Console()
117
-
118
- if os.path.exists(preinst_script):
119
- self.logger.info('Running %s scripts', preinst_script)
120
- resp = console.command(preinst_script, timeout=300)
121
- if resp["killed"] or resp["returncode"] != 0:
122
- self.logger.error('Error running %s [%s]: %s', preinst_script, resp["returncode"], ''.join(resp["stderr"]))
123
- return False
124
-
125
- if os.path.exists(postinst_script):
126
- self.logger.info('Running %s scripts', postinst_script)
127
- resp = console.command(postinst_script, timeout=300)
128
- if resp["killed"] or resp["returncode"] != 0:
129
- self.logger.error('Error running %s [%s]: %s', postinst_script, resp["returncode"], ''.join(resp["stderr"]))
130
- return False
131
-
136
+ script_path = os.path.join(config.MODULES_SCRIPTS_DST, module_name, script_name)
137
+ if not os.path.exists(script_path):
138
+ return True
139
+
140
+ self.logger.info('Running %s', script_path)
141
+ resp = Console().command(script_path, timeout=300)
142
+ if resp["killed"] or resp["returncode"] != 0:
143
+ self.logger.error(
144
+ 'Error running %s [%s]: %s',
145
+ script_path,
146
+ resp["returncode"],
147
+ ''.join(resp["stderr"]),
148
+ )
149
+ return False
132
150
  return True
151
+
152
+ def module_run_install_scripts(self, module_name):
153
+ """
154
+ Run module installation scripts (preinst then postinst)
155
+ """
156
+ return (
157
+ self.module_run_install_script(module_name, "preinst.sh")
158
+ and self.module_run_install_script(module_name, "postinst.sh")
159
+ )
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- VERSION = "1.44.0"
4
+ VERSION = "1.44.2"
@@ -11,6 +11,7 @@ from watchdog.observers import Observer
11
11
  from watchdog.events import (
12
12
  FileSystemEventHandler,
13
13
  EVENT_TYPE_CREATED,
14
+ EVENT_TYPE_DELETED,
14
15
  EVENT_TYPE_MODIFIED,
15
16
  EVENT_TYPE_MOVED,
16
17
  )
@@ -18,7 +19,12 @@ import watchdog.events as events
18
19
  from .cleepapi import CleepApi
19
20
  from .file import File
20
21
 
21
- _WATCHED_EVENT_TYPES = (EVENT_TYPE_CREATED, EVENT_TYPE_MODIFIED, EVENT_TYPE_MOVED)
22
+ _WATCHED_EVENT_TYPES = (
23
+ EVENT_TYPE_CREATED,
24
+ EVENT_TYPE_DELETED,
25
+ EVENT_TYPE_MODIFIED,
26
+ EVENT_TYPE_MOVED,
27
+ )
22
28
 
23
29
 
24
30
  def _is_under(path, root):
@@ -27,20 +33,42 @@ def _is_under(path, root):
27
33
  return path == root or path.startswith(root + os.sep)
28
34
 
29
35
 
30
- def iter_watch_directories(root):
36
+ # One observer.schedule() = one inotify instance. Keep that count low
37
+ # (kernel default is often 128) by using recursive watches on a few roots.
38
+ MODULE_CONTENT_DIRS = ("backend", "frontend", "scripts")
39
+
40
+
41
+ def iter_watch_roots():
31
42
  """
32
- Yield directories to watch under root.
43
+ Yield (path, recursive) pairs to schedule on the observer.
33
44
 
34
- Rejected directory names are not yielded and are not descended into, so
35
- inotify never watches them.
45
+ Core/html/bin are watched recursively. Modules watch only backend,
46
+ frontend and scripts so .git under each app is never registered.
36
47
  """
37
- if not os.path.isdir(root):
48
+ for root in (config.CORE_SRC, config.HTML_SRC, config.BIN_SRC):
49
+ if os.path.isdir(root):
50
+ yield root, True
51
+
52
+ if not os.path.isdir(config.MODULES_SRC):
53
+ return
54
+
55
+ yield config.MODULES_SRC, False
56
+ try:
57
+ names = sorted(os.listdir(config.MODULES_SRC))
58
+ except OSError:
38
59
  return
39
- for dirpath, dirnames, _filenames in os.walk(root):
40
- dirnames[:] = [
41
- name for name in dirnames if name not in CleepHandler.REJECTED_DIRS
42
- ]
43
- yield dirpath
60
+ for name in names:
61
+ if name in CleepHandler.REJECTED_DIRS:
62
+ continue
63
+ module_path = os.path.join(config.MODULES_SRC, name)
64
+ if not os.path.isdir(module_path):
65
+ continue
66
+ # Non-recursive: see backend/frontend/scripts appear without watching .git
67
+ yield module_path, False
68
+ for sub in MODULE_CONTENT_DIRS:
69
+ content = os.path.join(module_path, sub)
70
+ if os.path.isdir(content):
71
+ yield content, True
44
72
 
45
73
 
46
74
  class _Batch:
@@ -96,9 +124,12 @@ class ActionsExecutor(Thread):
96
124
  """
97
125
  self.running = False
98
126
 
99
- def note_change(self, module=None, core=False, frontend=False):
127
+ def note_change(self, module=None, core=False, frontend=False, backend=False):
100
128
  """
101
129
  Remember a source change. The sync runs once writes stay quiet.
130
+
131
+ Scripts-only edits set neither frontend nor backend: files are synced
132
+ and Cleep is left running (install scripts are not re-run on watch).
102
133
  """
103
134
  with self._lock:
104
135
  if module:
@@ -107,7 +138,7 @@ class ActionsExecutor(Thread):
107
138
  self._batch.core = True
108
139
  if frontend:
109
140
  self._batch.frontend = True
110
- else:
141
+ if backend:
111
142
  self._batch.backend = True
112
143
  self._batch.last = time.monotonic()
113
144
 
@@ -200,24 +231,54 @@ class CleepHandler(FileSystemEventHandler):
200
231
  return getattr(event, "dest_path", None) or event.src_path
201
232
  return None
202
233
 
203
- def schedule_tree(self, root):
234
+ def schedule_all(self):
204
235
  """
205
- Watch root and every directory under it, except rejected names.
236
+ Install the few recursive watches needed for core and module sources.
206
237
  """
207
- for directory in iter_watch_directories(root):
208
- self._watch_directory(directory)
238
+ for path, recursive in iter_watch_roots():
239
+ self._schedule(path, recursive=recursive)
209
240
 
210
- def _watch_directory(self, path):
241
+ def _schedule(self, path, recursive=True):
211
242
  """
212
- Watch one directory. Rejected names and directories already watched are skipped.
243
+ Watch path once. Rejected names and duplicates are skipped.
213
244
  """
214
245
  if self.observer is None or not path or self._rejected_path(path):
215
- return
246
+ return False
216
247
  real = os.path.abspath(path)
217
248
  if real in self._watched or not os.path.isdir(real):
218
- return
219
- self.observer.schedule(self, real, recursive=False)
249
+ return False
250
+ self.observer.schedule(self, real, recursive=recursive)
220
251
  self._watched.add(real)
252
+ return True
253
+
254
+ def _schedule_module(self, module_path):
255
+ """
256
+ Watch a module root (non-recursive) and its content dirs. Never watches .git.
257
+ """
258
+ if not module_path or not os.path.isdir(module_path):
259
+ return
260
+ if os.path.basename(module_path) in self.REJECTED_DIRS:
261
+ return
262
+ self._schedule(module_path, recursive=False)
263
+ for sub in MODULE_CONTENT_DIRS:
264
+ self._schedule(os.path.join(module_path, sub), recursive=True)
265
+
266
+ def _maybe_schedule_new_path(self, path):
267
+ """
268
+ Add watches when a new module or module content dir appears.
269
+
270
+ Core/html/bin are already recursive, so new dirs there need no schedule.
271
+ """
272
+ if not path or self._rejected_path(path):
273
+ return
274
+ parent = os.path.dirname(path)
275
+ name = os.path.basename(path)
276
+ modules_root = os.path.abspath(config.MODULES_SRC)
277
+ if os.path.abspath(parent) == modules_root:
278
+ self._schedule_module(path)
279
+ return
280
+ if name in MODULE_CONTENT_DIRS and os.path.abspath(os.path.dirname(parent)) == modules_root:
281
+ self._schedule(path, recursive=True)
221
282
 
222
283
  def _module_name_from_path(self, path):
223
284
  prefix = config.MODULES_SRC.rstrip(os.sep) + os.sep
@@ -313,7 +374,7 @@ class CleepHandler(FileSystemEventHandler):
313
374
  Returns:
314
375
  module name (string) or None if nothing found
315
376
  """
316
- pattern = r"^%s/(.*?)/(?:frontend|backend|scripts)/.*$" % (
377
+ pattern = r"^%s/(.*?)/(?:frontend|backend|scripts)(?:/.*)?$" % (
317
378
  re.escape(config.MODULES_SRC)
318
379
  )
319
380
  # self.logger.debug('Module pattern: %s' % pattern)
@@ -364,7 +425,7 @@ class CleepHandler(FileSystemEventHandler):
364
425
  Returns:
365
426
  True if change on frontend, False otherwise
366
427
  """
367
- pattern = r"^(%s/|%s/(?:.*?)/frontend/).*$" % (
428
+ pattern = r"^(%s/|%s/(?:.*?)/frontend(?:/)?).*$" % (
368
429
  re.escape(config.HTML_SRC),
369
430
  re.escape(config.MODULES_SRC),
370
431
  )
@@ -376,16 +437,31 @@ class CleepHandler(FileSystemEventHandler):
376
437
 
377
438
  return False
378
439
 
440
+ def __change_on_scripts(self, event):
441
+ """
442
+ Detect change on module install scripts
443
+ """
444
+ pattern = r"^%s/(?:.*?)/scripts(?:/.*)?$" % (re.escape(config.MODULES_SRC))
445
+ path = self._event_path(event)
446
+ return bool(path and re.search(pattern, path))
447
+
379
448
  def _queue_change(self, event):
380
449
  frontend = self.__change_on_frontend(event)
450
+ scripts = self.__change_on_scripts(event)
381
451
  module = self.__change_on_module(event)
382
452
  core = module is None and self.__change_on_core(event)
453
+ # Scripts are synced for the next install; they do not reload Cleep.
454
+ backend = not frontend and not scripts
383
455
  if module:
384
456
  self.logger.debug(' Change on module "%s"' % module)
385
- self.actions_executor.note_change(module=module, frontend=frontend)
457
+ self.actions_executor.note_change(
458
+ module=module, frontend=frontend, backend=backend
459
+ )
386
460
  elif core:
387
461
  self.logger.debug(" Change on core")
388
- self.actions_executor.note_change(core=True, frontend=frontend)
462
+ self.actions_executor.note_change(
463
+ core=True, frontend=frontend, backend=backend
464
+ )
389
465
  else:
390
466
  self.logger.debug(" Dropped due to useless change")
391
467
 
@@ -394,22 +470,25 @@ class CleepHandler(FileSystemEventHandler):
394
470
  self.logger.debug("Event: %s" % event)
395
471
  created = self._created_directory(event)
396
472
  if created:
397
- self.schedule_tree(created)
398
473
  if self._rejected_path(created):
399
474
  self.logger.debug(" Filtered: directory")
400
475
  return
401
- module = self._module_name_from_path(created)
402
- if module:
403
- frontend = self.__change_on_frontend(event)
404
- self.logger.debug(' Change on module "%s"' % module)
405
- self.actions_executor.note_change(module=module, frontend=frontend)
476
+ self._maybe_schedule_new_path(created)
477
+ # New module root: only schedule watches; wait for content files
478
+ modules_root = os.path.abspath(config.MODULES_SRC)
479
+ if os.path.abspath(os.path.dirname(created)) == modules_root:
406
480
  return
407
- if _is_under(created, config.CORE_SRC) or _is_under(
481
+ if self.__change_on_module(event):
482
+ self._queue_change(event)
483
+ elif _is_under(created, config.CORE_SRC) or _is_under(
408
484
  created, config.HTML_SRC
409
485
  ) or _is_under(created, config.BIN_SRC):
410
486
  frontend = self.__change_on_frontend(event)
411
- self.logger.debug(" Change on core")
412
- self.actions_executor.note_change(core=True, frontend=frontend)
487
+ self.actions_executor.note_change(
488
+ core=True,
489
+ frontend=frontend,
490
+ backend=not frontend,
491
+ )
413
492
  return
414
493
 
415
494
  if self.__is_event_dropped(event):
@@ -451,25 +530,38 @@ class CleepWatchdog:
451
530
  event_handler = CleepHandler(self.actions_executor)
452
531
  observer = Observer()
453
532
  event_handler.observer = observer
454
- scheduled = 0
455
- for root in (
456
- config.CORE_SRC,
457
- config.HTML_SRC,
458
- config.BIN_SRC,
459
- config.MODULES_SRC,
460
- ):
461
- before = len(event_handler._watched)
462
- event_handler.schedule_tree(root)
463
- scheduled += len(event_handler._watched) - before
464
-
465
- if not scheduled:
533
+ try:
534
+ event_handler.schedule_all()
535
+ except OSError as error:
536
+ self.logger.error(
537
+ "Unable to start filesystem watches (%s). "
538
+ "Check fs.inotify.max_user_instances / max_user_watches",
539
+ error,
540
+ )
541
+ self.actions_executor.stop()
542
+ return False
543
+
544
+ if not event_handler._watched:
466
545
  self.logger.error(
467
546
  'Please get Cleep sources using "cleep-cli getcore" command'
468
547
  )
548
+ self.actions_executor.stop()
469
549
  return False
470
550
 
471
- self.logger.info('Cleep watchdog is running on "%s" (CTRL-C to stop)' % path)
472
- observer.start()
551
+ self.logger.info(
552
+ 'Cleep watchdog is running on "%s" (%d watches, CTRL-C to stop)'
553
+ % (path, len(event_handler._watched))
554
+ )
555
+ try:
556
+ observer.start()
557
+ except OSError as error:
558
+ self.logger.error(
559
+ "Unable to start filesystem watches (%s). "
560
+ "Check fs.inotify.max_user_instances / max_user_watches",
561
+ error,
562
+ )
563
+ self.actions_executor.stop()
564
+ return False
473
565
  try:
474
566
  while True:
475
567
  time.sleep(0.25)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.44.0
3
+ Version: 1.44.2
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ import time
8
+ import unittest
9
+ from unittest.mock import Mock, patch
10
+
11
+ from watchdog.events import (
12
+ DirCreatedEvent,
13
+ FileDeletedEvent,
14
+ FileModifiedEvent,
15
+ FileMovedEvent,
16
+ )
17
+ from watchdog.observers import Observer
18
+
19
+ from cleepcli import config
20
+ from cleepcli.watch import ActionsExecutor, CleepHandler, iter_watch_roots
21
+
22
+
23
+ class IterWatchRootsTests(unittest.TestCase):
24
+ def test_skips_module_git_and_watches_content_dirs(self):
25
+ repo = tempfile.mkdtemp()
26
+ try:
27
+ core = os.path.join(repo, "cleep")
28
+ html = os.path.join(repo, "html")
29
+ bin_dir = os.path.join(repo, "bin")
30
+ modules = os.path.join(repo, "modules")
31
+ os.makedirs(os.path.join(core, "libs"))
32
+ os.makedirs(html)
33
+ os.makedirs(bin_dir)
34
+ os.makedirs(os.path.join(modules, "audio", ".git", "objects", "ab"))
35
+ os.makedirs(os.path.join(modules, "audio", "backend"))
36
+ os.makedirs(os.path.join(modules, "audio", "frontend"))
37
+ os.makedirs(os.path.join(modules, "audio", "tests"))
38
+
39
+ with patch.object(config, "CORE_SRC", core), patch.object(
40
+ config, "HTML_SRC", html
41
+ ), patch.object(config, "BIN_SRC", bin_dir), patch.object(
42
+ config, "MODULES_SRC", modules
43
+ ):
44
+ roots = list(iter_watch_roots())
45
+ finally:
46
+ shutil.rmtree(repo)
47
+
48
+ paths = {path for path, _recursive in roots}
49
+ recursive = {path: recursive for path, recursive in roots}
50
+ self.assertIn(core, paths)
51
+ self.assertTrue(recursive[core])
52
+ self.assertIn(html, paths)
53
+ self.assertIn(bin_dir, paths)
54
+ self.assertIn(modules, paths)
55
+ self.assertFalse(recursive[modules])
56
+ self.assertIn(os.path.join(modules, "audio"), paths)
57
+ self.assertFalse(recursive[os.path.join(modules, "audio")])
58
+ self.assertIn(os.path.join(modules, "audio", "backend"), paths)
59
+ self.assertIn(os.path.join(modules, "audio", "frontend"), paths)
60
+ self.assertTrue(recursive[os.path.join(modules, "audio", "backend")])
61
+ self.assertFalse(any(".git" in path.split(os.sep) for path in paths))
62
+ self.assertFalse(any(path.endswith(os.path.join("audio", "tests")) for path in paths))
63
+ # 3 core trees + modules root + module root + 2 content dirs
64
+ self.assertEqual(len(roots), 7)
65
+
66
+
67
+ class HandlerTests(unittest.TestCase):
68
+ def setUp(self):
69
+ self.executor = Mock()
70
+ self.handler = CleepHandler(self.executor)
71
+
72
+ def test_git_file_is_ignored(self):
73
+ path = os.path.join(
74
+ config.MODULES_SRC, "cleepbus.tangb", ".git", "objects", "ab", "deadbeef"
75
+ )
76
+ self.handler.on_any_event(FileModifiedEvent(path))
77
+ self.executor.note_change.assert_not_called()
78
+
79
+ def test_tests_file_is_ignored(self):
80
+ path = os.path.join(config.CORE_SRC, "libs", "tests", "test_rpcserver.py")
81
+ self.handler.on_any_event(FileModifiedEvent(path))
82
+ self.executor.note_change.assert_not_called()
83
+
84
+ def test_core_file_is_queued(self):
85
+ path = os.path.join(config.CORE_SRC, "rpcserver.py")
86
+ self.handler.on_any_event(FileModifiedEvent(path))
87
+ self.executor.note_change.assert_called_once_with(
88
+ core=True, frontend=False, backend=True
89
+ )
90
+
91
+ def test_atomic_save_move_is_queued(self):
92
+ src = os.path.join(config.CORE_SRC, ".rpcserver.py.tmp")
93
+ dest = os.path.join(config.CORE_SRC, "rpcserver.py")
94
+ self.handler.on_any_event(FileMovedEvent(src, dest))
95
+ self.executor.note_change.assert_called_once_with(
96
+ core=True, frontend=False, backend=True
97
+ )
98
+
99
+ def test_module_file_delete_is_queued(self):
100
+ path = os.path.join(config.MODULES_SRC, "audio", "backend", "audio.py")
101
+ self.handler.on_any_event(FileDeletedEvent(path))
102
+ self.executor.note_change.assert_called_once_with(
103
+ module="audio", frontend=False, backend=True
104
+ )
105
+
106
+ def test_module_scripts_do_not_restart_backend(self):
107
+ path = os.path.join(config.MODULES_SRC, "audio", "scripts", "postinst.sh")
108
+ self.handler.on_any_event(FileModifiedEvent(path))
109
+ self.executor.note_change.assert_called_once_with(
110
+ module="audio", frontend=False, backend=False
111
+ )
112
+
113
+ def test_module_frontend_is_queued(self):
114
+ path = os.path.join(config.MODULES_SRC, "audio", "frontend", "desc.json")
115
+ self.handler.on_any_event(FileModifiedEvent(path))
116
+ self.executor.note_change.assert_called_once_with(
117
+ module="audio", frontend=True, backend=False
118
+ )
119
+
120
+ def test_created_git_directory_is_not_watched(self):
121
+ self.handler.observer = Mock()
122
+ path = os.path.join(config.MODULES_SRC, "cleepbus.tangb", ".git")
123
+ self.handler.on_any_event(DirCreatedEvent(path))
124
+ self.handler.observer.schedule.assert_not_called()
125
+ self.executor.note_change.assert_not_called()
126
+
127
+ def test_schedule_all_is_idempotent(self):
128
+ repo = tempfile.mkdtemp()
129
+ try:
130
+ core = os.path.join(repo, "cleep")
131
+ html = os.path.join(repo, "html")
132
+ bin_dir = os.path.join(repo, "bin")
133
+ modules = os.path.join(repo, "modules")
134
+ os.makedirs(core)
135
+ os.makedirs(html)
136
+ os.makedirs(bin_dir)
137
+ os.makedirs(os.path.join(modules, "audio", "backend"))
138
+ self.handler.observer = Mock()
139
+ with patch.object(config, "CORE_SRC", core), patch.object(
140
+ config, "HTML_SRC", html
141
+ ), patch.object(config, "BIN_SRC", bin_dir), patch.object(
142
+ config, "MODULES_SRC", modules
143
+ ):
144
+ self.handler.schedule_all()
145
+ first = self.handler.observer.schedule.call_count
146
+ self.handler.schedule_all()
147
+ self.assertEqual(self.handler.observer.schedule.call_count, first)
148
+ self.assertLess(first, 20)
149
+ finally:
150
+ shutil.rmtree(repo)
151
+
152
+ def test_created_module_backend_is_scheduled(self):
153
+ self.handler.observer = Mock()
154
+ path = os.path.join(config.MODULES_SRC, "audio", "backend")
155
+ with patch("os.path.isdir", return_value=True):
156
+ self.handler.on_any_event(DirCreatedEvent(path))
157
+ self.handler.observer.schedule.assert_called_once()
158
+ args, kwargs = self.handler.observer.schedule.call_args
159
+ self.assertEqual(os.path.abspath(args[1]), os.path.abspath(path))
160
+ self.assertTrue(kwargs.get("recursive", args[2] if len(args) > 2 else False))
161
+ self.executor.note_change.assert_called_once_with(
162
+ module="audio", frontend=False, backend=True
163
+ )
164
+
165
+ def test_created_core_directory_is_queued(self):
166
+ self.handler.observer = Mock()
167
+ path = os.path.join(config.CORE_SRC, "libs", "newpkg")
168
+ self.handler.on_any_event(DirCreatedEvent(path))
169
+ self.handler.observer.schedule.assert_not_called()
170
+ self.executor.note_change.assert_called_once_with(
171
+ core=True, frontend=False, backend=True
172
+ )
173
+
174
+
175
+ class QuietPeriodTests(unittest.TestCase):
176
+ def _executor(self):
177
+ executor = ActionsExecutor("https://127.0.0.1:443")
178
+ executor.file = Mock()
179
+ executor.file.core_sync.return_value = True
180
+ executor.file.module_sync.return_value = True
181
+ executor.cleep = Mock()
182
+ executor.quiet_period = 0.2
183
+ executor.poll_interval = 0.05
184
+ return executor
185
+
186
+ def _wait_until(self, predicate, timeout=2.0):
187
+ deadline = time.time() + timeout
188
+ while time.time() < deadline:
189
+ if predicate():
190
+ return True
191
+ time.sleep(0.05)
192
+ return False
193
+
194
+ def test_burst_runs_one_sync_and_one_restart(self):
195
+ executor = self._executor()
196
+ executor.start()
197
+ try:
198
+ for _ in range(5):
199
+ executor.note_change(core=True, backend=True)
200
+ time.sleep(0.02)
201
+ self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
202
+ time.sleep(0.4)
203
+ self.assertEqual(executor.file.core_sync.call_count, 1)
204
+ executor.cleep.restart_backend.assert_called_once()
205
+ executor.cleep.restart_frontend.assert_not_called()
206
+ finally:
207
+ executor.stop()
208
+ executor.join(timeout=2)
209
+
210
+ def test_failed_sync_does_not_restart(self):
211
+ executor = self._executor()
212
+ executor.file.core_sync.return_value = False
213
+ executor.start()
214
+ try:
215
+ executor.note_change(core=True, backend=True)
216
+ self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
217
+ time.sleep(0.4)
218
+ executor.cleep.restart_backend.assert_not_called()
219
+ executor.cleep.restart_frontend.assert_not_called()
220
+ finally:
221
+ executor.stop()
222
+ executor.join(timeout=2)
223
+
224
+ def test_frontend_only_refreshes_the_ui(self):
225
+ executor = self._executor()
226
+ executor.start()
227
+ try:
228
+ executor.note_change(module="audio", frontend=True)
229
+ self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
230
+ time.sleep(0.4)
231
+ executor.cleep.restart_backend.assert_not_called()
232
+ executor.cleep.restart_frontend.assert_called_once()
233
+ finally:
234
+ executor.stop()
235
+ executor.join(timeout=2)
236
+
237
+ def test_scripts_only_does_not_restart(self):
238
+ executor = self._executor()
239
+ executor.start()
240
+ try:
241
+ executor.note_change(module="audio")
242
+ self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
243
+ time.sleep(0.4)
244
+ executor.cleep.restart_backend.assert_not_called()
245
+ executor.cleep.restart_frontend.assert_not_called()
246
+ finally:
247
+ executor.stop()
248
+ executor.join(timeout=2)
249
+
250
+ def test_core_and_modules_restart_once(self):
251
+ executor = self._executor()
252
+ executor.start()
253
+ try:
254
+ executor.note_change(core=True, backend=True)
255
+ executor.note_change(module="network", backend=True)
256
+ executor.note_change(module="audio", frontend=True)
257
+ self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
258
+ time.sleep(0.4)
259
+ self.assertEqual(executor.file.core_sync.call_count, 1)
260
+ self.assertEqual(executor.file.module_sync.call_count, 2)
261
+ executor.cleep.restart_backend.assert_called_once()
262
+ executor.cleep.restart_frontend.assert_not_called()
263
+ finally:
264
+ executor.stop()
265
+ executor.join(timeout=2)
266
+
267
+
268
+ class InotifyTests(unittest.TestCase):
269
+ def test_module_git_objects_are_not_watched(self):
270
+ repo = tempfile.mkdtemp()
271
+ observer = Observer()
272
+ try:
273
+ modules = os.path.join(repo, "modules")
274
+ git_dir = os.path.join(modules, "audio", ".git", "objects", "ab")
275
+ backend = os.path.join(modules, "audio", "backend")
276
+ os.makedirs(git_dir)
277
+ os.makedirs(backend)
278
+ os.makedirs(os.path.join(repo, "cleep"))
279
+ os.makedirs(os.path.join(repo, "html"))
280
+ os.makedirs(os.path.join(repo, "bin"))
281
+
282
+ executor = Mock()
283
+ handler = CleepHandler(executor)
284
+ handler.observer = observer
285
+ seen = []
286
+
287
+ def record(event):
288
+ seen.append(getattr(event, "src_path", "") or "")
289
+
290
+ handler.on_any_event = record
291
+ with patch.object(config, "CORE_SRC", os.path.join(repo, "cleep")), patch.object(
292
+ config, "HTML_SRC", os.path.join(repo, "html")
293
+ ), patch.object(config, "BIN_SRC", os.path.join(repo, "bin")), patch.object(
294
+ config, "MODULES_SRC", modules
295
+ ):
296
+ handler.schedule_all()
297
+ observer.start()
298
+ time.sleep(0.2)
299
+ with open(os.path.join(git_dir, "deadbeef"), "w", encoding="utf-8") as handle:
300
+ handle.write("x")
301
+ with open(os.path.join(backend, "audio.py"), "w", encoding="utf-8") as handle:
302
+ handle.write("x")
303
+ time.sleep(0.5)
304
+ finally:
305
+ observer.stop()
306
+ observer.join(timeout=2)
307
+ shutil.rmtree(repo)
308
+
309
+ self.assertTrue(any(path.endswith("audio.py") for path in seen))
310
+ self.assertFalse(any(".git" in path.split(os.sep) for path in seen))
311
+
312
+
313
+ if __name__ == "__main__":
314
+ unittest.main()
@@ -1,208 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- import os
5
- import shutil
6
- import tempfile
7
- import time
8
- import unittest
9
- from unittest.mock import Mock
10
-
11
- from watchdog.events import DirCreatedEvent, FileModifiedEvent, FileMovedEvent
12
- from watchdog.observers import Observer
13
-
14
- from cleepcli import config
15
- from cleepcli.watch import ActionsExecutor, CleepHandler, iter_watch_directories
16
-
17
-
18
- class IterWatchDirectoriesTests(unittest.TestCase):
19
- def test_skips_rejected_directories(self):
20
- root = tempfile.mkdtemp()
21
- try:
22
- os.makedirs(os.path.join(root, ".git", "objects", "ab"))
23
- os.makedirs(os.path.join(root, "pkg", "__pycache__"))
24
- os.makedirs(os.path.join(root, "pkg", "tests"))
25
- os.makedirs(os.path.join(root, "pkg", "node_modules", "lib"))
26
- os.makedirs(os.path.join(root, "pkg", "backend"))
27
- watched = set(iter_watch_directories(root))
28
- finally:
29
- shutil.rmtree(root)
30
-
31
- self.assertIn(root, watched)
32
- self.assertIn(os.path.join(root, "pkg"), watched)
33
- self.assertIn(os.path.join(root, "pkg", "backend"), watched)
34
- self.assertFalse(any(".git" in path.split(os.sep) for path in watched))
35
- self.assertFalse(any("__pycache__" in path.split(os.sep) for path in watched))
36
- self.assertFalse(any("tests" in path.split(os.sep) for path in watched))
37
- self.assertFalse(any("node_modules" in path.split(os.sep) for path in watched))
38
-
39
-
40
- class HandlerTests(unittest.TestCase):
41
- def setUp(self):
42
- self.executor = Mock()
43
- self.handler = CleepHandler(self.executor)
44
-
45
- def test_git_file_is_ignored(self):
46
- path = os.path.join(
47
- config.MODULES_SRC, "cleepbus.tangb", ".git", "objects", "ab", "deadbeef"
48
- )
49
- self.handler.on_any_event(FileModifiedEvent(path))
50
- self.executor.note_change.assert_not_called()
51
-
52
- def test_tests_file_is_ignored(self):
53
- path = os.path.join(config.CORE_SRC, "libs", "tests", "test_rpcserver.py")
54
- self.handler.on_any_event(FileModifiedEvent(path))
55
- self.executor.note_change.assert_not_called()
56
-
57
- def test_core_file_is_queued(self):
58
- path = os.path.join(config.CORE_SRC, "rpcserver.py")
59
- self.handler.on_any_event(FileModifiedEvent(path))
60
- self.executor.note_change.assert_called_once_with(core=True, frontend=False)
61
-
62
- def test_atomic_save_move_is_queued(self):
63
- src = os.path.join(config.CORE_SRC, ".rpcserver.py.tmp")
64
- dest = os.path.join(config.CORE_SRC, "rpcserver.py")
65
- self.handler.on_any_event(FileMovedEvent(src, dest))
66
- self.executor.note_change.assert_called_once_with(core=True, frontend=False)
67
-
68
- def test_created_git_directory_is_not_watched(self):
69
- self.handler.observer = Mock()
70
- path = os.path.join(config.MODULES_SRC, "cleepbus.tangb", ".git")
71
- self.handler.on_any_event(DirCreatedEvent(path))
72
- self.handler.observer.schedule.assert_not_called()
73
- self.executor.note_change.assert_not_called()
74
-
75
- def test_directory_is_scheduled_once(self):
76
- root = tempfile.mkdtemp()
77
- try:
78
- os.makedirs(os.path.join(root, "pkg"))
79
- self.handler.observer = Mock()
80
- self.handler.schedule_tree(root)
81
- self.handler.schedule_tree(root)
82
- self.assertEqual(self.handler.observer.schedule.call_count, 2)
83
- finally:
84
- shutil.rmtree(root)
85
-
86
- def test_created_core_directory_is_queued(self):
87
- self.handler.observer = Mock()
88
- path = os.path.join(config.CORE_SRC, "libs", "newpkg")
89
- self.handler.on_any_event(DirCreatedEvent(path))
90
- self.executor.note_change.assert_called_once_with(core=True, frontend=False)
91
-
92
-
93
- class QuietPeriodTests(unittest.TestCase):
94
- def _executor(self):
95
- executor = ActionsExecutor("https://127.0.0.1:443")
96
- executor.file = Mock()
97
- executor.file.core_sync.return_value = True
98
- executor.file.module_sync.return_value = True
99
- executor.cleep = Mock()
100
- executor.quiet_period = 0.2
101
- executor.poll_interval = 0.05
102
- return executor
103
-
104
- def _wait_until(self, predicate, timeout=2.0):
105
- deadline = time.time() + timeout
106
- while time.time() < deadline:
107
- if predicate():
108
- return True
109
- time.sleep(0.05)
110
- return False
111
-
112
- def test_burst_runs_one_sync_and_one_restart(self):
113
- executor = self._executor()
114
- executor.start()
115
- try:
116
- for _ in range(5):
117
- executor.note_change(core=True, frontend=False)
118
- time.sleep(0.02)
119
- self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
120
- time.sleep(0.4)
121
- self.assertEqual(executor.file.core_sync.call_count, 1)
122
- executor.cleep.restart_backend.assert_called_once()
123
- executor.cleep.restart_frontend.assert_not_called()
124
- finally:
125
- executor.stop()
126
- executor.join(timeout=2)
127
-
128
- def test_failed_sync_does_not_restart(self):
129
- executor = self._executor()
130
- executor.file.core_sync.return_value = False
131
- executor.start()
132
- try:
133
- executor.note_change(core=True, frontend=False)
134
- self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
135
- time.sleep(0.4)
136
- executor.cleep.restart_backend.assert_not_called()
137
- executor.cleep.restart_frontend.assert_not_called()
138
- finally:
139
- executor.stop()
140
- executor.join(timeout=2)
141
-
142
- def test_frontend_only_refreshes_the_ui(self):
143
- executor = self._executor()
144
- executor.start()
145
- try:
146
- executor.note_change(module="audio", frontend=True)
147
- self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
148
- time.sleep(0.4)
149
- executor.cleep.restart_backend.assert_not_called()
150
- executor.cleep.restart_frontend.assert_called_once()
151
- finally:
152
- executor.stop()
153
- executor.join(timeout=2)
154
-
155
- def test_core_and_modules_restart_once(self):
156
- executor = self._executor()
157
- executor.start()
158
- try:
159
- executor.note_change(core=True, frontend=False)
160
- executor.note_change(module="network", frontend=False)
161
- executor.note_change(module="audio", frontend=True)
162
- self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
163
- time.sleep(0.4)
164
- self.assertEqual(executor.file.core_sync.call_count, 1)
165
- self.assertEqual(executor.file.module_sync.call_count, 2)
166
- executor.cleep.restart_backend.assert_called_once()
167
- executor.cleep.restart_frontend.assert_not_called()
168
- finally:
169
- executor.stop()
170
- executor.join(timeout=2)
171
-
172
-
173
- class InotifyTests(unittest.TestCase):
174
- def test_git_objects_are_not_watched(self):
175
- root = tempfile.mkdtemp()
176
- observer = Observer()
177
- try:
178
- git_dir = os.path.join(root, ".git", "objects", "ab")
179
- os.makedirs(git_dir)
180
- os.makedirs(os.path.join(root, "pkg"))
181
- executor = Mock()
182
- handler = CleepHandler(executor)
183
- seen = []
184
-
185
- def record(event):
186
- seen.append(event.src_path)
187
-
188
- handler.on_any_event = record
189
- for directory in iter_watch_directories(root):
190
- observer.schedule(handler, directory, recursive=False)
191
- observer.start()
192
- time.sleep(0.2)
193
- with open(os.path.join(git_dir, "deadbeef"), "w", encoding="utf-8") as handle:
194
- handle.write("x")
195
- with open(os.path.join(root, "pkg", "a.py"), "w", encoding="utf-8") as handle:
196
- handle.write("x")
197
- time.sleep(0.5)
198
- finally:
199
- observer.stop()
200
- observer.join(timeout=2)
201
- shutil.rmtree(root)
202
-
203
- self.assertTrue(any(path.endswith("a.py") for path in seen))
204
- self.assertFalse(any(".git" in path.split(os.sep) for path in seen))
205
-
206
-
207
- if __name__ == "__main__":
208
- unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes