Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FileNotFoundError during development #207

Open
Kludex opened this issue Nov 11, 2022 · 11 comments
Open

FileNotFoundError during development #207

Kludex opened this issue Nov 11, 2022 · 11 comments
Labels

Comments

@Kludex
Copy link
Contributor

Kludex commented Nov 11, 2022

Description

Hi Samuel 👋

I'm having a bad experience when developing on uvicorn, and I'd really appreciate some help. 🙏

Since a couple of days ago I've been facing a lot of FileNotFoundError, which happens from time to time.

Would you perhaps be able to help me here?

Full Pytest Trace
uvicorn on  master [$?] via 🐍 v3.10.5 (uvicorn3.10) on ☁️  marcelotryle@gmail.com took 51s 
❯ coverage run -m pytest
============================================================================ test session starts ============================================================================
platform linux -- Python 3.10.5, pytest-7.1.3, pluggy-1.0.0
rootdir: /home/marcelo/Development/encode/uvicorn, configfile: setup.cfg
plugins: anyio-3.6.1, mock-3.10.0, asyncio-0.15.1, time-machine-2.8.1
collected 448 items                                                                                                                                                         

tests/test_auto_detection.py ...                                                                                                                                      [  0%]
tests/test_cli.py ............                                                                                                                                        [  3%]
tests/test_config.py ..................................................                                                                                               [ 14%]
tests/test_default_headers.py ......                                                                                                                                  [ 15%]
tests/test_lifespan.py ...............                                                                                                                                [ 19%]
tests/test_main.py .........                                                                                                                                          [ 21%]
tests/test_ssl.py ....                                                                                                                                                [ 22%]
tests/test_subprocess.py ..                                                                                                                                           [ 22%]
tests/importer/test_importer.py ......                                                                                                                                [ 23%]
tests/middleware/test_logging.py ..............                                                                                                                       [ 27%]
tests/middleware/test_message_logger.py ..                                                                                                                            [ 27%]
tests/middleware/test_proxy_headers.py ...........                                                                                                                    [ 29%]
tests/middleware/test_wsgi.py ......                                                                                                                                  [ 31%]
tests/protocols/test_http.py ........................................................................................................                                 [ 54%]
tests/protocols/test_utils.py ......                                                                                                                                  [ 55%]
tests/protocols/test_websocket.py ................................................................................................................................... [ 85%]
...............................                                                                                                                                       [ 91%]
tests/supervisors/test_multiprocess.py .                                                                                                                              [ 92%]
tests/supervisors/test_reload.py .....F..FF..FF...F..F..FF..........                                                                                                  [100%]

================================================================================= FAILURES ==================================================================================
_________________________________________________ TestBaseReload.test_reload_when_python_file_is_changed[WatchFilesReload] __________________________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d6830>, touch_soon = <function touch_soon.<locals>.start at 0x7f759b2d05e0>

    @pytest.mark.parametrize(
        "reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
    )
    def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
        file = self.reload_path / "main.py"
    
        with as_cwd(self.reload_path):
            config = Config(app="tests.test_config:asgi_app", reload=True)
            reloader = self._setup_reloader(config)
    
>           changes = self._reload_tester(touch_soon, reloader, file)

tests/supervisors/test_reload.py:91: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759dde3190>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
_________________________________________ TestBaseReload.test_should_reload_when_python_file_in_subdir_is_changed[WatchFilesReload] _________________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d58d0>, touch_soon = <function touch_soon.<locals>.start at 0x7f759b2ffa30>

    @pytest.mark.parametrize(
        "reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
    )
    def test_should_reload_when_python_file_in_subdir_is_changed(
        self, touch_soon
    ) -> None:
        file = self.reload_path / "app" / "sub" / "sub.py"
    
        with as_cwd(self.reload_path):
            config = Config(app="tests.test_config:asgi_app", reload=True)
            reloader = self._setup_reloader(config)
    
>           assert self._reload_tester(touch_soon, reloader, file)

tests/supervisors/test_reload.py:108: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759dde1030>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
__________________________________ TestBaseReload.test_should_not_reload_when_python_file_in_excluded_subdir_is_changed[WatchFilesReload] ___________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d6a40>, touch_soon = <function touch_soon.<locals>.start at 0x7f759b2ff130>

    @pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
    def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(
        self, touch_soon
    ) -> None:
        sub_dir = self.reload_path / "app" / "sub"
        sub_file = sub_dir / "sub.py"
    
        with as_cwd(self.reload_path):
            config = Config(
                app="tests.test_config:asgi_app",
                reload=True,
                reload_excludes=[str(sub_dir)],
            )
            reloader = self._setup_reloader(config)
    
>           assert not self._reload_tester(touch_soon, reloader, sub_file)

tests/supervisors/test_reload.py:127: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759dde3f10>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
__________________________________________ TestBaseReload.test_reload_when_pattern_matched_file_is_changed[WatchFilesReload-True] ___________________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d6dd0>, result = True, touch_soon = <function touch_soon.<locals>.start at 0x7f759a1a0670>

    @pytest.mark.parametrize(
        "reloader_class, result", [(StatReload, False), (WatchFilesReload, True)]
    )
    def test_reload_when_pattern_matched_file_is_changed(
        self, result: bool, touch_soon
    ) -> None:
        file = self.reload_path / "app" / "js" / "main.js"
    
        with as_cwd(self.reload_path):
            config = Config(
                app="tests.test_config:asgi_app", reload=True, reload_includes=["*.js"]
            )
            reloader = self._setup_reloader(config)
    
>           assert bool(self._reload_tester(touch_soon, reloader, file)) == result

tests/supervisors/test_reload.py:145: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759dde69e0>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
____________________________________ TestBaseReload.test_should_not_reload_when_exclude_pattern_match_file_is_changed[WatchFilesReload] _____________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d6fe0>, touch_soon = <function touch_soon.<locals>.start at 0x7f759a1a0e50>

    @pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
    def test_should_not_reload_when_exclude_pattern_match_file_is_changed(
        self, touch_soon
    ) -> None:
        python_file = self.reload_path / "app" / "src" / "main.py"
        css_file = self.reload_path / "app" / "css" / "main.css"
        js_file = self.reload_path / "app" / "js" / "main.js"
    
        with as_cwd(self.reload_path):
            config = Config(
                app="tests.test_config:asgi_app",
                reload=True,
                reload_includes=["*"],
                reload_excludes=["*.js"],
            )
            reloader = self._setup_reloader(config)
    
>           assert self._reload_tester(touch_soon, reloader, python_file)

tests/supervisors/test_reload.py:166: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759a17f430>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1/.dotted_dir', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_first', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_second', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_third', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/ext']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1/.dotted_dir', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_first', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_second', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_third', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/ext']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
_____________________________________________ TestBaseReload.test_should_not_reload_when_dot_file_is_changed[WatchFilesReload] ______________________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d7430>, touch_soon = <function touch_soon.<locals>.start at 0x7f759b354310>

    @pytest.mark.parametrize(
        "reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
    )
    def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
        file = self.reload_path / ".dotted"
    
        with as_cwd(self.reload_path):
            config = Config(app="tests.test_config:asgi_app", reload=True)
            reloader = self._setup_reloader(config)
    
>           assert not self._reload_tester(touch_soon, reloader, file)

tests/supervisors/test_reload.py:182: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759de1d990>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
___________________________________________ TestBaseReload.test_should_reload_when_directories_have_same_prefix[WatchFilesReload] ___________________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d7790>, touch_soon = <function touch_soon.<locals>.start at 0x7f759dcfd090>

    @pytest.mark.parametrize(
        "reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
    )
    def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> None:
        app_dir = self.reload_path / "app"
        app_file = app_dir / "src" / "main.py"
        app_first_dir = self.reload_path / "app_first"
        app_first_file = app_first_dir / "src" / "main.py"
    
        with as_cwd(self.reload_path):
            config = Config(
                app="tests.test_config:asgi_app",
                reload=True,
                reload_dirs=[str(app_dir), str(app_first_dir)],
            )
            reloader = self._setup_reloader(config)
    
>           assert self._reload_tester(touch_soon, reloader, app_file)

tests/supervisors/test_reload.py:203: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759a137730>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_first']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app', '/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app_first']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
_________________________________________ TestBaseReload.test_should_not_reload_when_only_subdirectory_is_watched[WatchFilesReload] _________________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d7af0>, touch_soon = <function touch_soon.<locals>.start at 0x7f759a1a0f70>

    @pytest.mark.parametrize(
        "reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
    )
    def test_should_not_reload_when_only_subdirectory_is_watched(
        self, touch_soon
    ) -> None:
        app_dir = self.reload_path / "app"
        app_dir_file = self.reload_path / "app" / "src" / "main.py"
        root_file = self.reload_path / "main.py"
    
        config = Config(
            app="tests.test_config:asgi_app",
            reload=True,
            reload_dirs=[str(app_dir)],
        )
        reloader = self._setup_reloader(config)
    
>       assert self._reload_tester(touch_soon, reloader, app_dir_file)

tests/supervisors/test_reload.py:225: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759ddab280>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/home/marcelo/Development/encode/uvicorn/tests/__pycache__"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
__________________________________________________________ TestBaseReload.test_override_defaults[WatchFilesReload] __________________________________________________________

self = <test_reload.TestBaseReload object at 0x7f759e5d7d00>, touch_soon = <function touch_soon.<locals>.start at 0x7f759a1a2e60>

    @pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
    def test_override_defaults(self, touch_soon) -> None:
        dotted_file = self.reload_path / ".dotted"
        dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
        python_file = self.reload_path / "main.py"
    
        with as_cwd(self.reload_path):
            config = Config(
                app="tests.test_config:asgi_app",
                reload=True,
                # We need to add *.txt otherwise no regular files will match
                reload_includes=[".*", "*.txt"],
                reload_excludes=["*.py"],
            )
            reloader = self._setup_reloader(config)
    
>           assert self._reload_tester(touch_soon, reloader, dotted_file)

tests/supervisors/test_reload.py:248: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/supervisors/test_reload.py:64: in _reload_tester
    return next(reloader)
uvicorn/supervisors/basereload.py:64: in __next__
    return self.should_restart()
uvicorn/supervisors/watchfilesreload.py:85: in should_restart
    changes = next(self.watcher)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

watch_filter = None, debounce = 1600, step = 50, stop_event = <threading.Event object at 0x7f759ddbb850>

    def watch(
        *paths: Union[Path, str],
        watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
        debounce: int = 1_600,
        step: int = 50,
        stop_event: Optional['AbstractEvent'] = None,
        rust_timeout: int = 5_000,
        yield_on_timeout: bool = False,
        debug: bool = False,
        raise_interrupt: bool = True,
        force_polling: Optional[bool] = None,
        poll_delay_ms: int = 300,
        recursive: bool = True,
    ) -> Generator[Set[FileChange], None, None]:
        """
        Watch one or more paths and yield a set of changes whenever files change.
    
        The paths watched can be directories or files, directories are watched recursively - changes in subdirectories
        are also detected.
    
        #### Force polling
    
        Notify will fall back to file polling if it can't use file system notifications, but we also force notify
        to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable
        force polling thus:
    
        * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty:
           * if the value is `false`, `disable` or `disabled`, force polling is disabled
           * otherwise, force polling is enabled
        * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux)
    
        Args:
            *paths: filesystem paths to watch.
            watch_filter: callable used to filter out changes which are not important, you can either use a raw callable
                or a [`BaseFilter`][watchfiles.BaseFilter] instance,
                defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`.
            debounce: maximum time in milliseconds to group changes over before yielding them.
            step: time to wait for new changes in milliseconds, if no changes are detected in this time, and
                at least one change has been detected, the changes are yielded.
            stop_event: event to stop watching, if this is set, the generator will stop iteration,
                this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`.
            rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout.
            yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected.
            debug: whether to print information about all filesystem changes in rust to stdout.
            raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating.
            force_polling: See [Force polling](#force-polling) above.
            poll_delay_ms: delay between polling for changes, only used if `force_polling=True`.
            recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the
                top-level directory, default is `True`.
    
        Yields:
            The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s.
    
        ```py title="Example of watch usage"
        from watchfiles import watch
    
        for changes in watch('./first/dir', './second/dir', raise_interrupt=False):
            print(changes)
        ```
        """
        force_polling = _default_force_polling(force_polling)
>       with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher:
E       FileNotFoundError: OS file watch limit reached. about ["/tmp/pytest-of-marcelo/pytest-1/reload_directory1/app/css"]

../../../.pyenv/versions/uvicorn3.10/lib/python3.10/site-packages/watchfiles/main.py:119: FileNotFoundError
--------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------
INFO:     Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO:     Started reloader process [28645] using WatchFiles
----------------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------------
INFO     uvicorn.error:config.py:353 Will watch for changes in these directories: ['/tmp/pytest-of-marcelo/pytest-1/reload_directory1']
INFO     uvicorn.error:basereload.py:72 Started reloader process [28645] using WatchFiles
====================================================================== 9 failed, 439 passed in 51.93s =======================================================================

Example Code

No response

Watchfiles Output

No response

Operating System & Architecture

Linux-5.15.0-52-generic-x86_64-with-glibc2.31
#58~20.04.1-Ubuntu SMP Thu Oct 13 13:09:46 UTC 2022

Environment

No response

Python & Watchfiles Version

python: 3.10.5 (main, Jul 9 2022, 13:39:02) [GCC 9.4.0], watchfiles: 0.18.1

Rust & Cargo Version

No response

@Kludex Kludex added the bug label Nov 11, 2022
@samuelcolvin
Copy link
Owner

Thanks for reporting, do the errors go away if your revert to v18.0?

@Kludex
Copy link
Contributor Author

Kludex commented Nov 11, 2022

If my memory doesn't fail me, I was able to reproduce it yesterday with 0.15.0, and then I bumped it to 0.18.1. Assume that I can be mistaken.

I've downgraded to try, but as I said, "[...] happens from time to time", so I can't easily reproduce.

@samuelcolvin
Copy link
Owner

samuelcolvin commented Nov 11, 2022

Okay, I see what's happening.

This is not actually "file not found", it's that you're hitting the OS file watching limit.

See the Error is FileNotFoundError: OS file watch limit reached. about, which comes from

.map_err(|e| PyFileNotFoundError::new_err(format!("{}", e)))?;

Basically, the only time I've seen an error there was when the path didn't exist, so I created a PyFileNotFoundError, obviously we should look at the error type or error message and use a different exception.

I'm 99.9% sure this won't be related to the recent v0.18.1 release.


So the next question is what should you do?

Either watchfiles or your test suite is watching too many files, or your IDE and other apps are watching lots of files, then watchfiles is just going over the limit.

If the problem is just that you're watching lots of files legitimately, you can increase max_user_watches, see here for example.

If the problem is that watchfiles is somehow not "un-watching" fies when the watcher is dropped, we'll need to make a change in watchfiles. I'll do some investigation.

@Kludex
Copy link
Contributor Author

Kludex commented Nov 11, 2022

If the problem is that watchfiles is somehow not "un-watching" fies when the watcher is dropped, we'll need to make a change in watchfiles. I'll do some investigation.

Thanks.

@samuelcolvin
Copy link
Owner

samuelcolvin commented Nov 11, 2022

Okay, I've fixed the wrong-error issue in #208.

I've also done some digging into how many files watchfiles is watching, using inotify-info.

I ran the following script, then monitored the number of "watches" from the "python3.10" process using inotify-info.

import watchfiles

print(f'Watchfiles v{watchfiles.__version__}')

for i in range(100):
    for changes in watchfiles.watch('/home/samuel/code/pydantic/', rust_timeout=1, yield_on_timeout=True):
        print(i, changes)
        break

I don't see the number of watches increase, also there are 127196 files in the pydantic directory (multiple venvs), so if the watches weren't being dropped between steps, we should hit the OS limit fairly quickly.

I think the problem is probably with either a test which watches a big directory, or your system is close to the watch limit.

Maybe you could have a quick look with inotify-info?

samuelcolvin added a commit that referenced this issue Nov 11, 2022
* better error on OS watcher limit, #207

* skip message check on other OSes

* fix linting

* more error details, skip build on PR

* fix docs build, support rust 1.56

* file not found on force polling

* fix error typo
@Kludex
Copy link
Contributor Author

Kludex commented Nov 15, 2022

Am I supposed to use inotify-info on the python process running pytest?

@samuelcolvin
Copy link
Owner

I just ran it and it showed which processes where holding the most watches.

@ZaSkittles
Copy link

Hi, I've been seeing a similar issue where I suspect the root cause is not a missing file/folder. I was wondering if it would be possible to cut a new release which includes the fix

@samuelcolvin
Copy link
Owner

I think your link is broken and you mean #208.

I'll try to make a new release soon.

@samuelcolvin
Copy link
Owner

done, building now, with luck should be released in a ~30mins.

@ZaSkittles
Copy link

ZaSkittles commented Mar 27, 2023

I think your link is broken and you mean #208.

Hahah yes, oops.

done, building now, with luck should be released in a ~30mins.

Thank you so very much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants