This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new db2971251 feat(python): add Python 3.14 CI and wheels (#3781)
db2971251 is described below
commit db29712518a28d223288e2fb4d74c6a09b29ba9e
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Jun 23 19:04:45 2026 +0530
feat(python): add Python 3.14 CI and wheels (#3781)
## Why?
## What does this PR do?
## Related issues
Closes #3780
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.github/workflows/build-containerized-pr.yml | 2 +-
.github/workflows/build-containerized-release.yml | 1 +
.github/workflows/build-native-pr.yml | 8 ++---
.github/workflows/build-native-release.yml | 8 ++---
.github/workflows/ci.yml | 8 ++---
MODULE.bazel | 34 ++++++++++++++++--
bazel/cython_library.bzl | 11 ++++--
ci/build_linux_wheels.py | 44 ++++++++++++++++++++---
ci/deploy.sh | 9 +----
ci/tasks/python_container_build_script.sh | 12 ++++---
python/pyfory/serializer.py | 21 ++++++++++-
python/pyproject.toml | 1 +
12 files changed, 123 insertions(+), 36 deletions(-)
diff --git a/.github/workflows/build-containerized-pr.yml
b/.github/workflows/build-containerized-pr.yml
index fda2c28b9..55d516324 100644
--- a/.github/workflows/build-containerized-pr.yml
+++ b/.github/workflows/build-containerized-pr.yml
@@ -29,7 +29,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm]
- python: [cp313-cp313]
+ python: [cp314-cp314]
steps:
- uses: actions/checkout@v5
- name: Set up Bazel
diff --git a/.github/workflows/build-containerized-release.yml
b/.github/workflows/build-containerized-release.yml
index 51eb67c44..6e6899144 100644
--- a/.github/workflows/build-containerized-release.yml
+++ b/.github/workflows/build-containerized-release.yml
@@ -33,6 +33,7 @@ jobs:
- cp311-cp311
- cp312-cp312
- cp313-cp313
+ - cp314-cp314
steps:
- uses: actions/checkout@v5
- name: Set up Bazel
diff --git a/.github/workflows/build-native-pr.yml
b/.github/workflows/build-native-pr.yml
index d62da223c..6c7227a75 100644
--- a/.github/workflows/build-native-pr.yml
+++ b/.github/workflows/build-native-pr.yml
@@ -28,7 +28,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
- python-version: ['3.13']
+ python-version: ['3.14']
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
@@ -59,7 +59,7 @@ jobs:
runs-on: macos-15
strategy:
matrix:
- python-version: ['3.13']
+ python-version: ['3.14']
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
@@ -93,7 +93,7 @@ jobs:
runs-on: macos-15-intel
strategy:
matrix:
- python-version: ['3.13']
+ python-version: ['3.14']
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
@@ -128,7 +128,7 @@ jobs:
needs: [build-macos-arm64, build-macos-x86_64]
strategy:
matrix:
- python-version: ['3.13']
+ python-version: ['3.14']
steps:
- uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/build-native-release.yml
b/.github/workflows/build-native-release.yml
index 60d7aee6f..fac625cd2 100644
--- a/.github/workflows/build-native-release.yml
+++ b/.github/workflows/build-native-release.yml
@@ -25,7 +25,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v5
- name: Bump version
@@ -71,7 +71,7 @@ jobs:
runs-on: macos-15
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v5
- name: Bump version
@@ -118,7 +118,7 @@ jobs:
runs-on: macos-15-intel
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v5
- name: Bump version
@@ -166,7 +166,7 @@ jobs:
needs: [build-macos-arm64, build-macos-x86_64]
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 44290afb0..c46905dc2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1366,7 +1366,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- python-version: ["3.8", "3.12", "3.13"]
+ python-version: ["3.8", "3.12", "3.14"]
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
steps:
- uses: actions/checkout@v5
@@ -1406,9 +1406,9 @@ jobs:
runs-on: windows-2022
strategy:
matrix:
- # windows-2022 runners don't have Python 3.8 preinstalled, and pinned
3.13.3
- # forces a download/reinstall; use cached 3.11/3.12/3.13 on Windows
instead.
- python-version: ["3.11", "3.12", "3.13"]
+ # windows-2022 runners don't have Python 3.8 preinstalled; keep this
+ # matrix compact while covering the latest Python version.
+ python-version: ["3.11", "3.12", "3.14"]
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
diff --git a/MODULE.bazel b/MODULE.bazel
index 531f32fca..33361eddf 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -24,24 +24,52 @@ module(
bazel_dep(name = "platforms", version = "0.0.11")
# Bazel Skylib
-bazel_dep(name = "bazel_skylib", version = "1.7.1")
+bazel_dep(name = "bazel_skylib", version = "1.8.2")
# Rules CC (C++ rules)
bazel_dep(name = "rules_cc", version = "0.2.17")
# Rules Python - standard Python rules
-bazel_dep(name = "rules_python", version = "1.4.1")
+bazel_dep(name = "rules_python", version = "2.0.3")
# Configure Python toolchain to use version from BAZEL_PYTHON_VERSION env var
# This allows building extensions with the correct Python version headers
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+# rules_python 2.x no longer ships Python 3.8 metadata by default, but Fory
+# still tests and releases Python 3.8 wheels.
+python.single_version_override(
+ python_version = "3.8.20",
+ sha256 = {
+ "aarch64-apple-darwin":
"2ddfc04bdb3e240f30fb782fa1deec6323799d0e857e0b63fa299218658fd3d4",
+ "aarch64-unknown-linux-gnu":
"9d8798f9e79e0fc0f36fcb95bfa28a1023407d51a8ea5944b4da711f1f75f1ed",
+ "x86_64-apple-darwin":
"68d060cd373255d2ca5b8b3441363d5aa7cc45b0c11bbccf52b1717c2b5aa8bb",
+ "x86_64-pc-windows-msvc":
"41b6709fec9c56419b7de1940d1f87fa62045aff81734480672dcb807eedc47e",
+ "x86_64-unknown-linux-gnu":
"285e141c36f88b2e9357654c5f77d1f8fb29cc25132698fe35bb30d787f38e87",
+ },
+ urls =
["20241002/cpython-{python_version}+20241002-{platform}-{build}.tar.gz"],
+)
+python.override(
+ minor_mapping = {
+ "3.8": "3.8.20",
+ },
+)
python.toolchain(python_version = "3.8")
python.toolchain(python_version = "3.9")
python.toolchain(python_version = "3.10")
python.toolchain(python_version = "3.11")
python.toolchain(python_version = "3.12")
python.toolchain(python_version = "3.13")
-use_repo(python, "python_versions")
+python.toolchain(python_version = "3.14")
+use_repo(
+ python,
+ "python_3_8",
+ "python_3_9",
+ "python_3_10",
+ "python_3_11",
+ "python_3_12",
+ "python_3_13",
+ "python_3_14",
+)
# Cython from BCR with official rules
bazel_dep(name = "cython", version = "3.1.3")
diff --git a/bazel/cython_library.bzl b/bazel/cython_library.bzl
index 143994829..bcaefd22a 100644
--- a/bazel/cython_library.bzl
+++ b/bazel/cython_library.bzl
@@ -41,8 +41,15 @@ def pyx_library(name, deps = [], cc_kwargs = {}, py_deps =
[], srcs = [], **kwar
name = filename + "_cython_translation",
srcs = [filename],
outs = [filename.split(".")[0] + ".cpp"],
- cmd = "PYTHONHASHSEED=0 $(execpath @cython//:cython_binary)
--cplus $(SRCS) --output-file $(OUTS)",
- tools = ["@cython//:cython_binary"] + pxd_srcs,
+ # rules_python 2.x py_binary bootstrap requires Python 3.9+, while
+ # Fory still compiles extension modules against Python 3.8 headers.
+ cmd = (
+ "CYTHON_RUNFILES=$(execpath
@cython//:cython_binary).runfiles/cython+ && " +
+ "PYTHONHASHSEED=0 PYTHONPATH=$$CYTHON_RUNFILES " +
+ "$(execpath @python_3_9//:python3) " +
+ "$$CYTHON_RUNFILES/cython.py --cplus $(SRCS) --output-file
$(OUTS)"
+ ),
+ tools = ["@python_3_9//:python3", "@cython//:cython_binary"] +
pxd_srcs,
)
shared_objects = []
diff --git a/ci/build_linux_wheels.py b/ci/build_linux_wheels.py
index 041636a48..f2112658f 100755
--- a/ci/build_linux_wheels.py
+++ b/ci/build_linux_wheels.py
@@ -22,7 +22,7 @@ Host-side wrapper for building Python wheels in manylinux
containers.
Usage:
./build_linux_wheels.py --arch X86 --python cp38-cp38
- ./build_linux_wheels.py --arch AARCH64 --python cp313-cp313 --release
+ ./build_linux_wheels.py --arch AARCH64 --python cp314-cp314 --release
Environment:
- GITHUB_WORKSPACE (optional; defaults to cwd)
@@ -49,6 +49,14 @@ DEFAULT_AARCH64_IMAGES = [
"quay.io/pypa/manylinux2014_aarch64@sha256:e29044ae873f56799ea849c794a163246ac78120d7a6b458a17f918c05a143e1",
]
+MANYLINUX_2_28_X86_IMAGES = [
+
"quay.io/pypa/manylinux_2_28_x86_64@sha256:a694e7d81cdc90b1a3f4e8207d95d63a226df973dbd681a3b31599e90dd9436d",
+]
+
+MANYLINUX_2_28_AARCH64_IMAGES = [
+
"quay.io/pypa/manylinux_2_28_aarch64@sha256:817404d425b2edff4657a4bbf59e5a9fdb274609d31c99c1f9edc3be4426b00b",
+]
+
ARCH_ALIASES = {
"X86": "x86",
"X64": "x86",
@@ -60,13 +68,19 @@ ARCH_ALIASES = {
}
+def get_platform(python_version: str) -> str:
+ if python_version.startswith(("cp313-", "cp314-")):
+ return "manylinux_2_28"
+ return "manylinux2014"
+
+
def parse_args():
p = argparse.ArgumentParser()
p.add_argument(
"--arch", required=True, help="Architecture (e.g. X86, X64, AARCH64)"
)
p.add_argument(
- "--python", required=True, help="Python version (e.g. cp38-cp38,
cp313-cp313)"
+ "--python", required=True, help="Python version (e.g. cp38-cp38,
cp314-cp314)"
)
p.add_argument(
"--bazel-bin",
@@ -85,20 +99,31 @@ def normalize_arch(raw: str) -> str:
return ARCH_ALIASES.get(key, raw.strip().lower())
-def get_image_for_arch(arch_normalized: str) -> str:
+def get_image_for_arch(arch_normalized: str, platform_tag: str) -> str:
if arch_normalized == "x86":
+ if platform_tag == "manylinux_2_28":
+ return MANYLINUX_2_28_X86_IMAGES[0]
return DEFAULT_X86_IMAGES[0]
elif arch_normalized == "arm64":
+ if platform_tag == "manylinux_2_28":
+ return MANYLINUX_2_28_AARCH64_IMAGES[0]
return DEFAULT_AARCH64_IMAGES[0]
else:
raise SystemExit(f"Unsupported arch: {arch_normalized!r}")
+def auditwheel_plat(platform_tag: str, arch_normalized: str) -> str:
+ arch_tag = "x86_64" if arch_normalized == "x86" else "aarch64"
+ return f"{platform_tag}_{arch_tag}"
+
+
def build_docker_cmd(
workspace: str,
image: str,
python_version: str,
bazel_bin: str,
+ platform_tag: str,
+ arch_normalized: str,
release: bool = False,
) -> List[str]:
workspace = os.path.abspath(workspace)
@@ -118,6 +143,8 @@ def build_docker_cmd(
f"PYTHON_VERSIONS={python_version}",
"-e",
f"RELEASE_BUILD={'1' if release else '0'}",
+ "-e",
+ f"MANYLINUX_PLAT={auditwheel_plat(platform_tag, arch_normalized)}",
"-v",
f"{bazel_bin}:/usr/local/bin/bazel:ro",
]
@@ -132,7 +159,8 @@ def build_docker_cmd(
def main() -> int:
args = parse_args()
arch = normalize_arch(args.arch)
- image = get_image_for_arch(arch)
+ platform_tag = get_platform(args.python)
+ image = get_image_for_arch(arch, platform_tag)
workspace = os.environ.get("GITHUB_WORKSPACE", os.getcwd())
bazel_bin = args.bazel_bin or shutil.which("bazel")
@@ -151,7 +179,13 @@ def main() -> int:
return 2
docker_cmd = build_docker_cmd(
- workspace, image, args.python, bazel_bin, release=args.release
+ workspace,
+ image,
+ args.python,
+ bazel_bin,
+ platform_tag,
+ arch,
+ release=args.release,
)
printable = " ".join(shlex.quote(c) for c in docker_cmd)
print(f"+ {printable}")
diff --git a/ci/deploy.sh b/ci/deploy.sh
index c88ac210c..fbd5090a1 100755
--- a/ci/deploy.sh
+++ b/ci/deploy.sh
@@ -150,14 +150,7 @@ build_pyfory() {
}
install_pyarrow() {
- pyversion=$($PYTHON_CMD -V | cut -d' ' -f2)
- if [[ $pyversion == 3.13* ]]; then
- $PIP_CMD install pyarrow==18.0.0
- $PIP_CMD install numpy
- else
- $PIP_CMD install pyarrow==15.0.0
- # Automatically install numpy
- fi
+ $PIP_CMD install pyarrow numpy
}
deploy_scala() {
diff --git a/ci/tasks/python_container_build_script.sh
b/ci/tasks/python_container_build_script.sh
index 2c887c37e..0c306933b 100644
--- a/ci/tasks/python_container_build_script.sh
+++ b/ci/tasks/python_container_build_script.sh
@@ -66,11 +66,15 @@ for PY in $PYTHON_VERSIONS; do
export PATH="$PYTHON_BIN_DIR:$OLD_PATH"
hash -r
echo "Using $PYTHON_PATH"
- ARCH=$(uname -m)
- if [ "$ARCH" = "aarch64" ]; then
- export PLAT="manylinux2014_aarch64"
+ if [ -n "${MANYLINUX_PLAT:-}" ]; then
+ export PLAT="$MANYLINUX_PLAT"
else
- export PLAT="manylinux2014_x86_64"
+ ARCH=$(uname -m)
+ if [ "$ARCH" = "aarch64" ]; then
+ export PLAT="manylinux2014_aarch64"
+ else
+ export PLAT="manylinux2014_x86_64"
+ fi
fi
"$PYTHON_PATH" -m pip install cython wheel pytest auditwheel
ci/deploy.sh build_pyfory
diff --git a/python/pyfory/serializer.py b/python/pyfory/serializer.py
index bc07ed293..d3e43de30 100644
--- a/python/pyfory/serializer.py
+++ b/python/pyfory/serializer.py
@@ -1252,7 +1252,18 @@ class ReduceSerializer(Serializer):
raise ValueError(f"Invalid reduce data format flag:
{reduce_data[0]}")
-__skip_class_attr_names__ = ("__module__", "__qualname__", "__dict__",
"__weakref__")
+# Python 3.14 stores lazy class annotations in an internal function that closes
+# over the class namespace. Serialize materialized annotations instead so the
+# class dictionary does not contain a self-reference through that closure.
+__skip_class_attr_names__ = (
+ "__module__",
+ "__qualname__",
+ "__dict__",
+ "__weakref__",
+ "__annotate__",
+ "__annotate_func__",
+ "__annotations_cache__",
+)
class TypeSerializer(Serializer):
@@ -1316,6 +1327,14 @@ class TypeSerializer(Serializer):
class_method = class_methods[i]
write_context.write_ref(class_method.__func__)
+ if (
+ "__annotations__" in cls.__dict__
+ or "__annotations_cache__" in cls.__dict__
+ or "__annotate__" in cls.__dict__
+ or "__annotate_func__" in cls.__dict__
+ ):
+ class_dict["__annotations__"] = dict(getattr(cls,
"__annotations__", {}))
+
write_context.write_ref(class_dict)
def _deserialize_local_class(self, read_context):
diff --git a/python/pyproject.toml b/python/pyproject.toml
index fa18ca9b4..e0d4d0352 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -47,6 +47,7 @@ classifiers = [
"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",
]
keywords = ["fory", "serialization", "multi-language", "fast", "row-format",
"jit", "codegen", "polymorphic", "zero-copy"]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]