Phase 7: netfox + godot-jolt stack upgrade
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
This commit is contained in:
BIN
Binary file not shown.
+239
@@ -0,0 +1,239 @@
|
||||
# don't import any costly modules
|
||||
import os
|
||||
import sys
|
||||
|
||||
report_url = (
|
||||
"https://github.com/pypa/setuptools/issues/new?template=distutils-deprecation.yml"
|
||||
)
|
||||
|
||||
|
||||
def warn_distutils_present():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||
"using distutils directly, ensure that setuptools is installed in the "
|
||||
"traditional way (e.g. not an editable install), and/or make sure "
|
||||
"that setuptools is always imported before distutils."
|
||||
)
|
||||
|
||||
|
||||
def clear_distutils():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Setuptools is replacing distutils. Support for replacing "
|
||||
"an already imported distutils is deprecated. In the future, "
|
||||
"this condition will fail. "
|
||||
f"Register concerns at {report_url}"
|
||||
)
|
||||
mods = [
|
||||
name
|
||||
for name in sys.modules
|
||||
if name == "distutils" or name.startswith("distutils.")
|
||||
]
|
||||
for name in mods:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
def enabled():
|
||||
"""
|
||||
Allow selection of distutils by environment variable.
|
||||
"""
|
||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
||||
if which == 'stdlib':
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Reliance on distutils from stdlib is deprecated. Users "
|
||||
"must rely on setuptools to provide the distutils module. "
|
||||
"Avoid importing distutils or import setuptools first, "
|
||||
"and avoid setting SETUPTOOLS_USE_DISTUTILS=stdlib. "
|
||||
f"Register concerns at {report_url}"
|
||||
)
|
||||
return which == 'local'
|
||||
|
||||
|
||||
def ensure_local_distutils():
|
||||
import importlib
|
||||
|
||||
clear_distutils()
|
||||
|
||||
# With the DistutilsMetaFinder in place,
|
||||
# perform an import to cause distutils to be
|
||||
# loaded from setuptools._distutils. Ref #2906.
|
||||
with shim():
|
||||
importlib.import_module('distutils')
|
||||
|
||||
# check that submodules load as expected
|
||||
core = importlib.import_module('distutils.core')
|
||||
assert '_distutils' in core.__file__, core.__file__
|
||||
assert 'setuptools._distutils.log' not in sys.modules
|
||||
|
||||
|
||||
def do_override():
|
||||
"""
|
||||
Ensure that the local copy of distutils is preferred over stdlib.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||
for more motivation.
|
||||
"""
|
||||
if enabled():
|
||||
warn_distutils_present()
|
||||
ensure_local_distutils()
|
||||
|
||||
|
||||
class _TrivialRe:
|
||||
def __init__(self, *patterns) -> None:
|
||||
self._patterns = patterns
|
||||
|
||||
def match(self, string):
|
||||
return all(pat in string for pat in self._patterns)
|
||||
|
||||
|
||||
class DistutilsMetaFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
# optimization: only consider top level modules and those
|
||||
# found in the CPython test suite.
|
||||
if path is not None and not fullname.startswith('test.'):
|
||||
return None
|
||||
|
||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||
method = getattr(self, method_name, lambda: None)
|
||||
return method()
|
||||
|
||||
def spec_for_distutils(self):
|
||||
if self.is_cpython():
|
||||
return None
|
||||
|
||||
import importlib
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
mod = importlib.import_module('setuptools._distutils')
|
||||
except Exception:
|
||||
# There are a couple of cases where setuptools._distutils
|
||||
# may not be present:
|
||||
# - An older Setuptools without a local distutils is
|
||||
# taking precedence. Ref #2957.
|
||||
# - Path manipulation during sitecustomize removes
|
||||
# setuptools from the path but only after the hook
|
||||
# has been loaded. Ref #2980.
|
||||
# In either case, fall back to stdlib behavior.
|
||||
return None
|
||||
|
||||
class DistutilsLoader(importlib.abc.Loader):
|
||||
def create_module(self, spec):
|
||||
mod.__name__ = 'distutils'
|
||||
return mod
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
return importlib.util.spec_from_loader(
|
||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_cpython():
|
||||
"""
|
||||
Suppress supplying distutils for CPython (build and tests).
|
||||
Ref #2965 and #3007.
|
||||
"""
|
||||
return os.path.isfile('pybuilddir.txt')
|
||||
|
||||
def spec_for_pip(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running under pip.
|
||||
See pypa/pip#8761 for rationale.
|
||||
"""
|
||||
if sys.version_info >= (3, 12) or self.pip_imported_during_build():
|
||||
return
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
@classmethod
|
||||
def pip_imported_during_build(cls):
|
||||
"""
|
||||
Detect if pip is being imported in a build script. Ref #2355.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
return any(
|
||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def frame_file_is_setup(frame):
|
||||
"""
|
||||
Return True if the indicated frame suggests a setup.py file.
|
||||
"""
|
||||
# some frames may not have __file__ (#2940)
|
||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
||||
|
||||
def spec_for_sensitive_tests(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running select tests under CPython.
|
||||
|
||||
python/cpython#91169
|
||||
"""
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
sensitive_tests = (
|
||||
[
|
||||
'test.test_distutils',
|
||||
'test.test_peg_generator',
|
||||
'test.test_importlib',
|
||||
]
|
||||
if sys.version_info < (3, 10)
|
||||
else [
|
||||
'test.test_distutils',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
for name in DistutilsMetaFinder.sensitive_tests:
|
||||
setattr(
|
||||
DistutilsMetaFinder,
|
||||
f'spec_for_{name}',
|
||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
||||
)
|
||||
|
||||
|
||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||
|
||||
|
||||
def add_shim():
|
||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
||||
|
||||
|
||||
class shim:
|
||||
def __enter__(self) -> None:
|
||||
insert_shim()
|
||||
|
||||
def __exit__(self, exc: object, value: object, tb: object) -> None:
|
||||
_remove_shim()
|
||||
|
||||
|
||||
def insert_shim():
|
||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||
|
||||
|
||||
def _remove_shim():
|
||||
try:
|
||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# DistutilsMetaFinder can only be disabled in Python < 3.12 (PEP 632)
|
||||
remove_shim = _remove_shim
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
__import__('_distutils_hack').do_override()
|
||||
@@ -0,0 +1,33 @@
|
||||
# This is a stub package designed to roughly emulate the _yaml
|
||||
# extension module, which previously existed as a standalone module
|
||||
# and has been moved into the `yaml` package namespace.
|
||||
# It does not perfectly mimic its old counterpart, but should get
|
||||
# close enough for anyone who's relying on it even when they shouldn't.
|
||||
import yaml
|
||||
|
||||
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
||||
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
||||
if not getattr(yaml, '__with_libyaml__', False):
|
||||
from sys import version_info
|
||||
|
||||
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
||||
raise exc("No module named '_yaml'")
|
||||
else:
|
||||
from yaml._yaml import *
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The _yaml extension module is now located at yaml._yaml'
|
||||
' and its location is subject to change. To use the'
|
||||
' LibYAML-based parser and emitter, import from `yaml`:'
|
||||
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
||||
DeprecationWarning
|
||||
)
|
||||
del warnings
|
||||
# Don't `del yaml` here because yaml is actually an existing
|
||||
# namespace member of _yaml.
|
||||
|
||||
__name__ = '_yaml'
|
||||
# If the module is top-level (i.e. not a part of any specific package)
|
||||
# then the attribute should be set to ''.
|
||||
# https://docs.python.org/3.8/library/types.html
|
||||
__package__ = ''
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
pip
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: aiosqlite
|
||||
Version: 0.22.1
|
||||
Summary: asyncio bridge to the standard sqlite3 module
|
||||
Author-email: Amethyst Reese <amethyst@n7.gg>
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/x-rst
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Framework :: AsyncIO
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
License-File: LICENSE
|
||||
Requires-Dist: attribution==1.8.0 ; extra == "dev"
|
||||
Requires-Dist: black==25.11.0 ; extra == "dev"
|
||||
Requires-Dist: build>=1.2 ; extra == "dev"
|
||||
Requires-Dist: coverage[toml]==7.10.7 ; extra == "dev"
|
||||
Requires-Dist: flake8==7.3.0 ; extra == "dev"
|
||||
Requires-Dist: flake8-bugbear==24.12.12 ; extra == "dev"
|
||||
Requires-Dist: flit==3.12.0 ; extra == "dev"
|
||||
Requires-Dist: mypy==1.19.0 ; extra == "dev"
|
||||
Requires-Dist: ufmt==2.8.0 ; extra == "dev"
|
||||
Requires-Dist: usort==1.0.8.post1 ; extra == "dev"
|
||||
Requires-Dist: sphinx==8.1.3 ; extra == "docs"
|
||||
Requires-Dist: sphinx-mdinclude==0.6.2 ; extra == "docs"
|
||||
Project-URL: Documentation, https://aiosqlite.omnilib.dev
|
||||
Project-URL: Github, https://github.com/omnilib/aiosqlite
|
||||
Provides-Extra: dev
|
||||
Provides-Extra: docs
|
||||
|
||||
aiosqlite\: Sqlite for AsyncIO
|
||||
==============================
|
||||
|
||||
.. image:: https://readthedocs.org/projects/aiosqlite/badge/?version=latest
|
||||
:target: https://aiosqlite.omnilib.dev/en/latest/?badge=latest
|
||||
:alt: Documentation Status
|
||||
.. image:: https://img.shields.io/pypi/v/aiosqlite.svg
|
||||
:target: https://pypi.org/project/aiosqlite
|
||||
:alt: PyPI Release
|
||||
.. image:: https://img.shields.io/badge/change-log-blue
|
||||
:target: https://github.com/omnilib/aiosqlite/blob/master/CHANGELOG.md
|
||||
:alt: Changelog
|
||||
.. image:: https://img.shields.io/pypi/l/aiosqlite.svg
|
||||
:target: https://github.com/omnilib/aiosqlite/blob/master/LICENSE
|
||||
:alt: MIT Licensed
|
||||
|
||||
aiosqlite provides a friendly, async interface to sqlite databases.
|
||||
|
||||
It replicates the standard ``sqlite3`` module, but with async versions
|
||||
of all the standard connection and cursor methods, plus context managers for
|
||||
automatically closing connections and cursors:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with aiosqlite.connect(...) as db:
|
||||
await db.execute("INSERT INTO some_table ...")
|
||||
await db.commit()
|
||||
|
||||
async with db.execute("SELECT * FROM some_table") as cursor:
|
||||
async for row in cursor:
|
||||
...
|
||||
|
||||
It can also be used in the traditional, procedural manner:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
db = await aiosqlite.connect(...)
|
||||
cursor = await db.execute('SELECT * FROM some_table')
|
||||
row = await cursor.fetchone()
|
||||
rows = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
await db.close()
|
||||
|
||||
aiosqlite also replicates most of the advanced features of ``sqlite3``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with aiosqlite.connect(...) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute('SELECT * FROM some_table') as cursor:
|
||||
async for row in cursor:
|
||||
value = row['column']
|
||||
|
||||
await db.execute('INSERT INTO foo some_table')
|
||||
assert db.total_changes > 0
|
||||
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
aiosqlite is compatible with Python 3.8 and newer.
|
||||
You can install it from PyPI:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install aiosqlite
|
||||
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
aiosqlite allows interaction with SQLite databases on the main AsyncIO event
|
||||
loop without blocking execution of other coroutines while waiting for queries
|
||||
or data fetches. It does this by using a single, shared thread per connection.
|
||||
This thread executes all actions within a shared request queue to prevent
|
||||
overlapping actions.
|
||||
|
||||
Connection objects are proxies to the real connections, contain the shared
|
||||
execution thread, and provide context managers to handle automatically closing
|
||||
connections. Cursors are similarly proxies to the real cursors, and provide
|
||||
async iterators to query results.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
aiosqlite is copyright `Amethyst Reese <https://noswap.com>`_, and licensed under the
|
||||
MIT license. I am providing code in this repository to you under an open source
|
||||
license. This is my personal repository; the license you receive to my code
|
||||
is from me and not from my employer. See the `LICENSE`_ file for details.
|
||||
|
||||
.. _LICENSE: https://github.com/omnilib/aiosqlite/blob/master/LICENSE
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
aiosqlite-0.22.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
aiosqlite-0.22.1.dist-info/METADATA,sha256=zzyMxzl2h_dGAlV6Pk9c4YBlkaYsgv6UybOW_YDRs5o,4311
|
||||
aiosqlite-0.22.1.dist-info/RECORD,,
|
||||
aiosqlite-0.22.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
aiosqlite-0.22.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
aiosqlite-0.22.1.dist-info/licenses/LICENSE,sha256=qwwXHcPvi_MlqEu3fYVUIfJhEzXd9uCIFrKSLE7cD3Y,1071
|
||||
aiosqlite/__init__.py,sha256=kjZKcYP2eZ3IbBEHQ0D_Owsk_-FlRGEjQWlbybOs8jk,888
|
||||
aiosqlite/__pycache__/__init__.cpython-311.pyc,,
|
||||
aiosqlite/__pycache__/__version__.cpython-311.pyc,,
|
||||
aiosqlite/__pycache__/context.cpython-311.pyc,,
|
||||
aiosqlite/__pycache__/core.cpython-311.pyc,,
|
||||
aiosqlite/__pycache__/cursor.cpython-311.pyc,,
|
||||
aiosqlite/__version__.py,sha256=sEM7xBU6e8WQYHe3ESoQmkEbXGKrqXjrQ5ujl5zpyV0,157
|
||||
aiosqlite/context.py,sha256=9jJcPG_SGSshzNUwXy87C1__mrKGFbToX0UuOQ1uItQ,1448
|
||||
aiosqlite/core.py,sha256=eXar7Bxr1pQz3VShCD0t6zGpp_bwwvFtVzVdzS2opII,15095
|
||||
aiosqlite/cursor.py,sha256=X3k2gYJeo3yB84scDEAPFZpsC_rzjT8dT4p6W3MeezM,3476
|
||||
aiosqlite/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
aiosqlite/tests/__init__.py,sha256=sp0-HYboM6gOYrUxWy8xna-hdJyMUtKBvAKrpRBcDCE,90
|
||||
aiosqlite/tests/__main__.py,sha256=eZRuAxr1bwF9xAAqVjCi4vd1WFsFO35uyhtuVO0GjmY,162
|
||||
aiosqlite/tests/__pycache__/__init__.cpython-311.pyc,,
|
||||
aiosqlite/tests/__pycache__/__main__.cpython-311.pyc,,
|
||||
aiosqlite/tests/__pycache__/helpers.cpython-311.pyc,,
|
||||
aiosqlite/tests/__pycache__/perf.cpython-311.pyc,,
|
||||
aiosqlite/tests/__pycache__/smoke.cpython-311.pyc,,
|
||||
aiosqlite/tests/helpers.py,sha256=MWC839FiX63TBmFiIjabXNx-4G5eWYnE5MiInKIAdJw,722
|
||||
aiosqlite/tests/perf.py,sha256=-ipnXSHidO6VBKEdLAOcGa3cKHU5ul1w8-ifDNtGbfA,7249
|
||||
aiosqlite/tests/smoke.py,sha256=k5mp4AOHheOO6wKL_bgdH1fenY6-ve6aew19ifmcIWA,19851
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: flit 3.12.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Amethyst Reese
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
"""asyncio bridge to the standard sqlite3 module"""
|
||||
|
||||
from sqlite3 import ( # pylint: disable=redefined-builtin
|
||||
DatabaseError,
|
||||
Error,
|
||||
IntegrityError,
|
||||
NotSupportedError,
|
||||
OperationalError,
|
||||
paramstyle,
|
||||
ProgrammingError,
|
||||
register_adapter,
|
||||
register_converter,
|
||||
Row,
|
||||
sqlite_version,
|
||||
sqlite_version_info,
|
||||
Warning,
|
||||
)
|
||||
|
||||
__author__ = "Amethyst Reese"
|
||||
from .__version__ import __version__
|
||||
from .core import connect, Connection, Cursor
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"paramstyle",
|
||||
"register_adapter",
|
||||
"register_converter",
|
||||
"sqlite_version",
|
||||
"sqlite_version_info",
|
||||
"connect",
|
||||
"Connection",
|
||||
"Cursor",
|
||||
"Row",
|
||||
"Warning",
|
||||
"Error",
|
||||
"DatabaseError",
|
||||
"IntegrityError",
|
||||
"ProgrammingError",
|
||||
"OperationalError",
|
||||
"NotSupportedError",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
This file is automatically generated by attribution.
|
||||
|
||||
Do not edit manually. Get more info at https://attribution.omnilib.dev
|
||||
"""
|
||||
|
||||
__version__ = "0.22.1"
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
|
||||
from collections.abc import Coroutine, Generator
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
||||
from .cursor import Cursor
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class Result(AbstractAsyncContextManager[_T], Coroutine[Any, Any, _T]):
|
||||
__slots__ = ("_coro", "_obj")
|
||||
|
||||
def __init__(self, coro: Coroutine[Any, Any, _T]):
|
||||
self._coro = coro
|
||||
self._obj: _T
|
||||
|
||||
def send(self, value) -> None:
|
||||
return self._coro.send(value)
|
||||
|
||||
def throw(self, typ, val=None, tb=None) -> None:
|
||||
if val is None:
|
||||
return self._coro.throw(typ)
|
||||
|
||||
if tb is None:
|
||||
return self._coro.throw(typ, val)
|
||||
|
||||
return self._coro.throw(typ, val, tb)
|
||||
|
||||
def close(self) -> None:
|
||||
return self._coro.close()
|
||||
|
||||
def __await__(self) -> Generator[Any, None, _T]:
|
||||
return self._coro.__await__()
|
||||
|
||||
async def __aenter__(self) -> _T:
|
||||
self._obj = await self._coro
|
||||
return self._obj
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||
if isinstance(self._obj, Cursor):
|
||||
await self._obj.close()
|
||||
|
||||
|
||||
def contextmanager(
|
||||
method: Callable[..., Coroutine[Any, Any, _T]],
|
||||
) -> Callable[..., Result[_T]]:
|
||||
@wraps(method)
|
||||
def wrapper(self, *args, **kwargs) -> Result[_T]:
|
||||
return Result(method(self, *args, **kwargs))
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1,468 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
"""
|
||||
Core implementation of aiosqlite proxies
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sqlite3
|
||||
from collections.abc import AsyncIterator, Generator, Iterable
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from queue import Empty, Queue, SimpleQueue
|
||||
from threading import Thread
|
||||
from typing import Any, Callable, Literal, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from .context import contextmanager
|
||||
from .cursor import Cursor
|
||||
|
||||
__all__ = ["connect", "Connection", "Cursor"]
|
||||
|
||||
AuthorizerCallback = Callable[[int, str, str, str, str], int]
|
||||
|
||||
LOG = logging.getLogger("aiosqlite")
|
||||
|
||||
|
||||
IsolationLevel = Optional[Literal["DEFERRED", "IMMEDIATE", "EXCLUSIVE"]]
|
||||
|
||||
|
||||
def set_result(fut: asyncio.Future, result: Any) -> None:
|
||||
"""Set the result of a future if it hasn't been set already."""
|
||||
if not fut.done():
|
||||
fut.set_result(result)
|
||||
|
||||
|
||||
def set_exception(fut: asyncio.Future, e: BaseException) -> None:
|
||||
"""Set the exception of a future if it hasn't been set already."""
|
||||
if not fut.done():
|
||||
fut.set_exception(e)
|
||||
|
||||
|
||||
_STOP_RUNNING_SENTINEL = object()
|
||||
_TxQueue = SimpleQueue[tuple[Optional[asyncio.Future], Callable[[], Any]]]
|
||||
|
||||
|
||||
def _connection_worker_thread(tx: _TxQueue):
|
||||
"""
|
||||
Execute function calls on a separate thread.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
while True:
|
||||
# Continues running until all queue items are processed,
|
||||
# even after connection is closed (so we can finalize all
|
||||
# futures)
|
||||
|
||||
future, function = tx.get()
|
||||
|
||||
try:
|
||||
LOG.debug("executing %s", function)
|
||||
result = function()
|
||||
|
||||
if future:
|
||||
future.get_loop().call_soon_threadsafe(set_result, future, result)
|
||||
LOG.debug("operation %s completed", function)
|
||||
|
||||
if result is _STOP_RUNNING_SENTINEL:
|
||||
break
|
||||
|
||||
except BaseException as e: # noqa B036
|
||||
LOG.debug("returning exception %s", e)
|
||||
if future:
|
||||
future.get_loop().call_soon_threadsafe(set_exception, future, e)
|
||||
|
||||
|
||||
class Connection:
|
||||
def __init__(
|
||||
self,
|
||||
connector: Callable[[], sqlite3.Connection],
|
||||
iter_chunk_size: int,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
) -> None:
|
||||
self._running = True
|
||||
self._connection: Optional[sqlite3.Connection] = None
|
||||
self._connector = connector
|
||||
self._tx: _TxQueue = SimpleQueue()
|
||||
self._iter_chunk_size = iter_chunk_size
|
||||
self._thread = Thread(target=_connection_worker_thread, args=(self._tx,))
|
||||
|
||||
if loop is not None:
|
||||
warn(
|
||||
"aiosqlite.Connection no longer uses the `loop` parameter",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
def __del__(self):
|
||||
if self._connection is None:
|
||||
return
|
||||
|
||||
warn(
|
||||
(
|
||||
f"{self!r} was deleted before being closed. "
|
||||
"Please use 'async with' or '.close()' to close the connection properly."
|
||||
),
|
||||
ResourceWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
# Don't try to be creative here, the event loop may have already been closed.
|
||||
# Simply stop the worker thread, and let the underlying sqlite3 connection
|
||||
# be finalized by its own __del__.
|
||||
self.stop()
|
||||
|
||||
def stop(self) -> Optional[asyncio.Future]:
|
||||
"""Stop the background thread. Prefer `async with` or `await close()`"""
|
||||
self._running = False
|
||||
|
||||
def close_and_stop():
|
||||
if self._connection is not None:
|
||||
self._connection.close()
|
||||
self._connection = None
|
||||
return _STOP_RUNNING_SENTINEL
|
||||
|
||||
try:
|
||||
future = asyncio.get_event_loop().create_future()
|
||||
except Exception:
|
||||
future = None
|
||||
|
||||
self._tx.put_nowait((future, close_and_stop))
|
||||
return future
|
||||
|
||||
@property
|
||||
def _conn(self) -> sqlite3.Connection:
|
||||
if self._connection is None:
|
||||
raise ValueError("no active connection")
|
||||
|
||||
return self._connection
|
||||
|
||||
def _execute_insert(self, sql: str, parameters: Any) -> Optional[sqlite3.Row]:
|
||||
cursor = self._conn.execute(sql, parameters)
|
||||
cursor.execute("SELECT last_insert_rowid()")
|
||||
return cursor.fetchone()
|
||||
|
||||
def _execute_fetchall(self, sql: str, parameters: Any) -> Iterable[sqlite3.Row]:
|
||||
cursor = self._conn.execute(sql, parameters)
|
||||
return cursor.fetchall()
|
||||
|
||||
async def _execute(self, fn, *args, **kwargs):
|
||||
"""Queue a function with the given arguments for execution."""
|
||||
if not self._running or not self._connection:
|
||||
raise ValueError("Connection closed")
|
||||
|
||||
function = partial(fn, *args, **kwargs)
|
||||
future = asyncio.get_event_loop().create_future()
|
||||
|
||||
self._tx.put_nowait((future, function))
|
||||
|
||||
return await future
|
||||
|
||||
async def _connect(self) -> "Connection":
|
||||
"""Connect to the actual sqlite database."""
|
||||
if self._connection is None:
|
||||
try:
|
||||
future = asyncio.get_event_loop().create_future()
|
||||
self._tx.put_nowait((future, self._connector))
|
||||
self._connection = await future
|
||||
except BaseException:
|
||||
self.stop()
|
||||
self._connection = None
|
||||
raise
|
||||
|
||||
return self
|
||||
|
||||
def __await__(self) -> Generator[Any, None, "Connection"]:
|
||||
self._thread.start()
|
||||
return self._connect().__await__()
|
||||
|
||||
async def __aenter__(self) -> "Connection":
|
||||
return await self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
await self.close()
|
||||
|
||||
@contextmanager
|
||||
async def cursor(self) -> Cursor:
|
||||
"""Create an aiosqlite cursor wrapping a sqlite3 cursor object."""
|
||||
return Cursor(self, await self._execute(self._conn.cursor))
|
||||
|
||||
async def commit(self) -> None:
|
||||
"""Commit the current transaction."""
|
||||
await self._execute(self._conn.commit)
|
||||
|
||||
async def rollback(self) -> None:
|
||||
"""Roll back the current transaction."""
|
||||
await self._execute(self._conn.rollback)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Complete queued queries/cursors and close the connection."""
|
||||
|
||||
if self._connection is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await self._execute(self._conn.close)
|
||||
except Exception:
|
||||
LOG.info("exception occurred while closing connection")
|
||||
raise
|
||||
finally:
|
||||
self._connection = None
|
||||
future = self.stop()
|
||||
if future:
|
||||
await future
|
||||
|
||||
@contextmanager
|
||||
async def execute(
|
||||
self, sql: str, parameters: Optional[Iterable[Any]] = None
|
||||
) -> Cursor:
|
||||
"""Helper to create a cursor and execute the given query."""
|
||||
if parameters is None:
|
||||
parameters = []
|
||||
cursor = await self._execute(self._conn.execute, sql, parameters)
|
||||
return Cursor(self, cursor)
|
||||
|
||||
@contextmanager
|
||||
async def execute_insert(
|
||||
self, sql: str, parameters: Optional[Iterable[Any]] = None
|
||||
) -> Optional[sqlite3.Row]:
|
||||
"""Helper to insert and get the last_insert_rowid."""
|
||||
if parameters is None:
|
||||
parameters = []
|
||||
return await self._execute(self._execute_insert, sql, parameters)
|
||||
|
||||
@contextmanager
|
||||
async def execute_fetchall(
|
||||
self, sql: str, parameters: Optional[Iterable[Any]] = None
|
||||
) -> Iterable[sqlite3.Row]:
|
||||
"""Helper to execute a query and return all the data."""
|
||||
if parameters is None:
|
||||
parameters = []
|
||||
return await self._execute(self._execute_fetchall, sql, parameters)
|
||||
|
||||
@contextmanager
|
||||
async def executemany(
|
||||
self, sql: str, parameters: Iterable[Iterable[Any]]
|
||||
) -> Cursor:
|
||||
"""Helper to create a cursor and execute the given multiquery."""
|
||||
cursor = await self._execute(self._conn.executemany, sql, parameters)
|
||||
return Cursor(self, cursor)
|
||||
|
||||
@contextmanager
|
||||
async def executescript(self, sql_script: str) -> Cursor:
|
||||
"""Helper to create a cursor and execute a user script."""
|
||||
cursor = await self._execute(self._conn.executescript, sql_script)
|
||||
return Cursor(self, cursor)
|
||||
|
||||
async def interrupt(self) -> None:
|
||||
"""Interrupt pending queries."""
|
||||
return self._conn.interrupt()
|
||||
|
||||
async def create_function(
|
||||
self, name: str, num_params: int, func: Callable, deterministic: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Create user-defined function that can be later used
|
||||
within SQL statements. Must be run within the same thread
|
||||
that query executions take place so instead of executing directly
|
||||
against the connection, we defer this to `run` function.
|
||||
|
||||
If ``deterministic`` is true, the created function is marked as deterministic,
|
||||
which allows SQLite to perform additional optimizations. This flag is supported
|
||||
by SQLite 3.8.3 or higher, ``NotSupportedError`` will be raised if used with
|
||||
older versions.
|
||||
"""
|
||||
await self._execute(
|
||||
self._conn.create_function,
|
||||
name,
|
||||
num_params,
|
||||
func,
|
||||
deterministic=deterministic,
|
||||
)
|
||||
|
||||
@property
|
||||
def in_transaction(self) -> bool:
|
||||
return self._conn.in_transaction
|
||||
|
||||
@property
|
||||
def isolation_level(self) -> Optional[str]:
|
||||
return self._conn.isolation_level
|
||||
|
||||
@isolation_level.setter
|
||||
def isolation_level(self, value: IsolationLevel) -> None:
|
||||
self._conn.isolation_level = value
|
||||
|
||||
@property
|
||||
def row_factory(self) -> Optional[type]:
|
||||
return self._conn.row_factory
|
||||
|
||||
@row_factory.setter
|
||||
def row_factory(self, factory: Optional[type]) -> None:
|
||||
self._conn.row_factory = factory
|
||||
|
||||
@property
|
||||
def text_factory(self) -> Callable[[bytes], Any]:
|
||||
return self._conn.text_factory
|
||||
|
||||
@text_factory.setter
|
||||
def text_factory(self, factory: Callable[[bytes], Any]) -> None:
|
||||
self._conn.text_factory = factory
|
||||
|
||||
@property
|
||||
def total_changes(self) -> int:
|
||||
return self._conn.total_changes
|
||||
|
||||
async def enable_load_extension(self, value: bool) -> None:
|
||||
await self._execute(self._conn.enable_load_extension, value) # type: ignore
|
||||
|
||||
async def load_extension(self, path: str):
|
||||
await self._execute(self._conn.load_extension, path) # type: ignore
|
||||
|
||||
async def set_progress_handler(
|
||||
self, handler: Callable[[], Optional[int]], n: int
|
||||
) -> None:
|
||||
await self._execute(self._conn.set_progress_handler, handler, n)
|
||||
|
||||
async def set_trace_callback(self, handler: Callable) -> None:
|
||||
await self._execute(self._conn.set_trace_callback, handler)
|
||||
|
||||
async def set_authorizer(
|
||||
self, authorizer_callback: Optional[AuthorizerCallback]
|
||||
) -> None:
|
||||
"""
|
||||
Set an authorizer callback to control database access.
|
||||
|
||||
The authorizer callback is invoked for each SQL statement that is prepared,
|
||||
and controls whether specific operations are permitted.
|
||||
|
||||
Example::
|
||||
|
||||
import sqlite3
|
||||
|
||||
def restrict_drops(action_code, arg1, arg2, db_name, trigger_name):
|
||||
# Deny all DROP operations
|
||||
if action_code == sqlite3.SQLITE_DROP_TABLE:
|
||||
return sqlite3.SQLITE_DENY
|
||||
# Allow everything else
|
||||
return sqlite3.SQLITE_OK
|
||||
|
||||
await conn.set_authorizer(restrict_drops)
|
||||
|
||||
See ``sqlite3`` documentation for details:
|
||||
https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.set_authorizer
|
||||
|
||||
:param authorizer_callback: An optional callable that receives five arguments:
|
||||
|
||||
- ``action_code`` (int): The action to be authorized (e.g., ``SQLITE_READ``)
|
||||
- ``arg1`` (str): First argument, meaning depends on ``action_code``
|
||||
- ``arg2`` (str): Second argument, meaning depends on ``action_code``
|
||||
- ``db_name`` (str): Database name (e.g., ``"main"``, ``"temp"``)
|
||||
- ``trigger_name`` (str): Name of trigger or view that is doing the access,
|
||||
or ``None``
|
||||
|
||||
The callback should return:
|
||||
|
||||
- ``SQLITE_OK`` (0): Allow the operation
|
||||
- ``SQLITE_DENY`` (1): Deny the operation, raise ``sqlite3.DatabaseError``
|
||||
- ``SQLITE_IGNORE`` (2): Treat operation as no-op
|
||||
|
||||
Pass ``None`` to remove the authorizer.
|
||||
"""
|
||||
await self._execute(self._conn.set_authorizer, authorizer_callback)
|
||||
|
||||
async def iterdump(self) -> AsyncIterator[str]:
|
||||
"""
|
||||
Return an async iterator to dump the database in SQL text format.
|
||||
|
||||
Example::
|
||||
|
||||
async for line in db.iterdump():
|
||||
...
|
||||
|
||||
"""
|
||||
dump_queue: Queue = Queue()
|
||||
|
||||
def dumper():
|
||||
try:
|
||||
for line in self._conn.iterdump():
|
||||
dump_queue.put_nowait(line)
|
||||
dump_queue.put_nowait(None)
|
||||
|
||||
except Exception:
|
||||
LOG.exception("exception while dumping db")
|
||||
dump_queue.put_nowait(None)
|
||||
raise
|
||||
|
||||
fut = self._execute(dumper)
|
||||
task = asyncio.ensure_future(fut)
|
||||
|
||||
while True:
|
||||
try:
|
||||
line: Optional[str] = dump_queue.get_nowait()
|
||||
if line is None:
|
||||
break
|
||||
yield line
|
||||
|
||||
except Empty:
|
||||
if task.done():
|
||||
LOG.warning("iterdump completed unexpectedly")
|
||||
break
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
await task
|
||||
|
||||
async def backup(
|
||||
self,
|
||||
target: Union["Connection", sqlite3.Connection],
|
||||
*,
|
||||
pages: int = 0,
|
||||
progress: Optional[Callable[[int, int, int], None]] = None,
|
||||
name: str = "main",
|
||||
sleep: float = 0.250,
|
||||
) -> None:
|
||||
"""
|
||||
Make a backup of the current database to the target database.
|
||||
|
||||
Takes either a standard sqlite3 or aiosqlite Connection object as the target.
|
||||
"""
|
||||
if isinstance(target, Connection):
|
||||
target = target._conn
|
||||
|
||||
await self._execute(
|
||||
self._conn.backup,
|
||||
target,
|
||||
pages=pages,
|
||||
progress=progress,
|
||||
name=name,
|
||||
sleep=sleep,
|
||||
)
|
||||
|
||||
|
||||
def connect(
|
||||
database: Union[str, Path],
|
||||
*,
|
||||
iter_chunk_size=64,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
**kwargs: Any,
|
||||
) -> Connection:
|
||||
"""Create and return a connection proxy to the sqlite database."""
|
||||
|
||||
if loop is not None:
|
||||
warn(
|
||||
"aiosqlite.connect() no longer uses the `loop` parameter",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
def connector() -> sqlite3.Connection:
|
||||
if isinstance(database, str):
|
||||
loc = database
|
||||
elif isinstance(database, bytes):
|
||||
loc = database.decode("utf-8")
|
||||
else:
|
||||
loc = str(database)
|
||||
|
||||
return sqlite3.connect(loc, **kwargs)
|
||||
|
||||
return Connection(connector, iter_chunk_size)
|
||||
@@ -0,0 +1,110 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
import sqlite3
|
||||
from collections.abc import AsyncIterator, Iterable
|
||||
from typing import Any, Callable, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core import Connection
|
||||
|
||||
|
||||
class Cursor:
|
||||
def __init__(self, conn: "Connection", cursor: sqlite3.Cursor) -> None:
|
||||
self.iter_chunk_size = conn._iter_chunk_size
|
||||
self._conn = conn
|
||||
self._cursor = cursor
|
||||
|
||||
def __aiter__(self) -> AsyncIterator[sqlite3.Row]:
|
||||
"""The cursor proxy is also an async iterator."""
|
||||
return self._fetch_chunked()
|
||||
|
||||
async def _fetch_chunked(self):
|
||||
while True:
|
||||
rows = await self.fetchmany(self.iter_chunk_size)
|
||||
if not rows:
|
||||
return
|
||||
for row in rows:
|
||||
yield row
|
||||
|
||||
async def _execute(self, fn, *args, **kwargs):
|
||||
"""Execute the given function on the shared connection's thread."""
|
||||
return await self._conn._execute(fn, *args, **kwargs)
|
||||
|
||||
async def execute(
|
||||
self, sql: str, parameters: Optional[Iterable[Any]] = None
|
||||
) -> "Cursor":
|
||||
"""Execute the given query."""
|
||||
if parameters is None:
|
||||
parameters = []
|
||||
await self._execute(self._cursor.execute, sql, parameters)
|
||||
return self
|
||||
|
||||
async def executemany(
|
||||
self, sql: str, parameters: Iterable[Iterable[Any]]
|
||||
) -> "Cursor":
|
||||
"""Execute the given multiquery."""
|
||||
await self._execute(self._cursor.executemany, sql, parameters)
|
||||
return self
|
||||
|
||||
async def executescript(self, sql_script: str) -> "Cursor":
|
||||
"""Execute a user script."""
|
||||
await self._execute(self._cursor.executescript, sql_script)
|
||||
return self
|
||||
|
||||
async def fetchone(self) -> Optional[sqlite3.Row]:
|
||||
"""Fetch a single row."""
|
||||
return await self._execute(self._cursor.fetchone)
|
||||
|
||||
async def fetchmany(self, size: Optional[int] = None) -> Iterable[sqlite3.Row]:
|
||||
"""Fetch up to `cursor.arraysize` number of rows."""
|
||||
args: tuple[int, ...] = ()
|
||||
if size is not None:
|
||||
args = (size,)
|
||||
return await self._execute(self._cursor.fetchmany, *args)
|
||||
|
||||
async def fetchall(self) -> Iterable[sqlite3.Row]:
|
||||
"""Fetch all remaining rows."""
|
||||
return await self._execute(self._cursor.fetchall)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the cursor."""
|
||||
await self._execute(self._cursor.close)
|
||||
|
||||
@property
|
||||
def rowcount(self) -> int:
|
||||
return self._cursor.rowcount
|
||||
|
||||
@property
|
||||
def lastrowid(self) -> Optional[int]:
|
||||
return self._cursor.lastrowid
|
||||
|
||||
@property
|
||||
def arraysize(self) -> int:
|
||||
return self._cursor.arraysize
|
||||
|
||||
@arraysize.setter
|
||||
def arraysize(self, value: int) -> None:
|
||||
self._cursor.arraysize = value
|
||||
|
||||
@property
|
||||
def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...]:
|
||||
return self._cursor.description
|
||||
|
||||
@property
|
||||
def row_factory(self) -> Optional[Callable[[sqlite3.Cursor, sqlite3.Row], object]]:
|
||||
return self._cursor.row_factory
|
||||
|
||||
@row_factory.setter
|
||||
def row_factory(self, factory: Optional[type]) -> None:
|
||||
self._cursor.row_factory = factory
|
||||
|
||||
@property
|
||||
def connection(self) -> sqlite3.Connection:
|
||||
return self._cursor.connection
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
from .smoke import SmokeTest
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
import unittest
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(module="aiosqlite.tests", verbosity=2)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+29
@@ -0,0 +1,29 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
def setup_logger():
|
||||
log = logging.getLogger("")
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
logging.addLevelName(logging.ERROR, "E")
|
||||
logging.addLevelName(logging.WARNING, "W")
|
||||
logging.addLevelName(logging.INFO, "I")
|
||||
logging.addLevelName(logging.DEBUG, "V")
|
||||
|
||||
date_fmt = r"%H:%M:%S"
|
||||
verbose_fmt = (
|
||||
"%(asctime)s,%(msecs)d %(levelname)s "
|
||||
"%(module)s:%(funcName)s():%(lineno)d "
|
||||
"%(message)s"
|
||||
)
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.setFormatter(logging.Formatter(verbose_fmt, date_fmt))
|
||||
log.addHandler(handler)
|
||||
|
||||
return log
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
"""
|
||||
Simple perf tests for aiosqlite and the asyncio run loop.
|
||||
"""
|
||||
import sqlite3
|
||||
import string
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from unittest import IsolatedAsyncioTestCase as TestCase
|
||||
|
||||
import aiosqlite
|
||||
from .smoke import setup_logger
|
||||
|
||||
TEST_DB = ":memory:"
|
||||
TARGET = 2.0
|
||||
RESULTS = {}
|
||||
|
||||
|
||||
def timed(fn, name=None):
|
||||
"""
|
||||
Decorator for perf testing a block of async code.
|
||||
|
||||
Expects the wrapped function to return an async generator.
|
||||
The generator should do setup, then yield when ready to start perf testing.
|
||||
The decorator will then pump the generator repeatedly until the target
|
||||
time has been reached, then close the generator and print perf results.
|
||||
"""
|
||||
|
||||
name = name or fn.__name__
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
gen = fn(*args, **kwargs)
|
||||
|
||||
await gen.asend(None)
|
||||
count = 0
|
||||
before = time.time()
|
||||
|
||||
while True:
|
||||
count += 1
|
||||
value = time.time() - before < TARGET
|
||||
try:
|
||||
if value:
|
||||
await gen.asend(value)
|
||||
else:
|
||||
await gen.aclose()
|
||||
break
|
||||
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"exception occurred: {e}")
|
||||
return
|
||||
|
||||
duration = time.time() - before
|
||||
|
||||
RESULTS[name] = (count, duration)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class PerfTest(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
print(f"Running perf tests for at least {TARGET:.1f}s each...")
|
||||
setup_logger()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
print(f"\n{'Perf Test':<25} Iterations Duration {'Rate':>11}")
|
||||
for name in sorted(RESULTS):
|
||||
count, duration = RESULTS[name]
|
||||
rate = count / duration
|
||||
name = name.replace("test_", "")
|
||||
print(f"{name:<25} {count:>10} {duration:>7.1f}s {rate:>9.1f}/s")
|
||||
|
||||
@timed
|
||||
async def test_connection_memory(self):
|
||||
while True:
|
||||
yield
|
||||
async with aiosqlite.connect(TEST_DB):
|
||||
pass
|
||||
|
||||
@timed
|
||||
async def test_connection_file(self):
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tf:
|
||||
path = tf.name
|
||||
tf.close()
|
||||
|
||||
async with aiosqlite.connect(path) as db:
|
||||
await db.execute(
|
||||
"create table perf (i integer primary key asc, k integer)"
|
||||
)
|
||||
await db.execute("insert into perf (k) values (2), (3)")
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
async with aiosqlite.connect(path):
|
||||
pass
|
||||
|
||||
@timed
|
||||
async def test_atomics(self):
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute("create table perf (i integer primary key asc, k integer)")
|
||||
await db.execute("insert into perf (k) values (2), (3)")
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
async with db.execute("select last_insert_rowid()") as cursor:
|
||||
await cursor.fetchone()
|
||||
|
||||
@timed
|
||||
async def test_inserts(self):
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute("create table perf (i integer primary key asc, k integer)")
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
await db.execute("insert into perf (k) values (1), (2), (3)")
|
||||
await db.commit()
|
||||
|
||||
@timed
|
||||
async def test_inserts_authorized(self):
|
||||
def deny_drops(action_code, arg1, arg2, db_name, trigger_name):
|
||||
if action_code == sqlite3.SQLITE_DROP_TABLE:
|
||||
return sqlite3.SQLITE_DENY
|
||||
return sqlite3.SQLITE_OK
|
||||
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute("create table perf (i integer primary key asc, k integer)")
|
||||
await db.set_authorizer(deny_drops)
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
await db.execute("insert into perf (k) values (1), (2), (3)")
|
||||
await db.commit()
|
||||
|
||||
@timed
|
||||
async def test_insert_ids(self):
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute("create table perf (i integer primary key asc, k integer)")
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
cursor = await db.execute("insert into perf (k) values (1)")
|
||||
await cursor.execute("select last_insert_rowid()")
|
||||
await cursor.fetchone()
|
||||
await db.commit()
|
||||
|
||||
@timed
|
||||
async def test_insert_macro_ids(self):
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute("create table perf (i integer primary key asc, k integer)")
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
await db.execute_insert("insert into perf (k) values (1)")
|
||||
await db.commit()
|
||||
|
||||
@timed
|
||||
async def test_select(self):
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute("create table perf (i integer primary key asc, k integer)")
|
||||
for i in range(100):
|
||||
await db.execute("insert into perf (k) values (%d)" % (i,))
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
cursor = await db.execute("select i, k from perf")
|
||||
assert len(await cursor.fetchall()) == 100
|
||||
|
||||
@timed
|
||||
async def test_select_macro(self):
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute("create table perf (i integer primary key asc, k integer)")
|
||||
for i in range(100):
|
||||
await db.execute("insert into perf (k) values (%d)" % (i,))
|
||||
await db.commit()
|
||||
|
||||
while True:
|
||||
yield
|
||||
assert len(await db.execute_fetchall("select i, k from perf")) == 100
|
||||
|
||||
async def test_iterable_cursor_perf(self):
|
||||
async with aiosqlite.connect(TEST_DB) as db:
|
||||
await db.execute(
|
||||
"create table ic_perf ("
|
||||
"i integer primary key asc, k integer, a integer, b integer, c char(16))"
|
||||
)
|
||||
for batch in range(128): # add 128k rows
|
||||
r_start = batch * 1024
|
||||
await db.executemany(
|
||||
"insert into ic_perf (k, a, b, c) values(?, 1, 2, ?)",
|
||||
[
|
||||
*[
|
||||
(i, string.ascii_lowercase)
|
||||
for i in range(r_start, r_start + 1024)
|
||||
]
|
||||
],
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async def test_perf(chunk_size: int):
|
||||
while True:
|
||||
async with db.execute("SELECT * FROM ic_perf") as cursor:
|
||||
cursor.iter_chunk_size = chunk_size
|
||||
async for _ in cursor:
|
||||
yield
|
||||
|
||||
for chunk_size in [2**i for i in range(4, 11)]:
|
||||
await timed(test_perf, f"iterable_cursor @ {chunk_size}")(chunk_size)
|
||||
@@ -0,0 +1,537 @@
|
||||
# Copyright Amethyst Reese
|
||||
# Licensed under the MIT license
|
||||
|
||||
import asyncio
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from sqlite3 import OperationalError
|
||||
from tempfile import TemporaryDirectory
|
||||
from threading import Thread
|
||||
from unittest import IsolatedAsyncioTestCase, SkipTest
|
||||
from unittest.mock import patch
|
||||
|
||||
import aiosqlite
|
||||
from .helpers import setup_logger
|
||||
|
||||
|
||||
class SmokeTest(IsolatedAsyncioTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
setup_logger()
|
||||
|
||||
def setUp(self):
|
||||
td = TemporaryDirectory()
|
||||
self.addCleanup(td.cleanup)
|
||||
self.db = Path(td.name).resolve() / "test.db"
|
||||
|
||||
async def test_connection_await(self):
|
||||
db = await aiosqlite.connect(self.db)
|
||||
self.assertIsInstance(db, aiosqlite.Connection)
|
||||
|
||||
async with db.execute("select 1, 2") as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
self.assertEqual(rows, [(1, 2)])
|
||||
|
||||
await db.close()
|
||||
|
||||
async def test_connection_context(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
self.assertIsInstance(db, aiosqlite.Connection)
|
||||
|
||||
async with db.execute("select 1, 2") as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
self.assertEqual(rows, [(1, 2)])
|
||||
|
||||
async def test_connection_locations(self):
|
||||
TEST_DB = self.db.as_posix()
|
||||
|
||||
class Fake: # pylint: disable=too-few-public-methods
|
||||
def __str__(self):
|
||||
return TEST_DB
|
||||
|
||||
locs = (Path(TEST_DB), TEST_DB, TEST_DB.encode(), Fake())
|
||||
|
||||
async with aiosqlite.connect(locs[0]) as db:
|
||||
await db.execute("create table foo (i integer, k integer)")
|
||||
await db.execute("insert into foo (i, k) values (1, 5)")
|
||||
await db.commit()
|
||||
|
||||
cursor = await db.execute("select * from foo")
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
for loc in locs:
|
||||
async with aiosqlite.connect(loc) as db:
|
||||
cursor = await db.execute("select * from foo")
|
||||
self.assertEqual(await cursor.fetchall(), rows)
|
||||
|
||||
async def test_multiple_connections(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.execute(
|
||||
"create table multiple_connections "
|
||||
"(i integer primary key asc, k integer)"
|
||||
)
|
||||
|
||||
async def do_one_conn(i):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.execute("insert into multiple_connections (k) values (?)", [i])
|
||||
await db.commit()
|
||||
|
||||
await asyncio.gather(*[do_one_conn(i) for i in range(10)])
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
cursor = await db.execute("select * from multiple_connections")
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
assert len(rows) == 10
|
||||
|
||||
async def test_multiple_queries(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.execute(
|
||||
"create table multiple_queries "
|
||||
"(i integer primary key asc, k integer)"
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
db.execute("insert into multiple_queries (k) values (?)", [i])
|
||||
for i in range(10)
|
||||
]
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
cursor = await db.execute("select * from multiple_queries")
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
assert len(rows) == 10
|
||||
|
||||
async def test_iterable_cursor(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
cursor = await db.cursor()
|
||||
await cursor.execute(
|
||||
"create table iterable_cursor " "(i integer primary key asc, k integer)"
|
||||
)
|
||||
await cursor.executemany(
|
||||
"insert into iterable_cursor (k) values (?)", [[i] for i in range(10)]
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
cursor = await db.execute("select * from iterable_cursor")
|
||||
rows = []
|
||||
async for row in cursor:
|
||||
rows.append(row)
|
||||
|
||||
assert len(rows) == 10
|
||||
|
||||
async def test_multi_loop_usage(self):
|
||||
results = {}
|
||||
|
||||
def runner(k, conn):
|
||||
async def query():
|
||||
async with conn.execute("select * from foo") as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
self.assertEqual(len(rows), 2)
|
||||
return rows
|
||||
|
||||
with self.subTest(k):
|
||||
loop = asyncio.new_event_loop()
|
||||
rows = loop.run_until_complete(query())
|
||||
loop.close()
|
||||
results[k] = rows
|
||||
|
||||
async with aiosqlite.connect(":memory:") as db:
|
||||
await db.execute("create table foo (id int, name varchar)")
|
||||
await db.execute(
|
||||
"insert into foo values (?, ?), (?, ?)", (1, "Sally", 2, "Janet")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
threads = [Thread(target=runner, args=(k, db)) for k in range(4)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
self.assertEqual(len(results), 4)
|
||||
for rows in results.values():
|
||||
self.assertEqual(len(rows), 2)
|
||||
|
||||
async def test_context_cursor(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
async with db.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"create table context_cursor "
|
||||
"(i integer primary key asc, k integer)"
|
||||
)
|
||||
await cursor.executemany(
|
||||
"insert into context_cursor (k) values (?)",
|
||||
[[i] for i in range(10)],
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
async with db.execute("select * from context_cursor") as cursor:
|
||||
rows = []
|
||||
async for row in cursor:
|
||||
rows.append(row)
|
||||
|
||||
assert len(rows) == 10
|
||||
|
||||
async def test_cursor_return_self(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
cursor = await db.cursor()
|
||||
|
||||
result = await cursor.execute(
|
||||
"create table test_cursor_return_self (i integer, k integer)"
|
||||
)
|
||||
self.assertEqual(result, cursor, "cursor execute returns itself")
|
||||
|
||||
result = await cursor.executemany(
|
||||
"insert into test_cursor_return_self values (?, ?)", [(1, 1), (2, 2)]
|
||||
)
|
||||
self.assertEqual(result, cursor)
|
||||
|
||||
result = await cursor.executescript(
|
||||
"insert into test_cursor_return_self values (3, 3);"
|
||||
"insert into test_cursor_return_self values (4, 4);"
|
||||
"insert into test_cursor_return_self values (5, 5);"
|
||||
)
|
||||
self.assertEqual(result, cursor)
|
||||
|
||||
async def test_connection_properties(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
self.assertEqual(db.total_changes, 0)
|
||||
|
||||
async with db.cursor() as cursor:
|
||||
self.assertFalse(db.in_transaction)
|
||||
await cursor.execute(
|
||||
"create table test_properties "
|
||||
"(i integer primary key asc, k integer, d text)"
|
||||
)
|
||||
await cursor.execute(
|
||||
"insert into test_properties (k, d) values (1, 'hi')"
|
||||
)
|
||||
self.assertTrue(db.in_transaction)
|
||||
await db.commit()
|
||||
self.assertFalse(db.in_transaction)
|
||||
|
||||
self.assertEqual(db.total_changes, 1)
|
||||
|
||||
self.assertIsNone(db.row_factory)
|
||||
self.assertEqual(db.text_factory, str)
|
||||
|
||||
async with db.cursor() as cursor:
|
||||
await cursor.execute("select * from test_properties")
|
||||
row = await cursor.fetchone()
|
||||
self.assertIsInstance(row, tuple)
|
||||
self.assertEqual(row, (1, 1, "hi"))
|
||||
with self.assertRaises(TypeError):
|
||||
_ = row["k"]
|
||||
|
||||
async with db.cursor() as cursor:
|
||||
cursor.row_factory = aiosqlite.Row
|
||||
self.assertEqual(cursor.row_factory, aiosqlite.Row)
|
||||
await cursor.execute("select * from test_properties")
|
||||
row = await cursor.fetchone()
|
||||
self.assertIsInstance(row, aiosqlite.Row)
|
||||
self.assertEqual(row[1], 1)
|
||||
self.assertEqual(row[2], "hi")
|
||||
self.assertEqual(row["k"], 1)
|
||||
self.assertEqual(row["d"], "hi")
|
||||
|
||||
db.row_factory = aiosqlite.Row
|
||||
db.text_factory = bytes
|
||||
self.assertEqual(db.row_factory, aiosqlite.Row)
|
||||
self.assertEqual(db.text_factory, bytes)
|
||||
|
||||
async with db.cursor() as cursor:
|
||||
await cursor.execute("select * from test_properties")
|
||||
row = await cursor.fetchone()
|
||||
self.assertIsInstance(row, aiosqlite.Row)
|
||||
self.assertEqual(row[1], 1)
|
||||
self.assertEqual(row[2], b"hi")
|
||||
self.assertEqual(row["k"], 1)
|
||||
self.assertEqual(row["d"], b"hi")
|
||||
|
||||
async def test_fetch_all(self):
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.execute(
|
||||
"create table test_fetch_all (i integer primary key asc, k integer)"
|
||||
)
|
||||
await db.execute(
|
||||
"insert into test_fetch_all (k) values (10), (24), (16), (32)"
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
cursor = await db.execute("select k from test_fetch_all where k < 30")
|
||||
rows = await cursor.fetchall()
|
||||
self.assertEqual(rows, [(10,), (24,), (16,)])
|
||||
|
||||
async def test_enable_load_extension(self):
|
||||
"""Assert that after enabling extension loading, they can be loaded"""
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
try:
|
||||
await db.enable_load_extension(True)
|
||||
await db.load_extension("test")
|
||||
except OperationalError as e:
|
||||
assert "not authorized" not in e.args
|
||||
except AttributeError as e:
|
||||
raise SkipTest(
|
||||
"python was not compiled with sqlite3 "
|
||||
"extension support, so we can't test it"
|
||||
) from e
|
||||
|
||||
async def test_set_progress_handler(self):
|
||||
"""
|
||||
Assert that after setting a progress handler returning 1, DB operations are aborted
|
||||
"""
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.set_progress_handler(lambda: 1, 1)
|
||||
with self.assertRaises(OperationalError):
|
||||
await db.execute(
|
||||
"create table test_progress_handler (i integer primary key asc, k integer)"
|
||||
)
|
||||
|
||||
async def test_create_function(self):
|
||||
"""Assert that after creating a custom function, it can be used"""
|
||||
|
||||
def no_arg():
|
||||
return "no arg"
|
||||
|
||||
def one_arg(num):
|
||||
return num * 2
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.create_function("no_arg", 0, no_arg)
|
||||
await db.create_function("one_arg", 1, one_arg)
|
||||
|
||||
async with db.execute("SELECT no_arg();") as res:
|
||||
row = await res.fetchone()
|
||||
self.assertEqual(row[0], "no arg")
|
||||
|
||||
async with db.execute("SELECT one_arg(10);") as res:
|
||||
row = await res.fetchone()
|
||||
self.assertEqual(row[0], 20)
|
||||
|
||||
async def test_create_function_deterministic(self):
|
||||
"""Assert that after creating a deterministic custom function, it can be used.
|
||||
|
||||
https://sqlite.org/deterministic.html
|
||||
"""
|
||||
|
||||
def one_arg(num):
|
||||
return num * 2
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.create_function("one_arg", 1, one_arg, deterministic=True)
|
||||
await db.execute("create table foo (id int, bar int)")
|
||||
|
||||
# Non-deterministic functions cannot be used in indexes
|
||||
await db.execute("create index t on foo(one_arg(bar))")
|
||||
|
||||
async def test_set_trace_callback(self):
|
||||
statements = []
|
||||
|
||||
def callback(statement: str):
|
||||
statements.append(statement)
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.set_trace_callback(callback)
|
||||
|
||||
await db.execute("select 10")
|
||||
self.assertIn("select 10", statements)
|
||||
|
||||
async def test_set_authorizer_deny_drops(self):
|
||||
"""Test authorizer that denies DROP operations"""
|
||||
|
||||
def deny_drops(action_code, arg1, arg2, db_name, trigger_name):
|
||||
if action_code == sqlite3.SQLITE_DROP_TABLE:
|
||||
return sqlite3.SQLITE_DENY
|
||||
return sqlite3.SQLITE_OK
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.set_authorizer(deny_drops)
|
||||
|
||||
# Other operations should succeed
|
||||
await db.execute("CREATE TABLE test_drop (id INTEGER)")
|
||||
await db.execute("INSERT INTO test_drop VALUES (1)")
|
||||
await db.execute("SELECT * FROM test_drop")
|
||||
|
||||
# DROP should fail
|
||||
with self.assertRaises(sqlite3.DatabaseError):
|
||||
await db.execute("DROP TABLE test_drop")
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
# Disabling the authorizer re-enables DROP
|
||||
await db.set_authorizer(None)
|
||||
await db.execute("DROP TABLE test_drop")
|
||||
|
||||
async def test_set_authorizer_exception_propagation(self):
|
||||
"""Test that exceptions raised in authorizer callback are caught by SQLite"""
|
||||
|
||||
def raise_exception(action_code, arg1, arg2, db_name, trigger_name):
|
||||
raise ValueError("Test exception from authorizer")
|
||||
|
||||
async with aiosqlite.connect(self.db) as db:
|
||||
await db.set_authorizer(raise_exception)
|
||||
with self.assertRaises(sqlite3.DatabaseError):
|
||||
await db.execute("CREATE TABLE test_exception (id INTEGER)")
|
||||
|
||||
async def test_connect_error(self):
|
||||
bad_db = Path("/something/that/shouldnt/exist.db")
|
||||
with self.assertRaisesRegex(OperationalError, "unable to open database"):
|
||||
async with aiosqlite.connect(bad_db) as db:
|
||||
self.assertIsNone(db) # should never be reached
|
||||
|
||||
with self.assertRaisesRegex(OperationalError, "unable to open database"):
|
||||
await aiosqlite.connect(bad_db)
|
||||
|
||||
async def test_connect_base_exception(self):
|
||||
# Check if connect task is cancelled, thread is properly closed.
|
||||
def _raise_cancelled_error(*_, **__):
|
||||
raise asyncio.CancelledError("I changed my mind")
|
||||
|
||||
connection = aiosqlite.Connection(lambda: sqlite3.connect(":memory:"), 64)
|
||||
with (
|
||||
patch.object(sqlite3, "connect", side_effect=_raise_cancelled_error),
|
||||
self.assertRaisesRegex(asyncio.CancelledError, "I changed my mind"),
|
||||
):
|
||||
async with connection:
|
||||
...
|
||||
# Terminate the thread here if the test fails to have a clear error.
|
||||
if connection._running:
|
||||
connection.stop()
|
||||
raise AssertionError("connection thread was not stopped")
|
||||
|
||||
async def test_iterdump(self):
|
||||
async with aiosqlite.connect(":memory:") as db:
|
||||
await db.execute("create table foo (i integer, k charvar(250))")
|
||||
await db.executemany(
|
||||
"insert into foo values (?, ?)", [(1, "hello"), (2, "world")]
|
||||
)
|
||||
|
||||
lines = [line async for line in db.iterdump()]
|
||||
self.assertEqual(
|
||||
lines,
|
||||
[
|
||||
"BEGIN TRANSACTION;",
|
||||
"CREATE TABLE foo (i integer, k charvar(250));",
|
||||
"INSERT INTO \"foo\" VALUES(1,'hello');",
|
||||
"INSERT INTO \"foo\" VALUES(2,'world');",
|
||||
"COMMIT;",
|
||||
],
|
||||
)
|
||||
|
||||
async def test_cursor_on_closed_connection(self):
|
||||
db = await aiosqlite.connect(self.db)
|
||||
|
||||
cursor = await db.execute("select 1, 2")
|
||||
await db.close()
|
||||
with self.assertRaisesRegex(ValueError, "Connection closed"):
|
||||
await cursor.fetchall()
|
||||
with self.assertRaisesRegex(ValueError, "Connection closed"):
|
||||
await cursor.fetchall()
|
||||
|
||||
async def test_cursor_on_closed_connection_loop(self):
|
||||
db = await aiosqlite.connect(self.db)
|
||||
|
||||
cursor = await db.execute("select 1, 2")
|
||||
tasks = []
|
||||
for i in range(100):
|
||||
if i == 50:
|
||||
tasks.append(asyncio.ensure_future(db.close()))
|
||||
tasks.append(asyncio.ensure_future(cursor.fetchall()))
|
||||
for task in tasks:
|
||||
try:
|
||||
await task
|
||||
except sqlite3.ProgrammingError:
|
||||
pass
|
||||
|
||||
async def test_close_blocking_until_transaction_queue_empty(self):
|
||||
db = await aiosqlite.connect(self.db)
|
||||
# Insert transactions into the
|
||||
# transaction queue '_tx'
|
||||
for i in range(1000):
|
||||
await db.execute(f"select 1, {i}")
|
||||
# Wait for all transactions to complete
|
||||
await db.close()
|
||||
# Check no more transaction pending
|
||||
self.assertEqual(db._tx.empty(), True)
|
||||
|
||||
async def test_close_twice(self):
|
||||
db = await aiosqlite.connect(self.db)
|
||||
|
||||
await db.close()
|
||||
|
||||
# no error
|
||||
await db.close()
|
||||
|
||||
async def test_backup_aiosqlite(self):
|
||||
def progress(a, b, c):
|
||||
print(a, b, c)
|
||||
|
||||
async with (
|
||||
aiosqlite.connect(":memory:") as db1,
|
||||
aiosqlite.connect(":memory:") as db2,
|
||||
):
|
||||
await db1.execute("create table foo (i integer, k charvar(250))")
|
||||
await db1.executemany(
|
||||
"insert into foo values (?, ?)", [(1, "hello"), (2, "world")]
|
||||
)
|
||||
await db1.commit()
|
||||
|
||||
with self.assertRaisesRegex(OperationalError, "no such table: foo"):
|
||||
await db2.execute("select * from foo")
|
||||
|
||||
await db1.backup(db2, progress=progress)
|
||||
|
||||
async with db2.execute("select * from foo") as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
self.assertEqual(rows, [(1, "hello"), (2, "world")])
|
||||
|
||||
async def test_backup_sqlite(self):
|
||||
async with aiosqlite.connect(":memory:") as db1:
|
||||
with sqlite3.connect(":memory:") as db2:
|
||||
await db1.execute("create table foo (i integer, k charvar(250))")
|
||||
await db1.executemany(
|
||||
"insert into foo values (?, ?)", [(1, "hello"), (2, "world")]
|
||||
)
|
||||
await db1.commit()
|
||||
|
||||
with self.assertRaisesRegex(OperationalError, "no such table: foo"):
|
||||
db2.execute("select * from foo")
|
||||
|
||||
await db1.backup(db2)
|
||||
|
||||
cursor = db2.execute("select * from foo")
|
||||
rows = cursor.fetchall()
|
||||
self.assertEqual(rows, [(1, "hello"), (2, "world")])
|
||||
|
||||
async def test_emits_warning_when_left_open(self):
|
||||
db = await aiosqlite.connect(":memory:")
|
||||
|
||||
with self.assertWarnsRegex(
|
||||
ResourceWarning, r".*was deleted before being closed.*"
|
||||
):
|
||||
del db
|
||||
|
||||
async def test_stop_without_close(self):
|
||||
db = await aiosqlite.connect(":memory:")
|
||||
await db.stop()
|
||||
|
||||
def test_stop_after_event_loop_closed(self):
|
||||
db = None
|
||||
|
||||
async def inner():
|
||||
nonlocal db
|
||||
db = await aiosqlite.connect(":memory:")
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(inner())
|
||||
loop.close()
|
||||
|
||||
db.stop()
|
||||
+1
@@ -0,0 +1 @@
|
||||
pip
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: annotated-doc
|
||||
Version: 0.0.4
|
||||
Summary: Document parameters, class attributes, return types, and variables inline, with Annotated.
|
||||
Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <tiangolo@gmail.com>
|
||||
License-Expression: MIT
|
||||
License-File: LICENSE
|
||||
Classifier: Intended Audience :: Information Technology
|
||||
Classifier: Intended Audience :: System Administrators
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet
|
||||
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Topic :: Software Development
|
||||
Classifier: Typing :: Typed
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Project-URL: Homepage, https://github.com/fastapi/annotated-doc
|
||||
Project-URL: Documentation, https://github.com/fastapi/annotated-doc
|
||||
Project-URL: Repository, https://github.com/fastapi/annotated-doc
|
||||
Project-URL: Issues, https://github.com/fastapi/annotated-doc/issues
|
||||
Project-URL: Changelog, https://github.com/fastapi/annotated-doc/release-notes.md
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
# Annotated Doc
|
||||
|
||||
Document parameters, class attributes, return types, and variables inline, with `Annotated`.
|
||||
|
||||
<a href="https://github.com/fastapi/annotated-doc/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
|
||||
<img src="https://github.com/fastapi/annotated-doc/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
|
||||
</a>
|
||||
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/annotated-doc" target="_blank">
|
||||
<img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/annotated-doc.svg" alt="Coverage">
|
||||
</a>
|
||||
<a href="https://pypi.org/project/annotated-doc" target="_blank">
|
||||
<img src="https://img.shields.io/pypi/v/annotated-doc?color=%2334D058&label=pypi%20package" alt="Package version">
|
||||
</a>
|
||||
<a href="https://pypi.org/project/annotated-doc" target="_blank">
|
||||
<img src="https://img.shields.io/pypi/pyversions/annotated-doc.svg?color=%2334D058" alt="Supported Python versions">
|
||||
</a>
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install annotated-doc
|
||||
```
|
||||
|
||||
Or with `uv`:
|
||||
|
||||
```Python
|
||||
uv add annotated-doc
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Import `Doc` and pass a single literal string with the documentation for the specific parameter, class attribute, return type, or variable.
|
||||
|
||||
For example, to document a parameter `name` in a function `hi` you could do:
|
||||
|
||||
```Python
|
||||
from typing import Annotated
|
||||
|
||||
from annotated_doc import Doc
|
||||
|
||||
def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
|
||||
print(f"Hi, {name}!")
|
||||
```
|
||||
|
||||
You can also use it to document class attributes:
|
||||
|
||||
```Python
|
||||
from typing import Annotated
|
||||
|
||||
from annotated_doc import Doc
|
||||
|
||||
class User:
|
||||
name: Annotated[str, Doc("The user's name")]
|
||||
age: Annotated[int, Doc("The user's age")]
|
||||
```
|
||||
|
||||
The same way, you could document return types and variables, or anything that could have a type annotation with `Annotated`.
|
||||
|
||||
## Who Uses This
|
||||
|
||||
`annotated-doc` was made for:
|
||||
|
||||
* [FastAPI](https://fastapi.tiangolo.com/)
|
||||
* [Typer](https://typer.tiangolo.com/)
|
||||
* [SQLModel](https://sqlmodel.tiangolo.com/)
|
||||
* [Asyncer](https://asyncer.tiangolo.com/)
|
||||
|
||||
`annotated-doc` is supported by [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc), which powers reference documentation like the one in the [FastAPI Reference](https://fastapi.tiangolo.com/reference/).
|
||||
|
||||
## Reasons not to use `annotated-doc`
|
||||
|
||||
You are already comfortable with one of the existing docstring formats, like:
|
||||
|
||||
* Sphinx
|
||||
* numpydoc
|
||||
* Google
|
||||
* Keras
|
||||
|
||||
Your team is already comfortable using them.
|
||||
|
||||
You prefer having the documentation about parameters all together in a docstring, separated from the code defining them.
|
||||
|
||||
You care about a specific set of users, using one specific editor, and that editor already has support for the specific docstring format you use.
|
||||
|
||||
## Reasons to use `annotated-doc`
|
||||
|
||||
* No micro-syntax to learn for newcomers, it’s **just Python** syntax.
|
||||
* **Editing** would be already fully supported by default by any editor (current or future) supporting Python syntax, including syntax errors, syntax highlighting, etc.
|
||||
* **Rendering** would be relatively straightforward to implement by static tools (tools that don't need runtime execution), as the information can be extracted from the AST they normally already create.
|
||||
* **Deduplication of information**: the name of a parameter would be defined in a single place, not duplicated inside of a docstring.
|
||||
* **Elimination** of the possibility of having **inconsistencies** when removing a parameter or class variable and **forgetting to remove** its documentation.
|
||||
* **Minimization** of the probability of adding a new parameter or class variable and **forgetting to add its documentation**.
|
||||
* **Elimination** of the possibility of having **inconsistencies** between the **name** of a parameter in the **signature** and the name in the docstring when it is renamed.
|
||||
* **Access** to the documentation string for each symbol at **runtime**, including existing (older) Python versions.
|
||||
* A more formalized way to document other symbols, like type aliases, that could use Annotated.
|
||||
* **Support** for apps using FastAPI, Typer and others.
|
||||
* **AI Accessibility**: AI tools will have an easier way understanding each parameter as the distance from documentation to parameter is much closer.
|
||||
|
||||
## History
|
||||
|
||||
I ([@tiangolo](https://github.com/tiangolo)) originally wanted for this to be part of the Python standard library (in [PEP 727](https://peps.python.org/pep-0727/)), but the proposal was withdrawn as there was a fair amount of negative feedback and opposition.
|
||||
|
||||
The conclusion was that this was better done as an external effort, in a third-party library.
|
||||
|
||||
So, here it is, with a simpler approach, as a third-party library, in a way that can be used by others, starting with FastAPI and friends.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of the MIT license.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
annotated_doc-0.0.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
annotated_doc-0.0.4.dist-info/METADATA,sha256=Irm5KJua33dY2qKKAjJ-OhKaVBVIfwFGej_dSe3Z1TU,6566
|
||||
annotated_doc-0.0.4.dist-info/RECORD,,
|
||||
annotated_doc-0.0.4.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
||||
annotated_doc-0.0.4.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
||||
annotated_doc-0.0.4.dist-info/licenses/LICENSE,sha256=__Fwd5pqy_ZavbQFwIfxzuF4ZpHkqWpANFF-SlBKDN8,1086
|
||||
annotated_doc/__init__.py,sha256=VuyxxUe80kfEyWnOrCx_Bk8hybo3aKo6RYBlkBBYW8k,52
|
||||
annotated_doc/__pycache__/__init__.cpython-311.pyc,,
|
||||
annotated_doc/__pycache__/main.cpython-311.pyc,,
|
||||
annotated_doc/main.py,sha256=5Zfvxv80SwwLqpRW73AZyZyiM4bWma9QWRbp_cgD20s,1075
|
||||
annotated_doc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: pdm-backend (2.4.5)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[console_scripts]
|
||||
|
||||
[gui_scripts]
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 Sebastián Ramírez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,3 @@
|
||||
from .main import Doc as Doc
|
||||
|
||||
__version__ = "0.0.4"
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
class Doc:
|
||||
"""Define the documentation of a type annotation using `Annotated`, to be
|
||||
used in class attributes, function and method parameters, return values,
|
||||
and variables.
|
||||
|
||||
The value should be a positional-only string literal to allow static tools
|
||||
like editors and documentation generators to use it.
|
||||
|
||||
This complements docstrings.
|
||||
|
||||
The string value passed is available in the attribute `documentation`.
|
||||
|
||||
Example:
|
||||
|
||||
```Python
|
||||
from typing import Annotated
|
||||
from annotated_doc import Doc
|
||||
|
||||
def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
|
||||
print(f"Hi, {name}!")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, documentation: str, /) -> None:
|
||||
self.documentation = documentation
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Doc({self.documentation!r})"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.documentation)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Doc):
|
||||
return NotImplemented
|
||||
return self.documentation == other.documentation
|
||||
+1
@@ -0,0 +1 @@
|
||||
pip
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
Metadata-Version: 2.3
|
||||
Name: annotated-types
|
||||
Version: 0.7.0
|
||||
Summary: Reusable constraint types to use with typing.Annotated
|
||||
Project-URL: Homepage, https://github.com/annotated-types/annotated-types
|
||||
Project-URL: Source, https://github.com/annotated-types/annotated-types
|
||||
Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases
|
||||
Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin <s@muelcolvin.com>, Zac Hatfield-Dodds <zac@zhd.dev>
|
||||
License-File: LICENSE
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Environment :: MacOS X
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: Information Technology
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: Unix
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.8
|
||||
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9'
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
# annotated-types
|
||||
|
||||
[](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
|
||||
[](https://pypi.python.org/pypi/annotated-types)
|
||||
[](https://github.com/annotated-types/annotated-types)
|
||||
[](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
|
||||
|
||||
[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
|
||||
adding context-specific metadata to existing types, and specifies that
|
||||
`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
|
||||
logic for `x`.
|
||||
|
||||
This package provides metadata objects which can be used to represent common
|
||||
constraints such as upper and lower bounds on scalar values and collection sizes,
|
||||
a `Predicate` marker for runtime checks, and
|
||||
descriptions of how we intend these metadata to be interpreted. In some cases,
|
||||
we also note alternative representations which do not require this package.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install annotated-types
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from annotated_types import Gt, Len, Predicate
|
||||
|
||||
class MyClass:
|
||||
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
|
||||
# Invalid: 17, 18, "19", 19.0, ...
|
||||
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
|
||||
# Invalid: 4, 8, -2, 5.0, "prime", ...
|
||||
|
||||
my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
|
||||
# Invalid: (1, 2), ["abc"], [0] * 20
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
_While `annotated-types` avoids runtime checks for performance, users should not
|
||||
construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
|
||||
Downstream implementors may choose to raise an error, emit a warning, silently ignore
|
||||
a metadata item, etc., if the metadata objects described below are used with an
|
||||
incompatible type - or for any other reason!_
|
||||
|
||||
### Gt, Ge, Lt, Le
|
||||
|
||||
Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
|
||||
dates, times, strings, sets, etc. Note that the boundary value need not be of the
|
||||
same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
|
||||
is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
|
||||
|
||||
We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
|
||||
as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
|
||||
the `annotated-types` package.
|
||||
|
||||
To be explicit, these types have the following meanings:
|
||||
|
||||
* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
|
||||
* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
|
||||
* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
|
||||
* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
|
||||
|
||||
### Interval
|
||||
|
||||
`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
|
||||
metadata object. `None` attributes should be ignored, and non-`None` attributes
|
||||
treated as per the single bounds above.
|
||||
|
||||
### MultipleOf
|
||||
|
||||
`MultipleOf(multiple_of=x)` might be interpreted in two ways:
|
||||
|
||||
1. Python semantics, implying `value % multiple_of == 0`, or
|
||||
2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
|
||||
where `int(value / multiple_of) == value / multiple_of`.
|
||||
|
||||
We encourage users to be aware of these two common interpretations and their
|
||||
distinct behaviours, especially since very large or non-integer numbers make
|
||||
it easy to cause silent data corruption due to floating-point imprecision.
|
||||
|
||||
We encourage libraries to carefully document which interpretation they implement.
|
||||
|
||||
### MinLen, MaxLen, Len
|
||||
|
||||
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
|
||||
|
||||
As well as `Len()` which can optionally include upper and lower bounds, we also
|
||||
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
|
||||
and `Len(max_length=y)` respectively.
|
||||
|
||||
`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
|
||||
|
||||
Examples of usage:
|
||||
|
||||
* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less
|
||||
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
|
||||
* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more
|
||||
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
|
||||
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
|
||||
|
||||
#### Changed in v0.4.0
|
||||
|
||||
* `min_inclusive` has been renamed to `min_length`, no change in meaning
|
||||
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
|
||||
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
|
||||
meaning of the upper bound in slices vs. `Len`
|
||||
|
||||
See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
|
||||
|
||||
### Timezone
|
||||
|
||||
`Timezone` can be used with a `datetime` or a `time` to express which timezones
|
||||
are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
|
||||
`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
|
||||
expresses that any timezone-aware datetime is allowed. You may also pass a specific
|
||||
timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects)
|
||||
object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only
|
||||
allow a specific timezone, though we note that this is often a symptom of fragile design.
|
||||
|
||||
#### Changed in v0.x.x
|
||||
|
||||
* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of
|
||||
`timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries.
|
||||
|
||||
### Unit
|
||||
|
||||
`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of
|
||||
a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]`
|
||||
would be a float representing a velocity in meters per second.
|
||||
|
||||
Please note that `annotated_types` itself makes no attempt to parse or validate
|
||||
the unit string in any way. That is left entirely to downstream libraries,
|
||||
such as [`pint`](https://pint.readthedocs.io) or
|
||||
[`astropy.units`](https://docs.astropy.org/en/stable/units/).
|
||||
|
||||
An example of how a library might use this metadata:
|
||||
|
||||
```python
|
||||
from annotated_types import Unit
|
||||
from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args
|
||||
|
||||
# given a type annotated with a unit:
|
||||
Meters = Annotated[float, Unit("m")]
|
||||
|
||||
|
||||
# you can cast the annotation to a specific unit type with any
|
||||
# callable that accepts a string and returns the desired type
|
||||
T = TypeVar("T")
|
||||
def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
|
||||
if get_origin(tp) is Annotated:
|
||||
for arg in get_args(tp):
|
||||
if isinstance(arg, Unit):
|
||||
return unit_cls(arg.unit)
|
||||
return None
|
||||
|
||||
|
||||
# using `pint`
|
||||
import pint
|
||||
pint_unit = cast_unit(Meters, pint.Unit)
|
||||
|
||||
|
||||
# using `astropy.units`
|
||||
import astropy.units as u
|
||||
astropy_unit = cast_unit(Meters, u.Unit)
|
||||
```
|
||||
|
||||
### Predicate
|
||||
|
||||
`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
|
||||
Users should prefer the statically inspectable metadata above, but if you need
|
||||
the full power and flexibility of arbitrary runtime predicates... here it is.
|
||||
|
||||
For some common constraints, we provide generic types:
|
||||
|
||||
* `IsLower = Annotated[T, Predicate(str.islower)]`
|
||||
* `IsUpper = Annotated[T, Predicate(str.isupper)]`
|
||||
* `IsDigit = Annotated[T, Predicate(str.isdigit)]`
|
||||
* `IsFinite = Annotated[T, Predicate(math.isfinite)]`
|
||||
* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]`
|
||||
* `IsNan = Annotated[T, Predicate(math.isnan)]`
|
||||
* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]`
|
||||
* `IsInfinite = Annotated[T, Predicate(math.isinf)]`
|
||||
* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]`
|
||||
|
||||
so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer
|
||||
(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`.
|
||||
|
||||
Some libraries might have special logic to handle known or understandable predicates,
|
||||
for example by checking for `str.isdigit` and using its presence to both call custom
|
||||
logic to enforce digit-only strings, and customise some generated external schema.
|
||||
Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
|
||||
favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
|
||||
|
||||
To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
|
||||
|
||||
We do not specify what behaviour should be expected for predicates that raise
|
||||
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
|
||||
skip invalid constraints, or statically raise an error; or it might try calling it
|
||||
and then propagate or discard the resulting
|
||||
`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
|
||||
exception. We encourage libraries to document the behaviour they choose.
|
||||
|
||||
### Doc
|
||||
|
||||
`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
|
||||
|
||||
It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
|
||||
|
||||
It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
|
||||
|
||||
This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
|
||||
|
||||
### Integrating downstream types with `GroupedMetadata`
|
||||
|
||||
Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
|
||||
This can help reduce verbosity and cognitive overhead for users.
|
||||
For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator
|
||||
from annotated_types import GroupedMetadata, Ge
|
||||
|
||||
@dataclass
|
||||
class Field(GroupedMetadata):
|
||||
ge: int | None = None
|
||||
description: str | None = None
|
||||
|
||||
def __iter__(self) -> Iterator[object]:
|
||||
# Iterating over a GroupedMetadata object should yield annotated-types
|
||||
# constraint metadata objects which describe it as fully as possible,
|
||||
# and may include other unknown objects too.
|
||||
if self.ge is not None:
|
||||
yield Ge(self.ge)
|
||||
if self.description is not None:
|
||||
yield Description(self.description)
|
||||
```
|
||||
|
||||
Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
|
||||
|
||||
Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
|
||||
|
||||
Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
|
||||
|
||||
### Consuming metadata
|
||||
|
||||
We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
|
||||
|
||||
It is up to the implementer to determine how this metadata is used.
|
||||
You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
|
||||
|
||||
## Design & History
|
||||
|
||||
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
|
||||
and Hypothesis, with the goal of making it as easy as possible for end-users to
|
||||
provide more informative annotations for use by runtime libraries.
|
||||
|
||||
It is deliberately minimal, and following PEP-593 allows considerable downstream
|
||||
discretion in what (if anything!) they choose to support. Nonetheless, we expect
|
||||
that staying simple and covering _only_ the most common use-cases will give users
|
||||
and maintainers the best experience we can. If you'd like more constraints for your
|
||||
types - follow our lead, by defining them and documenting them downstream!
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
annotated_types-0.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
annotated_types-0.7.0.dist-info/METADATA,sha256=7ltqxksJJ0wCYFGBNIQCWTlWQGeAH0hRFdnK3CB895E,15046
|
||||
annotated_types-0.7.0.dist-info/RECORD,,
|
||||
annotated_types-0.7.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
||||
annotated_types-0.7.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
|
||||
annotated_types/__init__.py,sha256=RynLsRKUEGI0KimXydlD1fZEfEzWwDo0Uon3zOKhG1Q,13819
|
||||
annotated_types/__pycache__/__init__.cpython-311.pyc,,
|
||||
annotated_types/__pycache__/test_cases.cpython-311.pyc,,
|
||||
annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
annotated_types/test_cases.py,sha256=zHFX6EpcMbGJ8FzBYDbO56bPwx_DYIVSKbZM-4B3_lg,6421
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.24.2
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 the contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+432
@@ -0,0 +1,432 @@
|
||||
import math
|
||||
import sys
|
||||
import types
|
||||
from dataclasses import dataclass
|
||||
from datetime import tzinfo
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
else:
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated, Literal
|
||||
else:
|
||||
from typing import Annotated, Literal
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
EllipsisType = type(Ellipsis)
|
||||
KW_ONLY = {}
|
||||
SLOTS = {}
|
||||
else:
|
||||
from types import EllipsisType
|
||||
|
||||
KW_ONLY = {"kw_only": True}
|
||||
SLOTS = {"slots": True}
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseMetadata',
|
||||
'GroupedMetadata',
|
||||
'Gt',
|
||||
'Ge',
|
||||
'Lt',
|
||||
'Le',
|
||||
'Interval',
|
||||
'MultipleOf',
|
||||
'MinLen',
|
||||
'MaxLen',
|
||||
'Len',
|
||||
'Timezone',
|
||||
'Predicate',
|
||||
'LowerCase',
|
||||
'UpperCase',
|
||||
'IsDigits',
|
||||
'IsFinite',
|
||||
'IsNotFinite',
|
||||
'IsNan',
|
||||
'IsNotNan',
|
||||
'IsInfinite',
|
||||
'IsNotInfinite',
|
||||
'doc',
|
||||
'DocInfo',
|
||||
'__version__',
|
||||
)
|
||||
|
||||
__version__ = '0.7.0'
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# arguments that start with __ are considered
|
||||
# positional only
|
||||
# see https://peps.python.org/pep-0484/#positional-only-arguments
|
||||
|
||||
|
||||
class SupportsGt(Protocol):
|
||||
def __gt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsGe(Protocol):
|
||||
def __ge__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLt(Protocol):
|
||||
def __lt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLe(Protocol):
|
||||
def __le__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsMod(Protocol):
|
||||
def __mod__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class SupportsDiv(Protocol):
|
||||
def __div__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class BaseMetadata:
|
||||
"""Base class for all metadata.
|
||||
|
||||
This exists mainly so that implementers
|
||||
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Gt(BaseMetadata):
|
||||
"""Gt(gt=x) implies that the value must be greater than x.
|
||||
|
||||
It can be used with any type that supports the ``>`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
gt: SupportsGt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Ge(BaseMetadata):
|
||||
"""Ge(ge=x) implies that the value must be greater than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``>=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
ge: SupportsGe
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Lt(BaseMetadata):
|
||||
"""Lt(lt=x) implies that the value must be less than x.
|
||||
|
||||
It can be used with any type that supports the ``<`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
lt: SupportsLt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Le(BaseMetadata):
|
||||
"""Le(le=x) implies that the value must be less than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``<=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
le: SupportsLe
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class GroupedMetadata(Protocol):
|
||||
"""A grouping of multiple objects, like typing.Unpack.
|
||||
|
||||
`GroupedMetadata` on its own is not metadata and has no meaning.
|
||||
All of the constraints and metadata should be fully expressable
|
||||
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
|
||||
|
||||
Concrete implementations should override `GroupedMetadata.__iter__()`
|
||||
to add their own metadata.
|
||||
For example:
|
||||
|
||||
>>> @dataclass
|
||||
>>> class Field(GroupedMetadata):
|
||||
>>> gt: float | None = None
|
||||
>>> description: str | None = None
|
||||
...
|
||||
>>> def __iter__(self) -> Iterable[object]:
|
||||
>>> if self.gt is not None:
|
||||
>>> yield Gt(self.gt)
|
||||
>>> if self.description is not None:
|
||||
>>> yield Description(self.gt)
|
||||
|
||||
Also see the implementation of `Interval` below for an example.
|
||||
|
||||
Parsers should recognize this and unpack it so that it can be used
|
||||
both with and without unpacking:
|
||||
|
||||
- `Annotated[int, Field(...)]` (parser must unpack Field)
|
||||
- `Annotated[int, *Field(...)]` (PEP-646)
|
||||
""" # noqa: trailing-whitespace
|
||||
|
||||
@property
|
||||
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def __iter__(self) -> Iterator[object]:
|
||||
...
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
__slots__ = () # allow subclasses to use slots
|
||||
|
||||
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
|
||||
# Basic ABC like functionality without the complexity of an ABC
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
if cls.__iter__ is GroupedMetadata.__iter__:
|
||||
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
|
||||
|
||||
def __iter__(self) -> Iterator[object]: # noqa: F811
|
||||
raise NotImplementedError # more helpful than "None has no attribute..." type errors
|
||||
|
||||
|
||||
@dataclass(frozen=True, **KW_ONLY, **SLOTS)
|
||||
class Interval(GroupedMetadata):
|
||||
"""Interval can express inclusive or exclusive bounds with a single object.
|
||||
|
||||
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
|
||||
are interpreted the same way as the single-bound constraints.
|
||||
"""
|
||||
|
||||
gt: Union[SupportsGt, None] = None
|
||||
ge: Union[SupportsGe, None] = None
|
||||
lt: Union[SupportsLt, None] = None
|
||||
le: Union[SupportsLe, None] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack an Interval into zero or more single-bounds."""
|
||||
if self.gt is not None:
|
||||
yield Gt(self.gt)
|
||||
if self.ge is not None:
|
||||
yield Ge(self.ge)
|
||||
if self.lt is not None:
|
||||
yield Lt(self.lt)
|
||||
if self.le is not None:
|
||||
yield Le(self.le)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MultipleOf(BaseMetadata):
|
||||
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
|
||||
|
||||
1. Python semantics, implying ``value % multiple_of == 0``, or
|
||||
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
|
||||
|
||||
We encourage users to be aware of these two common interpretations,
|
||||
and libraries to carefully document which they implement.
|
||||
"""
|
||||
|
||||
multiple_of: Union[SupportsDiv, SupportsMod]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MinLen(BaseMetadata):
|
||||
"""
|
||||
MinLen() implies minimum inclusive length,
|
||||
e.g. ``len(value) >= min_length``.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MaxLen(BaseMetadata):
|
||||
"""
|
||||
MaxLen() implies maximum inclusive length,
|
||||
e.g. ``len(value) <= max_length``.
|
||||
"""
|
||||
|
||||
max_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Len(GroupedMetadata):
|
||||
"""
|
||||
Len() implies that ``min_length <= len(value) <= max_length``.
|
||||
|
||||
Upper bound may be omitted or ``None`` to indicate no upper length bound.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)] = 0
|
||||
max_length: Optional[Annotated[int, Ge(0)]] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack a Len into zone or more single-bounds."""
|
||||
if self.min_length > 0:
|
||||
yield MinLen(self.min_length)
|
||||
if self.max_length is not None:
|
||||
yield MaxLen(self.max_length)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Timezone(BaseMetadata):
|
||||
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
|
||||
|
||||
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
|
||||
``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be
|
||||
tz-aware but any timezone is allowed.
|
||||
|
||||
You may also pass a specific timezone string or tzinfo object such as
|
||||
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
|
||||
you only allow a specific timezone, though we note that this is often
|
||||
a symptom of poor design.
|
||||
"""
|
||||
|
||||
tz: Union[str, tzinfo, EllipsisType, None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Unit(BaseMetadata):
|
||||
"""Indicates that the value is a physical quantity with the specified unit.
|
||||
|
||||
It is intended for usage with numeric types, where the value represents the
|
||||
magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]``
|
||||
or ``speed: Annotated[float, Unit('m/s')]``.
|
||||
|
||||
Interpretation of the unit string is left to the discretion of the consumer.
|
||||
It is suggested to follow conventions established by python libraries that work
|
||||
with physical quantities, such as
|
||||
|
||||
- ``pint`` : <https://pint.readthedocs.io/en/stable/>
|
||||
- ``astropy.units``: <https://docs.astropy.org/en/stable/units/>
|
||||
|
||||
For indicating a quantity with a certain dimensionality but without a specific unit
|
||||
it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`.
|
||||
Note, however, ``annotated_types`` itself makes no use of the unit string.
|
||||
"""
|
||||
|
||||
unit: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Predicate(BaseMetadata):
|
||||
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
|
||||
|
||||
Users should prefer statically inspectable metadata, but if you need the full
|
||||
power and flexibility of arbitrary runtime predicates... here it is.
|
||||
|
||||
We provide a few predefined predicates for common string constraints:
|
||||
``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and
|
||||
``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which
|
||||
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
|
||||
|
||||
Some libraries might have special logic to handle certain predicates, e.g. by
|
||||
checking for `str.isdigit` and using its presence to both call custom logic to
|
||||
enforce digit-only strings, and customise some generated external schema.
|
||||
|
||||
We do not specify what behaviour should be expected for predicates that raise
|
||||
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
|
||||
skip invalid constraints, or statically raise an error; or it might try calling it
|
||||
and then propagate or discard the resulting exception.
|
||||
"""
|
||||
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if getattr(self.func, "__name__", "<lambda>") == "<lambda>":
|
||||
return f"{self.__class__.__name__}({self.func!r})"
|
||||
if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and (
|
||||
namespace := getattr(self.func.__self__, "__name__", None)
|
||||
):
|
||||
return f"{self.__class__.__name__}({namespace}.{self.func.__name__})"
|
||||
if isinstance(self.func, type(str.isascii)): # method descriptor
|
||||
return f"{self.__class__.__name__}({self.func.__qualname__})"
|
||||
return f"{self.__class__.__name__}({self.func.__name__})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Not:
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
def __call__(self, __v: Any) -> bool:
|
||||
return not self.func(__v)
|
||||
|
||||
|
||||
_StrType = TypeVar("_StrType", bound=str)
|
||||
|
||||
LowerCase = Annotated[_StrType, Predicate(str.islower)]
|
||||
"""
|
||||
Return True if the string is a lowercase string, False otherwise.
|
||||
|
||||
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
|
||||
"""
|
||||
Return True if the string is an uppercase string, False otherwise.
|
||||
|
||||
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
IsDigit = Annotated[_StrType, Predicate(str.isdigit)]
|
||||
IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63
|
||||
"""
|
||||
Return True if the string is a digit string, False otherwise.
|
||||
|
||||
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
|
||||
""" # noqa: E501
|
||||
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
|
||||
"""
|
||||
Return True if all characters in the string are ASCII, False otherwise.
|
||||
|
||||
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
|
||||
"""
|
||||
|
||||
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
|
||||
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
|
||||
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
|
||||
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
|
||||
"""Return True if x is one of infinity or NaN, and False otherwise"""
|
||||
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
|
||||
"""Return True if x is a NaN (not a number), and False otherwise."""
|
||||
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
|
||||
"""Return True if x is anything but NaN (not a number), and False otherwise."""
|
||||
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
|
||||
"""Return True if x is a positive or negative infinity, and False otherwise."""
|
||||
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
|
||||
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
|
||||
|
||||
try:
|
||||
from typing_extensions import DocInfo, doc # type: ignore [attr-defined]
|
||||
except ImportError:
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class DocInfo: # type: ignore [no-redef]
|
||||
""" "
|
||||
The return value of doc(), mainly to be used by tools that want to extract the
|
||||
Annotated documentation at runtime.
|
||||
"""
|
||||
|
||||
documentation: str
|
||||
"""The documentation string passed to doc()."""
|
||||
|
||||
def doc(
|
||||
documentation: str,
|
||||
) -> DocInfo:
|
||||
"""
|
||||
Add documentation to a type annotation inside of Annotated.
|
||||
|
||||
For example:
|
||||
|
||||
>>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ...
|
||||
"""
|
||||
return DocInfo(documentation)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+151
@@ -0,0 +1,151 @@
|
||||
import math
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated
|
||||
else:
|
||||
from typing import Annotated
|
||||
|
||||
import annotated_types as at
|
||||
|
||||
|
||||
class Case(NamedTuple):
|
||||
"""
|
||||
A test case for `annotated_types`.
|
||||
"""
|
||||
|
||||
annotation: Any
|
||||
valid_cases: Iterable[Any]
|
||||
invalid_cases: Iterable[Any]
|
||||
|
||||
|
||||
def cases() -> Iterable[Case]:
|
||||
# Gt, Ge, Lt, Le
|
||||
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(date(2000, 1, 1))],
|
||||
[date(2000, 1, 2), date(2000, 1, 3)],
|
||||
[date(2000, 1, 1), date(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(Decimal('1.123'))],
|
||||
[Decimal('1.1231'), Decimal('123')],
|
||||
[Decimal('1.123'), Decimal('0')],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
|
||||
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
|
||||
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
|
||||
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
|
||||
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
# Interval
|
||||
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
|
||||
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
|
||||
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
|
||||
|
||||
# lengths
|
||||
|
||||
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
|
||||
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
|
||||
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
|
||||
|
||||
yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
|
||||
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
|
||||
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
|
||||
|
||||
# Timezone
|
||||
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(timezone.utc)],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone('Europe/London')],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
|
||||
# Quantity
|
||||
|
||||
yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m'))
|
||||
|
||||
# predicate types
|
||||
|
||||
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
|
||||
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
|
||||
yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2'])
|
||||
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
|
||||
|
||||
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
|
||||
|
||||
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
|
||||
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
|
||||
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
|
||||
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
|
||||
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
|
||||
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
|
||||
|
||||
# check stacked predicates
|
||||
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
|
||||
|
||||
# doc
|
||||
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
|
||||
|
||||
# custom GroupedMetadata
|
||||
class MyCustomGroupedMetadata(at.GroupedMetadata):
|
||||
def __iter__(self) -> Iterator[at.Predicate]:
|
||||
yield at.Predicate(lambda x: float(x).is_integer())
|
||||
|
||||
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])
|
||||
+1
@@ -0,0 +1 @@
|
||||
pip
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: anyio
|
||||
Version: 4.14.1
|
||||
Summary: High-level concurrency and networking framework on top of asyncio or Trio
|
||||
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
|
||||
License-Expression: MIT
|
||||
Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/
|
||||
Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html
|
||||
Project-URL: Source code, https://github.com/agronholm/anyio
|
||||
Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Framework :: AnyIO
|
||||
Classifier: Typing :: Typed
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Python :: 3.15
|
||||
Requires-Python: >=3.10
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11"
|
||||
Requires-Dist: idna>=2.8
|
||||
Requires-Dist: typing_extensions>=4.5; python_version < "3.13"
|
||||
Provides-Extra: trio
|
||||
Requires-Dist: trio>=0.32.0; extra == "trio"
|
||||
Dynamic: license-file
|
||||
|
||||
.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg
|
||||
:target: https://github.com/agronholm/anyio/actions/workflows/test.yml
|
||||
:alt: Build Status
|
||||
.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/agronholm/anyio?branch=master
|
||||
:alt: Code Coverage
|
||||
.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest
|
||||
:target: https://anyio.readthedocs.io/en/latest/?badge=latest
|
||||
:alt: Documentation
|
||||
.. image:: https://badges.gitter.im/gitterHQ/gitter.svg
|
||||
:target: https://gitter.im/python-trio/AnyIO
|
||||
:alt: Gitter chat
|
||||
.. image:: https://tidelift.com/badges/package/pypi/anyio
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-anyio
|
||||
:alt: Tidelift
|
||||
|
||||
AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or
|
||||
Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony
|
||||
with the native SC of Trio itself.
|
||||
|
||||
Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or
|
||||
Trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full
|
||||
refactoring necessary. It will blend in with the native libraries of your chosen backend.
|
||||
|
||||
To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it
|
||||
`here <https://anyio.readthedocs.io/en/stable/why.html>`_.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
View full documentation at: https://anyio.readthedocs.io/
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
AnyIO offers the following functionality:
|
||||
|
||||
* Task groups (nurseries_ in trio terminology)
|
||||
* High-level networking (TCP, UDP and UNIX sockets)
|
||||
|
||||
* `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python
|
||||
3.8)
|
||||
* async/await style UDP sockets (unlike asyncio where you still have to use Transports and
|
||||
Protocols)
|
||||
|
||||
* A versatile API for byte streams and object streams
|
||||
* Inter-task synchronization and communication (locks, conditions, events, semaphores, object
|
||||
streams)
|
||||
* Worker threads
|
||||
* Subprocesses
|
||||
* Subinterpreter support for code parallelization (on Python 3.13 and later)
|
||||
* Asynchronous file I/O (using worker threads)
|
||||
* Signal handling
|
||||
* Asynchronous versions of the functools_ and itertools_ modules
|
||||
|
||||
AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures.
|
||||
It even works with the popular Hypothesis_ library.
|
||||
|
||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||
.. _Trio: https://github.com/python-trio/trio
|
||||
.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency
|
||||
.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
|
||||
.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs
|
||||
.. _pytest: https://docs.pytest.org/en/latest/
|
||||
.. _functools: https://docs.python.org/3/library/functools.html
|
||||
.. _itertools: https://docs.python.org/3/library/itertools.html
|
||||
.. _Hypothesis: https://hypothesis.works/
|
||||
|
||||
Security contact information
|
||||
----------------------------
|
||||
|
||||
To report a security vulnerability, please use the `Tidelift security contact`_.
|
||||
Tidelift will coordinate the fix and disclosure.
|
||||
|
||||
.. _Tidelift security contact: https://tidelift.com/security
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
anyio-4.14.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
anyio-4.14.1.dist-info/METADATA,sha256=bfkjYaZLYPsPI5JV_Gn7HYF65mteyE8nhjaI0ZqC4L4,4645
|
||||
anyio-4.14.1.dist-info/RECORD,,
|
||||
anyio-4.14.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
||||
anyio-4.14.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
|
||||
anyio-4.14.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
|
||||
anyio-4.14.1.dist-info/scm_file_list.json,sha256=wDSXGv8Ehn5ZW5BhB-RlaAc16zY_OfO27qrlMfMMZy8,3654
|
||||
anyio-4.14.1.dist-info/scm_version.json,sha256=gw22Q2aBbdiYhyMbObTYNN7BN-wSpzOCktNiAuulRN8,161
|
||||
anyio-4.14.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
|
||||
anyio/__init__.py,sha256=HitUIfzvAojSeaHVmJ9rFn8k_yI63G6s_jUL2QChf4U,6405
|
||||
anyio/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/__pycache__/from_thread.cpython-311.pyc,,
|
||||
anyio/__pycache__/functools.cpython-311.pyc,,
|
||||
anyio/__pycache__/itertools.cpython-311.pyc,,
|
||||
anyio/__pycache__/lowlevel.cpython-311.pyc,,
|
||||
anyio/__pycache__/pytest_plugin.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_interpreter.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_process.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_thread.cpython-311.pyc,,
|
||||
anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_backends/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/_backends/__pycache__/_asyncio.cpython-311.pyc,,
|
||||
anyio/_backends/__pycache__/_trio.cpython-311.pyc,,
|
||||
anyio/_backends/_asyncio.py,sha256=-q-5gUYg_r5SsN-OYbQnF_lvtW0v51-dFlsU8_gduWA,102077
|
||||
anyio/_backends/_trio.py,sha256=vR0ZgxVnOo4AHhHcHVG0worMc-3ZNpAZ6Vxh0m0ZZC0,45189
|
||||
anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_core/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_asyncio_selector_thread.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_contextmanagers.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_eventloop.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_exceptions.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_fileio.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_resources.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_signals.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_sockets.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_streams.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_subprocesses.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_synchronization.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_tasks.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_tempfile.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_testing.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_typedattr.cpython-311.pyc,,
|
||||
anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626
|
||||
anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215
|
||||
anyio/_core/_eventloop.py,sha256=ByZUeJD9alMfcyTseRo5IzTO0IltEul_Gyq9iqSjqDk,6658
|
||||
anyio/_core/_exceptions.py,sha256=OfzLO4Z3Hog1TnipbIn72YNtkoYxS4lHW9MqKDeGc88,4936
|
||||
anyio/_core/_fileio.py,sha256=hHfyV0bXDL-R2ZNnInwse3nmTAd36AIz1cBxgmAwzAQ,31358
|
||||
anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435
|
||||
anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016
|
||||
anyio/_core/_sockets.py,sha256=9FU423j52XBBfGVr6MdzPTdyw8bGrzApZ5m338-AtsY,35286
|
||||
anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806
|
||||
anyio/_core/_subprocesses.py,sha256=tkmkPKEkEaiMD8C9WRZBlmgjOYRDRbZdte6e-unay2E,7916
|
||||
anyio/_core/_synchronization.py,sha256=jn2nIbTRlBAUXL-mx_a3I_VnasF8GbVFpBRp2-YwCx0,21591
|
||||
anyio/_core/_tasks.py,sha256=ELL2jscaSW0Jw_xA6MtQlm3xwvFEzjTbc1u9Tteyt0I,13244
|
||||
anyio/_core/_tempfile.py,sha256=jE2w59FRF3yRo4vjkjfZF2YcqsBZvc66VWRwrJGDYGk,19624
|
||||
anyio/_core/_testing.py,sha256=u7MPqGXwpTxqI7hclSdNA30z2GH1Nw258uwKvy_RfBg,2340
|
||||
anyio/_core/_typedattr.py,sha256=P4ozZikn3-DbpoYcvyghS_FOYAgbmUxeoU8-L_07pZM,2508
|
||||
anyio/abc/__init__.py,sha256=6mWhcl_pGXhrgZVHP_TCfMvIXIOp9mroEFM90fYCU_U,2869
|
||||
anyio/abc/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_eventloop.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_resources.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_sockets.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_streams.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_subprocesses.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_tasks.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_testing.cpython-311.pyc,,
|
||||
anyio/abc/_eventloop.py,sha256=OqWYSEj0TmwL_xniCJt3_jHFWsuMk9THk8tCTGsKapI,10681
|
||||
anyio/abc/_resources.py,sha256=DrYvkNN1hH6Uvv5_5uKySvDsnknGVDe8FCKfko0VtN8,783
|
||||
anyio/abc/_sockets.py,sha256=OmVDrfemVvF9c5K1tpBgQyV6fn5v0XyCExLAqBOGz9o,13124
|
||||
anyio/abc/_streams.py,sha256=HYvna1iZbWcwLROTO6IhLX79RTRLPShZMWe0sG1q54I,7481
|
||||
anyio/abc/_subprocesses.py,sha256=cumAPJTktOQtw63IqG0lDpyZqu_l1EElvQHMiwJgL08,2067
|
||||
anyio/abc/_tasks.py,sha256=m-FtE4phxeNIELSG7A3H7VUz3jA2Ib5J2JIew8-PS6o,6642
|
||||
anyio/abc/_testing.py,sha256=9YYM2AXsYFvf4PLjUEr6yRxDiUeB5QbY_gOg0X_C6lY,2034
|
||||
anyio/from_thread.py,sha256=JYsbaCaIB_Iit6kNhtXSteJGt4PcQ7ncq0nIpcelIrg,19265
|
||||
anyio/functools.py,sha256=T4JS8IXq-x1S0Lbo2owF8l9fza2KypO147QLeyz4cjs,11797
|
||||
anyio/itertools.py,sha256=QV-9mnRCr2yBph8g01QFvN-bQ_Yle-8Sl13YSydBlMI,16168
|
||||
anyio/lowlevel.py,sha256=WPtppHfI2qs1nokzjn8elL8LvyqI05AK5Zslhlo71A4,6242
|
||||
anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/pytest_plugin.py,sha256=paMpI_VMNQf2bir0LfvgMpXSiYJoHDzWdKUVTyoHmvQ,13609
|
||||
anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/streams/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/buffered.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/file.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/memory.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/stapled.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/text.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/tls.cpython-311.pyc,,
|
||||
anyio/streams/buffered.py,sha256=v3xKtjFHgNV41g2SvMAkA_qd2t9WYlCI1_lNGCAatw0,6650
|
||||
anyio/streams/file.py,sha256=msnrotVKGMQomUu_Rj2qz9MvIdUp6d3JGr7MOEO8kV4,4428
|
||||
anyio/streams/memory.py,sha256=F0zwzvFJKAhX_LRZGoKzzqDC2oMM-f-yyTBrEYEGOaU,10740
|
||||
anyio/streams/stapled.py,sha256=T8Xqwf8K6EgURPxbt1N4i7A8BAk-gScv-GRhjLXIf_o,4390
|
||||
anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765
|
||||
anyio/streams/tls.py,sha256=DQVkXUvsTEYKkBO8dlVU7j_5H8QOtLy4sGi1Wrjqevo,15303
|
||||
anyio/to_interpreter.py,sha256=_mLngrMy97TMR6VbW4Y6YzDUk9ZuPcQMPlkuyRh3C9k,7100
|
||||
anyio/to_process.py,sha256=68qhLfce7MeXysid4fOpmhfWkgdo7Z7-9BC0VyUciIE,9809
|
||||
anyio/to_thread.py,sha256=f6h_k2d743GBv9FhAnhM_YpTvWgIrzBy9cOE0eJ1UJw,2693
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (82.0.1)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[pytest11]
|
||||
anyio = anyio.pytest_plugin
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Alex Grönholm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"files": [
|
||||
".pre-commit-config.yaml",
|
||||
"LICENSE",
|
||||
"pyproject.toml",
|
||||
"AGENTS.md",
|
||||
"README.rst",
|
||||
"CLAUDE.md",
|
||||
".readthedocs.yml",
|
||||
".gitignore",
|
||||
"docs/tempfile.rst",
|
||||
"docs/signals.rst",
|
||||
"docs/synchronization.rst",
|
||||
"docs/contextmanagers.rst",
|
||||
"docs/testing.rst",
|
||||
"docs/networking.rst",
|
||||
"docs/contributing.rst",
|
||||
"docs/index.rst",
|
||||
"docs/versionhistory.rst",
|
||||
"docs/threads.rst",
|
||||
"docs/api.rst",
|
||||
"docs/typedattrs.rst",
|
||||
"docs/basics.rst",
|
||||
"docs/fileio.rst",
|
||||
"docs/cancellation.rst",
|
||||
"docs/support.rst",
|
||||
"docs/streams.rst",
|
||||
"docs/why.rst",
|
||||
"docs/tasks.rst",
|
||||
"docs/migration.rst",
|
||||
"docs/conf.py",
|
||||
"docs/subprocesses.rst",
|
||||
"docs/faq.rst",
|
||||
"docs/subinterpreters.rst",
|
||||
"src/anyio/functools.py",
|
||||
"src/anyio/py.typed",
|
||||
"src/anyio/__init__.py",
|
||||
"src/anyio/pytest_plugin.py",
|
||||
"src/anyio/itertools.py",
|
||||
"src/anyio/to_interpreter.py",
|
||||
"src/anyio/from_thread.py",
|
||||
"src/anyio/to_process.py",
|
||||
"src/anyio/to_thread.py",
|
||||
"src/anyio/lowlevel.py",
|
||||
"src/anyio/_backends/_trio.py",
|
||||
"src/anyio/_backends/__init__.py",
|
||||
"src/anyio/_backends/_asyncio.py",
|
||||
"src/anyio/streams/memory.py",
|
||||
"src/anyio/streams/__init__.py",
|
||||
"src/anyio/streams/tls.py",
|
||||
"src/anyio/streams/file.py",
|
||||
"src/anyio/streams/text.py",
|
||||
"src/anyio/streams/stapled.py",
|
||||
"src/anyio/streams/buffered.py",
|
||||
"src/anyio/abc/_eventloop.py",
|
||||
"src/anyio/abc/__init__.py",
|
||||
"src/anyio/abc/_sockets.py",
|
||||
"src/anyio/abc/_tasks.py",
|
||||
"src/anyio/abc/_subprocesses.py",
|
||||
"src/anyio/abc/_resources.py",
|
||||
"src/anyio/abc/_streams.py",
|
||||
"src/anyio/abc/_testing.py",
|
||||
"src/anyio/_core/_typedattr.py",
|
||||
"src/anyio/_core/_eventloop.py",
|
||||
"src/anyio/_core/__init__.py",
|
||||
"src/anyio/_core/_tempfile.py",
|
||||
"src/anyio/_core/_sockets.py",
|
||||
"src/anyio/_core/_tasks.py",
|
||||
"src/anyio/_core/_fileio.py",
|
||||
"src/anyio/_core/_synchronization.py",
|
||||
"src/anyio/_core/_subprocesses.py",
|
||||
"src/anyio/_core/_resources.py",
|
||||
"src/anyio/_core/_contextmanagers.py",
|
||||
"src/anyio/_core/_exceptions.py",
|
||||
"src/anyio/_core/_streams.py",
|
||||
"src/anyio/_core/_signals.py",
|
||||
"src/anyio/_core/_asyncio_selector_thread.py",
|
||||
"src/anyio/_core/_testing.py",
|
||||
"tests/test_itertools.py",
|
||||
"tests/test_functools.py",
|
||||
"tests/test_eventloop.py",
|
||||
"tests/__init__.py",
|
||||
"tests/test_to_thread.py",
|
||||
"tests/test_from_thread.py",
|
||||
"tests/test_lowlevel.py",
|
||||
"tests/test_to_interpreter.py",
|
||||
"tests/test_sockets.py",
|
||||
"tests/test_typedattr.py",
|
||||
"tests/test_to_process.py",
|
||||
"tests/test_all_attributes.py",
|
||||
"tests/test_synchronization.py",
|
||||
"tests/test_debugging.py",
|
||||
"tests/test_contextmanagers.py",
|
||||
"tests/test_fileio.py",
|
||||
"tests/conftest.py",
|
||||
"tests/test_signals.py",
|
||||
"tests/test_deprecations.py",
|
||||
"tests/test_tempfile.py",
|
||||
"tests/test_taskgroups.py",
|
||||
"tests/test_pytest_plugin.py",
|
||||
"tests/test_subprocesses.py",
|
||||
"tests/streams/test_text.py",
|
||||
"tests/streams/test_memory.py",
|
||||
"tests/streams/__init__.py",
|
||||
"tests/streams/test_file.py",
|
||||
"tests/streams/test_stapled.py",
|
||||
"tests/streams/test_tls.py",
|
||||
"tests/streams/test_buffered.py",
|
||||
".github/pull_request_template.md",
|
||||
".github/dependabot.yml",
|
||||
".github/FUNDING.yml",
|
||||
".github/ISSUE_TEMPLATE/features_request.yaml",
|
||||
".github/ISSUE_TEMPLATE/bug_report.yaml",
|
||||
".github/ISSUE_TEMPLATE/config.yml",
|
||||
".github/workflows/test.yml",
|
||||
".github/workflows/test-downstream.yml",
|
||||
".github/workflows/publish.yml"
|
||||
]
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"tag": "4.14.1",
|
||||
"distance": 0,
|
||||
"node": "g149b9e907618fadf6840a4d3cebad533b0c7d033",
|
||||
"dirty": false,
|
||||
"branch": "HEAD",
|
||||
"node_date": "2026-06-24"
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
anyio
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._core._contextmanagers import AsyncContextManagerMixin as AsyncContextManagerMixin
|
||||
from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin
|
||||
from ._core._eventloop import current_time as current_time
|
||||
from ._core._eventloop import get_all_backends as get_all_backends
|
||||
from ._core._eventloop import get_available_backends as get_available_backends
|
||||
from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class
|
||||
from ._core._eventloop import run as run
|
||||
from ._core._eventloop import sleep as sleep
|
||||
from ._core._eventloop import sleep_forever as sleep_forever
|
||||
from ._core._eventloop import sleep_until as sleep_until
|
||||
from ._core._exceptions import BrokenResourceError as BrokenResourceError
|
||||
from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter
|
||||
from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess
|
||||
from ._core._exceptions import BusyResourceError as BusyResourceError
|
||||
from ._core._exceptions import ClosedResourceError as ClosedResourceError
|
||||
from ._core._exceptions import ConnectionFailed as ConnectionFailed
|
||||
from ._core._exceptions import DelimiterNotFound as DelimiterNotFound
|
||||
from ._core._exceptions import EndOfStream as EndOfStream
|
||||
from ._core._exceptions import IncompleteRead as IncompleteRead
|
||||
from ._core._exceptions import NoEventLoopError as NoEventLoopError
|
||||
from ._core._exceptions import RunFinishedError as RunFinishedError
|
||||
from ._core._exceptions import TaskCancelled as TaskCancelled
|
||||
from ._core._exceptions import TaskFailed as TaskFailed
|
||||
from ._core._exceptions import TaskNotFinished as TaskNotFinished
|
||||
from ._core._exceptions import TypedAttributeLookupError as TypedAttributeLookupError
|
||||
from ._core._exceptions import WouldBlock as WouldBlock
|
||||
from ._core._fileio import AsyncFile as AsyncFile
|
||||
from ._core._fileio import Path as Path
|
||||
from ._core._fileio import open_file as open_file
|
||||
from ._core._fileio import wrap_file as wrap_file
|
||||
from ._core._resources import aclose_forcefully as aclose_forcefully
|
||||
from ._core._signals import open_signal_receiver as open_signal_receiver
|
||||
from ._core._sockets import TCPConnectable as TCPConnectable
|
||||
from ._core._sockets import UNIXConnectable as UNIXConnectable
|
||||
from ._core._sockets import as_connectable as as_connectable
|
||||
from ._core._sockets import connect_tcp as connect_tcp
|
||||
from ._core._sockets import connect_unix as connect_unix
|
||||
from ._core._sockets import create_connected_udp_socket as create_connected_udp_socket
|
||||
from ._core._sockets import (
|
||||
create_connected_unix_datagram_socket as create_connected_unix_datagram_socket,
|
||||
)
|
||||
from ._core._sockets import create_tcp_listener as create_tcp_listener
|
||||
from ._core._sockets import create_udp_socket as create_udp_socket
|
||||
from ._core._sockets import create_unix_datagram_socket as create_unix_datagram_socket
|
||||
from ._core._sockets import create_unix_listener as create_unix_listener
|
||||
from ._core._sockets import getaddrinfo as getaddrinfo
|
||||
from ._core._sockets import getnameinfo as getnameinfo
|
||||
from ._core._sockets import notify_closing as notify_closing
|
||||
from ._core._sockets import wait_readable as wait_readable
|
||||
from ._core._sockets import wait_socket_readable as wait_socket_readable
|
||||
from ._core._sockets import wait_socket_writable as wait_socket_writable
|
||||
from ._core._sockets import wait_writable as wait_writable
|
||||
from ._core._streams import create_memory_object_stream as create_memory_object_stream
|
||||
from ._core._subprocesses import open_process as open_process
|
||||
from ._core._subprocesses import run_process as run_process
|
||||
from ._core._synchronization import CapacityLimiter as CapacityLimiter
|
||||
from ._core._synchronization import (
|
||||
CapacityLimiterStatistics as CapacityLimiterStatistics,
|
||||
)
|
||||
from ._core._synchronization import Condition as Condition
|
||||
from ._core._synchronization import ConditionStatistics as ConditionStatistics
|
||||
from ._core._synchronization import Event as Event
|
||||
from ._core._synchronization import EventStatistics as EventStatistics
|
||||
from ._core._synchronization import Lock as Lock
|
||||
from ._core._synchronization import LockStatistics as LockStatistics
|
||||
from ._core._synchronization import ResourceGuard as ResourceGuard
|
||||
from ._core._synchronization import Semaphore as Semaphore
|
||||
from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics
|
||||
from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED
|
||||
from ._core._tasks import CancelScope as CancelScope
|
||||
from ._core._tasks import TaskHandle as TaskHandle
|
||||
from ._core._tasks import create_task_group as create_task_group
|
||||
from ._core._tasks import current_effective_deadline as current_effective_deadline
|
||||
from ._core._tasks import fail_after as fail_after
|
||||
from ._core._tasks import move_on_after as move_on_after
|
||||
from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile
|
||||
from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile
|
||||
from ._core._tempfile import TemporaryDirectory as TemporaryDirectory
|
||||
from ._core._tempfile import TemporaryFile as TemporaryFile
|
||||
from ._core._tempfile import gettempdir as gettempdir
|
||||
from ._core._tempfile import gettempdirb as gettempdirb
|
||||
from ._core._tempfile import mkdtemp as mkdtemp
|
||||
from ._core._tempfile import mkstemp as mkstemp
|
||||
from ._core._testing import TaskInfo as TaskInfo
|
||||
from ._core._testing import get_current_task as get_current_task
|
||||
from ._core._testing import get_running_tasks as get_running_tasks
|
||||
from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked
|
||||
from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider
|
||||
from ._core._typedattr import TypedAttributeSet as TypedAttributeSet
|
||||
from ._core._typedattr import typed_attribute as typed_attribute
|
||||
|
||||
# Re-export imports so they look like they live directly in this package
|
||||
for __value in list(locals().values()):
|
||||
if getattr(__value, "__module__", "").startswith("anyio."):
|
||||
__value.__module__ = __name__
|
||||
|
||||
|
||||
del __value
|
||||
|
||||
|
||||
def __getattr__(attr: str) -> type[BrokenWorkerInterpreter]:
|
||||
"""Support deprecated aliases."""
|
||||
if attr == "BrokenWorkerIntepreter":
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"The 'BrokenWorkerIntepreter' alias is deprecated, use 'BrokenWorkerInterpreter' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return BrokenWorkerInterpreter
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+3077
File diff suppressed because it is too large
Load Diff
+1456
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+167
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import FileDescriptorLike
|
||||
|
||||
_selector_lock = threading.Lock()
|
||||
_selector: Selector | None = None
|
||||
|
||||
|
||||
class Selector:
|
||||
def __init__(self) -> None:
|
||||
self._thread = threading.Thread(target=self.run, name="AnyIO socket selector")
|
||||
self._selector = DefaultSelector()
|
||||
self._send, self._receive = socket.socketpair()
|
||||
self._send.setblocking(False)
|
||||
self._receive.setblocking(False)
|
||||
# This somewhat reduces the amount of memory wasted queueing up data
|
||||
# for wakeups. With these settings, maximum number of 1-byte sends
|
||||
# before getting BlockingIOError:
|
||||
# Linux 4.8: 6
|
||||
# macOS (darwin 15.5): 1
|
||||
# Windows 10: 525347
|
||||
# Windows you're weird. (And on Windows setting SNDBUF to 0 makes send
|
||||
# blocking, even on non-blocking sockets, so don't do that.)
|
||||
self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)
|
||||
self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1)
|
||||
# On Windows this is a TCP socket so this might matter. On other
|
||||
# platforms this fails b/c AF_UNIX sockets aren't actually TCP.
|
||||
try:
|
||||
self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self._selector.register(self._receive, EVENT_READ)
|
||||
self._closed = False
|
||||
|
||||
def start(self) -> None:
|
||||
self._thread.start()
|
||||
threading._register_atexit(self._stop) # type: ignore[attr-defined]
|
||||
|
||||
def _stop(self) -> None:
|
||||
global _selector
|
||||
self._closed = True
|
||||
self._notify_self()
|
||||
self._send.close()
|
||||
self._thread.join()
|
||||
self._selector.unregister(self._receive)
|
||||
self._receive.close()
|
||||
self._selector.close()
|
||||
_selector = None
|
||||
assert not self._selector.get_map(), (
|
||||
"selector still has registered file descriptors after shutdown"
|
||||
)
|
||||
|
||||
def _notify_self(self) -> None:
|
||||
try:
|
||||
self._send.send(b"\x00")
|
||||
except BlockingIOError:
|
||||
pass
|
||||
|
||||
def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)})
|
||||
else:
|
||||
if EVENT_READ in key.data:
|
||||
raise ValueError(
|
||||
"this file descriptor is already registered for reading"
|
||||
)
|
||||
|
||||
key.data[EVENT_READ] = loop, callback
|
||||
self._selector.modify(fd, key.events | EVENT_READ, key.data)
|
||||
|
||||
self._notify_self()
|
||||
|
||||
def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)})
|
||||
else:
|
||||
if EVENT_WRITE in key.data:
|
||||
raise ValueError(
|
||||
"this file descriptor is already registered for writing"
|
||||
)
|
||||
|
||||
key.data[EVENT_WRITE] = loop, callback
|
||||
self._selector.modify(fd, key.events | EVENT_WRITE, key.data)
|
||||
|
||||
self._notify_self()
|
||||
|
||||
def remove_reader(self, fd: FileDescriptorLike) -> bool:
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if new_events := key.events ^ EVENT_READ:
|
||||
del key.data[EVENT_READ]
|
||||
self._selector.modify(fd, new_events, key.data)
|
||||
else:
|
||||
self._selector.unregister(fd)
|
||||
|
||||
return True
|
||||
|
||||
def remove_writer(self, fd: FileDescriptorLike) -> bool:
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if new_events := key.events ^ EVENT_WRITE:
|
||||
del key.data[EVENT_WRITE]
|
||||
self._selector.modify(fd, new_events, key.data)
|
||||
else:
|
||||
self._selector.unregister(fd)
|
||||
|
||||
return True
|
||||
|
||||
def run(self) -> None:
|
||||
while not self._closed:
|
||||
for key, events in self._selector.select():
|
||||
if key.fileobj is self._receive:
|
||||
try:
|
||||
while self._receive.recv(4096):
|
||||
pass
|
||||
except BlockingIOError:
|
||||
pass
|
||||
|
||||
continue
|
||||
|
||||
if events & EVENT_READ:
|
||||
loop, callback = key.data[EVENT_READ]
|
||||
self.remove_reader(key.fd)
|
||||
try:
|
||||
loop.call_soon_threadsafe(callback)
|
||||
except RuntimeError:
|
||||
pass # the loop was already closed
|
||||
|
||||
if events & EVENT_WRITE:
|
||||
loop, callback = key.data[EVENT_WRITE]
|
||||
self.remove_writer(key.fd)
|
||||
try:
|
||||
loop.call_soon_threadsafe(callback)
|
||||
except RuntimeError:
|
||||
pass # the loop was already closed
|
||||
|
||||
|
||||
def get_selector() -> Selector:
|
||||
global _selector
|
||||
|
||||
with _selector_lock:
|
||||
if _selector is None:
|
||||
_selector = Selector()
|
||||
_selector.start()
|
||||
|
||||
return _selector
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
||||
from inspect import isasyncgen, iscoroutine, isgenerator
|
||||
from types import TracebackType
|
||||
from typing import Protocol, TypeVar, cast, final
|
||||
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound="bool | None")
|
||||
|
||||
|
||||
class _SupportsCtxMgr(Protocol[_T_co, _ExitT_co]):
|
||||
def __contextmanager__(self) -> AbstractContextManager[_T_co, _ExitT_co]: ...
|
||||
|
||||
|
||||
class _SupportsAsyncCtxMgr(Protocol[_T_co, _ExitT_co]):
|
||||
def __asynccontextmanager__(
|
||||
self,
|
||||
) -> AbstractAsyncContextManager[_T_co, _ExitT_co]: ...
|
||||
|
||||
|
||||
class ContextManagerMixin:
|
||||
"""
|
||||
Mixin class providing context manager functionality via a generator-based
|
||||
implementation.
|
||||
|
||||
This class allows you to implement a context manager via :meth:`__contextmanager__`
|
||||
which should return a generator. The mechanics are meant to mirror those of
|
||||
:func:`@contextmanager <contextlib.contextmanager>`.
|
||||
|
||||
.. note:: Classes using this mix-in are not reentrant as context managers, meaning
|
||||
that once you enter it, you can't re-enter before first exiting it.
|
||||
|
||||
.. seealso:: :doc:`contextmanagers`
|
||||
"""
|
||||
|
||||
__cm: AbstractContextManager[object, bool | None] | None = None
|
||||
|
||||
@final
|
||||
def __enter__(self: _SupportsCtxMgr[_T_co, bool | None]) -> _T_co:
|
||||
# Needed for mypy to assume self still has the __cm member
|
||||
assert isinstance(self, ContextManagerMixin)
|
||||
if self.__cm is not None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has already been entered"
|
||||
)
|
||||
|
||||
cm = self.__contextmanager__()
|
||||
if not isinstance(cm, AbstractContextManager):
|
||||
if isgenerator(cm):
|
||||
raise TypeError(
|
||||
"__contextmanager__() returned a generator object instead of "
|
||||
"a context manager. Did you forget to add the @contextmanager "
|
||||
"decorator?"
|
||||
)
|
||||
|
||||
raise TypeError(
|
||||
f"__contextmanager__() did not return a context manager object, "
|
||||
f"but {cm.__class__!r}"
|
||||
)
|
||||
|
||||
if cm is self:
|
||||
raise TypeError(
|
||||
f"{self.__class__.__qualname__}.__contextmanager__() returned "
|
||||
f"self. Did you forget to add the @contextmanager decorator and a "
|
||||
f"'yield' statement?"
|
||||
)
|
||||
|
||||
value = cm.__enter__()
|
||||
self.__cm = cm
|
||||
return value
|
||||
|
||||
@final
|
||||
def __exit__(
|
||||
self: _SupportsCtxMgr[object, _ExitT_co],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> _ExitT_co:
|
||||
# Needed for mypy to assume self still has the __cm member
|
||||
assert isinstance(self, ContextManagerMixin)
|
||||
if self.__cm is None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has not been entered yet"
|
||||
)
|
||||
|
||||
# Prevent circular references
|
||||
cm = self.__cm
|
||||
del self.__cm
|
||||
|
||||
return cast(_ExitT_co, cm.__exit__(exc_type, exc_val, exc_tb))
|
||||
|
||||
@abstractmethod
|
||||
def __contextmanager__(self) -> AbstractContextManager[object, bool | None]:
|
||||
"""
|
||||
Implement your context manager logic here.
|
||||
|
||||
This method **must** be decorated with
|
||||
:func:`@contextmanager <contextlib.contextmanager>`.
|
||||
|
||||
.. note:: Remember that the ``yield`` will raise any exception raised in the
|
||||
enclosed context block, so use a ``finally:`` block to clean up resources!
|
||||
|
||||
:return: a context manager object
|
||||
"""
|
||||
|
||||
|
||||
class AsyncContextManagerMixin:
|
||||
"""
|
||||
Mixin class providing async context manager functionality via a generator-based
|
||||
implementation.
|
||||
|
||||
This class allows you to implement a context manager via
|
||||
:meth:`__asynccontextmanager__`. The mechanics are meant to mirror those of
|
||||
:func:`@asynccontextmanager <contextlib.asynccontextmanager>`.
|
||||
|
||||
.. note:: Classes using this mix-in are not reentrant as context managers, meaning
|
||||
that once you enter it, you can't re-enter before first exiting it.
|
||||
|
||||
.. seealso:: :doc:`contextmanagers`
|
||||
"""
|
||||
|
||||
__cm: AbstractAsyncContextManager[object, bool | None] | None = None
|
||||
|
||||
@final
|
||||
async def __aenter__(self: _SupportsAsyncCtxMgr[_T_co, bool | None]) -> _T_co:
|
||||
# Needed for mypy to assume self still has the __cm member
|
||||
assert isinstance(self, AsyncContextManagerMixin)
|
||||
if self.__cm is not None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has already been entered"
|
||||
)
|
||||
|
||||
cm = self.__asynccontextmanager__()
|
||||
if not isinstance(cm, AbstractAsyncContextManager):
|
||||
if isasyncgen(cm):
|
||||
raise TypeError(
|
||||
"__asynccontextmanager__() returned an async generator instead of "
|
||||
"an async context manager. Did you forget to add the "
|
||||
"@asynccontextmanager decorator?"
|
||||
)
|
||||
elif iscoroutine(cm):
|
||||
cm.close()
|
||||
raise TypeError(
|
||||
"__asynccontextmanager__() returned a coroutine object instead of "
|
||||
"an async context manager. Did you forget to add the "
|
||||
"@asynccontextmanager decorator and a 'yield' statement?"
|
||||
)
|
||||
|
||||
raise TypeError(
|
||||
f"__asynccontextmanager__() did not return an async context manager, "
|
||||
f"but {cm.__class__!r}"
|
||||
)
|
||||
|
||||
if cm is self:
|
||||
raise TypeError(
|
||||
f"{self.__class__.__qualname__}.__asynccontextmanager__() returned "
|
||||
f"self. Did you forget to add the @asynccontextmanager decorator and a "
|
||||
f"'yield' statement?"
|
||||
)
|
||||
|
||||
value = await cm.__aenter__()
|
||||
self.__cm = cm
|
||||
return value
|
||||
|
||||
@final
|
||||
async def __aexit__(
|
||||
self: _SupportsAsyncCtxMgr[object, _ExitT_co],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> _ExitT_co:
|
||||
assert isinstance(self, AsyncContextManagerMixin)
|
||||
if self.__cm is None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has not been entered yet"
|
||||
)
|
||||
|
||||
# Prevent circular references
|
||||
cm = self.__cm
|
||||
del self.__cm
|
||||
|
||||
return cast(_ExitT_co, await cm.__aexit__(exc_type, exc_val, exc_tb))
|
||||
|
||||
@abstractmethod
|
||||
def __asynccontextmanager__(
|
||||
self,
|
||||
) -> AbstractAsyncContextManager[object, bool | None]:
|
||||
"""
|
||||
Implement your async context manager logic here.
|
||||
|
||||
This method **must** be decorated with
|
||||
:func:`@asynccontextmanager <contextlib.asynccontextmanager>`.
|
||||
|
||||
.. note:: Remember that the ``yield`` will raise any exception raised in the
|
||||
enclosed context block, so use a ``finally:`` block to clean up resources!
|
||||
|
||||
:return: an async context manager object
|
||||
"""
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import Token
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from ._exceptions import NoEventLoopError
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
sniffio: Any
|
||||
try:
|
||||
import sniffio
|
||||
except ModuleNotFoundError:
|
||||
sniffio = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..abc import AsyncBackend
|
||||
|
||||
# This must be updated when new backends are introduced
|
||||
BACKENDS = "asyncio", "trio"
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
threadlocals = threading.local()
|
||||
loaded_backends: dict[str, type[AsyncBackend]] = {}
|
||||
|
||||
|
||||
def run(
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
||||
*args: Unpack[PosArgsT],
|
||||
backend: str = "asyncio",
|
||||
backend_options: dict[str, Any] | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Run the given coroutine function in an asynchronous event loop.
|
||||
|
||||
The current thread must not be already running an event loop.
|
||||
|
||||
:param func: a coroutine function
|
||||
:param args: positional arguments to ``func``
|
||||
:param backend: name of the asynchronous event loop implementation – currently
|
||||
either ``asyncio`` or ``trio``
|
||||
:param backend_options: keyword arguments to call the backend ``run()``
|
||||
implementation with (documented :ref:`here <backend options>`)
|
||||
:return: the return value of the coroutine function
|
||||
:raises RuntimeError: if an asynchronous event loop is already running in this
|
||||
thread
|
||||
:raises LookupError: if the named backend is not found
|
||||
|
||||
"""
|
||||
if asynclib_name := current_async_library():
|
||||
raise RuntimeError(f"Already running {asynclib_name} in this thread")
|
||||
|
||||
try:
|
||||
async_backend = get_async_backend(backend)
|
||||
except ImportError as exc:
|
||||
if backend in BACKENDS:
|
||||
raise LookupError(
|
||||
f"Backend {backend!r} is not available. "
|
||||
f"Install it with: pip install anyio[{backend}]"
|
||||
) from exc
|
||||
|
||||
raise LookupError(f"No such backend: {backend}") from exc
|
||||
|
||||
token = None
|
||||
if asynclib_name is None:
|
||||
# Since we're in control of the event loop, we can cache the name of the async
|
||||
# library
|
||||
token = set_current_async_library(backend)
|
||||
|
||||
try:
|
||||
backend_options = backend_options or {}
|
||||
return async_backend.run(func, args, {}, backend_options)
|
||||
finally:
|
||||
reset_current_async_library(token)
|
||||
|
||||
|
||||
async def sleep(delay: float) -> None:
|
||||
"""
|
||||
Pause the current task for the specified duration.
|
||||
|
||||
:param delay: the duration, in seconds
|
||||
|
||||
"""
|
||||
return await get_async_backend().sleep(delay)
|
||||
|
||||
|
||||
async def sleep_forever() -> None:
|
||||
"""
|
||||
Pause the current task until it's cancelled.
|
||||
|
||||
This is a shortcut for ``sleep(math.inf)``.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
"""
|
||||
await sleep(math.inf)
|
||||
|
||||
|
||||
async def sleep_until(deadline: float) -> None:
|
||||
"""
|
||||
Pause the current task until the given time.
|
||||
|
||||
:param deadline: the absolute time to wake up at (according to the internal
|
||||
monotonic clock of the event loop)
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
"""
|
||||
now = current_time()
|
||||
await sleep(max(deadline - now, 0))
|
||||
|
||||
|
||||
def current_time() -> float:
|
||||
"""
|
||||
Return the current value of the event loop's internal clock.
|
||||
|
||||
:return: the clock value (seconds)
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().current_time()
|
||||
|
||||
|
||||
def get_all_backends() -> tuple[str, ...]:
|
||||
"""Return a tuple of the names of all built-in backends."""
|
||||
return BACKENDS
|
||||
|
||||
|
||||
def get_available_backends() -> tuple[str, ...]:
|
||||
"""
|
||||
Test for the availability of built-in backends.
|
||||
|
||||
:return a tuple of the built-in backend names that were successfully imported
|
||||
|
||||
.. versionadded:: 4.12
|
||||
|
||||
"""
|
||||
available_backends: list[str] = []
|
||||
for backend_name in get_all_backends():
|
||||
try:
|
||||
get_async_backend(backend_name)
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
available_backends.append(backend_name)
|
||||
|
||||
return tuple(available_backends)
|
||||
|
||||
|
||||
def get_cancelled_exc_class() -> type[BaseException]:
|
||||
"""
|
||||
Return the current async library's cancellation exception class.
|
||||
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().cancelled_exception_class()
|
||||
|
||||
|
||||
#
|
||||
# Private API
|
||||
#
|
||||
|
||||
|
||||
@contextmanager
|
||||
def claim_worker_thread(
|
||||
backend_class: type[AsyncBackend], token: object
|
||||
) -> Generator[Any, None, None]:
|
||||
from ..lowlevel import EventLoopToken
|
||||
|
||||
threadlocals.current_token = EventLoopToken(backend_class, token)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
del threadlocals.current_token
|
||||
|
||||
|
||||
def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]:
|
||||
if asynclib_name is None:
|
||||
asynclib_name = current_async_library()
|
||||
if not asynclib_name:
|
||||
raise NoEventLoopError(
|
||||
f"Not currently running on any asynchronous event loop. "
|
||||
f"Available async backends: {', '.join(get_all_backends())}"
|
||||
)
|
||||
|
||||
# We use our own dict instead of sys.modules to get the already imported back-end
|
||||
# class because the appropriate modules in sys.modules could potentially be only
|
||||
# partially initialized
|
||||
try:
|
||||
return loaded_backends[asynclib_name]
|
||||
except KeyError:
|
||||
module = import_module(f"anyio._backends._{asynclib_name}")
|
||||
loaded_backends[asynclib_name] = module.backend_class
|
||||
return module.backend_class
|
||||
|
||||
|
||||
def current_async_library() -> str | None:
|
||||
if sniffio is None:
|
||||
# If sniffio is not installed, we assume we're either running asyncio or nothing
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
return "asyncio"
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
return sniffio.current_async_library()
|
||||
except sniffio.AsyncLibraryNotFoundError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def set_current_async_library(asynclib_name: str | None) -> Token | None:
|
||||
# no-op if sniffio is not installed
|
||||
if sniffio is None:
|
||||
return None
|
||||
|
||||
return sniffio.current_async_library_cvar.set(asynclib_name)
|
||||
|
||||
|
||||
def reset_current_async_library(token: Token | None) -> None:
|
||||
if token is not None:
|
||||
sniffio.current_async_library_cvar.reset(token)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user