pw_presubmit: Add with_filter() method to Check

Rename _Check to Check so it can be used as a decorator. Then add a
with_filter() method to Check objects, so it's simple to reuse imported
checks but apply additional filters to them.

Change-Id: I3917e2d7a5fa74577eba5599b024dcf4d102c112
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/60920
Reviewed-by: Wyatt Hepler <hepler@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_presubmit/docs.rst b/pw_presubmit/docs.rst
index 063f522..139026d 100644
--- a/pw_presubmit/docs.rst
+++ b/pw_presubmit/docs.rst
@@ -118,9 +118,9 @@
 
 Python Checks
 =============
-There are two checks in the ``pw_presubmit.python_checks`` module, ``gn_lint``
+There are two checks in the ``pw_presubmit.python_checks`` module, ``gn_pylint``
 and ``gn_python_check``. They assume there's a top-level ``python`` GN target.
-``gn_lint`` runs Pylint and Mypy checks and ``gn_python_check`` runs Pylint,
+``gn_pylint`` runs Pylint and Mypy checks and ``gn_python_check`` runs Pylint,
 Mypy, and all Python tests.
 
 Inclusive Language
@@ -228,7 +228,7 @@
       # Include the upstream inclusive language check.
       inclusive_language.inclusive_language,
       # Include just the lint-related Python checks.
-      python_checks.lint_checks(exclude=PATH_EXCLUSIONS),
+      python_checks.gn_pylint.with_filter(exclude=PATH_EXCLUSIONS),
   )
 
   FULL = (
@@ -236,7 +236,7 @@
       release_build,
       # Use the upstream Python checks, with custom path filters applied.
       # Checks listed multiple times are only run once.
-      python_checks.all_checks(exclude=PATH_EXCLUSIONS),
+      python_checks.gn_python_check.with_filter(exclude=PATH_EXCLUSIONS),
   )
 
   PROGRAMS = pw_presubmit.Programs(quick=QUICK, full=FULL)
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 3b165a9..e8d292c 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -840,7 +840,7 @@
 
 LINTFORMAT = (
     _LINTFORMAT,
-    python_checks.gn_lint,
+    python_checks.gn_python_lint,
 )
 
 QUICK = (
diff --git a/pw_presubmit/py/pw_presubmit/presubmit.py b/pw_presubmit/py/pw_presubmit/presubmit.py
index dc82110..d9baea9 100644
--- a/pw_presubmit/py/pw_presubmit/presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/presubmit.py
@@ -37,6 +37,8 @@
 See pigweed_presbumit.py for an example of how to define presubmit checks.
 """
 
+from __future__ import annotations
+
 import collections
 import contextlib
 import dataclasses
@@ -251,12 +253,12 @@
         return not failed and not skipped
 
     def apply_filters(
-            self, program: Sequence[Callable]
-    ) -> List[Tuple['_Check', Sequence[Path]]]:
+            self,
+            program: Sequence[Callable]) -> List[Tuple[Check, Sequence[Path]]]:
         """Returns list of (check, paths) for checks that should run."""
-        checks = [c if isinstance(c, _Check) else _Check(c) for c in program]
+        checks = [c if isinstance(c, Check) else Check(c) for c in program]
         filter_to_checks: Dict[_Filter,
-                               List[_Check]] = collections.defaultdict(list)
+                               List[Check]] = collections.defaultdict(list)
 
         for check in checks:
             filter_to_checks[check.filter].append(check)
@@ -265,9 +267,9 @@
         return [(c, check_to_paths[c]) for c in checks if c in check_to_paths]
 
     def _map_checks_to_paths(
-        self, filter_to_checks: Dict[_Filter, List['_Check']]
-    ) -> Dict['_Check', Sequence[Path]]:
-        checks_to_paths: Dict[_Check, Sequence[Path]] = {}
+        self, filter_to_checks: Dict[_Filter, List[Check]]
+    ) -> Dict[Check, Sequence[Path]]:
+        checks_to_paths: Dict[Check, Sequence[Path]] = {}
 
         posix_paths = tuple(p.as_posix() for p in self._relative_paths)
 
@@ -469,7 +471,11 @@
     return presubmit.run(program, keep_going)
 
 
-class _Check:
+def _make_str_tuple(value: Union[Iterable[str], str]) -> Tuple[str, ...]:
+    return tuple([value] if isinstance(value, str) else value)
+
+
+class Check:
     """Wraps a presubmit check function.
 
     This class consolidates the logic for running and logging a presubmit check.
@@ -485,9 +491,22 @@
         self.filter: _Filter = path_filter
         self.always_run: bool = always_run
 
-        # Since _Check wraps a presubmit function, adopt that function's name.
+        # Since Check wraps a presubmit function, adopt that function's name.
         self.__name__ = self._check.__name__
 
+    def with_filter(
+        self,
+        endswith: Iterable[str] = '',
+        exclude: Iterable[Union[Pattern[str], str]] = ()
+    ) -> Check:
+        return Check(check_function=self._check,
+                     path_filter=_Filter(endswith=self.filter.endswith +
+                                         _make_str_tuple(endswith),
+                                         exclude=self.filter.exclude +
+                                         tuple(re.compile(e)
+                                               for e in exclude)),
+                     always_run=self.always_run)
+
     @property
     def name(self):
         return self.__name__
@@ -530,7 +549,7 @@
         return _Result.PASS
 
     def __call__(self, ctx: PresubmitContext, *args, **kwargs):
-        """Calling a _Check calls its underlying function directly.
+        """Calling a Check calls its underlying function directly.
 
       This makes it possible to call functions wrapped by @filter_paths. The
       prior filters are ignored, so new filters may be applied.
@@ -564,13 +583,9 @@
              if required_args else ''))
 
 
-def _make_str_tuple(value: Iterable[str]) -> Tuple[str, ...]:
-    return tuple([value] if isinstance(value, str) else value)
-
-
-def filter_paths(endswith: Iterable[str] = (''),
+def filter_paths(endswith: Iterable[str] = '',
                  exclude: Iterable[Union[Pattern[str], str]] = (),
-                 always_run: bool = False) -> Callable[[Callable], _Check]:
+                 always_run: bool = False) -> Callable[[Callable], Check]:
     """Decorator for filtering the paths list for a presubmit check function.
 
     Path filters only apply when the function is used as a presubmit check.
@@ -586,10 +601,10 @@
         a wrapped version of the presubmit function
     """
     def filter_paths_for_function(function: Callable):
-        return _Check(function,
-                      _Filter(_make_str_tuple(endswith),
-                              tuple(re.compile(e) for e in exclude)),
-                      always_run=always_run)
+        return Check(function,
+                     _Filter(_make_str_tuple(endswith),
+                             tuple(re.compile(e) for e in exclude)),
+                     always_run=always_run)
 
     return filter_paths_for_function
 
diff --git a/pw_presubmit/py/pw_presubmit/python_checks.py b/pw_presubmit/py/pw_presubmit/python_checks.py
index 67295d0..f793fec 100644
--- a/pw_presubmit/py/pw_presubmit/python_checks.py
+++ b/pw_presubmit/py/pw_presubmit/python_checks.py
@@ -46,12 +46,17 @@
 
 # TODO(mohrr) Remove gn_check=False when it passes for all downstream projects.
 @filter_paths(endswith=_PYTHON_EXTENSIONS)
-def gn_lint(ctx: pw_presubmit.PresubmitContext) -> None:
+def gn_python_lint(ctx: pw_presubmit.PresubmitContext) -> None:
     build.gn_gen(ctx.root, ctx.output_dir, gn_check=False)
     build.ninja(ctx.output_dir, 'python.lint')
 
 
-_LINT_CHECKS = (gn_lint, )
+# TODO(mohrr) Remove gn_lint when downstream projects no longer reference it.
+gn_lint = gn_python_lint
+
+# TODO(pwbug/454) Remove after downstream projects switch to using functions
+# directly.
+_LINT_CHECKS = (gn_python_lint, )
 _ALL_CHECKS = (gn_python_check, )