diff --git a/AGENTS.md b/AGENTS.md index e332143..ad23b2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,6 +127,10 @@ timeout 120 uv run pytest -n 4 Testes normally run under 60s (~40s on average), so take a closer look if they take longer, it can be a regression. +When analyzing warnings or extensive output, run the tests **once** saving the output to a file +(`> /tmp/pytest-output.txt 2>&1`), then analyze the file — instead of running the suite +repeatedly with different greps. + Coverage is enabled by default (`--cov` is in `pyproject.toml`'s `addopts`). To generate a coverage report to a file, pass `--cov-report` **in addition to** `--cov`: diff --git a/conftest.py b/conftest.py index 5b0a38f..2ae7e7b 100644 --- a/conftest.py +++ b/conftest.py @@ -29,11 +29,11 @@ def __init__(self): doctest_namespace["asyncio"] = ContribAsyncio() -def pytest_ignore_collect(collection_path, path, config): +def pytest_ignore_collect(collection_path, config): if sys.version_info >= (3, 10): # noqa: UP036 return None - if "django_project" in str(path): + if "django_project" in str(collection_path): return True diff --git a/docs/releases/2.3.0.md b/docs/releases/2.3.0.md index 57cb927..83bb090 100644 --- a/docs/releases/2.3.0.md +++ b/docs/releases/2.3.0.md @@ -26,24 +26,22 @@ async code with a state machine. ``` -```py ->>> class AsyncStateMachine(StateMachine): -... initial = State('Initial', initial=True) -... final = State('Final', final=True) -... -... advance = initial.to(final) -... -... async def on_advance(self): -... return 42 +```python +class AsyncStateMachine(StateMachine): + initial = State('Initial', initial=True) + final = State('Final', final=True) + advance = initial.to(final) + async def on_advance(self): + return 42 ->>> async def run_sm(): -... sm = AsyncStateMachine() -... res = await sm.advance() -... return (42, sm.current_state.name) ->>> asyncio.run(run_sm()) -(42, 'Final') +async def run_sm(): + sm = AsyncStateMachine() + res = await sm.advance() + return (42, sm.current_state.name) +asyncio.run(run_sm()) +# (42, 'Final') ``` diff --git a/docs/releases/2.4.0.md b/docs/releases/2.4.0.md index d2f776b..8054af3 100644 --- a/docs/releases/2.4.0.md +++ b/docs/releases/2.4.0.md @@ -16,31 +16,29 @@ This release introduces support for conditionals with Boolean algebra. You can n Example (with a spoiler of the next highlight): -```py ->>> from statemachine import StateMachine, State, Event +```python +from statemachine import StateMachine, State, Event ->>> class AnyConditionSM(StateMachine): -... start = State(initial=True) -... end = State(final=True) -... -... submit = Event( -... start.to(end, cond="used_money or used_credit"), -... name="finish order", -... ) -... -... used_money: bool = False -... used_credit: bool = False +class AnyConditionSM(StateMachine): + start = State(initial=True) + end = State(final=True) + + submit = Event( + start.to(end, cond="used_money or used_credit"), + name="finish order", + ) ->>> sm = AnyConditionSM() ->>> sm.submit() -Traceback (most recent call last): -TransitionNotAllowed: Can't finish order when in Start. + used_money: bool = False + used_credit: bool = False ->>> sm.used_credit = True ->>> sm.submit() ->>> sm.current_state.id -'end' +sm = AnyConditionSM() +sm.submit() +# TransitionNotAllowed: Can't finish order when in Start. +sm.used_credit = True +sm.submit() +sm.current_state.id +# 'end' ``` ```{seealso} diff --git a/docs/releases/2.5.0.md b/docs/releases/2.5.0.md index fbdb8b0..8cfe584 100644 --- a/docs/releases/2.5.0.md +++ b/docs/releases/2.5.0.md @@ -64,46 +64,27 @@ You can think of the event as an implementation of the **command** design patter On this example, we iterate until the state machine reaches a final state, listing the current state allowed events and executing the simulated user choice: -``` ->>> import random ->>> random.seed("15") +```python +import random +random.seed("15") ->>> sm = AccountStateMachine() +sm = AccountStateMachine() ->>> while not sm.current_state.final: -... allowed_events = sm.allowed_events -... print("Choose an action: ") -... for idx, event in enumerate(allowed_events): -... print(f"{idx} - {event.name}") -... -... user_input = random.randint(0, len(allowed_events)-1) -... print(f"User input: {user_input}") -... -... event = allowed_events[user_input] -... print(f"Running the option {user_input} - {event.name}") -... event() -Choose an action: -0 - Suspend -1 - Overdraft -2 - Close account -User input: 0 -Running the option 0 - Suspend -Choose an action: -0 - Activate -1 - Close account -User input: 0 -Running the option 0 - Activate -Choose an action: -0 - Suspend -1 - Overdraft -2 - Close account -User input: 2 -Running the option 2 - Close account -Account has been closed. +while not sm.current_state.final: + allowed_events = sm.allowed_events + print("Choose an action: ") + for idx, event in enumerate(allowed_events): + print(f"{idx} - {event.name}") + + user_input = random.randint(0, len(allowed_events)-1) + print(f"User input: {user_input}") ->>> print(f"SM is in {sm.current_state.name} state.") -SM is in Closed state. + event = allowed_events[user_input] + print(f"Running the option {user_input} - {event.name}") + event() +print(f"SM is in {sm.current_state.name} state.") +# SM is in Closed state. ``` ### Conditions expressions in 2.5.0 @@ -120,30 +101,28 @@ The following comparison operators are supported: Example: -```py ->>> from statemachine import StateMachine, State, Event +```python +from statemachine import StateMachine, State, Event ->>> class AnyConditionSM(StateMachine): -... start = State(initial=True) -... end = State(final=True) -... -... submit = Event( -... start.to(end, cond="order_value > 100"), -... name="finish order", -... ) -... -... order_value: float = 0 +class AnyConditionSM(StateMachine): + start = State(initial=True) + end = State(final=True) + + submit = Event( + start.to(end, cond="order_value > 100"), + name="finish order", + ) ->>> sm = AnyConditionSM() ->>> sm.submit() -Traceback (most recent call last): -TransitionNotAllowed: Can't finish order when in Start. + order_value: float = 0 ->>> sm.order_value = 135.0 ->>> sm.submit() ->>> sm.current_state.id -'end' +sm = AnyConditionSM() +sm.submit() +# TransitionNotAllowed: Can't finish order when in Start. +sm.order_value = 135.0 +sm.submit() +sm.current_state.id +# 'end' ``` ```{seealso} diff --git a/pyproject.toml b/pyproject.toml index 39b24f1..8e12652 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,9 +39,9 @@ dev = [ "pytest-cov >=6.0.0; python_version >='3.9'", "pytest-cov; python_version <'3.9'", "pytest-sugar >=1.0.0", - "pytest-mock >=3.10.0", + "pytest-mock >=3.14.0", "pytest-benchmark >=4.0.0", - "pytest-asyncio", + "pytest-asyncio >=0.25.0", "pydot", "django >=5.2.11; python_version >='3.10'", "pytest-django >=4.8.0; python_version >'3.8'", @@ -93,6 +93,9 @@ log_cli_level = "DEBUG" log_cli_format = "%(relativeCreated)6.0fms %(threadName)-18s %(name)-35s %(message)s" log_cli_date_format = "%H:%M:%S" asyncio_default_fixture_loop_scope = "module" +filterwarnings = [ + "ignore::pytest_benchmark.logger.PytestBenchmarkWarning", +] [tool.coverage.run] branch = true diff --git a/statemachine/engines/base.py b/statemachine/engines/base.py index 0186926..2a199a0 100644 --- a/statemachine/engines/base.py +++ b/statemachine/engines/base.py @@ -159,7 +159,7 @@ def handler(error: Exception) -> None: # new error.execution is a separate event that may trigger a different # transition (see W3C test 152). The infinite-loop guard lives at the # *microstep* level (in ``_send_error_execution``), not here. - self.sm.send(_ERROR_EXECUTION, error=error, internal=True) + BoundEvent(_ERROR_EXECUTION, internal=True, _sm=self.sm).put(error=error) return handler @@ -188,7 +188,7 @@ def _send_error_execution(self, error: Exception, trigger_data: TriggerData): if trigger_data.event and str(trigger_data.event) == _ERROR_EXECUTION: logger.warning("Error while processing error.execution, ignoring: %s", error) return - self.sm.send(_ERROR_EXECUTION, error=error, internal=True) + BoundEvent(_ERROR_EXECUTION, internal=True, _sm=self.sm).put(error=error) def start(self, **kwargs): if self.sm.current_state_value is not None: diff --git a/statemachine/invoke.py b/statemachine/invoke.py index 68eae85..9c77556 100644 --- a/statemachine/invoke.py +++ b/statemachine/invoke.py @@ -455,7 +455,7 @@ async def _run_async_handler( None, lambda: callback.call(ctx=ctx, machine=ctx.machine, **ctx.kwargs) ) if not ctx.cancelled.is_set(): - self.sm.send( + await self.sm.send( f"done.invoke.{ctx.invokeid}", data=result, ) @@ -466,7 +466,7 @@ async def _run_async_handler( except Exception as e: if not ctx.cancelled.is_set(): # External queue — see comment in _run_sync_handler. - self.sm.send("error.execution", error=e) + await self.sm.send("error.execution", error=e) finally: invocation.terminated = True logger.debug( diff --git a/statemachine/io/scxml/actions.py b/statemachine/io/scxml/actions.py index 5b7cb5c..9830f26 100644 --- a/statemachine/io/scxml/actions.py +++ b/statemachine/io/scxml/actions.py @@ -7,6 +7,7 @@ from typing import Callable from uuid import uuid4 +from ...event import BoundEvent from ...event import Event from ...event import _event_data_kwargs from ...spec_parser import InState @@ -426,7 +427,7 @@ def _send_to_invoke(action: SendAction, invokeid: str, **kwargs): params_values[param.name] = _eval(param.expr, **kwargs) if not machine._engine._invoke_manager.send_to_child(invokeid, event, **params_values): # Per SCXML spec: if target is not reachable → error.communication - machine.send("error.communication", internal=True) + BoundEvent("error.communication", internal=True, _sm=machine).put() def create_send_action_callable(action: SendAction) -> Callable: # noqa: C901 @@ -452,7 +453,7 @@ def send_action(*args, **kwargs): # noqa: C901 if target not in _valid_targets: if target and target.startswith("#_scxml_"): # Valid SCXML session reference but undispatchable → error.communication - machine.send("error.communication", internal=True) + BoundEvent("error.communication", internal=True, _sm=machine).put() elif target and target.startswith("#_"): # #_ → route to invoked child session _send_to_invoke(action, target[2:], **kwargs) diff --git a/statemachine/io/scxml/invoke.py b/statemachine/io/scxml/invoke.py index 1b4b355..436c1bf 100644 --- a/statemachine/io/scxml/invoke.py +++ b/statemachine/io/scxml/invoke.py @@ -6,7 +6,9 @@ finalize. """ +import asyncio import logging +from inspect import isawaitable from pathlib import Path from typing import Any from typing import Callable @@ -222,7 +224,9 @@ def __init__(self, parent, invokeid: str): def send_to_parent(self, event: str, **data): """Send an event to the parent machine's external queue.""" - self.parent.send(event, _invokeid=self.invokeid, **data) + result = self.parent.send(event, _invokeid=self.invokeid, **data) + if isawaitable(result): + asyncio.ensure_future(result) # Verify protocol compliance at import time diff --git a/tests/conftest.py b/tests/conftest.py index 9c5e83c..052e4df 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -309,8 +309,13 @@ def _check_leaked_threads(): if not new_threads: return - # Filter out asyncio event loop threads (managed by pytest-asyncio, not by us). - new_threads = {t for t in new_threads if not t.name.startswith("asyncio_")} + # Filter out asyncio event loop threads (managed by pytest-asyncio, not by us) + # and DummyThreads (created by Python for foreign threads — cannot be joined). + new_threads = { + t + for t in new_threads + if not t.name.startswith("asyncio_") and not isinstance(t, threading._DummyThread) + } if not new_threads: return diff --git a/tests/test_invoke.py b/tests/test_invoke.py index 87d6fa2..54d3524 100644 --- a/tests/test_invoke.py +++ b/tests/test_invoke.py @@ -254,7 +254,7 @@ def task_b(): return "b" class SM(StateChart): - loading = State(initial=True, invoke=[task_a, task_b]) + loading = State(initial=True, invoke=invoke_group(task_a, task_b)) ready = State(final=True) done_invoke_loading = loading.to(ready) diff --git a/tests/test_scxml_units.py b/tests/test_scxml_units.py index 3128740..c3a467d 100644 --- a/tests/test_scxml_units.py +++ b/tests/test_scxml_units.py @@ -522,7 +522,9 @@ def test_sends_error_communication_when_child_not_found(self): _send_to_invoke(action, "unknown", machine=machine) - machine.send.assert_called_once_with("error.communication", internal=True) + machine._put_nonblocking.assert_called_once() + trigger_data = machine._put_nonblocking.call_args[0][0] + assert str(trigger_data.event) == "error.communication" def test_evaluates_eventexpr(self): """_send_to_invoke evaluates eventexpr when event is None.""" @@ -610,7 +612,9 @@ def test_send_action_callable_scxml_session_target(self): send_callable(machine=machine) - machine.send.assert_called_once_with("error.communication", internal=True) + machine._put_nonblocking.assert_called_once() + trigger_data = machine._put_nonblocking.call_args[0][0] + assert str(trigger_data.event) == "error.communication" machine._engine._invoke_manager.send_to_child.assert_not_called() diff --git a/tests/test_weighted_transitions.py b/tests/test_weighted_transitions.py index f3cd55f..d2a612c 100644 --- a/tests/test_weighted_transitions.py +++ b/tests/test_weighted_transitions.py @@ -22,20 +22,20 @@ class TestWeightedTransitionsBasic: def test_deterministic_with_seed(self, WeightedIdleSC): sm = WeightedIdleSC() sm.send("idle") - first_state = sm.current_state + first_config = sm.configuration sm.send("finish") sm.send("idle") - second_state = sm.current_state + second_config = sm.configuration # With seed=42, results are deterministic # Create a fresh instance to verify same seed produces same sequence sm2 = WeightedIdleSC() sm2.send("idle") - assert sm2.current_state == first_state + assert sm2.configuration == first_config sm2.send("finish") sm2.send("idle") - assert sm2.current_state == second_state + assert sm2.configuration == second_config def test_statistical_distribution(self, WeightedIdleSC): """Over many iterations, the distribution should approximate the weights.""" @@ -45,7 +45,7 @@ def test_statistical_distribution(self, WeightedIdleSC): for _ in range(iterations): sm.send("idle") - counts[sm.current_state.id] += 1 + counts[next(iter(sm.configuration)).id] += 1 sm.send("finish") # With 70/20/10 weights, check roughly correct distribution (within 5%) @@ -63,7 +63,7 @@ class SingleWeighted(StateChart): sm = SingleWeighted() sm.send("go") - assert sm.current_state == SingleWeighted.s2 + assert sm.configuration == {SingleWeighted.s2} def test_equal_weights(self): class EqualWeights(StateChart): @@ -80,7 +80,7 @@ class EqualWeights(StateChart): for _ in range(iterations): sm.send("go") - counts[sm.current_state.id] += 1 + counts[next(iter(sm.configuration)).id] += 1 sm.send("back") # Should be roughly 50/50 within 5% @@ -102,7 +102,7 @@ class FloatWeights(StateChart): for _ in range(iterations): sm.send("go") - counts[sm.current_state.id] += 1 + counts[next(iter(sm.configuration)).id] += 1 sm.send("back") assert abs(counts["s2"] / iterations - 0.70) < 0.05 @@ -119,7 +119,7 @@ class MixedWeights(StateChart): sm = MixedWeights() sm.send("go") - assert sm.current_state in (MixedWeights.s2, MixedWeights.s3) + assert sm.configuration & {MixedWeights.s2, MixedWeights.s3} class TestWeightedTransitionsWithGuards: @@ -147,7 +147,7 @@ def is_allowed(self): counts = Counter() for _ in range(1000): sm.send("go") - counts[sm.current_state.id] += 1 + counts[next(iter(sm.configuration)).id] += 1 sm.send("back") assert counts["s2"] > 0 @@ -175,7 +175,7 @@ def is_blocked(self): # When not blocked, s2 can fire sm.send("go") - first_state = sm.current_state + first_state = next(iter(sm.configuration)) sm.send("back") # When blocked, s2 cond fails even if weight selects it @@ -184,7 +184,7 @@ def is_blocked(self): for _ in range(100): try: sm.send("go") - results[sm.current_state.id] += 1 + results[next(iter(sm.configuration)).id] += 1 sm.send("back") except Exception: results["failed"] += 1 @@ -427,12 +427,12 @@ class MultiGroup(StateChart): sm = MultiGroup() sm.send("go_a") - state_a = sm.current_state + state_a = next(iter(sm.configuration)) assert state_a in (MultiGroup.s2, MultiGroup.s3) sm.send("back") sm.send("go_b") - state_b = sm.current_state + state_b = next(iter(sm.configuration)) assert state_b in (MultiGroup.s4, MultiGroup.s5) diff --git a/uv.lock b/uv.lock index c72653e..496ec66 100644 --- a/uv.lock +++ b/uv.lock @@ -22,8 +22,7 @@ dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } wheels = [ @@ -35,7 +34,7 @@ name = "asgiref" version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } wheels = [ @@ -51,6 +50,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313 }, +] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -571,7 +579,7 @@ resolution-markers = [ dependencies = [ { name = "mypy-extensions", marker = "python_full_version < '3.10'" }, { name = "tomli", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", size = 2855162 } wheels = [ @@ -603,7 +611,7 @@ resolution-markers = [ dependencies = [ { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } wheels = [ @@ -819,25 +827,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/16/70be3b725073035aa5fc3229321d06e22e73e3e09f6af78dcfdf16c7636c/platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", size = 17562 }, ] -[[package]] -name = "pluggy" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695 }, -] - [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, @@ -914,8 +907,7 @@ version = "1.1.408" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578 } wheels = [ @@ -924,55 +916,53 @@ wheels = [ [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] [[package]] -name = "pytest" -version = "8.3.3" +name = "pytest-asyncio" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10'" }, + { name = "pytest", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095 }, ] [[package]] name = "pytest-asyncio" -version = "0.21.2" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] dependencies = [ - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*'" }, + { name = "pytest", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/53/57663d99acaac2fcdafdc697e52a9b1b7d6fcf36616281ff9768a44e7ff3/pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45", size = 30656 } +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/ce/1e4b53c213dce25d6e8b163697fbce2d43799d76fa08eea6ad270451c370/pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b", size = 13368 }, + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, ] [[package]] @@ -984,7 +974,7 @@ resolution-markers = [ ] dependencies = [ { name = "py-cpuinfo", marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/08/e6b0067efa9a1f2a1eb3043ecd8a0c48bfeb60d3255006dcc829d72d5da2/pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1", size = 334641 } wheels = [ @@ -1000,7 +990,7 @@ resolution-markers = [ ] dependencies = [ { name = "py-cpuinfo", marker = "python_full_version >= '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/d0/a8bd08d641b393db3be3819b03e2d9bb8760ca8479080a26a5f6e540e99c/pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105", size = 337810 } wheels = [ @@ -1013,10 +1003,8 @@ version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, - { name = "pluggy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } wheels = [ @@ -1028,8 +1016,7 @@ name = "pytest-django" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/c0/43c8b2528c24d7f1a48a47e3f7381f5ab2ae8c64634b0c3f4bd843063955/pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314", size = 84067 } wheels = [ @@ -1038,15 +1025,14 @@ wheels = [ [[package]] name = "pytest-mock" -version = "3.11.1" +version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/2d/b3a811ec4fa24190a9ec5013e23c89421a7916167c6240c31fdc445f850c/pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f", size = 31251 } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/85/80ae98e019a429445bfb74e153d4cb47c3695e3e908515e95e95c18237e5/pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39", size = 9590 }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 }, ] [[package]] @@ -1055,8 +1041,7 @@ version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, { name = "termcolor" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992 } @@ -1069,8 +1054,7 @@ name = "pytest-timeout" version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973 } wheels = [ @@ -1083,8 +1067,7 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069 } wheels = [ @@ -1114,9 +1097,9 @@ dev = [ { name = "pre-commit" }, { name = "pydot" }, { name = "pyright" }, - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pytest-asyncio" }, + { name = "pytest" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest-benchmark", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-cov" }, @@ -1148,12 +1131,12 @@ dev = [ { name = "pydot" }, { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest" }, - { name = "pytest-asyncio" }, + { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "pytest-benchmark", specifier = ">=4.0.0" }, { name = "pytest-cov", marker = "python_full_version < '3.9'" }, { name = "pytest-cov", marker = "python_full_version >= '3.9'", specifier = ">=6.0.0" }, { name = "pytest-django", marker = "python_full_version >= '3.9'", specifier = ">=4.8.0" }, - { name = "pytest-mock", specifier = ">=3.10.0" }, + { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "pytest-sugar", specifier = ">=1.0.0" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, { name = "pytest-xdist", specifier = ">=3.6.1" }, @@ -1225,8 +1208,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } wheels = [ @@ -1441,7 +1423,7 @@ resolution-markers = [ ] dependencies = [ { name = "anyio", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/d0/0332bd8a25779a0e2082b0e179805ad39afad642938b371ae0882e7f880d/starlette-0.47.0.tar.gz", hash = "sha256:1f64887e94a447fed5f23309fb6890ef23349b7e478faa7b24a851cd4eb844af", size = 2582856 } wheels = [ @@ -1457,7 +1439,7 @@ resolution-markers = [ ] dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031 } wheels = [ @@ -1482,25 +1464,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, ] -[[package]] -name = "typing-extensions" -version = "4.7.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232 }, -] - [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, @@ -1531,8 +1498,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } wheels = [ @@ -1567,7 +1533,7 @@ dependencies = [ { name = "distlib", marker = "python_full_version >= '3.10'" }, { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "platformdirs", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239 } wheels = [