diff --git a/.cookiecutter-replay.json b/.cookiecutter-replay.json
new file mode 100644
index 00000000..e04d5a1f
--- /dev/null
+++ b/.cookiecutter-replay.json
@@ -0,0 +1,28 @@
+{
+ "cookiecutter": {
+ "type": "lib",
+ "name": "channels",
+ "description": "Channel implementations for Python",
+ "title": "Frequenz channels",
+ "keywords": "channel",
+ "github_org": "frequenz-floss",
+ "license": "MIT",
+ "author_name": "Frequenz Energy-as-a-Service GmbH",
+ "author_email": "floss@frequenz.com",
+ "python_package": "frequenz.channels",
+ "pypi_package_name": "frequenz-channels",
+ "github_repo_name": "frequenz-channels-python",
+ "default_codeowners": "@frequenz-floss/python-sdk-team",
+ "_extensions": [
+ "jinja2_time.TimeExtension",
+ "local_extensions.default_codeowners",
+ "local_extensions.github_repo_name",
+ "local_extensions.keywords",
+ "local_extensions.pypi_package_name",
+ "local_extensions.python_package",
+ "local_extensions.src_path",
+ "local_extensions.title"
+ ],
+ "_template": "gh:frequenz-floss/frequenz-repo-config-python"
+ }
+}
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index d9b39289..3b12aa28 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -5,7 +5,9 @@ name: Report something is not working properly 🐛
description:
Use this if there is something that is not working properly. If you are not
sure or you need help making something work, please ask a question instead.
-labels: priority:❓, type:bug
+labels:
+ - "priority:❓"
+ - "type:bug"
body:
- type: markdown
attributes:
@@ -59,6 +61,6 @@ body:
label: Extra information
description:
Please write here any extra information you think it might be relevant,
- e.g., if this didn't happened before, or if you suspect where the
- problem might be.
+ e.g., if this didn't happen before, or if you suspect where the problem
+ might be.
placeholder: Any extra information you think it might be relevant.
diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml
index 061b837c..459dd4b1 100644
--- a/.github/ISSUE_TEMPLATE/feature.yml
+++ b/.github/ISSUE_TEMPLATE/feature.yml
@@ -3,7 +3,10 @@
name: Request a feature or enhancement ✨
description: Use this if something is missing or could be done better or more easily.
-labels: part:❓, priority:❓, type:enhancement
+labels:
+ - "part:❓"
+ - "priority:❓"
+ - "type:enhancement"
body:
- type: markdown
attributes:
diff --git a/.github/RELEASE_NOTES.template.md b/.github/RELEASE_NOTES.template.md
index 5468717a..96a0240b 100644
--- a/.github/RELEASE_NOTES.template.md
+++ b/.github/RELEASE_NOTES.template.md
@@ -1,4 +1,4 @@
-# Frequenz Channels Release Notes
+# Frequenz channels Release Notes
## Summary
@@ -6,7 +6,7 @@
## Upgrading
-
+
## New Features
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 7bb5972b..6989df44 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -9,6 +9,7 @@
"part:docs":
- "**/*.md"
- "docs/**"
+ - "examples/**"
- LICENSE
"part:tests":
@@ -18,14 +19,13 @@
- "**/*.ini"
- "**/*.toml"
- "**/*.yaml"
- - "*requirements*.txt"
+ - "**/*.yml"
- ".git*"
- ".git*/**"
+ - docs/*.py
- CODEOWNERS
- MANIFEST.in
- - docs/mkdocstrings_autoapi.py
- noxfile.py
- - setup.py
"part:channels":
- any:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 71b849af..ba8647c4 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -1,9 +1,11 @@
-name: frequenz-channels-python
+name: CI
on:
merge_group:
pull_request:
push:
+ # We need to explicitly include tags because otherwise when adding
+ # `branches-ignore` it will only trigger on branches.
tags:
- '*'
branches-ignore:
@@ -14,44 +16,47 @@ on:
workflow_dispatch:
env:
- DEFAULT_PYTHON_VERSION: "3.11"
+ # Please make sure this version is included in the `matrix`, as the
+ # `matrix` section can't use `env`, so it must be entered manually
+ DEFAULT_PYTHON_VERSION: '3.11'
+ # It would be nice to be able to also define a DEFAULT_UBUNTU_VERSION
+ # but sadly `env` can't be used either in `runs-on`.
jobs:
- test:
+ nox:
+ name: Test with nox
strategy:
+ fail-fast: false
matrix:
os:
- ubuntu-20.04
- python-version:
+ python:
- "3.11"
runs-on: ${{ matrix.os }}
steps:
- - name: Fetch sources
- uses: actions/checkout@v3
-
- - name: Set up Python
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
-
- - uses: actions/cache@v3
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('pyproject.toml') }}
- restore-keys: |
- ${{ runner.os }}-${{ matrix.python-version }}-pip-
-
- - name: Install required Python packages
- run: |
- python -m pip install --upgrade pip
- python -m pip install nox
-
- - name: run nox
- run: nox
- timeout-minutes: 10
-
- build-dist:
+ - name: Fetch sources
+ uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python }}
+ cache: 'pip'
+
+ - name: Install required Python packages
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -e .[dev-noxfile]
+
+ - name: Run nox
+ # To speed things up a bit we use the speciall ci_checks_max session
+ # that uses the same venv to run multiple linting sessions
+ run: nox -e ci_checks_max pytest_min
+ timeout-minutes: 10
+
+ build:
+ name: Build distribution packages
runs-on: ubuntu-20.04
steps:
- name: Fetch sources
@@ -61,8 +66,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
+ cache: 'pip'
- - name: Install build dependencies
+ - name: Install required Python packages
run: |
python -m pip install -U pip
python -m pip install -U build
@@ -70,14 +76,15 @@ jobs:
- name: Build the source and binary distribution
run: python -m build
- - name: Upload dist files
+ - name: Upload distribution files
uses: actions/upload-artifact@v3
with:
- name: frequenz-channels-python-dist
+ name: dist-packages
path: dist/
if-no-files-found: error
- test-generate-docs:
+ test-docs:
+ name: Test documentation website generation
if: github.event_name != 'push'
runs-on: ubuntu-20.04
steps:
@@ -91,11 +98,12 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
+ cache: 'pip'
- name: Install build dependencies
run: |
python -m pip install -U pip
- python -m pip install .[docs]
+ python -m pip install .[dev-mkdocs]
- name: Generate the documentation
env:
@@ -107,12 +115,13 @@ jobs:
- name: Upload site
uses: actions/upload-artifact@v3
with:
- name: frequenz-channels-python-site
+ name: docs-site
path: site/
if-no-files-found: error
publish-docs:
- needs: ["test", "build-dist"]
+ name: Publish documentation website to GitHub pages
+ needs: ["nox", "build"]
if: github.event_name == 'push'
runs-on: ubuntu-20.04
permissions:
@@ -168,12 +177,13 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
+ cache: 'pip'
- name: Install build dependencies
if: steps.mike-metadata.outputs.version
run: |
python -m pip install -U pip
- python -m pip install .[docs]
+ python -m pip install .[dev-mkdocs]
- name: Fetch the gh-pages branch
if: steps.mike-metadata.outputs.version
@@ -188,9 +198,10 @@ jobs:
mike deploy --push --update-aliases "$VERSION" $ALIASES
create-github-release:
+ name: Create GitHub release
needs: ["publish-docs"]
# Create a release only on tags creation
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
permissions:
# We need write permissions on contents to create GitHub releases and on
# discussions to create the release announcement in the discussion forums
@@ -198,10 +209,10 @@ jobs:
discussions: write
runs-on: ubuntu-20.04
steps:
- - name: Download dist files
+ - name: Download distribution files
uses: actions/download-artifact@v3
with:
- name: frequenz-channels-python-dist
+ name: dist-packages
path: dist
- name: Download RELEASE_NOTES.md
@@ -225,7 +236,6 @@ jobs:
if echo "$REF_NAME" | grep -- -; then extra_opts=" --prerelease"; fi
gh release create \
-R "$REPOSITORY" \
- --discussion-category announcements \
--notes-file RELEASE_NOTES.md \
--generate-notes \
$extra_opts \
@@ -237,6 +247,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-to-pypi:
+ name: Publish packages to PyPI
needs: ["create-github-release"]
runs-on: ubuntu-20.04
permissions:
@@ -244,10 +255,10 @@ jobs:
# https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
id-token: write
steps:
- - name: Download dist files
+ - name: Download distribution files
uses: actions/download-artifact@v3
with:
- name: frequenz-channels-python-dist
+ name: dist-packages
path: dist
- name: Publish the Python distribution to PyPI
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index eafafa8e..cf458e6b 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -1,13 +1,5 @@
name: Pull Request Labeler
-# XXX: !!! SECURITY WARNING !!!
-# pull_request_target has write access to the repo, and can read secrets. We
-# need to audit any external actions executed in this workflow and make sure no
-# checked out code is run (not even installing dependencies, as installing
-# dependencies usually can execute pre/post-install scripts). We should also
-# only use hashes to pick the action to execute (instead of tags or branches).
-# For more details read:
-# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
on: [pull_request_target]
jobs:
@@ -18,7 +10,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Labeler
- # Only use hashes, see the security comment above
+ # XXX: !!! SECURITY WARNING !!!
+ # pull_request_target has write access to the repo, and can read secrets. We
+ # need to audit any external actions executed in this workflow and make sure no
+ # checked out code is run (not even installing dependencies, as installing
+ # dependencies usually can execute pre/post-install scripts). We should also
+ # only use hashes to pick the action to execute (instead of tags or branches).
+ # For more details read:
+ # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
uses: actions/labeler@0967ca812e7fdc8f5f71402a1b486d5bd061fe20 # 4.2.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.gitignore b/.gitignore
index 2716aa17..6997f699 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,12 +20,12 @@ parts/
sdist/
var/
wheels/
-pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
+.vscode
# PyInstaller
# Usually these files are written by a python script from a template
@@ -39,6 +39,7 @@ pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
+.htmlcov*/
.tox/
.nox/
.coverage
@@ -50,6 +51,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
+cover/
# Translations
*.mo
@@ -72,6 +74,7 @@ instance/
docs/_build/
# PyBuilder
+.pybuilder/
target/
# Jupyter Notebook
@@ -82,7 +85,9 @@ profile_default/
ipython_config.py
# pyenv
-.python-version
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@@ -109,6 +114,9 @@ venv/
ENV/
env.bak/
venv.bak/
+# direnv https://github.com/direnv/direnv
+.envrc
+.direnv/
# Spyder project settings
.spyderproject
@@ -128,6 +136,15 @@ dmypy.json
# Pyre type checker
.pyre/
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+.idea
+
# Automatically generated documentation
docs/reference/
site/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b6ef7f31..81e98c42 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,5 +1,4 @@
-# Contributing to `frequenz-channels`
-
+# Contributing to Frequenz channels
## Build
@@ -19,17 +18,55 @@ all the dependencies too):
python -m pip install -e .
```
-You can also use `nox` to run the tests and other checks:
+Or you can install all development dependencies (`mypy`, `pylint`, `pytest`,
+etc.) in one go too:
+```sh
+python -m pip install -e .[dev]
+```
+
+If you don't want to install all the dependencies, you can also use `nox` to
+run the tests and other checks creating its own virtual environments:
```sh
-python -m pip install nox
+python -m pip install .[dev-noxfile]
nox
```
-To build the documentation, first install the dependencies:
+You can also use `nox -R` to reuse the current testing environment to speed up
+test at the expense of a higher chance to end up with a dirty test environment.
+
+### Running tests / checks individually
+
+For a better development test cycle you can install the runtime and test
+dependencies and run `pytest` manually.
+
+```sh
+python -m pip install .[dev-pytest] # included in .[dev] too
+
+# And for example
+pytest tests/test_*.py
+```
+
+Or you can use `nox`:
+
+```sh
+nox -R -s pytest -- test/test_*.py
+```
+
+The same appliest to `pylint` or `mypy` for example:
+
+```sh
+nox -R -s pylint -- test/test_*.py
+nox -R -s mypy -- test/test_*.py
+```
+
+### Building the documentation
+
+To build the documentation, first install the dependencies (if you didn't
+install all `dev` dependencies):
```sh
-python -m pip install -e .[docs]
+python -m pip install -e .[dev-mkdocs]
```
Then you can build the documentation (it will be written in the `site/`
@@ -85,9 +122,9 @@ These are the steps to create a new release:
1. Get the latest head you want to create a release from.
2. Update the `RELEASE_NOTES.md` file if it is not complete, up to date, and
- clean from template comments (`
## Upgrading
-* The minimum supported Python version was bumped to 3.11, downstream projects will need to upgrade too to use this version.
-
-* The `Select` class was replaced by a new `select()` function, with the following improvements:
-
- * Type-safe: proper type hinting by using the new helper type guard `selected_from()`.
- * Fixes potential starvation issues.
- * Simplifies the interface by providing values one-by-one.
- * Guarantees there are no dangling tasks left behind when used as an async context manager.
-
- This new function is an [async iterator](https://docs.python.org/3.11/library/collections.abc.html#collections.abc.AsyncIterator), and makes sure no dangling tasks are left behind after a select loop is done.
-
- Example:
- ```python
- timer1 = Timer.periodic(datetime.timedelta(seconds=1))
- timer2 = Timer.timeout(datetime.timedelta(seconds=0.5))
-
- async for selected in select(timer1, timer2):
- if selected_from(selected, timer1):
- # Beware: `selected.value` might raise an exception, you can always
- # check for exceptions with `selected.exception` first or use
- # a try-except block. You can also quickly check if the receiver was
- # stopped and let any other unexpected exceptions bubble up.
- if selected.was_stopped():
- print("timer1 was stopped")
- continue
- print(f"timer1: now={datetime.datetime.now()} drift={selected.value}")
- timer2.stop()
- elif selected_from(selected, timer2):
- # Explicitly handling of exceptions
- match selected.exception:
- case ReceiverStoppedError():
- print("timer2 was stopped")
- case Exception() as exception:
- print(f"timer2: exception={exception}")
- case None:
- # All good, no exception, we can use `selected.value` safely
- print(
- f"timer2: now={datetime.datetime.now()} "
- f"drift={selected.value}"
- )
- case _ as unhanded:
- assert_never(unhanded)
- else:
- # This is not necessary, as select() will check for exhaustiveness, but
- # it is good practice to have it in case you forgot to handle a new
- # receiver added to `select()` at a later point in time.
- assert False
- ```
+
## New Features
-* A new `select()` function was added, please look at the *Upgrading* section for details.
-
-* A new `Event` utility receiver was added.
-
- This receiver can be made ready manually. It is mainly useful for testing but can also become handy in scenarios where a simple, on-off signal needs to be sent to a select loop for example.
-
- Example:
-
- ```python
- import asyncio
- from frequenz.channels import Receiver
- from frequenz.channels.util import Event, select, selected_from
-
- other_receiver: Receiver[int] = ...
- exit_event = Event()
-
- async def exit_after_10_seconds() -> None:
- asyncio.sleep(10)
- exit_event.set()
-
- asyncio.ensure_future(exit_after_10_seconds())
+
- async for selected in select(exit_event, other_receiver):
- if selected_from(selected, exit_event):
- break
- if selected_from(selected, other_receiver):
- print(selected.value)
- else:
- assert False, "Unknow receiver selected"
- ```
+## Bug Fixes
-* The `Timer` class now has more descriptive `__str__` and `__repr__` methods.
+
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index bbe88188..3755def6 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -1,3 +1,3 @@
* [Home](index.md)
* [API Reference](reference/)
-* [Development](CONTRIBUTING.md)
+* [Contributing](CONTRIBUTING.md)
diff --git a/docs/mkdocstrings_autoapi.py b/docs/mkdocstrings_autoapi.py
index e5a0ee71..b5cd911c 100644
--- a/docs/mkdocstrings_autoapi.py
+++ b/docs/mkdocstrings_autoapi.py
@@ -1,60 +1,8 @@
# License: MIT
-# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
-"""Generate the code reference pages.
+"""Generate the code reference pages."""
-Based on the recipe at:
-https://mkdocstrings.github.io/recipes/#automatic-code-reference-pages
-"""
+from frequenz.repo.config import mkdocs
-from pathlib import Path
-from typing import Tuple
-
-import mkdocs_gen_files
-
-SRC_PATH = "src"
-DST_PATH = "reference"
-
-
-def is_internal(path_parts: Tuple[str, ...]) -> bool:
- """Tell if the path is internal judging by the parts.
-
- Args:
- path_parts: Path.parts of the path to check.
-
- Returns:
- True if the path is internal.
- """
-
- def with_underscore_not_init(part: str) -> bool:
- return part.startswith("_") and part != "__init__"
-
- return any(p for p in path_parts if with_underscore_not_init(p))
-
-
-# type ignore because mkdocs_gen_files uses a very weird module-level
-# __getattr__() which messes up the type system
-nav = mkdocs_gen_files.Nav() # type: ignore
-
-for path in sorted(Path(SRC_PATH).rglob("*.py")):
- module_path = path.relative_to(SRC_PATH).with_suffix("")
-
- doc_path = path.relative_to(SRC_PATH).with_suffix(".md")
- full_doc_path = Path(DST_PATH, doc_path)
- parts = tuple(module_path.parts)
- if is_internal(parts):
- continue
- if parts[-1] == "__init__":
- doc_path = doc_path.with_name("index.md")
- full_doc_path = full_doc_path.with_name("index.md")
- parts = parts[:-1]
-
- nav[parts] = doc_path.as_posix()
-
- with mkdocs_gen_files.open(full_doc_path, "w") as output_file:
- output_file.write(f"::: {'.'.join(parts)}\n")
-
- mkdocs_gen_files.set_edit_path(full_doc_path, Path("..") / path)
-
-with mkdocs_gen_files.open(Path(DST_PATH) / "SUMMARY.md", "w") as nav_file:
- nav_file.writelines(nav.build_literate_nav())
+mkdocs.generate_python_api_pages("src", "reference")
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index 1e13bf95..980f3abb 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -2,7 +2,7 @@
{% block outdated %}
You're not viewing the latest (stable) version.
-
+
Click here to go to latest (stable) version
{% endblock %}
diff --git a/mkdocs.yml b/mkdocs.yml
index ad367d46..07a4a05e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -2,13 +2,14 @@
# For details see: https://www.mkdocs.org/user-guide/configuration/
# Project information
-site_name: Frequenz's channels for Python
-site_description: Frequenz's channels implementation for Python.
-site_author: Frequenz Energy-as-a-Service GmbH
-copyright: Frequenz Energy-as-a-Service GmbH
+site_name: "Frequenz channels"
+site_description: "Channel implementations for Python"
+site_author: "Frequenz Energy-as-a-Service GmbH"
+copyright: "Copyright © 2022 Frequenz Energy-as-a-Service GmbH"
repo_name: "frequenz-channels-python"
repo_url: "https://github.com/frequenz-floss/frequenz-channels-python"
edit_uri: "edit/v0.x.x/docs/"
+strict: true # Treat warnings as errors
# Build directories
theme:
@@ -88,7 +89,7 @@ plugins:
handlers:
python:
options:
- paths: [src]
+ paths: ["src"]
docstring_section_style: spacy
merge_init_into_class: false
show_category_heading: true
@@ -102,6 +103,6 @@ plugins:
# Preview controls
watch:
- - src
+ - "src"
- README.md
- CONTRIBUTING.md
diff --git a/noxfile.py b/noxfile.py
index e6e97452..81f3fc71 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,110 +1,8 @@
# License: MIT
-# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
-"""Code quality checks."""
+"""Configuration file for nox."""
-import nox
+from frequenz.repo.config import RepositoryType, nox
-check_dirs = [
- "benchmarks",
- "docs",
- "src",
- "tests",
-]
-
-check_files = [
- "noxfile.py",
-]
-
-
-@nox.session
-def formatting(session: nox.Session) -> None:
- """Run black and isort to make sure the format is uniform."""
- session.install("black", "isort")
- session.run("black", "--check", *check_dirs, *check_files)
- session.run("isort", "--check", *check_dirs, *check_files)
-
-
-@nox.session
-def pylint(session: nox.Session) -> None:
- """Run pylint to do lint checks."""
- session.install(
- "-e",
- ".[docs]",
- "pylint",
- "pytest",
- "sybil",
- "nox",
- "async-solipsism",
- "hypothesis",
- )
- session.run("pylint", *check_dirs, *check_files)
-
-
-@nox.session
-def mypy(session: nox.Session) -> None:
- """Run mypy to check type hints."""
- session.install(
- "-e",
- ".[docs]",
- "pytest",
- "nox",
- "mypy",
- "async-solipsism",
- "hypothesis",
- )
-
- common_args = [
- "--namespace-packages",
- "--non-interactive",
- "--install-types",
- "--explicit-package-bases",
- "--strict",
- ]
-
- pkg_args = []
- for pkg in check_dirs:
- if pkg == "src":
- pkg = "frequenz.channels"
- pkg_args.append("-p")
- pkg_args.append(pkg)
-
- session.run("mypy", *common_args, *pkg_args)
- session.run("mypy", *common_args, *check_files)
-
-
-@nox.session
-def docstrings(session: nox.Session) -> None:
- """Check docstring tone with pydocstyle and param descriptions with darglint."""
- session.install("pydocstyle", "darglint", "tomli")
-
- session.run("pydocstyle", *check_dirs, *check_files)
-
- # Darglint checks that function argument and return values are documented.
- # This is needed only for the `src` dir, so we exclude the other top level
- # dirs that contain code.
- session.run("darglint", "-v2", "src")
-
-
-@nox.session
-def pytest(session: nox.Session) -> None:
- """Run all tests using pytest."""
- session.install(
- "pytest",
- "pytest-cov",
- "pytest-mock",
- "pytest-asyncio",
- "async-solipsism",
- "hypothesis",
- "sybil",
- "pylint",
- )
- session.install("-e", ".")
- session.run(
- "pytest",
- "-W=all",
- "-vv",
- "--cov=frequenz.channels",
- "--cov-report=term",
- "--cov-report=html:.htmlcov",
- )
+nox.configure(RepositoryType.LIB)
diff --git a/pyproject.toml b/pyproject.toml
index 0f2eafd8..3a1102b8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,14 +1,20 @@
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+
[build-system]
-requires = ["setuptools == 65.3.0", "setuptools_scm[toml] == 7.0.5", "wheel"]
+requires = [
+ "setuptools == 67.7.2",
+ "setuptools_scm[toml] == 7.1.0",
+ "frequenz-repo-config[lib] == 0.3.0",
+]
build-backend = "setuptools.build_meta"
-
[project]
name = "frequenz-channels"
description = "Channel implementations for Python"
readme = "README.md"
license = { text = "MIT" }
-keywords = ["frequenz", "channel"]
+keywords = ["frequenz", "python", "lib", "channels", "channel"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
@@ -16,9 +22,13 @@ classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries",
+ "Typing :: Typed",
]
requires-python = ">= 3.11, < 4"
-dependencies = ["watchfiles >= 0.15.0, < 0.20.0"]
+dependencies = [
+ "typing-extensions >= 4.5.0, < 5",
+ "watchfiles >= 0.15.0, < 0.20.0",
+]
dynamic = ["version"]
[[project.authors]]
@@ -26,26 +36,61 @@ name = "Frequenz Energy-as-a-Service GmbH"
email = "floss@frequenz.com"
[project.optional-dependencies]
-docs = [
+dev-docstrings = [
+ "pydocstyle == 6.3.0",
+ "darglint == 1.8.1",
+ "tomli == 2.0.1", # Needed by pydocstyle to read pyproject.toml
+]
+dev-formatting = ["black == 23.3.0", "isort == 5.12.0"]
+dev-mkdocs = [
"mike == 1.1.2",
"mkdocs-gen-files == 0.5.0",
"mkdocs-literate-nav == 0.6.0",
"mkdocs-material == 9.1.17",
"mkdocs-section-index == 0.3.5",
"mkdocstrings[python] == 0.22.0",
+ "frequenz-repo-config[lib] == 0.3.0",
+]
+dev-mypy = [
+ "mypy == 1.2.0",
+ # For checking the noxfile, docs/ script, and tests
+ "frequenz-channels[dev-mkdocs,dev-noxfile,dev-pytest]",
+]
+dev-noxfile = ["nox == 2023.4.22", "frequenz-repo-config[lib] == 0.3.0"]
+dev-pylint = [
+ "pylint == 2.17.3",
+ # For checking the noxfile, docs/ script, and tests
+ "frequenz-channels[dev-mkdocs,dev-noxfile,dev-pytest]",
+]
+dev-pytest = [
+ "pytest == 7.3.1",
+ "async-solipsism == 0.5",
+ "hypothesis == 6.80.0",
+ "pytest-asyncio == 0.21.0",
+ "pytest-mock == 3.10.0",
+ # For checking docs examples
+ "sybil == 5.0.2",
+ "pylint == 2.17.3",
+]
+dev = [
+ "frequenz-channels[dev-mkdocs,dev-docstrings,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
]
[project.urls]
Changelog = "https://github.com/frequenz-floss/frequenz-channels-python/releases"
-Repository = "https://github.com/frequenz-floss/frequenz-channels-python"
Issues = "https://github.com/frequenz-floss/frequenz-channels-python/issues"
+Repository = "https://github.com/frequenz-floss/frequenz-channels-python"
Support = "https://github.com/frequenz-floss/frequenz-channels-python/discussions/categories/support"
-[tool.setuptools]
-include-package-data = true
+[tool.black]
+line-length = 88
+target-version = ['py311']
+include = '\.pyi?$'
-[tool.setuptools_scm]
-version_scheme = "post-release"
+[tool.isort]
+profile = "black"
+line_length = 88
+src_paths = ["src", "examples", "tests"]
[tool.pylint.similarities]
ignore-comments = ['yes']
@@ -54,17 +99,15 @@ ignore-imports = ['no']
min-similarity-lines = 40
[tool.pylint.messages_control]
-# disable wrong-import-order, ungrouped-imports because it conflicts with isort
-disable = ["too-few-public-methods", "wrong-import-order", "ungrouped-imports"]
-[tool.pylint.'DESIGN']
-max-attributes = 12
-
-[tool.isort]
-profile = "black"
-line_length = 88
-src_paths = ["src", "examples", "tests"]
+disable = [
+ "too-few-public-methods",
+ # disabled because it conflicts with isort
+ "wrong-import-order",
+ "ungrouped-imports",
+]
[tool.pytest.ini_options]
+testpaths = ["tests", "src"] # src for docs examples
asyncio_mode = "auto"
required_plugins = ["pytest-asyncio", "pytest-mock"]
markers = [
@@ -72,5 +115,8 @@ markers = [
]
[[tool.mypy.overrides]]
-module = ["async_solipsism", "async_solipsism.*"]
+module = ["async_solipsism", "async_solipsism.*", "sybil", "sybil.*"]
ignore_missing_imports = true
+
+[tool.setuptools_scm]
+version_scheme = "post-release"
diff --git a/src/conftest.py b/src/conftest.py
index 26192246..a4f76bc6 100644
--- a/src/conftest.py
+++ b/src/conftest.py
@@ -42,11 +42,12 @@ def get_import_statements(code: str) -> list[str]:
A list of import statements.
"""
tree = ast.parse(code)
- import_statements = []
+ import_statements: list[str] = []
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
import_statement = ast.get_source_segment(code, node)
+ assert import_statement is not None
import_statements.append(import_statement)
return import_statements
@@ -85,7 +86,9 @@ def path_to_import_statement(path: Path) -> str:
return import_statement
-class CustomPythonCodeBlockParser(CodeBlockParser):
+# We need to add the type ignore comment here because the Sybil library does not
+# have type annotations.
+class CustomPythonCodeBlockParser(CodeBlockParser): # type: ignore[misc]
"""Code block parser that validates extracted code examples using pylint.
This parser is a modified version of the default Python code block parser
@@ -103,7 +106,7 @@ class CustomPythonCodeBlockParser(CodeBlockParser):
Pylint warnings which are unimportant for code examples are disabled.
"""
- def __init__(self):
+ def __init__(self) -> None:
"""Initialize the parser."""
super().__init__("python")
@@ -199,7 +202,9 @@ def validate_with_pylint(
check=True,
)
except subprocess.CalledProcessError as exception:
- return exception.output.splitlines()
+ output = exception.output
+ assert isinstance(output, str)
+ return output.splitlines()
return []