diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67333e3c0..75abc888e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/libcst/codemod/_cli.py b/libcst/codemod/_cli.py index 71143d698..178cabcb8 100644 --- a/libcst/codemod/_cli.py +++ b/libcst/codemod/_cli.py @@ -8,18 +8,19 @@ """ 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( diff --git a/libcst/codemod/_dummy_pool.py b/libcst/codemod/_dummy_pool.py index 34c911bdc..aa23a7d49 100644 --- a/libcst/codemod/_dummy_pool.py +++ b/libcst/codemod/_dummy_pool.py @@ -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], diff --git a/native/libcst/src/py.rs b/native/libcst/src/py.rs index bd7dfe6d3..57da11e78 100644 --- a/native/libcst/src/py.rs +++ b/native/libcst/src/py.rs @@ -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) -> PyResult<()> { #[pyfn(m)] diff --git a/pyproject.toml b/pyproject.toml index 6bf33d40c..27dbb710c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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)' @@ -95,6 +98,7 @@ skip = [ "*-musllinux_s390x", "*-musllinux_armv7l", ] +enable = ["cpython-freethreading"] [tool.cibuildwheel.linux] environment-pass = ["LIBCST_NO_LOCAL_SCHEME"]