Repository: pre-commit/pre-commit

Issue:
Cannot run --files on Windows
(pre-commit version 2.4.0) We're running in to an odd issue on Windows

```
$ pre-commit run --files my_file.py
black................................................(no files to check)Skipped
```

However, running `pre-commit run --all-files` _will_ correctly format the file. Equally, running `pre-commit run --files my_file.py` in the same repository on a Mac works fine.

It's like the file is somehow not being read in Windows... Has anybody else encountered this?

Discussion:

Patch:
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
index f6916c2..567b7cd 100644
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -72,13 +72,7 @@ def filter_by_include_exclude(
 
 
 class Classifier:
-    def __init__(self, filenames: Sequence[str]) -> None:
-        # on windows we normalize all filenames to use forward slashes
-        # this makes it easier to filter using the `files:` regex
-        # this also makes improperly quoted shell-based hooks work better
-        # see #1173
-        if os.altsep == '/' and os.sep == '\\':
-            filenames = [f.replace(os.sep, os.altsep) for f in filenames]
+    def __init__(self, filenames: Collection[str]) -> None:
         self.filenames = [f for f in filenames if os.path.lexists(f)]
 
     @functools.lru_cache(maxsize=None)
@@ -105,6 +99,22 @@ class Classifier:
         names = self.by_types(names, hook.types, hook.exclude_types)
         return tuple(names)
 
+    @classmethod
+    def from_config(
+            cls,
+            filenames: Collection[str],
+            include: str,
+            exclude: str,
+    ) -> 'Classifier':
+        # on windows we normalize all filenames to use forward slashes
+        # this makes it easier to filter using the `files:` regex
+        # this also makes improperly quoted shell-based hooks work better
+        # see #1173
+        if os.altsep == '/' and os.sep == '\\':
+            filenames = [f.replace(os.sep, os.altsep) for f in filenames]
+        filenames = filter_by_include_exclude(filenames, include, exclude)
+        return Classifier(filenames)
+
 
 def _get_skips(environ: EnvironT) -> Set[str]:
     skips = environ.get('SKIP', '')
@@ -247,10 +257,9 @@ def _run_hooks(
     """Actually run the hooks."""
     skips = _get_skips(environ)
     cols = _compute_cols(hooks)
-    filenames = filter_by_include_exclude(
+    classifier = Classifier.from_config(
         _all_filenames(args), config['files'], config['exclude'],
     )
-    classifier = Classifier(filenames)
     retval = 0
     for hook in hooks:
         retval |= _run_single_hook(
diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py
index d0244a9..a1e9352 100644
--- a/pre_commit/meta_hooks/check_hooks_apply.py
+++ b/pre_commit/meta_hooks/check_hooks_apply.py
@@ -11,10 +11,13 @@ from pre_commit.store import Store
 
 
 def check_all_hooks_match_files(config_file: str) -> int:
-    classifier = Classifier(git.get_all_files())
+    config = load_config(config_file)
+    classifier = Classifier.from_config(
+        git.get_all_files(), config['files'], config['exclude'],
+    )
     retv = 0
 
-    for hook in all_hooks(load_config(config_file), Store()):
+    for hook in all_hooks(config, Store()):
         if hook.always_run or hook.language == 'fail':
             continue
         elif not classifier.filenames_for_hook(hook):
diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py
index 30b8d81..db6865c 100644
--- a/pre_commit/meta_hooks/check_useless_excludes.py
+++ b/pre_commit/meta_hooks/check_useless_excludes.py
@@ -28,11 +28,14 @@ def exclude_matches_any(
 
 def check_useless_excludes(config_file: str) -> int:
     config = load_config(config_file)
-    classifier = Classifier(git.get_all_files())
+    filenames = git.get_all_files()
+    classifier = Classifier.from_config(
+        filenames, config['files'], config['exclude'],
+    )
     retv = 0
 
     exclude = config['exclude']
-    if not exclude_matches_any(classifier.filenames, '', exclude):
+    if not exclude_matches_any(filenames, '', exclude):
         print(
             f'The global exclude pattern {exclude!r} does not match any files',
         )


Test patch:
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
index cf9794e..2461ed5 100644
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -939,7 +939,7 @@ def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir):
         tmpdir.join('a/b/c').ensure()
         with mock.patch.object(os, 'altsep', '/'):
             with mock.patch.object(os, 'sep', '\\'):
-                classifier = Classifier((r'a\b\c',))
+                classifier = Classifier.from_config((r'a\b\c',), '', '^$')
                 assert classifier.filenames == ['a/b/c']
 
 
@@ -947,7 +947,7 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir):
     with mock.patch.object(os.path, 'lexists', return_value=True):
         with mock.patch.object(os, 'altsep', None):
             with mock.patch.object(os, 'sep', '/'):
-                classifier = Classifier((r'a/b\c',))
+                classifier = Classifier.from_config((r'a/b\c',), '', '^$')
                 assert classifier.filenames == [r'a/b\c']