From b47894bd28a29e7722b4f1633bb96935a7f7c10e Mon Sep 17 00:00:00 2001 From: traoreera Date: Thu, 1 Oct 2026 23:45:37 +0000 Subject: [PATCH 1/2] chore(python): Python 3.14 minimum, CI sur 3.14 uniquement (v2.8.0, breaking) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le développement, les mesures GC/reload des 2.6.x et tous les smoke tests de release tournaient déjà sur CPython 3.14, alors que la matrice CI ne testait que 3.12 et 3.13 : les versions annoncées n'étaient pas celles réellement exécutées. - requires-python >=3.14,<4.0 (était >=3.12) — breaking : pip sur 3.12/3.13 ne sélectionnera plus cette version ni les suivantes (reste sur XCoreRuntime<2.8) - CI : matrice Tests & Coverage ["3.14"] ; lint, docs, security, PR et les deux workflows de release passés en 3.14 (étaient 3.12 ou 3.13) - poetry.lock régénéré pour le nouveau plancher : seuls les marqueurs de version Python et les métadonnées changent, aucune version de paquet ne bouge - devcontainer python:3.14 ; pages d'installation, CLAUDE.md et GEMINI.md à jour - black reste en target-version py313 : avec py314 il réécrit `except (A, B):` en `except A, B:` (PEP 758) sur tout le code - limitation connue : l'extra [cpp] (xcorescanner 0.1.0) n'a pas de wheel cp314 (cp312/cp313 + sdist) — compilation depuis les sources en 3.14 - bump 2.8.0, entrée CHANGELOG dans les deux fichiers --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release-manual.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/security.yml | 6 +++--- CHANGELOG.md | 15 +++++++++++++++ CLAUDE.md | 4 ++-- GEMINI.md | 4 ++-- doc/changelog.md | 15 +++++++++++++++ doc/cli/getting-started/install.md | 2 +- doc/installation.md | 4 ++-- doc/plugins/trusted-plugins.md | 2 +- doc/sdk/installation.md | 2 +- poetry.lock | 14 +++----------- pyproject.toml | 9 ++++++--- xcore/__version__.py | 4 ++-- 18 files changed, 60 insertions(+), 35 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9c4eddc7..5531b043 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # .devcontainer/Dockerfile -FROM mcr.microsoft.com/devcontainers/python:3.12 +FROM mcr.microsoft.com/devcontainers/python:3.14 # Install system dependencies RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aea0871..e30ef356 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ concurrency: cancel-in-progress: true env: - PYTHON_VERSION: "3.12" + PYTHON_VERSION: "3.14" POETRY_VERSION: "2.4.1" # ────────────────────────────────────────────────────────────── @@ -78,7 +78,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.12", "3.13"] + python-version: ["3.14"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d4d1ad01..2ead1e7d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ permissions: pull-requests: write env: - PYTHON_VERSION: "3.12" + PYTHON_VERSION: "3.14" jobs: build: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index eeff6da4..cee553f2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -106,7 +106,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.14" - name: Install Poetry uses: snok/install-poetry@v1 diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml index a1bade52..028d3d1b 100644 --- a/.github/workflows/release-manual.yml +++ b/.github/workflows/release-manual.yml @@ -44,7 +44,7 @@ permissions: actions: write env: - PYTHON_VERSION: "3.12" + PYTHON_VERSION: "3.14" POETRY_VERSION: "2.4.1" jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f623bfcb..0ceba92e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ permissions: contents: read env: - PYTHON_VERSION: "3.12" + PYTHON_VERSION: "3.14" POETRY_VERSION: "2.4.1" jobs: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 46fccd45..2caa2245 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.14" - name: Install Poetry uses: snok/install-poetry@v1 @@ -54,7 +54,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.14" - name: Install Bandit run: pip install bandit[toml] @@ -115,7 +115,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.14" - name: Install Poetry uses: snok/install-poetry@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 360af9b0..f1b22d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.8.0] - 2026-10-01 + +### Changed +- **BREAKING: the minimum supported Python is now 3.14** (`requires-python = ">=3.14,<4.0"`, was `>=3.12`). Development, the benchmarks behind the 2.6.x lifecycle/GC work and every release smoke test already ran on CPython 3.14, while the CI matrix only exercised 3.12 and 3.13 — so the versions the project claimed to support were not the ones it was actually run on. Consequences for users: `pip install XCoreRuntime` on Python 3.12 or 3.13 will no longer select this version or later (pip's `Requires-Python` check falls back to 2.7.x); stay on `XCoreRuntime<2.8` there. +- **CI now runs on 3.14 only**: the `Tests & Coverage` matrix is `["3.14"]` (jobs are renamed `Tests & Coverage (3.14)`; there is no required-status-check rule on those names), and the lint, docs, security, PR and both release workflows moved to 3.14 (they were pinned to 3.12 or 3.13). The wheel published by `release.yml` is built on 3.14 (pure Python, `py3-none-any`). +- `poetry.lock` regenerated for the new floor: only the Python-version markers and metadata change (the `python_version < "3.13"` backport markers on `typing-extensions` and the pydantic marker ladder collapse); **no package version changes**. +- `.devcontainer` image moved to `python:3.14`; install/requirements pages, `CLAUDE.md` and `GEMINI.md` updated. + +### Known limitation +- **The `[cpp]` extra (`xcorescanner`) has no 3.14 wheel yet**: `xcorescanner 0.1.0` on PyPI ships `cp312` and `cp313` wheels plus an sdist, so on Python 3.14 `pip install 'XCoreRuntime[cpp]'` builds it from source and needs a C++ toolchain until a `cp314` wheel is published. Without the extra, xcore falls back to the Python AST scanner. + +### Notes +- Black stays on `target-version = ["py313"]`: with `py314` it rewrites `except (A, B):` into the PEP 758 form `except A, B:` across the code base — valid on 3.14, but easy to misread and not handled by all tooling. The code itself already requires 3.14 through `requires-python`. +- The `Upload to Codecov` step in `ci.yml` is still guarded by `matrix.python-version == '3.11'` (a leftover that has been false for a long time), so it still never runs; left untouched here. + ## [2.7.0] - 2026-10-01 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 0dab1596..e788fd29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Présentation -**xcore v2.7.0** — framework d'orchestration plugin-first construit sur FastAPI. +**xcore v2.8.0** — framework d'orchestration plugin-first construit sur FastAPI. Charge, isole et gère des plugins modulaires dans un environnement sandboxé. -- **Language** : Python 3.12+ +- **Language** : Python 3.14+ - **Framework** : FastAPI + uvicorn - **Dépendances** : Poetry - **Config** : `integration.yaml` (principal) — pas `xcore.yaml` diff --git a/GEMINI.md b/GEMINI.md index 6c8c5792..44426b19 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -12,7 +12,7 @@ - **Security**: Supports plugin signing, manifest validation, and strict import/resource restriction. ### Main Technologies -- **Language**: Python 3.12+ (with C++ extensions for security) +- **Language**: Python 3.14+ (with C++ extensions for security) - **Framework**: FastAPI [standard] - **Dependency Management**: Poetry / UV - **Data Validation**: Pydantic v2 @@ -26,7 +26,7 @@ ## Building and Running ### Prerequisites -- Python 3.12 or higher +- Python 3.14 or higher - [Poetry](https://python-poetry.org/) 2.0+ ### Key Commands diff --git a/doc/changelog.md b/doc/changelog.md index 360af9b0..f1b22d5c 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.8.0] - 2026-10-01 + +### Changed +- **BREAKING: the minimum supported Python is now 3.14** (`requires-python = ">=3.14,<4.0"`, was `>=3.12`). Development, the benchmarks behind the 2.6.x lifecycle/GC work and every release smoke test already ran on CPython 3.14, while the CI matrix only exercised 3.12 and 3.13 — so the versions the project claimed to support were not the ones it was actually run on. Consequences for users: `pip install XCoreRuntime` on Python 3.12 or 3.13 will no longer select this version or later (pip's `Requires-Python` check falls back to 2.7.x); stay on `XCoreRuntime<2.8` there. +- **CI now runs on 3.14 only**: the `Tests & Coverage` matrix is `["3.14"]` (jobs are renamed `Tests & Coverage (3.14)`; there is no required-status-check rule on those names), and the lint, docs, security, PR and both release workflows moved to 3.14 (they were pinned to 3.12 or 3.13). The wheel published by `release.yml` is built on 3.14 (pure Python, `py3-none-any`). +- `poetry.lock` regenerated for the new floor: only the Python-version markers and metadata change (the `python_version < "3.13"` backport markers on `typing-extensions` and the pydantic marker ladder collapse); **no package version changes**. +- `.devcontainer` image moved to `python:3.14`; install/requirements pages, `CLAUDE.md` and `GEMINI.md` updated. + +### Known limitation +- **The `[cpp]` extra (`xcorescanner`) has no 3.14 wheel yet**: `xcorescanner 0.1.0` on PyPI ships `cp312` and `cp313` wheels plus an sdist, so on Python 3.14 `pip install 'XCoreRuntime[cpp]'` builds it from source and needs a C++ toolchain until a `cp314` wheel is published. Without the extra, xcore falls back to the Python AST scanner. + +### Notes +- Black stays on `target-version = ["py313"]`: with `py314` it rewrites `except (A, B):` into the PEP 758 form `except A, B:` across the code base — valid on 3.14, but easy to misread and not handled by all tooling. The code itself already requires 3.14 through `requires-python`. +- The `Upload to Codecov` step in `ci.yml` is still guarded by `matrix.python-version == '3.11'` (a leftover that has been false for a long time), so it still never runs; left untouched here. + ## [2.7.0] - 2026-10-01 ### Changed diff --git a/doc/cli/getting-started/install.md b/doc/cli/getting-started/install.md index 4e0508ae..f2cf4614 100644 --- a/doc/cli/getting-started/install.md +++ b/doc/cli/getting-started/install.md @@ -4,7 +4,7 @@ Getting started with `xcorecli` is straightforward. The project uses [Poetry](ht ## Prerequisites -- **Python**: 3.12 or higher. +- **Python**: 3.14 or higher. - **Poetry**: Recommended for environment management. - **Make**: To use the provided automation scripts. diff --git a/doc/installation.md b/doc/installation.md index bdb4aee0..c1d6bfab 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -6,7 +6,7 @@ icon: material/download # Installation & Setup -Xcore requires **Python 3.12** or higher. +Xcore requires **Python 3.14** or higher. ## Using Xcore in a project (PyPI) @@ -41,7 +41,7 @@ no external dependency. `[sdk]` unlocks the extended feature set above. ## Contributing to Xcore itself (from source) -- [x] Python 3.12+ installed +- [x] Python 3.14+ installed - [x] [Poetry](https://python-poetry.org/docs/#installation) installed - [x] C++ compiler (GCC, Clang, or MSVC) — only needed to build the optional `scanner_core` accelerator in-place - [ ] Redis (optional, required for Redis cache and background workers) diff --git a/doc/plugins/trusted-plugins.md b/doc/plugins/trusted-plugins.md index ae89d4a3..6d3411f0 100644 --- a/doc/plugins/trusted-plugins.md +++ b/doc/plugins/trusted-plugins.md @@ -13,7 +13,7 @@ Trusted plugins are native Python extensions that run directly in the main Xcore ### Prerequisites - [x] [Plugin Anatomy](./plugin-anatomy.md) understood -- [x] Python 3.12+ and `asyncio` proficiency +- [x] Python 3.14+ and `asyncio` proficiency --- diff --git a/doc/sdk/installation.md b/doc/sdk/installation.md index d5a3a5ba..4803e8cf 100644 --- a/doc/sdk/installation.md +++ b/doc/sdk/installation.md @@ -4,7 +4,7 @@ | Requirement | Version | |-------------|---------| -| Python | 3.12+ | +| Python | 3.14+ | | uv | latest | | xcore | installed in the environment | diff --git a/poetry.lock b/poetry.lock index c10e1d13..36807086 100644 --- a/poetry.lock +++ b/poetry.lock @@ -148,7 +148,6 @@ attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" propcache = ">=0.2.0" -typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} yarl = ">=1.17.0,<2.0" [package.extras] @@ -168,7 +167,6 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "aiosqlite" @@ -1028,11 +1026,7 @@ files = [ detect-installer = ">=0.1.0" fastar = ">=0.10.0" httpx = ">=0.27.0" -pydantic = [ - {version = ">=2.7.4", extras = ["email"], markers = "python_version < \"3.13\""}, - {version = ">=2.8.0", extras = ["email"], markers = "python_version == \"3.13\""}, - {version = ">=2.12.0", extras = ["email"], markers = "python_version >= \"3.14\""}, -] +pydantic = {version = ">=2.12.0", extras = ["email"], markers = "python_version >= \"3.14\""} rich-toolkit = ">=0.20.1" rignore = ">=0.5.1" sentry-sdk = ">=2.20.0" @@ -3218,7 +3212,6 @@ files = [ [package.dependencies] pytest = ">=8.4,<10" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] @@ -3940,7 +3933,6 @@ files = [ [package.dependencies] anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] @@ -4614,5 +4606,5 @@ xcli = ["xcorecli"] [metadata] lock-version = "2.1" -python-versions = ">=3.12,<4.0" -content-hash = "4be4c7dba93366ef2f6637a7cbad2204f95760a03acd7ec3f1e8f0011a601aed" +python-versions = ">=3.14,<4.0" +content-hash = "958ee4d3cc0e449844851418e8604ab306d742cdd7f565b33d7cf7f7837e8a31" diff --git a/pyproject.toml b/pyproject.toml index 5c3443a6..a90c5e37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "XCoreRuntime" -version = "2.7.0" +version = "2.8.0" description = "Plugin-first orchestration framework built on FastAPI" authors = [ { name = "Eliezer Traore", email = "68350805+traoreera@users.noreply.github.com" }, @@ -15,7 +15,7 @@ classifiers = [ "Operating System :: OS Independent", ] -requires-python = ">=3.12,<4.0" +requires-python = ">=3.14,<4.0" # ── Noyau ─────────────────────────────────────────────────────────────────────── # Ce que `import xcore` et la config par défaut (zero-config) chargent @@ -138,7 +138,10 @@ markdown = "^3.8.2" # ── Linting ──────────────────────────────────────────────────────────────────── [tool.black] line-length = 88 -target-version = ["py312"] # était py311, ton requires-python est >=3.12 +# py313 et non py314 : avec py314, black réécrit `except (A, B):` en `except A, B:` +# (PEP 758, sans parenthèses) — valide en 3.14 mais trompeur à lire et mal géré par +# une partie de l'outillage. Le code reste valide sur 3.14 (requires-python >=3.14). +target-version = ["py313"] [tool.isort] profile = "black" diff --git a/xcore/__version__.py b/xcore/__version__.py index 50414e6a..d6b4923e 100644 --- a/xcore/__version__.py +++ b/xcore/__version__.py @@ -1,4 +1,4 @@ -__version__ = "2.7.0" -__version_info__ = (2, 7, 0) +__version__ = "2.8.0" +__version_info__ = (2, 8, 0) __author__ = "xcore contributors" __license__ = "MIT" From a14bfb91762b4343d19e912f96a616ade56a835a Mon Sep 17 00:00:00 2001 From: traoreera Date: Fri, 2 Oct 2026 00:01:44 +0000 Subject: [PATCH 2/2] test: un test ne pose plus RLIMIT_AS sur le process pytest (can't start new thread en 3.14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestMemoryLimiter.test_apply_positive appelait MemoryLimiter.apply(512) pour de bon dans le process pytest : setrlimit(RLIMIT_AS, 512 Mo) est irréversible (la limite dure baisse) et valable pour toute la session. Chaque thread créé ensuite réserve ~72 Mo d'espace d'adressage (pile + arène glibc) : après 8 threads, RuntimeError « can't start new thread » — reproduit en local. Les tests placés plus loin dans la session (TestClient de test_xcore_plugin_routes) échouaient selon l'empreinte mémoire de l'interpréteur : jamais en 3.12/3.13, systématiquement sur le runner en 3.14 (Quick Test : « can't start new thread » puis abort dans pydantic-core sur un thread de portail). - le test vérifie l'appel (resource.setrlimit patché) au lieu de l'exécuter, plus un test du cas d'échec (journalisé, pas levé) - tests/conftest.py : fixture autouse qui échoue si un test modifie RLIMIT_AS / RLIMIT_DATA / RLIMIT_CPU du process pytest --- tests/conftest.py | 42 +++++++++++++++++++++++++++++ tests/unit/kernel/test_isolation.py | 29 +++++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 621d9709..c3102c53 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ import os import shutil +import sys import tempfile from pathlib import Path from typing import Generator @@ -163,6 +164,47 @@ def _cleanup_stray_tmp_dirs() -> Generator[None, None, None]: sweep_stray_tmp_dirs() +@pytest.fixture(autouse=True) +def _no_process_rlimit_leak() -> Generator[None, None, None]: + """ + Un test ne doit jamais modifier les limites de ressources du process pytest. + + RLIMIT_AS / RLIMIT_DATA / RLIMIT_CPU sont globaux au process et la limite DURE + ne se relève pas : un `setrlimit` réel dans un test s'applique à tous les tests + suivants de la session. RLIMIT_AS à 512 Mo suffisait à faire échouer + `threading.Thread.start()` (« can't start new thread ») dans les tests placés + plus loin — selon l'empreinte mémoire de l'interpréteur. Tester ces limites en + patchant `resource.setrlimit`, ou dans un sous-processus. + """ + if sys.platform == "win32": + yield + return + import resource + + tracked = [ + getattr(resource, name) + for name in ("RLIMIT_AS", "RLIMIT_DATA", "RLIMIT_CPU") + if hasattr(resource, name) + ] + before = {r: resource.getrlimit(r) for r in tracked} + yield + changed = { + r: resource.getrlimit(r) for r in tracked if resource.getrlimit(r) != before[r] + } + if changed: + for r, (soft, _hard) in changed.items(): # remet au moins la limite souple + try: + resource.setrlimit(r, (before[r][0], resource.getrlimit(r)[1])) + except (ValueError, OSError): + pass + pytest.fail( + "ce test a modifié les limites de ressources du process pytest " + f"({sorted(changed)}) — irréversible et valable pour toute la session ; " + "patcher resource.setrlimit ou utiliser un sous-processus", + pytrace=False, + ) + + @pytest.fixture def invalid_plugin_dir(plugins_dir: Path) -> Path: """Plugin invalide — manque PLUGIN_INFO.""" diff --git a/tests/unit/kernel/test_isolation.py b/tests/unit/kernel/test_isolation.py index 2d98ae53..2ed037fa 100644 --- a/tests/unit/kernel/test_isolation.py +++ b/tests/unit/kernel/test_isolation.py @@ -69,10 +69,31 @@ def test_apply_negative_noop(self): MemoryLimiter.apply(-1) # should not raise def test_apply_positive(self): + """ + Vérifie l'appel à setrlimit SANS l'exécuter : RLIMIT_AS est irréversible + (la limite dure baisse) et s'appliquerait à tout le process pytest — chaque + thread créé ensuite (TestClient, to_thread…) réserve ~72 Mo d'espace + d'adressage, et au bout de quelques-uns `can't start new thread`. Les tests + placés après celui-ci dans la session plantaient selon l'empreinte mémoire + de l'interpréteur (vu sur Python 3.14). + """ + import resource import sys + from unittest.mock import patch + + if sys.platform == "win32": + pytest.skip("Not supported on Windows") + with patch("resource.setrlimit") as mock_setrlimit: + MemoryLimiter.apply(512) + + limit = 512 * 1024 * 1024 + mock_setrlimit.assert_called_once_with(resource.RLIMIT_AS, (limit, limit)) + + def test_apply_failure_is_logged_not_raised(self): + import sys + from unittest.mock import patch + if sys.platform == "win32": pytest.skip("Not supported on Windows") - try: - MemoryLimiter.apply(512) # may raise or not depending on OS limits - except Exception: - pass # some systems don't allow raising RLIMIT_AS + with patch("resource.setrlimit", side_effect=ValueError("not allowed")): + MemoryLimiter.apply(512) # ne lève pas