Skip to content

Commit 2e084dc

Browse files
authored
Improve how mypy is run (#128)
This PR mainly split `mypy` check for the source package and dev files. `mypy` tends to have issues with checking source files inside a `src/` directory. There are tricks[^1] but it seems like the only way to do it is to run `mypy` on `.` using `MYPYPATH=src`, which will still check the whole `.` directory (include whole virtualenvs if there are any), which is not what we want. For now we've been using `-p` to check for packages instead of files, which seemed to have done the trick, but we need to pass directories that don't really have a package structure (like benchmarks or examples or tests) as packages, which is also not great. We try a different approach, we check the source code as a package separately, and then check the development files (tests, examples, etc.), which are passed as simple paths. This also opens up the door to using more relaxed rules for these development files if needed in the future. To achieve this we also move the `packages` specification to the `pyproject.toml` file, so now `mypy` can be called without any arguments to check the source code package, making it easier to call it without relaying in `nox`. We also move all the options to the `pyproject.toml` file, so `mypy` can be run consistently also outside of `nox`, and the stub dependencies now need to be specified manually in the `pyproject.toml` file, so they can be pinned. This is also a step towards removing `nox` (#71). [^1]: https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules
2 parents 7a6802f + e412deb commit 2e084dc

File tree

12 files changed

+257
-73
lines changed

12 files changed

+257
-73
lines changed

RELEASE_NOTES.md

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,66 @@
22

33
## Summary
44

5-
<!-- Here goes a general summary of what this release is about -->
5+
This release replaces [`darglint`](https://github.com/terrencepreilly/darglint) (not maintained anymore) with [`pydoclint`](https://github.com/jsh9/pydoclint) which brings performance and checks improvements. It also adds basic `flake8` checks and `mypy` fixes.
66

77
## Upgrading
88

99
- `flake8` basic checks are enabled now. Most are already covered by `pylint`, but there are a few differences, so you might need to fix your code if `flake8` find some issues.
1010

11-
- `darglint` was replaced by `pydoclint`, `pydoclint` can find a few more issues than `darglint`, so your code might need adjusting.
11+
- `darglint`:
1212

13-
- `darglint` is not used anymore, but if it is installed, it will make `flake8` run extremely slowly anyways, so it is extremely recommended to uninstall it (`pip uninstall darglint`) and rebuild you `nox` *venvs* if you use `-R`.
13+
* Replaced by `pydoclint`, `pydoclint` can find a few more issues than `darglint`, so your code might need adjusting.
14+
15+
* It is recommended to remove the `darglint` configuration file `.darglint` and the `darglint` `pip` package, if it is kept installed, it will make `flake8` run extremely slowly even if not used: `pip uninstall darglint`) and rebuild you `nox` *venvs* if you use `-R`.
1416

1517
- If you are upgrading without regenerating the cookiecutter templates, you'll need to adjust the dependencies accordingly.
1618

19+
- `nox`: The `Config.package_args()` method was removed.
20+
21+
- `mypy`
22+
23+
* Options must be specified in the `pyproject.toml` file, including the option `packages` specifying the package containing the source code of the project. The documentation was updated to suggest the recommended options.
24+
25+
* Dependencies on *stubs* for running the type check need to be specified manually in the `pyproject.toml` file, so they can be pinned (before they were installed automatically by `mypy`.
26+
27+
* The `mypy` `nox` session runs `mypy` 2 times, one without options to check the package including the sources, and one with the paths of development paths (`tests`, `benchmarks`, etc.).
28+
29+
To migrate existing projects:
30+
31+
1. Add the recommended options (previously specified in the default options object):
32+
33+
```toml
34+
[tool.mypy]
35+
explicit_package_bases = true
36+
namespace_packages = true
37+
packages = ["<package.name>"] # For example: "frequenz.repo.config" for this package
38+
strict = true
39+
```
40+
41+
2. Find out which *stubs* were previously installed automatically by `mypy`:
42+
43+
```sh
44+
python -m venv tmp_venv
45+
. tmp_venv/bin/activate
46+
pip install -e .[dev-mypy]
47+
mypy --install-types
48+
deactivate
49+
```
50+
51+
3. Look at the list of packages it offers to install and answer "no".
52+
53+
4. Search for the latest package version for those packages in https://pypi.org/project/<package-name>/
54+
55+
5. Edit the `pyproject.toml` file to add those dependencies in `dev-mypy`, for example:
56+
57+
```toml
58+
[project.optional-dependencies]
59+
dev-mypy = [
60+
"mypy == 1.5.1",
61+
"types-setuptools == 68.1.0.0",
62+
# ...
63+
```
64+
1765
### Cookiecutter template
1866

1967
- CI: The `nox` job now uses a matrix to run the different `nox` sessions in parallel. If you use branch projection with the `nox` job you need to update the rules to include each matrix job.
@@ -26,6 +74,8 @@
2674

2775
- `darlint` was replaced by `pydoclint`, which is way faster and detect more issues.
2876

77+
- `nox`: The `Config.path_args()` method now accepts two optional arguments to control with paths to include.
78+
2979
### Cookiecutter template
3080

3181
- Now dependabot updates will be done weekly and grouped by *required* and *optional* for minor and patch updates (major updates are still done individually for each dependency).
@@ -34,12 +84,6 @@
3484

3585
The output of `pip freeze` is printed to be able to more easily debug different behaviours between GitHub workflow runs and local runs.
3686

37-
- See the general new features section.
87+
- `mypy`: Add a commented out `no-incremental` option, which makes the run slower but prevents some issues with `mypy` giving different results on different runs.
3888

39-
## Bug Fixes
40-
41-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
42-
43-
### Cookiecutter template
44-
45-
<!-- Here bug fixes for cookiecutter specifically -->
89+
- See the general new features section.

cookiecutter/{{cookiecutter.github_repo_name}}/pyproject.toml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ dev-flake8 = [
6464
"flake8 == 6.1.0",
6565
"flake8-docstrings == 1.7.0",
6666
"flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
67-
"pydoclint == 0.3.0",
67+
"pydoclint == 0.3.1",
6868
"pydocstyle == 6.3.0",
6969
]
7070
dev-formatting = ["black == 23.7.0", "isort == 5.12.0"]
@@ -79,6 +79,9 @@ dev-mkdocs = [
7979
]
8080
dev-mypy = [
8181
"mypy == 1.5.1",
82+
{%- if cookiecutter.type == "api" %}
83+
"grpc-stubs == 1.53.0.2",
84+
{%- endif %}
8285
# For checking the noxfile, docs/ script, and tests
8386
"{{cookiecutter.pypi_package_name}}[dev-mkdocs,dev-noxfile,dev-pytest]",
8487
]
@@ -155,6 +158,10 @@ disable = [
155158
# pylint's unsubscriptable check is buggy and is not needed because
156159
# it is a type-check, for which we already have mypy.
157160
"unsubscriptable-object",
161+
# Checked by flake8
162+
"line-too-long",
163+
"unused-variable",
164+
"unnecessary-lambda-assignment",
158165
]
159166

160167
[tool.pytest.ini_options]
@@ -165,6 +172,18 @@ required_plugins = ["pytest-asyncio", "pytest-mock"]
165172
{%- else %}
166173
testpaths = ["pytests"]
167174
{%- endif %}
175+
176+
[tool.mypy]
177+
explicit_package_bases = true
178+
namespace_packages = true
179+
# This option disables mypy cache, and it is sometimes useful to enable it if
180+
# you are getting weird intermittent error, or error in the CI but not locally
181+
# (or vice versa). In particular errors saying that type: ignore is not
182+
# used but getting the original ignored error when removing the type: ignore.
183+
# See for example: https://github.com/python/mypy/issues/2960
184+
#no_incremental = true
185+
packages = ["{{cookiecutter.python_package}}"]
186+
strict = true
168187
{%- if cookiecutter.type != "api" %}
169188

170189
[[tool.mypy.overrides]]

pyproject.toml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ dev-flake8 = [
6565
"flake8 == 6.1.0",
6666
"flake8-docstrings == 1.7.0",
6767
"flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
68-
"pydoclint == 0.3.0",
68+
"pydoclint == 0.3.1",
6969
"pydocstyle == 6.3.0",
7070
]
7171
dev-formatting = ["black == 23.7.0", "isort == 5.12.0"]
@@ -79,7 +79,11 @@ dev-mkdocs = [
7979
]
8080
dev-mypy = [
8181
"mypy == 1.5.1",
82-
"types-setuptools >= 67.6.0, < 68", # Should match the global dependency
82+
"types-setuptools == 68.1.0.0", # Should match the build dependency
83+
"types-Markdown == 3.4.2.10",
84+
"types-PyYAML == 6.0.12.11",
85+
"types-babel == 2.11.0.15",
86+
"types-colorama == 0.4.15.12",
8387
# For checking the noxfile, docs/ script, and tests
8488
"frequenz-repo-config[dev-mkdocs,dev-noxfile,dev-pytest]",
8589
]
@@ -147,8 +151,24 @@ disable = [
147151
# pylint's unsubscriptable check is buggy and is not needed because
148152
# it is a type-check, for which we already have mypy.
149153
"unsubscriptable-object",
154+
# Checked by flake8
155+
"line-too-long",
156+
"unused-variable",
157+
"unnecessary-lambda-assignment",
150158
]
151159

160+
[tool.mypy]
161+
explicit_package_bases = true
162+
namespace_packages = true
163+
# This option disables mypy cache, and it is sometimes useful to enable it if
164+
# you are getting weird intermittent error, or error in the CI but not locally
165+
# (or vice versa). In particular errors saying that type: ignore is not
166+
# used but getting the original ignored error when removing the type: ignore.
167+
# See for example: https://github.com/python/mypy/issues/2960
168+
#no_incremental = true
169+
packages = ["frequenz.repo.config"]
170+
strict = true
171+
152172
[[tool.mypy.overrides]]
153173
module = ["cookiecutter", "cookiecutter.*", "sybil", "sybil.*"]
154174
ignore_missing_imports = true

src/frequenz/repo/config/__init__.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
nox.configure(config)
5353
```
5454
55+
!!! note
56+
57+
When possible, it is recommended to define options in the `pyproject.toml` file
58+
(most tools can do this), so they can be consistently used even if the tool is used
59+
outside of `nox`.
60+
5561
If you need further customization or to define new sessions, you can use the
5662
following modules:
5763
@@ -134,7 +140,7 @@
134140
"flake8 == 6.1.0",
135141
"flake8-docstrings == 1.7.0",
136142
"flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
137-
"pydoclint == 0.2.4",
143+
"pydoclint == 0.3.1",
138144
"pydocstyle == 6.3.0",
139145
]
140146
dev-formatting = ["black == 23.3.0", "isort == 5.12.0"]
@@ -167,6 +173,39 @@
167173
]
168174
```
169175
176+
## `mypy` (static type checking)
177+
178+
To configure `mypy` you can add the recommended options to the `pyproject.toml` file as
179+
follows:
180+
181+
```toml
182+
[tool.mypy]
183+
explicit_package_bases = true
184+
namespace_packages = true
185+
packages = ["your_package_name"] # Use the actual package name here
186+
strict = true
187+
```
188+
189+
You can just call `mypy` to check the package of your sources or you can use `mypy
190+
tests` to check the tests, for example.
191+
192+
You might also need to extra optional dependencies to install type checking stubs for
193+
some packages. For example for API projects you need the `grpc-stubs` package:
194+
195+
```toml
196+
[project.optional-dependencies]
197+
# ...
198+
dev-mypy = [
199+
# ...
200+
"grpc-stubs == 1.53.0.2",
201+
# ...
202+
]
203+
```
204+
205+
You can use `mypy --install-types` to install to get a list of missing stubs, `mypy`
206+
will list them for you and ask if you want to proceed with the installation. You can
207+
answer no and copy the list of missing stubs to the `pyproject.toml` file.
208+
170209
## `mkdocs` (generating documentation)
171210
172211
### API reference generation

src/frequenz/repo/config/nox/config.py

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"""
1414

1515
import dataclasses as _dataclasses
16-
import itertools as _itertools
1716
from typing import Self, assert_never, overload
1817

1918
import nox as _nox
@@ -114,7 +113,14 @@ def copy(self, /) -> Self:
114113
extra_paths=self.extra_paths.copy(),
115114
)
116115

117-
def path_args(self, session: _nox.Session, /) -> list[str]:
116+
def path_args(
117+
self,
118+
session: _nox.Session,
119+
/,
120+
*,
121+
include_sources: bool = True,
122+
include_extra: bool = True,
123+
) -> list[str]:
118124
"""Return the file paths to run the checks on.
119125
120126
If positional arguments are present in the nox session, those are used
@@ -123,54 +129,22 @@ def path_args(self, session: _nox.Session, /) -> list[str]:
123129
124130
Args:
125131
session: The nox session to use to look for command-line arguments.
132+
include_sources: Whether to include the source paths or not.
133+
include_extra: Whether to include the extra paths or not.
126134
127135
Returns:
128136
The file paths to run the checks on.
129137
"""
130138
if session.posargs:
131139
return session.posargs
132140

133-
return list(
134-
str(p) for p in _util.existing_paths(self.source_paths + self.extra_paths)
135-
)
136-
137-
def package_args(self, session: _nox.Session, /) -> list[str]:
138-
"""Return the package names to run the checks on.
139-
140-
If positional arguments are present in the nox session, those are used
141-
as the file paths verbatim, and if not, all **existing** `source_paths`
142-
are searched for python packges by looking for `__init__.py` files.
143-
`extra_paths` are used as is, only converting the paths to python
144-
package names (replacing `/` with `.` and removing the suffix `.pyi?`
145-
if it exists.
146-
147-
Args:
148-
session: The nox session to use to look for command-line arguments.
149-
150-
Returns:
151-
The package names found in the `source_paths`.
152-
"""
153-
if session.posargs:
154-
return session.posargs
155-
156-
sources_package_dirs_with_roots = (
157-
(p, _util.find_toplevel_package_dirs(p))
158-
for p in _util.existing_paths(self.source_paths)
159-
)
141+
paths: list[str] = []
142+
if include_sources:
143+
paths.extend(self.source_paths)
144+
if include_extra:
145+
paths.extend(self.extra_paths)
160146

161-
source_packages = (
162-
_util.path_to_package(pkg_path, root=root)
163-
for root, pkg_paths in sources_package_dirs_with_roots
164-
for pkg_path in pkg_paths
165-
)
166-
167-
extra_packages = (
168-
_util.path_to_package(p) for p in _util.existing_paths(self.extra_paths)
169-
)
170-
171-
return list(
172-
_util.deduplicate(_itertools.chain(source_packages, extra_packages))
173-
)
147+
return list(str(p) for p in _util.existing_paths(paths))
174148

175149

176150
_config: Config | None = None

src/frequenz/repo/config/nox/default.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,7 @@
3535
"--diff",
3636
"--check",
3737
],
38-
mypy=[
39-
"--install-types",
40-
"--namespace-packages",
41-
"--non-interactive",
42-
"--explicit-package-bases",
43-
"--strict",
44-
],
45-
# SDK: pylint "--extension-pkg-whitelist=pydantic"
38+
mypy=[],
4639
pytest=[
4740
"-W=all",
4841
"-vv",

src/frequenz/repo/config/nox/session.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ def ci_checks_max(session: nox.Session) -> None:
2626
session.install("-e", ".[dev]")
2727

2828
formatting(session, False)
29+
flake8(session, False)
2930
mypy(session, False)
3031
pylint(session, False)
31-
flake8(session, False)
3232
pytest_max(session, False)
3333

3434

@@ -62,8 +62,22 @@ def mypy(session: nox.Session, install_deps: bool = True) -> None:
6262
session.install("-e", ".[dev-mypy]")
6363

6464
conf = _config.get()
65-
pkg_args = _util.flatten(("-p", p) for p in conf.package_args(session))
66-
session.run("mypy", *conf.opts.mypy, *pkg_args)
65+
66+
# If we get CLI options, we run mypy on those, but still passing the
67+
# configured options (they can be overridden by the CLI options).
68+
if session.posargs:
69+
session.run("mypy", *conf.opts.mypy, *session.posargs)
70+
return
71+
72+
# We separate running the mypy checks into two runs, one is the default, as
73+
# configured in `pyproject.toml`, which should run against the sources.
74+
session.run("mypy", *conf.opts.mypy)
75+
76+
# The second run checks development files, like tests, benchmarks, etc.
77+
# This is an attempt to minimize mypy internal errors.
78+
session.run(
79+
"mypy", *conf.opts.mypy, *conf.path_args(session, include_sources=False)
80+
)
6781

6882

6983
@nox.session

0 commit comments

Comments
 (0)