Skip to content

Commit dbd34c2

Browse files
committed
Enable support for free-threading
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 depends on #1294 and #1289.
1 parent 52acdf4 commit dbd34c2

File tree

6 files changed

+59
-88
lines changed

6 files changed

+59
-88
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
os: [macos-latest, ubuntu-latest, windows-latest]
16-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
16+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"]
1717
steps:
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.10"
21+
- name: Install hatch
22+
run: pip install -U hatch
1823
- uses: actions/checkout@v4
1924
with:
2025
fetch-depth: 0
@@ -24,9 +29,6 @@ jobs:
2429
cache: pip
2530
cache-dependency-path: "pyproject.toml"
2631
python-version: ${{ matrix.python-version }}
27-
- name: Install hatch
28-
run: |
29-
pip install -U hatch
3032
- uses: actions-rs/toolchain@v1
3133
with:
3234
toolchain: stable
@@ -44,50 +46,6 @@ jobs:
4446
hatch run coverage combine .coverage.pure
4547
hatch run coverage report
4648
47-
# TODO:
48-
# merge into regular CI once hatch has support for creating environments on
49-
# the free-threaded build: https://github.com/pypa/hatch/issues/1931
50-
free-threaded-tests:
51-
name: "test (${{ matrix.os }}, 3.13t)"
52-
runs-on: ${{ matrix.os }}
53-
strategy:
54-
fail-fast: false
55-
matrix:
56-
os: [macos-latest, ubuntu-latest, windows-latest]
57-
steps:
58-
- uses: actions/checkout@v4
59-
with:
60-
fetch-depth: 0
61-
persist-credentials: false
62-
- uses: actions/setup-python@v5
63-
with:
64-
cache: pip
65-
cache-dependency-path: "pyproject.toml"
66-
python-version: '3.13t'
67-
- name: Build LibCST
68-
run: |
69-
# Install build-system.requires dependencies
70-
pip install setuptools setuptools-scm setuptools-rust wheel
71-
# Jupyter is annoying to install on free-threaded Python
72-
pip install -e .[dev-without-jupyter]
73-
- name: Native Parser Tests
74-
# TODO: remove when native modules declare free-threaded support
75-
env:
76-
PYTHON_GIL: '0'
77-
run: |
78-
python -m coverage run -m libcst.tests
79-
- name: Pure Parser Tests
80-
env:
81-
COVERAGE_FILE: .coverage.pure
82-
LIBCST_PARSER_TYPE: pure
83-
run: |
84-
python -m coverage run -m libcst.tests
85-
- name: Coverage
86-
run: |
87-
python -m coverage combine .coverage.pure
88-
python -m coverage report
89-
90-
9149
# Run linters
9250
lint:
9351
runs-on: ubuntu-latest
@@ -139,7 +97,7 @@ jobs:
13997
- name: Install hatch
14098
run: pip install -U hatch
14199
- uses: ts-graphviz/setup-graphviz@v2
142-
- run: hatch run docs
100+
- run: hatch run docs:docs
143101
- name: Archive Docs
144102
uses: actions/upload-artifact@v4
145103
with:

libcst/codemod/_cli.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@
88
"""
99

1010
import difflib
11+
import functools
1112
import os.path
1213
import re
1314
import subprocess
1415
import sys
1516
import time
1617
import traceback
17-
from concurrent.futures import as_completed, Executor, ProcessPoolExecutor
18+
from concurrent.futures import as_completed, Executor
1819
from copy import deepcopy
1920
from dataclasses import dataclass
2021
from multiprocessing import cpu_count
2122
from pathlib import Path
22-
from typing import AnyStr, cast, Dict, List, Optional, Sequence, Type, Union
23+
from typing import AnyStr, Callable, cast, Dict, List, Optional, Sequence, Type, Union
2324
from warnings import warn
2425

2526
from libcst import parse_module, PartialParserConfig
@@ -624,14 +625,20 @@ def parallel_exec_transform_with_prettyprint( # noqa: C901
624625
python_version=python_version,
625626
)
626627

627-
pool_impl: type[Executor]
628+
pool_impl: Callable[[], Executor]
628629
if total == 1 or jobs == 1:
629630
# Simple case, we should not pay for process overhead.
630631
# Let's just use a dummy synchronous executor.
631632
jobs = 1
632633
pool_impl = DummyExecutor
634+
elif getattr(sys, "_is_gil_enabled", lambda: False)(): # pyre-ignore[16]
635+
from concurrent.futures import ThreadPoolExecutor
636+
637+
pool_impl = functools.partial(ThreadPoolExecutor, max_workers=jobs)
633638
else:
634-
pool_impl = ProcessPoolExecutor
639+
from concurrent.futures import ProcessPoolExecutor
640+
641+
pool_impl = functools.partial(ProcessPoolExecutor, max_workers=jobs)
635642
# Warm the parser, pre-fork.
636643
parse_module(
637644
"",
@@ -650,7 +657,7 @@ def parallel_exec_transform_with_prettyprint( # noqa: C901
650657
deepcopy(transform.context.scratch) if isinstance(transform, Codemod) else {}
651658
)
652659

653-
with pool_impl(max_workers=jobs) as executor: # type: ignore
660+
with pool_impl() as executor: # type: ignore
654661
try:
655662
futures = [
656663
executor.submit(

libcst/codemod/_dummy_pool.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ class DummyExecutor(Executor):
2222
Synchronous dummy `concurrent.futures.Executor` analogue.
2323
"""
2424

25-
def __init__(self, max_workers: Optional[int] = None) -> None:
26-
pass
27-
2825
def submit(
2926
self,
3027
fn: Callable[Params, Return],

libcst/metadata/full_repo_manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66

77
from pathlib import Path
8+
from threading import RLock
89
from typing import Collection, Dict, List, Mapping, TYPE_CHECKING
910

1011
import libcst as cst
@@ -38,6 +39,7 @@ def __init__(
3839
"""
3940
self.root_path: Path = Path(repo_root_dir)
4041
self._cache: Dict["ProviderT", Mapping[str, object]] = {}
42+
self._cache_lock = RLock()
4143
self._timeout = timeout
4244
self._use_pyproject_toml = use_pyproject_toml
4345
self._providers = providers
@@ -61,7 +63,11 @@ def resolve_cache(self) -> None:
6163
forking, it is a good idea to call this explicitly to control when cache
6264
resolution happens.
6365
"""
64-
if not self._cache:
66+
if self._cache:
67+
return
68+
with self._cache_lock:
69+
if self._cache:
70+
return
6571
cache: Dict["ProviderT", Mapping[str, object]] = {}
6672
for provider in self._providers:
6773
handler = provider.gen_cache

native/libcst/src/py.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use crate::nodes::traits::py::TryIntoPy;
77
use pyo3::prelude::*;
88

9-
#[pymodule]
9+
#[pymodule(gil_used = false)]
1010
#[pyo3(name = "native")]
1111
pub fn libcst_native(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
1212
#[pyfn(m)]

pyproject.toml

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,9 @@ classifiers = [
1818
"Typing :: Typed",
1919
]
2020
requires-python = ">=3.9"
21-
dependencies = ["pyyaml>=5.2"]
22-
23-
[project.optional-dependencies]
24-
dev = [
25-
"libcst[dev-without-jupyter]",
26-
"jupyter>=1.0.0",
27-
"nbsphinx>=0.4.2",
28-
]
29-
dev-without-jupyter = [
30-
"black==25.1.0",
31-
"coverage[toml]>=4.5.4",
32-
"build>=0.10.0",
33-
"fixit==2.1.0",
34-
"flake8==7.2.0",
35-
"Sphinx>=5.1.1",
36-
"hypothesis>=4.36.0",
37-
"hypothesmith>=0.0.4",
38-
"maturin>=1.7.0,<1.8",
39-
"prompt-toolkit>=2.0.9",
40-
"pyre-check==0.9.18; platform_system != 'Windows'",
41-
"setuptools_scm>=6.0.1",
42-
"sphinx-rtd-theme>=0.4.3",
43-
"ufmt==2.8.0",
44-
"usort==1.0.8.post1",
45-
"setuptools-rust>=1.5.2",
46-
"slotscheck>=0.7.1",
47-
"jinja2==3.1.6",
21+
dependencies = [
22+
"pyyaml>=5.2; python_version < '3.13'",
23+
"pyyaml-ft; python_version >= '3.13'",
4824
]
4925

5026
[project.urls]
@@ -63,10 +39,26 @@ show_missing = true
6339
skip_covered = true
6440

6541
[tool.hatch.envs.default]
66-
features = ["dev"]
42+
installer = "uv"
43+
dependencies = [
44+
"black==25.1.0",
45+
"coverage[toml]>=4.5.4",
46+
"build>=0.10.0",
47+
"fixit==2.1.0",
48+
"flake8==7.2.0",
49+
"hypothesis>=4.36.0",
50+
"hypothesmith>=0.0.4",
51+
"maturin>=1.7.0,<1.8",
52+
"prompt-toolkit>=2.0.9",
53+
"pyre-check==0.9.18; platform_system != 'Windows'",
54+
"setuptools_scm>=6.0.1",
55+
"ufmt==2.8.0",
56+
"usort==1.0.8.post1",
57+
"setuptools-rust>=1.5.2",
58+
"slotscheck>=0.7.1",
59+
]
6760

6861
[tool.hatch.envs.default.scripts]
69-
docs = "sphinx-build -ab html docs/source docs/build"
7062
fixtures = ["python scripts/regenerate-fixtures.py", "git diff --exit-code"]
7163
format = "ufmt format libcst scripts"
7264
lint = [
@@ -78,6 +70,17 @@ lint = [
7870
test = ["python --version", "python -m coverage run -m libcst.tests"]
7971
typecheck = ["pyre --version", "pyre check"]
8072

73+
[tool.hatch.envs.docs]
74+
extra-dependencies = [
75+
"Sphinx>=5.1.1",
76+
"sphinx-rtd-theme>=0.4.3",
77+
"jupyter>=1.0.0",
78+
"nbsphinx>=0.4.2",
79+
"jinja2==3.1.6",
80+
]
81+
[tool.hatch.envs.docs.scripts]
82+
docs = "sphinx-build -ab html docs/source docs/build"
83+
8184
[tool.slotscheck]
8285
exclude-modules = '^libcst\.(testing|tests)'
8386

0 commit comments

Comments
 (0)