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