launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32508
[Merge] ~jugmac00/lpci:add-missing-docstrings into lpci:main
Jürgen Gmach has proposed merging ~jugmac00/lpci:add-missing-docstrings into lpci:main.
Commit message:
Add missing docstrings
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~jugmac00/lpci/+git/lpcraft/+merge/485949
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/lpci:add-missing-docstrings into lpci:main.
diff --git a/NEWS.rst b/NEWS.rst
index 42530b0..1d9f214 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -7,7 +7,7 @@ Version history
- Reverts changes in 0.2.11 and 0.2.10
- Use "security.nesting=true" for GPU LPCI builds to workaround LPCI Noble failures
- when running on a focal host due to apparmor permission issues.
+ when running on a focal host due to apparmor permission issues.
0.2.11 (2025-04-11)
==================
diff --git a/lpci/__init__.py b/lpci/__init__.py
index e69de29..78b96c7 100644
--- a/lpci/__init__.py
+++ b/lpci/__init__.py
@@ -0,0 +1 @@
+"""lpci - the Launchpad CI runner - also works as a standalone."""
diff --git a/lpci/commands/__init__.py b/lpci/commands/__init__.py
index e69de29..ee51fcd 100644
--- a/lpci/commands/__init__.py
+++ b/lpci/commands/__init__.py
@@ -0,0 +1 @@
+"""Package implements all commands."""
diff --git a/lpci/commands/clean.py b/lpci/commands/clean.py
index bfd1287..30f86c5 100644
--- a/lpci/commands/clean.py
+++ b/lpci/commands/clean.py
@@ -1,3 +1,5 @@
+"""Implementation of the `clean` command."""
+
# Copyright 2022 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/commands/release.py b/lpci/commands/release.py
index fd523eb..53968f9 100644
--- a/lpci/commands/release.py
+++ b/lpci/commands/release.py
@@ -1,3 +1,5 @@
+"""Implementation of the `release` command."""
+
# Copyright 2023 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/commands/run.py b/lpci/commands/run.py
index e42a8a9..4f24266 100644
--- a/lpci/commands/run.py
+++ b/lpci/commands/run.py
@@ -1,3 +1,5 @@
+"""Implementation of the `run` and the `run one` commands."""
+
# Copyright 2021-2022 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/commands/tests/test_run.py b/lpci/commands/tests/test_run.py
index efd180c..6078a7d 100644
--- a/lpci/commands/tests/test_run.py
+++ b/lpci/commands/tests/test_run.py
@@ -3545,7 +3545,10 @@ class TestRun(RunBaseTestCase):
lxc.profile_edit.assert_called_once_with(
profile="default",
config={
- "config": {"nvidia.runtime": "true", "security.nesting": "true"},
+ "config": {
+ "nvidia.runtime": "true",
+ "security.nesting": "true",
+ },
"devices": {"gpu": {"type": "gpu"}},
},
project="test-project",
@@ -4816,7 +4819,10 @@ class TestRunOne(RunBaseTestCase):
lxc.profile_edit.assert_called_once_with(
profile="default",
config={
- "config": {"nvidia.runtime": "true", "security.nesting": "true"},
+ "config": {
+ "nvidia.runtime": "true",
+ "security.nesting": "true",
+ },
"devices": {"gpu": {"type": "gpu"}},
},
project="test-project",
diff --git a/lpci/commands/version.py b/lpci/commands/version.py
index b527140..640abfa 100644
--- a/lpci/commands/version.py
+++ b/lpci/commands/version.py
@@ -1,3 +1,5 @@
+"""Implementation of the `version` command."""
+
# Copyright 2021 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/config.py b/lpci/config.py
index ccedfdc..49aa02c 100644
--- a/lpci/config.py
+++ b/lpci/config.py
@@ -1,3 +1,5 @@
+"""Implementation of the configuration format, based on pydantic."""
+
# Copyright 2021-2022 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
@@ -58,6 +60,7 @@ class Output(ModelConfigDefaults):
@pydantic.validator("expires")
def validate_expires(cls, v: timedelta) -> timedelta:
+ """Validate for the `expires` field."""
if v < timedelta(0):
raise ValueError("non-negative duration expected")
return v
@@ -165,6 +168,7 @@ class PackageRepository(ModelConfigDefaults):
def validate_multiple_fields(
cls, values: Dict[str, Any]
) -> Dict[str, Any]:
+ """Make sure the usage of `ppa`, `url` and `components` is correct."""
if "url" in values:
if "ppa" in values:
raise ValueError(
@@ -194,6 +198,7 @@ class PackageRepository(ModelConfigDefaults):
def infer_components_if_ppa_is_set(
cls, v: List[PackageComponent], values: Dict[str, Any]
) -> List[PackageComponent]:
+ """Infer the component in case the shortform notation is used."""
if v is None and values["ppa"]:
return ["main"]
return v
@@ -202,6 +207,7 @@ class PackageRepository(ModelConfigDefaults):
def infer_url_if_ppa_is_set(
cls, v: AnyHttpUrl, values: Dict[str, Any]
) -> AnyHttpUrl:
+ """Infer the URL in case the shortform notation is used."""
if v is None and values["ppa"]:
owner, distribution, archive = get_ppa_url_parts(values["ppa"])
v = "{}/{}/{}/{}".format(
@@ -216,13 +222,14 @@ class PackageRepository(ModelConfigDefaults):
def set_formats_default_value(
cls, v: List[PackageFormat]
) -> List[PackageFormat]:
+ """Set package format to `deb` by default."""
if not v:
v = [PackageFormat.deb]
return v
@validator("trusted")
def convert_trusted(cls, v: bool) -> str:
- # trusted is True or False, but we need `yes` or `no`
+ """`trusted` is True or False, but we need `yes` or `no`."""
return v and "yes" or "no"
def sources_list_lines(self) -> Iterator[str]:
@@ -250,6 +257,7 @@ class Snap(ModelConfigDefaults):
@validator("channel")
def prevent_channel_none(cls, v: StrictStr) -> Any:
+ """Disallow an empty `channel` field."""
if v is None:
raise ValueError(
"You configured a Snap `channel`, "
@@ -259,6 +267,7 @@ class Snap(ModelConfigDefaults):
@validator("classic")
def prevent_classic_none(cls, v: bool) -> Any:
+ """Disallow an empty `classic` field."""
if v is None:
raise ValueError(
"You configured a Snap `classic`, "
@@ -294,12 +303,14 @@ class Job(ModelConfigDefaults):
def validate_architectures(
cls, v: Union[_Identifier, List[_Identifier]]
) -> List[_Identifier]:
+ """Validate the `architectures` field."""
if isinstance(v, str):
v = [v]
return v
@pydantic.validator("root", pre=True)
def validate_root(cls, v: Any) -> Any:
+ """Validate the `root` field."""
if type(v) is not bool or v is None:
raise ValueError(
"You configured `root` parameter, "
@@ -310,6 +321,7 @@ class Job(ModelConfigDefaults):
@pydantic.validator("snaps", pre=True)
def validate_snaps(cls, v: List[Any]) -> Any:
+ """Validate the `snaps` field."""
clean_values = []
for value in v:
# Backward compatibility, i.e. [chromium, firefox]
@@ -352,6 +364,7 @@ class Job(ModelConfigDefaults):
def validate_package_repositories(
cls, v: List[PackageRepository], values: Dict[StrictStr, Any]
) -> List[PackageRepository]:
+ """Validate the `package_repositories` field."""
package_repositories = None
for index, package_repository in enumerate(v):
if not package_repository.suites:
@@ -415,6 +428,7 @@ class License(ModelConfigDefaults):
def disallow_setting_both_sources(
cls, path: str, values: Dict[str, str]
) -> str:
+ """Only allow either `spdx` or `path` to be set."""
if values.get("spdx") and path:
raise ValueError(
"You cannot set `spdx` and `path` at the same time."
@@ -433,15 +447,22 @@ class Config(ModelConfigDefaults):
def validate_pipeline(
cls, v: List[Union[_Identifier, List[_Identifier]]]
) -> List[List[_Identifier]]:
+ """Validate the pipeline field.
+
+ Also, expand to a list of stages.
+ """
return [[stage] if isinstance(stage, str) else stage for stage in v]
- # XXX cjwatson 2021-11-17: This expansion strategy works, but it may
- # produce suboptimal error messages, and doesn't have a good way to do
- # things like limiting the keys that can be set in a matrix.
@pydantic.root_validator(pre=True)
def expand_matrix(
cls, values: Dict[StrictStr, Any]
) -> Dict[StrictStr, Any]:
+ """Use the validator to expand the matrix.
+
+ # XXX cjwatson 2021-11-17: This expansion strategy works, but it may
+ # produce suboptimal error messages, and doesn't have a good way to do
+ # things like limiting the keys that can be set in a matrix.
+ """
expanded_values = values.copy()
expanded_values["jobs"] = {
job_name: _expand_job_values(job_values)
diff --git a/lpci/env.py b/lpci/env.py
index 0aa9b96..2b4ac88 100644
--- a/lpci/env.py
+++ b/lpci/env.py
@@ -7,6 +7,7 @@ from pathlib import Path
def get_non_root_user() -> str:
+ """Return the non-root user."""
return "_lpci"
diff --git a/lpci/errors.py b/lpci/errors.py
index 3f45abb..bb67bba 100644
--- a/lpci/errors.py
+++ b/lpci/errors.py
@@ -17,9 +17,11 @@ class CommandError(CraftError):
"""Base exception for all error commands."""
def __init__(self, message: str, retcode: int = 1):
+ """Class initialization."""
super().__init__(message, retcode=retcode)
def __eq__(self, other: Any) -> bool:
+ """Implement comparison."""
if type(self) != type(other):
return NotImplemented
return str(self) == str(other) and self.retcode == other.retcode
diff --git a/lpci/git.py b/lpci/git.py
index f4da298..11de192 100644
--- a/lpci/git.py
+++ b/lpci/git.py
@@ -1,3 +1,5 @@
+"""Provide helper functions for git handling."""
+
# Copyright 2023 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/plugin/__init__.py b/lpci/plugin/__init__.py
index a3ab6b4..76d587c 100644
--- a/lpci/plugin/__init__.py
+++ b/lpci/plugin/__init__.py
@@ -1,3 +1,5 @@
+"""Implementation of the plugin system via pluggy."""
+
# Copyright 2021 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/plugin/hookspecs.py b/lpci/plugin/hookspecs.py
index aee2b34..8697ff6 100644
--- a/lpci/plugin/hookspecs.py
+++ b/lpci/plugin/hookspecs.py
@@ -1,3 +1,5 @@
+"""Providing the hooks which plugins can use for lpci."""
+
# Copyright 2021 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/plugin/lib.py b/lpci/plugin/lib.py
index 1ea3bf7..71f22c4 100644
--- a/lpci/plugin/lib.py
+++ b/lpci/plugin/lib.py
@@ -15,20 +15,27 @@ from lpci.plugin import hookimpl
class InternalPlugins:
+ """Provide default implementation via internal plugins.
+
+ The hooks can be overwritten by custom plugins.
+ """
INTERPOLATES_RUN_COMMAND: bool = False
def __init__(self, config: Job) -> None:
+ """Class initialization."""
self.config = config
@hookimpl
def lpci_install_packages(self) -> list[str]:
+ """`install_packages` implementation for internal plugins."""
if self.config.packages:
return self.config.packages
return []
@hookimpl
def lpci_install_snaps(self) -> list[Snap]:
+ """`install_snaps` implementation for internal plugins."""
if self.config.snaps:
return self.config.snaps
return []
diff --git a/lpci/plugin/manager.py b/lpci/plugin/manager.py
index dc234d9..02c0a0f 100644
--- a/lpci/plugin/manager.py
+++ b/lpci/plugin/manager.py
@@ -1,3 +1,5 @@
+"""Implementation of the plugin manager."""
+
from typing import Dict, Optional
import pluggy
@@ -11,6 +13,7 @@ from lpci.plugins import PLUGINS
def get_plugin_manager(
job: Job, plugin_settings: Optional[Dict[str, str]] = None
) -> pluggy.PluginManager:
+ """Return the plugin manager."""
pm = pluggy.PluginManager(NAME)
pm.add_hookspecs(hookspecs)
diff --git a/lpci/plugins/__init__.py b/lpci/plugins/__init__.py
index 38cd38d..d78dc69 100644
--- a/lpci/plugins/__init__.py
+++ b/lpci/plugins/__init__.py
@@ -1,3 +1,5 @@
+"""Package provides all available plugins which come with lpci."""
+
from typing import Any, Callable, Type, TypeVar
PLUGINS = dict() #: Collection of builtin plugins
@@ -7,10 +9,12 @@ TypeT = TypeVar("TypeT", bound=Type[Any])
def register(name: str) -> Callable[[TypeT], TypeT]:
- # this function registers all decorated plugin classes
- # the result looks like:
- #
- # PLUGINS = {'tox': <class 'lpci.plugins.plugins.ToxPlugin'>}
+ """Register all decorated plugin classes.
+
+ the result looks like:
+ PLUGINS = {'tox': <class 'lpci.plugins.plugins.ToxPlugin'>}
+ """
+
def inner(cls: TypeT) -> TypeT:
PLUGINS[name] = cls
return cls
diff --git a/lpci/plugins/plugins.py b/lpci/plugins/plugins.py
index 6f8708f..2c158cb 100644
--- a/lpci/plugins/plugins.py
+++ b/lpci/plugins/plugins.py
@@ -1,3 +1,5 @@
+"""Module contains all built-in plugins for lpci."""
+
# Copyright 2021 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
@@ -66,16 +68,19 @@ class ToxPlugin(BasePlugin):
@hookimpl
def lpci_install_packages(self) -> list[str]:
+ """`install_packages` implementation for the tox plugin."""
return ["python3-pip"]
@hookimpl
def lpci_execute_run(self) -> str:
+ """`execute_run` implementation for the tox plugin."""
# XXX jugmac00 2022-01-07: we should consider using a requirements.txt
# as this allows updating via `pip-tools`
return "python3 -m pip install tox==3.24.5; tox"
@hookimpl
def lpci_set_environment(self) -> dict[str, str | None]:
+ """`set_environment` implementation for the tox plugin."""
# Work around https://github.com/tox-dev/tox/issues/2372: without
# this, tox won't pass through the lower-case proxy environment
# variables set by launchpad-buildd.
@@ -93,6 +98,7 @@ class PyProjectBuildPlugin(BasePlugin):
@hookimpl
def lpci_install_packages(self) -> list[str]:
+ """`install_packages` implementation for the pyproject build plugin."""
# Ubuntu 20.04 does not provide a packaged version of build,
# so we need pip to install it
#
@@ -105,6 +111,7 @@ class PyProjectBuildPlugin(BasePlugin):
@hookimpl
def lpci_execute_run(self) -> str:
+ """`execute_run` implementation for the pyproject build plugin."""
# XXX jugmac00 2022-01-20: we should consider using a PPA
return "python3 -m pip install build==0.7.0; python3 -m build"
@@ -134,12 +141,15 @@ class MiniCondaPlugin(BasePlugin):
"""
class Config(BaseConfig):
+ """Configuration for the mini conda plugin."""
+
conda_packages: Optional[List[StrictStr]]
conda_python: Optional[StrictStr]
conda_channels: Optional[List[StrictStr]]
@pydantic.validator("conda_python", pre=True)
def validate_conda_python(cls, v: str | float | int) -> str:
+ """Validate the Python version identifier."""
return str(v)
INTERPOLATES_RUN_COMMAND = True
@@ -148,10 +158,12 @@ class MiniCondaPlugin(BasePlugin):
DEFAULT_CONDA_CHANNELS = ("defaults",)
def get_plugin_config(self) -> "MiniCondaPlugin.Config":
+ """Return the mini conda plugin configuration."""
return cast(MiniCondaPlugin.Config, self.config.plugin_config)
@property
def conda_packages(self) -> list[str]:
+ """List of conda packages."""
conda_packages: set[str] = set()
conda_python = self.DEFAULT_CONDA_PYTHON
plugin_config = self.get_plugin_config()
@@ -165,6 +177,7 @@ class MiniCondaPlugin(BasePlugin):
@property
def conda_channels(self) -> list[str]:
+ """List of conda channels."""
conda_channels: list[str] = []
plugin_config = self.get_plugin_config()
if plugin_config.conda_channels:
@@ -181,11 +194,13 @@ class MiniCondaPlugin(BasePlugin):
@hookimpl
def lpci_set_environment(self) -> dict[str, str]:
+ """`set_environment` implementation for the mini conda plugin."""
# `CONDA_ENV` sets the name of the Conda virtual environment
return {"CONDA_ENV": "lpci"}
@hookimpl
def lpci_install_packages(self) -> list[str]:
+ """`install_packages` implementation for the mini conda plugin."""
return [
"git",
"python3-dev",
@@ -196,6 +211,7 @@ class MiniCondaPlugin(BasePlugin):
@hookimpl
def lpci_execute_before_run(self) -> str:
+ """`execute_before_run` implementation for the mini conda plugin."""
run = self.config.run_before or ""
conda_channels = " ".join(f"-c {_}" for _ in self.conda_channels)
return textwrap.dedent(
@@ -212,11 +228,13 @@ class MiniCondaPlugin(BasePlugin):
@hookimpl
def lpci_execute_run(self) -> str:
+ """`execute_run` implementation for the mini conda plugin."""
run = self.config.run or ""
return textwrap.dedent(f"{run}")
@hookimpl
def lpci_execute_after_run(self) -> str:
+ """`execute_after_run` implementation for the mini conda plugin."""
run = f"; {self.config.run_after}" if self.config.run_after else ""
return f"export PATH=$HOME/miniconda3/bin:$PATH; conda env export{run}"
@@ -250,6 +268,8 @@ class CondaBuildPlugin(MiniCondaPlugin):
"""
class Config(MiniCondaPlugin.Config):
+ """Configuration for the conda build plugin."""
+
build_target: Optional[StrictStr]
conda_channels: Optional[List[StrictStr]]
conda_packages: Optional[List[StrictStr]]
@@ -260,10 +280,12 @@ class CondaBuildPlugin(MiniCondaPlugin):
DEFAULT_RECIPE_FOLDER = "./info"
def get_plugin_config(self) -> "CondaBuildPlugin.Config":
+ """Return the conda build plugin configuration."""
return cast(CondaBuildPlugin.Config, self.config.plugin_config)
@property
def recipe_folder(self) -> str:
+ """Return the folder where the recipe is in."""
recipe_folder = self.DEFAULT_RECIPE_FOLDER
plugin_config = self.get_plugin_config()
if plugin_config.recipe_folder:
@@ -288,6 +310,8 @@ class CondaBuildPlugin(MiniCondaPlugin):
template_path.replace(dir_ / "meta.yaml")
def find_recipe(self) -> Path:
+ """Find the recipe for the conda build."""
+
def _find_recipe_dir(path: Path) -> Path:
for subpath in path.iterdir():
if subpath.is_dir():
@@ -303,6 +327,8 @@ class CondaBuildPlugin(MiniCondaPlugin):
return _find_recipe_dir(Path(self.recipe_folder))
def find_build_target(self) -> str:
+ """Find the build target for conda builds."""
+
def find_parents(pth: Path) -> Path:
for parent in pth.iterdir():
if parent.is_dir():
@@ -327,6 +353,7 @@ class CondaBuildPlugin(MiniCondaPlugin):
@property
def build_configs(self) -> list[str]:
+ """`build_configuration` for conda builds."""
try:
recipe = self.find_recipe()
except FileNotFoundError:
@@ -338,6 +365,7 @@ class CondaBuildPlugin(MiniCondaPlugin):
@property
def build_target(self) -> str:
+ """`build_target` for the conda builds."""
build_target = self.get_plugin_config().build_target
if not build_target:
return self.find_build_target()
@@ -345,24 +373,28 @@ class CondaBuildPlugin(MiniCondaPlugin):
@hookimpl
def lpci_set_environment(self) -> dict[str, str]:
+ """`set_environment` implementation for the conda build plugin."""
# XXX techalchemy 2022-04-01: mypy is struggling with the super() call
rv: dict[str, str] = super().lpci_set_environment()
return rv
@hookimpl
def lpci_execute_before_run(self) -> str:
+ """`execute_before_run` implementation for the conda build plugin."""
# XXX techalchemy 2022-04-01: mypy is struggling with the super() call
rv: str = super().lpci_execute_before_run()
return rv
@hookimpl
def lpci_execute_after_run(self) -> str:
+ """`execute_after_run` implementation for the conda build plugin."""
# XXX techalchemy 2022-04-01: mypy is struggling with the super() call
rv: str = super().lpci_execute_after_run()
return rv
@hookimpl
def lpci_install_packages(self) -> list[str]:
+ """`install_packages` implementation for the conda build plugin."""
# XXX techalchemy 2022-04-01: mypy is struggling with the super() call
base_packages: list[str] = super().lpci_install_packages()
base_packages.extend(
@@ -387,6 +419,7 @@ class CondaBuildPlugin(MiniCondaPlugin):
@hookimpl
def lpci_execute_run(self) -> str:
+ """`execute_run` implementation for the conda build plugin."""
conda_channels = " ".join(f"-c {_}" for _ in self.conda_channels)
conda_channels = f" {conda_channels}" if conda_channels else ""
configs = " ".join(f"-m {_}" for _ in self.build_configs)
@@ -429,20 +462,25 @@ class GolangPlugin(BasePlugin):
"""
class Config(BaseConfig):
+ """Configuration for the golang plugin."""
+
golang_version: StrictStr
INTERPOLATES_RUN_COMMAND = True
def get_plugin_config(self) -> "GolangPlugin.Config":
+ """Return the golang plugin configuration."""
return cast(GolangPlugin.Config, self.config.plugin_config)
@hookimpl
def lpci_install_packages(self) -> list[str]:
+ """`install_packages` implementation for the golang plugin."""
version = self.get_plugin_config().golang_version
return [f"golang-{version}"]
@hookimpl
def lpci_execute_run(self) -> str:
+ """`execute_run` implementation for the golang plugin."""
version = self.get_plugin_config().golang_version
run_command = self.config.run or ""
return textwrap.dedent(
diff --git a/lpci/providers/__init__.py b/lpci/providers/__init__.py
index a2df69b..851e38a 100644
--- a/lpci/providers/__init__.py
+++ b/lpci/providers/__init__.py
@@ -1,3 +1,5 @@
+"""The package contains implementations for various providers."""
+
# Copyright 2021 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/lpci/providers/tests/test_lxd.py b/lpci/providers/tests/test_lxd.py
index 8bf3940..cf49fd1 100644
--- a/lpci/providers/tests/test_lxd.py
+++ b/lpci/providers/tests/test_lxd.py
@@ -933,7 +933,10 @@ class TestLXDProvider(TestCase):
mock_lxc.profile_edit.assert_called_once_with(
profile="default",
config={
- "config": {"nvidia.runtime": "true", "security.nesting": "true"},
+ "config": {
+ "nvidia.runtime": "true",
+ "security.nesting": "true",
+ },
"devices": {"gpu": {"type": "gpu"}},
},
project="test-project",
diff --git a/lpci/utils.py b/lpci/utils.py
index cd17360..5a29ab8 100644
--- a/lpci/utils.py
+++ b/lpci/utils.py
@@ -1,3 +1,5 @@
+"""Utils module which provides various helper functions."""
+
# Copyright 2021 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).
diff --git a/setup.py b/setup.py
index 5f30036..e553d28 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,9 @@
+"""Compatibility configuration for tools not supporting PEP517/PEP660.
+
+We probably could get rid of it, see
+https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html.
+"""
+
# Copyright 2021 Canonical Ltd. This software is licensed under the
# GNU General Public License version 3 (see the file LICENSE).