1
0
mirror of https://github.com/MarkParker5/STARK.git synced 2025-02-17 11:55:35 +02:00

pass test_commands_context [no ci]

This commit is contained in:
Mark Parker 2023-09-15 16:49:02 +02:00
parent 0009c26744
commit c5057a1716
No known key found for this signature in database
GPG Key ID: C10A60786A07A300
11 changed files with 271 additions and 206 deletions

104
poetry.lock generated
View File

@ -35,6 +35,24 @@ files = [
[package.dependencies]
anyio = ">=3.4.0,<4.0.0"
[[package]]
name = "attrs"
version = "23.1.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
{file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
]
[package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
dev = ["attrs[docs,tests]", "pre-commit"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
[[package]]
name = "babel"
version = "2.12.1"
@ -90,7 +108,7 @@ files = [
name = "cffi"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
optional = true
optional = false
python-versions = "*"
files = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@ -878,6 +896,20 @@ files = [
setuptools = "*"
wheel = "*"
[[package]]
name = "outcome"
version = "1.2.0"
description = "Capture the outcome of Python function calls."
optional = false
python-versions = ">=3.7"
files = [
{file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"},
{file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"},
]
[package.dependencies]
attrs = ">=19.2.0"
[[package]]
name = "packaging"
version = "23.1"
@ -1008,7 +1040,7 @@ pyasn1 = ">=0.4.6,<0.6.0"
name = "pycparser"
version = "2.21"
description = "C parser in Python"
optional = true
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
@ -1122,19 +1154,38 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-anyio"
version = "0.0.0"
description = "The pytest anyio plugin is built into anyio. You don't need this package."
name = "pytest-asyncio"
version = "0.21.1"
description = "Pytest support for asyncio"
optional = false
python-versions = "*"
python-versions = ">=3.7"
files = [
{file = "pytest-anyio-0.0.0.tar.gz", hash = "sha256:b41234e9e9ad7ea1dbfefcc1d6891b23d5ef7c9f07ccf804c13a9cc338571fd3"},
{file = "pytest_anyio-0.0.0-py2.py3-none-any.whl", hash = "sha256:dc8b5c4741cb16ff90be37fddd585ca943ed12bbeb563de7ace6cd94441d8746"},
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
]
[package.dependencies]
anyio = "*"
pytest = "*"
pytest = ">=7.0.0"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
[[package]]
name = "pytest-trio"
version = "0.8.0"
description = "Pytest plugin for trio"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-trio-0.8.0.tar.gz", hash = "sha256:8363db6336a79e6c53375a2123a41ddbeccc4aa93f93788651641789a56fb52e"},
{file = "pytest_trio-0.8.0-py3-none-any.whl", hash = "sha256:e6a7e7351ae3e8ec3f4564d30ee77d1ec66e1df611226e5618dbb32f9545c841"},
]
[package.dependencies]
outcome = ">=1.1.0"
pytest = ">=7.2.0"
trio = ">=0.22.0"
[[package]]
name = "python-dateutil"
@ -1415,6 +1466,17 @@ files = [
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
]
[[package]]
name = "sounddevice"
version = "0.4.6"
@ -1548,6 +1610,26 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
[[package]]
name = "trio"
version = "0.22.2"
description = "A friendly Python library for async concurrency and I/O"
optional = false
python-versions = ">=3.7"
files = [
{file = "trio-0.22.2-py3-none-any.whl", hash = "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"},
{file = "trio-0.22.2.tar.gz", hash = "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3"},
]
[package.dependencies]
attrs = ">=20.1.0"
cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""}
exceptiongroup = {version = ">=1.0.0rc9", markers = "python_version < \"3.11\""}
idna = "*"
outcome = "*"
sniffio = "*"
sortedcontainers = "*"
[[package]]
name = "typing-extensions"
version = "4.7.1"
@ -1738,4 +1820,4 @@ vosk = ["sounddevice", "vosk"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "321b841eb5e19b2a8d0b5096aaffde6819ff0238435fd802ebabea7714ebef3d"
content-hash = "c03b79855601eac20974ce1d5affe7c9da3fdd8ee7c943f0333d5eb3fbec4e9f"

View File

@ -27,14 +27,15 @@ silero = ["torch", "numpy", "sounddevice"]
sound = ["sounddevice", "soundfile"]
[tool.poetry.group.dev.dependencies]
# tests and static validation
pytest = "^7.2.1"
mypy = "^1.1.1"
# docs generation
mkdocs-material = { version = "^9.2.8", optional = true }
mkdocs-git-revision-date-localized-plugin = { version = "^1.2.0", optional = true }
mkdocs-swagger-ui-tag = { version = "^0.6.4", optional = true }
pytest-anyio = "^0.0.0"
# tests and static validation
pytest = "^7.2.1"
mypy = "^1.1.1"
pytest-asyncio = "^0.21.1"
pytest-trio = "^0.8.0"
[build-system]
requires = ["poetry-core"]
@ -44,6 +45,8 @@ build-backend = "poetry.core.masonry.api"
pythonpath = [
"stark",
]
# asyncio_mode = "auto"
trio_mode = "true"
[tool.mypy]
ignore_missing_imports = true

View File

@ -1,10 +1,11 @@
from __future__ import annotations
from typing import Callable, Awaitable, Any, Optional, Protocol
from typing import Callable, Awaitable, Any, Protocol
from enum import auto, Enum
from datetime import datetime
import inspect
from pydantic import BaseModel, Field
import asyncer
from general.classproperty import classproperty
from .patterns import Pattern
@ -17,11 +18,10 @@ CommandRunner = Callable[..., 'Response | None'] | AsyncCommandRunner
class Command:
name: str
pattern: Pattern
_runner: AsyncCommandRunner
_runner: CommandRunner
def __init__(self, name: str, pattern: Pattern, runner: AsyncCommandRunner):
def __init__(self, name: str, pattern: Pattern, runner: CommandRunner):
assert isinstance(pattern, Pattern)
assert inspect.iscoroutinefunction(runner)
self.name = name
self.pattern = pattern
self._runner = runner
@ -32,14 +32,21 @@ class Command:
# where parameters is dict {'foo': bar, 'lorem': ipsum}
parameters = parameters_dict or {}
parameters.update(kwparameters)
parameters.update(kwparameters)
runner: AsyncCommandRunner
if inspect.iscoroutinefunction(self._runner):
runner = self._runner
else:
runner = asyncer.asyncify(self._runner)
if any(p.kind == p.VAR_KEYWORD for p in inspect.signature(self._runner).parameters.values()):
# all extra params will be passed as **kwargs
return self._runner(**parameters)
# if command runner accepts **kwargs, pass all parameters
return runner(**parameters)
else:
# avoid TypeError: self._runner got an unexpected keyword argument
return self._runner(**{k: v for k, v in parameters.items() if k in self._runner.__annotations__})
# otherwise pass only parameters that are in command runner signature to prevent TypeError: got an unexpected keyword argument
return runner(**{k: v for k, v in parameters.items() if k in self._runner.__annotations__})
def __call__(self, *args, **kwargs) -> AwaitResponse:
# just syntactic sugar for command() instead of command.run()

View File

@ -33,7 +33,7 @@ class CommandsContext:
_task_group: TaskGroup
def __init__(self, task_group: TaskGroup, commands_manager: CommandsManager, dependency_manager: DependencyManager = default_dependency_manager):
assert isinstance(task_group, TaskGroup)
assert isinstance(task_group, TaskGroup), task_group
assert isinstance(commands_manager, CommandsManager)
assert isinstance(dependency_manager, DependencyManager)
self.commands_manager = commands_manager
@ -114,7 +114,10 @@ class CommandsContext:
while not self.is_stopped:
while self._response_queue:
self._process_response(self._response_queue.pop(0))
await anyio.sleep(1)
await anyio.sleep(0.1)
def stop(self):
self.is_stopped = True
def _process_response(self, response: Response):
if response is Response.repeat_last and self.last_response:

View File

@ -76,14 +76,7 @@ class CommandsManager:
error_msg = f'Command {self.name}.{runner.__name__} must have all parameters from pattern: {pattern.parameters=} {runner.__annotations__=}'
assert pattern.parameters.items() <= runner.__annotations__.items(), error_msg
if not inspect.iscoroutinefunction(runner):
async_runner = asyncer.asyncify(runner) # type: ignore
async_runner.__name__ = runner.__name__
async_runner.__annotations__ = runner.__annotations__
else:
async_runner = runner
cmd = Command(f'{self.name}.{runner.__name__}', pattern, async_runner) # type: ignore
cmd = Command(f'{self.name}.{runner.__name__}', pattern, runner)
if not hidden:
self.commands.append(cmd)

View File

@ -1,5 +1,8 @@
import pytest
from typing import AsyncGenerator
import time
import contextlib
import pytest
import asyncer
from general.dependencies import DependencyManager
from core import (
CommandsManager,
@ -45,114 +48,70 @@ class SpeechSynthesizerMock:
return result
@pytest.fixture
def commands_context_flow() -> tuple[CommandsManager, CommandsContext, CommandsContextDelegateMock]:
dependencies = DependencyManager()
manager = CommandsManager()
context = CommandsContext(manager, dependencies)
context_delegate = CommandsContextDelegateMock()
context.delegate = context_delegate
assert len(context_delegate.responses) == 0
assert len(context._context_queue) == 1
return manager, context, context_delegate
@pytest.fixture
def commands_context_flow_filled(commands_context_flow) -> tuple[CommandsContext, CommandsContextDelegateMock]:
manager, context, context_delegate = commands_context_flow
@manager.new('test')
def test():
return Response()
@manager.new('lorem * dolor')
def lorem():
return Response(text = 'Lorem!', voice = 'Lorem!')
@manager.new('hello', hidden = True)
def hello_context(**params):
voice = text = f'Hi, {params["name"]}!'
return Response(text = text, voice = voice)
@manager.new('bye', hidden = True)
def bye_context(name: Word, handler: ResponseHandler):
handler.pop_context()
return Response(
text = f'Bye, {name}!'
)
@manager.new('hello $name:Word')
def hello(name: Word):
text = voice = f'Hello, {name}!'
return Response(
text = text,
voice = voice,
commands = [hello_context, bye_context],
parameters = {'name': name}
)
@manager.new('repeat')
def repeat():
return Response.repeat_last
# background commands
@manager.new('background min')
@manager.background(Response(text = 'Starting background task', voice = 'Starting background task'))
def background():
text = voice = 'Finished background task'
return Response(text = text, voice = voice)
@manager.new('background multiple responses')
@manager.background(Response(text = 'Starting long background task'))
def background_multiple_responses(handler: ResponseHandler):
time.sleep(0.05)
handler.process_response(Response(text = 'First response'))
time.sleep(0.05)
handler.process_response(Response(text = 'Second response'))
time.sleep(0.05)
text = voice = 'Finished long background task'
return Response(text = text, voice = voice)
@manager.new('background needs input')
@manager.background(Response(text = 'Starting long background task'))
def background_needs_input(handler: ResponseHandler):
time.sleep(0.01)
for text in ['First response', 'Second response', 'Third response']:
handler.process_response(Response(text = text, voice = text))
async def commands_context_flow():
@contextlib.asynccontextmanager
async def _commands_context_flow() -> AsyncGenerator[tuple[CommandsManager, CommandsContext, CommandsContextDelegateMock], None]:
async with asyncer.create_task_group() as main_task_group:
dependencies = DependencyManager()
manager = CommandsManager()
context = CommandsContext(main_task_group, manager, dependencies)
context_delegate = CommandsContextDelegateMock()
context.delegate = context_delegate
text = 'Needs input'
handler.process_response(Response(text = text, voice = text, needs_user_input = True))
for text in ['Fourth response', 'Fifth response', 'Sixth response']:
handler.process_response(Response(text = text, voice = text))
text = voice = 'Finished long background task'
return Response(text = text, voice = voice)
@manager.new('background with context')
@manager.background(Response(text = 'Starting long background task', voice = 'Starting long background task'))
def background_multiple_contexts(handler: ResponseHandler):
time.sleep(0.01)
text = voice = 'Finished long background task'
return Response(text = text, voice = voice, commands = [hello_context, bye_context], parameters = {'name': 'John'})
@manager.new('background remove response')
@manager.background(Response(text = 'Starting long background task'))
def background_remove_response(handler: ResponseHandler):
time.sleep(0.01)
response = Response(text = 'Deleted response', voice = 'Deleted response')
handler.process_response(response)
time.sleep(0.05)
handler.remove_response(response)
return None
return context, context_delegate
assert len(context_delegate.responses) == 0
assert len(context._context_queue) == 1
main_task_group.soonify(context.handle_responses)()
yield (manager, context, context_delegate)
context.stop()
return _commands_context_flow
@pytest.fixture
def voice_assistant(commands_context_flow_filled):
async def commands_context_flow_filled(commands_context_flow):
@contextlib.asynccontextmanager
async def _commands_context_flow_filled() -> AsyncGenerator[tuple[CommandsContext, CommandsContextDelegateMock], None]:
async with commands_context_flow() as (manager, context, context_delegate):
@manager.new('test')
def test():
return Response()
@manager.new('lorem * dolor')
def lorem():
return Response(text = 'Lorem!', voice = 'Lorem!')
@manager.new('hello', hidden = True)
def hello_context(**params):
voice = text = f'Hi, {params["name"]}!'
return Response(text = text, voice = voice)
@manager.new('bye', hidden = True)
def bye_context(name: Word, handler: ResponseHandler):
handler.pop_context()
return Response(
text = f'Bye, {name}!'
)
@manager.new('hello $name:Word')
def hello(name: Word):
text = voice = f'Hello, {name}!'
return Response(
text = text,
voice = voice,
commands = [hello_context, bye_context],
parameters = {'name': name}
)
@manager.new('repeat')
def repeat():
return Response.repeat_last
yield (context, context_delegate)
return _commands_context_flow_filled
@pytest.fixture
async def voice_assistant(commands_context_flow_filled):
context, _ = commands_context_flow_filled
voice_assistant = VoiceAssistant(
speech_recognizer = SpeechRecognizerMock(),
@ -160,4 +119,5 @@ def voice_assistant(commands_context_flow_filled):
commands_context = context
)
voice_assistant.start()
return voice_assistant
yield voice_assistant

View File

@ -1,3 +1,8 @@
import pytest
pytestmark = pytest.mark.skip(reason = 'Background mode is deprecated')
def test_background_command(commands_context_flow_filled):
context, context_delegate = commands_context_flow_filled

View File

@ -2,7 +2,7 @@ import pytest
from core import CommandsManager, Response
@pytest.mark.anyio
@pytest.mark.asyncio
async def test_call_async_command_from_command():
manager = CommandsManager()

View File

@ -1,71 +1,81 @@
import pytest
import anyio
def test_basic_search(commands_context_flow_filled):
context, context_delegate = commands_context_flow_filled
assert len(context_delegate.responses) == 0
assert len(context._context_queue) == 1
context.process_string('lorem ipsum dolor')
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Lorem!'
assert len(context._context_queue) == 1
def test_second_context_layer(commands_context_flow_filled):
context, context_delegate = commands_context_flow_filled
context.process_string('hello world')
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'
assert len(context._context_queue) == 2
context_delegate.responses.clear()
context.process_string('hello')
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hi, world!'
assert len(context._context_queue) == 2
context_delegate.responses.clear()
def test_context_pop_on_not_found(commands_context_flow_filled):
context, context_delegate = commands_context_flow_filled
context.process_string('hello world')
assert len(context._context_queue) == 2
assert len(context_delegate.responses) == 1
context_delegate.responses.clear()
context.process_string('lorem ipsum dolor')
assert len(context._context_queue) == 1
assert len(context_delegate.responses) == 1
async def test_basic_search(commands_context_flow_filled, autojump_clock):
async with commands_context_flow_filled() as (context, context_delegate):
assert len(context_delegate.responses) == 0
assert len(context._context_queue) == 1
context.process_string('lorem ipsum dolor')
await anyio.sleep(5)
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Lorem!'
assert len(context._context_queue) == 1
def test_context_pop_context_response_action(commands_context_flow_filled):
context, context_delegate = commands_context_flow_filled
async def test_second_context_layer(commands_context_flow_filled, autojump_clock):
async with commands_context_flow_filled() as (context, context_delegate):
context.process_string('hello world')
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'
assert len(context._context_queue) == 2
context_delegate.responses.clear()
context.process_string('hello world')
await anyio.sleep(5)
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'
assert len(context._context_queue) == 2
context_delegate.responses.clear()
context.process_string('hello')
await anyio.sleep(5)
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hi, world!'
assert len(context._context_queue) == 2
context_delegate.responses.clear()
context.process_string('bye')
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Bye, world!'
assert len(context._context_queue) == 1
context_delegate.responses.clear()
async def test_context_pop_on_not_found(commands_context_flow_filled, autojump_clock):
async with commands_context_flow_filled() as (context, context_delegate):
context.process_string('hello')
assert len(context_delegate.responses) == 0
context.process_string('hello world')
await anyio.sleep(5)
assert len(context._context_queue) == 2
assert len(context_delegate.responses) == 1
context_delegate.responses.clear()
context.process_string('lorem ipsum dolor')
await anyio.sleep(5)
assert len(context._context_queue) == 1
assert len(context_delegate.responses) == 1
async def test_context_pop_context_response_action(commands_context_flow_filled, autojump_clock):
async with commands_context_flow_filled() as (context, context_delegate):
context.process_string('hello world')
await anyio.sleep(5)
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'
assert len(context._context_queue) == 2
context_delegate.responses.clear()
context.process_string('bye')
await anyio.sleep(5)
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Bye, world!'
assert len(context._context_queue) == 1
context_delegate.responses.clear()
context.process_string('hello')
await anyio.sleep(5)
assert len(context_delegate.responses) == 0
def test_repeat_last_answer_response_action(commands_context_flow_filled):
context, context_delegate = commands_context_flow_filled
async def test_repeat_last_answer_response_action(commands_context_flow_filled, autojump_clock):
async with commands_context_flow_filled() as (context, context_delegate):
context.process_string('hello world')
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'
context_delegate.responses.clear()
assert len(context_delegate.responses) == 0
context.process_string('repeat')
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'
context.process_string('hello world')
await anyio.sleep(5)
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'
context_delegate.responses.clear()
assert len(context_delegate.responses) == 0
context.process_string('repeat')
await anyio.sleep(5)
assert len(context_delegate.responses) == 1
assert context_delegate.responses[0].text == 'Hello, world!'

View File

@ -125,4 +125,3 @@ def test_objects_parse_caching(commands_context_flow):
assert Mock.parsing_counter == 1
manager.search('hello foobar 22')
assert Mock.parsing_counter == 2

View File

@ -1,7 +1,10 @@
import pytest
from datetime import timedelta
from voice_assistant import Mode
pytestmark = pytest.mark.skip(reason = 'Background mode is deprecated. TODO: test VA modes with new concurrency approach')
def test_background_command(voice_assistant):
voice_assistant.speech_recognizer_did_receive_final_result('background min')
@ -165,4 +168,4 @@ def test_background_waiting_remove_response(voice_assistant):
# interact to reset timeout mode, check that removed response is not repeated
voice_assistant.speech_recognizer_did_receive_final_result('hello world')
assert len(voice_assistant.speech_synthesizer.results) == 1
assert voice_assistant.speech_synthesizer.results.pop(0).text == 'Hello, world!'
assert voice_assistant.speech_synthesizer.results.pop(0).text == 'Hello, world!'