diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c9640ac2..c79de05a 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -100,22 +100,22 @@ jobs:
python_version: '3.13'
- tox_env: py313-m_ans-ans8
python_version: '3.13'
- - tox_env: py313-m_ans-ans9
- python_version: '3.13'
- - tox_env: py313-m_ans-ans10
- python_version: '3.13'
- - tox_env: py313-m_ans-ans11
- python_version: '3.13'
- - tox_env: py313-m_ans-ans12
- python_version: '3.13'
-
- - tox_env: py313-m_ans-ans11-s_lin
- python_version: '3.13'
- - tox_env: py313-m_ans-ans12-s_lin
- python_version: '3.13'
-
- - tox_env: py313-m_mtg
- python_version: '3.13'
+ - tox_env: py314-m_ans-ans9
+ python_version: '3.14.0-rc.3'
+ - tox_env: py314-m_ans-ans10
+ python_version: '3.14.0-rc.3'
+ - tox_env: py314-m_ans-ans11
+ python_version: '3.14.0-rc.3'
+ - tox_env: py314-m_ans-ans12
+ python_version: '3.14.0-rc.3'
+
+ - tox_env: py314-m_ans-ans11-s_lin
+ python_version: '3.14.0-rc.3'
+ - tox_env: py314-m_ans-ans12-s_lin
+ python_version: '3.14.0-rc.3'
+
+ - tox_env: py314-m_mtg
+ python_version: '3.14.0-rc.3'
steps:
- uses: actions/checkout@v4
@@ -161,14 +161,19 @@ jobs:
fail-fast: false
matrix:
include:
- - tox_env: py313-m_lcl-ans11
+ - tox_env: py314-m_lcl-ans11
+ python_version: '3.14.0-rc.3'
sshpass_version: "1.10"
- - tox_env: py313-m_lcl-ans11-s_lin
+ - tox_env: py314-m_lcl-ans11-s_lin
+ python_version: '3.14.0-rc.3'
sshpass_version: "1.10"
- - tox_env: py313-m_lcl-ans12
- - tox_env: py313-m_lcl-ans12-s_lin
+ - tox_env: py314-m_lcl-ans12
+ python_version: '3.14.0-rc.3'
+ - tox_env: py314-m_lcl-ans12-s_lin
+ python_version: '3.14.0-rc.3'
- - tox_env: py313-m_mtg
+ - tox_env: py314-m_mtg
+ python_version: '3.14.0-rc.3'
steps:
- uses: actions/checkout@v4
diff --git a/docs/ansible_detailed.rst b/docs/ansible_detailed.rst
index 06aa4955..e692c43a 100644
--- a/docs/ansible_detailed.rst
+++ b/docs/ansible_detailed.rst
@@ -138,11 +138,11 @@ Noteworthy Differences
| 8 | 3.9 - 3.13 |
+-----------------+-----------------+
| 9 | |
- +-----------------+ 3.10 - 3.13 |
+ +-----------------+ 3.10 - 3.14 |
| 10 | |
+-----------------+-----------------+
| 11 | |
- +-----------------+ 3.11 - 3.13+ |
+ +-----------------+ 3.11 - 3.14 |
| 12 | |
+-----------------+-----------------+
diff --git a/docs/changelog.rst b/docs/changelog.rst
index c44a793e..52c04f38 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -18,6 +18,13 @@ To avail of fixes in an unreleased version, please download a ZIP file
`directly from GitHub `_.
+v0.3.29 (2025-09-18)
+--------------------
+
+* :gh:issue:`1287` Python 3.14 support
+* :gh:issue:`1287` tests: Bump dependencies
+
+
v0.3.28 (2025-09-17)
--------------------
diff --git a/mitogen/__init__.py b/mitogen/__init__.py
index 76c83cab..0a1e31dc 100644
--- a/mitogen/__init__.py
+++ b/mitogen/__init__.py
@@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup.
#: Library version as a tuple.
-__version__ = (0, 3, 28)
+__version__ = (0, 3, 29)
#: This is :data:`False` in slave contexts. Previously it was used to prevent
diff --git a/mitogen/imports/__init__.py b/mitogen/imports/__init__.py
index ecbdb795..a54e9b01 100644
--- a/mitogen/imports/__init__.py
+++ b/mitogen/imports/__init__.py
@@ -4,7 +4,9 @@
import sys
-if sys.version_info >= (3, 6):
+if sys.version_info >= (3, 14):
+ from mitogen.imports._py314 import _code_imports
+elif sys.version_info >= (3, 6):
from mitogen.imports._py36 import _code_imports
elif sys.version_info >= (2, 5):
from mitogen.imports._py2 import _code_imports_py25 as _code_imports
diff --git a/mitogen/imports/_py314.py b/mitogen/imports/_py314.py
new file mode 100644
index 00000000..7fa69cff
--- /dev/null
+++ b/mitogen/imports/_py314.py
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: 2025 Mitogen authors
+# SPDX-License-Identifier: MIT
+# !mitogen: minify_safe
+
+import opcode
+
+IMPORT_NAME = opcode.opmap['IMPORT_NAME']
+LOAD_CONST = opcode.opmap['LOAD_CONST']
+LOAD_SMALL_INT = opcode.opmap['LOAD_SMALL_INT']
+
+
+def _code_imports(code, consts, names):
+ start = 4
+ while True:
+ op3_idx = code.find(IMPORT_NAME, start, -1)
+ if op3_idx < 0:
+ return
+ if op3_idx % 2:
+ start = op3_idx + 1
+ continue
+ if code[op3_idx-4] != LOAD_SMALL_INT or code[op3_idx-2] != LOAD_CONST:
+ start = op3_idx + 2
+ continue
+ start = op3_idx + 6
+ arg1, arg2, arg3 = code[op3_idx-3], code[op3_idx-1], code[op3_idx+1]
+ yield (arg1, names[arg3], consts[arg2] or ())
diff --git a/setup.py b/setup.py
index f8aeffea..fe561fd4 100644
--- a/setup.py
+++ b/setup.py
@@ -102,6 +102,7 @@ setup(
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
+ 'Programming Language :: Python :: 3.14',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: System :: Distributed Computing',
'Topic :: System :: Systems Administration',
diff --git a/tests/ansible/regression/issue_766__get_with_context.yml b/tests/ansible/regression/issue_766__get_with_context.yml
index 5dae142f..56a10123 100644
--- a/tests/ansible/regression/issue_766__get_with_context.yml
+++ b/tests/ansible/regression/issue_766__get_with_context.yml
@@ -9,6 +9,8 @@
netconf_container_image: ghcr.io/mitogen-hq/sysrepo-netopeer2:latest
netconf_container_name: sysrepo
netconf_container_port: 8030
+ # https://github.com/ansible-collections/ansible.netcommon/issues/698#issuecomment-2910082548
+ ansible_network_import_modules: "{{ (ansible_version_major_minor is version('2.19', '==', strict=True)) | bool }}"
tasks:
- meta: end_play
@@ -28,13 +30,6 @@
- ansible_version_major_minor is version('2.11', '>=', strict=True)
- ansible_version_major_minor is version('2.12', '<', strict=True)
- - meta: end_play
- when:
- # TASK [Get running configuration and state data ]
- # Error: : Task failed: ActionBase._parse_returned_data() missing 1 required positional argument: 'profile'
- # https://github.com/ansible-collections/ansible.netcommon/issues/698#issuecomment-2910082548
- - ansible_version_major_minor is version('2.19', '>=', strict=True)
-
- block:
- name: Start container
command:
@@ -63,6 +58,9 @@
ansible.netcommon.netconf_get:
always:
+ - name: Close connections
+ meta: reset_connection
+
- name: Cleanup container
command:
cmd: podman stop "{{ netconf_container_name }}"
diff --git a/tests/ansible/requirements.txt b/tests/ansible/requirements.txt
index 11457e66..98184e89 100644
--- a/tests/ansible/requirements.txt
+++ b/tests/ansible/requirements.txt
@@ -1,5 +1,6 @@
paramiko==2.12.0; python_version <= '2.7'
-paramiko==3.5.0; python_version >= '3.6'
+paramiko==3.5.1; python_version >= '3.6' and python_version <= '3.8'
+paramiko==4.0.0; python_version >= '3.9'
# Incompatible with pip >= 72, due to removal of `setup.py test`:
# ModuleNotFoundError: No module named 'setuptools.command.test'
@@ -7,7 +8,8 @@ paramiko==3.5.0; python_version >= '3.6'
hdrhistogram==0.6.1
ncclient==0.6.13; python_version <= '2.7'
-ncclient==0.6.16; python_version > '2.7'
+ncclient==0.6.19; python_version >= '3.5'
PyYAML==3.11; python_version < '2.7'
-PyYAML==5.3.1; python_version >= '2.7' # Latest release (Jan 2021)
+PyYAML==5.4.1; python_version >= '2.7' and python_version <= '3.7'
+PyYAML==6.0.2; python_version >= '3.8'
diff --git a/tests/bench/scan_code b/tests/bench/scan_code
index 118dd9d7..302db869 100755
--- a/tests/bench/scan_code
+++ b/tests/bench/scan_code
@@ -10,7 +10,7 @@ BIG_MODULE_PATH="$(dirname -- "$0")/data/big_module.py"
IMPORTS="from collections import deque; from mitogen.imports import $BENCH_FUNC"
COMPILE="co=compile(open('$BIG_MODULE_PATH').read(), '$BIG_MODULE_PATH', 'exec')"
PYTHONS=(
- python2.7 python3.9 python3.10 python3.11 python3.12 python3.13
+ python2.7 python3.{9..14}
)
for p in "${PYTHONS[@]}"; do
echo -e -n "$BENCH_FUNC $p "
diff --git a/tests/image_prep/roles/user_policies/tasks/main.yml b/tests/image_prep/roles/user_policies/tasks/main.yml
index 89fff6bc..427f8a3b 100644
--- a/tests/image_prep/roles/user_policies/tasks/main.yml
+++ b/tests/image_prep/roles/user_policies/tasks/main.yml
@@ -1,4 +1,13 @@
-- name: Set login attempts (macOS)
+- name: Set global login attempts (macOS)
+ command:
+ pwpolicy
+ -n /Local/Default
+ -setglobalpolicy 'maxFailedLoginAttempts={{ user_policies_max_failed_logins }}'
+ when:
+ - ansible_system == 'Darwin'
+ changed_when: true
+
+- name: Set user login attempts (macOS)
vars:
max_failed_logins: "{{ item.policies.max_failed_logins | default(user_policies_max_failed_logins) }}"
command: >
diff --git a/tests/requirements.txt b/tests/requirements.txt
index c5671b37..571f89d9 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,25 +1,25 @@
-cffi==1.15.1; python_version < '3.8'
-cffi==1.17.1; python_version >= '3.8'
-
coverage==5.5; python_version == '2.7'
coverage==6.2; python_version == '3.6'
coverage==7.2.7; python_version == '3.7'
-coverage==7.4.3; python_version >= '3.8'
+coverage==7.5.4; python_version == '3.8'
+coverage==7.10.6; python_version >= '3.9'
Django==1.11.29; python_version < '3.0'
Django==3.2.20; python_version >= '3.6'
mock==3.0.5; python_version == '2.7'
-mock==5.1.0; python_version >= '3.6'
+mock==5.2.0; python_version >= '3.6'
-pexpect==4.8
+pexpect==4.9
-psutil==5.9.8
+psutil==6.1.1; python_version <= '2.7'
+psutil==7.1.0; python_version >= '3.6'
pytest==4.6.11; python_version == '2.7'
pytest==7.0.1; python_version == '3.6'
pytest==7.4.4; python_version == '3.7'
-pytest==8.0.2; python_version >= '3.8'
+pytest==8.3.5; python_version == '3.8'
+pytest==8.4.2; python_version >= '3.9'
subprocess32==3.5.4; python_version < '3.0'
timeoutcontext==1.2.0
@@ -32,4 +32,5 @@ idna==2.7; python_version < '2.7'
virtualenv==20.15.1; python_version == '2.7'
virtualenv==20.17.1; python_version == '3.6'
-virtualenv==20.25.1; python_version >= '3.7'
+virtualenv==20.26.6; python_version == '3.7'
+virtualenv==20.34.0; python_version >= '3.8'
diff --git a/tox.ini b/tox.ini
index 9fa61dc6..aac84fa8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@
# 2.7 <= 2.11 <= 2.16 <= 5.5 <= 1.11.29 <= 2.11.3 <= 20 <= 4.6.11 <= 3.28 <= 20.15²
# 3.5 <= 2.11 <= 2.15 <= 5.5 <= 2.2.28 <= 2.11.3 <= 20 <= 5.9.5 <= 6.1.0 <= 3.28 <= 20.15²
# 3.6 <= 2.11 <= 2.16 <= 6.2 <= 3.2.20 <= 3.0.3 <= 21 <= 7.0.1 <= 3.28 <= 20.17²
-# 3.7 <= 2.12 <= 2.17 <= 7.2.7 <= 3.2.20 <= 7.4.4 <= 4.8.0
+# 3.7 <= 2.12 <= 2.17 <= 7.2.7 <= 3.2.20 <= 7.4.4 <= 4.8.0 <= 20.26.6²
# 3.8 <= 2.12
# 3.9 <= 2.15
# 3.10 <= 2.17
@@ -31,8 +31,10 @@
# Python 3.12 removed deprecated httplib.HTTPSConnection() arguments.
# https://github.com/ansible/ansible/pull/80751
#
-# 2. Higher virtualenv versions cannot run under this Python version. They can
-# still generate virtual environments for it.
+# 2. Higher Virtualenv versions do not support this Python as *host* Python.
+# Virtualenv <= 20.21.1 supports creating virtualenvs with any *target* Python.
+# Virtualenv >= 20.22 supports creating virtualenvs with target Python >= 3.7.
+# https://virtualenv.pypa.io/en/latest/#compatibility
# Ansible Dependency
# ================== ======================
@@ -57,8 +59,9 @@ envlist =
init,
py{27,36}-m_ans-ans{2.10,3,4}
py{311}-m_ans-ans{2.10,3-5}
- py{313}-m_ans-ans{6-12}
- py{27,36,313}-m_mtg
+ py{313}-m_ans-ans{6-9}
+ py{314}-m_ans-ans{10-12}
+ py{27,36,314}-m_mtg
report,
[testenv]
@@ -74,6 +77,7 @@ basepython =
py311: python3.11
py312: python3.12
py313: python3.13
+ py314: python3.14
deps =
-r{toxinidir}/tests/requirements.txt
m_ans: -r{toxinidir}/tests/ansible/requirements.txt
@@ -88,7 +92,7 @@ deps =
ans9: ansible~=9.0
ans10: ansible~=10.0
ans11: ansible~=11.0
- ans12: ansible>=12.0.0b2
+ ans12: ansible~=12.0
install_command =
python -m pip --no-python-version-warning --disable-pip-version-check install {opts} {packages}
commands_pre =