Skip to content

Add support for Python 3.14 - #7240

Merged
agoscinski merged 4 commits into
aiidateam:mainfrom
agoscinski:support-py3.14
Mar 4, 2026
Merged

Add support for Python 3.14#7240
agoscinski merged 4 commits into
aiidateam:mainfrom
agoscinski:support-py3.14

Conversation

@agoscinski

@agoscinski agoscinski commented Feb 27, 2026

Copy link
Copy Markdown
Collaborator

Split from PR #7233.

Each required change is explained in the commit message. I tried to not introduce changes in the behavior of the functionalities but to continue to use the old behavior for py3.14. For example I copied the old url2pathname function from python that was changed in py3.14.

I marked the PR number in the commit message so we can identify that all commits are related without doing a merge commit.

After this PR is merged we need to update the branch protection rules to switch from 3.13 to 3.14.

@codecov

codecov Bot commented Feb 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.74%. Comparing base (be0d6b5) to head (3e6e2a3).
⚠️ Report is 98 commits behind head on main.

Files with missing lines Patch % Lines
src/aiida/common/utils.py 75.00% 3 Missing ⚠️
src/aiida/manage/configuration/profile.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7240      +/-   ##
==========================================
- Coverage   79.74%   79.74%   -0.00%     
==========================================
  Files         566      566              
  Lines       43882    43895      +13     
==========================================
+ Hits        34990    34998       +8     
- Misses       8892     8897       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@danielhollas

Copy link
Copy Markdown
Collaborator

btw: my previous attempt is at #7074

agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Feb 28, 2026
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Feb 28, 2026
asyncssh 2.19.0 uses a custom `Record` class (in `misc.py`) that
breaks on Python 3.14. This causes `AttributeError` on any object
built on `Record`, including `SFTPLimits` (missing `max_packet_len`)
and `SSHCompletedProcess` (missing `exit_status`, `stdout`, etc.),
effectively breaking all `core.ssh_async` transport operations.

The fix (migrating from `Record` to Python dataclasses) landed in
asyncssh 2.20.0, as tracked in ronf/asyncssh#732. We pin to
~=2.21.0 rather than ~=2.22.0 because asyncssh 2.22.0 dropped
support for Python 3.9, which this project still supports.
@agoscinski
agoscinski force-pushed the support-py3.14 branch 4 times, most recently from b4ca165 to 6405e1e Compare March 1, 2026 13:56
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 1, 2026
The orm/test_fields.py test relies on the inbuilt `repr` function of
python to compare the orm fields. With python 3.14 the formatting of
`repr` changes. To avoid maintaining two sets of referenece files with
different formatting we change the reference files to the formatting of
3.14 and run the tests only for 3.14 and above.
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 1, 2026
…aiidateam#7240)

In Python 3.14, `url2pathname` was rewritten to be a full file-URL
parser per RFC 8089 (python/cpython#126838).
The new implementation prepends `file:` to the input and re-parses it
with `urlsplit`, then validates that the URL authority resolves to
localhost.

This breaks our usage because we pass `urlparse(uri).path` — a pre-parsed
path component — not a full URL. The new `url2pathname` misinterprets
this input, and raises:

    URLError: file:// scheme is supported only on localhost

Since we already validate that the scheme is `file://` before extracting
the path, we replace the stdlib import with a local `url2pathname` shim
that preserves the simple Python <= 3.13 behavior.
@agoscinski
agoscinski marked this pull request as ready for review March 1, 2026 14:18
@agoscinski
agoscinski requested a review from unkcpz as a code owner March 1, 2026 14:18
@agoscinski
agoscinski requested a review from unkcpz as a code owner March 1, 2026 14:18
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 1, 2026
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 1, 2026
asyncssh 2.19.0 uses a custom `Record` class (in `misc.py`) that
breaks on Python 3.14. This causes `AttributeError` on any object
built on `Record`, including `SFTPLimits` (missing `max_packet_len`)
and `SSHCompletedProcess` (missing `exit_status`, `stdout`, etc.),
effectively breaking all `core.ssh_async` transport operations.

The fix (migrating from `Record` to Python dataclasses) landed in
asyncssh 2.20.0, as tracked in ronf/asyncssh#732. We pin to
~=2.21.0 rather than ~=2.22.0 because asyncssh 2.22.0 dropped
support for Python 3.9, which this project still supports.
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 1, 2026
The orm/test_fields.py test relies on the inbuilt `repr` function of
python to compare the orm fields. With python 3.14 the formatting of
`repr` changes. To avoid maintaining two sets of referenece files with
different formatting we change the reference files to the formatting of
3.14 and run the tests only for 3.14 and above.
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 1, 2026
…aiidateam#7240)

In Python 3.14, `url2pathname` was rewritten to be a full file-URL
parser per RFC 8089 (python/cpython#126838).
The new implementation prepends `file:` to the input and re-parses it
with `urlsplit`, then validates that the URL authority resolves to
localhost.

This breaks our usage because we pass `urlparse(uri).path` — a pre-parsed
path component — not a full URL. The new `url2pathname` misinterprets
this input, and raises:

    URLError: file:// scheme is supported only on localhost

Since we already validate that the scheme is `file://` before extracting
the path, we replace the stdlib import with a local `url2pathname` shim
that preserves the simple Python <= 3.13 behavior.
@agoscinski

Copy link
Copy Markdown
Collaborator Author

Test coverage complains because I touch a deprecated method in src/aiida/manage/configuration/profile.py which is not tested. It does not make sense to me to add test to a deprecated method in this PR just to make patch coverage happy.

Comment thread pyproject.toml
'alembic~=1.8',
'archive-path~=0.4.2',
"asyncssh~=2.19.0",
"asyncssh~=2.21.0",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd go for 2.22.0
where they have fix an internal race condition

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ok I see. 2.22.0 has dropped 3.9..

Comment thread tests/orm/test_fields.py Outdated
# generated with. Rather than maintaining two sets of reference files, we skip on
# Python versions that don't match. Once 3.14 becomes the baseline, regenerate the
# reference files and update this marker.
skip_below_py314 = pytest.mark.skipif(sys.version_info < (3, 14), reason='typing repr fixtures require Python 3.14+')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

agreed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would be good to get @edan-bainglass eyes on these changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, I'd have to test this. The fields system constructs from the model system. Pydantic field typing changes between Python versions if I recall correctly. At some point, one couldn't use int | None without an additional dependency, instead having to use Optional[int]. Anyhow, the commit message of the relevant commit here mentions that repr changed in 3.14. But I think this would be the same result if we uniformly update all field annotations to the newer style, i.e. use pipes.

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.

Okay then we can try to drop the skipif once we we drop 3.9. I note this into the PR that drops 3.9

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.

Noted in PR #7244

@khsrali

khsrali commented Mar 2, 2026

Copy link
Copy Markdown
Collaborator

| operator is said to be slightly more efficient than Optional in 3.14
So I recommend we migrate also those also in the right direction..

Comment thread environment.yml
Comment thread src/aiida/common/utils.py
# https://github.com/python/cpython/blob/1a2b0fb3e5eac4e767e6bbb0b2c3cedaedafc07b/Lib/urllib/request.py#L1664-L1679
# Only minor changes were applied to conform with mypy
if os.name == 'nt':
from nturl2path import url2pathname

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This still has the same behavior as before?

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.

So this is a 1to1 copy from as from py3.13, where this function seemed to still have worked in profile code. The if case is probably not needed because it is for Window NT but i just kept it to be sure to have the same behavior.

@GeigerJ2

GeigerJ2 commented Mar 2, 2026

Copy link
Copy Markdown
Collaborator

| operator is said to be slightly more efficient than Optional in 3.14
So I recommend we migrate also those also in the right direction..

Honestly, I'd do that in a follow-up PR, to keep things here clean. Current state looks very good as is, so I'd say this is ready to be merged. Also, just as a reminder, these typing changes, we can easily do via ruff using its pyupgrade rules, e.g., for Optional -> | rule UP007.

EDIT: Started working on this, out of curiosity ^^

@GeigerJ2 GeigerJ2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great work, very clean! Thanks, @agoscinski 🫶
Wondering why 3.13 psql tests are pending? Maybe we can re-trigger manually? Once we know that everything passes, I'd say this can be merged.

@agoscinski

agoscinski commented Mar 2, 2026

Copy link
Copy Markdown
Collaborator Author

Wondering why 3.13 psql tests are pending? Maybe we can re-trigger manually? Once we know that everything passes, I'd say this can be merged.

Because of the branch protection rules. We replaced the CI workflow with 3.14, the 3.13 workflow does not exist anymore but because they are in the branch protection rules they still appear here as not being run. After this PR is merged with have to switch the branch protection rule from the CI 3.13 to 3.14.

agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 2, 2026
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 2, 2026
asyncssh 2.19.0 uses a custom `Record` class (in `misc.py`) that
breaks on Python 3.14. This causes `AttributeError` on any object
built on `Record`, including `SFTPLimits` (missing `max_packet_len`)
and `SSHCompletedProcess` (missing `exit_status`, `stdout`, etc.),
effectively breaking all `core.ssh_async` transport operations.

The fix (migrating from `Record` to Python dataclasses) landed in
asyncssh 2.20.0, as tracked in ronf/asyncssh#732. We pin to
~=2.21.0 rather than ~=2.22.0 because asyncssh 2.22.0 dropped
support for Python 3.9, which this project still supports.
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 2, 2026
The orm/test_fields.py test relies on the inbuilt `repr` function of
python to compare the orm fields. With python 3.14 the formatting of
`repr` changes. To avoid maintaining two sets of referenece files with
different formatting we change the reference files to the formatting of
3.14 and run the tests only for 3.14 and above.
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Mar 2, 2026
…aiidateam#7240)

In Python 3.14, `url2pathname` was rewritten to be a full file-URL
parser per RFC 8089 (python/cpython#126838).
The new implementation prepends `file:` to the input and re-parses it
with `urlsplit`, then validates that the URL authority resolves to
localhost.

This breaks our usage because we pass `urlparse(uri).path` — a pre-parsed
path component — not a full URL. The new `url2pathname` misinterprets
this input, and raises:

    URLError: file:// scheme is supported only on localhost

Since we already validate that the scheme is `file://` before extracting
the path, we replace the stdlib import with a local `url2pathname` shim
that preserves the simple Python <= 3.13 behavior.
Comment thread tests/orm/test_fields.py Outdated
Comment on lines +28 to +29
# Python versions that don't match. Once 3.14 becomes the baseline, regenerate the
# reference files and update this marker.

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.

Suggested change
# Python versions that don't match. Once 3.14 becomes the baseline, regenerate the
# reference files and update this marker.
# Python versions that don't match.

@danielhollas

Copy link
Copy Markdown
Collaborator

btw: Would be good to verify that circus works well with Python 3.14
circus-tent/circus#1233
(not sure if @agoscinski has access to the repo and can run the workflow? Or do we need to get Seb?)

@agoscinski

agoscinski commented Mar 4, 2026

Copy link
Copy Markdown
Collaborator Author

@danielhollas I dont have any rights for circus, I ran the tests on my own fork now agoscinski/circus#3 I needed to add some minor fixes. My understanding is that existing test errors will not affect aiida-core. You can check the fixes in the PR by yourself if you want. One test error is the change of get_event_loop that now raises a RuntimeError if no event loop is present. This problem we do not have in aiida-core since always start the event loop before starting an arbiter. The other test error happens only in the tests because they use multiprocessing to fork a process in the signal test which changed its default from fork to forkserver (see https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods). In circus/process.py they however use psutils which uses subprocess which did not change with py3.14 so this is safe.

In principle one can fix these issues upstream but the only required change in the source code of circus will not affect us and I would like to remove circus as dependency latest in the next minor release.

@agoscinski

agoscinski commented Mar 4, 2026

Copy link
Copy Markdown
Collaborator Author

@danielhollas I will remove the py3.14 classifier in the pyproject.toml for now until you give me the go. But will merge the current changes to main, so we can start another prerelease. For the actual release we can then decide if we want to classify it as officially supporting py3.14.

asyncssh 2.19.0 uses a custom `Record` class (in `misc.py`) that
breaks on Python 3.14. This causes `AttributeError` on any object
built on `Record`, including `SFTPLimits` (missing `max_packet_len`)
and `SSHCompletedProcess` (missing `exit_status`, `stdout`, etc.),
effectively breaking all `core.ssh_async` transport operations.

The fix (migrating from `Record` to Python dataclasses) landed in
asyncssh 2.20.0, as tracked in ronf/asyncssh#732. We pin to
~=2.21.0 rather than ~=2.22.0 because asyncssh 2.22.0 dropped
support for Python 3.9, which this project still supports.
The orm/test_fields.py test relies on the inbuilt `repr` function of
python to compare the orm fields. With python 3.14 the formatting of
`repr` changes. To avoid maintaining two sets of referenece files with
different formatting we change the reference files to the formatting of
3.14 and run the tests only for 3.14 and above.
…aiidateam#7240)

In Python 3.14, `url2pathname` was rewritten to be a full file-URL
parser per RFC 8089 (python/cpython#126838).
The new implementation prepends `file:` to the input and re-parses it
with `urlsplit`, then validates that the URL authority resolves to
localhost.

This breaks our usage because we pass `urlparse(uri).path` — a pre-parsed
path component — not a full URL. The new `url2pathname` misinterprets
this input, and raises:

    URLError: file:// scheme is supported only on localhost

Since we already validate that the scheme is `file://` before extracting
the path, we replace the stdlib import with a local `url2pathname` shim
that preserves the simple Python <= 3.13 behavior.
@agoscinski
agoscinski enabled auto-merge (rebase) March 4, 2026 13:36
@agoscinski
agoscinski merged commit 1607da4 into aiidateam:main Mar 4, 2026
32 of 33 checks passed
agoscinski added a commit that referenced this pull request Mar 4, 2026
agoscinski added a commit that referenced this pull request Mar 4, 2026
asyncssh 2.19.0 uses a custom `Record` class (in `misc.py`) that
breaks on Python 3.14. This causes `AttributeError` on any object
built on `Record`, including `SFTPLimits` (missing `max_packet_len`)
and `SSHCompletedProcess` (missing `exit_status`, `stdout`, etc.),
effectively breaking all `core.ssh_async` transport operations.

The fix (migrating from `Record` to Python dataclasses) landed in
asyncssh 2.20.0, as tracked in ronf/asyncssh#732. We pin to
~=2.21.0 rather than ~=2.22.0 because asyncssh 2.22.0 dropped
support for Python 3.9, which this project still supports.
agoscinski added a commit that referenced this pull request Mar 4, 2026
The orm/test_fields.py test relies on the inbuilt `repr` function of
python to compare the orm fields. With python 3.14 the formatting of
`repr` changes. To avoid maintaining two sets of referenece files with
different formatting we change the reference files to the formatting of
3.14 and run the tests only for 3.14 and above.
@agoscinski
agoscinski deleted the support-py3.14 branch March 4, 2026 13:37

@danielhollas danielhollas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@agoscinski thanks, this PR looks good to me, seems fine to add the marker hopefully after some real-world testing with python 3.14 with the new rc release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants