Repository: jorenham/mainpy (language: python)

Issue:
python 3.13 support



Discussion / hints:

Fix patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index fc4c82d..3ff6a12 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -2,10 +2,19 @@ version: 2
 updates:
   - package-ecosystem: github-actions
     directory: /
+    target-branch: "dev"
     schedule:
       interval: monthly
-
+    groups:
+      actions:
+        patterns:
+          - "*"
   - package-ecosystem: pip
     directory: /
+    target-branch: "dev"
     schedule:
       interval: monthly
+    groups:
+      actions:
+        patterns:
+          - "*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9c66c65..fa394a2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,12 +4,17 @@ on:
   push:
     branches:
       - main
+      - dev
   pull_request:
   workflow_dispatch:
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   lint:
-    timeout-minutes: 1
+    timeout-minutes: 5
     runs-on: ubuntu-latest
 
     steps:
@@ -34,13 +39,24 @@ jobs:
       - name: ruff check
         run: poetry run ruff check --output-format=github
 
+      - name: markdownlint
+        uses: DavidAnson/markdownlint-cli2-action@v16
+        with:
+          config: ".markdownlint.yaml"
+          globs: "**/*.md"
+
+      - name: repo-review
+        uses: scientific-python/repo-review@v0.10.6
+        with:
+          plugins: sp-repo-review
+
   typecheck:
-    timeout-minutes: 2
+    timeout-minutes: 5
     runs-on: ubuntu-latest
 
     strategy:
       matrix:
-        python-version: ["3.8", "3.12"]
+        python-version: ["3.8", "3.12", "3.13-dev"]
 
     steps:
       - name: checkout
@@ -58,16 +74,16 @@ jobs:
       - name: install
         run: poetry install --only=typecheck --sync
 
-      - name: pyright
-        run: poetry run pyright
+      - name: basedpyright
+        run: poetry run basedpyright
 
   test:
-    timeout-minutes: 2
+    timeout-minutes: 5
 
     strategy:
       matrix:
         os: [ubuntu-latest, windows-latest]
-        python-version: ["3.8", "3.12"]
+        python-version: ["3.8", "3.12", "3.13-dev"]
 
     runs-on: ${{ matrix.os }}
 
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
deleted file mode 100644
index 58925e8..0000000
--- a/.github/workflows/stale.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-name: Close stale issues and pull requests
-
-on:
-  workflow_dispatch:
-  schedule:
-    - cron: "0 0 * * *" # Run every day at midnight
-
-jobs:
-  stale:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/stale@v9
-        with:
-          days-before-stale: 30
-          exempt-issue-labels: |
-            in-progress
-            help-wanted
-            pinned
-            security
-            enhancement
diff --git a/.gitignore b/.gitignore
index 39c6871..c937f25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,142 +3,20 @@ __pycache__/
 *.py[cod]
 *$py.class
 
-# C extensions
-*.so
-
 # Distribution / packaging
-.Python
 build/
-develop-eggs/
 dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
+site/
 
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
+# Cache
 .pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-#   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.
-#   However, in case of collaboration, if having platform-specific dependencies or dependencies
-#   having no cross-platform support, pipenv may install dependencies that don't work, or not
-#   install all needed dependencies.
-#Pipfile.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
+.ruff_cache/
 
 # Environments
 .env
-.venv
-env/
+.venv/
 venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
 
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# Jetbrains
+# IntelliJ
 .idea/
-
-# VSCode
-.vscode/
+.run/
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 0000000..205b139
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,7 @@
+default: true
+no-hard-tabs: true
+
+MD007:
+  indent: 4
+MD013:
+  line_length: 79
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..04f8547
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,61 @@
+ci:
+  autoupdate_branch: "dev"
+  autoupdate_commit_msg: "update pre-commit hooks"
+
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.6.0
+    hooks:
+      - id: check-added-large-files
+      - id: check-ast
+      - id: check-case-conflict
+      - id: check-docstring-first
+      - id: check-executables-have-shebangs
+      - id: check-json
+      - id: check-merge-conflict
+      - id: check-shebang-scripts-are-executable
+      - id: check-symlinks
+      - id: check-toml
+      - id: check-vcs-permalinks
+      - id: check-xml
+      - id: check-yaml
+      - id: debug-statements
+      - id: destroyed-symlinks
+      - id: detect-aws-credentials
+        args: [--allow-missing-credentials]
+      - id: detect-private-key
+      - id: fix-byte-order-marker
+      - id: forbid-submodules
+      - id: name-tests-test
+        args: [--pytest-test-first]
+      - id: no-commit-to-branch
+        args: [--branch, main, --branch, dev]
+      - id: trailing-whitespace
+        args: [--markdown-linebreak-ext=md]
+
+  - repo: https://github.com/python-poetry/poetry
+    rev: 1.8.2
+    hooks:
+      - id: poetry-check
+
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.2.6
+    hooks:
+      - id: codespell
+
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.4.2
+    hooks:
+      - id: ruff
+        args: [--fix, --show-fixes]
+
+  - repo: https://github.com/DetachHead/basedpyright
+    rev: v1.11.0
+    hooks:
+      - id: basedpyright
+
+  - repo: https://github.com/igorshubovych/markdownlint-cli
+    rev: v0.40.0
+    hooks:
+      - id: markdownlint
+        args: [--config, .markdownlint.yaml, --fix, "**/*.md"]
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..b0a62f5
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+  "recommendations": [
+    "charliermarsh.ruff",
+    "codezombiech.gitignore",
+    "davidanson.vscode-markdownlint",
+    "detachhead.basedpyright",
+    "editorconfig.editorconfig",
+    "elagil.pre-commit-helper",
+    "ms-python.python",
+    "serhioromano.vscode-gitflow",
+    "zeshuaro.vscode-python-poetry"
+  ],
+  "unwantedRecommendations": ["ms-python.vscode-pylance"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..aa34e83
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+  "editor.rulers": [79],
+  "git.branchProtection": ["main"],
+  "git.defaultBranchName": "dev",
+  "git.pullBeforeCheckout": true,
+  "python.analysis.typeCheckingMode": "strict",
+  "python.testing.pytestEnabled": true,
+  "python.testing.unittestEnabled": false,
+  "python.languageServer": "None"
+}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..ef1979c
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,70 @@
+# Code of Conduct - mainpy
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to make participation in our project and
+our community a harassment-free experience for everyone, regardless of age,
+body size, disability, ethnicity, sex characteristics, gender identity and
+expression, level of experience, education, socio-economic status, nationality,
+personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the
+  overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or
+  advances
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email
+  address, without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying and enforcing our standards
+of acceptable behavior and will take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban
+temporarily or permanently any contributor for other behaviors that they deem
+inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail
+address, posting via an official social media account, or acting as an
+appointed representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+`jhammudoglu<at>gmail<dot>com`
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Attribution
+
+This Code of Conduct is adapted from the
+[Contributor Covenant](https://contributor-covenant.org/) and was generated by
+[contributing-gen](https://github.com/bttger/contributing-gen).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2e4e3ee
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,249 @@
+<!-- omit in toc -->
+# Contributing to mainpy
+
+First off, thanks for taking the time to contribute! ❤️
+
+All types of contributions are encouraged and valued.
+See the [Table of Contents](#table-of-contents) for different ways to help and
+details about how this project handles them.
+Please make sure to read the relevant section before making your contribution.
+It will make it a lot easier for us maintainers and smooth out the experience
+for all involved.
+The community looks forward to your contributions. 🎉
+
+> [!NOTE]
+> And if you like mainpy, but just don't have time to contribute, that's fine.
+> There are other easy ways to support the project and show your appreciation,
+> which we would also be very happy about:
+>
+> - Star the project
+> - Tweet about it
+> - Refer this project in your project's readme
+> - Mention the project at local meetups and tell your friends/colleagues
+
+<!-- omit in toc -->
+## Table of Contents
+
+- [Code of Conduct](#code-of-conduct)
+- [I Have a Question](#i-have-a-question)
+- [I Want To Contribute](#i-want-to-contribute)
+    - [Reporting Bugs](#reporting-bugs)
+    - [Suggesting Enhancements](#suggesting-enhancements)
+    - [Your First Code Contribution](#your-first-code-contribution)
+    - [Improving The Documentation](#improving-the-documentation)
+
+## Code of Conduct
+
+This project and everyone participating in it is governed by the
+[mainpy Code of Conduct][COC].
+By participating, you are expected to uphold this code.
+Please report unacceptable behavior to `jhammudoglu<at>gmail<dot>com`.
+
+## I Have a Question
+
+> [!NOTE]
+> If you want to ask a question, we assume that you have read the
+> available [Documentation][DOC].
+
+Before you ask a question, it is best to search for existing [Issues][BUG]
+that might help you.
+In case you have found a suitable issue and still need clarification,
+you can write your question in this issue.
+It is also advisable to search the internet for answers first.
+
+If you then still feel the need to ask a question and need clarification, we
+recommend the following:
+
+- Open an [Issue][BUG].
+- Provide as much context as you can about what you're running into.
+- Provide project and platform versions (Python, pyright, ruff, etc),
+depending on what seems relevant.
+
+We will then take care of the issue as soon as possible.
+
+## I Want To Contribute
+
+> ### Legal Notice <!-- omit in toc -->
+>
+> When contributing to this project,
+> you must agree that you have authored 100% of the content,
+> that you have the necessary rights to the content and that the content you
+> contribute may be provided under the project license.
+
+### Reporting Bugs
+
+<!-- omit in toc -->
+#### Before Submitting a Bug Report
+
+A good bug report shouldn't leave others needing to chase you up for more
+information.
+Therefore, we ask you to investigate carefully, collect information and
+describe the issue in detail in your report.
+Please complete the following steps in advance to help us fix any potential
+bug as fast as possible.
+
+- Make sure that you are using the latest version.
+- Determine if your bug is really a bug and not an error on your side e.g.
+using incompatible environment components/versions
+(Make sure that you have read the [documentation][DOC].
+If you are looking for support, you might want to check
+[this section](#i-have-a-question)).
+- To see if other users have experienced (and potentially already solved)
+the same issue you are having, check if there is not already a bug report
+existing for your bug or error in the [bug tracker][BUG].
+- Also make sure to search the internet (including Stack Overflow) to see if
+users outside of the GitHub community have discussed the issue.
+- Collect information about the bug:
+    - Stack trace (Traceback)
+    - OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
+    - Version of the interpreter, compiler, SDK, runtime environment,
+    package manager, depending on what seems relevant.
+    - Possibly your input and the output
+    - Can you reliably reproduce the issue?
+    And can you also reproduce it with older versions?
+
+<!-- omit in toc -->
+#### How Do I Submit a Good Bug Report?
+
+> You must never report security related issues, vulnerabilities or bugs
+including sensitive information to the issue tracker, or elsewhere in public.
+Instead sensitive bugs must be sent by email to `jhammudoglu<at>gmail<dot>com`.
+
+We use GitHub issues to track bugs and errors.
+If you run into an issue with the project:
+
+- Open an [Issue][BUG].
+(Since we can't be sure at this point whether it is a bug or not,
+we ask you not to talk about a bug yet and not to label the issue.)
+- Explain the behavior you would expect and the actual behavior.
+- Please provide as much context as possible and describe the
+*reproduction steps* that someone else can follow to recreate the issue on
+their own.
+This usually includes your code.
+For good bug reports you should isolate the problem and create a reduced test
+case.
+- Provide the information you collected in the previous section.
+
+Once it's filed:
+
+- The project team will label the issue accordingly.
+- A team member will try to reproduce the issue with your provided steps.
+If there are no reproduction steps or no obvious way to reproduce the issue,
+the team will ask you for those steps and mark the issue as `needs-repro`.
+Bugs with the `needs-repro` tag will not be addressed until they are
+reproduced.
+- If the team is able to reproduce the issue, it will be marked `needs-fix`,
+as well as possibly other tags (such as `critical`), and the issue will be
+left to be [implemented by someone](#your-first-code-contribution).
+
+### Suggesting Enhancements
+
+This section guides you through submitting an enhancement suggestion for
+mainpy, **including completely new features and minor improvements to existing
+functionality**.
+Following these guidelines will help maintainers and the community to
+understand your suggestion and find related suggestions.
+
+<!-- omit in toc -->
+#### Before Submitting an Enhancement
+
+- Make sure that you are using the latest version.
+- Read the [documentation][DOC] carefully and find out if the functionality is
+already covered, maybe by an individual configuration.
+- Perform a [search][BUG] to see if the enhancement has already been suggested.
+If it has, add a comment to the existing issue instead of opening a new one.
+- Find out whether your idea fits with the scope and aims of the project.
+It's up to you to make a strong case to convince the project's developers of
+the merits of this feature. Keep in mind that we want features that will be
+useful to the majority of our users and not just a small subset. If you're
+just targeting a minority of users, consider writing an add-on/plugin library.
+
+<!-- omit in toc -->
+#### How Do I Submit a Good Enhancement Suggestion?
+
+Enhancement suggestions are tracked as [GitHub issues][BUG].
+
+- Use a **clear and descriptive title** for the issue to identify the
+suggestion.
+- Provide a **step-by-step description of the suggested enhancement** in as
+many details as possible.
+- **Describe the current behavior** and **explain which behavior you expected
+to see instead** and why. At this point you can also tell which alternatives
+do not work for you.
+- **Explain why this enhancement would be useful** to most mainpy users.
+You may also want to point out the other projects that solved it better and
+which could serve as inspiration.
+
+### Your First Code Contribution
+
+Ensure you have [poetry](https://python-poetry.org/docs/#installation)
+installed.
+It can help to use mainpy's lowest-supported Python version, so that you don't
+accidentally use those bleeding-edge Python features that you shouldn't, e.g.
+
+```bash
+poetry env use python3.11
+```
+
+Now you can install the dev dependencies using
+
+```bash
+poetry install --sync
+```
+
+### pre-commit
+
+Mainpy uses [pre-commit](https://pre-commit.com/) to ensure that the code is
+formatted and typed correctly when committing the changes.
+
+You can install it with
+
+```bash
+poetry run pre-commit install
+```
+
+It can also be manually run:
+
+```bash
+poetry run pre-commit run --all-files
+```
+
+This is roughly the same as manually running
+
+```bash
+poetry install --sync
+poetry check
+poetry run codespell
+poetry run ruff --fix
+poetry run basedpyright
+poetry run repo-review pyproject.toml
+```
+
+> [!NOTE]
+> Pre-commit doesn't run the tests. This will be run by github actions when
+> submitting a pull request. See the next section for more details.
+
+### Testing
+
+Mainpy uses [pytest](https://docs.pytest.org/en/stable/) for unit testing.
+These tests can be run with
+
+```bash
+poetry run pytest
+```
+
+### Improving The Documentation
+
+All [documentation] lives in the `README.md`. Please read it carefully before
+proposing any changes. Ensure that the markdown is formatted correctly with
+[markdownlint](https://github.com/DavidAnson/markdownlint/tree/main).
+
+<!-- omit in toc -->
+## Attribution
+
+This guide is based on the **contributing-gen**.
+[Make your own](https://github.com/bttger/contributing-gen)!
+
+[BUG]: https://github.com/jorenham/mainpy/issues
+[COC]: https://github.com/jorenham/mainpy/blob/main/CODE_OF_CONDUCT.md
+[DOC]: https://github.com/jorenham/mainpy?tab=readme-ov-file#mainpy
diff --git a/README.md b/README.md
index a4a8bf4..582729a 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,12 @@
 
 -----
 
-[![PyPI version shields.io](https://img.shields.io/pypi/v/mainpy.svg)][PYPI]
-[![PyPI pyversions](https://img.shields.io/pypi/pyversions/mainpy.svg)][PYPI]
-[![PyPI license](https://img.shields.io/pypi/l/mainpy.svg)][PYPI]
-[![Actions status](https://github.com/jorenham/mainpy/workflows/CI/badge.svg)][CI]
-[![Pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)][PYRIGHT]
-[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)][RUFF]
+[![mainpy - pypi version](https://img.shields.io/pypi/v/mainpy.svg)][PYPI]
+[![mainpy - python versions](https://img.shields.io/pypi/pyversions/mainpy.svg)][PYPI]
+[![mainpy - license](https://img.shields.io/pypi/l/mainpy.svg)][PYPI]
+[![mainpy - workflow status](https://github.com/jorenham/mainpy/workflows/CI/badge.svg)][CI]
+[![mainpy - basedpyright](https://img.shields.io/badge/basedpyright-checked-42b983)][BASEDPYRIGHT]
+[![mainpy - ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)][RUFF]
 
 -----
 
@@ -63,7 +63,6 @@ def async_app(): ...
 main(async_app)
 ```
 
-
 ## External Libraries
 
 Even though `mainpy` requires no other dependencies than `typing_extensions`
@@ -166,7 +165,7 @@ pip install mainpy[uvloop]
 
 [PYPI]: https://pypi.org/project/mainpy/
 [CI]: https://github.com/jorenham/mainpy/actions
-[PYRIGHT]: https://microsoft.github.io/pyright/
+[BASEDPYRIGHT]: https://detachhead.github.io/basedpyright/
 [RUFF]: https://github.com/astral-sh/ruff
 [UVLOOP]: https://github.com/MagicStack/uvloop
 [CLICK]: https://github.com/pallets/click
diff --git a/mainpy/__init__.py b/mainpy/__init__.py
index a91cae3..f45a7d3 100644
--- a/mainpy/__init__.py
+++ b/mainpy/__init__.py
@@ -1,8 +1,6 @@
 from __future__ import annotations
 
 import asyncio
-import functools
-import importlib.util
 import inspect
 import os
 import sys
@@ -14,6 +12,7 @@
     Protocol,
     TypeVar,
     cast,
+    final,
     overload,
 )
 
@@ -21,27 +20,18 @@
 if TYPE_CHECKING:
     import contextvars
 
-
-if sys.version_info < (3, 10):
-    from typing_extensions import TypeAlias
+if sys.version_info < (3, 11):
+    from typing_extensions import Never, TypeAlias
 else:
-    from typing import TypeAlias
+    from typing import Never, TypeAlias
 
 __all__ = ('main',)
 
-
 _R = TypeVar('_R')
-_F = TypeVar('_F', bound=Callable[..., Any])
+_F = TypeVar('_F', bound=Callable[[], Any])
 
 _SFunc: TypeAlias = Callable[[], _R]
-_AFunc: TypeAlias = _SFunc[Coroutine[Any, None, _R]]
-
-
-class _MainDecorator(Protocol):
-    @overload
-    def __call__(self, __f: _AFunc[_R], /) -> _R: ...
-    @overload
-    def __call__(self, __f: _SFunc[_R], /) -> _R: ...
+_AFunc: TypeAlias = Callable[[], Coroutine[Any, Any, _R]]
 
 
 def _infer_debug() -> bool:
@@ -57,9 +47,8 @@ def _infer_debug() -> bool:
     try:
         env_debug = int(env_debug)
     except ValueError as e:
-        raise OSError(
-            f'Invalid value for `DEBUG` env var: {env_debug!r}',
-        ) from e
+        errmsg = f'Invalid value for `DEBUG` env var: {env_debug!r}'
+        raise OSError(errmsg) from e
 
     return bool(env_debug)
 
@@ -69,10 +58,12 @@ def _infer_uvloop() -> bool:
     if 'uvloop' in sys.modules:
         return True
 
+    import importlib.util
+
     return importlib.util.find_spec('uvloop') is not None
 
 
-def _enable_debug():
+def _enable_debug() -> None:
     """Enable debug mode."""
     env = os.environ
 
@@ -87,72 +78,98 @@ def _enable_debug():
         faulthandler.enable()
 
 
-@overload
-def main(__f: _AFunc[_R], /) -> _R | _AFunc[_R]: ...
-@overload
-def main(__f: _SFunc[_R], /) -> _R | _SFunc[_R]: ...
+@final
+class _MainDecorator(Protocol):
+    @overload
+    def __call__(self, func: _AFunc[_R]) -> _AFunc[_R] | _R: ...
+    @overload
+    def __call__(self, func: _SFunc[_R]) -> _SFunc[_R] | _R: ...
+
 
 @overload
 def main(
+    func: None = ...,
+    /,
     *,
     debug: bool | None = ...,
     is_async: bool | None = ...,
     use_uvloop: bool | None = ...,
     context: contextvars.Context | None = ...,
 ) -> _MainDecorator: ...
+@overload
+def main(
+    func: _AFunc[_R],
+    /,
+    *,
+    debug: bool | None = ...,
+    is_async: bool | None = ...,
+    use_uvloop: bool | None = ...,
+    context: contextvars.Context | None = ...,
+) -> _AFunc[_R] | _R: ...
+@overload
+def main(
+    func: _SFunc[_R],
+    /,
+    *,
+    debug: bool | None = ...,
+    is_async: bool | None = ...,
+    use_uvloop: bool | None = ...,
+    context: contextvars.Context | None = ...,
+) -> _SFunc[_R] | _R: ...
 
 
 def main(
-    func: _F | None = None,
+    func: _AFunc[_R] | _SFunc[_R] | None = None,
     /,
     *,
     debug: bool | None = None,
     is_async: bool | None = None,
     use_uvloop: bool | None = None,
     context: contextvars.Context | None = None,
-) -> _MainDecorator | _F | Any:
+) -> _MainDecorator | _AFunc[_R] | _SFunc[_R] | _R:
     """
     Decorate a function to be the main entrypoint.
     """
     if func is None:
-        return cast(
-            _MainDecorator,
-            functools.partial(
-                main,
+        def _main(_func: _F, /) -> _F | object:
+            return main(
+                _func,
                 debug=debug,
                 is_async=is_async,
                 use_uvloop=use_uvloop,
                 context=context,
-            ),
-        )
+            )
+
+        return cast(_MainDecorator, _main)
 
     if not callable(func):
-        raise TypeError(f'expected a callable, got {func!r}')
+        errmsg = f'expected a callable, got {type(func)}'
+        raise TypeError(errmsg)
 
     if func.__module__ != '__main__':
         frame = inspect.currentframe()
         if not frame or frame.f_globals.get('__name__') != '__main__':
             return func
 
-    if debug or debug is None and _infer_debug():
+    if debug or (debug is None and _infer_debug()):
         _enable_debug()
 
-    if (
-        is_async is False
-        or is_async is None and not asyncio.iscoroutinefunction(func)
+    if is_async is False or (
+        is_async is None
+        and not asyncio.iscoroutinefunction(func)
     ):
-        return func()
+        return cast(_R, func())
 
     if use_uvloop is None:
         use_uvloop = _infer_uvloop()
 
     if sys.version_info < (3, 11):
         if use_uvloop:
-            import uvloop
+            import uvloop  # pyright: ignore[reportMissingImports]
 
             uvloop.install()  # pyright: ignore[reportUnknownMemberType]
 
-        return asyncio.run(func(), debug=debug)
+        return asyncio.run(cast(Coroutine[Any, Any, _R], func()), debug=debug)
 
     loop_factory = None
     if use_uvloop:
@@ -165,6 +182,6 @@ def main(
 
 
 @main
-def __main():
+def __main() -> Never:  # pyright: ignore[reportUnusedFunction]
     # this should never run
     raise AssertionError
diff --git a/poetry.lock b/poetry.lock
index 121ef95..a679a6c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,5 +1,66 @@
 # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 
+[[package]]
+name = "basedpyright"
+version = "1.11.0"
+description = "static type checking for Python (but based)"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "basedpyright-1.11.0-py3-none-any.whl", hash = "sha256:4c6d70428989acdd6277cb72d5adc0ca7084dfe4412e3d5d84672a3aa1881601"},
+    {file = "basedpyright-1.11.0.tar.gz", hash = "sha256:35dc0f4425b6f875f2e727b9062db5a5452fab945ea64b8a1a83f73390c94ab3"},
+]
+
+[package.dependencies]
+nodejs-bin = ">=18.4.0a4"
+
+[[package]]
+name = "cachetools"
+version = "5.3.3"
+description = "Extensible memoizing collections and decorators"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"},
+    {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"},
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+    {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
+[[package]]
+name = "chardet"
+version = "5.2.0"
+description = "Universal encoding detector for Python 3"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
+    {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
 [[package]]
 name = "codespell"
 version = "2.2.6"
@@ -28,20 +89,61 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
 [[package]]
 name = "exceptiongroup"
-version = "1.2.0"
+version = "1.2.1"
 description = "Backport of PEP 654 (exception groups)"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
-    {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+    {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+    {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
 ]
 
 [package.extras]
 test = ["pytest (>=6)"]
 
+[[package]]
+name = "filelock"
+version = "3.14.0"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"},
+    {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
+
+[[package]]
+name = "identify"
+version = "2.5.36"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
+    {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
 [[package]]
 name = "iniconfig"
 version = "2.0.0"
@@ -53,6 +155,41 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+    {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
+]
+
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+    {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
 [[package]]
 name = "nodeenv"
 version = "1.8.0"
@@ -67,26 +204,61 @@ files = [
 [package.dependencies]
 setuptools = "*"
 
+[[package]]
+name = "nodejs-bin"
+version = "18.4.0a4"
+description = "Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser."
+optional = false
+python-versions = "~=3.5"
+files = [
+    {file = "nodejs_bin-18.4.0a4-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:16cb1abf7fe8c11c574e1e474d9f934a0df49a480290eae6e733d8bb09512e22"},
+    {file = "nodejs_bin-18.4.0a4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:068ca987ed83ea1123775fafe5dc22d8f2ff920d7d31571e1bfe6fb1093833eb"},
+    {file = "nodejs_bin-18.4.0a4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:06cfeaa4d26eec94d8edb9927525ce94eb96dadc81f7d1daed42d1a7d003a4c9"},
+    {file = "nodejs_bin-18.4.0a4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:431ee3529f4fb226ddcfd4f14cb37e7df31238c42dfd051f4bf8f0c21029b133"},
+    {file = "nodejs_bin-18.4.0a4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21f1f77ddc8fe05353bb6d6ee8e5a62edb3a8dcdb2740a5f9307fd8d9eef6691"},
+    {file = "nodejs_bin-18.4.0a4-py3-none-win32.whl", hash = "sha256:59671fdc563dabb8be8a0b6dae4169d780482b3c9e0fba3f9aa2b7ee8d2261ac"},
+    {file = "nodejs_bin-18.4.0a4-py3-none-win_amd64.whl", hash = "sha256:cbd509218b4b17f75ee7841f9c21d5cacc1626d3b823a652a6627dbad18228ec"},
+]
+
+[package.extras]
+cmd = ["nodejs-cmd"]
+
 [[package]]
 name = "packaging"
-version = "23.2"
+version = "24.0"
 description = "Core utilities for Python packages"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
-    {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+    {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+    {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
 ]
 
+[[package]]
+name = "platformdirs"
+version = "4.2.1"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"},
+    {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+type = ["mypy (>=1.8)"]
+
 [[package]]
 name = "pluggy"
-version = "1.4.0"
+version = "1.5.0"
 description = "plugin and hook calling mechanisms for python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
-    {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
 ]
 
 [package.extras]
@@ -94,32 +266,66 @@ dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
-name = "pyright"
-version = "1.1.356"
-description = "Command line wrapper for pyright"
+name = "pre-commit"
+version = "3.7.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"},
+    {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pygments"
+version = "2.17.2"
+description = "Pygments is a syntax highlighting package written in Python."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pyright-1.1.356-py3-none-any.whl", hash = "sha256:a101b0f375f93d7082f9046cfaa7ba15b7cf8e1939ace45e984c351f6e8feb99"},
-    {file = "pyright-1.1.356.tar.gz", hash = "sha256:f05b8b29d06b96ed4a0885dad5a31d9dff691ca12b2f658249f583d5f2754021"},
+    {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+    {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+]
+
+[package.extras]
+plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pyproject-api"
+version = "1.6.1"
+description = "API to interact with the python pyproject.toml based projects"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"},
+    {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"},
 ]
 
 [package.dependencies]
-nodeenv = ">=1.6.0"
+packaging = ">=23.1"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
 
 [package.extras]
-all = ["twine (>=3.4.1)"]
-dev = ["twine (>=3.4.1)"]
+docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"]
 
 [[package]]
 name = "pytest"
-version = "8.1.1"
+version = "8.2.0"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
-    {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
+    {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"},
+    {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"},
 ]
 
 [package.dependencies]
@@ -127,11 +333,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
 exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=1.4,<2.0"
+pluggy = ">=1.5,<2.0"
 tomli = {version = ">=1", markers = "python_version < \"3.11\""}
 
 [package.extras]
-testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
 [[package]]
 name = "pytest-github-actions-annotate-failures"
@@ -147,48 +353,196 @@ files = [
 [package.dependencies]
 pytest = ">=4.0.0"
 
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "repo-review"
+version = "0.10.6"
+description = "Framework that can run checks on repos"
+optional = false
+python-versions = ">=3.10"
+files = [
+    {file = "repo_review-0.10.6-py3-none-any.whl", hash = "sha256:305a19b9d801d3e6b3052a0d740863e7954ffb68054359b7459ad7bdc65ea7ab"},
+    {file = "repo_review-0.10.6.tar.gz", hash = "sha256:df9021869625b77b579722a93d2dc417203e24395713b400357c4d4e6bd914b4"},
+]
+
+[package.dependencies]
+click = {version = ">=8", optional = true, markers = "extra == \"cli\""}
+markdown-it-py = "*"
+pyyaml = "*"
+rich = {version = ">=12.2", optional = true, markers = "extra == \"cli\""}
+rich-click = {version = "*", optional = true, markers = "extra == \"cli\""}
+tomli = {version = "*", markers = "python_version < \"3.11\""}
+typing-extensions = {version = "*", markers = "python_version < \"3.11\""}
+
+[package.extras]
+cli = ["click (>=8)", "rich (>=12.2)", "rich-click"]
+dev = ["click (>=8)", "pytest (>=7)", "rich (>=12.2)", "rich-click", "sp-repo-review (>=2023.12.21)", "validate-pyproject (>=0.14)"]
+docs = ["click (>=8)", "furo", "myst-parser (>=0.13)", "rich (>=12.2)", "rich-click", "sphinx (>=4.0)", "sphinx-autodoc-typehints", "sphinx-copybutton", "sphinx-github-changelog", "sphinxcontrib-programoutput", "sphinxext-opengraph"]
+test = ["pytest (>=7)", "sp-repo-review (>=2023.12.21)", "validate-pyproject (>=0.14)"]
+
+[[package]]
+name = "rich"
+version = "13.7.1"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
+    {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "rich-click"
+version = "1.8.0"
+description = "Format click help output nicely with rich"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "rich_click-1.8.0-py3-none-any.whl", hash = "sha256:846f504eb83a948d864888b2d73c71e52c310490c2babceac57e388aead086e2"},
+    {file = "rich_click-1.8.0.tar.gz", hash = "sha256:f8cad0d67d286d6cd6fc9f69f2d6f25c6c4c2d99fb9d6cb3b8987b593dbe6fa8"},
+]
+
+[package.dependencies]
+click = ">=7"
+rich = ">=10.7"
+typing-extensions = "*"
+
+[package.extras]
+dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"]
+docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"]
+
 [[package]]
 name = "ruff"
-version = "0.3.4"
+version = "0.4.2"
 description = "An extremely fast Python linter and code formatter, written in Rust."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
-    {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
-    {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
-    {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
-    {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
-    {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
-    {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
-    {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
-    {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
-    {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
-    {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
-    {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
-    {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
-    {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
-    {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
-    {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
-    {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
+    {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"},
+    {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"},
+    {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"},
+    {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"},
+    {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"},
+    {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"},
+    {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"},
+    {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"},
+    {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"},
+    {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"},
+    {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"},
+    {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"},
+    {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"},
+    {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"},
+    {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"},
+    {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"},
+    {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"},
 ]
 
 [[package]]
 name = "setuptools"
-version = "69.1.1"
+version = "69.5.1"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"},
-    {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"},
+    {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
+    {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
+[[package]]
+name = "sp-repo-review"
+version = "2024.4.23"
+description = "Review repos for compliance to the Scientific-Python development guidelines"
+optional = false
+python-versions = ">=3.10"
+files = [
+    {file = "sp_repo_review-2024.4.23-py3-none-any.whl", hash = "sha256:c13ed7e9136c50dd97256cf5fa3aae7aac44154789f2795d1b1486c5659a876b"},
+    {file = "sp_repo_review-2024.4.23.tar.gz", hash = "sha256:88cf57dcf7994024a9705e887c6744e923de5f0695711e29739fe7ae882fa2b8"},
+]
+
+[package.dependencies]
+pyyaml = "*"
+repo-review = [
+    {version = "*"},
+    {version = "*", extras = ["cli"], optional = true, markers = "extra == \"cli\""},
+]
+tomli = {version = "*", markers = "python_version < \"3.11\""}
+
+[package.extras]
+cli = ["repo-review[cli]"]
+dev = ["pytest (>=7)"]
+pyproject = ["validate-pyproject-schema-store[all]"]
+test = ["pytest (>=7)", "repo-review (>=0.10.6)"]
+
 [[package]]
 name = "tomli"
 version = "2.0.1"
@@ -200,15 +554,42 @@ files = [
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
 
+[[package]]
+name = "tox"
+version = "4.15.0"
+description = "tox is a generic virtualenv management and test command line tool"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "tox-4.15.0-py3-none-any.whl", hash = "sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea"},
+    {file = "tox-4.15.0.tar.gz", hash = "sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6"},
+]
+
+[package.dependencies]
+cachetools = ">=5.3.2"
+chardet = ">=5.2"
+colorama = ">=0.4.6"
+filelock = ">=3.13.1"
+packaging = ">=23.2"
+platformdirs = ">=4.1"
+pluggy = ">=1.3"
+pyproject-api = ">=1.6.1"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+virtualenv = ">=20.25"
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"]
+testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=8.0.2)", "distlib (>=0.3.8)", "flaky (>=3.7)", "hatch-vcs (>=0.4)", "hatchling (>=1.21)", "psutil (>=5.9.7)", "pytest (>=7.4.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-xdist (>=3.5)", "re-assert (>=1.1)", "time-machine (>=2.13)", "wheel (>=0.42)"]
+
 [[package]]
 name = "typing-extensions"
-version = "4.10.0"
+version = "4.11.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
-    {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
+    {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+    {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
 ]
 
 [[package]]
@@ -255,10 +636,30 @@ files = [
 docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
 test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
 
+[[package]]
+name = "virtualenv"
+version = "20.26.1"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"},
+    {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
 [extras]
 uvloop = ["uvloop"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "002fc4c17719ce8330cece427f5cde395cbee9a81268a4e1016fbdcad8d435ce"
+content-hash = "d8a41543bd2c5210a47945d0ef0480a1805c3e8c3204cc5a125fb62aa1bc26b8"
diff --git a/pyproject.toml b/pyproject.toml
index bf1370c..4c67cb7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,92 +5,115 @@ build-backend = "poetry.core.masonry.api"
 
 [tool.poetry]
 name = "mainpy"
-version = "1.2.1"
+version = "1.3.0"
 description = "Simplify your project's main entrypoint definition with @main"
 authors = ["Joren Hammudoglu <jhammudoglu@gmail.com>"]
 license = "MIT"
 readme = "README.md"
 repository = "https://github.com/jorenham/mainpy"
+documentation = "https://github.com/jorenham/mainpy?tab=readme-ov-file#mainpy"
 classifiers = [
     "Development Status :: 5 - Production/Stable",
-    "Intended Audience :: Developers",
-    "Natural Language :: English",
-    "Topic :: Software Development",
-    "Topic :: Utilities",
+    "Operating System :: OS Independent",
     "Typing :: Typed",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
 ]
 
+[tool.poetry.urls]
+"Bug Tracker" = "https://github.com/jorenham/mainpy/issues"
+"Changelog" = "https://github.com/jorenham/mainpy/releases"
+
+[tool.poetry.extras]
+uvloop = ["uvloop"]
+
 [tool.poetry.dependencies]
 python = "^3.8"
-typing_extensions = {version = "^4.1", python = "<3.10"}
+typing_extensions = {version = "^4.1", python = "<3.11"}
 
 [tool.poetry.dependencies.uvloop]
 version = ">=0.14,<1.0"
 optional = true
 markers = 'sys_platform != "win32"'
+python = "<3.13"
+
+[tool.poetry.group.dev.dependencies]
+pre-commit = {version = "^3.7.0", python = ">=3.9"}
+tox = "^4.15.0"
 
 [tool.poetry.group.lint.dependencies]
+sp-repo-review = {version = "^2024.4.23", extras = ["cli"], python = ">=3.10"}
 codespell = "^2.2.6"
-ruff = "^0.3.0"
-
-[tool.poetry.group.typecheck.dependencies]
-pyright = "^1.1.351"
-pytest = "^8.0"  # needed to infer annotations when linting
+ruff = "^0.4.2"
 
 [tool.poetry.group.test.dependencies]
-pytest = "^8.0"
+pytest = "^8.2.0"
+
+[tool.poetry.group.typecheck.dependencies]
+basedpyright = "^1.11.0"
+typing_extensions = "*"
+pytest = "*"
 
 [tool.poetry.group.test-github]
 optional = true
 [tool.poetry.group.test-github.dependencies]
-pytest-github-actions-annotate-failures = ">=0.2,<1.0"
+pytest-github-actions-annotate-failures = ">=0.2.0,<1"
 
-[tool.poetry.extras]
-uvloop = ["uvloop"]
 
-[tool.pyright]
+[tool.pytest.ini_options]
+addopts = ["-ra", "--strict-markers", "--strict-config"]
+filterwarnings = ["error"]
+log_cli_level = "INFO"
+minversion = "8.0"
+testpaths = ["tests"]
+xfail_strict = true
+
+
+[tool.codespell]
+context = 2
+
+
+[tool.basedpyright]
 include = ["mainpy", "tests"]
 exclude = [
     "**/__pycache__",
     ".git",
     ".github",
-    ".hypothesis",
     ".pytest_cache",
     ".ruff_cache",
     ".vscode",
     "dist",
 ]
+ignore = ["**/.venv"]
+stubPath = "."
 venvPath = "."
 venv = ".venv"
 pythonVersion = "3.8"
 pythonPlatform = "All"
-typeCheckingMode = "strict"
-useLibraryCodeForTypes = true
-
-deprecateTypingAliases = true
-disableBytesTypePromotions = true
-reportPropertyTypeMismatch = "warning"
-reportMissingImports = false  # uvloop is optional
-reportMissingModuleSource = false  # typing_extensions not needed on py310+
-reportMissingTypeStubs = false
-reportUnusedImport = "warning"
-reportUnusedClass = "warning"
-reportUnusedFunction = false  # @main decorated functions
-reportUnusedVariable = "warning"
-reportConstantRedefinition = "warning"
-reportInconsistentConstructor = "warning"
-reportMissingTypeArgument = "warning"
-reportUninitializedInstanceVariable = "warning"
-reportCallInDefaultInitializer = "warning"
-reportUnnecessaryIsInstance = "warning"
-reportUnnecessaryCast = "warning"
-reportUnnecessaryComparison = "warning"
-reportUnnecessaryContains = "warning"
-reportUnusedCallResult = "warning"
-reportUnusedExpression = "warning"
-reportUnnecessaryTypeIgnoreComment = false  # some aren't needed with `uvloop`
-reportMatchNotExhaustive = "warning"
-reportShadowedImports = "warning"
+typeCheckingMode = "all"
+reportAny = false
+reportUnusedCallResult = false
+reportInvalidCast = false
+# this appears to be broken since pyright 1.1.359
+reportUntypedFunctionDecorator = false
+# because of `sys.version_info()` conditionals
+reportUnreachable = false
+
+
+[tool.repo-review]
+ignore = [
+    "PY004",    # README.md >> docs/
+    "PC110",    # optype's style >> (black | ruff-format)
+    "PC140",    # (based)pyright >> mypy (by several orders of magnitude)
+    "PC170",    # no .rst
+    "PC180",    # no .css or .js
+    "MY",       # (based)pyright >> mypy (by several orders of magnitude)
+    "RTD",      # README.md >> rtd
+]
 
 
 [tool.ruff]
@@ -98,13 +121,9 @@ src = ["mainpy", "tests"]
 target-version = "py38"
 line-length = 79
 indent-width = 4
+show-fixes = true
 force-exclude = true
-extend-exclude = [
-    ".github",
-    ".vscode",
-    ".venv",
-    "py.typed",
-]
+extend-exclude = [".github", ".vscode"]
 
 [tool.ruff.lint]
 ignore-init-module-imports = true
@@ -119,6 +138,7 @@ select = [
     "YTT",      # flake8-2020
     "ANN",      # flake8-annotations
     "ASYNC",    # flake8-async
+    "TRIO",     # flake8-trio
     "S",        # flake8-bandit
     "BLE",      # flake8-blind-except
     "B",        # flake8-bugbear
@@ -147,41 +167,31 @@ select = [
     "TCH",      # flake8-type-checking
     "ARG",      # flake8-unused-arguments
     "PTH",      # flake8-use-pathlib
+    "TD",       # flake8-todos
+    "FIX",      # flake8-fixme
     "ERA",      # eradicate
+    "PD",       # pandas-vet
     "PGH",      # pygrep-hooks
     "PL",       # pylint
     "TRY",      # tryceratops
     "FLY",      # flynt
+    "NPY",      # numpy
+    "AIR",      # airflow
     "PERF",     # perflint,
     "FURB",     # refurb
     "LOG",      # flake8-logging
     "RUF",      # ruff
 ]
-extend-ignore = [
-    # flake8-annotations
-    "ANN001",   # missing-type-function-argument (deprecated)
-    "ANN002",   # missing-type-args (deprecated)
-    "ANN401",   # any-type
-    # flake8-bandit
-    "S101",     # assert
-    # pylint
-    "PLC0415",  # import-outside-top-level
-    "PLW1641",  # eq-without-hash (bug: doesn't consider super)
-    # tryceratops
-    "TRY003",   # raise-vanilla-args
-    # ruff
-    "RUF021",   # parenthesize-chained-operators
+ignore = [
+    "ANN401",   # flake8-annotations: any-type
+    "PLC0415",  # pylint: import-outside-top-level
 ]
 [tool.ruff.lint.per-file-ignores]
-"**/__init__.py" = [
-    "F401",     # pyflakes: unused-import
-]
 "tests/*" = [
-    "D",        # pydocstyle
+    "S",        # flake8-bandit
     "ANN",      # flake8-annotations
     "ARG001",   # flake8-unused-arguments: unused-function-argument
     "SLF001",   # flake8-self: private-member-access
-    "PT004",    # flake8-pytest-style: pytest-missing-fixture-name-underscore
 ]
 
 [tool.ruff.lint.pycodestyle]
@@ -190,43 +200,61 @@ max-line-length = 79
 [tool.ruff.lint.isort]
 case-sensitive = true
 combine-as-imports = true
-force-wrap-aliases = true
 known-first-party = ["mainpy"]
 lines-after-imports = 2
 lines-between-types = 0
-no-lines-before = ["future", "local-folder"]
-split-on-trailing-comma = false
-
-[tool.ruff.lint.pydocstyle]
-convention = "google"
-ignore-decorators = ["typing.overload"]
-
-[tool.ruff.lint.flake8-annotations]
-suppress-none-returning = true
 
 [tool.ruff.lint.flake8-quotes]
 inline-quotes = "single"
 
+[tool.ruff.lint.flake8-type-checking]
+strict = true
+
 [tool.ruff.format]
 docstring-code-format = true
-quote-style = "single"
 indent-style = "space"
 line-ending = "lf"
+quote-style = "single"
 skip-magic-trailing-comma = true
 
 
-[tool.codespell]
-skip = "./dist,./site,*.lock,*.pyc"
-context = 2
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+isolated_build = true
+requires =
+    tox>=4
+envlist =
+    py{38,312,313}
+    codespell
+    ruff
+    basedpyright
 
+[testenv]
+description = pytest
+skip_install = true
+allowlist_externals = poetry
+commands_pre = poetry install --without=lint --extras=uvloop --sync
+commands = poetry run pytest
 
-[tool.pytest.ini_options]
-minversion = "8.0"
-addopts = [
-    "-ra",
-    "--strict-markers",
-    "--strict-config",
-]
-xfail_strict = true
-testpaths = ["tests"]
-filterwarnings = ["error"]
+[testenv:codespell]
+description = codespell
+skip_install = true
+allowlist_externals = poetry
+commands_pre = poetry install --only=lint --no-root --sync
+commands = poetry run codespell .
+
+[testenv:ruff]
+description = ruff check
+skip_install = true
+allowlist_externals = poetry
+commands_pre = poetry install --only=lint --no-root --sync
+commands = poetry run ruff check
+
+[testenv:basedpyright]
+description = basedpyright
+skip_install = true
+allowlist_externals = poetry
+commands_pre = poetry install --only=typecheck --no-root --sync
+commands = poetry run basedpyright
+"""


Test patch:
diff --git a/tests/test_mainpy.py b/tests/test_mainpy.py
index 0e1b991..4c700ba 100644
--- a/tests/test_mainpy.py
+++ b/tests/test_mainpy.py
@@ -34,7 +34,7 @@ def no_uvloop():
     mp._infer_uvloop = lambda: False
 
     try:
-        yield
+        yield orig
     finally:
         mp._infer_uvloop = orig
 
@@ -96,6 +96,7 @@ def test_async_implicit_uvloop(monkeypatch: pytest.MonkeyPatch):
     @mp.main
     @_patch_module(monkeypatch, '__main__')
     async def loop_module():
+        await asyncio.sleep(0)
         loop = asyncio.get_running_loop()
         return loop.__module__.split('.')[0]