Enable support for free-threading (#1295)

This PR:
1. marks the `libcst.native` module as free-threading-compatible
2. replaces the use of ProcessPoolExecutor with ThreadPoolExecutor if free-threaded CPython is detected at runtime
This commit is contained in:
Zsolt Dollenstein 2025-05-25 11:43:18 +01:00 committed by GitHub
parent 52acdf4163
commit 16ed48d74b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 52 additions and 87 deletions

View file

@ -13,8 +13,13 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"]
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install hatch
run: pip install -U hatch
- uses: actions/checkout@v4
with:
fetch-depth: 0
@ -24,9 +29,6 @@ jobs:
cache: pip
cache-dependency-path: "pyproject.toml"
python-version: ${{ matrix.python-version }}
- name: Install hatch
run: |
pip install -U hatch
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@ -44,50 +46,6 @@ jobs:
hatch run coverage combine .coverage.pure
hatch run coverage report
# TODO:
# merge into regular CI once hatch has support for creating environments on
# the free-threaded build: https://github.com/pypa/hatch/issues/1931
free-threaded-tests:
name: "test (${{ matrix.os }}, 3.13t)"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@v5
with:
cache: pip
cache-dependency-path: "pyproject.toml"
python-version: '3.13t'
- name: Build LibCST
run: |
# Install build-system.requires dependencies
pip install setuptools setuptools-scm setuptools-rust wheel
# Jupyter is annoying to install on free-threaded Python
pip install -e .[dev-without-jupyter]
- name: Native Parser Tests
# TODO: remove when native modules declare free-threaded support
env:
PYTHON_GIL: '0'
run: |
python -m coverage run -m libcst.tests
- name: Pure Parser Tests
env:
COVERAGE_FILE: .coverage.pure
LIBCST_PARSER_TYPE: pure
run: |
python -m coverage run -m libcst.tests
- name: Coverage
run: |
python -m coverage combine .coverage.pure
python -m coverage report
# Run linters
lint:
runs-on: ubuntu-latest
@ -139,7 +97,7 @@ jobs:
- name: Install hatch
run: pip install -U hatch
- uses: ts-graphviz/setup-graphviz@v2
- run: hatch run docs
- run: hatch run docs:docs
- name: Archive Docs
uses: actions/upload-artifact@v4
with:

View file

@ -8,18 +8,19 @@ Provides helpers for CLI interaction.
"""
import difflib
import functools
import os.path
import re
import subprocess
import sys
import time
import traceback
from concurrent.futures import as_completed, Executor, ProcessPoolExecutor
from concurrent.futures import as_completed, Executor
from copy import deepcopy
from dataclasses import dataclass
from multiprocessing import cpu_count
from pathlib import Path
from typing import AnyStr, cast, Dict, List, Optional, Sequence, Type, Union
from typing import AnyStr, Callable, cast, Dict, List, Optional, Sequence, Type, Union
from warnings import warn
from libcst import parse_module, PartialParserConfig
@ -624,14 +625,20 @@ def parallel_exec_transform_with_prettyprint( # noqa: C901
python_version=python_version,
)
pool_impl: type[Executor]
pool_impl: Callable[[], Executor]
if total == 1 or jobs == 1:
# Simple case, we should not pay for process overhead.
# Let's just use a dummy synchronous executor.
jobs = 1
pool_impl = DummyExecutor
elif getattr(sys, "_is_gil_enabled", lambda: False)(): # pyre-ignore[16]
from concurrent.futures import ThreadPoolExecutor
pool_impl = functools.partial(ThreadPoolExecutor, max_workers=jobs)
else:
pool_impl = ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
pool_impl = functools.partial(ProcessPoolExecutor, max_workers=jobs)
# Warm the parser, pre-fork.
parse_module(
"",
@ -650,7 +657,7 @@ def parallel_exec_transform_with_prettyprint( # noqa: C901
deepcopy(transform.context.scratch) if isinstance(transform, Codemod) else {}
)
with pool_impl(max_workers=jobs) as executor: # type: ignore
with pool_impl() as executor: # type: ignore
try:
futures = [
executor.submit(

View file

@ -22,9 +22,6 @@ class DummyExecutor(Executor):
Synchronous dummy `concurrent.futures.Executor` analogue.
"""
def __init__(self, max_workers: Optional[int] = None) -> None:
pass
def submit(
self,
fn: Callable[Params, Return],

View file

@ -6,7 +6,7 @@
use crate::nodes::traits::py::TryIntoPy;
use pyo3::prelude::*;
#[pymodule]
#[pymodule(gil_used = false)]
#[pyo3(name = "native")]
pub fn libcst_native(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
#[pyfn(m)]

View file

@ -18,33 +18,9 @@ classifiers = [
"Typing :: Typed",
]
requires-python = ">=3.9"
dependencies = ["pyyaml>=5.2"]
[project.optional-dependencies]
dev = [
"libcst[dev-without-jupyter]",
"jupyter>=1.0.0",
"nbsphinx>=0.4.2",
]
dev-without-jupyter = [
"black==25.1.0",
"coverage[toml]>=4.5.4",
"build>=0.10.0",
"fixit==2.1.0",
"flake8==7.2.0",
"Sphinx>=5.1.1",
"hypothesis>=4.36.0",
"hypothesmith>=0.0.4",
"maturin>=1.7.0,<1.8",
"prompt-toolkit>=2.0.9",
"pyre-check==0.9.18; platform_system != 'Windows'",
"setuptools_scm>=6.0.1",
"sphinx-rtd-theme>=0.4.3",
"ufmt==2.8.0",
"usort==1.0.8.post1",
"setuptools-rust>=1.5.2",
"slotscheck>=0.7.1",
"jinja2==3.1.6",
dependencies = [
"pyyaml>=5.2; python_version < '3.13'",
"pyyaml-ft; python_version >= '3.13'",
]
[project.urls]
@ -63,10 +39,26 @@ show_missing = true
skip_covered = true
[tool.hatch.envs.default]
features = ["dev"]
installer = "uv"
dependencies = [
"black==25.1.0",
"coverage[toml]>=4.5.4",
"build>=0.10.0",
"fixit==2.1.0",
"flake8==7.2.0",
"hypothesis>=4.36.0",
"hypothesmith>=0.0.4",
"maturin>=1.7.0,<1.8",
"prompt-toolkit>=2.0.9",
"pyre-check==0.9.18; platform_system != 'Windows'",
"setuptools_scm>=6.0.1",
"ufmt==2.8.0",
"usort==1.0.8.post1",
"setuptools-rust>=1.5.2",
"slotscheck>=0.7.1",
]
[tool.hatch.envs.default.scripts]
docs = "sphinx-build -ab html docs/source docs/build"
fixtures = ["python scripts/regenerate-fixtures.py", "git diff --exit-code"]
format = "ufmt format libcst scripts"
lint = [
@ -78,6 +70,17 @@ lint = [
test = ["python --version", "python -m coverage run -m libcst.tests"]
typecheck = ["pyre --version", "pyre check"]
[tool.hatch.envs.docs]
extra-dependencies = [
"Sphinx>=5.1.1",
"sphinx-rtd-theme>=0.4.3",
"jupyter>=1.0.0",
"nbsphinx>=0.4.2",
"jinja2==3.1.6",
]
[tool.hatch.envs.docs.scripts]
docs = "sphinx-build -ab html docs/source docs/build"
[tool.slotscheck]
exclude-modules = '^libcst\.(testing|tests)'