diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 296ac34a3fb..7ba2a84d049 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: @@ -41,9 +41,15 @@ jobs: python -m pip install --upgrade tox - name: Unit tests + if: "!startsWith(matrix.python-version, 'pypy')" run: | tox -e ci-py -- -v --color=yes + - name: Unit tests pypy + if: "startsWith(matrix.python-version, 'pypy')" + run: | + tox -e ci-pypy3 -- -v --color=yes + - name: Publish coverage to Coveralls # If pushed / is a pull request against main repo AND # we're running on Linux (this action only supports Linux) diff --git a/CHANGES.md b/CHANGES.md index b2e8f7439b7..c8b9c849815 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Warn about Python 2 deprecation in more cases by improving Python 2 only syntax detection (#2592) +- Add experimental PyPy support (#2559) - Add partial support for the match statement. As it's experimental, it's only enabled when `--target-version py310` is explicitly specified (#2586) - Add support for parenthesized with (#2586) diff --git a/docs/faq.md b/docs/faq.md index 77f9df51fd4..72bae6b389d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -92,3 +92,8 @@ influence their behavior. While Black does its best to recognize such comments a them in the right place, this detection is not and cannot be perfect. Therefore, you'll sometimes have to manually move these comments to the right place after you format your codebase with _Black_. + +## Can I run black with PyPy? + +Yes, there is support for PyPy 3.7 and higher. You cannot format Python 2 files under +PyPy, because PyPy's inbuilt ast module does not support this. diff --git a/setup.py b/setup.py index de84dc37bb8..a0c2006ef33 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def get_long_description() -> str: "click>=7.1.2", "platformdirs>=2", "tomli>=0.2.6,<2.0.0", - "typed-ast>=1.4.2; python_version < '3.8'", + "typed-ast>=1.4.2; python_version < '3.8' and implementation_name == 'cpython'", "regex>=2020.1.8", "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", diff --git a/src/black/cache.py b/src/black/cache.py index 3f165de2ed6..bca7279f990 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -35,7 +35,7 @@ def read_cache(mode: Mode) -> Cache: with cache_file.open("rb") as fobj: try: cache: Cache = pickle.load(fobj) - except (pickle.UnpicklingError, ValueError): + except (pickle.UnpicklingError, ValueError, IndexError): return {} return cache diff --git a/src/black/parsing.py b/src/black/parsing.py index fc540ad021d..ee6aae1e7ff 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -2,6 +2,7 @@ Parse Python code and perform AST validation. """ import ast +import platform import sys from typing import Iterable, Iterator, List, Set, Union, Tuple @@ -15,10 +16,13 @@ from black.mode import TargetVersion, Feature, supports_feature from black.nodes import syms +_IS_PYPY = platform.python_implementation() == "PyPy" + try: from typed_ast import ast3, ast27 except ImportError: - if sys.version_info < (3, 8): + # Either our python version is too low, or we're on pypy + if sys.version_info < (3, 7) or (sys.version_info < (3, 8) and not _IS_PYPY): print( "The typed_ast package is required but not installed.\n" "You can upgrade to Python 3.8+ or install typed_ast with\n" @@ -117,7 +121,10 @@ def parse_single_version( if sys.version_info >= (3, 8) and version >= (3,): return ast.parse(src, filename, feature_version=version) elif version >= (3,): - return ast3.parse(src, filename, feature_version=version[1]) + if _IS_PYPY: + return ast3.parse(src, filename) + else: + return ast3.parse(src, filename, feature_version=version[1]) elif version == (2, 7): return ast27.parse(src) raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!") @@ -151,12 +158,14 @@ def stringify_ast( yield f"{' ' * depth}{node.__class__.__name__}(" for field in sorted(node._fields): # noqa: F402 - # TypeIgnore has only one field 'lineno' which breaks this comparison - type_ignore_classes = (ast3.TypeIgnore, ast27.TypeIgnore) - if sys.version_info >= (3, 8): - type_ignore_classes += (ast.TypeIgnore,) - if isinstance(node, type_ignore_classes): - break + # TypeIgnore will not be present using pypy < 3.8, so need for this + if not (_IS_PYPY and sys.version_info < (3, 8)): + # TypeIgnore has only one field 'lineno' which breaks this comparison + type_ignore_classes = (ast3.TypeIgnore, ast27.TypeIgnore) + if sys.version_info >= (3, 8): + type_ignore_classes += (ast.TypeIgnore,) + if isinstance(node, type_ignore_classes): + break try: value = getattr(node, field) diff --git a/tests/test_black.py b/tests/test_black.py index 7dbc3809d26..301a3a5b363 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -944,7 +944,7 @@ def test_broken_symlink(self) -> None: symlink = workspace / "broken_link.py" try: symlink.symlink_to("nonexistent.py") - except OSError as e: + except (OSError, NotImplementedError) as e: self.skipTest(f"Can't create symlinks: {e}") self.invokeBlack([str(workspace.resolve())]) diff --git a/tox.ini b/tox.ini index 57f41acb3d1..683a5439ea9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {,ci-}py{36,37,38,39,310},fuzz +envlist = {,ci-}py{36,37,38,39,310,py3},fuzz [testenv] setenv = PYTHONPATH = {toxinidir}/src @@ -31,6 +31,31 @@ commands = --cov --cov-append {posargs} coverage report +[testenv:{,ci-}pypy3] +setenv = PYTHONPATH = {toxinidir}/src +skip_install = True +recreate = True +deps = + -r{toxinidir}/test_requirements.txt +; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317 +; this seems to cause tox to wait forever +; remove this when pypy releases the bugfix +commands = + pip install -e .[d] + coverage erase + pytest tests --run-optional no_python2 \ + --run-optional no_jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov {posargs} + pip install -e .[jupyter] + pytest tests --run-optional jupyter \ + -m jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov --cov-append {posargs} + coverage report + [testenv:fuzz] skip_install = True deps =