From 33f38024222fef3cff051fddb93cc4ac95888ac6 Mon Sep 17 00:00:00 2001 From: jpalanca Date: Sat, 30 Nov 2024 16:08:13 +0000 Subject: [PATCH 01/10] Update Python version and linting tool in CI workflow --- .github/workflows/python-package.yml | 46 ++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3024dca7..6725fb98 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -20,13 +20,13 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: - python-version: "3.10" + python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox virtualenv flake8 + pip install tox virtualenv ruff - name: Lint - run: "tox -e flake8" + run: "tox -e lint" test: runs-on: ${{ matrix.os }} @@ -37,14 +37,20 @@ jobs: - "ubuntu-py38" - "ubuntu-py39" - "ubuntu-py310" + - "ubuntu-py311" + - "ubuntu-py312" - "macos-py38" - "macos-py39" - "macos-py310" + - "macos-py311" + - "macos-py312" - "windows-py38" - "windows-py39" - "windows-py310" + - "windows-py311" + - "windows-py312" include: - name: "ubuntu-py38" @@ -59,6 +65,14 @@ jobs: python: "3.10" os: ubuntu-latest tox_env: "py310" + - name: "ubuntu-py311" + python: "3.11" + os: ubuntu-latest + tox_env: "py311" + - name: "ubuntu-py312" + python: "3.12" + os: ubuntu-latest + tox_env: "py312" - name: "macos-py38" python: "3.8" @@ -72,6 +86,14 @@ jobs: python: "3.10" os: macos-latest tox_env: "py310" + - name: "macos-py311" + python: "3.11" + os: macos-latest + tox_env: "py311" + - name: "macos-py312" + python: "3.12" + os: macos-latest + tox_env: "py312" - name: "windows-py38" python: "3.8" @@ -85,6 +107,14 @@ jobs: python: "3.10" os: windows-latest tox_env: "py310" + - name: "windows-py311" + python: "3.11" + os: windows-latest + tox_env: "py311" + - name: "windows-py312" + python: "3.12" + os: windows-latest + tox_env: "py312" steps: - uses: actions/checkout@v2 @@ -97,12 +127,10 @@ jobs: python -m pip install --upgrade pip python -m pip install -r requirements_dev.txt python -m pip install -r requirements.txt - - name: Lint with flake8 + - name: Lint with ruff run: | - # stop the build if there are Python syntax errors or undefined names - flake8 spade --count --ignore=E501 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 spade --count --exit-zero --max-complexity=10 --ignore=E501 --statistics + # ruff lint + ruff check spade - name: Test with pytest run: | pytest @@ -112,7 +140,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 43a71379e5306a63c5ff386bbce7e0a668c6afe8 Mon Sep 17 00:00:00 2001 From: jpalanca Date: Sat, 30 Nov 2024 16:10:54 +0000 Subject: [PATCH 02/10] Fix typo in coverage command by changing 'py.test' to 'pytest' --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6725fb98..3d41b095 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -155,7 +155,7 @@ jobs: - name: Run coverage run: | coverage erase - coverage run --source=spade -m py.test + coverage run --source=spade -m pytest - name: Coveralls uses: javipalanca/coveralls-python-action@develop with: From a936864867447eb8eacb6861fcf0c3489bd1e8f7 Mon Sep 17 00:00:00 2001 From: jpalanca Date: Sat, 30 Nov 2024 16:18:33 +0000 Subject: [PATCH 03/10] Refactor ruff configuration and improve event loop handling on Windows --- pyproject.toml | 6 +++--- spade/container.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 04b8c8e2..88763b09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,8 +44,8 @@ version = {attr = "spade.__version__"} dependencies = {file = "requirements.txt"} [tool.ruff] -select = ["E", "F"] -ignore = ["E501"] +lint.select = ["E", "F"] +lint.ignore = ["E501"] line-length = 88 target-version = "py38" @@ -54,5 +54,5 @@ quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["spade"] \ No newline at end of file diff --git a/spade/container.py b/spade/container.py index 946736d7..a581c598 100644 --- a/spade/container.py +++ b/spade/container.py @@ -1,6 +1,7 @@ import asyncio import logging import sys +import platform from contextlib import suppress from typing import Coroutine, Awaitable @@ -26,6 +27,9 @@ def get_or_create_eventloop(): # pragma: no cover loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() + if platform.system() == 'Windows': + # Force SelectorEventLoop on Windows + loop = asyncio.SelectorEventLoop() asyncio.set_event_loop(loop) return loop From c271cea04c261fb4f6cef86c50b58469fc61fdb8 Mon Sep 17 00:00:00 2001 From: jpalanca Date: Sat, 30 Nov 2024 21:42:46 +0000 Subject: [PATCH 04/10] Refactor CLI and event loop handling for improved readability and Windows compatibility --- spade/cli.py | 25 ++++++++++++++++--------- spade/container.py | 3 +-- tests/conftest.py | 10 ++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/spade/cli.py b/spade/cli.py index 3c58f8fb..c02b9e3c 100644 --- a/spade/cli.py +++ b/spade/cli.py @@ -4,7 +4,8 @@ import socket from pyjabber.server import Server -def check_port_in_use(port, host='0.0.0.0'): + +def check_port_in_use(port, host="0.0.0.0"): """Check if a port is in use""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: @@ -13,18 +14,20 @@ def check_port_in_use(port, host='0.0.0.0'): except socket.error: return True + def create_cli(): """Factory function to create the CLI""" + @click.group() def cli(): """SPADE command line tool""" pass @cli.command() - @click.option('--host', default='0.0.0.0', help='Server host') - @click.option('--client_port', default=5222, help='Client port') - @click.option('--server_port', default=5269, help='Server port') - @click.option('--debug', is_flag=True, default=False, help='Enables debug mode') + @click.option("--host", default="0.0.0.0", help="Server host") + @click.option("--client_port", default=5222, help="Client port") + @click.option("--server_port", default=5269, help="Server port") + @click.option("--debug", is_flag=True, default=False, help="Enables debug mode") def run(host, client_port, server_port, debug): """Launch an XMPP server""" if check_port_in_use(client_port, host): @@ -34,9 +37,12 @@ def run(host, client_port, server_port, debug): click.echo(f"Error: The port {server_port} is already in use.") sys.exit(1) - server = Server(host=host, client_port=client_port, - server_port=server_port, - connection_timeout=600) + server = Server( + host=host, + client_port=client_port, + server_port=server_port, + connection_timeout=600, + ) server.start(debug=debug) @cli.command() @@ -46,8 +52,9 @@ def version(): return cli + # Create a single instance of the CLI cli = create_cli() if __name__ == "__main__": - sys.exit(cli()) # pragma: no cover \ No newline at end of file + sys.exit(cli()) # pragma: no cover diff --git a/spade/container.py b/spade/container.py index a581c598..3bf5cb40 100644 --- a/spade/container.py +++ b/spade/container.py @@ -27,10 +27,9 @@ def get_or_create_eventloop(): # pragma: no cover loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() - if platform.system() == 'Windows': + if platform.system() == "Windows": # Force SelectorEventLoop on Windows loop = asyncio.SelectorEventLoop() - asyncio.set_event_loop(loop) return loop diff --git a/tests/conftest.py b/tests/conftest.py index 61cf7c8f..9eb3b802 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,10 @@ import asyncio +import platform import pytest from slixmpp import JID, Iq +from aiohttp.test_utils import TestClient +from aiohttp import web from spade.container import Container from spade.message import Message @@ -73,6 +76,13 @@ def cleanup(request): pass +@pytest.fixture(scope="module", autouse=True) +def setup_event_loop(): + if platform.system() == "Windows": + loop = asyncio.SelectorEventLoop() + asyncio.set_event_loop(loop) + + async def wait_for_behaviour_is_killed(behaviour, tries=500, sleep=0.01): counter = 0 while not behaviour.is_killed() and counter < tries: From 2ea0c9893411c9a9f32fa66be5d1092bb39778c7 Mon Sep 17 00:00:00 2001 From: jpalanca Date: Sat, 30 Nov 2024 21:51:12 +0000 Subject: [PATCH 05/10] Set Windows-specific event loop for compatibility in web server startup --- spade/web.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spade/web.py b/spade/web.py index 22e41d67..1f462672 100644 --- a/spade/web.py +++ b/spade/web.py @@ -2,6 +2,8 @@ import logging import socket from typing import Optional, Coroutine, Type +import platform +import asyncio import aiohttp_jinja2 import jinja2 @@ -34,6 +36,9 @@ async def start_server_in_loop(runner: AppRunner, hostname: str, port: int, agen port (int): port to listen from. agent (spade.agent.Agent): agent that owns the web app. """ + if platform.system() == "Windows": + loop = asyncio.SelectorEventLoop() + asyncio.set_event_loop(loop) await runner.setup() agent.web.server = aioweb.TCPSite(runner, hostname, port) await agent.web.server.start() From 86d42b2a5371feb2e9a3c595cb31007c7f7546e6 Mon Sep 17 00:00:00 2001 From: javipalanca Date: Mon, 2 Dec 2024 18:33:12 +0100 Subject: [PATCH 06/10] Refactor Makefile and setup for improved build process; update requirements and add event loop handling for Windows compatibility --- Makefile | 18 ++++++++++-------- pyproject.toml | 6 +++++- pytest.ini | 1 + requirements.txt | 6 ++++-- requirements_dev.txt | 7 ++++--- setup.py | 6 ++++++ spade/cli.py | 9 +++------ spade/container.py | 35 +++++++++++++++++++++++++++++------ tests/conftest.py | 33 ++++++++++++++++++++------------- 9 files changed, 82 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index 4c7dc529..59951712 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +# Variables +PYTHON = python +BUILD_DIR = dist + .PHONY: clean clean-test clean-pyc clean-build docs help .DEFAULT_GOAL := help define BROWSER_PYSCRIPT @@ -48,7 +52,7 @@ clean-test: ## remove test and coverage artifacts rm -fr htmlcov/ lint: ## check style with flake8 - flake8 spade tests + ruff check spade tests test: ## run tests quickly with the default Python pytest @@ -74,14 +78,12 @@ servedocs: docs ## compile the docs watching for changes watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . release: clean ## package and upload a release - python setup.py sdist - python setup.py bdist_wheel - twine upload dist/* + $(PYTHON) -m build + twine upload $(BUILD_DIR)/* dist: clean ## builds source and wheel package - python setup.py sdist - python setup.py bdist_wheel - ls -l dist + $(PYTHON) -m build + ls -l $(BUILD_DIR) install: clean ## install the package to the active Python's site-packages - python setup.py install \ No newline at end of file + $(PYTHON) -m pip install . \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 88763b09..439acf53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,9 @@ [build-system] -requires = ["setuptools>=61.0"] +requires = [ + "setuptools>=61.0", + "wheel", + "build" + ] build-backend = "setuptools.build_meta" [project] diff --git a/pytest.ini b/pytest.ini index 39baf331..9266cdb5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,3 @@ [pytest] testpaths = tests/ +asyncio_default_fixture_loop_scope = function \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5b065a95..784a0603 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ multidict==5.2.0 -pyasn1-modules==0.2.8 -pyasn1==0.4.8 +#pyasn1-modules==0.4.1 +#pyasn1==0.6.1 slixmpp==1.8.5 aiohttp==3.10.5 aiohttp_jinja2==1.6 @@ -10,6 +10,8 @@ timeago==1.0.16 singletonify==0.2.4 pytz==2022.1 pyjabber==0.1.6 +uvloop>=0.21.0; platform_system != "Windows" +winloop>=0.1.7; platform_system == "Windows" #cryptography==2.8 #3.2 diff --git a/requirements_dev.txt b/requirements_dev.txt index 8442196a..7c44cf98 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ +build==1.2.2.post1 bump2version==1.0.1 wheel==0.37.1 watchdog==2.1.9 @@ -7,14 +8,14 @@ coverage==7.2.1 Sphinx==5.1.0 twine==3.8.0 #4.0.1 pyyaml==6.0.2 -pytest==8.3.2 #7.0.1 #7.1.2 +pytest==8.3.4 #7.0.1 #7.1.2 pytest-runner==5.3.2 #6.0.0 -pytest-asyncio==0.16.0 #0.19.0 +pytest-asyncio==0.24.0 #0.19.0 pytest-cov==3.0.0 pytest-mock==3.6.1 #3.8.2 #pluggy==0.13.1 #<0.7,>=0.5 python-coveralls==2.9.3 -requests==2.27.1 #2.28.1 +requests==2.32.3 parsel==1.6.0 sphinx_rtd_theme==1.0.0 testfixtures==7.0.0 diff --git a/setup.py b/setup.py index af8e4d18..7d700cad 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """The setup script.""" +import sys from setuptools import setup, find_packages @@ -19,6 +20,11 @@ def parse_requirements(filename): requirements = parse_requirements("requirements.txt") +if sys.platform.startswith("win32"): + requirements.append("winloop>=0.1.7") +else: + requirements.append("uvloop>=0.21.0") + setup_requirements = [ 'pytest-runner', # put setup requirements (distutils extensions, etc.) here diff --git a/spade/cli.py b/spade/cli.py index c02b9e3c..30dece04 100644 --- a/spade/cli.py +++ b/spade/cli.py @@ -6,13 +6,10 @@ def check_port_in_use(port, host="0.0.0.0"): - """Check if a port is in use""" + """Checks if a port is in use""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - s.bind((host, port)) - return False - except socket.error: - return True + result = s.connect_ex((host, port)) + return result == 0 def create_cli(): diff --git a/spade/container.py b/spade/container.py index 3bf5cb40..4ab323fb 100644 --- a/spade/container.py +++ b/spade/container.py @@ -20,6 +20,25 @@ def get_or_create_eventloop(): # pragma: no cover + """ + Create or retrieve the appropriate event loop, using uvloop on Linux/macOS + and winloop on Windows if available. + """ + # Use uvloop or winloop if available + if platform.system() == "Windows": + try: + import winloop + asyncio.set_event_loop_policy(winloop.WindowsProactorEventLoopPolicy()) + except ImportError: + pass # winloop is not available, use default + else: + try: + import uvloop + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + except ImportError: + pass # uvloop is not available, use default + + # Create or retrieve the event loop if sys.version_info < (3, 10): loop = asyncio.get_event_loop() else: @@ -27,10 +46,7 @@ def get_or_create_eventloop(): # pragma: no cover loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() - if platform.system() == "Windows": - # Force SelectorEventLoop on Windows - loop = asyncio.SelectorEventLoop() - asyncio.set_event_loop(loop) + asyncio.set_event_loop(loop) return loop @@ -120,14 +136,21 @@ def run_container(main_func: Coroutine) -> None: # pragma: no cover except Exception as e: # pragma: no cover logger.error("Exception in the event loop: {}".format(e)) + # Cancel all pending tasks if sys.version_info >= (3, 7): # pragma: no cover - tasks = asyncio.all_tasks(loop=container.loop) # pragma: no cover + tasks = asyncio.all_tasks(loop=container.loop) else: - tasks = asyncio.Task.all_tasks(loop=container.loop) # pragma: no cover + tasks = asyncio.Task.all_tasks(loop=container.loop) + for task in tasks: task.cancel() with suppress(asyncio.CancelledError): container.run(task) + # Shutdown asynchronous generators + container.loop.run_until_complete(container.loop.shutdown_asyncgens()) + + # Close the event loop container.loop.close() logger.debug("Loop closed") + diff --git a/tests/conftest.py b/tests/conftest.py index 9eb3b802..0b27cfb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,30 +59,37 @@ def iq(): } return iq +@pytest.fixture(scope='session') +def loop(): + from spade.container import get_or_create_eventloop + loop = get_or_create_eventloop() + yield loop + loop.close() -@pytest.fixture(autouse=True) -def run_around_tests(): - # Code that will run before your test, for example: - # A test function will be run at this point + +@pytest.fixture(autouse=True, scope="function") +async def run_around_tests(loop): container = Container() + container.reset() if not container.is_running: container.__init__() + container.loop = loop yield - # Code that will run after your test, for example: - + # Cancel tasks and close loop after testing + tasks = asyncio.all_tasks(loop=container.loop) + for task in tasks: + task.cancel() + from contextlib import suppress + with suppress(asyncio.CancelledError): + #container.loop.run_until_complete(task) + await task + @pytest.fixture(scope="module", autouse=True) def cleanup(request): pass -@pytest.fixture(scope="module", autouse=True) -def setup_event_loop(): - if platform.system() == "Windows": - loop = asyncio.SelectorEventLoop() - asyncio.set_event_loop(loop) - - async def wait_for_behaviour_is_killed(behaviour, tries=500, sleep=0.01): counter = 0 while not behaviour.is_killed() and counter < tries: From 2757b980b53c960cff5e37e118c5f9aa32e41e34 Mon Sep 17 00:00:00 2001 From: jpalanca Date: Mon, 2 Dec 2024 18:53:05 +0000 Subject: [PATCH 07/10] Update Windows event loop policy to use winloop.EventLoopPolicy for improved compatibility --- requirements_dev.txt | 2 +- spade/container.py | 2 +- tests/conftest.py | 3 --- tests/test_behaviour.py | 8 ++++---- tests/test_message.py | 4 ++-- tests/test_presence.py | 1 - tests/test_web.py | 4 ++-- 7 files changed, 10 insertions(+), 14 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 7c44cf98..33e841fa 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -21,5 +21,5 @@ sphinx_rtd_theme==1.0.0 testfixtures==7.0.0 pytest-aiohttp>=0.3.0 #1.0.4 factory-boy==3.2.1 -click==8.0.3 +click==8.1.7 #docutils==0.12 diff --git a/spade/container.py b/spade/container.py index 4ab323fb..10bcade3 100644 --- a/spade/container.py +++ b/spade/container.py @@ -28,7 +28,7 @@ def get_or_create_eventloop(): # pragma: no cover if platform.system() == "Windows": try: import winloop - asyncio.set_event_loop_policy(winloop.WindowsProactorEventLoopPolicy()) + asyncio.set_event_loop_policy(winloop.EventLoopPolicy()) except ImportError: pass # winloop is not available, use default else: diff --git a/tests/conftest.py b/tests/conftest.py index 0b27cfb6..9c203d26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,7 @@ import asyncio -import platform import pytest from slixmpp import JID, Iq -from aiohttp.test_utils import TestClient -from aiohttp import web from spade.container import Container from spade.message import Message diff --git a/tests/test_behaviour.py b/tests/test_behaviour.py index dad9a2af..0783a613 100644 --- a/tests/test_behaviour.py +++ b/tests/test_behaviour.py @@ -1,6 +1,6 @@ import asyncio import datetime -from unittest.mock import AsyncMock, Mock, MagicMock +from unittest.mock import AsyncMock, Mock import pytest @@ -102,7 +102,7 @@ async def on_end(self): async def test_on_start_exception(): class TestOneShotBehaviour(OneShotBehaviour): async def on_start(self): - result = 1 / 0 + 1 / 0 self.agent.flag = True async def run(self): @@ -125,7 +125,7 @@ async def run(self): async def test_on_run_exception(): class TestOneShotBehaviour(OneShotBehaviour): async def run(self): - result = 1 / 0 + 1 / 0 self.agent.flag = True agent = MockedAgentFactory() @@ -148,7 +148,7 @@ async def run(self): pass async def on_end(self): - result = 1 / 0 + 1 / 0 self.agent.flag = True agent = MockedAgentFactory() diff --git a/tests/test_message.py b/tests/test_message.py index 095e78d1..17cfbfec 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -43,7 +43,7 @@ def test_make_reply(message): def test_message_from_node_attribute_error(): - with pytest.raises(AttributeError) as e: + with pytest.raises(AttributeError): Message.from_node(Message()) @@ -113,7 +113,7 @@ def test_not_equal(message, message2): def test_id(message): - assert type(message.id) == int + assert isinstance(message.id, int) def test_metadata_is_string(): diff --git a/tests/test_presence.py b/tests/test_presence.py index 2847c8ed..bcb9cf7b 100644 --- a/tests/test_presence.py +++ b/tests/test_presence.py @@ -1,7 +1,6 @@ from unittest.mock import Mock import pytest from slixmpp.stanza import Presence, Iq -from slixmpp.xmlstream import ET from slixmpp import JID from spade.presence import ContactNotFound, PresenceShow, PresenceType, Contact diff --git a/tests/test_web.py b/tests/test_web.py index 8ca60111..63201d2c 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,5 +1,5 @@ import asyncio -from unittest.mock import AsyncMock, Mock, MagicMock +from unittest.mock import Mock import requests from aiohttp import web @@ -246,7 +246,7 @@ async def test_unsubscribe_agent(aiohttp_client, jid): response = await client.get(f"/spade/agent/unsubscribe/{jid}/") - assert str(response.url.relative()) == f"/spade" + assert str(response.url.relative()) == "/spade" assert agent.client.send_presence.mock_calls arg = agent.client.send_presence.call_args[1] From 807cfc8b5b4790e31fba2374abaf6ee670db303c Mon Sep 17 00:00:00 2001 From: javipalanca Date: Thu, 5 Dec 2024 16:43:59 +0100 Subject: [PATCH 08/10] Enhance event loop handling and improve test configuration. Refactor Makefile and update dependencies --- Makefile | 10 +++++----- pytest.ini | 3 ++- requirements_dev.txt | 4 ++-- spade/agent.py | 7 +++++-- spade/container.py | 3 ++- spade/web.py | 5 ----- tests/conftest.py | 42 ++++++++++++++++++------------------------ tests/test_web.py | 2 +- tox.ini | 2 +- 9 files changed, 36 insertions(+), 42 deletions(-) diff --git a/Makefile b/Makefile index 59951712..75f82695 100644 --- a/Makefile +++ b/Makefile @@ -38,13 +38,13 @@ clean-build: ## remove build artifacts rm -fr dist/ rm -fr .eggs/ find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + + find . -name '*.egg' -exec rm -fr {} + clean-pyc: ## remove Python file artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + + find . -name '*.pyc' -not -path "./.tox/*" -exec rm -f {} + + find . -name '*.pyo' -not -path "./.tox/*" -exec rm -f {} + + find . -name '*~' -not -path "./.tox/*" -exec rm -f {} + + find . -name '__pycache__' -not -path "./.tox/*" -exec rm -fr {} + clean-test: ## remove test and coverage artifacts rm -fr .tox/ diff --git a/pytest.ini b/pytest.ini index 9266cdb5..99bc8a65 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] testpaths = tests/ -asyncio_default_fixture_loop_scope = function \ No newline at end of file +asyncio_default_fixture_loop_scope = function +asyncio_mode = auto \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index 33e841fa..eedd7486 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -16,10 +16,10 @@ pytest-mock==3.6.1 #3.8.2 #pluggy==0.13.1 #<0.7,>=0.5 python-coveralls==2.9.3 requests==2.32.3 -parsel==1.6.0 +parsel==1.9.1 sphinx_rtd_theme==1.0.0 testfixtures==7.0.0 -pytest-aiohttp>=0.3.0 #1.0.4 +pytest-aiohttp>=1.0.5 factory-boy==3.2.1 click==8.1.7 #docutils==0.12 diff --git a/spade/agent.py b/spade/agent.py index d6536237..9519b1f1 100644 --- a/spade/agent.py +++ b/spade/agent.py @@ -34,7 +34,9 @@ class DisconnectedException(Exception): class Agent(object): - def __init__(self, jid: str, password: str, verify_security: bool = False): + def __init__( + self, jid: str, password: str, port: int = 5222, verify_security: bool = False + ): """ Creates an agent @@ -45,6 +47,7 @@ def __init__(self, jid: str, password: str, verify_security: bool = False): """ self.jid = JID(jid) self.password = password + self.xmpp_port = port self.verify_security = verify_security self.behaviours = [] @@ -166,7 +169,7 @@ async def _async_connect(self) -> None: # pragma: no cover ) self.client.add_event_handler("message", self._message_received) - self.client.connect() + self.client.connect(self.jid.host, self.xmpp_port) done, pending = await asyncio.wait( [connected_task, disconnected_task, failed_auth_task], diff --git a/spade/container.py b/spade/container.py index 10bcade3..80eef73a 100644 --- a/spade/container.py +++ b/spade/container.py @@ -28,12 +28,14 @@ def get_or_create_eventloop(): # pragma: no cover if platform.system() == "Windows": try: import winloop + asyncio.set_event_loop_policy(winloop.EventLoopPolicy()) except ImportError: pass # winloop is not available, use default else: try: import uvloop + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except ImportError: pass # uvloop is not available, use default @@ -153,4 +155,3 @@ def run_container(main_func: Coroutine) -> None: # pragma: no cover # Close the event loop container.loop.close() logger.debug("Loop closed") - diff --git a/spade/web.py b/spade/web.py index 1f462672..22e41d67 100644 --- a/spade/web.py +++ b/spade/web.py @@ -2,8 +2,6 @@ import logging import socket from typing import Optional, Coroutine, Type -import platform -import asyncio import aiohttp_jinja2 import jinja2 @@ -36,9 +34,6 @@ async def start_server_in_loop(runner: AppRunner, hostname: str, port: int, agen port (int): port to listen from. agent (spade.agent.Agent): agent that owns the web app. """ - if platform.system() == "Windows": - loop = asyncio.SelectorEventLoop() - asyncio.set_event_loop(loop) await runner.setup() agent.web.server = aioweb.TCPSite(runner, hostname, port) await agent.web.server.start() diff --git a/tests/conftest.py b/tests/conftest.py index 9c203d26..a2a5ab18 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,9 @@ from spade.message import Message +pytest_plugins = 'pytest_asyncio' + + @pytest.fixture def jid(): return JID("friend@localhost/home") @@ -56,30 +59,21 @@ def iq(): } return iq -@pytest.fixture(scope='session') -def loop(): - from spade.container import get_or_create_eventloop - loop = get_or_create_eventloop() - yield loop - loop.close() - - -@pytest.fixture(autouse=True, scope="function") -async def run_around_tests(loop): - container = Container() - container.reset() - if not container.is_running: - container.__init__() - container.loop = loop - yield - # Cancel tasks and close loop after testing - tasks = asyncio.all_tasks(loop=container.loop) - for task in tasks: - task.cancel() - from contextlib import suppress - with suppress(asyncio.CancelledError): - #container.loop.run_until_complete(task) - await task +# @pytest.fixture(autouse=True, scope="function") +# async def run_around_tests(event_loop): +# container = Container() +# container.reset() +# if not container.is_running: +# container.__init__() +# container.loop = event_loop +# yield +# # Cancel tasks and close loop after testing +# tasks = asyncio.all_tasks(loop=container.loop) +# for task in tasks: +# task.cancel() +# from contextlib import suppress +# with suppress(asyncio.CancelledError): +# await task @pytest.fixture(scope="module", autouse=True) diff --git a/tests/test_web.py b/tests/test_web.py index 63201d2c..1c8e291f 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -100,7 +100,7 @@ async def test_add_template_path(): await agent.stop() -async def test_check_server(aiohttp_client, loop): +async def test_check_server(aiohttp_client): agent = MockedPresenceAgentFactory() await agent.start(auto_register=False) diff --git a/tox.ini b/tox.ini index aa12749c..febfcd67 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = docs, py38, py39, py310, py311, py312, flake8 +envlist = docs, py38, py39, py310, py311, py312, lint isolated_build = false [testenv:lint] From 837202bcf57b0fc3c571e059e7ebc3f6c528f0ec Mon Sep 17 00:00:00 2001 From: javipalanca Date: Thu, 5 Dec 2024 16:51:05 +0100 Subject: [PATCH 09/10] =?UTF-8?q?Bump=20version:=204.0.0rc1=20=E2=86=92=20?= =?UTF-8?q?4.0.0rc2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- spade/__init__.py | 2 +- spade/templates/internal_tpl_base.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 81860a6a..27461c3c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.0.0rc1 +current_version = 4.0.0rc2 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/setup.py b/setup.py index 7d700cad..46d9fb81 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def parse_requirements(filename): setup( name='spade', - version='4.0.0rc1', + version='4.0.0rc2', description="Smart Python Agent Development Environment", long_description=readme + '\n\n' + history, author="Javi Palanca", diff --git a/spade/__init__.py b/spade/__init__.py index e8884656..5788b504 100644 --- a/spade/__init__.py +++ b/spade/__init__.py @@ -12,7 +12,7 @@ __author__ = """Javi Palanca""" __email__ = "jpalanca@gmail.com" -__version__ = "4.0.0rc1" +__version__ = "4.0.0rc2" __all__ = ["agent", "behaviour", "message", "template"] diff --git a/spade/templates/internal_tpl_base.html b/spade/templates/internal_tpl_base.html index eb998d52..9d6c244b 100644 --- a/spade/templates/internal_tpl_base.html +++ b/spade/templates/internal_tpl_base.html @@ -302,7 +302,7 @@

Agent Friends

Copyright © {% now 'local', '%Y' %} SPADE.
From 40bb387be115fc1bddb78dcf0750a8895485c391 Mon Sep 17 00:00:00 2001 From: javipalanca Date: Thu, 5 Dec 2024 16:57:14 +0100 Subject: [PATCH 10/10] Preparing release 4.0.0rc2 --- HISTORY.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5711d157..0b1a05ce 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,13 +3,15 @@ History ======= -4.0.0rc (2024-11-28) +4.0.0 (2024-12-05) -------------------- -* Added XMPP server as cli. +* Added custom XMPP server as cli. * Migrated to pyproject. -* Migrated to ruff. +* Migrated linter to ruff. * Improved presence management API. +* Agents can now select the XMPP port. +* Use uvloop or winloop if available. 3.3.3 (2024-09-05) ------------------