Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.13']
python-version: ['3.9', '3.14']
database-backend: [psql]

services:
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:
- name: Install aiida-core
uses: ./.github/actions/install-aiida-core
with:
python-version: '3.13'
python-version: '3.14'

- name: Setup SSH on localhost
run: .github/workflows/setup_ssh.sh
Expand All @@ -159,7 +159,7 @@ jobs:
- name: Install aiida-core
uses: ./.github/actions/install-aiida-core
with:
python-version: '3.13'
python-version: '3.14'
from-lock: 'true'
extras: ''

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
fail-fast: false
matrix:

python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']

# Not being able to install with conda on a specific Python version is
# not sufficient to fail the run, but something we want to be aware of.
Expand Down
5 changes: 3 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dependencies:
- python~=3.9
- alembic~=1.8
- archive-path~=0.4.2
- asyncssh~=2.19.0
- asyncssh~=2.21.0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bump is needed as 2.20.0 version had some Python 3.14 fixes

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- circus~=0.19.0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened a PR with tests for 3.14
circus-tent/circus#1233

- click-spinner~=0.1.8
- click<8.3,>=8.1.0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw: get-annotations dependency can be dropped once we drop support for Python 3.9

https://pypi.org/project/get-annotations/

Expand All @@ -20,7 +20,7 @@ dependencies:
- ipython>=7.6
- jedi<0.19
- jinja2~=3.0
- kiwipy[rmq]~=0.8.4
- kiwipy[rmq]>=0.8.4
- importlib-metadata~=6.0

@danielhollas danielhollas Nov 5, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw: importlib-metadata can be dropped once we drop 3.9 in favour of stdlib importlib.metadata, see comment in: aiida/plugins/entry_point.py

- numpy<3,>=1.21
- paramiko~=3.0
Expand All @@ -38,3 +38,4 @@ dependencies:
- upf_to_json~=0.9.2

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tabulate seems to work with 3.14
astanin/python-tabulate#372

- wrapt~=1.11

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapt just had a 2.0 release: https://wrapt.readthedocs.io/en/latest/changes.html#version-2-0-0

Looking at the changelog it seems that it should be safe to upgrade (we're only using wrapt.decorator).
We could also choose to support both 1.x and 2.x, as there might be aiida plugins that depend on wrapt as well.

Possibly the biggest reason to update is that the package is now typed. :-)

- chardet~=5.2.0
- nest-asyncio
20 changes: 13 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,19 @@ classifiers = [
dependencies = [
'alembic~=1.8',
'archive-path~=0.4.2',
"asyncssh~=2.19.0",
"asyncssh~=2.21.0",
'circus~=0.19.0',
'click-spinner~=0.1.8',
'click>=8.1.0,<8.3',
'disk-objectstore~=1.4.0',
'docstring-parser',
'get-annotations~=0.1;python_version<"3.10"',
'graphviz~=0.19',
'plumpy~=0.25.0',
"plumpy~=0.25.0",
'ipython>=7.6',
'jedi<0.19',
'jinja2~=3.0',
'kiwipy[rmq]~=0.8.4',
"kiwipy[rmq]>=0.8.4",
'importlib-metadata~=6.0',
'numpy>=1.21,<3',
'paramiko~=3.0',
Expand All @@ -60,7 +60,8 @@ dependencies = [
'typing-extensions~=4.1',
'upf_to_json~=0.9.2',
'wrapt~=1.11',
'chardet~=5.2.0;platform_system=="Windows"'
'chardet~=5.2.0;platform_system=="Windows"',
"nest-asyncio"
]
description = 'AiiDA is a workflow manager for computational science with a strong focus on provenance, performance and extensibility.'
dynamic = ['version'] # read from aiida/__init__.py
Expand Down Expand Up @@ -280,7 +281,7 @@ tests = [
'pytest-asyncio~=0.12,<0.17',
'pytest-timeout~=2.0',
'pytest-cov~=7.0',
'pytest-rerunfailures~=12.0',
'pytest-rerunfailures~=16.0',
'pytest-benchmark~=4.0',
'pytest-regressions~=2.2',
'pytest-xdist~=3.6',
Expand Down Expand Up @@ -411,8 +412,9 @@ filterwarnings = [
'ignore:The `aiida.orm.nodes.data.upf` module is deprecated.*:aiida.common.warnings.AiidaDeprecationWarning',
'ignore:The `Code` class is deprecated.*:aiida.common.warnings.AiidaDeprecationWarning',
# https://github.com/aiidateam/plumpy/issues/283
'ignore:There is no current event loop:DeprecationWarning:plumpy',
'ignore:There is no current event loop:DeprecationWarning:nest_asyncio',
"ignore:'asyncio.[sg]et_event_loop_policy' is deprecated:DeprecationWarning",
"ignore:'asyncio.DefaultEventLoopPolicy' is deprecated:DeprecationWarning",
"ignore:'asyncio.iscoroutinefunction' is deprecated:DeprecationWarning:pytest_asyncio",
# spglib deprecation
'ignore:dict interface is deprecated:DeprecationWarning',
# https://github.com/aiidateam/archive-path/issues/21
Expand Down Expand Up @@ -579,3 +581,7 @@ commands = molecule {posargs:test}
# .github/actions/install-aiida-core/action.yml
# .readthedocs.yml
required-version = ">=0.8.4"

[tool.uv.sources]
nest-asyncio = {git = "https://github.com/danielhollas/nest_asyncio", branch = "python-3.14"}
plumpy = {git = "https://github.com/danielhollas/plumpy", rev = "06d6f3c32ee4e7f7dafcba0bd507401f349184c6"}
12 changes: 10 additions & 2 deletions src/aiida/engine/processes/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ def __init__(
from .process import ProcessState

# create future in specified event loop
loop = loop if loop is not None else asyncio.get_event_loop()
if not loop:
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()

super().__init__(loop=loop)

assert not (poll_interval is None and communicator is None), 'Must poll or have a communicator to use'
Expand All @@ -70,7 +75,10 @@ def _subscriber(*args, **kwargs):

# Start polling
if poll_interval is not None:
loop.create_task(self._poll_process(node, poll_interval))
# TODO: Write a test with gc.collect to see if the assignment is needed?
# (per RUF006)
# self._task = loop.create_task(self._poll_process(node, poll_interval))
loop.create_task(self._poll_process(node, poll_interval)) # noqa: RUF006

def cleanup(self) -> None:
"""Clean up the future by removing broadcast subscribers from the communicator if it still exists."""
Expand Down
9 changes: 8 additions & 1 deletion src/aiida/engine/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,14 @@ def __init__(
), 'Must supply a persister if you want to submit using communicator'

set_event_loop_policy()
self._loop = loop if loop is not None else asyncio.get_event_loop()
if loop is not None:
self._loop = loop
else:
try:
self._loop = asyncio.get_event_loop()
except RuntimeError:
self._loop = asyncio.new_event_loop()

self._poll_interval = poll_interval
self._broker_submit = broker_submit
self._transport = transports.TransportQueue(self._loop)
Expand Down
9 changes: 8 additions & 1 deletion src/aiida/engine/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ class TransportQueue:

def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None):
""":param loop: An asyncio event, will use `asyncio.get_event_loop()` if not supplied"""
self._loop = loop if loop is not None else asyncio.get_event_loop()

if loop is not None:
self._loop = loop
else:
try:
self._loop = asyncio.get_event_loop()
except RuntimeError:
self._loop = asyncio.new_event_loop()
self._transport_requests: Dict[Hashable, TransportRequest] = {}

@property
Expand Down
17 changes: 13 additions & 4 deletions src/aiida/engine/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ def interruptable_task(
:param loop: the event loop in which to run the coroutine, by default uses asyncio.get_event_loop()
:return: an InterruptableFuture
"""
loop = loop or asyncio.get_event_loop()
try:
loop = loop or asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()

future = InterruptableFuture()

async def execute_coroutine():
Expand All @@ -151,7 +155,9 @@ async def execute_coroutine():
if not future.done():
future.set_result(result)

loop.create_task(execute_coroutine())
# TODO: Store the task somewhere?
# See `ruff rule RUF006`
loop.create_task(execute_coroutine()) # noqa: RUF006

return future

Expand All @@ -164,7 +170,7 @@ def ensure_coroutine(fct: Callable[..., Any]) -> Callable[..., Awaitable[Any]]:
:param fct: the function
:returns: the coroutine
"""
if asyncio.iscoroutinefunction(fct):
if inspect.iscoroutinefunction(fct):
return fct

async def wrapper(*args, **kwargs):
Expand Down Expand Up @@ -252,7 +258,10 @@ def loop_scope(loop) -> Iterator[None]:

:param loop: The event loop to make current for the duration of the scope
"""
current = asyncio.get_event_loop()
try:
current = asyncio.get_event_loop()
except RuntimeError:
current = None

try:
asyncio.set_event_loop(loop)
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ def event_loop(manager, aiida_profile_clean):

This is automatically called as a fixture for any test marked with ``@pytest.mark.asyncio``.
"""
assert manager.get_runner().loop is not None
yield manager.get_runner().loop


Expand Down
13 changes: 10 additions & 3 deletions tests/engine/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class TestJobManager:
@pytest.fixture(autouse=True)
def init_profile(self, aiida_localhost):
"""Initialize the profile."""
self.loop = asyncio.get_event_loop()
try:
self.loop = asyncio.get_event_loop()
except RuntimeError:
self.loop = asyncio.new_event_loop()
self.transport_queue = TransportQueue(self.loop)
self.user = User.collection.get_default()
self.computer = aiida_localhost
Expand All @@ -39,7 +42,8 @@ def test_get_jobs_list(self):
# Calling the method again, should return the exact same instance of `JobsList`
assert self.manager.get_jobs_list(self.auth_info) == jobs_list

def test_request_job_info_update(self):
@pytest.mark.asyncio
async def test_request_job_info_update(self):
"""Test the `JobManager.request_job_info_update` method."""
with self.manager.request_job_info_update(self.auth_info, job_id=1) as request:
assert isinstance(request, asyncio.Future)
Expand All @@ -54,7 +58,10 @@ class TestJobsList:
@pytest.fixture(autouse=True)
def init_profile(self, aiida_localhost):
"""Initialize the profile."""
self.loop = asyncio.get_event_loop()
try:
self.loop = asyncio.get_event_loop()
except RuntimeError:
self.loop = asyncio.new_event_loop()
self.transport_queue = TransportQueue(self.loop)
self.user = User.collection.get_default()
self.computer = aiida_localhost
Expand Down
2 changes: 0 additions & 2 deletions tests/engine/test_process_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
CUSTOM_LABEL = 'Custom label'
CUSTOM_DESCRIPTION = 'Custom description'

pytest.mark.requires_rmq


@workfunction
def function_return_input(data):
Expand Down
7 changes: 5 additions & 2 deletions tests/engine/test_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
def runner():
"""Construct and return a `Runner`."""
loop = asyncio.new_event_loop()
return get_manager().create_runner(poll_interval=0.5, loop=loop)
runner = get_manager().create_runner(poll_interval=0.5, loop=loop)
yield runner
if not runner.is_closed():
runner.close()
loop.close()


class Proc(Process):
Expand All @@ -46,7 +50,6 @@ def the_hans_klok_comeback(loop):
loop.stop()


@pytest.mark.requires_rmq
def test_call_on_process_finish(runner):
"""Test call on calculation finish."""
loop = runner.loop
Expand Down
1 change: 1 addition & 0 deletions tests/engine/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ async def nested(queue, authinfo):

loop.run_until_complete(nested(transport_queue, self.authinfo))

@pytest.mark.asyncio
def test_get_transport_interleaved(self):
"""Test interleaved calls to get the same transport."""
transport_queue = TransportQueue()
Expand Down
Loading
Loading