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