Repository: theacodes/nox
Pull request #530 (2021-12-24 00:53:20)

style: add pre-commit and new checks
Adds pre-commit as a check-driver (in turn, driven by Nox), and adds some new checks - I added most of my favorites, figuring we could scale back or split up as needed.

* Black (like before)
* Pre-commit-hooks: some common simple checks, like for nice end-of-files, etc.
* PyUpgrade, set to 3.6+: A fantastic tool that can keep syntax fresh and clean.
* isort (like before)
* setup-cfg-fmt: Some standardisation for setup.cfg, also keeps trove classifiers accurate
* pycln: Cleans out unused imports automatically (flake8's check but mutating)
* yesqa: cleans out noqa's that are no longer needed
* flake8: This time, with bugbear, which looks for problematic expressions that might be correct but tend to confuse readers or cause bugs
* mypy (as before)
* codespell: Looks for common misspellings. Very handy!
* pygrephooks: Avoid allowing blanket noqa or type ignores, type commends, and some other common mistakes.
* ~~shellcheck: looks for mistakes in shell scripts. Might not be needed, not sure if there are any shell scripts.~~

Removes "nox -s blacken", since some checks modify in place anyway, and everything is in git.

I'll comment on the changes a bit inline.


FollowTheProcess: I'm on board with this and agree with the changes overall, there are a lot of changes in this PR but they all seem to be small linty/style type stuff which is fine!

However since it's a reasonable change to how our CI works (albeit not to the user, they simply run `nox` as normal which is great!) I'd like to get some other maintainers thoughts/comments before merging 🙂
FollowTheProcess: Happy to merge as is or do we want to change to the pre-commit action?
henryiii: No, that action is deprecated in favor of pre-commit.Ci. Someone with permissions will have to enable pre-commit.ci for the repo.
FollowTheProcess: > No, that action is deprecated in favor of pre-commit.Ci. Someone with permissions will have to enable pre-commit.ci for the repo.

Okay, sounds like that's best done under a new PR then
FollowTheProcess: > I don't have experience with pre-commit, butit seems like it will fit well with Nox!
> 
> Also, and sorry for my ignorance, but... will pre-commit do the changes for us, or it will just fail?

It will just fail. It effectively just runs all the linters specified in the config file. You can use it how we do here (by calling `pre-commit run` either manually or from another tool like Nox) or you can install it as a git pre-commit hook so that every time you commit it will run and abort the commit if it fails.

Some people (myself included) don't like the latter option as long running checks can interrupt your commit flow but it depends on what you have configured
Tolker-KU: > > I don't have experience with pre-commit, butit seems like it will fit well with Nox!
> 
> > 
> 
> > Also, and sorry for my ignorance, but... will pre-commit do the changes for us, or it will just fail?
> 
> 
> 
> It will just fail. It effectively just runs all the linters specified in the config file. You can use it how we do here (by calling `pre-commit run` either manually or from another tool like Nox) or you can install it as a git pre-commit hook so that every time you commit it will run and abort the commit if it fails.
> 
> 
> 
> Some people (myself included) don't like the latter option as long running checks can interrupt your commit flow but it depends on what you have configured

Does that imply that this PR leaves no easy way for a developer to run black and isort to make changes as the "blacken" session does now?
henryiii: I almost always run `pre-commit run -a`, but `pre-commit install` is quite speedy, as the git hook only looks at changed files. It even handles partial stages. And git hooks can be skipped with `-n` in a pinch. I just like having control.

Pre-commit supports both changing checks (black, pyupgrade) as well as failing checks (flake8). The changing ones are better, since they can be fixed automatically by pre-commit.ci and pushed.
DiddiLeija: > Does that imply that this PR leaves no easy way for a developer to run black and isort to make changes as the "blacken" session does now?

Well... not exactly. As far as I know, if you install `pre-commit`, you can run it by yourself, and it should fix your mistakes?

BTW, I would suggest to add `--show-diff` to the pre-commit command called by Nox, to show the Git diff where the failure is?
FollowTheProcess: > Does that imply that this PR leaves no easy way for a developer to run black and isort to make changes as the "blacken" session does now?

Sorry just re-read my reply and it wasn't too clear! As @henryiii said it can do both changing and non-changing fixes, my saying it will fail was confused by pre-commits commit hook functionality which would fail the commit even if it managed to change the code e.g. black, pyupgrade etc as you'd have to re-commit the changes.

Looks like with `pre-commit run` this isn't an issue :)

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..4e5164d
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,89 @@
+ci:
+  autoupdate_commit_msg: "chore: update pre-commit hooks"
+  autofix_commit_msg: "style: pre-commit fixes"
+
+repos:
+- repo: https://github.com/psf/black
+  rev: 21.12b0
+  hooks:
+  - id: black
+
+- repo: https://github.com/pre-commit/pre-commit-hooks
+  rev: v4.0.1
+  hooks:
+  - id: check-added-large-files
+  - id: check-case-conflict
+  - id: check-merge-conflict
+  - id: check-symlinks
+  - id: check-yaml
+  - id: debug-statements
+  - id: end-of-file-fixer
+  - id: mixed-line-ending
+  - id: requirements-txt-fixer
+  - id: trailing-whitespace
+
+- repo: https://github.com/asottile/pyupgrade
+  rev: v2.29.1
+  hooks:
+  - id: pyupgrade
+    args: [--py36-plus]
+
+- repo: https://github.com/PyCQA/isort
+  rev: 5.10.1
+  hooks:
+  - id: isort
+
+- repo: https://github.com/asottile/setup-cfg-fmt
+  rev: v1.20.0
+  hooks:
+  - id: setup-cfg-fmt
+
+- repo: https://github.com/hadialqattan/pycln
+  rev: v1.1.0
+  hooks:
+  - id: pycln
+    args: [--config=pyproject.toml]
+
+- repo: https://github.com/asottile/yesqa
+  rev: v1.3.0
+  hooks:
+  - id: yesqa
+    additional_dependencies: &flake8-dependencies
+    - flake8-bugbear
+
+- repo: https://github.com/pycqa/flake8
+  rev: 4.0.1
+  hooks:
+  - id: flake8
+    exclude: docs/conf.py
+    additional_dependencies: *flake8-dependencies
+
+- repo: https://github.com/pre-commit/mirrors-mypy
+  rev: v0.920
+  hooks:
+  - id: mypy
+    files: ^nox/
+    args: [--show-error-codes]
+    additional_dependencies:
+    - types-jinja2
+    - packaging
+    - importlib_metadata
+
+- repo: https://github.com/codespell-project/codespell
+  rev: v2.1.0
+  hooks:
+  - id: codespell
+
+- repo: https://github.com/pre-commit/pygrep-hooks
+  rev: v1.9.0
+  hooks:
+  - id: python-check-blanket-noqa
+  - id: python-check-blanket-type-ignore
+  - id: python-no-log-warn
+    exclude: ^tests/test_sessions.py$
+  - id: python-no-eval
+    exclude: ^nox/manifest.py$
+  - id: python-use-type-annotations
+  - id: rst-backticks
+  - id: rst-directive-colons
+  - id: rst-inline-touching-normal
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a8a4069..cae2869 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -54,4 +54,4 @@ If you've contributed to Nox, you can get a cute little Nox sticker. Reach out t
 
 ## Getting paid
 
-Contributions to Nox can be expensed through [our Open Collective](https://opencollective.com/python-nox). The maintainers will let you know when and for how much you can expense contributions, but always feel free to ask. 
+Contributions to Nox can be expensed through [our Open Collective](https://opencollective.com/python-nox). The maintainers will let you know when and for how much you can expense contributions, but always feel free to ask.
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index 101a133..399bb9c 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -120,7 +120,7 @@ div.footer::before {
 
 div.footer {
     text-align: center;
-    color: #029be2; 
+    color: #029be2;
 }
 
 div.footer a {
diff --git a/docs/conf.py b/docs/conf.py
index 6e9befc..e78ab2c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # nox documentation build configuration file, created by
 # sphinx-quickstart on Sun Feb 28 00:05:25 2016.
diff --git a/docs/cookbook.rst b/docs/cookbook.rst
index 3ce96ad..0830d04 100644
--- a/docs/cookbook.rst
+++ b/docs/cookbook.rst
@@ -40,7 +40,7 @@ Enter the ``dev`` nox session:
 .. code-block:: python
 
     import os
-    
+
     import nox
 
     # It's a good idea to keep your dev session out of the default list
@@ -91,7 +91,7 @@ Even more so with a sprinkling of Nox:
         Kicks off an automated release process by creating and pushing a new tag.
 
         Invokes bump2version with the posarg setting the version.
-        
+
         Usage:
         $ nox -s release -- [major|minor|patch]
         """
@@ -126,4 +126,4 @@ Even more so with a sprinkling of Nox:
         session.run("git", "push", external=True)
         session.run("git", "push", "--tags", external=True)
 
-Now a simple ``nox -s release -- patch`` will automate your release (provided you have Bump2Version set up to change your files). This is especially powerful if you have a CI/CD pipeline set up!
\ No newline at end of file
+Now a simple ``nox -s release -- patch`` will automate your release (provided you have Bump2Version set up to change your files). This is especially powerful if you have a CI/CD pipeline set up!
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 57ba7f4..3441036 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -273,7 +273,7 @@ If you want to queue up (or "notify") another session from the current one, you
         session.install("pytest")
         session.run("pytest")
         # Here we queue up the test coverage session to run next
-        session.notify("coverage") 
+        session.notify("coverage")
 
     @nox.session
     def coverage(session):
diff --git a/docs/usage.rst b/docs/usage.rst
index 5b51d31..1309a84 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -128,7 +128,7 @@ Note that using this option does not change the backend for sessions where ``ven
 Forcing the sessions backend
 ----------------------------
 
-You might work in a different environment than a project's default continuous integration setttings, and might wish to get a quick way to execute the same tasks but on a different venv backend. For this purpose, you can temporarily force the backend used by **all** sessions in the current nox execution by using ``-fb <backend>`` or ``--force-venv-backend <backend>``. No exceptions are made, the backend will be forced for all sessions run whatever the other options values and nox file configuration. Supported names are ``('none', 'virtualenv', 'conda', 'venv')``.
+You might work in a different environment than a project's default continuous integration settings, and might wish to get a quick way to execute the same tasks but on a different venv backend. For this purpose, you can temporarily force the backend used by **all** sessions in the current nox execution by using ``-fb <backend>`` or ``--force-venv-backend <backend>``. No exceptions are made, the backend will be forced for all sessions run whatever the other options values and nox file configuration. Supported names are ``('none', 'virtualenv', 'conda', 'venv')``.
 
 .. code-block:: console
 
diff --git a/nox/_decorators.py b/nox/_decorators.py
index 8466138..a2e5b6a 100644
--- a/nox/_decorators.py
+++ b/nox/_decorators.py
@@ -35,14 +35,14 @@ class FunctionDecorator:
 def _copy_func(src: Callable, name: Optional[str] = None) -> Callable:
     dst = types.FunctionType(
         src.__code__,
-        src.__globals__,  # type: ignore
+        src.__globals__,  # type: ignore[attr-defined]
         name=name or src.__name__,
-        argdefs=src.__defaults__,  # type: ignore
-        closure=src.__closure__,  # type: ignore
+        argdefs=src.__defaults__,  # type: ignore[attr-defined]
+        closure=src.__closure__,  # type: ignore[attr-defined]
     )
     dst.__dict__.update(copy.deepcopy(src.__dict__))
     dst = functools.update_wrapper(dst, src)
-    dst.__kwdefaults__ = src.__kwdefaults__  # type: ignore
+    dst.__kwdefaults__ = src.__kwdefaults__  # type: ignore[attr-defined]
     return dst
 
 
diff --git a/nox/_option_set.py b/nox/_option_set.py
index 91dcc15..237020d 100644
--- a/nox/_option_set.py
+++ b/nox/_option_set.py
@@ -191,12 +191,10 @@ class OptionSet:
     def __init__(self, *args: Any, **kwargs: Any) -> None:
         self.parser_args = args
         self.parser_kwargs = kwargs
-        self.options = (
+        self.options: "collections.OrderedDict[str, Option]" = collections.OrderedDict()
+        self.groups: "collections.OrderedDict[str, OptionGroup]" = (
             collections.OrderedDict()
-        )  # type: collections.OrderedDict[str, Option]
-        self.groups = (
-            collections.OrderedDict()
-        )  # type: collections.OrderedDict[str, OptionGroup]
+        )
 
     def add_options(self, *args: Option) -> None:
         """Adds a sequence of Options to the OptionSet.
@@ -242,8 +240,8 @@ class OptionSet:
             argument = groups[option.group.name].add_argument(
                 *option.flags, help=option.help, default=option.default, **option.kwargs
             )
-            if getattr(option, "completer"):
-                setattr(argument, "completer", option.completer)
+            if option.completer:
+                argument.completer = option.completer  # type: ignore[attr-defined]
 
         return parser
 
diff --git a/nox/_parametrize.py b/nox/_parametrize.py
index eb8145d..8bcac35 100644
--- a/nox/_parametrize.py
+++ b/nox/_parametrize.py
@@ -81,7 +81,7 @@ class Param:
 def _apply_param_specs(param_specs: List[Param], f: Any) -> Any:
     previous_param_specs = getattr(f, "parametrize", None)
     new_param_specs = update_param_specs(previous_param_specs, param_specs)
-    setattr(f, "parametrize", new_param_specs)
+    f.parametrize = new_param_specs
     return f
 
 
@@ -119,7 +119,7 @@ def parametrize_decorator(
 
     # If there's only one arg_name, arg_values_list should be a single item
     # or list. Transform it so it'll work with the combine step.
-    _arg_values_list = []  # type: List[Union[Param, Iterable[Union[Any, ArgValue]]]]
+    _arg_values_list: List[Union[Param, Iterable[Union[Any, ArgValue]]]] = []
     if len(arg_names) == 1:
         # In this case, the arg_values_list can also just be a single item.
         # Must be mutable for the transformation steps
@@ -141,7 +141,7 @@ def parametrize_decorator(
         ids = []
 
     # Generate params for each item in the param_args_values list.
-    param_specs = []  # type: List[Param]
+    param_specs: List[Param] = []
     for param_arg_values, param_id in itertools.zip_longest(_arg_values_list, ids):
         if isinstance(param_arg_values, Param):
             param_spec = param_arg_values
diff --git a/nox/_version.py b/nox/_version.py
index 2e24753..feeb5c7 100644
--- a/nox/_version.py
+++ b/nox/_version.py
@@ -36,7 +36,7 @@ class InvalidVersionSpecifier(Exception):
 
 def get_nox_version() -> str:
     """Return the version of the installed Nox package."""
-    return metadata.version("nox")  # type: ignore
+    return metadata.version("nox")  # type: ignore[no-untyped-call]
 
 
 def _parse_string_constant(node: ast.AST) -> Optional[str]:  # pragma: no cover
diff --git a/nox/command.py b/nox/command.py
index b26f58d..7e3bfcd 100644
--- a/nox/command.py
+++ b/nox/command.py
@@ -32,7 +32,7 @@ class CommandFailed(Exception):
     """Raised when an executed command returns a non-success status code."""
 
     def __init__(self, reason: Optional[str] = None) -> None:
-        super(CommandFailed, self).__init__(reason)
+        super().__init__(reason)
         self.reason = reason
 
 
diff --git a/nox/logger.py b/nox/logger.py
index 599cc23..7077d8b 100644
--- a/nox/logger.py
+++ b/nox/logger.py
@@ -71,7 +71,7 @@ class NoxColoredFormatter(ColoredFormatter):
         return super().format(record)
 
 
-class LoggerWithSuccessAndOutput(logging.getLoggerClass()):  # type: ignore
+class LoggerWithSuccessAndOutput(logging.getLoggerClass()):  # type: ignore[misc]
     def __init__(self, name: str, level: int = logging.NOTSET):
         super().__init__(name, level)
         logging.addLevelName(SUCCESS, "SUCCESS")
diff --git a/nox/manifest.py b/nox/manifest.py
index f7c5fde..ccbd08c 100644
--- a/nox/manifest.py
+++ b/nox/manifest.py
@@ -65,11 +65,11 @@ class Manifest:
         global_config: argparse.Namespace,
         module_docstring: Optional[str] = None,
     ) -> None:
-        self._all_sessions = []  # type: List[SessionRunner]
-        self._queue = []  # type: List[SessionRunner]
-        self._consumed = []  # type: List[SessionRunner]
-        self._config = global_config  # type: argparse.Namespace
-        self.module_docstring = module_docstring  # type: Optional[str]
+        self._all_sessions: List[SessionRunner] = []
+        self._queue: List[SessionRunner] = []
+        self._consumed: List[SessionRunner] = []
+        self._config: argparse.Namespace = global_config
+        self.module_docstring: Optional[str] = module_docstring
 
         # Create the sessions based on the provided session functions.
         for name, func in session_functions.items():
@@ -217,7 +217,7 @@ class Manifest:
         if self._config.extra_pythons:
             # If extra python is provided, expand the func.python list to
             # include additional python interpreters
-            extra_pythons = self._config.extra_pythons  # type: List[str]
+            extra_pythons: List[str] = self._config.extra_pythons
             if isinstance(func.python, (list, tuple, set)):
                 func.python = _unique_list(*func.python, *extra_pythons)
             elif not multi and func.python:
@@ -253,7 +253,7 @@ class Manifest:
 
         # Since this function is parametrized, we need to add a distinct
         # session for each permutation.
-        parametrize = func.parametrize  # type: ignore
+        parametrize = func.parametrize  # type: ignore[attr-defined]
         calls = Call.generate_calls(func, parametrize)
         for call in calls:
             long_names = []
diff --git a/nox/py.typed b/nox/py.typed
index ec7a0d7..fd3f611 100644
--- a/nox/py.typed
+++ b/nox/py.typed
@@ -1,1 +1,1 @@
-# Marker file for PEP 561.  The nox package uses inline types.
\ No newline at end of file
+# Marker file for PEP 561.  The nox package uses inline types.
diff --git a/nox/registry.py b/nox/registry.py
index f08b188..eefcf38 100644
--- a/nox/registry.py
+++ b/nox/registry.py
@@ -22,7 +22,7 @@ from ._typing import Python
 
 F = TypeVar("F", bound=Callable[..., Any])
 
-_REGISTRY = collections.OrderedDict()  # type: collections.OrderedDict[str, Func]
+_REGISTRY: "collections.OrderedDict[str, Func]" = collections.OrderedDict()
 
 
 @overload
diff --git a/nox/sessions.py b/nox/sessions.py
index 7af4f41..f476bb2 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -128,7 +128,7 @@ class Session:
         self._runner = runner
 
     @property
-    def __dict__(self) -> "Dict[str, SessionRunner]":  # type: ignore
+    def __dict__(self) -> "Dict[str, SessionRunner]":  # type: ignore[override]
         """Attribute dictionary for object inspection.
 
         This is needed because ``__slots__`` turns off ``__dict__`` by
@@ -411,7 +411,7 @@ class Session:
         """
         venv = self._runner.venv
 
-        prefix_args = ()  # type: Tuple[str, ...]
+        prefix_args: Tuple[str, ...] = ()
         if isinstance(venv, CondaEnv):
             prefix_args = ("--prefix", venv.location)
         elif not isinstance(venv, PassthroughEnv):  # pragma: no cover
@@ -431,7 +431,7 @@ class Session:
         if "silent" not in kwargs:
             kwargs["silent"] = True
 
-        extraopts = []  # type: List[str]
+        extraopts: List[str] = []
         if auto_offline and venv.is_offline():
             logger.warning(
                 "Automatically setting the `--offline` flag as conda repo seems unreachable."
@@ -612,14 +612,14 @@ class SessionRunner:
         if backend is None or backend == "virtualenv":
             self.venv = VirtualEnv(
                 self.envdir,
-                interpreter=self.func.python,  # type: ignore
+                interpreter=self.func.python,  # type: ignore[arg-type]
                 reuse_existing=reuse_existing,
                 venv_params=self.func.venv_params,
             )
         elif backend in {"conda", "mamba"}:
             self.venv = CondaEnv(
                 self.envdir,
-                interpreter=self.func.python,  # type: ignore
+                interpreter=self.func.python,  # type: ignore[arg-type]
                 reuse_existing=reuse_existing,
                 venv_params=self.func.venv_params,
                 conda_cmd=backend,
@@ -627,7 +627,7 @@ class SessionRunner:
         elif backend == "venv":
             self.venv = VirtualEnv(
                 self.envdir,
-                interpreter=self.func.python,  # type: ignore
+                interpreter=self.func.python,  # type: ignore[arg-type]
                 reuse_existing=reuse_existing,
                 venv=True,
                 venv_params=self.func.venv_params,
diff --git a/nox/tasks.py b/nox/tasks.py
index ca0baf6..9b149af 100644
--- a/nox/tasks.py
+++ b/nox/tasks.py
@@ -14,7 +14,6 @@
 
 import ast
 import importlib.util
-import io
 import json
 import os
 import sys
@@ -51,19 +50,19 @@ def _load_and_exec_nox_module(global_config: Namespace) -> types.ModuleType:
         "user_nox_module", global_config.noxfile
     )
     if not spec:
-        raise IOError(f"Could not get module spec from {global_config.noxfile}")
+        raise OSError(f"Could not get module spec from {global_config.noxfile}")
 
     module = importlib.util.module_from_spec(spec)
     if not module:
-        raise IOError(f"Noxfile {global_config.noxfile} is not a valid python module.")
+        raise OSError(f"Noxfile {global_config.noxfile} is not a valid python module.")
 
     sys.modules["user_nox_module"] = module
 
     loader = spec.loader
     if not loader:  # pragma: no cover
-        raise IOError(f"Could not get module loader for {global_config.noxfile}")
+        raise OSError(f"Could not get module loader for {global_config.noxfile}")
     # See https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
-    loader.exec_module(module)
+    loader.exec_module(module)  # type: ignore[attr-defined]
     return module
 
 
@@ -111,7 +110,7 @@ def load_nox_module(global_config: Namespace) -> Union[types.ModuleType, int]:
             f"Failed to load Noxfile {global_config.noxfile}, no such file exists."
         )
         return 2
-    except (IOError, OSError):
+    except OSError:
         logger.exception(f"Failed to load Noxfile {global_config.noxfile}")
         return 2
 
@@ -360,7 +359,7 @@ def create_report(results: List[Result], global_config: Namespace) -> List[Resul
         return results
 
     # Write the JSON report.
-    with io.open(global_config.report, "w") as report_file:
+    with open(global_config.report, "w") as report_file:
         json.dump(
             {
                 "result": int(all(results)),
diff --git a/nox/tox_to_nox.py b/nox/tox_to_nox.py
index 7e12190..0d6c6bb 100644
--- a/nox/tox_to_nox.py
+++ b/nox/tox_to_nox.py
@@ -15,7 +15,6 @@
 """Naively converts tox.ini files into noxfile.py files."""
 
 import argparse
-import io
 import pkgutil
 from typing import Any, Iterator
 
@@ -23,7 +22,7 @@ import jinja2
 import tox.config
 
 _TEMPLATE = jinja2.Template(
-    pkgutil.get_data(__name__, "tox_to_nox.jinja2").decode("utf-8"),  # type: ignore
+    pkgutil.get_data(__name__, "tox_to_nox.jinja2").decode("utf-8"),  # type: ignore[union-attr]
     extensions=["jinja2.ext.do"],
 )
 
@@ -41,5 +40,5 @@ def main() -> None:
     config = tox.config.parseconfig([])
     output = _TEMPLATE.render(config=config, wrapjoin=wrapjoin)
 
-    with io.open(args.output, "w") as outfile:
+    with open(args.output, "w") as outfile:
         outfile.write(output)
diff --git a/nox/virtualenv.py b/nox/virtualenv.py
index 4d928ab..02a63e0 100644
--- a/nox/virtualenv.py
+++ b/nox/virtualenv.py
@@ -52,7 +52,7 @@ class ProcessEnv:
     is_sandboxed = False
 
     # Special programs that aren't included in the environment.
-    allowed_globals = ()  # type: _typing.ClassVar[Tuple[Any, ...]]
+    allowed_globals: _typing.ClassVar[Tuple[Any, ...]] = ()
 
     def __init__(
         self, bin_paths: None = None, env: Optional[Mapping[str, str]] = None
@@ -203,7 +203,7 @@ class CondaEnv(ProcessEnv):
         self.reuse_existing = reuse_existing
         self.venv_params = venv_params if venv_params else []
         self.conda_cmd = conda_cmd
-        super(CondaEnv, self).__init__()
+        super().__init__()
 
     def _clean_location(self) -> bool:
         """Deletes existing conda environment"""
@@ -285,7 +285,7 @@ class CondaEnv(ProcessEnv):
             # DNS resolution to detect situation (1) or (2).
             host = gethostbyname("repo.anaconda.com")
             return host is None
-        except:  # pragma: no cover # noqa E722
+        except BaseException:  # pragma: no cover
             return True
 
 
@@ -323,11 +323,11 @@ class VirtualEnv(ProcessEnv):
         self.location_name = location
         self.location = os.path.abspath(location)
         self.interpreter = interpreter
-        self._resolved = None  # type: Union[None, str, InterpreterNotFound]
+        self._resolved: Union[None, str, InterpreterNotFound] = None
         self.reuse_existing = reuse_existing
         self.venv_or_virtualenv = "venv" if venv else "virtualenv"
         self.venv_params = venv_params if venv_params else []
-        super(VirtualEnv, self).__init__(env={"VIRTUAL_ENV": self.location})
+        super().__init__(env={"VIRTUAL_ENV": self.location})
 
     def _clean_location(self) -> bool:
         """Deletes any existing virtual environment"""
diff --git a/nox/workflow.py b/nox/workflow.py
index 598da20..bb2d7fc 100644
--- a/nox/workflow.py
+++ b/nox/workflow.py
@@ -43,7 +43,7 @@ def execute(
         return_value = None
         for function_ in workflow:
             # Send the previous task's return value if there was one.
-            args = []  # type: List[Any]
+            args: List[Any] = []
             if return_value is not None:
                 args.append(return_value)
             return_value = function_(*args, global_config=global_config)
diff --git a/noxfile.py b/noxfile.py
index a770d9f..c4051c4 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -90,33 +90,12 @@ def cover(session):
     session.run("coverage", "erase")
 
 
-black_pins = ["black==21.12b0", "isort==5.10.1"]
-
-
-@nox.session(python="3.9")
-def blacken(session):
-    """Run black code formatter."""
-    session.install(*black_pins)
-    files = ["nox", "tests", "noxfile.py", "docs/conf.py"]
-    session.run("black", *files)
-    session.run("isort", *files)
-
-
 @nox.session(python="3.9")
 def lint(session):
-    session.install(
-        *black_pins,
-        "flake8==4.0.1",
-        "mypy==0.930",
-        "types-jinja2",
-        "packaging",
-        "importlib_metadata",
+    session.install("pre-commit")
+    session.run(
+        "pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs
     )
-    session.run("mypy", "--show-error-codes")
-    files = ["nox", "tests", "noxfile.py", "docs/conf.py"]
-    session.run("black", "--check", *files)
-    session.run("isort", "--check", *files)
-    session.run("flake8", *files)
 
 
 @nox.session
diff --git a/pyproject.toml b/pyproject.toml
index a163ffe..f332c6c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,4 +41,4 @@ strict_equality = true
 
 [[tool.mypy.overrides]]
 module = [ "argcomplete", "colorlog.*", "py", "tox.*" ]
-ignore_missing_imports = true
\ No newline at end of file
+ignore_missing_imports = true
diff --git a/setup.cfg b/setup.cfg
index 76cbc88..4cd8ac3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,9 +41,9 @@ install_requires =
     colorlog>=2.6.1,<7.0.0
     packaging>=20.9
     py>=1.4.0,<2.0.0
-    typing_extensions>=3.7.4;python_version < '3.8'
     virtualenv>=14.0.0
-    importlib_metadata;python_version < '3.8'
+    importlib-metadata;python_version < '3.8'
+    typing-extensions>=3.7.4;python_version < '3.8'
 python_requires = >=3.6
 include_package_data = True
 zip_safe = False
@@ -62,8 +62,6 @@ tox_to_nox =
 nox = py.typed
 
 [flake8]
-# Ignore black styles.
-ignore = E501, W503, E203
-# Imports
+extend-ignore = E501, W503, E203
 import-order-style = google
 application-import-names = nox,tests


Test patch:
diff --git a/requirements-conda-test.txt b/requirements-conda-test.txt
index 39b52c9..946e337 100644
--- a/requirements-conda-test.txt
+++ b/requirements-conda-test.txt
@@ -1,7 +1,7 @@
 argcomplete >=1.9.4,<2.0
 colorlog >=2.6.1,<7.0.0
-py >=1.4.0,<2.0.0
-virtualenv >=14.0.0
 jinja2
+py >=1.4.0,<2.0.0
+pytest
 tox
-pytest
\ No newline at end of file
+virtualenv >=14.0.0
diff --git a/requirements-test.txt b/requirements-test.txt
index 6a0bc99..6dd7e2d 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,7 +1,7 @@
 flask
 pytest
 pytest-cov
+recommonmark
 sphinx
 sphinx-autobuild
-recommonmark
 witchhazel
diff --git a/tests/test__parametrize.py b/tests/test__parametrize.py
index e581380..d7bd2c3 100644
--- a/tests/test__parametrize.py
+++ b/tests/test__parametrize.py
@@ -34,7 +34,7 @@ def test_param_eq(param, other, expected):
 
 def test_param_eq_fail():
     with pytest.raises(NotImplementedError):
-        _parametrize.Param() == "a"
+        _parametrize.Param() == "a"  # noqa: B015
 
 
 def test_parametrize_decorator_one():
diff --git a/tests/test__version.py b/tests/test__version.py
index b4497d0..fe3cd24 100644
--- a/tests/test__version.py
+++ b/tests/test__version.py
@@ -46,7 +46,7 @@ def test_needs_version_default() -> None:
 def test_get_nox_version() -> None:
     """It returns something that looks like a Nox version."""
     result = get_nox_version()
-    year, month, day = [int(part) for part in result.split(".")[:3]]
+    year, month, day, *_ = (int(part) for part in result.split("."))
     assert year >= 2020
 
 
diff --git a/tests/test_command.py b/tests/test_command.py
index db49a87..0b6e4d4 100644
--- a/tests/test_command.py
+++ b/tests/test_command.py
@@ -121,7 +121,7 @@ def test_run_env_unicode():
 
 
 def test_run_env_systemroot():
-    systemroot = os.environ.setdefault("SYSTEMROOT", str("sigil"))
+    systemroot = os.environ.setdefault("SYSTEMROOT", "sigil")
 
     result = nox.command.run(
         [PYTHON, "-c", 'import os; print(os.environ["SYSTEMROOT"])'], silent=True
@@ -460,7 +460,7 @@ def test_output_decoding() -> None:
 
 
 def test_output_decoding_non_ascii() -> None:
-    result = nox.popen.decode_output("ü".encode("utf-8"))
+    result = nox.popen.decode_output("ü".encode())
 
     assert result == "ü"
 
diff --git a/tests/test_manifest.py b/tests/test_manifest.py
index 4c2cf15..8da925d 100644
--- a/tests/test_manifest.py
+++ b/tests/test_manifest.py
@@ -48,7 +48,7 @@ def create_mock_config():
 def test__normalize_arg():
     assert _normalize_arg('test(foo="bar")') == _normalize_arg('test(foo="bar")')
 
-    # In the case of SyntaxError it should fallback to strng
+    # In the case of SyntaxError it should fallback to string
     assert (
         _normalize_arg("datetime.datetime(1990; 2, 18),")
         == "datetime.datetime(1990; 2, 18),"
@@ -81,7 +81,7 @@ def test_contains():
     assert "baz" not in manifest
 
     # Establish that __contains__ works post-iteration.
-    for session in manifest:
+    for _session in manifest:
         pass
     assert "foo" in manifest
     assert "bar" in manifest
@@ -104,7 +104,7 @@ def test_getitem():
 
     # Establish that the sessions are still present even after being
     # consumed by iteration.
-    for session in manifest:
+    for _session in manifest:
         pass
     assert manifest["foo"].func is sessions["foo"]
     assert manifest["bar"].func is sessions["bar"]
@@ -144,7 +144,7 @@ def test_len():
     sessions = create_mock_sessions()
     manifest = Manifest(sessions, create_mock_config())
     assert len(manifest) == 2
-    for session in manifest:
+    for _session in manifest:
         assert len(manifest) == 2
 
 
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index 2df4cfd..906ef24 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -13,8 +13,8 @@
 # limitations under the License.
 
 import argparse
+import builtins
 import copy
-import io
 import json
 import os
 import platform
@@ -456,7 +456,7 @@ def test_print_summary():
 
 def test_create_report_noop():
     config = _options.options.namespace(report=None)
-    with mock.patch.object(io, "open", autospec=True) as open_:
+    with mock.patch.object(builtins, "open", autospec=True) as open_:
         results = tasks.create_report(mock.sentinel.RESULTS, config)
         assert not open_.called
     assert results is mock.sentinel.RESULTS
@@ -472,7 +472,7 @@ def test_create_report():
             status=sessions.Status.SUCCESS,
         )
     ]
-    with mock.patch.object(io, "open", autospec=True) as open_:
+    with mock.patch.object(builtins, "open", autospec=True) as open_:
         with mock.patch.object(json, "dump", autospec=True) as dump:
             answer = tasks.create_report(results, config)
             assert answer is results