Skip to content

Commit 97c1259

Browse files
authored
Miscelaneous improvements (#16)
- Rename dev `nox` sessions to make them more explicit - Rename optional dependencies to match `nox` sessions - Add optional dependencies for actor, app and lib - Move `py.typed` to `frequenz.repo.config` - Use imports from the top-level package - Make `flatten()` really return an iterator - Auto-discover `pytest` paths - Fix `pytest` invocation - Improve general module documentation
2 parents 0f6cb07 + be370ff commit 97c1259

File tree

8 files changed

+190
-43
lines changed

8 files changed

+190
-43
lines changed

noxfile.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
"""Configuration file for nox."""
55

6-
from frequenz.repo import config
6+
from frequenz.repo.config import nox
77

88
# Remove the pytest sessions because we don't have tests yet
9-
conf = config.nox.default.lib_config.copy()
10-
conf.sessions = [s for s in conf.sessions if not s.startswith("pytest")]
9+
config = nox.default.lib_config.copy()
10+
config.sessions = [s for s in config.sessions if not s.startswith("pytest")]
1111

12-
config.nox.configure(conf)
12+
nox.configure(config)

pyproject.toml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,35 +37,40 @@ name = "Frequenz Energy-as-a-Service GmbH"
3737
3838

3939
[project.optional-dependencies]
40+
actor = []
4041
api = [
4142
"grpcio-tools >= 1.47.0, < 2",
4243
"mypy-protobuf >= 3.0.0, < 4",
4344
"setuptools >= 67.6.0, < 68",
4445
]
45-
docs-gen = [
46+
app = []
47+
lib = []
48+
dev-docs-gen = [
4649
"mike == 1.1.2",
4750
"mkdocs-gen-files == 0.4.0",
4851
"mkdocs-literate-nav == 0.4.0",
4952
"mkdocs-material == 9.1.4",
5053
"mkdocs-section-index == 0.3.5",
5154
"mkdocstrings[python] == 0.20.0",
5255
]
53-
docs-lint = ["pydocstyle == 6.3.0", "darglint == 1.8.1"]
54-
format = ["black == 23.3.0", "isort == 5.12.0"]
55-
mypy = [
56+
dev-docstrings = ["pydocstyle == 6.3.0", "darglint == 1.8.1"]
57+
dev-formatting = ["black == 23.3.0", "isort == 5.12.0"]
58+
dev-mypy = [
5659
"mypy == 1.1.1",
5760
"types-setuptools >= 67.6.0, < 68", # Should match the global dependency
5861
# For checking the docs/ script, and tests
5962
"frequenz-repo-config[docs-gen,pytest]",
6063
]
61-
pylint = [
64+
dev-pylint = [
6265
"pylint == 2.17.1",
6366
"pylint-google-style-guide-imports-enforcing == 1.3.0",
6467
# For checking the docs/ script, and tests
6568
"frequenz-repo-config[docs-gen,pytest]",
6669
]
67-
pytest = ["pytest == 7.2.2"]
68-
dev = ["frequenz-repo-config[docs-gen,docs-lint,format,pytest,mypy,pylint]"]
70+
dev-pytest = ["pytest == 7.2.2"]
71+
dev = [
72+
"frequenz-repo-config[dev-docs-gen,dev-docstrings,dev-formatting,dev-pytest,dev-mypy,dev-pylint]",
73+
]
6974

7075
[project.urls]
7176
Changelog = "https://github.com/frequenz-floss/frequenz-repo-config-python/releases"

src/frequenz/repo/config/__init__.py

Lines changed: 95 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,11 @@
1414
1515
## `nox`
1616
17+
### Writing the `noxfile.py`
18+
1719
Projects wanting to use `nox` to run lint checkers and other utilities can use
1820
the [`frequenz.repo.config.nox`][] package.
1921
20-
Make sure to add this package as `dev` dependency to your project's
21-
`pyproject.tom` file, for example:
22-
23-
```toml
24-
[project.optional-dependencies]
25-
nox = [
26-
"nox == 2022.11.21",
27-
"frequenz-repo-config[lib] == 0.1.0",
28-
]
29-
```
30-
31-
Please note the `lib` optional dependency. Make sure you specify the correct
32-
one based on the type of your project (`api`, `actor`, `app`, `lib`). Also make
33-
sure to adjust the versions.
34-
3522
When writing the `noxfile.py` you should import the `nox` module from this
3623
package and use the [`frequenz.repo.config.nox.configure`][] function,
3724
which will configure all nox sessions.
@@ -40,9 +27,9 @@
4027
in the [`frequenz.repo.config.nox.default`][] module. For example:
4128
4229
```python
43-
from frequenz.repo import config
30+
from frequenz.repo.config import nox
4431
45-
config.nox.configure(config.nox.default.lib_config)
32+
nox.configure(nox.default.lib_config)
4633
```
4734
4835
Again, make sure to pick the correct default configuration based on the type of
@@ -53,11 +40,11 @@
5340
[`copy()`][frequenz.repo.config.nox.config.Config.copy] method:
5441
5542
```python
56-
from frequenz.repo import config
43+
from frequenz.repo.config import nox
5744
58-
conf = config.nox.default.lib_config.copy()
59-
conf.opts.black.append("--diff")
60-
config.nox.configure(conf)
45+
config = nox.default.lib_config.copy()
46+
config.opts.black.append("--diff")
47+
nox.configure(config)
6148
```
6249
6350
If you need further customization or to define new sessions, you can use the
@@ -73,6 +60,93 @@
7360
7461
- [`frequenz.repo.config.nox.util`][]: General purpose utility functions.
7562
63+
### `pyproject.toml` configuration
64+
65+
All sessions configured by this package expect the `pyproject.toml` file to
66+
define specific *dev* dependencies that will be used by the different `nox`
67+
sessions.
68+
69+
The following optional dependencies are used and must be defined:
70+
71+
- `dev-docstrings`: Dependencies to lint the documentation.
72+
73+
At least these packages should be included:
74+
75+
- `pydocstyle`: To check the docstrings' format.
76+
- `darglint`: To check the docstrings' content.
77+
78+
- `dev-formatting`: Dependencies to check the code's formatting.
79+
80+
At least these packages should be included:
81+
82+
- `black`: To check the code's formatting.
83+
- `isort`: To check the imports' formatting.
84+
85+
- `dev-mypy`: Dependencies to run `mypy` to check the code's type annotations.
86+
87+
At least these packages should be included:
88+
89+
- `mypy`: To check the code's type annotations.
90+
91+
- `dev-pylint`: Dependencies to run `pylint` to lint the code.
92+
93+
At least these packages should be included:
94+
95+
- `pylint`: To lint the code.
96+
97+
- `dev-pytest`: Dependencies to run the tests using `pytest`.
98+
99+
At least these packages should be included:
100+
101+
- `pytest`: To run the tests.
102+
103+
For some of these you should install too any other dependencies that are used
104+
by the project. For example, if the project uses `pytest-asyncio`, you should
105+
include it in the `dev-pytest` optional dependency.
106+
107+
It is also recommended, but not required, to provide a global `dev` optional
108+
dependency that includes all the other optional dependencies, so users can
109+
install all the dependencies needed while developing the project without having
110+
to run `nox`, which might be a bit slow if you want to do quick iterations.
111+
112+
```console
113+
$ pip install -e .[dev]
114+
...
115+
$ pytest
116+
...
117+
```
118+
119+
Here is a sample `pyproject.toml` file that defines all the optional
120+
dependencies:
121+
122+
```toml
123+
[project]
124+
name = "my-package"
125+
# ...
126+
127+
[project.optional-dependencies]
128+
dev-docstrings = ["pydocstyle == 6.3.0", "darglint == 1.8.1"]
129+
dev-formatting = ["black == 23.3.0", "isort == 5.12.0"]
130+
dev-mypy = [
131+
"mypy == 1.1.1",
132+
# For checking tests
133+
"my-package[dev-pytest]",
134+
]
135+
dev-pylint = [
136+
"pylint == 2.17.1",
137+
"pylint-google-style-guide-imports-enforcing == 1.3.0",
138+
# For checking tests
139+
"my-package[dev-pytest]",
140+
]
141+
dev-pytest = [
142+
"pytest == 7.2.2",
143+
"pytest-asyncio == 0.21.0",
144+
"pytest-mock == 3.10.0",
145+
]
146+
dev = [
147+
"my-package[dev-docstrings,dev-formatting,dev-mypy,dev-nox,dev-pylint,dev-pytest]",
148+
]
149+
```
76150
77151
# APIs
78152

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ class Config:
8484
tools invoked by the sessions.
8585
"""
8686

87+
def __post_init__(self) -> None:
88+
"""Initialize the configuration object.
89+
90+
This will add extra paths discovered in config files and other sources.
91+
"""
92+
for path in util.discover_paths():
93+
if path not in self.extra_paths:
94+
self.extra_paths.append(path)
95+
8796
def copy(self, /) -> Self:
8897
"""Create a new object as a copy of self.
8998

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
method.
2828
"""
2929

30+
import dataclasses
31+
3032
from . import config as _config
33+
from . import util
3134

3235
common_command_options: _config.CommandsOptions = _config.CommandsOptions(
3336
black=[
@@ -86,8 +89,16 @@
8689
api_command_options: _config.CommandsOptions = common_command_options.copy()
8790
"""Default command-line options for APIs."""
8891

89-
api_config: _config.Config = common_config.copy()
90-
"""Default configuration for APIs."""
92+
api_config: _config.Config = dataclasses.replace(
93+
common_config,
94+
source_paths=list(util.replace(common_config.source_paths, {"src": "py"})),
95+
extra_paths=list(util.replace(common_config.extra_paths, {"tests": "pytests"})),
96+
)
97+
"""Default configuration for APIs.
98+
99+
Same as `common_config`, but with `source_paths` replacing `"src"` with `"py"`
100+
and `extra_paths` replacing `"tests"` with `"pytests"`.
101+
"""
91102

92103
actor_command_options: _config.CommandsOptions = common_command_options.copy()
93104
"""Default command-line options for actors."""

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def formatting(session: nox.Session, install_deps: bool = True) -> None:
4040
install_deps: True if dependencies should be installed.
4141
"""
4242
if install_deps:
43-
session.install("-e", ".[format]")
43+
session.install("-e", ".[dev-formatting]")
4444

4545
conf = config.get()
4646
session.run("black", *conf.opts.black, *conf.path_args(session))
@@ -58,7 +58,7 @@ def mypy(session: nox.Session, install_deps: bool = True) -> None:
5858
if install_deps:
5959
# install the package itself as editable, so that it is possible to do
6060
# fast local tests with `nox -R -e mypy`.
61-
session.install("-e", ".[mypy]")
61+
session.install("-e", ".[dev-mypy]")
6262

6363
conf = config.get()
6464
pkg_args = util.flatten(("-p", p) for p in conf.package_args(session))
@@ -76,7 +76,7 @@ def pylint(session: nox.Session, install_deps: bool = True) -> None:
7676
if install_deps:
7777
# install the package itself as editable, so that it is possible to do
7878
# fast local tests with `nox -R -e pylint`.
79-
session.install("-e", ".[pylint]")
79+
session.install("-e", ".[dev-pylint]")
8080

8181
conf = config.get()
8282
session.run("pylint", *conf.opts.pylint, *conf.path_args(session))
@@ -91,7 +91,7 @@ def docstrings(session: nox.Session, install_deps: bool = True) -> None:
9191
install_deps: True if dependencies should be installed.
9292
"""
9393
if install_deps:
94-
session.install("-e", ".[docs-lint]")
94+
session.install("-e", ".[dev-docstrings]")
9595

9696
conf = config.get()
9797
session.run("pydocstyle", *conf.opts.pydocstyle, *conf.path_args(session))
@@ -120,7 +120,7 @@ def pytest_max(session: nox.Session, install_deps: bool = True) -> None:
120120
if install_deps:
121121
# install the package itself as editable, so that it is possible to do
122122
# fast local tests with `nox -R -e pytest_max`.
123-
session.install("-e", ".[pytest]")
123+
session.install("-e", ".[dev-pytest]")
124124

125125
_pytest_impl(session, "max")
126126

@@ -136,7 +136,7 @@ def pytest_min(session: nox.Session, install_deps: bool = True) -> None:
136136
if install_deps:
137137
# install the package itself as editable, so that it is possible to do
138138
# fast local tests with `nox -R -e pytest_min`.
139-
session.install("-e", ".[pytest]", *util.min_dependencies())
139+
session.install("-e", ".[dev-pytest]", *util.min_dependencies())
140140

141141
_pytest_impl(session, "min")
142142

@@ -145,7 +145,7 @@ def _pytest_impl(
145145
session: nox.Session, max_or_min_deps: str # pylint: disable=unused-argument
146146
) -> None:
147147
conf = config.get()
148-
session.run("pytest", *conf.opts.pytest, *conf.path_args(session))
148+
session.run("pytest", *conf.opts.pytest, *session.posargs)
149149

150150
# pylint: disable=fixme
151151
# TODO: Implement coverage reporting, we need to research this a bit and it

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

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import pathlib
1212
import tomllib
13-
from collections.abc import Iterable
13+
from collections.abc import Iterable, Mapping, Set
1414
from typing import TypeVar
1515

1616
_T = TypeVar("_T")
@@ -28,7 +28,28 @@ def flatten(iterables: Iterable[Iterable[_T]], /) -> Iterable[_T]:
2828
Example:
2929
>>> assert list(flatten([(1, 2), (3, 4)]) == [1, 2, 3, 4]
3030
"""
31-
return [item for sublist in iterables for item in sublist]
31+
return (item for sublist in iterables for item in sublist)
32+
33+
34+
def replace(iterable: Iterable[_T], replacements: Mapping[_T, _T], /) -> Iterable[_T]:
35+
"""Replace elements in an iterable.
36+
37+
Args:
38+
iterable: The iterable to replace elements in.
39+
old: The elements to replace.
40+
new: The elements to replace with.
41+
42+
Returns:
43+
An iterable with the elements in `iterable` replaced.
44+
45+
Example:
46+
>>> assert list(replace([1, 2, 3], old={1, 2}, new={4, 5})) == [4, 5, 3]
47+
"""
48+
for item in iterable:
49+
if item in replacements:
50+
yield replacements[item]
51+
else:
52+
yield item
3253

3354

3455
def existing_paths(paths: Iterable[str], /) -> Iterable[pathlib.Path]:
@@ -170,3 +191,30 @@ def min_dependencies() -> list[str]:
170191
else:
171192
raise RuntimeError(f"Minimum requirement is not set: {dep}")
172193
return min_deps
194+
195+
196+
def discover_paths() -> list[str]:
197+
"""Discover paths to check.
198+
199+
Discover the paths to check by looking into different sources, like the
200+
`pyproject.toml` file.
201+
202+
Currently the following paths are discovered:
203+
204+
- The `testpaths` option in the `tools.pytest.ini_options` section of
205+
`pyproject.toml`.
206+
207+
Returns:
208+
The discovered paths to check.
209+
"""
210+
with open("pyproject.toml", "rb") as toml_file:
211+
data = tomllib.load(toml_file)
212+
213+
testpaths = (
214+
data.get("tool", {})
215+
.get("pytest", {})
216+
.get("ini_options", {})
217+
.get("testpaths", [])
218+
)
219+
220+
return testpaths

0 commit comments

Comments
 (0)