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]

Reply via email to