Repository: encode/uvicorn

Issue:
Interactions between --reload-dir, --reload-include, and --reload-exclude are not very intuitive or well explained (and may even be wrong)
### Checklist

- [X] The bug is reproducible against the latest release or `master`.
- [X] There are no similar issues or pull requests to fix it yet.

### Describe the bug

I am trying to get `uvicorn` to automatically reload in a dev environment based on source file changes and config file changes. I have two Python src directories (`/path/to/first_dir` and `/path/to/second_dir`) and a config directory (`/path/to/env_dir`) which contains `*.env` files that my application loads.

It is _**really**_ hard to understand what `uvicorn` intends to do, much less what it actually does with regard to tuned reload behavior. Consider:

``` sh
% uvicorn \
     --reload \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/first_dir \
     --reload-dir /path/to/second_dir \
     mm.txns.main:app
```

~~The above will _**only**_ ignore files matching `*_flymake.py` and `flycheck_*.py` in `/path/to/first_dir`, _**not**_ `/path/to/second_dir`. So it looks like one needs to repeat the exclude patterns prior to each `--reload-dir`:~~ _UPDATE: This wasn't correct. It turns out_ nothing _was being excluded in this configuration because watchgod was not installed. This confusion is part of the bug._

``` sh
% uvicorn \
     --reload \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/first_dir \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/second_dir \
     mm.txns.main:app
```

~~There is no mention of the need for repeated uses of `--reload-exclude` _**in advance of**_ any `--reload-dir` options that I can find.~~

Further, (in what I am perceiving as a contradiction of the docs), this won't work:

``` sh
% uvicorn \
     --reload \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/first_dir \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/second_dir \
     --reload-include '*.env' \
     --reload-dir /path/to/env_dir \
     mm.txns.main:app
```

Adding, deleting, or modifying `/path/to/env_dir/foo.env` will _**not**_ trigger a reload.

The [documentation for ``--reload-exclude``](https://www.uvicorn.org/settings/#development) makes it clear (or at least strongly suggests) I should be able to expand the patterns matched for reload detection (**_emphasis_** mine):

> `--reload-exclude <glob-pattern>` - Specify a glob pattern to match files or directories which will excluded from watching. May be used multiple times. By default the following patterns are excluded: `.*, .py[cod], .sw.*, ~*`. **_These defaults can be overwritten by including them in `--reload-include`._**

That's not what I'm seeing by including `*.env` files. Then there's this, which seems to contradict the inclusion docs (**_emphasis_** mine):

> By default Uvicorn uses simple changes detection strategy that compares **_python files_** modification times few times a second. If … or you need watching of **_non python files_** you can install [watchgod](https://pypi.org/project/watchgod/) or install uvicorn with `uvicorn[standard]`, which will include watchgod.

Does that mean there's some "upper bound" of patterns uvicorn is capable of considering? Is it `*.py`? That can't be correct, because the counterexample explicitly states that `*.py[cod]` is excluded by default, but could be included via `--reload-include`. It's not clear where the limitations are, even after spending dozens of minutes reading, re-reading, re-re-reading, experimenting, etc.

If the answer is, "Use watchgod," I can't find any documentation of how to do that or what additional configuration steps I would need to meet my use case.

### Environment

[root@localhost vagrant]# uname -a
Linux localhost.localdomain 5.4.17-2136.300.7.el8uek.x86_64 #2 SMP Fri Oct 8 16:23:01 PDT 2021 x86_64 x86_64 x86_64 GNU/Linux
[root@localhost vagrant]# /opt/local/app/bin/uvicorn --version
Running uvicorn 0.16.0 with CPython 3.9.6 on Linux

### Additional context

I'm not the only person to be confused by this. [This StackOverflow question](https://stackoverflow.com/questions/64118680/reload-flag-with-uvicorn-can-we-exclude-certain-code#comment123408591_64118929) poses something very similar, but remains unanswered:

> Could you advise on how to exclude [\*.json] using this method while preserving the reload functionality? I tried: [uvicorn main:app --reload-exclude '\*.json'] however it generated this warning: WARNING: Current configuration will not reload as not all conditions are met, please refer to documentation.

When I [misfiled this against gunicorn](https://github.com/benoitc/gunicorn/issues/2722) in a fit of blindess, I noted that [this comment](https://github.com/benoitc/gunicorn/issues/2339#issuecomment-651270632) from @Andrew-Chen-Wang suggests there is some magic around merely making watchgod available to uvicorn, but fails to go into additional detail, so I have no way of verifying it (at least not without digging into a lot of code), much less figuring out how I can take advantage:

> … [I]f you have watchgod installed, uvicorn should automatically pick that up to make the reloading process better. …

He responded with:

> … In terms of resolving the issue, I believe you're not supposed to reuse reload-include several times. Instead, wrap all your values in double quotes and make them csv

If that's true, it's also not clear from the docs. It also doesn't help me, since I'm only using `--reload-include` once. (The above use of `--reload-exclude` multiple times does appear to get the behavior I want with respect to excluding various files/directories.)

_UPDATE: I'm pretty sure the single, comma-separated argument advice is wrong. [This strongly suggests](https://github.com/encode/uvicorn/blob/27f76476a14dac68a62dc2998717997607a36197/uvicorn/main.py#L85-L99) one should be able to provide `--reload-include` and `--reload-exclude` multiple times via the command line._


Discussion:
Apparently, others have been similarly bitten by the ambiguity here. See #1265, which was closed as invalid without a clarifying update to the docs. However, [this comment](https://github.com/encode/uvicorn/pull/521#issuecomment-751843304) from @euri10 suggests that uvicorn _**does**_ have the capability to watch all files, and not just those matching `*.py`. @euri10 doesn't expand on that, so I'm not sure if that means watchgod is still required to do that.

If `watchgod` is a requirement of `--reload-include` to exceed some upper bound (beyond "python files", which isn't well-defined), the documentation should make that much clearer, preferably with examples.
Further investigation suggests that @Andrew-Chen-Wang's suggestion isn't valid (unless he was speaking solely of `--reload-include`, in which case the asymmetry is even _**more**_ confusing and certainly not made clear in the existing documentation). The following does not appear to exclude any of the indicated pattens:

``` sh
% uvicorn \
     --reload \
     --reload-exclude '*_flymake.py,flycheck_*.py' \
     --reload-dir /path/to/first_dir \
     --reload-exclude '*_flymake.py,flycheck_*.py' \
     --reload-dir /path/to/second_dir \
     mm.txns.main:app
```

Resolving patch:
diff --git a/uvicorn/main.py b/uvicorn/main.py
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -87,7 +87,8 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
     "reload_includes",
     multiple=True,
     help="Set glob patterns to include while watching for files. Includes '*.py' "
-    "by default; these defaults can be overridden in `--reload-exclude`.",
+    "by default; these defaults can be overridden with `--reload-exclude`. "
+    "This option has no effect unless watchgod is installed.",
 )
 @click.option(
     "--reload-exclude",
@@ -95,7 +96,8 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
     multiple=True,
     help="Set glob patterns to exclude while watching for files. Includes "
     "'.*, .py[cod], .sw.*, ~*' by default; these defaults can be overridden "
-    "in `--reload-include`.",
+    "with `--reload-include`. This option has no effect unless watchgod is "
+    "installed.",
 )
 @click.option(
     "--reload-delay",
diff --git a/uvicorn/supervisors/statreload.py b/uvicorn/supervisors/statreload.py
--- a/uvicorn/supervisors/statreload.py
+++ b/uvicorn/supervisors/statreload.py
@@ -20,6 +20,12 @@ def __init__(
         self.reloader_name = "statreload"
         self.mtimes: Dict[Path, float] = {}
 
+        if config.reload_excludes or config.reload_includes:
+            logger.warning(
+                "--reload-include and --reload-exclude have no effect unless watchgod "
+                "is installed."
+            )
+
     def should_restart(self) -> bool:
         for file in self.iter_py_files():
             try:


Test patch: