diff --git a/.github/labeler.yml b/.github/labeler.yml
index 200f1664..765559ce 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -8,19 +8,23 @@
"part:docs":
- "**/*.md"
+ - "docs/**"
- LICENSE
"part:tests":
- "tests/**"
"part:tooling":
+ - "**/*.ini"
+ - "**/*.toml"
+ - "**/*.yaml"
+ - "*requirements*.txt"
- ".git*"
- ".git*/**"
- - "**/*.toml"
- - "**/*.ini"
- CODEOWNERS
- MANIFEST.in
- - "*requirements*.txt"
+ - docs/mkdocstrings_autoapi.py
+ - noxfile.py
- setup.py
"part:channels":
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 5cae5477..ff03a9c0 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -17,8 +17,6 @@ jobs:
steps:
- name: Fetch sources
uses: actions/checkout@v3
- with:
- submodules: true
- name: Set up Python
uses: actions/setup-python@v4
@@ -46,8 +44,6 @@ jobs:
steps:
- name: Fetch sources
uses: actions/checkout@v3
- with:
- submodules: true
- name: Set up Python
uses: actions/setup-python@v4
@@ -69,8 +65,118 @@ jobs:
path: dist/
if-no-files-found: error
- create-github-release:
+ generate-docs-pr:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Fetch sources
+ uses: actions/checkout@v3
+
+ - name: Setup Git user and e-mail
+ uses: frequenz-floss/setup-git-user@v1
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.10"
+
+ - name: Install build dependencies
+ run: |
+ python -m pip install -U pip
+ python -m pip install .[docs]
+
+ - name: Generate the documentation
+ env:
+ MIKE_VERSION: pr-${{ github.event.number }}
+ run: |
+ mike deploy $MIKE_VERSION
+ mike set-default $MIKE_VERSION
+
+ - name: Upload site
+ uses: actions/upload-artifact@v3
+ with:
+ name: frequenz-channels-python-site
+ path: site/
+ if-no-files-found: error
+
+ publish-docs:
needs: ["test", "build-dist"]
+ if: github.event_name == 'push'
+ runs-on: ubuntu-20.04
+ permissions:
+ contents: write
+ steps:
+ - name: Calculate and check version
+ id: mike-metadata
+ env:
+ REF: ${{ github.ref }}
+ REF_NAME: ${{ github.ref_name }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ run: |
+ aliases=
+ version=
+ if test "$REF_NAME" = "$DEFAULT_BRANCH"
+ then
+ version=next
+ # A tag that starts with vX.Y or X.Y
+ elif echo "$REF" | grep -q '^refs/tags' && echo "$REF_NAME" | grep -Pq '^v?\d+\.\d+\.'
+ then
+ if echo "$REF_NAME" | grep -Pq -- "-" # pre-release
+ then
+ echo "::notice title=Documentation was not published::" \
+ "The tag '$REF_NAME' looks like a pre-release."
+ exit 0
+ fi
+ version=$(echo "$REF_NAME" | sed -r 's/^(v?[0-9]+\.[0-9]+)\..*$/\1/') # vX.Y
+ major=$(echo "$REF_NAME" | sed -r 's/^(v?[0-9]+)\..*$/\1/') # vX
+ default_major=$(echo "$DEFAULT_BRANCH" | sed -r 's/^(v?[0-9]+)\..*$/\1/') # vX
+ aliases=$major
+ if test "$major" = "$default_major"
+ then
+ aliases="$aliases latest"
+ fi
+ else
+ echo "::warning title=Documentation was not published::" \
+ "Don't know how to handle '$REF' to make 'mike' version."
+ exit 0
+ fi
+ echo "version=$version" >> $GITHUB_OUTPUT
+ echo "aliases=$aliases" >> $GITHUB_OUTPUT
+
+ - name: Fetch sources
+ if: steps.mike-metadata.outputs.version
+ uses: actions/checkout@v3
+
+ - name: Setup Git user and e-mail
+ if: steps.mike-metadata.outputs.version
+ uses: frequenz-floss/setup-git-user@v1
+
+ - name: Set up Python
+ if: steps.mike-metadata.outputs.version
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.10"
+
+ - name: Install build dependencies
+ if: steps.mike-metadata.outputs.version
+ run: |
+ python -m pip install -U pip
+ python -m pip install .[docs]
+
+ - name: Fetch the gh-pages branch
+ if: steps.mike-metadata.outputs.version
+ run: git fetch origin gh-pages --depth=1
+
+ - name: Publish site
+ if: steps.mike-metadata.outputs.version
+ env:
+ VERSION: ${{ steps.mike-metadata.outputs.version }}
+ ALIASES: ${{ steps.mike-metadata.outputs.aliases }}
+ run: |
+ mike deploy --push "$VERSION" $ALIASES
+
+ create-github-release:
+ needs: ["publish-docs"]
# Create a release only on tags creation
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
permissions:
diff --git a/.gitignore b/.gitignore
index b6e47617..2716aa17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,7 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# Automatically generated documentation
+docs/reference/
+site/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 49700b28..34b0ba82 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -29,6 +29,58 @@ python -m pip install nox
nox
```
+To build the documentation, first install the dependencies:
+
+```sh
+python -m pip install -e .[docs]
+```
+
+Then you can build the documentation (it will be written in the `site/`
+directory):
+
+```sh
+mkdocs build
+```
+
+Or you can just serve the documentation without building it using:
+
+```sh
+mkdocs serve
+```
+
+Your site will be updated **live** when you change your files (provided that
+you used `pip install -e .`, beware of a common pitfall of using `pip install`
+without `-e`, in that case the API reference won't change unless you do a new
+`pip install`).
+
+To build multi-version documentation, we use
+[mike](https://github.com/jimporter/mike). If you want to see how the
+multi-version sites looks like locally, you can use:
+
+```sh
+mike deploy my-version
+mike set-default my-version
+mike serve
+```
+
+`mike` works in mysterious ways. Some basic information:
+
+* `mike deploy` will do a `mike build` and write the results to your **local**
+ `gh-pages` branch. `my-version` is an arbitrary name for the local version
+ you want to preview.
+* `mike set-default` is needed so when you serve the documentation, it goes to
+ your newly produced documentation by default.
+* `mike serve` will serve the contents of your **local** `gh-pages` branch. Be
+ aware that, unlike `mkdocs serve`, changes to the sources won't be shown
+ live, as the `mike deploy` step is needed to refresh them.
+
+Be careful not to use `--push` with `mike deploy`, otherwise it will push your
+local `gh-pages` branch to the `origin` remote.
+
+That said, if you want to test the actual website in **your fork**, you can
+always use `mike deploy --push --remote your-fork-remote`, and then access the
+GitHub pages produced for your fork.
+
Releasing
=========
diff --git a/benchmarks/benchmark_anycast.py b/benchmarks/benchmark_anycast.py
index 2645a576..c461f3ea 100644
--- a/benchmarks/benchmark_anycast.py
+++ b/benchmarks/benchmark_anycast.py
@@ -1,11 +1,7 @@
-"""Benchmark for Anycast channels.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Benchmark for Anycast channels."""
import asyncio
import csv
diff --git a/benchmarks/benchmark_broadcast.py b/benchmarks/benchmark_broadcast.py
index fe54d7b1..1fc08ffe 100644
--- a/benchmarks/benchmark_broadcast.py
+++ b/benchmarks/benchmark_broadcast.py
@@ -1,11 +1,7 @@
-"""Benchmark for Broadcast channels.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Benchmark for Broadcast channels."""
import asyncio
import csv
diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css
new file mode 100644
index 00000000..572abff1
--- /dev/null
+++ b/docs/css/mkdocstrings.css
@@ -0,0 +1,44 @@
+/* Recommended style from:
+ * https://mkdocstrings.github.io/python/customization/#recommended-style-material
+ * With some additions from:
+ * https://github.com/mkdocstrings/mkdocstrings/blob/master/docs/css/mkdocstrings.css
+ */
+
+/* Indentation. */
+div.doc-contents:not(.first) {
+ padding-left: 25px;
+ border-left: .05rem solid var(--md-typeset-table-color);
+}
+
+/* Indentation. */
+div.doc-contents:not(.first) {
+ padding-left: 25px;
+ border-left: 4px solid rgba(230, 230, 230);
+ margin-bottom: 80px;
+}
+
+/* Avoid breaking parameters name, etc. in table cells. */
+td code {
+ word-break: normal !important;
+}
+
+/* Mark external links as such. */
+a.autorefs-external::after {
+ /* https://primer.style/octicons/arrow-up-right-24 */
+ background-image: url('data:image/svg+xml,');
+ content: ' ';
+
+ display: inline-block;
+ position: relative;
+ top: 0.1em;
+ margin-left: 0.2em;
+ margin-right: 0.1em;
+
+ height: 1em;
+ width: 1em;
+ border-radius: 100%;
+ background-color: var(--md-typeset-a-color);
+}
+a.autorefs-external:hover::after {
+ background-color: var(--md-accent-fg-color);
+}
diff --git a/docs/css/style.css b/docs/css/style.css
new file mode 100644
index 00000000..bbe472c8
--- /dev/null
+++ b/docs/css/style.css
@@ -0,0 +1,28 @@
+/* Based on:
+ * https://github.com/mkdocstrings/mkdocstrings/blob/master/docs/css/style.css
+ */
+
+/* Increase logo size */
+.md-header__button.md-logo {
+ padding-bottom: 0.2rem;
+ padding-right: 0;
+}
+.md-header__button.md-logo img {
+ height: 1.5rem;
+}
+
+/* Mark external links as such (also in nav) */
+a.external:hover::after, a.md-nav__link[href^="https:"]:hover::after {
+ /* https://primer.style/octicons/link-external-16 */
+ background-image: url('data:image/svg+xml,');
+ height: 0.8em;
+ width: 0.8em;
+ margin-left: 0.2em;
+ content: ' ';
+ display: inline-block;
+}
+
+/* More space at the bottom of the page */
+.md-main__inner {
+ margin-bottom: 1.5rem;
+}
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..1d736802
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,5 @@
+# Home
+
+Welcome to Frequenz's channels implementation for Python.
+
+This website is still under heavy construction. Most information can be found in the [Reference](reference/frequenz/channels) section.
diff --git a/docs/logo.png b/docs/logo.png
new file mode 100644
index 00000000..7a9db364
Binary files /dev/null and b/docs/logo.png differ
diff --git a/docs/mkdocstrings_autoapi.py b/docs/mkdocstrings_autoapi.py
new file mode 100644
index 00000000..f1a0e3ca
--- /dev/null
+++ b/docs/mkdocstrings_autoapi.py
@@ -0,0 +1,40 @@
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+
+"""Generate the code reference pages.
+
+Based on the recipe at:
+https://mkdocstrings.github.io/recipes/#automatic-code-reference-pages
+"""
+
+from pathlib import Path
+
+import mkdocs_gen_files
+
+SRC_PATH = "src"
+DST_PATH = "reference"
+
+# 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 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 fd:
+ fd.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())
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
new file mode 100644
index 00000000..1e13bf95
--- /dev/null
+++ b/docs/overrides/main.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% 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
new file mode 100644
index 00000000..272bf5fa
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,105 @@
+# MkDocs configuration
+# 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
+repo_name: "frequenz-channels-python"
+repo_url: "https://github.com/frequenz-floss/frequenz-channels-python"
+edit_uri: "edit/v0.x.x/docs/"
+
+# Build directories
+theme:
+ name: "material"
+ logo: logo.png
+ favicon: logo.png
+ language: en
+ icon:
+ edit: material/file-edit-outline
+ repo: fontawesome/brands/github
+ custom_dir: docs/overrides
+ features:
+ - navigation.instant
+ - navigation.tabs
+ - navigation.top
+ - navigation.tracking
+ - toc.follow
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: indigo
+ accent: deep purple
+ toggle:
+ icon: material/weather-sunny
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: black
+ accent: teal
+ toggle:
+ icon: material/weather-night
+ name: Switch to light mode
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/frequenz-floss
+ - icon: fontawesome/brands/linkedin
+ link: https://www.linkedin.com/company/frequenz-com
+ version:
+ provider: mike
+ default: latest
+
+extra_css:
+ - css/style.css
+ - css/mkdocstrings.css
+
+# Formatting options
+markdown_extensions:
+ - admonition
+ - attr_list
+ - pymdownx.details
+ - pymdownx.superfences
+ - pymdownx.tasklist
+ - pymdownx.tabbed
+ - pymdownx.snippets:
+ check_paths: true
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: "!!python/name:pymdownx.superfences.fence_code_format"
+ - toc:
+ permalink: "¤"
+
+plugins:
+ - gen-files:
+ scripts:
+ - docs/mkdocstrings_autoapi.py
+ - literate-nav:
+ nav_file: SUMMARY.md
+ - mike:
+ canonical_version: latest
+ - mkdocstrings:
+ custom_templates: templates
+ default_handler: python
+ handlers:
+ python:
+ options:
+ paths: [src]
+ docstring_section_style: spacy
+ merge_init_into_class: false
+ show_category_heading: true
+ show_root_heading: true
+ show_root_members_full_path: true
+ show_source: true
+ import:
+ - https://docs.python.org/3/objects.inv
+ - search
+ - section-index
+
+# Preview controls
+watch:
+ - src
diff --git a/noxfile.py b/noxfile.py
index c34fe5f9..5ec05dbe 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,44 +1,59 @@
-"""Code quality checks.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Code quality checks."""
import 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", "src", "tests", "benchmarks")
- session.run("isort", "--check", "src", "tests", "benchmarks")
+ session.run("black", "--check", *check_dirs, *check_files)
+ session.run("isort", "--check", *check_dirs, *check_files)
@nox.session
def pylint(session: nox.Session) -> None:
- session.install(".", "pylint", "pytest")
- session.run("pylint", "src", "tests", "benchmarks")
+ """Run pylint to do lint checks."""
+ session.install("-e", ".[docs]", "pylint", "pytest", "nox")
+ session.run("pylint", *check_dirs, *check_files)
@nox.session
def mypy(session: nox.Session) -> None:
- session.install(".", "mypy")
- session.run(
- "mypy",
- "--ignore-missing-imports",
+ """Run mypy to check type hints."""
+ session.install("-e", ".[docs]", "pytest", "nox", "mypy")
+
+ common_args = [
"--namespace-packages",
"--non-interactive",
"--install-types",
"--explicit-package-bases",
- "--follow-imports=silent",
"--strict",
- "src",
- "tests",
- "benchmarks",
- )
+ ]
+
+ 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
@@ -46,7 +61,7 @@ def docstrings(session: nox.Session) -> None:
"""Check docstring tone with pydocstyle and param descriptions with darglint."""
session.install("pydocstyle", "darglint", "toml")
- session.run("pydocstyle", "src", "tests", "benchmarks")
+ 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
@@ -56,6 +71,7 @@ def docstrings(session: nox.Session) -> None:
@nox.session
def pytest(session: nox.Session) -> None:
+ """Run all tests using pytest."""
session.install("pytest", "pytest-cov", "pytest-mock", "pytest-asyncio")
session.install("-e", ".")
session.run(
diff --git a/pyproject.toml b/pyproject.toml
index 380d39dd..452bb4ee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,6 +34,16 @@ dynamic = [ "version" ]
name ="Frequenz Energy-as-a-Service GmbH"
email = "floss@frequenz.com"
+[project.optional-dependencies]
+docs = [
+ "mike >= 1.1.2, < 2",
+ "mkdocs-gen-files >= 0.4.0, < 0.5.0",
+ "mkdocs-literate-nav >= 0.4.0, < 0.5.0",
+ "mkdocs-material >= 8.5.7, < 9",
+ "mkdocs-section-index >= 0.3.4, < 0.4.0",
+ "mkdocstrings[python] >= 0.19.0, < 0.20.0",
+]
+
[project.urls]
Changelog = "https://github.com/frequenz-floss/frequenz-channels-pytyhon/releases"
Repository = "https://github.com/frequenz-floss/frequenz-channels-pytyhon"
diff --git a/src/frequenz/channels/__init__.py b/src/frequenz/channels/__init__.py
index 7086ce69..b3d82484 100644
--- a/src/frequenz/channels/__init__.py
+++ b/src/frequenz/channels/__init__.py
@@ -1,11 +1,7 @@
-"""Channel implementations.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Channel implementations."""
from frequenz.channels.anycast import Anycast
from frequenz.channels.base_classes import BufferedReceiver, Peekable, Receiver, Sender
diff --git a/src/frequenz/channels/anycast.py b/src/frequenz/channels/anycast.py
index d5b0087b..740d14ce 100644
--- a/src/frequenz/channels/anycast.py
+++ b/src/frequenz/channels/anycast.py
@@ -1,11 +1,9 @@
-"""A channel for sending data across async tasks.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+"""A channel for sending data across async tasks."""
-License
-MIT
-"""
+from __future__ import annotations
from asyncio import Condition
from collections import deque
@@ -23,39 +21,43 @@ class Anycast(Generic[T]):
through a sender will be received by exactly one receiver.
In cases where each message need to be received by every receiver, a
- `Broadcast` channel may be used.
+ [Broadcast][frequenz.channels.Broadcast] channel may be used.
- Uses an `deque` internally, so Anycast channels are not thread-safe.
+ Uses an [deque][collections.deque] internally, so Anycast channels are not
+ thread-safe.
+
+ When there are multiple channel receivers, they can be awaited
+ simultaneously using [Select][frequenz.channels.Select],
+ [Merge][frequenz.channels.Merge] or
+ [MergeNamed][frequenz.channels.MergeNamed].
Example:
- ``` python
- async def send(sender: channel.Sender) -> None:
- while True:
- next = random.randint(3, 17)
- print(f"sending: {next}")
- await sender.send(next)
+ ``` python
+ async def send(sender: channel.Sender) -> None:
+ while True:
+ next = random.randint(3, 17)
+ print(f"sending: {next}")
+ await sender.send(next)
- async def recv(id: int, receiver: channel.Receiver) -> None:
- while True:
- next = await receiver.receive()
- print(f"receiver_{id} received {next}")
- await asyncio.sleep(0.1) # sleep (or work) with the data
+ async def recv(id: int, receiver: channel.Receiver) -> None:
+ while True:
+ next = await receiver.receive()
+ print(f"receiver_{id} received {next}")
+ await asyncio.sleep(0.1) # sleep (or work) with the data
- acast = channel.Anycast()
+ acast = channel.Anycast()
- sender = acast.get_sender()
- receiver_1 = acast.get_receiver()
+ sender = acast.get_sender()
+ receiver_1 = acast.get_receiver()
- asyncio.create_task(send(sender))
+ asyncio.create_task(send(sender))
- await recv(1, receiver_1)
- ```
+ await recv(1, receiver_1)
+ ```
- Check the `tests` and `benchmarks` directories for more examples. When
- there are multiple channel receivers, they can be awaited simultaneously
- using `channel.Select` or `channel.Merge`.
+ Check the `tests` and `benchmarks` directories for more examples.
"""
def __init__(self, maxsize: int = 10) -> None:
@@ -73,10 +75,13 @@ def __init__(self, maxsize: int = 10) -> None:
async def close(self) -> None:
"""Close the channel.
- Any further attempts to `send` data will return False.
+ Any further attempts to [send()][frequenz.channels.Sender.send] data
+ will return `False`.
Receivers will still be able to drain the pending items on the channel,
- but after that, subsequent `recv` calls will return None immediately.
+ but after that, subsequent
+ [receive()][frequenz.channels.Receiver.receive] calls will return `None`
+ immediately.
"""
self.closed = True
@@ -85,7 +90,7 @@ async def close(self) -> None:
async with self.recv_cv:
self.recv_cv.notify_all()
- def get_sender(self) -> "Sender[T]":
+ def get_sender(self) -> Sender[T]:
"""Create a new sender.
Returns:
@@ -93,7 +98,7 @@ def get_sender(self) -> "Sender[T]":
"""
return Sender(self)
- def get_receiver(self) -> "Receiver[T]":
+ def get_receiver(self) -> Receiver[T]:
"""Create a new receiver.
Returns:
@@ -129,8 +134,8 @@ async def send(self, msg: T) -> bool:
msg: The message to be sent.
Returns:
- Boolean indicating whether the message was sent, based on whether
- the channel is open or not.
+ Whether the message was sent, based on whether the channel is open
+ or not.
"""
if self._chan.closed:
return False
@@ -166,7 +171,7 @@ async def receive(self) -> Optional[T]:
will receive each message.
Returns:
- None, if the channel is closed, a message otherwise.
+ `None`, if the channel is closed, a message otherwise.
"""
while len(self._chan.deque) == 0:
if self._chan.closed:
diff --git a/src/frequenz/channels/base_classes.py b/src/frequenz/channels/base_classes.py
index 6cabf705..fd7f86f0 100644
--- a/src/frequenz/channels/base_classes.py
+++ b/src/frequenz/channels/base_classes.py
@@ -1,12 +1,9 @@
-"""
-Baseclasses for Channel Sender and Receiver.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+"""Baseclasses for Channel Sender and Receiver."""
-License
-MIT
-"""
+from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Callable, Generic, Optional, TypeVar
@@ -16,7 +13,7 @@
class Sender(ABC, Generic[T]):
- """A base class for channel Sender."""
+ """A channel Sender."""
@abstractmethod
async def send(self, msg: T) -> bool:
@@ -27,26 +24,26 @@ async def send(self, msg: T) -> bool:
Returns:
Whether the message was sent, based on whether the channel is open
- or not.
+ or not.
"""
class Receiver(ABC, Generic[T]):
- """A base class for channel Receiver."""
+ """A channel Receiver."""
@abstractmethod
async def receive(self) -> Optional[T]:
"""Receive a message from the channel.
Returns:
- None, if the channel is closed, a message otherwise.
+ `None`, if the channel is closed, a message otherwise.
"""
- def __aiter__(self) -> "Receiver[T]":
+ def __aiter__(self) -> Receiver[T]:
"""Initialize the async iterator over received values.
Returns:
- self, since no extra setup is needed for the iterator
+ `self`, since no extra setup is needed for the iterator.
"""
return self
@@ -65,27 +62,23 @@ async def __anext__(self) -> T:
raise StopAsyncIteration
return received
- def map(self, call: Callable[[T], U]) -> "Receiver[U]":
+ def map(self, call: Callable[[T], U]) -> Receiver[U]:
"""Return a receiver with `call` applied on incoming messages.
Args:
call: function to apply on incoming messages.
Returns:
- A receiver to read results of the given function from.
+ A `Receiver` to read results of the given function from.
"""
return _Map(self, call)
- def into_peekable(self) -> "Peekable[T]":
+ def into_peekable(self) -> Peekable[T]:
"""Convert the `Receiver` implementation into a `Peekable`.
Once this function has been called, the receiver will no longer be
usable, and calling `receive` on the receiver will raise an exception.
- This is a default implementation of `into_peekable` that always raises
- an exception. This method can be overridden in other implementations
- of `Receiver.`
-
Raises:
NotImplementedError: when a `Receiver` implementation doesn't have
a custom `get_peekable` implementation.
@@ -94,10 +87,11 @@ def into_peekable(self) -> "Peekable[T]":
class Peekable(ABC, Generic[T]):
- """A base class for creating Peekables for peeking into channels.
+ """A channel peekable.
- A Peekable provides a `peek` method that allows the user to get a peek at
- the latest value in the channel, without consuming anything.
+ A Peekable provides a [peek()][frequenz.channels.Peekable] method that
+ allows the user to get a peek at the latest value in the channel, without
+ consuming anything.
"""
@abstractmethod
@@ -105,13 +99,13 @@ def peek(self) -> Optional[T]:
"""Return the latest value that was sent to the channel.
Returns:
- The latest value received by the channel, and None, if nothing has
- been sent to the channel yet.
+ The latest value received by the channel, and `None`, if nothing
+ has been sent to the channel yet.
"""
class BufferedReceiver(Receiver[T]):
- """A base class for buffered channel receivers."""
+ """A channel receiver with a buffer."""
@abstractmethod
def enqueue(self, msg: T) -> None:
@@ -125,7 +119,8 @@ def enqueue(self, msg: T) -> None:
class _Map(Receiver[U], Generic[T, U]):
"""Apply a transform function on a channel receiver.
- Has two generic types -
+ Has two generic types:
+
- The input type: value type in the input receiver.
- The output type: return type of the transform method.
"""
@@ -145,7 +140,7 @@ async def receive(self) -> Optional[U]:
"""Return a transformed message received from the input channel.
Returns:
- None, if the channel is closed, a message otherwise.
+ `None`, if the channel is closed, a message otherwise.
"""
msg = await self._recv.receive()
if msg is None:
diff --git a/src/frequenz/channels/bidirectional.py b/src/frequenz/channels/bidirectional.py
index dbe06a65..b8d75ed5 100644
--- a/src/frequenz/channels/bidirectional.py
+++ b/src/frequenz/channels/bidirectional.py
@@ -1,11 +1,9 @@
-"""An abstraction to provide bi-directional communication between actors.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+"""An abstraction to provide bi-directional communication between actors."""
-License
-MIT
-"""
+from __future__ import annotations
from typing import Generic, Optional
@@ -39,7 +37,7 @@ def __init__(self, client_id: str, service_id: str) -> None:
)
@property
- def client_handle(self) -> "BidirectionalHandle[T, U]":
+ def client_handle(self) -> BidirectionalHandle[T, U]:
"""Get a BidirectionalHandle for the client to use.
Returns:
@@ -48,8 +46,8 @@ def client_handle(self) -> "BidirectionalHandle[T, U]":
return self._client_handle
@property
- def service_handle(self) -> "BidirectionalHandle[U, T]":
- """Get a BidirectionalHandle for the service to use.
+ def service_handle(self) -> BidirectionalHandle[U, T]:
+ """Get a `BidirectionalHandle` for the service to use.
Returns:
Object to send/receive messages with.
@@ -58,7 +56,7 @@ def service_handle(self) -> "BidirectionalHandle[U, T]":
class BidirectionalHandle(Sender[T], Receiver[U]):
- """A handle to a Bidirectional instance.
+ """A handle to a [Bidirectional][frequenz.channels.Bidirectional] instance.
It can be used to send/receive values between the client and service.
"""
@@ -80,7 +78,7 @@ async def send(self, msg: T) -> bool:
msg: The value to send.
Returns:
- Boolean indicating whether the send was successful.
+ Whether the send was successful or not.
"""
return await self._sender.send(msg)
@@ -88,6 +86,6 @@ async def receive(self) -> Optional[U]:
"""Receive a value from the other side.
Returns:
- Received value, or None if the channels are closed.
+ Received value, or `None` if the channels are closed.
"""
return await self._receiver.receive()
diff --git a/src/frequenz/channels/broadcast.py b/src/frequenz/channels/broadcast.py
index d83064bc..229f51ed 100644
--- a/src/frequenz/channels/broadcast.py
+++ b/src/frequenz/channels/broadcast.py
@@ -1,11 +1,9 @@
-"""A channel to broadcast messages to all receivers.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+"""A channel to broadcast messages to all receivers."""
-License
-MIT
-"""
+from __future__ import annotations
import logging
from asyncio import Condition
@@ -24,43 +22,46 @@
class Broadcast(Generic[T]):
"""A channel to broadcast messages to multiple receivers.
- Broadcast channels can have multiple senders and multiple receivers. Each
+ `Broadcast` channels can have multiple senders and multiple receivers. Each
message sent through any of the senders is received by all of the
receivers.
Internally, a broadcast receiver's buffer is implemented with just
- append/pop operations on either side of a `collections.deque`, which are
- thread-safe. Because of this, `Broadcast` channels are thread-safe.
+ append/pop operations on either side of a [deque][collections.deque], which
+ are thread-safe. Because of this, `Broadcast` channels are thread-safe.
+
+ When there are multiple channel receivers, they can be awaited
+ simultaneously using [Select][frequenz.channels.Select],
+ [Merge][frequenz.channels.Merge] or
+ [MergeNamed][frequenz.channels.MergeNamed].
Example:
- ``` python
- async def send(sender: channel.Sender) -> None:
- while True:
- next = random.randint(3, 17)
- print(f"sending: {next}")
- await sender.send(next)
+ ``` python
+ async def send(sender: channel.Sender) -> None:
+ while True:
+ next = random.randint(3, 17)
+ print(f"sending: {next}")
+ await sender.send(next)
- async def recv(id: int, receiver: channel.Receiver) -> None:
- while True:
- next = await receiver.receive()
- print(f"receiver_{id} received {next}")
- await asyncio.sleep(0.1) # sleep (or work) with the data
+ async def recv(id: int, receiver: channel.Receiver) -> None:
+ while True:
+ next = await receiver.receive()
+ print(f"receiver_{id} received {next}")
+ await asyncio.sleep(0.1) # sleep (or work) with the data
- bcast = channel.Broadcast()
+ bcast = channel.Broadcast()
- sender = bcast.get_sender()
- receiver_1 = bcast.get_receiver()
+ sender = bcast.get_sender()
+ receiver_1 = bcast.get_receiver()
- asyncio.create_task(send(sender))
+ asyncio.create_task(send(sender))
- await recv(1, receiver_1)
- ```
+ await recv(1, receiver_1)
+ ```
- Check the `tests` and `benchmarks` directories for more examples. When
- there are multiple channel receivers, they can be awaited simultaneously
- using `channel.Select` or `channel.Merge`.
+ Check the `tests` and `benchmarks` directories for more examples.
"""
def __init__(self, name: str, resend_latest: bool = False) -> None:
@@ -87,10 +88,13 @@ def __init__(self, name: str, resend_latest: bool = False) -> None:
async def close(self) -> None:
"""Close the Broadcast channel.
- Any further attempts to `send` data will return False.
+ Any further attempts to [send()][frequenz.channels.Sender.send] data
+ will return `False`.
Receivers will still be able to drain the pending items on their queues,
- but after that, subsequent `recv` calls will return None immediately.
+ but after that, subsequent
+ [receive()][frequenz.channels.Receiver.receive] calls will return `None`
+ immediately.
"""
self._latest = None
self.closed = True
@@ -108,7 +112,7 @@ def _drop_receiver(self, uuid: UUID) -> None:
if uuid in self.receivers:
del self.receivers[uuid]
- def get_sender(self) -> "Sender[T]":
+ def get_sender(self) -> Sender[T]:
"""Create a new broadcast sender.
Returns:
@@ -118,7 +122,7 @@ def get_sender(self) -> "Sender[T]":
def get_receiver(
self, name: Optional[str] = None, maxsize: int = 50
- ) -> "Receiver[T]":
+ ) -> Receiver[T]:
"""Create a new broadcast receiver.
Broadcast receivers have their own buffer, and when messages are not
@@ -135,17 +139,18 @@ def get_receiver(
uuid = uuid4()
if name is None:
name = str(uuid)
- recv: "Receiver[T]" = Receiver(uuid, name, maxsize, self)
+ recv: Receiver[T] = Receiver(uuid, name, maxsize, self)
self.receivers[uuid] = recv
if self._resend_latest and self._latest is not None:
recv.enqueue(self._latest)
return recv
- def get_peekable(self) -> "Peekable[T]":
+ def get_peekable(self) -> Peekable[T]:
"""Create a new Peekable for the broadcast channel.
- A Peekable provides a `peek` method that allows the user to get a peek
- at the latest value in the channel, without consuming anything.
+ A Peekable provides a [peek()][frequenz.channels.Peekable.peek] method
+ that allows the user to get a peek at the latest value in the channel,
+ without consuming anything.
Returns:
A Peekable to peek into the broadcast channel with.
@@ -156,7 +161,8 @@ def get_peekable(self) -> "Peekable[T]":
class Sender(BaseSender[T]):
"""A sender to send messages to the broadcast channel.
- Should not be created directly, but through the `Channel.get_sender()`
+ Should not be created directly, but through the
+ [Broadcast.get_sender()][frequenz.channels.Broadcast.get_sender]
method.
"""
@@ -175,8 +181,8 @@ async def send(self, msg: T) -> bool:
msg: The message to be broadcast.
Returns:
- Boolean indicating whether the message was sent, based on whether
- the broadcast channel is open or not.
+ Whether the message was sent, based on whether the broadcast
+ channel is open or not.
"""
if self._chan.closed:
return False
@@ -192,7 +198,8 @@ async def send(self, msg: T) -> bool:
class Receiver(BufferedReceiver[T]):
"""A receiver to receive messages from the broadcast channel.
- Should not be created directly, but through the `Channel.get_receiver()`
+ Should not be created directly, but through the
+ [Broadcast.get_receiver()][frequenz.channels.Broadcast.get_receiver]
method.
"""
@@ -257,14 +264,15 @@ async def receive(self) -> Optional[T]:
them. If there are no remaining messages in the buffer and the channel
is closed, returns `None` immediately.
- If `into_peekable` is called on a broadcast `Receiver`, further calls to
- `receive`, will raise an `EOFError`.
+ If [into_peekable()][frequenz.channels.Receiver.into_peekable] is called
+ on a broadcast `Receiver`, further calls to `receive`, will raise an
+ `EOFError`.
Raises:
EOFError: when the receiver has been converted into a `Peekable`.
Returns:
- None, if the channel is closed, a message otherwise.
+ `None`, if the channel is closed, a message otherwise.
"""
if not self._active:
raise EOFError("This receiver is no longer active.")
@@ -277,11 +285,12 @@ async def receive(self) -> Optional[T]:
ret = self._q.popleft()
return ret
- def into_peekable(self) -> "Peekable[T]":
+ def into_peekable(self) -> Peekable[T]:
"""Convert the `Receiver` implementation into a `Peekable`.
Once this function has been called, the receiver will no longer be
- usable, and calling `receive` on the receiver will raise an exception.
+ usable, and calling [receive()][frequenz.channels.Receiver.receive] on
+ the receiver will raise an exception.
Returns:
A `Peekable` instance.
@@ -294,8 +303,9 @@ def into_peekable(self) -> "Peekable[T]":
class Peekable(BasePeekable[T]):
"""A Peekable to peek into broadcast channels.
- A Peekable provides a `peek` method that allows the user to get a peek at
- the latest value in the channel, without consuming anything.
+ A Peekable provides a [peek()][frequenz.channels.Peekable] method that
+ allows the user to get a peek at the latest value in the channel, without
+ consuming anything.
"""
def __init__(self, chan: Broadcast[T]) -> None:
@@ -310,7 +320,7 @@ def peek(self) -> Optional[T]:
"""Return the latest value that was sent to the channel.
Returns:
- The latest value received by the channel, and None, if nothing has
- been sent to the channel yet, or if the channel is closed.
+ The latest value received by the channel, and `None`, if nothing
+ has been sent to the channel yet, or if the channel is closed.
"""
return self._chan._latest # pylint: disable=protected-access
diff --git a/src/frequenz/channels/merge.py b/src/frequenz/channels/merge.py
index 6f8abd0b..85e01fdd 100644
--- a/src/frequenz/channels/merge.py
+++ b/src/frequenz/channels/merge.py
@@ -1,11 +1,7 @@
-"""Merge messages coming from channels into a single stream.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Merge messages coming from channels into a single stream."""
import asyncio
from collections import deque
@@ -17,16 +13,17 @@
class Merge(Receiver[T]):
"""Merge messages coming from multiple channels into a single stream.
- For example, if there are two channel receivers with the same type, they
- can be awaited together, and their results merged into a single stream, by
- using `Merge` like this:
+ Example:
+ For example, if there are two channel receivers with the same type,
+ they can be awaited together, and their results merged into a single
+ stream, by using `Merge` like this:
- ```
- merge = Merge(receiver1, receiver2)
- while msg := await merge.receive():
- # do something with msg
- pass
- ```
+ ```python
+ merge = Merge(receiver1, receiver2)
+ while msg := await merge.receive():
+ # do something with msg
+ pass
+ ```
"""
def __init__(self, *args: Receiver[T]) -> None:
@@ -51,8 +48,8 @@ async def receive(self) -> Optional[T]:
"""Wait until there's a message in any of the channels.
Returns:
- The next message that was received, or None, if all channels have
- closed.
+ The next message that was received, or `None`, if all channels have
+ closed.
"""
# we use a while loop to continue to wait for new data, in case the
# previous `wait` completed because a channel was closed.
diff --git a/src/frequenz/channels/merge_named.py b/src/frequenz/channels/merge_named.py
index 210bc7bd..95381fe9 100644
--- a/src/frequenz/channels/merge_named.py
+++ b/src/frequenz/channels/merge_named.py
@@ -1,11 +1,7 @@
-"""Merge messages coming from channels into a single stream containing name of message.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Merge messages coming from channels into a single stream containing name of message."""
import asyncio
from collections import deque
@@ -39,8 +35,8 @@ async def receive(self) -> Optional[Tuple[str, T]]:
"""Wait until there's a message in any of the channels.
Returns:
- The next message that was received, or None, if all channels have
- closed.
+ The next message that was received, or `None`, if all channels have
+ closed.
"""
# we use a while loop to continue to wait for new data, in case the
# previous `wait` completed because a channel was closed.
diff --git a/src/frequenz/channels/select.py b/src/frequenz/channels/select.py
index e1615750..4afb7edc 100644
--- a/src/frequenz/channels/select.py
+++ b/src/frequenz/channels/select.py
@@ -1,14 +1,11 @@
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+
"""Select the first among multiple AsyncIterators.
Expects AsyncIterator class to raise `StopAsyncIteration`
exception once no more messages are expected or the channel
is closed in case of `Receiver` class.
-
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
"""
import asyncio
@@ -34,29 +31,31 @@ class _Selected:
class Select:
"""Select the next available message from a group of AsyncIterators.
- For example, if there are two async iterators that you want to
- simultaneously wait on, this can be done with:
-
- ```
- select = Select(name1 = receiver1, name2 = receiver2)
- while await select.ready():
- if msg := select.name1:
- if val := msg.inner:
- # do something with `val`
- pass
- else:
- # handle closure of receiver.
- pass
- elif msg := select.name2:
- # do something with `msg.inner`
- pass
- ```
-
If `Select` was created with more `AsyncIterator` than what are read in
- the if-chain after each call to `ready()`, messages coming in the
- additional async iterators are dropped, and a warning message is logged.
-
- `Receivers` also function as AsyncIterator.
+ the if-chain after each call to [ready()][frequenz.channels.Select.ready],
+ messages coming in the additional async iterators are dropped, and
+ a warning message is logged.
+
+ [Receiver][frequenz.channels.Receiver]s also function as `AsyncIterator`.
+
+ Example:
+ For example, if there are two async iterators that you want to
+ simultaneously wait on, this can be done with:
+
+ ```python
+ select = Select(name1 = receiver1, name2 = receiver2)
+ while await select.ready():
+ if msg := select.name1:
+ if val := msg.inner:
+ # do something with `val`
+ pass
+ else:
+ # handle closure of receiver.
+ pass
+ elif msg := select.name2:
+ # do something with `msg.inner`
+ pass
+ ```
"""
def __init__(self, **kwargs: AsyncIterator[Any]) -> None:
@@ -87,11 +86,11 @@ def __del__(self) -> None:
async def ready(self) -> bool:
"""Wait until there is a message in any of the async iterators.
- Returns True if there is a message available, and False if all async
- iterators have closed.
+ Returns `True` if there is a message available, and `False` if all
+ async iterators have closed.
Returns:
- Boolean indicating whether there are further messages or not.
+ Whether there are further messages or not.
"""
if self._ready_count > 0:
if self._ready_count == self._prev_ready_count:
@@ -139,17 +138,17 @@ async def ready(self) -> bool:
return True
def __getattr__(self, name: str) -> Optional[Any]:
- """Return the latest unread message from a AsyncIterator, if available.
+ """Return the latest unread message from a `AsyncIterator`, if available.
Args:
name: Name of the channel.
Returns:
- Latest unread message for the specified AsyncIterator, or None.
+ Latest unread message for the specified `AsyncIterator`, or `None`.
Raises:
- KeyError: when the name was not specified when creating the Select
- instance.
+ KeyError: when the name was not specified when creating the
+ `Select` instance.
"""
result = self._result[name]
if result is None:
diff --git a/src/frequenz/channels/utils/__init__.py b/src/frequenz/channels/utils/__init__.py
index 1e111452..a3958207 100644
--- a/src/frequenz/channels/utils/__init__.py
+++ b/src/frequenz/channels/utils/__init__.py
@@ -1,8 +1,4 @@
-"""Channel utilities.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Channel utilities."""
diff --git a/src/frequenz/channels/utils/file_watcher.py b/src/frequenz/channels/utils/file_watcher.py
index cbe4e9ef..fab17a51 100644
--- a/src/frequenz/channels/utils/file_watcher.py
+++ b/src/frequenz/channels/utils/file_watcher.py
@@ -1,11 +1,7 @@
-"""A Channel receiver for watching for new (or modified) files.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""A Channel receiver for watching for new (or modified) files."""
import asyncio
import pathlib
from enum import Enum
@@ -25,7 +21,7 @@ class EventType(Enum):
class FileWatcher(Receiver[pathlib.Path]):
- """A channel receiver that watches for file events using watchfiles."""
+ """A channel receiver that watches for file events."""
def __init__(
self,
@@ -36,9 +32,8 @@ def __init__(
Args:
paths: Paths to watch for changes.
- event_types: Types of events to watch for. Available types are:
- CREATE, MODIFY, DELETE. By default, watcher will watch for all
- types of events.
+ event_types: Types of events to watch for or `None` to watch for
+ all event types.
"""
if event_types is None:
event_types = {EventType.CREATE, EventType.MODIFY, EventType.DELETE}
diff --git a/src/frequenz/channels/utils/timer.py b/src/frequenz/channels/utils/timer.py
index 9f9e823c..258f0caa 100644
--- a/src/frequenz/channels/utils/timer.py
+++ b/src/frequenz/channels/utils/timer.py
@@ -1,11 +1,7 @@
-"""A timer receiver that returns the timestamp every `interval`.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""A timer receiver that returns the timestamp every `interval`."""
import asyncio
from datetime import datetime, timedelta
@@ -17,43 +13,46 @@
class Timer(Receiver[datetime]):
"""A timer receiver that returns the timestamp every `interval` seconds.
- Primarily for use with `channel.Select` calls.
-
- When you want something to happen with a fixed period:
- ```
- timer = channel.Timer(30.0)
- select = Select(bat_1 = receiver1, timer = timer)
- while await select.ready():
- if msg := select.bat_1:
- if val := msg.inner:
- process_data(val)
- else:
- logging.warn("battery channel closed")
- elif ts := select.timer:
- # something to do once every 30 seconds
- pass
- ```
-
- When you want something to happen when nothing else has happened in a
- certain interval:
- ```
- timer = channel.Timer(30.0)
- select = Select(bat_1 = receiver1, timer = timer)
- while await select.ready():
- timer.reset()
- if msg := select.bat_1:
- if val := msg.inner:
- process_data(val)
- else:
- logging.warn("battery channel closed")
- elif ts := select.timer:
- # something to do if there's no battery data for 30 seconds
- pass
- ```
+ Primarily for use with [Select][frequenz.channels.Select].
+
+ Example:
+ When you want something to happen with a fixed period:
+
+ ```python
+ timer = channel.Timer(30.0)
+ select = Select(bat_1 = receiver1, timer = timer)
+ while await select.ready():
+ if msg := select.bat_1:
+ if val := msg.inner:
+ process_data(val)
+ else:
+ logging.warn("battery channel closed")
+ if ts := select.timer:
+ # something to do once every 30 seconds
+ pass
+ ```
+
+ When you want something to happen when nothing else has happened in a
+ certain interval:
+
+ ```python
+ timer = channel.Timer(30.0)
+ select = Select(bat_1 = receiver1, timer = timer)
+ while await select.ready():
+ timer.reset()
+ if msg := select.bat_1:
+ if val := msg.inner:
+ process_data(val)
+ else:
+ logging.warn("battery channel closed")
+ if ts := select.timer:
+ # something to do if there's no battery data for 30 seconds
+ pass
+ ```
"""
def __init__(self, interval: float) -> None:
- """Create a `LocalTimer` instance.
+ """Create a `Timer` instance.
Args:
interval: number of seconds between messages.
@@ -69,8 +68,9 @@ def reset(self) -> None:
def stop(self) -> None:
"""Stop the timer.
- Once `stop` has been called, all subsequent calls to `receive` will
- immediately return None.
+ Once `stop` has been called, all subsequent calls to
+ [receive()][frequenz.channels.Timer.receive] will immediately return
+ `None`.
"""
self._stopped = True
@@ -78,7 +78,9 @@ async def receive(self) -> Optional[datetime]:
"""Return the current time once the next tick is due.
Returns:
- The time of the next tick.
+ The time of the next tick or `None` if
+ [stop()][frequenz.channels.Timer.stop] has been called on the
+ timer.
"""
if self._stopped:
return None
diff --git a/tests/__init__.py b/tests/__init__.py
index 9637aa1a..dc11980d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,9 +1,4 @@
-"""
-Tests for channels.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Tests for channels."""
diff --git a/tests/test_anycast.py b/tests/test_anycast.py
index 972e6b32..de388f21 100644
--- a/tests/test_anycast.py
+++ b/tests/test_anycast.py
@@ -1,11 +1,7 @@
-"""Tests for the Channel implementation.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Tests for the Channel implementation."""
import asyncio
diff --git a/tests/test_bidirectional.py b/tests/test_bidirectional.py
index a741dbf6..593b4133 100644
--- a/tests/test_bidirectional.py
+++ b/tests/test_bidirectional.py
@@ -1,11 +1,7 @@
-"""Tests for the RequestResponse implementation.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Tests for the RequestResponse implementation."""
import asyncio
diff --git a/tests/test_broadcast.py b/tests/test_broadcast.py
index ae70c19a..23b5f04e 100644
--- a/tests/test_broadcast.py
+++ b/tests/test_broadcast.py
@@ -1,11 +1,7 @@
-"""Tests for the Broadcast implementation.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Tests for the Broadcast implementation."""
import asyncio
from typing import Tuple
diff --git a/tests/test_merge.py b/tests/test_merge.py
index 2240b40f..14fb80c5 100644
--- a/tests/test_merge.py
+++ b/tests/test_merge.py
@@ -1,11 +1,7 @@
-"""Tests for the Merge implementation.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Tests for the Merge implementation."""
import asyncio
from typing import List
diff --git a/tests/test_mergenamed.py b/tests/test_mergenamed.py
index a0652545..bc9aac56 100644
--- a/tests/test_mergenamed.py
+++ b/tests/test_mergenamed.py
@@ -1,11 +1,8 @@
-"""Tests for the MergeNamed implementation.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+"""Tests for the MergeNamed implementation."""
-License
-MIT
-"""
import asyncio
from typing import List, Tuple
diff --git a/tests/test_select.py b/tests/test_select.py
index 86b06844..29d3f4d5 100644
--- a/tests/test_select.py
+++ b/tests/test_select.py
@@ -1,11 +1,8 @@
-"""Tests for the Select implementation.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+"""Tests for the Select implementation."""
-License
-MIT
-"""
import asyncio
from asyncio import Queue
from typing import List, Optional
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
index f03758fb..25e1e6d9 100644
--- a/tests/utils/__init__.py
+++ b/tests/utils/__init__.py
@@ -1,9 +1,4 @@
-"""
-Tests for channel utils.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Tests for channel utils."""
diff --git a/tests/utils/test_file_watcher.py b/tests/utils/test_file_watcher.py
index 38ca4ed2..3f31f0b4 100644
--- a/tests/utils/test_file_watcher.py
+++ b/tests/utils/test_file_watcher.py
@@ -1,11 +1,8 @@
-"""Tests for `channel.FileWatcher`
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
+"""Tests for `channel.FileWatcher`."""
-License
-MIT
-"""
import os
import pathlib
diff --git a/tests/utils/test_timer.py b/tests/utils/test_timer.py
index 1c93e39f..9a88fe21 100644
--- a/tests/utils/test_timer.py
+++ b/tests/utils/test_timer.py
@@ -1,11 +1,7 @@
-"""Tests for `channel.Timer`.
+# License: MIT
+# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-Copyright
-Copyright © 2022 Frequenz Energy-as-a-Service GmbH
-
-License
-MIT
-"""
+"""Tests for `channel.Timer`."""
import asyncio
import logging