Skip to content

Commit 58386d8

Browse files
authored
Fixes, and clean-up (#1084)
* fix: pin tests deps * fix: Mypy * feat: skip type checking blocks in coverage * fix: remove unneeded `echo` functions * docs: tweak * fixes * fix
1 parent db698a5 commit 58386d8

File tree

14 files changed

+94
-124
lines changed

14 files changed

+94
-124
lines changed

.github/workflows/tests.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- name: Set up Python
2222
uses: actions/setup-python@v5
2323
with:
24-
python-version: "3.11"
24+
python-version: "3.13"
2525
cache: pip
2626

2727
- name: Install dependencies
@@ -58,7 +58,7 @@ jobs:
5858
- "3.10"
5959
- "3.11"
6060
- "3.12"
61-
- "3.13-dev"
61+
- "3.13"
6262
- "pypy-3.9"
6363
exclude:
6464
- os:

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Install from PyPI using ``pip``:
128128
$ python -m pip install -U watchdog
129129
130130
# or to install the watchmedo utility:
131-
$ python -m pip install -U "watchdog[watchmedo]"
131+
$ python -m pip install -U 'watchdog[watchmedo]'
132132
133133
Install from source:
134134

changelog.rst

+18-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,25 @@ Changelog
88

99
2024-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v5.0.3...HEAD>`__
1010

11+
- Pin test dependecies.
12+
- [docs] Add typing info to quick start. (`#1082 <https://github.com/gorakhargosh/watchdog/pull/1082>`__)
1113
- [inotify] Use of ``select.poll()`` instead of deprecated ``select.select()``, if available. (`#1078 <https://github.com/gorakhargosh/watchdog/pull/1078>`__)
12-
- Thanks to our beloved contributors: @BoboTiG, @
14+
- [inotify] Fix reading inotify file descriptor after closing it. (`#1081 <https://github.com/gorakhargosh/watchdog/pull/1081>`__)
15+
- [utils] The ``stop_signal`` keyword-argument type of the ``AutoRestartTrick`` class can now be either a ``signal.Signals`` or an ``int``.
16+
- [utils] Added the ``__repr__()`` method to the ``Trick`` class.
17+
- [utils] Removed the unused ``echo_class()`` function from the ``echo`` module.
18+
- [utils] Removed the unused ``echo_instancemethod()`` function from the ``echo`` module.
19+
- [utils] Removed the unused ``echo_module()`` function from the ``echo`` module.
20+
- [utils] Removed the unused ``is_class_private_name()`` function from the ``echo`` module.
21+
- [utils] Removed the unused ``is_classmethod()`` function from the ``echo`` module.
22+
- [utils] Removed the unused ``ic_method(met()`` function from the ``echo`` module.
23+
- [utils] Removed the unused ``method_name()`` function from the ``echo`` module.
24+
- [utils] Removed the unused ``name()`` function from the ``echo`` module.
25+
- [watchmedo] Fixed Mypy issues.
26+
- [watchmedo] Added the ``__repr__()`` method to the ``HelpFormatter`` class.
27+
- [watchmedo] Removed the ``--trace`` CLI argument from the ``watchmedo log`` command, useless since events are logged by default at the ``LoggerTrick`` class level.
28+
- [windows] Fixed Mypy issues.
29+
- Thanks to our beloved contributors: @BoboTiG, @g-pichlern, @ethan-vanderheijden, @nhairs
1330

1431
5.0.3
1532
~~~~~

docs/source/quickstart.rst

+4-2
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,18 @@ To stop the program, press Control-C.
5555

5656
Typing
5757
------
58+
5859
If you are using type annotations it is important to note that
59-
`watchdog.observers.Observer` is not actually a class; it is a variable that
60+
:class:`watchdog.observers.Observer` is not actually a class; it is a variable that
6061
hold the "best" observer class available on your platform.
6162

6263
In order to correctly type your own code your should use
63-
`watchdog.observers.api.BaseObserver`. For example:
64+
:class:`watchdog.observers.api.BaseObserver`. For example::
6465

6566
from watchdog.observers import Observer
6667
from watchdog.observers.api import BaseObserver
6768

69+
6870
def my_func(obs: BaseObserver) -> None:
6971
# Do something with obs
7072
pass

pyproject.toml

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
[tool.coverage.report]
2+
exclude_also = [
3+
"if TYPE_CHECKING:",
4+
"if __name__ == __main__:",
5+
]
6+
17
[tool.mypy]
28
# Ensure we know what we do
39
warn_redundant_casts = true

requirements-tests.txt

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
eventlet; python_version < "3.13"
2-
flaky
3-
pytest
4-
pytest-cov
5-
pytest-timeout
6-
ruff
7-
sphinx
8-
mypy
9-
types-PyYAML
1+
eventlet==0.37.0; python_version < "3.13"
2+
flaky==3.8.1
3+
pytest==8.3.3
4+
pytest-cov==6.0.0
5+
pytest-timeout==2.3.1
6+
ruff==0.7.1
7+
sphinx==7.4.7; python_version <= "3.9"
8+
sphinx==8.1.3; python_version > "3.9"
9+
mypy==1.13.0
10+
types-PyYAML==6.0.12.20240917

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"Programming Language :: Python :: 3.11",
120120
"Programming Language :: Python :: 3.12",
121121
"Programming Language :: Python :: 3.13",
122+
"Programming Language :: Python :: Implementation :: CPython",
122123
"Programming Language :: Python :: Implementation :: PyPy",
123124
"Programming Language :: C",
124125
"Topic :: Software Development :: Libraries",

src/watchdog/observers/read_directory_changes.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,10 @@ def queue_events(self, timeout: float) -> None:
8686
else:
8787
self.queue_event(FileMovedEvent(src_path, dest_path))
8888
elif winapi_event.is_modified:
89-
cls = DirModifiedEvent if os.path.isdir(src_path) else FileModifiedEvent
90-
self.queue_event(cls(src_path))
89+
self.queue_event((DirModifiedEvent if os.path.isdir(src_path) else FileModifiedEvent)(src_path))
9190
elif winapi_event.is_added:
9291
isdir = os.path.isdir(src_path)
93-
cls = DirCreatedEvent if isdir else FileCreatedEvent
94-
self.queue_event(cls(src_path))
92+
self.queue_event((DirCreatedEvent if isdir else FileCreatedEvent)(src_path))
9593
if isdir and self.watch.is_recursive:
9694
for sub_created_event in generate_sub_created_events(src_path):
9795
self.queue_event(sub_created_event)

src/watchdog/tricks/__init__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
class Trick(PatternMatchingEventHandler):
4747
"""Your tricks should subclass this class."""
4848

49+
def __repr__(self) -> str:
50+
return f"<{type(self).__name__}>"
51+
4952
@classmethod
5053
def generate_yaml(cls) -> str:
5154
return f"""- {cls.__module__}.{cls.__name__}:
@@ -158,7 +161,7 @@ def __init__(
158161
patterns: list[str] | None = None,
159162
ignore_patterns: list[str] | None = None,
160163
ignore_directories: bool = False,
161-
stop_signal: signal.Signals = signal.SIGINT,
164+
stop_signal: signal.Signals | int = signal.SIGINT,
162165
kill_after: int = 10,
163166
debounce_interval_seconds: int = 0,
164167
restart_on_command_exit: bool = True,
@@ -177,7 +180,7 @@ def __init__(
177180
)
178181

179182
self.command = command
180-
self.stop_signal = stop_signal
183+
self.stop_signal = stop_signal.value if isinstance(stop_signal, signal.Signals) else stop_signal
181184
self.kill_after = kill_after
182185
self.debounce_interval_seconds = debounce_interval_seconds
183186
self.restart_on_command_exit = restart_on_command_exit

src/watchdog/utils/echo.py

+6-94
Original file line numberDiff line numberDiff line change
@@ -5,81 +5,34 @@
55
#
66
# Place into the public domain.
77

8-
"""Echo calls made to functions and methods in a module.
8+
"""Echo calls made to functions in a module.
99
1010
"Echoing" a function call means printing out the name of the function
1111
and the values of its arguments before making the call (which is more
1212
commonly referred to as "tracing", but Python already has a trace module).
1313
14-
Example: to echo calls made to functions in "my_module" do:
15-
16-
import echo
17-
import my_module
18-
echo.echo_module(my_module)
19-
20-
Example: to echo calls made to functions in "my_module.my_class" do:
21-
22-
echo.echo_class(my_module.my_class)
23-
2414
Alternatively, echo.echo can be used to decorate functions. Calls to the
2515
decorated function will be echoed.
2616
2717
Example:
2818
-------
29-
@echo.echo
30-
def my_function(args):
31-
pass
3219
20+
@echo.echo
21+
def my_function(args):
22+
pass
3323
3424
"""
3525

3626
from __future__ import annotations
3727

38-
import inspect
28+
import functools
3929
import sys
4030
from typing import TYPE_CHECKING
4131

4232
if TYPE_CHECKING:
43-
from types import MethodType
4433
from typing import Any, Callable
4534

4635

47-
def name(item: Callable) -> str:
48-
"""Return an item's name."""
49-
return item.__name__
50-
51-
52-
def is_classmethod(instancemethod: MethodType, klass: type) -> bool:
53-
"""Determine if an instancemethod is a classmethod."""
54-
return inspect.ismethod(instancemethod) and instancemethod.__self__ is klass
55-
56-
57-
def is_static_method(method: MethodType, klass: type) -> bool:
58-
"""Returns True if method is an instance method of klass."""
59-
return next(
60-
(isinstance(c.__dict__[name(method)], staticmethod) for c in klass.mro() if name(method) in c.__dict__),
61-
False,
62-
)
63-
64-
65-
def is_class_private_name(name: str) -> bool:
66-
"""Determine if a name is a class private name."""
67-
# Exclude system defined names such as __init__, __add__ etc
68-
return name.startswith("__") and not name.endswith("__")
69-
70-
71-
def method_name(method: MethodType) -> str:
72-
"""Return a method's name.
73-
74-
This function returns the name the method is accessed by from
75-
outside the class (i.e. it prefixes "private" methods appropriately).
76-
"""
77-
mname = name(method)
78-
if is_class_private_name(mname):
79-
mname = f"_{name(method.__self__.__class__)}{mname}"
80-
return mname
81-
82-
8336
def format_arg_value(arg_val: tuple[str, tuple[Any, ...]]) -> str:
8437
"""Return a string representing a (name, value) pair."""
8538
arg, val = arg_val
@@ -93,8 +46,6 @@ def echo(fn: Callable, write: Callable[[str], int | None] = sys.stdout.write) ->
9346
made to it by writing out the function's name and the arguments it was
9447
called with.
9548
"""
96-
import functools
97-
9849
# Unpack function's arg count, arg names, arg defaults
9950
code = fn.__code__
10051
argcount = code.co_argcount
@@ -111,46 +62,7 @@ def wrapped(*v: Any, **k: Any) -> Callable:
11162
nameless = list(map(repr, v[argcount:]))
11263
keyword = list(map(format_arg_value, list(k.items())))
11364
args = positional + defaulted + nameless + keyword
114-
write(f"{name(fn)}({', '.join(args)})\n")
65+
write(f"{fn.__name__}({', '.join(args)})\n")
11566
return fn(*v, **k)
11667

11768
return wrapped
118-
119-
120-
def echo_instancemethod(klass: type, method: MethodType, write: Callable[[str], int | None] = sys.stdout.write) -> None:
121-
"""Change an instancemethod so that calls to it are echoed.
122-
123-
Replacing a classmethod is a little more tricky.
124-
See: http://www.python.org/doc/current/ref/types.html
125-
"""
126-
mname = method_name(method)
127-
128-
# Avoid recursion printing method calls
129-
if mname in {"__str__", "__repr__"}:
130-
return
131-
132-
if is_classmethod(method, klass):
133-
setattr(klass, mname, classmethod(echo(method.__func__, write)))
134-
else:
135-
setattr(klass, mname, echo(method, write))
136-
137-
138-
def echo_class(klass: type, write: Callable[[str], int | None] = sys.stdout.write) -> None:
139-
"""Echo calls to class methods and static functions"""
140-
for _, method in inspect.getmembers(klass, inspect.ismethod):
141-
# In python 3 only class methods are returned here
142-
echo_instancemethod(klass, method, write)
143-
for _, fn in inspect.getmembers(klass, inspect.isfunction):
144-
if is_static_method(fn, klass):
145-
setattr(klass, name(fn), staticmethod(echo(fn, write)))
146-
else:
147-
# It's not a class or a static method, so it must be an instance method.
148-
echo_instancemethod(klass, fn, write)
149-
150-
151-
def echo_module(mod: MethodType, write: Callable[[str], int | None] = sys.stdout.write) -> None:
152-
"""Echo calls to functions and methods in a module."""
153-
for fname, fn in inspect.getmembers(mod, inspect.isfunction):
154-
setattr(mod, fname, echo(fn, write))
155-
for _, klass in inspect.getmembers(mod, inspect.isclass):
156-
echo_class(klass, write)

src/watchdog/watchmedo.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def __init__(self, *args: Any, max_help_position: int = 6, **kwargs: Any) -> Non
5050
kwargs["max_help_position"] = max_help_position
5151
super().__init__(*args, **kwargs)
5252

53+
def __repr__(self) -> str:
54+
return f"<{type(self).__name__}>"
55+
5356
def _split_lines(self, text: str, width: int) -> list[str]:
5457
text = dedent(text).strip() + "\n\n"
5558
return text.splitlines()
@@ -428,7 +431,6 @@ def tricks_generate_yaml(args: Namespace) -> None:
428431
type=float,
429432
help="Use this as the polling interval/blocking timeout.",
430433
),
431-
argument("--trace", action="store_true", help="Dumps complete dispatching trace."),
432434
argument("--debug-force-polling", action="store_true", help="[debug] Forces polling."),
433435
argument(
434436
"--debug-force-kqueue",
@@ -455,11 +457,6 @@ def tricks_generate_yaml(args: Namespace) -> None:
455457
def log(args: Namespace) -> None:
456458
"""Command to log file system events to the console."""
457459
from watchdog.tricks import LoggerTrick
458-
from watchdog.utils import echo
459-
460-
if args.trace:
461-
class_module_logger = logging.getLogger(LoggerTrick.__module__)
462-
echo.echo_class(LoggerTrick, write=lambda msg: class_module_logger.info(msg))
463460

464461
patterns, ignore_patterns = parse_patterns(args.patterns, args.ignore_patterns)
465462
handler = LoggerTrick(

tests/shell.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def mkdir(path, *, parents=False):
3636
try:
3737
os.makedirs(path)
3838
except OSError as e:
39-
if not e.errno == errno.EEXIST:
39+
if e.errno != errno.EEXIST:
4040
raise
4141
else:
4242
os.mkdir(path)

tests/test_0_watchmedo.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
import os
45
import sys
56
import time
@@ -17,7 +18,7 @@
1718

1819
from watchdog import watchmedo # noqa: E402
1920
from watchdog.events import FileModifiedEvent, FileOpenedEvent # noqa: E402
20-
from watchdog.tricks import AutoRestartTrick, ShellCommandTrick # noqa: E402
21+
from watchdog.tricks import AutoRestartTrick, LoggerTrick, ShellCommandTrick # noqa: E402
2122
from watchdog.utils import WatchdogShutdownError, platform # noqa: E402
2223

2324

@@ -233,6 +234,38 @@ def test_auto_restart_arg_parsing():
233234
assert args.debounce_interval == pytest.approx(0.2)
234235

235236

237+
def test_auto_restart_events_echoed(tmpdir, caplog):
238+
script = make_dummy_script(tmpdir, n=2)
239+
240+
with caplog.at_level(logging.INFO):
241+
trick = AutoRestartTrick([sys.executable, script])
242+
trick.on_any_event(FileOpenedEvent("foo/bar.baz"))
243+
trick.on_any_event(FileOpenedEvent("foo/bar2.baz"))
244+
trick.on_any_event(FileOpenedEvent("foo/bar3.baz"))
245+
246+
records = [record.getMessage().strip() for record in caplog.get_records(when="call")]
247+
assert records == [
248+
"on_any_event(self=<AutoRestartTrick>, event=FileOpenedEvent(src_path='foo/bar.baz', dest_path='', event_type='opened', is_directory=False, is_synthetic=False))", # noqa: E501
249+
"on_any_event(self=<AutoRestartTrick>, event=FileOpenedEvent(src_path='foo/bar2.baz', dest_path='', event_type='opened', is_directory=False, is_synthetic=False))", # noqa: E501
250+
"on_any_event(self=<AutoRestartTrick>, event=FileOpenedEvent(src_path='foo/bar3.baz', dest_path='', event_type='opened', is_directory=False, is_synthetic=False))", # noqa: E501
251+
]
252+
253+
254+
def test_logger_events_echoed(caplog):
255+
with caplog.at_level(logging.INFO):
256+
trick = LoggerTrick()
257+
trick.on_any_event(FileOpenedEvent("foo/bar.baz"))
258+
trick.on_any_event(FileOpenedEvent("foo/bar2.baz"))
259+
trick.on_any_event(FileOpenedEvent("foo/bar3.baz"))
260+
261+
records = [record.getMessage().strip() for record in caplog.get_records(when="call")]
262+
assert records == [
263+
"on_any_event(self=<LoggerTrick>, event=FileOpenedEvent(src_path='foo/bar.baz', dest_path='', event_type='opened', is_directory=False, is_synthetic=False))", # noqa: E501
264+
"on_any_event(self=<LoggerTrick>, event=FileOpenedEvent(src_path='foo/bar2.baz', dest_path='', event_type='opened', is_directory=False, is_synthetic=False))", # noqa: E501
265+
"on_any_event(self=<LoggerTrick>, event=FileOpenedEvent(src_path='foo/bar3.baz', dest_path='', event_type='opened', is_directory=False, is_synthetic=False))", # noqa: E501
266+
]
267+
268+
236269
def test_shell_command_arg_parsing():
237270
args = watchmedo.cli.parse_args(["shell-command", "--command='cmd'"])
238271
assert args.command == "'cmd'"

0 commit comments

Comments
 (0)