From c76bf888a7e28660096fdc67aae3688ea9f3be5e Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 22 Jan 2023 22:43:00 +0200 Subject: [PATCH 01/13] feat: extras option and test dependencies for pyupgrade --- .github/workflows/pylint.yml | 1 + setup.cfg | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 7005eb088..85e4b067a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -23,6 +23,7 @@ jobs: pygments \ 'pylint<=3.2.7' \ pytest>=6.2.0 \ + pyupgrade>=2.31.0 \ regex \ requests \ requests-cache \ diff --git a/setup.cfg b/setup.cfg index 2f1c3ccef..2c7078f50 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,6 +59,8 @@ isort = isort>=5.0.1 color = Pygments>=2.4.0 +pyupgrade = + pyupgrade>=2.31.0 test = # NOTE: remember to keep `constraints-oldest.txt` in sync with these black>=22.3.0 @@ -74,6 +76,7 @@ test = pylint<=3.2.7 # pylint 3.3.0 dropped Python 3.8 support pytest>=6.2.0 pytest-kwparametrize>=0.0.3 + pyupgrade>=2.31.0 regex>=2021.4.4 requests_cache>=0.7 ruamel.yaml>=0.17.21 From 3eaabc1c37ee55578a047b67be760323e9824166 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 22 Jan 2023 22:43:00 +0200 Subject: [PATCH 02/13] test: ignore missing imports for pyupgrade in Mypy --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index eeb3faaf6..af0e1d623 100644 --- a/mypy.ini +++ b/mypy.ini @@ -81,6 +81,9 @@ ignore_missing_imports = True [mypy-pytest.*] ignore_missing_imports = True +[mypy-pyupgrade.*] +ignore_missing_imports = True + [mypy-setuptools.*] ignore_missing_imports = True From b2a6e721da6f643e58df7fbfe81c8f13d2980c91 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:55:54 +0300 Subject: [PATCH 03/13] feat: pyupgrade formatter plugin --- setup.cfg | 1 + src/darker/formatters/pyupgrade_config.py | 11 ++ src/darker/formatters/pyupgrade_formatter.py | 142 +++++++++++++++++++ src/darker/formatters/pyupgrade_wrapper.py | 34 +++++ 4 files changed, 188 insertions(+) create mode 100644 src/darker/formatters/pyupgrade_config.py create mode 100644 src/darker/formatters/pyupgrade_formatter.py create mode 100644 src/darker/formatters/pyupgrade_wrapper.py diff --git a/setup.cfg b/setup.cfg index 2c7078f50..b4eaac47e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ darker = darker.formatter = black = darker.formatters.black_formatter:BlackFormatter ruff = darker.formatters.ruff_formatter:RuffFormatter + pyupgrade = darker.formatters.pyupgrade_formatter:PyupgradeFormatter none = darker.formatters.none_formatter:NoneFormatter console_scripts = darker = darker.__main__:main_with_error_handling diff --git a/src/darker/formatters/pyupgrade_config.py b/src/darker/formatters/pyupgrade_config.py new file mode 100644 index 000000000..3c363024c --- /dev/null +++ b/src/darker/formatters/pyupgrade_config.py @@ -0,0 +1,11 @@ +"""Pyupgrade code formatter plugin configuration type definitions.""" + +from __future__ import annotations + +from darker.formatters.formatter_config import FormatterConfig + + +class PyupgradeConfig(FormatterConfig, total=False): + """Type definition for configuration dictionaries of Black compatible formatters.""" + + target_version: tuple[int, int] diff --git a/src/darker/formatters/pyupgrade_formatter.py b/src/darker/formatters/pyupgrade_formatter.py new file mode 100644 index 000000000..f87b96d45 --- /dev/null +++ b/src/darker/formatters/pyupgrade_formatter.py @@ -0,0 +1,142 @@ +"""Re-format Python source code using Pyupgrade. + +In examples below, a simple two-line snippet is used. +Everything will upgraded by Pyupgrade to newer Python syntax, except the last line:: + + >>> from pathlib import Path + >>> from unittest.mock import Mock + >>> src = Path("dummy/file/path.py") + >>> src_content = TextDocument.from_lines( + ... [ + ... "from typing import List", + ... "ls: List[int] = [42]", + ... "print('success!')" + ... ] + ... ) + +First, `PyupgradeFormatter.run` uses Pyupgrade to upgrade the contents of a given file. +All lines are returned e.g.:: + + >>> from darker.formatters.pyupgrade_formatter import PyupgradeFormatter + >>> dst = PyupgradeFormatter().run(src_content, src) + >>> dst.lines + ('from typing import List', 'ls: list[int] = [42]', "print('success!')") + +See :mod:`darker.diff` and :mod:`darker.chooser` +for how this result is further processed with: + +- :func:`~darker.diff.diff_and_get_opcodes` + to get a diff of the reformatting +- :func:`~darker.diff.opcodes_to_chunks` + to split the diff into chunks of original and reformatted content +- :func:`~darker.chooser.choose_lines` + to reconstruct the source code from original and reformatted chunks + based on whether reformats touch user-edited lines + +""" + +from __future__ import annotations + +import io +import logging +import sys +from typing import TYPE_CHECKING + +from darker.formatters.base_formatter import BaseFormatter, HasConfig +from darker.formatters.formatter_config import validate_target_versions +from darker.formatters.pyupgrade_config import PyupgradeConfig +from darkgraylib.utils import TextDocument + +if TYPE_CHECKING: + from argparse import Namespace + from pathlib import Path + +logger = logging.getLogger(__name__) + + +class PyupgradeFormatter(BaseFormatter, HasConfig[PyupgradeConfig]): + """Pyupgrade code formatter plugin interface.""" + + config: PyupgradeConfig # type: ignore[assignment] + + name = "pyupgrade" + + def run( + self, content: TextDocument, path_from_cwd: Path # noqa: ARG002 + ) -> TextDocument: + """Run the Pyupgrade code upgrader for the Python source code given as a string. + + :param content: The source code + :param path_from_cwd: The path to the file being upgraded, either absolute or + relative to the current working directory + :return: The upgraded content + + """ + # Collect relevant Pyupgrade configuration options from ``self.config`` in order + # to pass them to Pyupgrade. + if "target_version" in self.config: + supported_target_versions = _get_supported_target_versions() + target_versions_in = validate_target_versions( + self.config["target_version"], supported_target_versions + ) + target_version_str = min(target_versions_in) + target_version = (int(target_version_str[0]), int(target_version_str[1:])) + else: + target_version = (3, 9) + + contents_for_pyupgrade = content.string_with_newline("\n") + dst_contents = _pyupgrade_format_stdin(contents_for_pyupgrade, target_version) + return TextDocument.from_str( + dst_contents, + encoding=content.encoding, + override_newline=content.newline, + ) + + def _read_cli_args(self, args: Namespace) -> None: + if getattr(args, "target_version", None): + self.config["target_version"] = {args.target_version} + + +def _get_supported_target_versions() -> set[tuple[int, int]]: + """Get the supported target versions for Pyupgrade. + + Calls ``pyupgrade --help`` as a subprocess, looks for lines looking like + `` --py???-plus``, and returns the target versions as a set of int-tuples. + + """ + # Local import so Darker can be run also without pyupgrade installed + # pylint: disable=import-outside-toplevel + from darker.formatters.pyupgrade_wrapper import main + + stdout = sys.stdout + sys.stdout = buf = io.StringIO() + try: + main(["--help"]) + finally: + sys.stdout = stdout + version_strs = ( + line[6:-5] + for line in buf.getvalue().splitlines() + if line.startswith(" --py") and line.endswith("-plus") + ) + return {(int(v[0]), int(v[1:])) for v in version_strs} + + +def _pyupgrade_format_stdin(contents: str, min_version: tuple[int, int]) -> str: + """Run the contents through ``pyupgrade format``. + + :param contents: The source code to be reformatted + :param min_version: The minimum Python version to target + :return: The reformatted source code + + """ + # Local imports so Darker can be run also without pyupgrade installed + from darker.formatters.pyupgrade_wrapper import ( # pylint: disable=import-outside-toplevel + Settings, + _fix_plugins, + _fix_tokens, + ) + + return _fix_tokens( + _fix_plugins(contents, settings=Settings(min_version=min_version)) + ) diff --git a/src/darker/formatters/pyupgrade_wrapper.py b/src/darker/formatters/pyupgrade_wrapper.py new file mode 100644 index 000000000..acd2f1d3b --- /dev/null +++ b/src/darker/formatters/pyupgrade_wrapper.py @@ -0,0 +1,34 @@ +"""Attempt to import Pyupgrade internals needed by the Pyupgrade formatter plugin.""" + +import logging + +from darker.exceptions import DependencyError + +logger = logging.getLogger(__name__) + +try: + import pyupgrade # noqa: F401 # pylint: disable=unused-import +except ImportError as exc: + logger.warning( + "To update modified code using Pyupgrade, install it using e.g." + " `pip install 'darker[pyupgrade]'` or" + " `pip install pyupgrade`" + ) + logger.warning( + "To use a different formatter or no formatter, select it on the" + " command line (e.g. `--formatter=none`) or configuration" + " (e.g. `formatter=none`)" + ) + MESSAGE = "Can't find the Pyupgrade package" + raise DependencyError(MESSAGE) from exc + +# pylint: disable=wrong-import-position +from pyupgrade._data import Settings # noqa: E402 +from pyupgrade._main import _fix_plugins, _fix_tokens, main # noqa: E402 + +__all__ = [ + "Settings", + "_fix_plugins", + "_fix_tokens", + "main", +] From 6470f179f560b697ddff24dc3f58b6461535bea4 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:12:00 +0300 Subject: [PATCH 04/13] chore: tweak ruff rules --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index c0146ee94..522b2d95f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ ignore = [ "D203", # One blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line "D400", # First line should end with a period (duplicates D415) + "ISC001", # Checks for implicitly concatenated strings on a single line ] [tool.ruff.lint.per-file-ignores] From ba9fea17d130daba132310f98ce7b9c24614da65 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:29:31 +0200 Subject: [PATCH 05/13] docs: add --formatter=pyupgrade to README --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 980f0654c..8ab724be6 100644 --- a/README.rst +++ b/README.rst @@ -378,7 +378,8 @@ The following `command line arguments`_ can also be used to modify the defaults: versions that should be supported by Black's output. [default: per-file auto- detection] --formatter FORMATTER - [black\|none\|ruff] Formatter to use for reformatting code. [default: black] + [black\|none\|pyupgrade\|ruff] Formatter to use for reformatting code. [default: + black] To change default values for these options for a given project, add a ``[tool.darker]`` section to ``pyproject.toml`` in the project's root directory, From 3e6016747c51244f4333e975ce92faa004595db1 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:45:40 +0200 Subject: [PATCH 06/13] ci: convert existing CI job to run pyupgrade through Darker --- .github/workflows/pyupgrade.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pyupgrade.yml b/.github/workflows/pyupgrade.yml index a15c0bb7e..96b8a86b3 100644 --- a/.github/workflows/pyupgrade.yml +++ b/.github/workflows/pyupgrade.yml @@ -9,16 +9,13 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v3 - uses: actions/setup-python@v5 - name: Ensure modern Python style using pyupgrade - # This script is written in a Linux / macos / windows portable way run: | - uvx --from pyupgrade python -c " - import sys - from pyupgrade._main import main - from glob import glob - files = glob('**/*.py', recursive=True) - sys.exit(main(files + ['--py39-plus'])) - " || ( git diff ; false ) + uvx \ + --from '.[pyupgrade]' \ + darker --formatter=pyupgrade --target-version=py39 --diff From 5dab3bdeb61706f7d10bc8916647598f35168948 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:05:40 +0200 Subject: [PATCH 07/13] fix: pyupgrade entire modified files, not only user-modified chunks This is a limitation of Darker+Pyupgrade. --- src/darker/__main__.py | 52 ++++++++++---------- src/darker/formatters/base_formatter.py | 1 + src/darker/formatters/black_formatter.py | 1 + src/darker/formatters/none_formatter.py | 1 + src/darker/formatters/pyupgrade_formatter.py | 1 + src/darker/formatters/ruff_formatter.py | 1 + 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index 9b471cc55..29b066b41 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -227,9 +227,17 @@ def _reformat_and_flynt_single_file( # noqa: PLR0913 "no" if formatted == fstringified else "some", len(formatted.lines), ) + # 4. apply all re-formatter modifications if the re-formatter doesn't guarantee + # preserving the abstract syntax tree (AST); otherwise do steps 5 to 10 + if not formatter.preserves_ast: + logger.debug( + "Preserving the AST not guaranteed by %s, applying all changes", + formatter.name, + ) + return formatted - # 4. get a diff between the edited to-file and the processed content - # 5. convert the diff into chunks, keeping original and reformatted content for each + # 5. get a diff between the edited to-file and the processed content + # 6. convert the diff into chunks, keeping original and reformatted content for each # chunk new_chunks = diff_chunks(rev2_isorted, formatted) @@ -337,9 +345,9 @@ def _drop_changes_on_unedited_lines( context_lines, abspath_in_rev2, ) - # 6. diff the given revisions (optionally with isort modifications) for each + # 7. diff the given revisions (optionally with isort modifications) for each # file - # 7. extract line numbers in each edited to-file for changed lines + # 8. extract line numbers in each edited to-file for changed lines edited_linenums = edited_linenums_differ.revision_vs_lines( relpath_in_repo, rev2_isorted, context_lines ) @@ -348,7 +356,7 @@ def _drop_changes_on_unedited_lines( last_successful_reformat = rev2_isorted break - # 8. choose processed content for each chunk if there were any changed lines + # 9. choose processed content for each chunk if there were any changed lines # inside the chunk in the edited to-file, or choose the chunk's original # contents if no edits were done in that chunk chosen = TextDocument.from_lines( @@ -358,8 +366,8 @@ def _drop_changes_on_unedited_lines( mtime=datetime.utcnow().strftime(GIT_DATEFORMAT), ) - # 9. verify that the resulting reformatted source code parses to an identical - # AST as the original edited to-file + # 10. verify that the resulting reformatted source code parses to an identical + # AST as the original edited to-file if not has_fstring_changes and not verifier.is_equivalent_to_baseline(chosen): logger.debug( "Verifying that the %s original edited lines and %s reformatted lines " @@ -459,32 +467,26 @@ def _import_pygments(): # type: ignore def main( # noqa: C901,PLR0912,PLR0915 argv: List[str] = None, ) -> int: - """Parse the command line and reformat and optionally lint each source file + """Parse the command line and reformat and optionally lint each source file. 1. run isort on each edited file (optional) 2. run flynt (optional) on the isorted contents of each edited to-file 3. run a code re-formatter on the isorted and fstringified contents of each edited to-file - 4. get a diff between the edited to-file and the processed content - 5. convert the diff into chunks, keeping original and reformatted content for each + 4. apply all re-formatter modifications if the re-formatter doesn't guarantee + preserving the abstract syntax tree (AST); otherwise do steps 5 to 10 + 5. get a diff between the edited to-file and the processed content + 6. convert the diff into chunks, keeping original and reformatted content for each chunk - 6. diff the given revisions (optionally with isort modifications) for each + 7. diff the given revisions (optionally with isort modifications) for each file - 7. extract line numbers in each edited to-file for changed lines - 8. choose processed content for each chunk if there were any changed lines inside + 8. extract line numbers in each edited to-file for changed lines + 9. choose processed content for each chunk if there were any changed lines inside the chunk in the edited to-file, or choose the chunk's original contents if no edits were done in that chunk - 9. verify that the resulting reformatted source code parses to an identical AST as - the original edited to-file - 9. write the reformatted source back to the original file or print the diff - 10. run linter subprocesses twice for all modified and unmodified files which are - mentioned on the command line: first establish a baseline by running against - ``rev1``, then get current linting status by running against the working tree - (steps 10.-12. are optional) - 11. create a mapping from line numbers of unmodified lines in the current versions - to corresponding line numbers in ``rev1`` - 12. hide linter messages which appear in the current versions and identically on - corresponding lines in ``rev1``, and show all other linter messages + 10. verify that the resulting reformatted source code parses to an identical AST as + the original edited to-file + 11. write the reformatted source back to the original file or print the diff :param argv: The command line arguments to the ``darker`` command :return: 1 if the ``--check`` argument was provided and at least one file was (or @@ -611,7 +613,7 @@ def main( # noqa: C901,PLR0912,PLR0915 workers=config["workers"], ), ): - # 10. A re-formatted Python file which produces an identical AST was + # 11. A re-formatted Python file which produces an identical AST was # created successfully - write an updated file or print the diff if # there were any changes to the original formatting_failures_on_modified_lines = True diff --git a/src/darker/formatters/base_formatter.py b/src/darker/formatters/base_formatter.py index 15e10d833..d22dfea92 100644 --- a/src/darker/formatters/base_formatter.py +++ b/src/darker/formatters/base_formatter.py @@ -29,6 +29,7 @@ class BaseFormatter(HasConfig[FormatterConfig]): """Base class for code re-formatters.""" name: str + preserves_ast: bool def read_config(self, src: tuple[str, ...], args: Namespace) -> None: """Read code re-formatter configuration from a configuration file. diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index fe89b3465..383a6a2d8 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -79,6 +79,7 @@ class BlackFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]): name = "black" config_section = "tool.black" + preserves_ast = True def read_config(self, src: tuple[str, ...], args: Namespace) -> None: """Read Black configuration from ``pyproject.toml``. diff --git a/src/darker/formatters/none_formatter.py b/src/darker/formatters/none_formatter.py index 549fdde59..9a7d335a7 100644 --- a/src/darker/formatters/none_formatter.py +++ b/src/darker/formatters/none_formatter.py @@ -17,6 +17,7 @@ class NoneFormatter(BaseFormatter): """A dummy code formatter plugin interface.""" name = "dummy reformat" + preserves_ast = True def run( self, content: TextDocument, path_from_cwd: Path # noqa: ARG002 diff --git a/src/darker/formatters/pyupgrade_formatter.py b/src/darker/formatters/pyupgrade_formatter.py index f87b96d45..64c633fdd 100644 --- a/src/darker/formatters/pyupgrade_formatter.py +++ b/src/darker/formatters/pyupgrade_formatter.py @@ -60,6 +60,7 @@ class PyupgradeFormatter(BaseFormatter, HasConfig[PyupgradeConfig]): config: PyupgradeConfig # type: ignore[assignment] name = "pyupgrade" + preserves_ast = False def run( self, content: TextDocument, path_from_cwd: Path # noqa: ARG002 diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py index 667079d5f..ee0ad9810 100644 --- a/src/darker/formatters/ruff_formatter.py +++ b/src/darker/formatters/ruff_formatter.py @@ -76,6 +76,7 @@ class RuffFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]): name = "ruff format" config_section = "tool.ruff" + preserves_ast = True def run(self, content: TextDocument, path_from_cwd: Path) -> TextDocument: """Run the Ruff code re-formatter for the Python source code given as a string. From 43f0f358d87e8d7b0b997e517c0373fd04f16500 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 20 Oct 2024 10:55:13 +0200 Subject: [PATCH 08/13] test: add stubs for pyupgrade Can be removed if https://github.com/asottile/pyupgrade/issues/977 is resolved. --- .github/workflows/pylint.yml | 1 + mypy.ini | 5 ++--- pyproject.toml | 8 ++++++++ setup.cfg | 1 + stubs/pyupgrade/__init__.pyi | 0 stubs/pyupgrade/_data.pyi | 15 +++++++++++++++ stubs/pyupgrade/_main.pyi | 13 +++++++++++++ 7 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 stubs/pyupgrade/__init__.pyi create mode 100644 stubs/pyupgrade/_data.pyi create mode 100644 stubs/pyupgrade/_main.pyi diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 85e4b067a..6a543d691 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -22,6 +22,7 @@ jobs: defusedxml \ pygments \ 'pylint<=3.2.7' \ + pylint-per-file-ignores \ pytest>=6.2.0 \ pyupgrade>=2.31.0 \ regex \ diff --git a/mypy.ini b/mypy.ini index af0e1d623..5d0219f11 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1,6 @@ [mypy] +mypy_path = stubs/ + disallow_any_unimported = True disallow_any_expr = False disallow_any_decorated = True @@ -81,9 +83,6 @@ ignore_missing_imports = True [mypy-pytest.*] ignore_missing_imports = True -[mypy-pyupgrade.*] -ignore_missing_imports = True - [mypy-setuptools.*] ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 522b2d95f..ae69e1719 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,10 +31,18 @@ revision = "origin/master..." revision = "origin/master..." src = ["."] +[tool.pylint.MASTER] +load-plugins = [ + "pylint_per_file_ignores", +] + [tool.pylint."messages control"] # Check import order only with isort. Pylint doesn't support a custom list of first # party packages. We want to consider "darkgraylib" and "graylint" as first party. disable = ["wrong-import-order"] +per-file-ignores = [ + "/stubs/:missing-class-docstring,missing-function-docstring,unused-argument", +] [tool.ruff] target-version = "py39" diff --git a/setup.cfg b/setup.cfg index b4eaac47e..0016a1517 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,6 +75,7 @@ test = pydocstyle pygments pylint<=3.2.7 # pylint 3.3.0 dropped Python 3.8 support + pylint-per-file-ignores pytest>=6.2.0 pytest-kwparametrize>=0.0.3 pyupgrade>=2.31.0 diff --git a/stubs/pyupgrade/__init__.pyi b/stubs/pyupgrade/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/stubs/pyupgrade/_data.pyi b/stubs/pyupgrade/_data.pyi new file mode 100644 index 000000000..c9837d99b --- /dev/null +++ b/stubs/pyupgrade/_data.pyi @@ -0,0 +1,15 @@ +"""Type stubs for bits used from `pyupgrade._data`. + +Can be removed if https://github.com/asottile/pyupgrade/issues/977 is resolved. + +""" + +from typing import NamedTuple + +Version = tuple[int, ...] + +class Settings(NamedTuple): + min_version: Version = ... + keep_percent_format: bool = ... + keep_mock: bool = ... + keep_runtime_typing: bool = ... diff --git a/stubs/pyupgrade/_main.pyi b/stubs/pyupgrade/_main.pyi new file mode 100644 index 000000000..e6b7734a9 --- /dev/null +++ b/stubs/pyupgrade/_main.pyi @@ -0,0 +1,13 @@ +"""Type stubs for bits used from `pyupgrade._main`. + +Can be removed if https://github.com/asottile/pyupgrade/issues/977 is resolved. + +""" + +from typing import Sequence + +from pyupgrade._data import Settings + +def _fix_plugins(contents_text: str, settings: Settings) -> str: ... +def _fix_tokens(contents_text: str) -> str: ... +def main(argv: Sequence[str] | None = None) -> int: ... From 2f568040ae7778587d5bb564ca0618fd3edba533 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 20 Oct 2024 10:55:43 +0200 Subject: [PATCH 09/13] fix: swallow SystemExit from pyupgrade --help argparse --- src/darker/formatters/pyupgrade_formatter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/darker/formatters/pyupgrade_formatter.py b/src/darker/formatters/pyupgrade_formatter.py index 64c633fdd..b300f90ab 100644 --- a/src/darker/formatters/pyupgrade_formatter.py +++ b/src/darker/formatters/pyupgrade_formatter.py @@ -113,6 +113,8 @@ def _get_supported_target_versions() -> set[tuple[int, int]]: sys.stdout = buf = io.StringIO() try: main(["--help"]) + except SystemExit: # expected from argparse + pass finally: sys.stdout = stdout version_strs = ( From 9cbc07f24a8e323e0ae167a73496bc19fb6bc951 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 20 Oct 2024 10:56:13 +0200 Subject: [PATCH 10/13] fix: parse pyupgrade --target-version into an int-tuple --- src/darker/formatters/pyupgrade_formatter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/darker/formatters/pyupgrade_formatter.py b/src/darker/formatters/pyupgrade_formatter.py index b300f90ab..6b2ba9369 100644 --- a/src/darker/formatters/pyupgrade_formatter.py +++ b/src/darker/formatters/pyupgrade_formatter.py @@ -80,8 +80,7 @@ def run( target_versions_in = validate_target_versions( self.config["target_version"], supported_target_versions ) - target_version_str = min(target_versions_in) - target_version = (int(target_version_str[0]), int(target_version_str[1:])) + target_version = min(target_versions_in) else: target_version = (3, 9) @@ -95,7 +94,10 @@ def run( def _read_cli_args(self, args: Namespace) -> None: if getattr(args, "target_version", None): - self.config["target_version"] = {args.target_version} + self.config["target_version"] = ( + int(args.target_version[2]), + int(args.target_version[3:]), + ) def _get_supported_target_versions() -> set[tuple[int, int]]: From 34f7138d5335d7c675297a1003532e6a9acc59c8 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:22:44 +0200 Subject: [PATCH 11/13] ci: test also against main branch of Pyupgrade --- constraints-future.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/constraints-future.txt b/constraints-future.txt index 6c3d500e7..f4e6c39b8 100644 --- a/constraints-future.txt +++ b/constraints-future.txt @@ -7,3 +7,4 @@ black @ git+https://github.com/psf/black.git@main darkgraylib @ git+https://github.com/akaihola/darkgraylib.git@main flynt @ git+https://github.com/ikamensh/flynt.git@master isort @ git+https://github.com/PyCQA/isort.git@main +pyupgrade @ git+https://github.com/asottile/pyupgrade.git@main From 88709f7fdacde4478db772e3a2fa43978a96fabb Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:36:33 +0200 Subject: [PATCH 12/13] docs: update the change log --- CHANGES.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3acafb6d9..0fc30585e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,8 +10,8 @@ Added - Display exit code in parentheses after error message. - Do not reformat renamed files. - CI workflow to post recent project activity in a discussion. Triggered manually. -- CI "future" test now tests against ``main`` of Darkgraylib_ in addition to Black_, - Flynt_ and isort_. +- CI "future" test now tests against ``main`` of Darkgraylib_ and pyupgrade_ in addition + to Black_, Flynt_ and isort_. - The ``--preview`` configuration flag is now supported in the configuration files for Darker and Black - Prevent Pylint from updating beyond version 3.2.7 due to dropped Python 3.8 support. @@ -23,6 +23,8 @@ Added Isort or Flynt_. - Black_ is no longer installed by default. Use ``pip install 'darker[black]'`` to get Black support. +- pyupgrade_ is now supported as a formatter plugin. Note that changes from pyupgrade + are applied on a per-file basis, not only for modified lines as with Black_ and Ruff_. Removed ------- @@ -697,3 +699,4 @@ Added .. _Black: https://black.readthedocs.io/ .. _isort: https://pycqa.github.io/isort/ .. _NixOS: https://nixos.org/ +.. _pyupgrade: https://pypi.org/project/pyupgrade/ From 37fe7a53d7c12164146b313c9981d693b3036733 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:50:10 +0200 Subject: [PATCH 13/13] ci: larger fetch depth in working directory test --- .github/workflows/test-working-directory.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-working-directory.yml b/.github/workflows/test-working-directory.yml index 37167571c..b50a572a8 100644 --- a/.github/workflows/test-working-directory.yml +++ b/.github/workflows/test-working-directory.yml @@ -1,6 +1,11 @@ --- name: "GH Action `working-directory:` test" +# If these tests fail with "fatal: bad object" in the "Run Darker" step +# and "Error: Expected exit code " in the "Check exit code" step, +# make sure `fetch-steps:` in `actions/checkout` below is +# at least as large as the number of commits in the branch the test is run in. + on: push # yamllint disable-line rule:truthy jobs: @@ -66,7 +71,9 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 10 + # This needs to be at least the number of commits + # in the branch the test is run in: + fetch-depth: 30 - name: Download test repository uses: actions/download-artifact@v3