From 21f9c92c0279080eaf4ad531cb6b9e7dd7bd3e57 Mon Sep 17 00:00:00 2001 From: csteiner <47841949+clintonsteiner@users.noreply.github.com> Date: Fri, 20 Dec 2024 00:11:15 -0600 Subject: [PATCH 01/77] Update dependabot.yml - fix yamllint trailing space --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index edf307b5a..7d25a84b5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,6 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "daily" ignore: - dependency-name: "black" From ca5327ef0111f6599f0b53eff1b685d1f290bdf3 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:56:29 +0200 Subject: [PATCH 02/77] test: speed up `test_black_options` This is done by using the new module-scoped Git repository fixture from Darkgraylib. --- src/darker/tests/test_command_line.py | 48 ++++++++++++++++----------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 4666e1d7b..fd379395c 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -2,6 +2,8 @@ """Unit tests for :mod:`darker.command_line` and :mod:`darker.__main__`""" +from __future__ import annotations + import os import re from importlib import reload @@ -22,6 +24,7 @@ from darker.tests.helpers import flynt_present, isort_present from darkgraylib.config import ConfigurationError from darkgraylib.git import RevisionRange +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.testtools.helpers import raises_if_exception from darkgraylib.utils import TextDocument, joinlines @@ -463,6 +466,24 @@ def test_help_with_flynt_package(capsys): ) +@pytest.fixture(scope="module") +def black_options_files(request, tmp_path_factory): + """Fixture for the `test_black_options` test.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + (repo.root / "pyproject.toml").write_bytes(b"[tool.black]\n") + (repo.root / "black.cfg").write_text( + dedent( + """ + [tool.black] + line-length = 81 + skip-string-normalization = false + target-version = 'py38' + """ + ) + ) + yield repo.add({"main.py": 'print("Hello World!")\n'}, commit="Initial commit") + + @pytest.mark.kwparametrize( dict(options=[], expect=call()), dict( @@ -547,30 +568,19 @@ def test_help_with_flynt_package(capsys): ), ), ) -def test_black_options(monkeypatch, tmpdir, git_repo, options, expect): - """Black options from the command line are passed correctly to Black""" - monkeypatch.chdir(tmpdir) - (tmpdir / "pyproject.toml").write("[tool.black]\n") - (tmpdir / "black.cfg").write( - dedent( - """ - [tool.black] - line-length = 81 - skip-string-normalization = false - target-version = 'py38' - """ - ) - ) - added_files = git_repo.add( - {"main.py": 'print("Hello World!")\n'}, commit="Initial commit" - ) - added_files["main.py"].write_bytes(b'print ("Hello World!")\n') +def test_black_options(black_options_files, options, expect): + """Black options from the command line are passed correctly to Black.""" + # The Git repository set up by the module-scope `black_options_repo` fixture is + # shared by all test cases. The "main.py" file modified by the test run needs to be + # reset to its original content before the next test case. + black_options_files["main.py"].write_bytes(b'print ("Hello World!")\n') with patch.object( black_formatter, "Mode", wraps=black_formatter.Mode ) as file_mode_class: - main(options + [str(path) for path in added_files.values()]) + main(options + [str(path) for path in black_options_files.values()]) + assert black_options_files["main.py"].read_bytes() == b'print("Hello World!")\n' _, expect_args, expect_kwargs = expect file_mode_class.assert_called_once_with(*expect_args, **expect_kwargs) From 79126ceb50e9ec5732a8efe82f8c1e5c7a498426 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:39:55 +0200 Subject: [PATCH 03/77] chore: update to Darkgraylib 2.1.0 --- constraints-oldest.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/constraints-oldest.txt b/constraints-oldest.txt index 70d16afb2..462f7e015 100644 --- a/constraints-oldest.txt +++ b/constraints-oldest.txt @@ -4,7 +4,7 @@ # interpreter and Python ependencies. Keep this up-to-date with minimum # versions in `setup.cfg`. black==22.3.0 -darkgraylib==2.0.1 +darkgraylib==2.1.0 defusedxml==0.7.1 flake8-2020==1.6.1 flake8-bugbear==22.1.11 diff --git a/setup.cfg b/setup.cfg index 4739438df..9920959a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ packages = find: install_requires = # NOTE: remember to keep `constraints-oldest.txt` in sync with these black>=22.3.0 - darkgraylib~=2.0.1 + darkgraylib~=2.1.0 toml>=0.10.0 typing_extensions>=4.0.1 # NOTE: remember to keep `.github/workflows/python-package.yml` in sync From 653126efeeb88f3f2990f789e2b9ffc343703779 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:37:38 +0200 Subject: [PATCH 04/77] test: speed up `test_black_config_file_and_options` --- src/darker/tests/conftest.py | 23 ++++++++++++++++++++++ src/darker/tests/test_command_line.py | 28 +++++++++++++++++++-------- 2 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/darker/tests/conftest.py diff --git a/src/darker/tests/conftest.py b/src/darker/tests/conftest.py new file mode 100644 index 000000000..329e20130 --- /dev/null +++ b/src/darker/tests/conftest.py @@ -0,0 +1,23 @@ +"""Configuration and fixtures for the Pytest based test suite.""" + +import pytest + +try: + from black.files import _load_toml +except ImportError: + # Black 24.1.1 and earlier don't have `_load_toml`. + _load_toml = None # type: ignore[assignment] + + +@pytest.fixture +def load_toml_cache_clear() -> None: + """Clear LRU caching in `black.files._load_toml` before each test. + + To use this on all test cases in a test module, add this to the top:: + + pytestmark = pytest.mark.usefixtures("load_toml_cache_clear") + + """ + if _load_toml: + # Black 24.1.1 and earlier don't have `_load_toml`, so no LRU cache to clear. + _load_toml.cache_clear() diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index fd379395c..666876643 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -28,7 +28,10 @@ from darkgraylib.testtools.helpers import raises_if_exception from darkgraylib.utils import TextDocument, joinlines -pytestmark = pytest.mark.usefixtures("find_project_root_cache_clear") +# Clear LRU caches for `find_project_root()` and `_load_toml()` before each test +pytestmark = pytest.mark.usefixtures( + "find_project_root_cache_clear", "load_toml_cache_clear" +) @pytest.mark.kwparametrize( @@ -585,6 +588,16 @@ def test_black_options(black_options_files, options, expect): file_mode_class.assert_called_once_with(*expect_args, **expect_kwargs) +@pytest.fixture(scope="module") +def black_config_file_and_options_fixture(git_repo_m): + repo_files = git_repo_m.add( + {"main.py": "foo", "pyproject.toml": "* placeholder, will be overwritten"}, + commit="Initial commit", + ) + repo_files["main.py"].write_bytes(b"a = [1, 2,]") + return repo_files + + @pytest.mark.kwparametrize( dict(config=[], options=[], expect=call()), dict( @@ -683,19 +696,18 @@ def test_black_options(black_options_files, options, expect): expect=call(preview=True), ), ) -def test_black_config_file_and_options(git_repo, config, options, expect): +def test_black_config_file_and_options( + black_config_file_and_options_fixture, config, options, expect +): """Black configuration file and command line options are combined correctly""" - added_files = git_repo.add( - {"main.py": "foo", "pyproject.toml": joinlines(["[tool.black]"] + config)}, - commit="Initial commit", - ) - added_files["main.py"].write_bytes(b"a = [1, 2,]") + repo_files = black_config_file_and_options_fixture + repo_files["pyproject.toml"].write_text(joinlines(["[tool.black]"] + config)) mode_class_mock = Mock(wraps=black_formatter.Mode) # Speed up tests by mocking `format_str` to skip running Black format_str = Mock(return_value="a = [1, 2,]") with patch.multiple(black_formatter, Mode=mode_class_mock, format_str=format_str): - main(options + [str(path) for path in added_files.values()]) + main(options + [str(path) for path in repo_files.values()]) assert mode_class_mock.call_args_list == [expect] From 55b8e7f2033527ba4a767fa4e2c970f00c91d520 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:49:50 +0200 Subject: [PATCH 05/77] test: speed up `test_black_config_file_and_options` --- src/darker/tests/test_command_line.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 666876643..42cae8ac2 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -1,6 +1,7 @@ -# pylint: disable=too-many-arguments,too-many-locals,use-dict-literal +"""Unit tests for `darker.command_line` and `darker.__main__`.""" -"""Unit tests for :mod:`darker.command_line` and :mod:`darker.__main__`""" +# pylint: disable=too-many-arguments,too-many-locals +# pylint: disable=no-member,redefined-outer-name,use-dict-literal from __future__ import annotations @@ -589,13 +590,15 @@ def test_black_options(black_options_files, options, expect): @pytest.fixture(scope="module") -def black_config_file_and_options_fixture(git_repo_m): - repo_files = git_repo_m.add( - {"main.py": "foo", "pyproject.toml": "* placeholder, will be overwritten"}, - commit="Initial commit", - ) - repo_files["main.py"].write_bytes(b"a = [1, 2,]") - return repo_files +def black_config_file_and_options_files(request, tmp_path_factory): + """Git repository fixture for the `test_black_config_file_and_options` test.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + repo_files = repo.add( + {"main.py": "foo", "pyproject.toml": "* placeholder, will be overwritten"}, + commit="Initial commit", + ) + repo_files["main.py"].write_bytes(b"a = [1, 2,]") + yield repo_files @pytest.mark.kwparametrize( @@ -697,11 +700,11 @@ def black_config_file_and_options_fixture(git_repo_m): ), ) def test_black_config_file_and_options( - black_config_file_and_options_fixture, config, options, expect + black_config_file_and_options_files, config, options, expect ): """Black configuration file and command line options are combined correctly""" - repo_files = black_config_file_and_options_fixture - repo_files["pyproject.toml"].write_text(joinlines(["[tool.black]"] + config)) + repo_files = black_config_file_and_options_files + repo_files["pyproject.toml"].write_text(joinlines(["[tool.black]", *config])) mode_class_mock = Mock(wraps=black_formatter.Mode) # Speed up tests by mocking `format_str` to skip running Black format_str = Mock(return_value="a = [1, 2,]") From 84b04209b93ae357d1eea5f4700462ef1b5e536e Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:51:48 +0200 Subject: [PATCH 06/77] test: speed up `test_options` --- src/darker/tests/test_command_line.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 42cae8ac2..cdd037b29 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -715,6 +715,17 @@ def test_black_config_file_and_options( assert mode_class_mock.call_args_list == [expect] +@pytest.fixture(scope="module") +def options_repo(request, tmp_path_factory): + """Git repository fixture for the `test_options` test.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + paths = repo.add( + {"a.py": "1\n", "b.py": "2\n", "my.cfg": ""}, commit="Initial commit" + ) + paths["a.py"].write_bytes(b"one\n") + yield repo + + @pytest.mark.kwparametrize( dict( options=["a.py"], @@ -809,17 +820,14 @@ def test_black_config_file_and_options( ), ), ) -def test_options(git_repo, options, expect): +def test_options(options_repo, monkeypatch, options, expect): """The main engine is called with correct parameters based on the command line Executed in a clean directory so Darker's own ``pyproject.toml`` doesn't interfere. """ - paths = git_repo.add( - {"a.py": "1\n", "b.py": "2\n", "my.cfg": ""}, commit="Initial commit" - ) - paths["a.py"].write_bytes(b"one\n") with patch('darker.__main__.format_edited_parts') as format_edited_parts: + monkeypatch.chdir(options_repo.root) retval = main(options) @@ -827,7 +835,7 @@ def test_options(git_repo, options, expect): expect_formatter.config = expect[4] actual_formatter = format_edited_parts.call_args.args[4] assert actual_formatter.config == expect_formatter.config - expect = (Path(git_repo.root), expect[1]) + expect[2:4] + (expect_formatter,) + expect = (Path(options_repo.root), expect[1]) + expect[2:4] + (expect_formatter,) format_edited_parts.assert_called_once_with( *expect, report_unmodified=False, workers=1 ) From 26be628cc2cf8c7066afc3a77d205ccba28ca2fc Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:52:32 +0200 Subject: [PATCH 07/77] test: speed up `test_git_get_modified_python_files` --- src/darker/tests/test_git.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py index 446897a44..f0482f18f 100644 --- a/src/darker/tests/test_git.py +++ b/src/darker/tests/test_git.py @@ -13,7 +13,7 @@ from darker import git from darkgraylib.git import WORKTREE, RevisionRange -from darkgraylib.testtools.git_repo_plugin import GitRepoFixture +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture, branched_repo from darkgraylib.utils import TextDocument @@ -303,6 +303,12 @@ def test_git_get_modified_python_files(git_repo, modify_paths, paths, expect): assert result == {Path(p) for p in expect} +@pytest.fixture(scope="module") +def git_get_modified_python_files_revision_range_repo(request, tmp_path_factory): + """Fixture for a Git repository with multiple commits and branches.""" + yield from branched_repo(request, tmp_path_factory) + + @pytest.mark.kwparametrize( dict( _description="from latest commit in branch to worktree and index", @@ -383,15 +389,17 @@ def test_git_get_modified_python_files(git_repo, modify_paths, paths, expect): ), ) def test_git_get_modified_python_files_revision_range( - _description, branched_repo, revrange, expect + _description, # noqa: PT019 + git_get_modified_python_files_revision_range_repo, + revrange, + expect, ): """Test for :func:`darker.git.git_get_modified_python_files` with revision range""" + repo = git_get_modified_python_files_revision_range_repo result = git.git_get_modified_python_files( - [Path(branched_repo.root)], - RevisionRange.parse_with_common_ancestor( - revrange, branched_repo.root, stdin_mode=False - ), - Path(branched_repo.root), + [Path(repo.root)], + RevisionRange.parse_with_common_ancestor(revrange, repo.root, stdin_mode=False), + Path(repo.root), ) assert {path.name for path in result} == expect From f93b9681a9555e3699b3b63035c768754378b0e8 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:58:01 +0200 Subject: [PATCH 08/77] test: speed up `test_main_retval` --- pyproject.toml | 1 + src/darker/tests/test_command_line.py | 13 ++++++++++--- src/darker/tests/test_main_isort.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4cf7b7edc..578824a8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ ignore = [ "ANN001", # Missing type annotation for function argument "ANN201", # Missing return type annotation for public function "ANN204", # Missing return type annotation for special method `__init__` + "ARG001", # Unused function argument "C408", # Unnecessary `dict` call (rewrite as a literal) "PLR0913", # Too many arguments in function definition (n > 5) "S101", # Use of `assert` detected diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index cdd037b29..cb43cbb73 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -1,7 +1,7 @@ """Unit tests for `darker.command_line` and `darker.__main__`.""" # pylint: disable=too-many-arguments,too-many-locals -# pylint: disable=no-member,redefined-outer-name,use-dict-literal +# pylint: disable=no-member,redefined-outer-name,unused-argument,use-dict-literal from __future__ import annotations @@ -842,6 +842,14 @@ def test_options(options_repo, monkeypatch, options, expect): assert retval == 0 +@pytest.fixture(scope="module") +def main_retval_repo(request, tmp_path_factory): + """Git repository fixture for the `test_main_retval` test.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + repo.add({"a.py": ""}, commit="Initial commit") + yield + + @pytest.mark.kwparametrize( dict(arguments=["a.py"], changes=False), dict(arguments=["a.py"], changes=True), @@ -849,9 +857,8 @@ def test_options(options_repo, monkeypatch, options, expect): dict(arguments=["--check", "a.py"], changes=True, expect_retval=1), expect_retval=0, ) -def test_main_retval(git_repo, arguments, changes, expect_retval): +def test_main_retval(main_retval_repo, arguments, changes, expect_retval): """``main()`` return value is correct based on ``--check`` and reformatting.""" - git_repo.add({"a.py": ""}, commit="Initial commit") format_edited_parts = Mock() format_edited_parts.return_value = ( [ diff --git a/src/darker/tests/test_main_isort.py b/src/darker/tests/test_main_isort.py index 2bef090bf..2e420931e 100644 --- a/src/darker/tests/test_main_isort.py +++ b/src/darker/tests/test_main_isort.py @@ -18,7 +18,7 @@ pytestmark = pytest.mark.usefixtures("find_project_root_cache_clear") -def test_isort_option_without_isort(git_repo): # noqa: ARG001 +def test_isort_option_without_isort(git_repo): """Without isort, provide isort install instructions and error.""" # The `git_repo` fixture ensures test is not run in the Darker repository clone in # CI builds. It helps avoid a NixOS test issue. From aa8372e0aeebd5a5ba9d9cce61c28fc240c060eb Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:15:20 +0200 Subject: [PATCH 09/77] test: speed up `test_revision` --- src/darker/tests/test_main_revision.py | 71 ++++++++++++++------------ 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/src/darker/tests/test_main_revision.py b/src/darker/tests/test_main_revision.py index 029f8b8c5..d64dcc84f 100644 --- a/src/darker/tests/test_main_revision.py +++ b/src/darker/tests/test_main_revision.py @@ -1,10 +1,11 @@ """Unit tests for the ``--revision`` argument in `darker.main`""" -# pylint: disable=too-many-arguments,use-dict-literal +# pylint: disable=no-member,redefined-outer-name,too-many-arguments,use-dict-literal import pytest from darker.__main__ import main +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.testtools.helpers import raises_if_exception # The following test is a bit dense, so some explanation is due. @@ -43,6 +44,41 @@ # (`ORIGINAL=1`) at HEAD~2. +@pytest.fixture(scope="module") +def revision_files(request, tmp_path_factory): + """Git repository fixture for testing `--revision`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + # 2: HEAD~2: + paths = repo.add( + { + "+2.py": "ORIGINAL=1\n", + "+2M1.py": "ORIGINAL=1\n", + "+2-1.py": "ORIGINAL=1\n", + "+2M1-0.py": "ORIGINAL=1\n", + }, + commit="First commit", + ) + # 1: HEAD~1 i.e. HEAD^ + paths.update( + repo.add( + { + "+2M1.py": "MODIFIED=1\n", + "+1.py": "ORIGINAL=1\n", + "+1M0.py": "ORIGINAL=1\n", + "+2-1.py": None, + "+2M1-0.py": "MODIFIED=1\n", + }, + commit="Second commit", + ) + ) + # 0: HEAD~0 i.e. HEAD: + repo.add( + {"+1M0.py": "MODIFIED=1\n", "+2M1-0.py": None}, + commit="Third commit", + ) + yield paths + + @pytest.mark.kwparametrize( dict( revision="", @@ -109,39 +145,10 @@ ), dict(revision="HEAD~3", worktree_content=b"USERMOD=1\n", expect=SystemExit), ) -def test_revision(git_repo, monkeypatch, capsys, revision, worktree_content, expect): +def test_revision(revision_files, capsys, revision, worktree_content, expect): """``--diff`` with ``--revision`` reports correct files as modified""" - monkeypatch.chdir(git_repo.root) - # 2: HEAD~2: - paths = git_repo.add( - { - "+2.py": "ORIGINAL=1\n", - "+2M1.py": "ORIGINAL=1\n", - "+2-1.py": "ORIGINAL=1\n", - "+2M1-0.py": "ORIGINAL=1\n", - }, - commit="First commit", - ) - # 1: HEAD~1 i.e. HEAD^ - paths.update( - git_repo.add( - { - "+2M1.py": "MODIFIED=1\n", - "+1.py": "ORIGINAL=1\n", - "+1M0.py": "ORIGINAL=1\n", - "+2-1.py": None, - "+2M1-0.py": "MODIFIED=1\n", - }, - commit="Second commit", - ) - ) - # 0: HEAD~0 i.e. HEAD: - git_repo.add( - {"+1M0.py": "MODIFIED=1\n", "+2M1-0.py": None}, - commit="Third commit", - ) # Working tree: - for path in paths.values(): + for path in revision_files.values(): path.write_bytes(worktree_content) arguments = ["--diff", "--revision", revision, "."] From 930199e11fe8c6a2f14ca0835de89a0d9a7b6ec9 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:09:36 +0200 Subject: [PATCH 10/77] test: add `unix_and_windows_newline_repos` helper --- src/darker/tests/helpers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/darker/tests/helpers.py b/src/darker/tests/helpers.py index 39fbcc314..22f67e337 100644 --- a/src/darker/tests/helpers.py +++ b/src/darker/tests/helpers.py @@ -6,6 +6,8 @@ from typing import Generator, Optional from unittest.mock import patch +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture + @contextmanager def _package_present( @@ -43,3 +45,12 @@ def flynt_present(present: bool) -> Generator[None, None, None]: fake_flynt_module.code_editor = ModuleType("process") # type: ignore fake_flynt_module.code_editor.fstringify_code_by_line = None # type: ignore yield + + +@contextmanager +def unix_and_windows_newline_repos(request, tmp_path_factory): + """Create temporary repositories for Unix and windows newlines separately.""" + with GitRepoFixture.context( + request, tmp_path_factory + ) as repo_unix, GitRepoFixture.context(request, tmp_path_factory) as repo_windows: + yield {"\n": repo_unix, "\r\n": repo_windows} From a843d62f8e2401cfe553e4e6f895d2a0d24c1c03 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:09:08 +0200 Subject: [PATCH 11/77] test: speed up `test_format_edited_parts` --- .../tests/test_main_format_edited_parts.py | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/darker/tests/test_main_format_edited_parts.py b/src/darker/tests/test_main_format_edited_parts.py index 3d57c183f..cf3e2d440 100644 --- a/src/darker/tests/test_main_format_edited_parts.py +++ b/src/darker/tests/test_main_format_edited_parts.py @@ -1,12 +1,13 @@ """Tests for the `darker.__main__.format_edited_parts` function.""" -# pylint: disable=too-many-arguments,use-dict-literal +# pylint: disable=no-member,redefined-outer-name,too-many-arguments,use-dict-literal # pylint: disable=use-implicit-booleaness-not-comparison import logging import re from io import BytesIO from pathlib import Path +from types import SimpleNamespace from unittest.mock import Mock, patch import pytest @@ -16,6 +17,7 @@ from darker.config import Exclusions from darker.formatters.black_formatter import BlackFormatter from darker.tests.examples import A_PY, A_PY_BLACK, A_PY_BLACK_FLYNT, A_PY_BLACK_ISORT +from darker.tests.helpers import unix_and_windows_newline_repos from darker.verification import NotEquivalentError from darkgraylib.git import WORKTREE, RevisionRange from darkgraylib.utils import TextDocument, joinlines @@ -25,6 +27,27 @@ A_PY_BLACK_ISORT_FLYNT = ["import os", "import sys", "", 'print("42")', ""] +@pytest.fixture(scope="module") +def format_edited_parts_repo(request, tmp_path_factory): + """Create Git repositories for testing `format_edited_parts`.""" + fixture = {} + with unix_and_windows_newline_repos(request, tmp_path_factory) as repos: + for newline, repo in repos.items(): + fixture[newline] = SimpleNamespace( + root=repo.root, + paths=repo.add( + {"a.py": newline, "b.py": newline}, commit="Initial commit" + ), + ) + fixture[newline].paths["a.py"].write_bytes( + newline.join(A_PY).encode("ascii") + ) + fixture[newline].paths["b.py"].write_bytes( + f"print(42 ){newline}".encode("ascii") + ) + yield fixture + + @pytest.mark.kwparametrize( dict( black_exclude=set(), @@ -67,7 +90,7 @@ ) @pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["unix", "windows"]) def test_format_edited_parts( - git_repo, + format_edited_parts_repo, black_config, black_exclude, isort_exclude, @@ -82,14 +105,11 @@ def test_format_edited_parts( :func:`~darker.__main__.format_edited_parts`. """ - paths = git_repo.add({"a.py": newline, "b.py": newline}, commit="Initial commit") - paths["a.py"].write_bytes(newline.join(A_PY).encode("ascii")) - paths["b.py"].write_bytes(f"print(42 ){newline}".encode("ascii")) formatter = BlackFormatter() formatter.config = black_config result = darker.__main__.format_edited_parts( - Path(git_repo.root), + Path(format_edited_parts_repo[newline].root), {Path("a.py")}, Exclusions(formatter=black_exclude, isort=isort_exclude, flynt=flynt_exclude), RevisionRange("HEAD", ":WORKTREE:"), @@ -103,7 +123,7 @@ def test_format_edited_parts( ] expect_changes = [ ( - paths["a.py"], + format_edited_parts_repo[newline].paths["a.py"], newline.join(A_PY), newline.join(expect_lines), tuple(expect_lines[:-1]), From f92c50cc71f644ab68c3572f11e5ccc84292dcb6 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:51:54 +0200 Subject: [PATCH 12/77] test: speed up `test_main` --- src/darker/tests/test_main.py | 47 +++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index 446690ee7..f8c0aa19b 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -1,7 +1,7 @@ """Unit tests for :mod:`darker.__main__`""" # pylint: disable=too-many-locals,use-implicit-booleaness-not-comparison,unused-argument -# pylint: disable=protected-access,redefined-outer-name,too-many-arguments +# pylint: disable=no-member,protected-access,redefined-outer-name,too-many-arguments # pylint: disable=use-dict-literal import random @@ -12,6 +12,7 @@ from pathlib import Path from subprocess import PIPE, CalledProcessError, run # nosec from textwrap import dedent +from types import SimpleNamespace from unittest.mock import patch import pytest @@ -22,12 +23,15 @@ from darker.help import LINTING_GUIDE from darker.terminal import output from darker.tests.examples import A_PY, A_PY_BLACK, A_PY_BLACK_FLYNT, A_PY_BLACK_ISORT +from darker.tests.helpers import unix_and_windows_newline_repos from darker.tests.test_fstring import FLYNTED_SOURCE, MODIFIED_SOURCE, ORIGINAL_SOURCE from darkgraylib.git import RevisionRange from darkgraylib.testtools.highlighting_helpers import BLUE, CYAN, RESET, WHITE, YELLOW from darkgraylib.utils import WINDOWS, TextDocument, joinlines -pytestmark = pytest.mark.usefixtures("find_project_root_cache_clear") +pytestmark = pytest.mark.usefixtures( + "find_project_root_cache_clear", "load_toml_cache_clear" +) def randomword(length: int) -> str: @@ -86,6 +90,19 @@ def _replace_diff_timestamps(text, replacement=""): ] +@pytest.fixture(scope="module") +def main_repo(request, tmp_path_factory): + """Create Git repositories to test `darker.__main__.main`.""" + fixture = {} + with unix_and_windows_newline_repos(request, tmp_path_factory) as repos: + for newline, repo in repos.items(): + paths = repo.add( + {"subdir/a.py": newline, "b.py": newline}, commit="Initial commit" + ) + fixture[newline] = SimpleNamespace(root=repo.root, paths=paths) + yield fixture + + @pytest.mark.kwparametrize( dict(arguments=["--diff"], expect_stdout=A_PY_DIFF_BLACK), dict(arguments=["--isort"], expect_a_py=A_PY_BLACK_ISORT), @@ -190,7 +207,7 @@ def _replace_diff_timestamps(text, replacement=""): ) @pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["unix", "windows"]) def test_main( - git_repo, + main_repo, monkeypatch, capsys, arguments, @@ -203,27 +220,21 @@ def test_main( tmp_path_factory, ): """Main function outputs diffs and modifies files correctly""" + repo = main_repo[newline] if root_as_cwd: - cwd = git_repo.root + cwd = repo.root pwd = Path("") else: cwd = tmp_path_factory.mktemp("not_a_git_repo") - pwd = git_repo.root + pwd = repo.root monkeypatch.chdir(cwd) - paths = git_repo.add( - { - "pyproject.toml": dedent(pyproject_toml), - "subdir/a.py": newline, - "b.py": newline, - }, - commit="Initial commit", - ) - paths["subdir/a.py"].write_bytes(newline.join(A_PY).encode("ascii")) - paths["b.py"].write_bytes(f"print(42 ){newline}".encode("ascii")) + (repo.root / "pyproject.toml").write_text(dedent(pyproject_toml)) + repo.paths["subdir/a.py"].write_bytes(newline.join(A_PY).encode("ascii")) + repo.paths["b.py"].write_bytes(f"print(42 ){newline}".encode("ascii")) retval = darker.__main__.main(arguments + [str(pwd / "subdir")]) - stdout = capsys.readouterr().out.replace(str(git_repo.root), "") + stdout = capsys.readouterr().out.replace(str(repo.root), "") diff_output = stdout.splitlines(False) if expect_stdout: if "--diff" in arguments: @@ -237,10 +248,10 @@ def test_main( else: assert all("\t" not in line for line in diff_output) assert diff_output == expect_stdout - assert paths["subdir/a.py"].read_bytes().decode("ascii") == newline.join( + assert repo.paths["subdir/a.py"].read_bytes().decode("ascii") == newline.join( expect_a_py ) - assert paths["b.py"].read_bytes().decode("ascii") == f"print(42 ){newline}" + assert repo.paths["b.py"].read_bytes().decode("ascii") == f"print(42 ){newline}" assert retval == expect_retval From c1280bf92c501b8246b211e7103386095c804e8f Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:00:42 +0200 Subject: [PATCH 13/77] test: speed up `test_git_exists_in_revision` and `test_get_missing_at_revision` --- src/darker/tests/test_git.py | 54 +++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py index f0482f18f..380ea5d01 100644 --- a/src/darker/tests/test_git.py +++ b/src/darker/tests/test_git.py @@ -1,12 +1,13 @@ """Unit tests for :mod:`darker.git`""" -# pylint: disable=protected-access,redefined-outer-name,too-many-arguments +# pylint: disable=no-member,protected-access,redefined-outer-name,too-many-arguments # pylint: disable=too-many-lines,use-dict-literal import os from pathlib import Path from subprocess import DEVNULL, check_call # nosec from textwrap import dedent # nosec +from types import SimpleNamespace from unittest.mock import ANY, patch import pytest @@ -78,6 +79,23 @@ def test_git_exists_in_revision_git_call(retval, expect): assert result == expect +@pytest.fixture(scope="module") +def exists_missing_test_repo(request, tmp_path_factory): + """Git repository fixture for exists/missing tests.""" + fixture = SimpleNamespace() + with GitRepoFixture.context(request, tmp_path_factory) as repo: + fixture.root = repo.root + repo.add( + {"x/README": "", "x/dir/a.py": "", "x/dir/sub/b.py": ""}, + commit="Add x/dir/*.py", + ) + fixture.hash_add = repo.get_hash() + repo.add({"x/dir/a.py": None}, commit="Delete x/dir/a.py") + fixture.hash_del_a = repo.get_hash() + repo.add({"x/dir/sub/b.py": None}, commit="Delete x/dir/sub/b.py") + yield fixture + + @pytest.mark.kwparametrize( dict(cwd=".", rev2="{add}", path="x/dir/a.py", expect=True), dict(cwd=".", rev2="{add}", path="x/dir/sub/b.py", expect=True), @@ -110,20 +128,17 @@ def test_git_exists_in_revision_git_call(retval, expect): dict(cwd="x", rev2="HEAD", path="dir", expect=False), dict(cwd="x", rev2="HEAD", path="dir/sub", expect=False), ) -def test_git_exists_in_revision(git_repo, monkeypatch, cwd, rev2, path, expect): +def test_git_exists_in_revision( + exists_missing_test_repo, monkeypatch, cwd, rev2, path, expect +): """``_get_exists_in_revision()`` detects file/dir existence correctly""" - git_repo.add( - {"x/README": "", "x/dir/a.py": "", "x/dir/sub/b.py": ""}, - commit="Add x/dir/*.py", - ) - add = git_repo.get_hash() - git_repo.add({"x/dir/a.py": None}, commit="Delete x/dir/a.py") - del_a = git_repo.get_hash() - git_repo.add({"x/dir/sub/b.py": None}, commit="Delete x/dir/b.py") + repo = exists_missing_test_repo monkeypatch.chdir(cwd) result = git._git_exists_in_revision( - Path(path), rev2.format(add=add, del_a=del_a), git_repo.root / "x/dir/sub" + Path(path), + rev2.format(add=repo.hash_add, del_a=repo.hash_del_a), + repo.root / "x/dir/sub", ) assert result == expect @@ -178,23 +193,16 @@ def test_git_exists_in_revision(git_repo, monkeypatch, cwd, rev2, path, expect): git_cwd=".", ) def test_get_missing_at_revision( - git_repo, monkeypatch, paths, cwd, git_cwd, rev2, expect + exists_missing_test_repo, monkeypatch, paths, cwd, git_cwd, rev2, expect ): """``get_missing_at_revision()`` returns missing files/directories correctly""" - git_repo.add( - {"x/README": "", "x/dir/a.py": "", "x/dir/sub/b.py": ""}, - commit="Add x/dir/**/*.py", - ) - add = git_repo.get_hash() - git_repo.add({"x/dir/a.py": None}, commit="Delete x/dir/a.py") - del_a = git_repo.get_hash() - git_repo.add({"x/dir/sub/b.py": None}, commit="Delete x/dir/sub/b.py") - monkeypatch.chdir(git_repo.root / cwd) + repo = exists_missing_test_repo + monkeypatch.chdir(repo.root / cwd) result = git.get_missing_at_revision( {Path(p) for p in paths}, - rev2.format(add=add, del_a=del_a), - git_repo.root / git_cwd, + rev2.format(add=repo.hash_add, del_a=repo.hash_del_a), + repo.root / git_cwd, ) assert result == {Path(p) for p in expect} From 792581f4320799aa1f908850bcdde5f09a6f1f37 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 8 Nov 2024 21:37:52 +0200 Subject: [PATCH 14/77] test: speed up `test_main_stdin_filename_repo` --- src/darker/tests/test_main_stdin_filename.py | 34 +++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/darker/tests/test_main_stdin_filename.py b/src/darker/tests/test_main_stdin_filename.py index 5b1b3bf47..87914037d 100644 --- a/src/darker/tests/test_main_stdin_filename.py +++ b/src/darker/tests/test_main_stdin_filename.py @@ -1,8 +1,9 @@ """Tests for `darker.__main__.main` and the ``--stdin-filename`` option""" -# pylint: disable=too-many-arguments,use-dict-literal +# pylint: disable=no-member,redefined-outer-name,too-many-arguments,use-dict-literal from io import BytesIO +from types import SimpleNamespace from typing import List, Optional from unittest.mock import Mock, patch @@ -18,6 +19,18 @@ pytestmark = pytest.mark.usefixtures("find_project_root_cache_clear") +@pytest.fixture(scope="module") +def main_stdin_filename_repo(request, tmp_path_factory): + """Git repository fixture for `test_main_stdin_filename`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + yield SimpleNamespace( + root=repo.root, + paths=repo.add( + {"a.py": "original\n", "b.py": "original\n"}, commit="Initial commit" + ), + ) + + @pytest.mark.kwparametrize( dict(expect=SystemExit(EXIT_CODE_CMDLINE_ERROR)), dict(config_src=["a.py"], expect_a_py='modified = "a.py worktree"\n'), @@ -136,7 +149,7 @@ expect_a_py="original\n", ) def test_main_stdin_filename( - git_repo: GitRepoFixture, + main_stdin_filename_repo: SimpleNamespace, config_src: Optional[List[str]], src: List[str], stdin_filename: Optional[str], @@ -145,14 +158,13 @@ def test_main_stdin_filename( expect_a_py: str, ) -> None: """Tests for `darker.__main__.main` and the ``--stdin-filename`` option""" - if config_src is not None: - configuration = {"tool": {"darker": {"src": config_src}}} - git_repo.add({"pyproject.toml": toml.dumps(configuration)}) - paths = git_repo.add( - {"a.py": "original\n", "b.py": "original\n"}, commit="Initial commit" + repo = main_stdin_filename_repo + repo.paths["a.py"].write_text("modified = 'a.py worktree'") + repo.paths["b.py"].write_text("modified = 'b.py worktree'") + configuration = ( + {} if config_src is None else {"tool": {"darker": {"src": config_src}}} ) - paths["a.py"].write_text("modified = 'a.py worktree'") - paths["b.py"].write_text("modified = 'b.py worktree'") + (repo.root / "pyproject.toml").write_text(toml.dumps(configuration)) arguments = src[:] if stdin_filename is not None: arguments.insert(0, f"--stdin-filename={stdin_filename}") @@ -168,5 +180,5 @@ def test_main_stdin_filename( retval = darker.__main__.main(arguments) assert retval == expect - assert paths["a.py"].read_text() == expect_a_py - assert paths["b.py"].read_text() == "modified = 'b.py worktree'" + assert repo.paths["a.py"].read_text() == expect_a_py + assert repo.paths["b.py"].read_text() == "modified = 'b.py worktree'" From 21fab2178e74e5b32864bcbdf0c11544e173e042 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:26:30 +0200 Subject: [PATCH 15/77] test: speed up `test_apply_isort` --- src/darker/tests/test_import_sorting.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/darker/tests/test_import_sorting.py b/src/darker/tests/test_import_sorting.py index bfa805ace..79afcd7ba 100644 --- a/src/darker/tests/test_import_sorting.py +++ b/src/darker/tests/test_import_sorting.py @@ -1,6 +1,7 @@ """Tests for :mod:`darker.import_sorting`""" -# pylint: disable=unused-argument,protected-access,too-many-arguments,use-dict-literal +# pylint: disable=no-member,protected-access,redefined-outer-name,too-many-arguments +# pylint: disable=unused-argument,use-dict-literal from importlib import reload from pathlib import Path @@ -11,7 +12,7 @@ import darker.import_sorting from darker.git import EditedLinenumsDiffer -from darker.tests.helpers import isort_present +from darker.tests.helpers import isort_present, unix_and_windows_newline_repos from darkgraylib.git import RevisionRange from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.utils import TextDocument, joinlines @@ -35,6 +36,17 @@ def test_import_sorting_importable_with_and_without_isort(present): reload(darker.import_sorting) +@pytest.fixture(scope="module") +def apply_isort_repo_root(request, tmp_path_factory): + """Git repository fixture for `test_apply_isort`.""" + with unix_and_windows_newline_repos(request, tmp_path_factory) as repos: + for newline, repo in repos.items(): + repo.add( + {"test1.py": joinlines(ORIGINAL_SOURCE, newline)}, commit="Initial" + ) + yield {newline: repo.root for newline, repo in repos.items()} + + @pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-1"]) @pytest.mark.parametrize("newline", ["\n", "\r\n"]) @pytest.mark.kwparametrize( @@ -51,11 +63,10 @@ def test_import_sorting_importable_with_and_without_isort(present): dict(content=("import sys", "import os", "", "print(42)"), expect=ISORTED_SOURCE), dict(content=("import sys", "import os", "", "print(42)"), expect=ISORTED_SOURCE), ) -def test_apply_isort(git_repo, encoding, newline, content, expect): +def test_apply_isort(apply_isort_repo_root, encoding, newline, content, expect): """Imports are sorted if edits overlap them, with encoding and newline intact""" - git_repo.add({"test1.py": joinlines(ORIGINAL_SOURCE, newline)}, commit="Initial") edited_linenums_differ = EditedLinenumsDiffer( - git_repo.root, RevisionRange("HEAD", ":WORKTREE:") + apply_isort_repo_root[newline], RevisionRange("HEAD", ":WORKTREE:") ) src = Path("test1.py") content_ = TextDocument.from_lines(content, encoding=encoding, newline=newline) From 87c6443120cf39b8521e24f0053feadd3dc9da3d Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:28:41 +0200 Subject: [PATCH 16/77] test: speed up `test_apply_isort_exclude` --- src/darker/tests/test_import_sorting.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/darker/tests/test_import_sorting.py b/src/darker/tests/test_import_sorting.py index 79afcd7ba..a69ef85c0 100644 --- a/src/darker/tests/test_import_sorting.py +++ b/src/darker/tests/test_import_sorting.py @@ -96,11 +96,12 @@ def test_apply_isort(apply_isort_repo_root, encoding, newline, content, expect): expect=("import sys", "import os", "", "print(42)"), ), ) -def test_apply_isort_exclude(git_repo, encoding, newline, content, exclude, expect): +def test_apply_isort_exclude( + apply_isort_repo_root, encoding, newline, content, exclude, expect +): """Import sorting is skipped if file path matches exclusion patterns""" - git_repo.add({"test1.py": joinlines(ORIGINAL_SOURCE, newline)}, commit="Initial") edited_linenums_differ = EditedLinenumsDiffer( - git_repo.root, RevisionRange("HEAD", ":WORKTREE:") + apply_isort_repo_root[newline], RevisionRange("HEAD", ":WORKTREE:") ) src = Path("test1.py") content_ = TextDocument.from_lines(content, encoding=encoding, newline=newline) From 45abdd62a236a2291fc1c1a7d174156030c1ab1b Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:31:59 +0200 Subject: [PATCH 17/77] test: speed up `test_build_isort_args` --- src/darker/tests/test_import_sorting.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/darker/tests/test_import_sorting.py b/src/darker/tests/test_import_sorting.py index a69ef85c0..19d02d4aa 100644 --- a/src/darker/tests/test_import_sorting.py +++ b/src/darker/tests/test_import_sorting.py @@ -14,7 +14,6 @@ from darker.git import EditedLinenumsDiffer from darker.tests.helpers import isort_present, unix_and_windows_newline_repos from darkgraylib.git import RevisionRange -from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.utils import TextDocument, joinlines ORIGINAL_SOURCE = ("import sys", "import os", "", "print(42)") @@ -186,7 +185,6 @@ def test_isort_config(monkeypatch, tmpdir, line_length, settings_file, expect): line_length=None, ) def test_build_isort_args( - git_repo: GitRepoFixture, src: Path, config: Optional[str], line_length: int, From 7b470023bf93eef9a4bd4d75bd65f5eeb8c20706 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:40:06 +0200 Subject: [PATCH 18/77] test: speed up `test_main_historical_ok` --- src/darker/tests/test_main.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index f8c0aa19b..bdb0c534b 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -26,6 +26,7 @@ from darker.tests.helpers import unix_and_windows_newline_repos from darker.tests.test_fstring import FLYNTED_SOURCE, MODIFIED_SOURCE, ORIGINAL_SOURCE from darkgraylib.git import RevisionRange +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.testtools.highlighting_helpers import BLUE, CYAN, RESET, WHITE, YELLOW from darkgraylib.utils import WINDOWS, TextDocument, joinlines @@ -321,19 +322,26 @@ def test_main_historical(git_repo): darker.__main__.main(["--revision=foo..bar", "."]) +@pytest.fixture(scope="module") +def main_historical_ok_repo(request, tmp_path_factory): + """Git repository fixture for `test_main_historical_ok`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + repo.add({"README": "first"}, commit="Initial commit") + initial = repo.get_hash() + repo.add({"README": "second"}, commit="Second commit") + second = repo.get_hash() + + yield SimpleNamespace(root=repo.root, hash_initial=initial, hash_second=second) + + @pytest.mark.parametrize("arguments", [["--diff"], ["--check"], ["--diff", "--check"]]) -@pytest.mark.parametrize("src", [".", "foo/..", "{git_repo_root}"]) -def test_main_historical_ok(git_repo, arguments, src): +@pytest.mark.parametrize("src", [".", "foo/..", "{repo_root}"]) +def test_main_historical_ok(main_historical_ok_repo, arguments, src): """Runs ok for repository root with rev2 specified and ``--diff`` or ``--check``""" - git_repo.add({"README": "first"}, commit="Initial commit") - initial = git_repo.get_hash() - git_repo.add({"README": "second"}, commit="Second commit") - second = git_repo.get_hash() + repo = main_historical_ok_repo + revision_arg = f"--revision={repo.hash_initial}..{repo.hash_second}" - darker.__main__.main( - arguments - + [f"--revision={initial}..{second}", src.format(git_repo_root=git_repo.root)] - ) + darker.__main__.main([*arguments, revision_arg, src.format(repo_root=repo.root)]) def test_main_pre_commit_head(git_repo, monkeypatch): From 95f0bcdf812adddc56bc416fdd11f67077af9761 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 17:48:30 +0200 Subject: [PATCH 19/77] test: speed up `test_git_get_modified_python_files` --- src/darker/tests/test_git.py | 57 +++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py index 380ea5d01..dfe122b9f 100644 --- a/src/darker/tests/test_git.py +++ b/src/darker/tests/test_git.py @@ -265,6 +265,23 @@ def test_git_ls_files_others(git_repo): assert result == {Path("untracked.py")} +@pytest.fixture(scope="module") +def git_get_modified_python_files_repo(request, tmp_path_factory): + """Git repository fixture for `test_git_get_modified_python_files`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + repo.add( + { + "a.py": "original", + "b.py": "original", + "c/d.py": "original", + "c/e.js": "original", + "d/f/g.py": "original", + }, + commit="Initial commit", + ) + yield repo + + @pytest.mark.kwparametrize( dict(paths=["a.py"], expect=[]), dict(expect=[]), @@ -282,31 +299,23 @@ def test_git_ls_files_others(git_repo): modify_paths={}, paths=[], ) -def test_git_get_modified_python_files(git_repo, modify_paths, paths, expect): +def test_git_get_modified_python_files( + git_get_modified_python_files_repo, modify_paths, paths, expect, make_temp_copy +): """Tests for `darker.git.git_get_modified_python_files()`""" - root = Path(git_repo.root) - git_repo.add( - { - "a.py": "original", - "b.py": "original", - "c/d.py": "original", - "c/e.js": "original", - "d/f/g.py": "original", - }, - commit="Initial commit", - ) - for path, content in modify_paths.items(): - absolute_path = git_repo.root / path - if content is None: - absolute_path.unlink() - else: - absolute_path.parent.mkdir(parents=True, exist_ok=True) - absolute_path.write_bytes(content.encode("ascii")) - revrange = RevisionRange("HEAD", ":WORKTREE:") - - result = git.git_get_modified_python_files( - {root / p for p in paths}, revrange, repo_root=root - ) + with make_temp_copy(git_get_modified_python_files_repo.root) as root: + for path, content in modify_paths.items(): + absolute_path = root / path + if content is None: + absolute_path.unlink() + else: + absolute_path.parent.mkdir(parents=True, exist_ok=True) + absolute_path.write_bytes(content.encode("ascii")) + revrange = RevisionRange("HEAD", ":WORKTREE:") + + result = git.git_get_modified_python_files( + {root / p for p in paths}, revrange, repo_root=root + ) assert result == {Path(p) for p in expect} From f4588f83b55ebab525cd9a7512c2fea7c23f9287 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 17:53:56 +0200 Subject: [PATCH 20/77] test: speed up `test_reformat_and_flynt_single_file` --- ...est_main_reformat_and_flynt_single_file.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/darker/tests/test_main_reformat_and_flynt_single_file.py b/src/darker/tests/test_main_reformat_and_flynt_single_file.py index 5cee23891..0b1756a77 100644 --- a/src/darker/tests/test_main_reformat_and_flynt_single_file.py +++ b/src/darker/tests/test_main_reformat_and_flynt_single_file.py @@ -1,6 +1,6 @@ """Unit tests for `darker.__main__._reformat_and_flynt_single_file`.""" -# pylint: disable=too-many-arguments,use-dict-literal +# pylint: disable=no-member,redefined-outer-name,too-many-arguments,use-dict-literal from pathlib import Path from textwrap import dedent @@ -12,9 +12,21 @@ from darker.formatters.black_formatter import BlackFormatter from darker.git import EditedLinenumsDiffer from darkgraylib.git import RevisionRange +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.utils import TextDocument +@pytest.fixture(scope="module") +def reformat_and_flynt_single_file_repo(request, tmp_path_factory): + """Git repository fixture for `test_reformat_and_flynt_single_file`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + repo.add( + {"file.py": "import original\nprint( original )\n"}, + commit="Initial commit", + ) + yield repo + + @pytest.mark.kwparametrize( dict(), dict(relative_path="file.py.12345.tmp"), @@ -58,7 +70,7 @@ expect="import original\nprint( original )\n", ) def test_reformat_and_flynt_single_file( - git_repo, + reformat_and_flynt_single_file_repo, relative_path, rev2_content, rev2_isorted, @@ -66,17 +78,13 @@ def test_reformat_and_flynt_single_file( expect, ): """Test for `_reformat_and_flynt_single_file`.""" - git_repo.add( - {"file.py": "import original\nprint( original )\n"}, commit="Initial commit" - ) + repo = reformat_and_flynt_single_file_repo result = _reformat_and_flynt_single_file( - git_repo.root, + repo.root, Path(relative_path), Path("file.py"), exclusions, - EditedLinenumsDiffer( - git_repo.root, RevisionRange(rev1="HEAD", rev2=":WORKTREE") - ), + EditedLinenumsDiffer(repo.root, RevisionRange(rev1="HEAD", rev2=":WORKTREE")), TextDocument(rev2_content), TextDocument(rev2_isorted), has_isort_changes=False, From d2940926a33acaf576090aa73d45fa2d6a5d6319 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:06:32 +0200 Subject: [PATCH 21/77] test: speed up `test_format_edited_parts_stdin` --- .../tests/test_main_format_edited_parts.py | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/darker/tests/test_main_format_edited_parts.py b/src/darker/tests/test_main_format_edited_parts.py index cf3e2d440..7bc080f7a 100644 --- a/src/darker/tests/test_main_format_edited_parts.py +++ b/src/darker/tests/test_main_format_edited_parts.py @@ -133,6 +133,32 @@ def test_format_edited_parts( assert changes == expect_changes +@pytest.fixture(scope="module") +def format_edited_parts_stdin_repo(request, tmp_path_factory): + """Git repository fixture for `test_format_edited_parts_stdin`.""" + with unix_and_windows_newline_repos(request, tmp_path_factory) as repos: + fixture = {} + for newline, repo in repos.items(): + n = newline + paths = repo.add( + { + "a.py": f"print('a.py HEAD' ){n}#{n}print( 'a.py HEAD'){n}", + "b.py": f"print('b.py HEAD' ){n}#{n}print( 'b.py HEAD'){n}", + }, + commit="Initial commit", + ) + paths["a.py"].write_bytes( + f"print('a.py :WORKTREE:' ){n}#{n}print( 'a.py HEAD'){n}".encode( + "ascii" + ), + ) + paths["b.py"].write_bytes( + f"print('b.py HEAD' ){n}#{n}print( 'b.py WORKTREE'){n}".encode("ascii"), + ) + fixture[newline] = SimpleNamespace(root=repo.root, paths=paths) + yield fixture + + @pytest.mark.kwparametrize( dict( rev1="HEAD", @@ -169,22 +195,12 @@ def test_format_edited_parts( ), ) @pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["unix", "windows"]) -def test_format_edited_parts_stdin(git_repo, newline, rev1, rev2, expect): +def test_format_edited_parts_stdin( + format_edited_parts_stdin_repo, newline, rev1, rev2, expect +): """`format_edited_parts` with ``--stdin-filename``.""" + repo = format_edited_parts_stdin_repo[newline] n = newline # pylint: disable=invalid-name - paths = git_repo.add( - { - "a.py": f"print('a.py HEAD' ){n}#{n}print( 'a.py HEAD'){n}", - "b.py": f"print('b.py HEAD' ){n}#{n}print( 'b.py HEAD'){n}", - }, - commit="Initial commit", - ) - paths["a.py"].write_bytes( - f"print('a.py :WORKTREE:' ){n}#{n}print( 'a.py HEAD'){n}".encode("ascii"), - ) - paths["b.py"].write_bytes( - f"print('b.py HEAD' ){n}#{n}print( 'b.py WORKTREE'){n}".encode("ascii"), - ) stdin = f"print('a.py {rev1}' ){n}#{n}print( 'a.py STDIN'){n}".encode("ascii") with patch.object( darker.__main__.sys, # type: ignore[attr-defined] @@ -195,7 +211,7 @@ def test_format_edited_parts_stdin(git_repo, newline, rev1, rev2, expect): result = list( darker.__main__.format_edited_parts( - Path(git_repo.root), + Path(repo.root), {Path("a.py")}, Exclusions(formatter=set(), isort=set()), RevisionRange(rev1, rev2), @@ -205,7 +221,11 @@ def test_format_edited_parts_stdin(git_repo, newline, rev1, rev2, expect): ) expect = [ - (paths[path], TextDocument.from_lines(before), TextDocument.from_lines(after)) + ( + repo.paths[path], + TextDocument.from_lines(before), + TextDocument.from_lines(after), + ) for path, before, after in expect ] assert result == expect From 0d546db9cf5e0eb1a4d8bffc254ed2dcadebfea2 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:24:05 +0200 Subject: [PATCH 22/77] test: speed up `test_flynt_single_file` --- src/darker/tests/test_main.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index bdb0c534b..ec5bf2880 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -481,17 +481,30 @@ def test_print_diff(tmp_path, capsys): ] +@pytest.fixture(scope="module") +def maybe_flynt_single_file_repo(request, tmp_path_factory): + """Git repository fixture for `test_maybe_flynt_single_file`.""" + with unix_and_windows_newline_repos(request, tmp_path_factory) as repos: + for newline, repo in repos.items(): + repo.add( + {"test1.py": joinlines(ORIGINAL_SOURCE, newline)}, commit="Initial" + ) + yield repos + + @pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-1"]) @pytest.mark.parametrize("newline", ["\n", "\r\n"]) @pytest.mark.kwparametrize( dict(exclude=set(), expect=FLYNTED_SOURCE), dict(exclude={"**/*"}, expect=MODIFIED_SOURCE), ) -def test_maybe_flynt_single_file(git_repo, encoding, newline, exclude, expect): +def test_maybe_flynt_single_file( + maybe_flynt_single_file_repo, encoding, newline, exclude, expect +): """Flynt skipped if path matches exclusion patterns, encoding and newline intact""" - git_repo.add({"test1.py": joinlines(ORIGINAL_SOURCE, newline)}, commit="Initial") + repo = maybe_flynt_single_file_repo[newline] edited_linenums_differ = EditedLinenumsDiffer( - git_repo.root, RevisionRange("HEAD", ":WORKTREE:") + repo.root, RevisionRange("HEAD", ":WORKTREE:") ) # pylint: disable=duplicate-code src = Path("test1.py") content_ = TextDocument.from_lines( From f461f8618fdcf722fc33c65f05021a3f560542fc Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:44:35 +0200 Subject: [PATCH 23/77] test: speed up `test_format_edited_parts_historical` --- .../tests/test_main_format_edited_parts.py | 90 +++++++++++-------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/src/darker/tests/test_main_format_edited_parts.py b/src/darker/tests/test_main_format_edited_parts.py index 7bc080f7a..2fa93ab35 100644 --- a/src/darker/tests/test_main_format_edited_parts.py +++ b/src/darker/tests/test_main_format_edited_parts.py @@ -20,6 +20,7 @@ from darker.tests.helpers import unix_and_windows_newline_repos from darker.verification import NotEquivalentError from darkgraylib.git import WORKTREE, RevisionRange +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.utils import TextDocument, joinlines A_PY_ISORT = ["import os", "import sys", "", "print( '{}'.format('42'))", ""] @@ -320,51 +321,61 @@ def test_format_edited_parts_isort_on_already_formatted(git_repo): assert list(result) == [] +@pytest.fixture(scope="module") +def format_edited_parts_historical_repo(request, tmp_path_factory): + """Git repository fixture for `test_format_edited_parts_historical`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + a_py = { + "HEAD^": TextDocument.from_lines( + [ + "import a", + "from b import bar, foo", + "", + "a.foo()", + "bar()", + ], + ), + "HEAD": TextDocument.from_lines( + [ + "from b import bar, foo", + "", + "bar()", + ], + ), + ":WORKTREE:": TextDocument.from_lines( + [ + "from b import foo, bar", + "", + "bar( )", + ], + ), + "reformatted": TextDocument.from_lines( + [ + "from b import bar, foo", + "", + "bar()", + ], + ), + } + paths = repo.add({"a.py": a_py["HEAD^"].string}, commit="Initial commit") + repo.add({"a.py": a_py["HEAD"].string}, commit="Modified a.py") + paths["a.py"].write_text(a_py[":WORKTREE:"].string) + yield SimpleNamespace(root=repo.root, paths=paths, source_a_py=a_py) + + @pytest.mark.kwparametrize( dict(rev1="HEAD^", rev2="HEAD", expect=[]), dict(rev1="HEAD^", rev2=WORKTREE, expect=[(":WORKTREE:", "reformatted")]), dict(rev1="HEAD", rev2=WORKTREE, expect=[(":WORKTREE:", "reformatted")]), ) -def test_format_edited_parts_historical(git_repo, rev1, rev2, expect): +def test_format_edited_parts_historical( + format_edited_parts_historical_repo, rev1, rev2, expect +): """``format_edited_parts()`` is correct for different commit pairs.""" - a_py = { - "HEAD^": TextDocument.from_lines( - [ - "import a", - "from b import bar, foo", - "", - "a.foo()", - "bar()", - ], - ), - "HEAD": TextDocument.from_lines( - [ - "from b import bar, foo", - "", - "bar()", - ], - ), - ":WORKTREE:": TextDocument.from_lines( - [ - "from b import foo, bar", - "", - "bar( )", - ], - ), - "reformatted": TextDocument.from_lines( - [ - "from b import bar, foo", - "", - "bar()", - ], - ), - } - paths = git_repo.add({"a.py": a_py["HEAD^"].string}, commit="Initial commit") - git_repo.add({"a.py": a_py["HEAD"].string}, commit="Modified a.py") - paths["a.py"].write_text(a_py[":WORKTREE:"].string) + repo = format_edited_parts_historical_repo result = darker.__main__.format_edited_parts( - git_repo.root, + repo.root, {Path("a.py")}, Exclusions(), RevisionRange(rev1, rev2), @@ -372,4 +383,7 @@ def test_format_edited_parts_historical(git_repo, rev1, rev2, expect): report_unmodified=False, ) - assert list(result) == [(paths["a.py"], a_py[x[0]], a_py[x[1]]) for x in expect] + assert list(result) == [ + (repo.paths["a.py"], repo.source_a_py[x[0]], repo.source_a_py[x[1]]) + for x in expect + ] From 64953cff3f66a84ba94981fbd368aebe16acf3b3 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:50:39 +0200 Subject: [PATCH 24/77] test: speed up `test_edited_linenums_differ_compare_revisions` and `test_edited_linenums_differ_revision_vs_lines` --- src/darker/tests/test_git.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py index dfe122b9f..8dfdfdb4b 100644 --- a/src/darker/tests/test_git.py +++ b/src/darker/tests/test_git.py @@ -430,13 +430,23 @@ def test_git_get_modified_python_files_revision_range( ) +@pytest.fixture(scope="module") +def edited_linenums_differ_revisions_repo(request, tmp_path_factory): + """Git repository fixture for `git.EditedLinenumsDiffer` tests.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + paths = repo.add({"a.py": "1\n2\n3\n4\n5\n6\n7\n8\n"}, commit="Initial commit") + yield SimpleNamespace(root=repo.root, paths=paths) + + @edited_linenums_differ_cases -def test_edited_linenums_differ_compare_revisions(git_repo, context_lines, expect): +def test_edited_linenums_differ_compare_revisions( + edited_linenums_differ_revisions_repo, context_lines, expect +): """Tests for EditedLinenumsDiffer.revision_vs_worktree()""" - paths = git_repo.add({"a.py": "1\n2\n3\n4\n5\n6\n7\n8\n"}, commit="Initial commit") - paths["a.py"].write_bytes(b"1\n2\nthree\n4\n5\n6\nseven\n8\n") + repo = edited_linenums_differ_revisions_repo + repo.paths["a.py"].write_bytes(b"1\n2\nthree\n4\n5\n6\nseven\n8\n") revrange = RevisionRange("HEAD", ":WORKTREE:") - differ = git.EditedLinenumsDiffer(git_repo.root, revrange) + differ = git.EditedLinenumsDiffer(repo.root, revrange) linenums = differ.compare_revisions(Path("a.py"), context_lines) @@ -444,12 +454,14 @@ def test_edited_linenums_differ_compare_revisions(git_repo, context_lines, expec @edited_linenums_differ_cases -def test_edited_linenums_differ_revision_vs_lines(git_repo, context_lines, expect): +def test_edited_linenums_differ_revision_vs_lines( + edited_linenums_differ_revisions_repo, context_lines, expect +): """Tests for EditedLinenumsDiffer.revision_vs_lines()""" - git_repo.add({"a.py": "1\n2\n3\n4\n5\n6\n7\n8\n"}, commit="Initial commit") + repo = edited_linenums_differ_revisions_repo content = TextDocument.from_lines(["1", "2", "three", "4", "5", "6", "seven", "8"]) revrange = RevisionRange("HEAD", ":WORKTREE:") - differ = git.EditedLinenumsDiffer(git_repo.root, revrange) + differ = git.EditedLinenumsDiffer(repo.root, revrange) linenums = differ.revision_vs_lines(Path("a.py"), content, context_lines) From 8ef6e64437664df20c69755338be34ec3c618a29 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:59:48 +0200 Subject: [PATCH 25/77] test: speed up `test_edited_linenums_differ_revision_vs_lines_multiline_strings` --- src/darker/tests/test_git.py | 69 ++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py index 8dfdfdb4b..8cacd2228 100644 --- a/src/darker/tests/test_git.py +++ b/src/darker/tests/test_git.py @@ -468,33 +468,26 @@ def test_edited_linenums_differ_revision_vs_lines( assert linenums == expect -@pytest.mark.kwparametrize( - dict(context_lines=0, expect=[1, 3, 4, 5, 6, 8]), - dict(context_lines=1, expect=[1, 2, 3, 4, 5, 6, 7, 8]), -) -def test_edited_linenums_differ_revision_vs_lines_multiline_strings( - git_repo, context_lines, expect +@pytest.fixture(scope="module") +def edited_linenums_differ_revision_vs_lines_multiline_strings_repo( + request, tmp_path_factory ): - """Tests for EditedLinenumsDiffer.revision_vs_lines() with multi-line strings""" - git_repo.add( - { - "a.py": dedent( - """\ - change\n - keep\n - '''change first,\n - keep second\n - and third,\n - change fourth line of multiline'''\n - keep\n - change\n - """ - ) - }, - commit="Initial commit", - ) - content = TextDocument.from_lines( - [ + """Fixture for `test_edited_linenums_differ_revision_vs_lines_multiline_strings`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + a_py_content = dedent( + """\ + change\n + keep\n + '''change first,\n + keep second\n + and third,\n + change fourth line of multiline'''\n + keep\n + change\n + """ + ) + repo.add({"a.py": a_py_content}, commit="Initial commit") + content_lines = [ "CHANGED", "keep", "'''CHANGED FIRST,", @@ -504,11 +497,27 @@ def test_edited_linenums_differ_revision_vs_lines_multiline_strings( "keep", "CHANGED", ] - ) - revrange = RevisionRange("HEAD", ":WORKTREE:") - differ = git.EditedLinenumsDiffer(git_repo.root, revrange) + content = TextDocument.from_lines(content_lines) + revrange = RevisionRange("HEAD", ":WORKTREE:") + differ = git.EditedLinenumsDiffer(repo.root, revrange) + yield SimpleNamespace(content=content, differ=differ) - linenums = differ.revision_vs_lines(Path("a.py"), content, context_lines) + +@pytest.mark.kwparametrize( + dict(context_lines=0, expect=[1, 3, 4, 5, 6, 8]), + dict(context_lines=1, expect=[1, 2, 3, 4, 5, 6, 7, 8]), +) +def test_edited_linenums_differ_revision_vs_lines_multiline_strings( + edited_linenums_differ_revision_vs_lines_multiline_strings_repo, + context_lines, + expect, +): + """Tests for `git.EditedLinenumsDiffer.revision_vs_lines`, multi-line strings.""" + fixture = edited_linenums_differ_revision_vs_lines_multiline_strings_repo + + linenums = fixture.differ.revision_vs_lines( + Path("a.py"), fixture.content, context_lines + ) assert linenums == expect From 91742fb34124c65be604c7e6f276fe0652a4a895 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sat, 16 Nov 2024 18:41:43 +0200 Subject: [PATCH 26/77] test: speed up `test_main_isort.py` --- src/darker/tests/test_main_isort.py | 53 +++++++++++++++++------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/darker/tests/test_main_isort.py b/src/darker/tests/test_main_isort.py index 2e420931e..710381f8b 100644 --- a/src/darker/tests/test_main_isort.py +++ b/src/darker/tests/test_main_isort.py @@ -1,6 +1,6 @@ """Tests for the ``--isort`` option of the ``darker`` command-line interface.""" -# pylint: disable=redefined-outer-name,unused-argument,use-dict-literal +# pylint: disable=no-member,redefined-outer-name,unused-argument,use-dict-literal from types import SimpleNamespace from unittest.mock import patch @@ -12,13 +12,23 @@ from darker.exceptions import MissingPackageError from darker.formatters import black_formatter from darker.tests.helpers import isort_present +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture from darkgraylib.utils import TextDocument # Need to clear Black's `find_project_root` cache between tests pytestmark = pytest.mark.usefixtures("find_project_root_cache_clear") -def test_isort_option_without_isort(git_repo): +@pytest.fixture(scope="module") +def isort_repo(request, tmp_path_factory): + """Git repository fixture for `test_isort_option_with_isort*`.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + paths = repo.add({"test1.py": "original"}, commit="Initial commit") + paths["test1.py"].write_bytes(b"changed") + yield repo + + +def test_isort_option_without_isort(isort_repo): """Without isort, provide isort install instructions and error.""" # The `git_repo` fixture ensures test is not run in the Darker repository clone in # CI builds. It helps avoid a NixOS test issue. @@ -37,32 +47,33 @@ def test_isort_option_without_isort(git_repo): @pytest.fixture() -def run_isort(git_repo, monkeypatch, caplog, request): +def run_isort(isort_repo, make_temp_copy, monkeypatch, caplog, request): """Fixture for running Darker with requested arguments and a patched `isort`. Provides an `run_isort.isort_code` mock object which allows checking whether and how the `isort.code()` function was called. """ - monkeypatch.chdir(git_repo.root) - paths = git_repo.add({"test1.py": "original"}, commit="Initial commit") - paths["test1.py"].write_bytes(b"changed") - args = getattr(request, "param", ()) - isorted_code = "import os; import sys;" - blacken_code = "import os\nimport sys\n" - patch_run_black_ctx = patch.object( - black_formatter.BlackFormatter, - "run", - return_value=TextDocument(blacken_code), - ) - with patch_run_black_ctx, patch("darker.import_sorting.isort_code") as isort_code: - isort_code.return_value = isorted_code - darker.__main__.main(["--isort", "./test1.py", *args]) - return SimpleNamespace( - isort_code=isort_code, - caplog=caplog, - root=git_repo.root, + with make_temp_copy(isort_repo.root) as root: + monkeypatch.chdir(root) + args = getattr(request, "param", ()) + isorted_code = "import os; import sys;" + blacken_code = "import os\nimport sys\n" + patch_run_black_ctx = patch.object( + black_formatter.BlackFormatter, + "run", + return_value=TextDocument(blacken_code), ) + with patch_run_black_ctx, patch( + "darker.import_sorting.isort_code" + ) as isort_code: + isort_code.return_value = isorted_code + darker.__main__.main(["--isort", "./test1.py", *args]) + return SimpleNamespace( + isort_code=isort_code, + caplog=caplog, + root=root, + ) def test_isort_option_with_isort(run_isort): From d866a8c7eb0bf2373e165ec55cd25c54de38f3ef Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:10:45 +0300 Subject: [PATCH 27/77] feat: remove global Black imports --- src/darker/command_line.py | 3 +- src/darker/configuration/__init__.py | 1 + src/darker/configuration/target_version.py | 19 +++ src/darker/files.py | 153 ++++++++++++----- src/darker/formatters/black_formatter.py | 23 ++- src/darker/help.py | 3 +- src/darker/tests/test_command_line.py | 15 +- src/darker/tests/test_files.py | 37 +++-- src/darker/tests/test_formatters_black.py | 136 +-------------- src/darker/tests/test_verification.py | 34 +--- src/darker/verification.py | 185 ++++++++++++++++++--- 11 files changed, 349 insertions(+), 260 deletions(-) create mode 100644 src/darker/configuration/__init__.py create mode 100644 src/darker/configuration/target_version.py diff --git a/src/darker/command_line.py b/src/darker/command_line.py index 4a092ba63..5d4f94dfd 100644 --- a/src/darker/command_line.py +++ b/src/darker/command_line.py @@ -5,8 +5,6 @@ from functools import partial from typing import List, Optional, Tuple -from black import TargetVersion - import darkgraylib.command_line from darker import help as hlp from darker.config import ( @@ -15,6 +13,7 @@ DarkerConfig, OutputMode, ) +from darker.configuration.target_version import TargetVersion from darker.formatters import get_formatter_names from darker.version import __version__ from darkgraylib.command_line import add_parser_argument diff --git a/src/darker/configuration/__init__.py b/src/darker/configuration/__init__.py new file mode 100644 index 000000000..d6f0ec5c4 --- /dev/null +++ b/src/darker/configuration/__init__.py @@ -0,0 +1 @@ +"""Configuration and command line handling.""" diff --git a/src/darker/configuration/target_version.py b/src/darker/configuration/target_version.py new file mode 100644 index 000000000..07ad4c01f --- /dev/null +++ b/src/darker/configuration/target_version.py @@ -0,0 +1,19 @@ +"""Data structures configuring Darker and formatter plugin behavior.""" + +from enum import Enum + + +class TargetVersion(Enum): + """Python version numbers.""" + + PY33 = 3 + PY34 = 4 + PY35 = 5 + PY36 = 6 + PY37 = 7 + PY38 = 8 + PY39 = 9 + PY310 = 10 + PY311 = 11 + PY312 = 12 + PY313 = 13 diff --git a/src/darker/files.py b/src/darker/files.py index 068d12c4b..97425facb 100644 --- a/src/darker/files.py +++ b/src/darker/files.py @@ -2,18 +2,9 @@ from __future__ import annotations -import inspect -from typing import TYPE_CHECKING, Collection - -from black import ( - DEFAULT_EXCLUDES, - DEFAULT_INCLUDES, - Report, - err, - find_user_pyproject_toml, - gen_python_files, - re_compile_maybe_verbose, -) +import re +from functools import lru_cache +from typing import TYPE_CHECKING, Collection, Iterable, Iterator, Pattern from darkgraylib.files import find_project_root @@ -25,22 +16,116 @@ def find_pyproject_toml(path_search_start: tuple[str, ...]) -> str | None: """Find the absolute filepath to a pyproject.toml if it exists""" + path_project_root = find_project_root(path_search_start) path_pyproject_toml = path_project_root / "pyproject.toml" if path_pyproject_toml.is_file(): return str(path_pyproject_toml) + return None + + +DEFAULT_EXCLUDE_RE = re.compile( + r"/(\.direnv" + r"|\.eggs" + r"|\.git" + r"|\.hg" + r"|\.ipynb_checkpoints" + r"|\.mypy_cache" + r"|\.nox" + r"|\.pytest_cache" + r"|\.ruff_cache" + r"|\.tox" + r"|\.svn" + r"|\.venv" + r"|\.vscode" + r"|__pypackages__" + r"|_build" + r"|buck-out" + r"|build" + r"|dist" + r"|venv)/" +) +DEFAULT_INCLUDE_RE = re.compile(r"(\.pyi?|\.ipynb)$") + + +@lru_cache +def _cached_resolve(path: Path) -> Path: + return path.resolve() + +def _resolves_outside_root_or_cannot_stat(path: Path, root: Path) -> bool: + """Return whether path is a symlink that points outside the root directory. + + Also returns True if we failed to resolve the path. + + This function has been adapted from Black 24.10.0. + + """ try: - path_user_pyproject_toml = find_user_pyproject_toml() - return ( - str(path_user_pyproject_toml) - if path_user_pyproject_toml.is_file() - else None - ) - except (PermissionError, RuntimeError) as e: - # We do not have access to the user-level config directory, so ignore it. - err(f"Ignoring user configuration directory due to {e!r}") - return None + resolved_path = _cached_resolve(path) + except OSError: + return True + try: + resolved_path.relative_to(root) + except ValueError: + return True + return False + + +def _path_is_excluded( + normalized_path: str, + pattern: Pattern[str] | None, +) -> bool: + """Return whether the path is excluded by the pattern. + + This function has been adapted from Black 24.10.0. + + """ + match = pattern.search(normalized_path) if pattern else None + return bool(match and match.group(0)) + + +def _gen_python_files( + paths: Iterable[Path], + root: Path, + exclude: Pattern[str], + extend_exclude: Pattern[str] | None, + force_exclude: Pattern[str] | None, +) -> Iterator[Path]: + """Generate all files under ``path`` whose paths are not excluded. + + This function has been adapted from Black 24.10.0. + + """ + if not root.is_absolute(): + message = f"`root` must be absolute, not {root}" + raise ValueError(message) + for child in paths: + if not child.is_absolute(): + message = f"`child` must be absolute, not {child}" + raise ValueError(message) + root_relative_path = child.relative_to(root).as_posix() + + # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. + root_relative_path = f"/{root_relative_path}" + if child.is_dir(): + root_relative_path = f"{root_relative_path}/" + + if any( + _path_is_excluded(root_relative_path, x) + for x in [exclude, extend_exclude, force_exclude] + ) or _resolves_outside_root_or_cannot_stat(child, root): + continue + + if child.is_dir(): + yield from _gen_python_files( + child.iterdir(), root, exclude, extend_exclude, force_exclude + ) + + elif child.is_file(): + include_match = DEFAULT_INCLUDE_RE.search(root_relative_path) + if include_match: + yield child def filter_python_files( @@ -58,32 +143,16 @@ def filter_python_files( ``black_config``, relative to ``root``. """ - sig = inspect.signature(gen_python_files) - # those two exist and are required in black>=21.7b1.dev9 - kwargs = {"verbose": False, "quiet": False} if "verbose" in sig.parameters else {} - # `gitignore=` was replaced with `gitignore_dict=` in black==22.10.1.dev19+gffaaf48 - for param in sig.parameters: - if param == "gitignore": - kwargs[param] = None # type: ignore[assignment] - elif param == "gitignore_dict": - kwargs[param] = {} # type: ignore[assignment] absolute_paths = {p.resolve() for p in paths} directories = {p for p in absolute_paths if p.is_dir()} files = {p for p in absolute_paths if p not in directories} files_from_directories = set( - gen_python_files( + _gen_python_files( directories, root, - include=DEFAULT_INCLUDE_RE, - exclude=formatter.get_exclude(DEFAULT_EXCLUDE_RE), - extend_exclude=formatter.get_extend_exclude(), - force_exclude=formatter.get_force_exclude(), - report=Report(), - **kwargs, # type: ignore[arg-type] + formatter.get_exclude(DEFAULT_EXCLUDE_RE), + formatter.get_extend_exclude(), + formatter.get_force_exclude(), ) ) return {p.resolve().relative_to(root) for p in files_from_directories | files} - - -DEFAULT_EXCLUDE_RE = re_compile_maybe_verbose(DEFAULT_EXCLUDES) -DEFAULT_INCLUDE_RE = re_compile_maybe_verbose(DEFAULT_INCLUDES) diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index 540283b05..5b313381f 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -39,14 +39,6 @@ import logging from typing import TYPE_CHECKING, TypedDict -from black import FileMode as Mode -from black import ( - TargetVersion, - format_str, - parse_pyproject_toml, - re_compile_maybe_verbose, -) - from darker.files import find_pyproject_toml from darker.formatters.base_formatter import BaseFormatter from darkgraylib.config import ConfigurationError @@ -56,9 +48,11 @@ from argparse import Namespace from typing import Pattern + from black import FileMode as Mode + from black import TargetVersion + from darker.formatters.formatter_config import BlackConfig -__all__ = ["Mode"] logger = logging.getLogger(__name__) @@ -96,6 +90,9 @@ def read_config(self, src: tuple[str, ...], args: Namespace) -> None: self._read_cli_args(args) def _read_config_file(self, config_path: str) -> None: # noqa: C901 + # Local import so Darker can be run without Black installed + from black import parse_pyproject_toml, re_compile_maybe_verbose + raw_config = parse_pyproject_toml(config_path) if "line_length" in raw_config: self.config["line_length"] = raw_config["line_length"] @@ -153,6 +150,9 @@ def run(self, content: TextDocument) -> TextDocument: :return: The reformatted content """ + # Local import so Darker can be run without Black installed + from black import format_str + contents_for_black = content.string_with_newline("\n") if contents_for_black.strip(): dst_contents = format_str( @@ -173,6 +173,11 @@ def _make_black_options(self) -> Mode: # Collect relevant Black configuration options from ``self.config`` in order to # pass them to Black's ``format_str()``. File exclusion options aren't needed # since at this point we already have a single file's content to work on. + + # Local import so Darker can be run without Black installed + from black import FileMode as Mode + from black import TargetVersion + mode = BlackModeAttributes() if "line_length" in self.config: mode["line_length"] = self.config["line_length"] diff --git a/src/darker/help.py b/src/darker/help.py index 1768a0a84..d9e64cade 100644 --- a/src/darker/help.py +++ b/src/darker/help.py @@ -2,8 +2,7 @@ from textwrap import dedent -from black import TargetVersion - +from darker.configuration.target_version import TargetVersion from darker.formatters import get_formatter_names diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index cb43cbb73..964690677 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -14,13 +14,12 @@ import pytest import toml -from black import TargetVersion +from black import FileMode, TargetVersion import darker.help from darker.__main__ import main from darker.command_line import make_argument_parser, parse_command_line from darker.config import Exclusions -from darker.formatters import black_formatter from darker.formatters.black_formatter import BlackFormatter from darker.tests.helpers import flynt_present, isort_present from darkgraylib.config import ConfigurationError @@ -578,9 +577,8 @@ def test_black_options(black_options_files, options, expect): # shared by all test cases. The "main.py" file modified by the test run needs to be # reset to its original content before the next test case. black_options_files["main.py"].write_bytes(b'print ("Hello World!")\n') - with patch.object( - black_formatter, "Mode", wraps=black_formatter.Mode - ) as file_mode_class: + with patch("black.FileMode", wraps=FileMode) as file_mode_class: + # end of test setup, now call the function under test main(options + [str(path) for path in black_options_files.values()]) @@ -705,10 +703,13 @@ def test_black_config_file_and_options( """Black configuration file and command line options are combined correctly""" repo_files = black_config_file_and_options_files repo_files["pyproject.toml"].write_text(joinlines(["[tool.black]", *config])) - mode_class_mock = Mock(wraps=black_formatter.Mode) + mode_class_mock = Mock(wraps=FileMode) # Speed up tests by mocking `format_str` to skip running Black format_str = Mock(return_value="a = [1, 2,]") - with patch.multiple(black_formatter, Mode=mode_class_mock, format_str=format_str): + with patch("black.FileMode", mode_class_mock), patch( + "black.format_str", format_str + ): + # end of test setup, now call the function under test main(options + [str(path) for path in repo_files.values()]) diff --git a/src/darker/tests/test_files.py b/src/darker/tests/test_files.py index 227e03f11..32ed1b3b9 100644 --- a/src/darker/tests/test_files.py +++ b/src/darker/tests/test_files.py @@ -1,22 +1,35 @@ """Test for the `darker.files` module.""" -import io -from contextlib import redirect_stderr +# pylint: disable=use-dict-literal + from pathlib import Path -from unittest.mock import MagicMock, patch + +import pytest from darker import files -@patch("darker.files.find_user_pyproject_toml") -def test_find_pyproject_toml(find_user_pyproject_toml: MagicMock) -> None: +@pytest.mark.kwparametrize( + dict(start="only_pyproject/subdir", expect="only_pyproject/pyproject.toml"), + dict(start="only_git/subdir", expect=None), + dict(start="git_and_pyproject/subdir", expect="git_and_pyproject/pyproject.toml"), +) +def test_find_pyproject_toml(tmp_path: Path, start: str, expect: str) -> None: """Test `files.find_pyproject_toml` with no user home directory.""" - find_user_pyproject_toml.side_effect = RuntimeError() - with redirect_stderr(io.StringIO()) as stderr: - # end of test setup + (tmp_path / "only_pyproject").mkdir() + (tmp_path / "only_pyproject" / "pyproject.toml").touch() + (tmp_path / "only_pyproject" / "subdir").mkdir() + (tmp_path / "only_git").mkdir() + (tmp_path / "only_git" / ".git").mkdir() + (tmp_path / "only_git" / "subdir").mkdir() + (tmp_path / "git_and_pyproject").mkdir() + (tmp_path / "git_and_pyproject" / ".git").mkdir() + (tmp_path / "git_and_pyproject" / "pyproject.toml").touch() + (tmp_path / "git_and_pyproject" / "subdir").mkdir() - result = files.find_pyproject_toml(path_search_start=(str(Path.cwd().root),)) + result = files.find_pyproject_toml(path_search_start=(str(tmp_path / start),)) - assert result is None - err = stderr.getvalue() - assert "Ignoring user configuration" in err + if not expect: + assert result is None + else: + assert result == str(tmp_path / expect) diff --git a/src/darker/tests/test_formatters_black.py b/src/darker/tests/test_formatters_black.py index 74ec3d66e..18c94e6f2 100644 --- a/src/darker/tests/test_formatters_black.py +++ b/src/darker/tests/test_formatters_black.py @@ -7,17 +7,14 @@ from argparse import Namespace from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Optional, Pattern -from unittest.mock import ANY, Mock, call, patch +from typing import TYPE_CHECKING +from unittest.mock import ANY, patch import pytest import regex -from black import Mode, Report, TargetVersion -from pathspec import PathSpec +from black import Mode, TargetVersion -from darker import files from darker.files import DEFAULT_EXCLUDE_RE, filter_python_files -from darker.formatters import black_formatter from darker.formatters.black_formatter import BlackFormatter from darkgraylib.config import ConfigurationError from darkgraylib.testtools.helpers import raises_or_matches @@ -213,129 +210,6 @@ def test_filter_python_files( # pylint: disable=too-many-arguments assert result == expect_paths -def make_mock_gen_python_files_black_21_7b1_dev8(): - """Create `gen_python_files` mock for Black 21.7b1.dev8+ge76adbe - - Also record the call made to the mock function for test verification. - - This revision didn't yet have the `verbose` and `quiet` parameters. - - """ - calls = Mock() - - # pylint: disable=unused-argument - def gen_python_files( - paths: Iterable[Path], - root: Path, - include: Pattern[str], - exclude: Pattern[str], - extend_exclude: Optional[Pattern[str]], - force_exclude: Optional[Pattern[str]], - report: Report, - gitignore: Optional[PathSpec], - ) -> Iterator[Path]: - calls.gen_python_files = call(gitignore=gitignore) - for _ in []: - yield Path() - - return gen_python_files, calls - - -def make_mock_gen_python_files_black_21_7b1_dev9(): - """Create `gen_python_files` mock for Black 21.7b1.dev9+gb1d0601 - - Also record the call made to the mock function for test verification. - - This revision added `verbose` and `quiet` parameters to `gen_python_files`. - - """ - calls = Mock() - - # pylint: disable=unused-argument - def gen_python_files( - paths: Iterable[Path], - root: Path, - include: Pattern[str], - exclude: Pattern[str], - extend_exclude: Optional[Pattern[str]], - force_exclude: Optional[Pattern[str]], - report: Report, - gitignore: Optional[PathSpec], - *, - verbose: bool, - quiet: bool, - ) -> Iterator[Path]: - calls.gen_python_files = call( - gitignore=gitignore, - verbose=verbose, - quiet=quiet, - ) - for _ in []: - yield Path() - - return gen_python_files, calls - - -def make_mock_gen_python_files_black_22_10_1_dev19(): - """Create `gen_python_files` mock for Black 22.10.1.dev19+gffaaf48 - - Also record the call made to the mock function for test verification. - - This revision renamed the `gitignore` parameter to `gitignore_dict`. - - """ - calls = Mock() - - # pylint: disable=unused-argument - def gen_python_files( - paths: Iterable[Path], - root: Path, - include: Pattern[str], - exclude: Pattern[str], - extend_exclude: Optional[Pattern[str]], - force_exclude: Optional[Pattern[str]], - report: Report, - gitignore_dict: Optional[Dict[Path, PathSpec]], - *, - verbose: bool, - quiet: bool, - ) -> Iterator[Path]: - calls.gen_python_files = call( - gitignore_dict=gitignore_dict, - verbose=verbose, - quiet=quiet, - ) - for _ in []: - yield Path() - - return gen_python_files, calls - - -@pytest.mark.kwparametrize( - dict( - make_mock=make_mock_gen_python_files_black_21_7b1_dev8, - expect={"gitignore": None}, - ), - dict( - make_mock=make_mock_gen_python_files_black_21_7b1_dev9, - expect={"gitignore": None, "verbose": False, "quiet": False}, - ), - dict( - make_mock=make_mock_gen_python_files_black_22_10_1_dev19, - expect={"gitignore_dict": {}, "verbose": False, "quiet": False}, - ), -) -def test_filter_python_files_gitignore(make_mock, tmp_path, expect): - """`filter_python_files` uses per-Black-version params to `gen_python_files`""" - gen_python_files, calls = make_mock() - with patch.object(files, "gen_python_files", gen_python_files): - # end of test setup - - _ = filter_python_files(set(), tmp_path, BlackFormatter()) - - assert calls.gen_python_files.kwargs == expect - - @pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-1"]) @pytest.mark.parametrize("newline", ["\n", "\r\n"]) def test_run(encoding, newline): @@ -360,7 +234,7 @@ def test_run(encoding, newline): def test_run_always_uses_unix_newlines(newline): """Content is always passed to Black with Unix newlines""" src = TextDocument.from_str(f"print ( 'touché' ){newline}") - with patch.object(black_formatter, "format_str") as format_str: + with patch("black.format_str") as format_str: format_str.return_value = 'print("touché")\n' _ = BlackFormatter().run(src) @@ -472,7 +346,7 @@ def test_run_configuration( ): """`BlackFormatter.run` passes correct configuration to Black.""" src = TextDocument.from_str("import os\n") - with patch.object(black_formatter, "format_str") as format_str, raises_or_matches( + with patch("black.format_str") as format_str, raises_or_matches( expect, [] ) as check: format_str.return_value = "import os\n" diff --git a/src/darker/tests/test_verification.py b/src/darker/tests/test_verification.py index 4cad7f42a..39e49ae6d 100644 --- a/src/darker/tests/test_verification.py +++ b/src/darker/tests/test_verification.py @@ -2,40 +2,10 @@ # pylint: disable=use-dict-literal -from typing import List - import pytest -from darker.verification import ( - ASTVerifier, - BinarySearch, - NotEquivalentError, - verify_ast_unchanged, -) -from darkgraylib.utils import DiffChunk, TextDocument - - -@pytest.mark.kwparametrize( - dict(dst_content=["if False: pass"], expect=AssertionError), - dict(dst_content=["if True:", " pass"], expect=None), -) -def test_verify_ast_unchanged(dst_content, expect): - """``verify_ast_unchanged`` detects changes correctly""" - black_chunks: List[DiffChunk] = [(1, ("black",), ("chunks",))] - edited_linenums = [1, 2] - try: - - verify_ast_unchanged( - TextDocument.from_lines(["if True: pass"]), - TextDocument.from_lines(dst_content), - black_chunks, - edited_linenums, - ) - - except NotEquivalentError: - assert expect is AssertionError - else: - assert expect is None +from darker.verification import ASTVerifier, BinarySearch +from darkgraylib.utils import TextDocument def test_ast_verifier_is_equivalent(): diff --git a/src/darker/verification.py b/src/darker/verification.py index b3921c97c..f6bd00462 100644 --- a/src/darker/verification.py +++ b/src/darker/verification.py @@ -1,18 +1,14 @@ """Verification for unchanged AST before and after reformatting""" -from typing import Dict, List +from __future__ import annotations -from black import assert_equivalent, parse_ast, stringify_ast +import ast +import sys +import warnings +from typing import TYPE_CHECKING, Dict, Iterator -from darker.utils import debug_dump -from darkgraylib.utils import DiffChunk, TextDocument - -try: - # Black 24.2.1 and later - from black.parsing import ASTSafetyError # pylint: disable=ungrouped-imports -except ImportError: - # Black 24.2.0 and earlier - ASTSafetyError = AssertionError # type: ignore[assignment,misc] +if TYPE_CHECKING: + from darkgraylib.utils import TextDocument class NotEquivalentError(Exception): @@ -63,18 +59,161 @@ def result(self) -> int: return self.high -def verify_ast_unchanged( - edited_to_file: TextDocument, - reformatted: TextDocument, - black_chunks: List[DiffChunk], - edited_linenums: List[int], -) -> None: - """Verify that source code parses to the same AST before and after reformat""" - try: - assert_equivalent(edited_to_file.string, reformatted.string) - except ASTSafetyError as exc_info: - debug_dump(black_chunks, edited_linenums) - raise NotEquivalentError() from exc_info +def parse_ast(src: str) -> ast.AST: + """Parse source code with fallback for type comments. + + This function has been adapted from Black 24.10.0. + + """ + filename = "" + versions = [(3, minor) for minor in range(5, sys.version_info[1] + 1)] + + first_error = "" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", SyntaxWarning) + warnings.simplefilter("ignore", DeprecationWarning) + # Try with type comments first + for version in reversed(versions): + try: + return ast.parse( + src, filename, feature_version=version, type_comments=True + ) + except SyntaxError as e: # noqa: PERF203 + if not first_error: + first_error = str(e) + + # Fallback without type comments + for version in reversed(versions): + try: + return ast.parse( + src, filename, feature_version=version, type_comments=False + ) + except SyntaxError: # noqa: PERF203 + continue + + raise SyntaxError(first_error) + + +def _normalize(lineend: str, value: str) -> str: + """Strip any leading and trailing space from each line. + + This function has been adapted from Black 24.10.0. + + """ + stripped: list[str] = [i.strip() for i in value.splitlines()] + normalized = lineend.join(stripped) + # ...and remove any blank lines at the beginning and end of + # the whole string + return normalized.strip() + + +def stringify_ast(node: ast.AST) -> Iterator[str]: + """Generate strings to compare ASTs by content using a simple visitor. + + This function has been adapted from Black 24.10.0. + + """ + return _stringify_ast(node, []) + + +def _stringify_ast_with_new_parent( + node: ast.AST, parent_stack: list[ast.AST], new_parent: ast.AST +) -> Iterator[str]: + """Generate strings to compare, recurse with a new parent. + + This function has been adapted from Black 24.10.0. + + """ + parent_stack.append(new_parent) + yield from _stringify_ast(node, parent_stack) + parent_stack.pop() + + +def _stringify_ast(node: ast.AST, parent_stack: list[ast.AST]) -> Iterator[str]: + """Generate strings to compare ASTs by content. + + This function has been adapted from Black 24.10.0. + + """ + if ( + isinstance(node, ast.Constant) + and isinstance(node.value, str) + and node.kind == "u" + ): + # It's a quirk of history that we strip the u prefix over here. We used to + # rewrite the AST nodes for Python version compatibility and we never copied + # over the kind + node.kind = None + + yield f"{' ' * len(parent_stack)}{node.__class__.__name__}(" + + for field in sorted(node._fields): + # TypeIgnore has only one field 'lineno' which breaks this comparison + if isinstance(node, ast.TypeIgnore): + break + + try: + value: object = getattr(node, field) + except AttributeError: + continue + + yield f"{' ' * (len(parent_stack) + 1)}{field}=" + + if isinstance(value, list): + for item in value: + yield from _stringify_list_item(field, item, node, parent_stack) + + elif isinstance(value, ast.AST): + yield from _stringify_ast_with_new_parent(value, parent_stack, node) + + else: + normalized: object + if ( + isinstance(node, ast.Constant) + and field == "value" + and isinstance(value, str) + and len(parent_stack) >= 2 + # Any standalone string, ideally this would + # exactly match black.nodes.is_docstring + and isinstance(parent_stack[-1], ast.Expr) + ): + # Constant strings may be indented across newlines, if they are + # docstrings; fold spaces after newlines when comparing. Similarly, + # trailing and leading space may be removed. + normalized = _normalize("\n", value) + elif field == "type_comment" and isinstance(value, str): + # Trailing whitespace in type comments is removed. + normalized = value.rstrip() + else: + normalized = value + yield ( + f"{' ' * (len(parent_stack) + 1)}{normalized!r}, #" + f" {value.__class__.__name__}" + ) + + yield f"{' ' * len(parent_stack)}) # /{node.__class__.__name__}" + + +def _stringify_list_item( + field: str, item: ast.AST, node: ast.AST, parent_stack: list[ast.AST] +) -> Iterator[str]: + """Generate string for an AST list item. + + This function has been adapted from Black 24.10.0. + + """ + # Ignore nested tuples within del statements, because we may insert + # parentheses and they change the AST. + if ( + field == "targets" + and isinstance(node, ast.Delete) + and isinstance(item, ast.Tuple) + ): + for elt in item.elts: + yield from _stringify_ast_with_new_parent(elt, parent_stack, node) + + elif isinstance(item, ast.AST): + yield from _stringify_ast_with_new_parent(item, parent_stack, node) class ASTVerifier: # pylint: disable=too-few-public-methods From 0239a61331476c4244987718b99b51b93e9a3eaa Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:30:27 +0200 Subject: [PATCH 28/77] fix: pylint import outside toplevel Co-authored-by: csteiner <47841949+clintonsteiner@users.noreply.github.com> --- src/darker/formatters/black_formatter.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index 5b313381f..a4af39eb1 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -91,7 +91,10 @@ def read_config(self, src: tuple[str, ...], args: Namespace) -> None: def _read_config_file(self, config_path: str) -> None: # noqa: C901 # Local import so Darker can be run without Black installed - from black import parse_pyproject_toml, re_compile_maybe_verbose + from black import ( # pylint: disable=import-outside-toplevel + parse_pyproject_toml, + re_compile_maybe_verbose, + ) raw_config = parse_pyproject_toml(config_path) if "line_length" in raw_config: @@ -151,7 +154,7 @@ def run(self, content: TextDocument) -> TextDocument: """ # Local import so Darker can be run without Black installed - from black import format_str + from black import format_str # pylint: disable=import-outside-toplevel contents_for_black = content.string_with_newline("\n") if contents_for_black.strip(): @@ -175,8 +178,8 @@ def _make_black_options(self) -> Mode: # since at this point we already have a single file's content to work on. # Local import so Darker can be run without Black installed - from black import FileMode as Mode - from black import TargetVersion + from black import FileMode as Mode # pylint: disable=import-outside-toplevel + from black import TargetVersion # pylint: disable=import-outside-toplevel mode = BlackModeAttributes() if "line_length" in self.config: From 24484f3c24be969d21aa7be0905ca0e7f91c95c9 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:32:46 +0200 Subject: [PATCH 29/77] style: fix indentation Co-authored-by: csteiner <47841949+clintonsteiner@users.noreply.github.com> --- src/darker/files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/darker/files.py b/src/darker/files.py index 97425facb..beb92ae35 100644 --- a/src/darker/files.py +++ b/src/darker/files.py @@ -112,8 +112,8 @@ def _gen_python_files( root_relative_path = f"{root_relative_path}/" if any( - _path_is_excluded(root_relative_path, x) - for x in [exclude, extend_exclude, force_exclude] + _path_is_excluded(root_relative_path, x) + for x in [exclude, extend_exclude, force_exclude] ) or _resolves_outside_root_or_cannot_stat(child, root): continue From 6e7fd01a1b071ae4f9ef4e09909ffe815f0f32f6 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 21:57:13 +0200 Subject: [PATCH 30/77] feat: move black dependency to darker[black] extras --- action/main.py | 2 +- action/tests/test_main.py | 16 +++++++++------- setup.cfg | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/action/main.py b/action/main.py index b903bca4d..d4cd0f988 100644 --- a/action/main.py +++ b/action/main.py @@ -22,7 +22,7 @@ run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True) # nosec -req = ["darker[color,isort]"] +req = ["darker[black,color,isort]"] if VERSION: if VERSION.startswith("@"): req[0] = f"git+https://github.com/akaihola/darker{VERSION}#egg={req[0]}" diff --git a/action/tests/test_main.py b/action/tests/test_main.py index beca44654..9a10e684a 100644 --- a/action/tests/test_main.py +++ b/action/tests/test_main.py @@ -91,23 +91,25 @@ def test_creates_virtualenv(tmp_path, main_patch): @pytest.mark.kwparametrize( - dict(run_main_env={}, expect=["darker[color,isort]"]), + dict(run_main_env={}, expect=["darker[black,color,isort]"]), dict( - run_main_env={"INPUT_VERSION": "1.5.0"}, expect=["darker[color,isort]==1.5.0"] + run_main_env={"INPUT_VERSION": "1.5.0"}, + expect=["darker[black,color,isort]==1.5.0"], ), dict( run_main_env={"INPUT_VERSION": "@master"}, expect=[ - "git+https://github.com/akaihola/darker@master#egg=darker[color,isort]" + "git+https://github.com/akaihola/darker" + "@master#egg=darker[black,color,isort]" ], ), dict( run_main_env={"INPUT_LINT": "dummy"}, - expect=["darker[color,isort]"], + expect=["darker[black,color,isort]"], ), dict( run_main_env={"INPUT_LINT": "dummy,foobar"}, - expect=["darker[color,isort]"], + expect=["darker[black,color,isort]"], ), ) def test_installs_packages(tmp_path, main_patch, run_main_env, expect): @@ -208,7 +210,7 @@ def test_error_if_pip_fails(tmp_path, capsys): run_module("main") assert main_patch.subprocess.run.call_args_list[-1] == call( - [ANY, "-m", "pip", "install", "darker[color,isort]"], + [ANY, "-m", "pip", "install", "darker[black,color,isort]"], check=False, stdout=PIPE, stderr=STDOUT, @@ -216,7 +218,7 @@ def test_error_if_pip_fails(tmp_path, capsys): ) assert ( capsys.readouterr().out.splitlines()[-1] - == "::error::Failed to install darker[color,isort]." + == "::error::Failed to install darker[black,color,isort]." ) main_patch.sys.exit.assert_called_once_with(42) diff --git a/setup.cfg b/setup.cfg index 9920959a3..8b9989682 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,6 @@ package_dir = packages = find: install_requires = # NOTE: remember to keep `constraints-oldest.txt` in sync with these - black>=22.3.0 darkgraylib~=2.1.0 toml>=0.10.0 typing_extensions>=4.0.1 @@ -52,6 +51,8 @@ console_scripts = darker = darker.__main__:main_with_error_handling [options.extras_require] +black = + black>=22.3.0 flynt = flynt>=0.76 isort = From 771d131f10a09d62414358c4d84eb40d5ecbd5ea Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:09:20 +0200 Subject: [PATCH 31/77] feat: verbose error if Black not found --- src/darker/formatters/black_formatter.py | 32 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index a4af39eb1..604b717f1 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -39,6 +39,7 @@ import logging from typing import TYPE_CHECKING, TypedDict +from darker.exceptions import DependencyError from darker.files import find_pyproject_toml from darker.formatters.base_formatter import BaseFormatter from darkgraylib.config import ConfigurationError @@ -90,11 +91,26 @@ def read_config(self, src: tuple[str, ...], args: Namespace) -> None: self._read_cli_args(args) def _read_config_file(self, config_path: str) -> None: # noqa: C901 - # Local import so Darker can be run without Black installed - from black import ( # pylint: disable=import-outside-toplevel - parse_pyproject_toml, - re_compile_maybe_verbose, - ) + # Local import so Darker can be run without Black installed. + # Do error handling here. This is the first Black importing method being hit. + try: + from black import ( # pylint: disable=import-outside-toplevel + parse_pyproject_toml, + re_compile_maybe_verbose, + ) + except ImportError as exc: + logger.warning( + "To re-format code using Black, install it using e.g." + " `pip install 'darker[black]'` or" + " `pip install black`" + ) + 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 Black package" + raise DependencyError(message) from exc raw_config = parse_pyproject_toml(config_path) if "line_length" in raw_config: @@ -153,7 +169,8 @@ def run(self, content: TextDocument) -> TextDocument: :return: The reformatted content """ - # Local import so Darker can be run without Black installed + # Local import so Darker can be run without Black installed. + # No need for error handling, already done in `BlackFormatter.read_config`. from black import format_str # pylint: disable=import-outside-toplevel contents_for_black = content.string_with_newline("\n") @@ -177,7 +194,8 @@ def _make_black_options(self) -> Mode: # pass them to Black's ``format_str()``. File exclusion options aren't needed # since at this point we already have a single file's content to work on. - # Local import so Darker can be run without Black installed + # Local import so Darker can be run without Black installed. + # No need for error handling, already done in `BlackFormatter.read_config`. from black import FileMode as Mode # pylint: disable=import-outside-toplevel from black import TargetVersion # pylint: disable=import-outside-toplevel From 9c4a5424e5d0e0407609f47a2cfd58dc58a87a1d Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:30:16 +0200 Subject: [PATCH 32/77] test: correct operation with Black missing --- src/darker/tests/helpers.py | 7 ++++ src/darker/tests/test_formatters_black.py | 44 +++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/darker/tests/helpers.py b/src/darker/tests/helpers.py index 22f67e337..fffcf8cb1 100644 --- a/src/darker/tests/helpers.py +++ b/src/darker/tests/helpers.py @@ -24,6 +24,13 @@ def _package_present( yield fake_module +@contextmanager +def black_present(*, present: bool) -> Generator[None, None, None]: + """Context manager to remove or add the ``black`` package temporarily for a test.""" + with _package_present("black", present): + yield + + @contextmanager def isort_present(present: bool) -> Generator[None, None, None]: """Context manager to remove or add the `isort` package temporarily for a test""" diff --git a/src/darker/tests/test_formatters_black.py b/src/darker/tests/test_formatters_black.py index 18c94e6f2..4f7534c75 100644 --- a/src/darker/tests/test_formatters_black.py +++ b/src/darker/tests/test_formatters_black.py @@ -6,6 +6,7 @@ import sys from argparse import Namespace from dataclasses import dataclass, field +from importlib import reload from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import ANY, patch @@ -14,8 +15,12 @@ import regex from black import Mode, TargetVersion +import darker.formatters.black_formatter +from darker.exceptions import DependencyError from darker.files import DEFAULT_EXCLUDE_RE, filter_python_files +from darker.formatters import create_formatter from darker.formatters.black_formatter import BlackFormatter +from darker.tests.helpers import black_present from darkgraylib.config import ConfigurationError from darkgraylib.testtools.helpers import raises_or_matches from darkgraylib.utils import TextDocument @@ -48,6 +53,45 @@ def __eq__(self, other): ) +@pytest.mark.parametrize("present", [True, False]) +def test_formatters_black_importable_with_and_without_isort(present): + """Ensure `darker.formatters.black_formatter` imports with/without ``black``.""" + try: + with black_present(present=present): + # end of test setup, now import the module + + # Import when `black` has been removed temporarily + reload(darker.formatters.black_formatter) + + finally: + # Re-import after restoring `black` so other tests won't be affected + reload(darker.formatters.black_formatter) + + +def test_formatter_without_black(caplog): + """`BlackFormatter` logs warnings with instructions if `black` is not installed.""" + args = Namespace() + args.config = None + formatter = create_formatter("black") + with black_present(present=False), pytest.raises( + DependencyError, match="^Can't find the Black package$" + ): + # end of test setup, now exercise the Black formatter + + formatter.read_config((), args) + + assert [ + record.msg for record in caplog.records if record.levelname == "WARNING" + ] == [ + # warning 1: + "To re-format code using Black, install it using e.g." + " `pip install 'darker[black]'` or `pip install black`", + # warning 2: + "To use a different formatter or no formatter, select it on the command line" + " (e.g. `--formatter=none`) or configuration (e.g. `formatter=none`)", + ] + + @pytest.mark.kwparametrize( dict( config_path=None, config_lines=["line-length = 79"], expect={"line_length": 79} From ef47be5e4e66ffe4bff5741496971bb97f4b1aa7 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:38:42 +0200 Subject: [PATCH 33/77] test: --formatter=none config parsing --- src/darker/tests/test_command_line.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 964690677..90abff117 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -220,6 +220,18 @@ def get_darker_help_output(capsys): expect_config=("formatter", "black"), expect_modified=("formatter", ...), ), + dict( + argv=["--formatter", "none", "."], + expect_value=("formatter", "none"), + expect_config=("formatter", "none"), + expect_modified=("formatter", "none"), + ), + dict( + argv=["--formatter=none", "."], + expect_value=("formatter", "none"), + expect_config=("formatter", "none"), + expect_modified=("formatter", "none"), + ), dict( argv=["--formatter", "rustfmt", "."], expect_value=SystemExit, From 76f3c3bb4c86627f183078552ff649bbfe800db2 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:50:45 +0200 Subject: [PATCH 34/77] test: --formatter=none with Black installed or not --- src/darker/tests/test_main.py | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index ec5bf2880..484c67d9c 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -23,7 +23,7 @@ from darker.help import LINTING_GUIDE from darker.terminal import output from darker.tests.examples import A_PY, A_PY_BLACK, A_PY_BLACK_FLYNT, A_PY_BLACK_ISORT -from darker.tests.helpers import unix_and_windows_newline_repos +from darker.tests.helpers import black_present, unix_and_windows_newline_repos from darker.tests.test_fstring import FLYNTED_SOURCE, MODIFIED_SOURCE, ORIGINAL_SOURCE from darkgraylib.git import RevisionRange from darkgraylib.testtools.git_repo_plugin import GitRepoFixture @@ -662,3 +662,38 @@ def test_long_command_length(git_repo): git_repo.add(files, commit="Add all the files") result = darker.__main__.main(["--diff", "--check", "src"]) assert result == 0 + + +@pytest.fixture(scope="module") +def formatter_none_repo(git_repo_m): + """Create a Git repository with a single file and a formatter that does nothing.""" + files = git_repo_m.add({"file1.py": "# old content\n"}, commit="Initial") + files["file1.py"].write_text( + dedent( + """ + import sys, os + print ( 'untouched unformatted code' ) + """ + ) + ) + return files + + +@pytest.mark.parametrize("has_black", [False, True]) +def test_formatter_none(has_black, formatter_none_repo): + """The dummy formatter works regardless of whether Black is installed or not.""" + with black_present(present=has_black): + argv = ["--formatter=none", "--isort", "file1.py"] + + result = darker.__main__.main(argv) + + assert result == 0 + expect = dedent( + """ + import os + import sys + + print ( 'untouched unformatted code' ) + """ + ) + assert formatter_none_repo["file1.py"].read_text() == expect From 8ee1ae6709780ca3c0fd399ba1a3f4350d3625f5 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:54:00 +0200 Subject: [PATCH 35/77] docs: Black not installed by default --- README.rst | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 891e6633b..7931d98c1 100644 --- a/README.rst +++ b/README.rst @@ -133,11 +133,11 @@ How? To install or upgrade, use:: - pip install --upgrade darker~=2.1.1 + pip install --upgrade darker[black]~=2.1.1 Or, if you're using Conda_ for package management:: - conda install -c conda-forge darker~=2.1.1 isort + conda install -c conda-forge darker~=2.1.1 black isort conda update -c conda-forge darker .. @@ -146,6 +146,8 @@ Or, if you're using Conda_ for package management:: specifier for Darker. See `Guarding against Black compatibility breakage`_ for more information. +*New in version 3.0.0:* Black is no longer installed by default. + The ``darker `` or ``darker `` command reads the original file(s), formats them using Black_, @@ -478,7 +480,7 @@ PyCharm/IntelliJ IDEA 1. Install ``darker``:: - $ pip install darker + $ pip install 'darker[black]' 2. Locate your ``darker`` installation folder. @@ -540,7 +542,7 @@ Visual Studio Code 1. Install ``darker``:: - $ pip install darker + $ pip install 'darker[black]' 2. Locate your ``darker`` installation folder. @@ -683,8 +685,10 @@ other reformatter tools you use to known compatible versions, for example: Using arguments --------------- -You can provide arguments, such as enabling isort, by specifying ``args``. -Note the inclusion of the isort Python package under ``additional_dependencies``: +You can provide arguments, such as disabling Darker or enabling isort, +by specifying ``args``. +Note the absence of Black and the inclusion of the isort Python package +under ``additional_dependencies``: .. code-block:: yaml @@ -692,7 +696,9 @@ Note the inclusion of the isort Python package under ``additional_dependencies`` rev: v2.1.1 hooks: - id: darker - args: [--isort] + args: + - --formatter=none + - --isort additional_dependencies: - isort~=5.9 @@ -779,6 +785,9 @@ The ``lint:`` option. Removed the ``lint:`` option and moved it into the GitHub action of the Graylint_ package. +*New in version 3.0.0:* +Black is now explicitly installed when running the action. + Syntax highlighting =================== From 93aebe696494bfcd76ec04c61c581f6621ccd401 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:57:12 +0200 Subject: [PATCH 36/77] docs: update the change log [drop-black-dependency] --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e65063939..b2ce13132 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,8 @@ Added other formatters in the future. There's also a dummy ``none`` formatter plugin. - ``--formatter=none`` now skips running Black. This is useful when you only want to run Isort or Flynt_. +- Black_ is no longer installed by default. Use ``pip install 'darker[black]'`` to get + Black support. Removed ------- From ee0de17a37dfba59a4ac1e22539819269cd78975 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:50:45 +0200 Subject: [PATCH 37/77] test: better explanation for --formatter=none unit test repo fixture --- src/darker/tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index 484c67d9c..d8d1850f5 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -666,7 +666,7 @@ def test_long_command_length(git_repo): @pytest.fixture(scope="module") def formatter_none_repo(git_repo_m): - """Create a Git repository with a single file and a formatter that does nothing.""" + """Create a Git repo with a single file to test a formatter that does nothing.""" files = git_repo_m.add({"file1.py": "# old content\n"}, commit="Initial") files["file1.py"].write_text( dedent( From 3a715abf2e52e20f76113e894078f4deaa8785a0 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:52:43 +0200 Subject: [PATCH 38/77] fix: improve Black import error [optional-black-improvement] --- src/darker/formatters/black_formatter.py | 32 ++++++------------- src/darker/formatters/black_wrapper.py | 39 +++++++++++++++++++++++ src/darker/tests/helpers.py | 1 + src/darker/tests/test_command_line.py | 6 ++-- src/darker/tests/test_formatters_black.py | 8 ++--- 5 files changed, 58 insertions(+), 28 deletions(-) create mode 100644 src/darker/formatters/black_wrapper.py diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index 604b717f1..94fc444b0 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -39,7 +39,6 @@ import logging from typing import TYPE_CHECKING, TypedDict -from darker.exceptions import DependencyError from darker.files import find_pyproject_toml from darker.formatters.base_formatter import BaseFormatter from darkgraylib.config import ConfigurationError @@ -93,24 +92,11 @@ def read_config(self, src: tuple[str, ...], args: Namespace) -> None: def _read_config_file(self, config_path: str) -> None: # noqa: C901 # Local import so Darker can be run without Black installed. # Do error handling here. This is the first Black importing method being hit. - try: - from black import ( # pylint: disable=import-outside-toplevel - parse_pyproject_toml, - re_compile_maybe_verbose, - ) - except ImportError as exc: - logger.warning( - "To re-format code using Black, install it using e.g." - " `pip install 'darker[black]'` or" - " `pip install black`" - ) - 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 Black package" - raise DependencyError(message) from exc + # pylint: disable=import-outside-toplevel + from darker.formatters.black_wrapper import ( + parse_pyproject_toml, + re_compile_maybe_verbose, + ) raw_config = parse_pyproject_toml(config_path) if "line_length" in raw_config: @@ -171,7 +157,8 @@ def run(self, content: TextDocument) -> TextDocument: """ # Local import so Darker can be run without Black installed. # No need for error handling, already done in `BlackFormatter.read_config`. - from black import format_str # pylint: disable=import-outside-toplevel + # pylint: disable=import-outside-toplevel + from darker.formatters.black_wrapper import format_str contents_for_black = content.string_with_newline("\n") if contents_for_black.strip(): @@ -196,8 +183,9 @@ def _make_black_options(self) -> Mode: # Local import so Darker can be run without Black installed. # No need for error handling, already done in `BlackFormatter.read_config`. - from black import FileMode as Mode # pylint: disable=import-outside-toplevel - from black import TargetVersion # pylint: disable=import-outside-toplevel + # pylint: disable=import-outside-toplevel + from darker.formatters.black_wrapper import FileMode as Mode + from darker.formatters.black_wrapper import TargetVersion mode = BlackModeAttributes() if "line_length" in self.config: diff --git a/src/darker/formatters/black_wrapper.py b/src/darker/formatters/black_wrapper.py new file mode 100644 index 000000000..1a724c31d --- /dev/null +++ b/src/darker/formatters/black_wrapper.py @@ -0,0 +1,39 @@ +"""Attempt to import Black internals needed by the Black formatter plugin.""" + +import logging + +from darker.exceptions import DependencyError + +logger = logging.getLogger(__name__) + +try: + import black # noqa: F401 # pylint: disable=unused-import +except ImportError as exc: + logger.warning( + "To re-format code using Black, install it using e.g." + " `pip install 'darker[black]'` or" + " `pip install black`" + ) + 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 Black package" + raise DependencyError(MESSAGE) from exc + +from black import ( # noqa: E402 # pylint: disable=unused-import,wrong-import-position + FileMode, + TargetVersion, + format_str, + parse_pyproject_toml, + re_compile_maybe_verbose, +) + +__all__ = [ + "FileMode", + "TargetVersion", + "format_str", + "parse_pyproject_toml", + "re_compile_maybe_verbose", +] diff --git a/src/darker/tests/helpers.py b/src/darker/tests/helpers.py index fffcf8cb1..bd587ccbc 100644 --- a/src/darker/tests/helpers.py +++ b/src/darker/tests/helpers.py @@ -28,6 +28,7 @@ def _package_present( def black_present(*, present: bool) -> Generator[None, None, None]: """Context manager to remove or add the ``black`` package temporarily for a test.""" with _package_present("black", present): + del sys.modules["darker.formatters.black_wrapper"] yield diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 90abff117..9609516ac 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -589,7 +589,9 @@ def test_black_options(black_options_files, options, expect): # shared by all test cases. The "main.py" file modified by the test run needs to be # reset to its original content before the next test case. black_options_files["main.py"].write_bytes(b'print ("Hello World!")\n') - with patch("black.FileMode", wraps=FileMode) as file_mode_class: + with patch( + "darker.formatters.black_wrapper.FileMode", wraps=FileMode + ) as file_mode_class: # end of test setup, now call the function under test main(options + [str(path) for path in black_options_files.values()]) @@ -718,7 +720,7 @@ def test_black_config_file_and_options( mode_class_mock = Mock(wraps=FileMode) # Speed up tests by mocking `format_str` to skip running Black format_str = Mock(return_value="a = [1, 2,]") - with patch("black.FileMode", mode_class_mock), patch( + with patch("darker.formatters.black_wrapper.FileMode", mode_class_mock), patch( "black.format_str", format_str ): # end of test setup, now call the function under test diff --git a/src/darker/tests/test_formatters_black.py b/src/darker/tests/test_formatters_black.py index 4f7534c75..94332a03f 100644 --- a/src/darker/tests/test_formatters_black.py +++ b/src/darker/tests/test_formatters_black.py @@ -278,7 +278,7 @@ def test_run(encoding, newline): def test_run_always_uses_unix_newlines(newline): """Content is always passed to Black with Unix newlines""" src = TextDocument.from_str(f"print ( 'touché' ){newline}") - with patch("black.format_str") as format_str: + with patch("darker.formatters.black_wrapper.format_str") as format_str: format_str.return_value = 'print("touché")\n' _ = BlackFormatter().run(src) @@ -390,9 +390,9 @@ def test_run_configuration( ): """`BlackFormatter.run` passes correct configuration to Black.""" src = TextDocument.from_str("import os\n") - with patch("black.format_str") as format_str, raises_or_matches( - expect, [] - ) as check: + with patch( + "darker.formatters.black_wrapper.format_str" + ) as format_str, raises_or_matches(expect, []) as check: format_str.return_value = "import os\n" formatter = BlackFormatter() formatter.config = black_config From 1ed5d2e7a0bbb3548b0df1030cb485681aaba293 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:57:55 +0200 Subject: [PATCH 39/77] ci: disable removed ANN101 Ruff check --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 578824a8a..695aeca7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ target-version = "py38" select = ["ALL"] ignore = [ "A002", # builtin-argument-shadowing - "ANN101", # Missing type annotation for `self` in method "COM812", # Trailing comma missing "D203", # One blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line From 17e682cb244b37c04b4a3bbb469a9f00e8678ad9 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:22:22 +0300 Subject: [PATCH 40/77] feat: add ruff code re-formatter plugin --- README.rst | 2 +- setup.cfg | 1 + src/darker/formatters/base_formatter.py | 32 +++- src/darker/formatters/black_formatter.py | 39 ++--- src/darker/formatters/formatter_config.py | 56 +++++- src/darker/formatters/ruff_formatter.py | 198 ++++++++++++++++++++++ src/darker/tests/test_formatters_black.py | 4 +- 7 files changed, 292 insertions(+), 40 deletions(-) create mode 100644 src/darker/formatters/ruff_formatter.py diff --git a/README.rst b/README.rst index 7931d98c1..5698f72ef 100644 --- a/README.rst +++ b/README.rst @@ -373,7 +373,7 @@ 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] Formatter to use for reformatting code. [default: black] + [black\|none\|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, diff --git a/setup.cfg b/setup.cfg index 8b9989682..d8ee464d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ darker = [options.entry_points] darker.formatter = black = darker.formatters.black_formatter:BlackFormatter + ruff = darker.formatters.ruff_formatter:RuffFormatter none = darker.formatters.none_formatter:NoneFormatter console_scripts = darker = darker.__main__:main_with_error_handling diff --git a/src/darker/formatters/base_formatter.py b/src/darker/formatters/base_formatter.py index 2bbe32b98..b8c02ef1e 100644 --- a/src/darker/formatters/base_formatter.py +++ b/src/darker/formatters/base_formatter.py @@ -2,39 +2,52 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Pattern +from typing import TYPE_CHECKING, Generic, Pattern, TypeVar + +from darker.files import find_pyproject_toml +from darker.formatters.formatter_config import FormatterConfig if TYPE_CHECKING: from argparse import Namespace - from darker.formatters.formatter_config import FormatterConfig from darkgraylib.utils import TextDocument -class BaseFormatter: +T = TypeVar("T", bound=FormatterConfig) + + +class HasConfig(Generic[T]): # pylint: disable=too-few-public-methods """Base class for code re-formatters.""" def __init__(self) -> None: """Initialize the code re-formatter plugin base class.""" - self.config: FormatterConfig = {} + self.config = {} # type: ignore[var-annotated] + + +class BaseFormatter(HasConfig[FormatterConfig]): + """Base class for code re-formatters.""" name: str def read_config(self, src: tuple[str, ...], args: Namespace) -> None: - """Read the formatter configuration from a configuration file - - If not implemented by the subclass, this method does nothing, so the formatter - has no configuration options. + """Read code re-formatter configuration from a configuration file. :param src: The source code files and directories to be processed by Darker :param args: Command line arguments """ + config_path = args.config or find_pyproject_toml(src) + if config_path: + self._read_config_file(config_path) + self._read_cli_args(args) def run(self, content: TextDocument) -> TextDocument: """Reformat the content.""" raise NotImplementedError + def _read_cli_args(self, args: Namespace) -> None: + pass + def get_config_path(self) -> str | None: """Get the path of the configuration file.""" return None @@ -60,3 +73,6 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, BaseFormatter): return NotImplemented return type(self) is type(other) and self.config == other.config + + def _read_config_file(self, config_path: str) -> None: + pass diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index 94fc444b0..d22a52b2e 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -13,7 +13,7 @@ ... ] ... ) -First, :func:`run_black` uses Black to reformat the contents of a given file. +First, `BlackFormatter.run` uses Black to reformat the contents of a given file. Reformatted lines are returned e.g.:: >>> from darker.formatters.black_formatter import BlackFormatter @@ -40,7 +40,12 @@ from typing import TYPE_CHECKING, TypedDict from darker.files import find_pyproject_toml -from darker.formatters.base_formatter import BaseFormatter +from darker.formatters.base_formatter import BaseFormatter, HasConfig +from darker.formatters.formatter_config import ( + BlackCompatibleConfig, + read_black_compatible_cli_args, + validate_target_versions, +) from darkgraylib.config import ConfigurationError from darkgraylib.utils import TextDocument @@ -68,12 +73,10 @@ class BlackModeAttributes(TypedDict, total=False): preview: bool -class BlackFormatter(BaseFormatter): +class BlackFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]): """Black code formatter plugin interface.""" - def __init__(self) -> None: # pylint: disable=super-init-not-called - """Initialize the Black code re-formatter plugin.""" - self.config: BlackConfig = {} + config: BlackCompatibleConfig # type: ignore[assignment] name = "black" @@ -135,18 +138,7 @@ def _read_config_file(self, config_path: str) -> None: # noqa: C901 ) def _read_cli_args(self, args: Namespace) -> None: - if args.config: - self.config["config"] = args.config - if getattr(args, "line_length", None): - self.config["line_length"] = args.line_length - if getattr(args, "target_version", None): - self.config["target_version"] = {args.target_version} - if getattr(args, "skip_string_normalization", None) is not None: - self.config["skip_string_normalization"] = args.skip_string_normalization - if getattr(args, "skip_magic_trailing_comma", None) is not None: - self.config["skip_magic_trailing_comma"] = args.skip_magic_trailing_comma - if getattr(args, "preview", None): - self.config["preview"] = args.preview + return read_black_compatible_cli_args(args, self.config) def run(self, content: TextDocument) -> TextDocument: """Run the Black code re-formatter for the Python source code given as a string. @@ -191,15 +183,10 @@ def _make_black_options(self) -> Mode: if "line_length" in self.config: mode["line_length"] = self.config["line_length"] if "target_version" in self.config: - if isinstance(self.config["target_version"], set): - target_versions_in = self.config["target_version"] - else: - target_versions_in = {self.config["target_version"]} all_target_versions = {tgt_v.name.lower(): tgt_v for tgt_v in TargetVersion} - bad_target_versions = target_versions_in - set(all_target_versions) - if bad_target_versions: - message = f"Invalid target version(s) {bad_target_versions}" - raise ConfigurationError(message) + target_versions_in = validate_target_versions( + self.config["target_version"], all_target_versions + ) mode["target_versions"] = { all_target_versions[n] for n in target_versions_in } diff --git a/src/darker/formatters/formatter_config.py b/src/darker/formatters/formatter_config.py index 22ce27c09..a9e4da86a 100644 --- a/src/darker/formatters/formatter_config.py +++ b/src/darker/formatters/formatter_config.py @@ -2,15 +2,47 @@ from __future__ import annotations -from typing import Pattern, TypedDict +import re +from decimal import Decimal +from typing import TYPE_CHECKING, Iterable, Pattern, TypedDict + +from darkgraylib.config import ConfigurationError + +if TYPE_CHECKING: + from argparse import Namespace class FormatterConfig(TypedDict): """Base class for code re-formatter configuration.""" -class BlackConfig(FormatterConfig, total=False): - """Type definition for Black configuration dictionaries.""" +def validate_target_versions( + value: str | set[str], valid_target_versions: Iterable[str] +) -> set[str]: + """Validate the target-version configuration option value.""" + target_versions_in = {value} if isinstance(value, str) else value + if not isinstance(value, (str, set)): + message = f"Invalid target version(s) {value!r}" # type: ignore[unreachable] + raise ConfigurationError(message) + bad_target_versions = target_versions_in - set(valid_target_versions) + if bad_target_versions: + message = f"Invalid target version(s) {bad_target_versions}" + raise ConfigurationError(message) + return target_versions_in + + +get_version_num = re.compile(r"\d+").search + + +def get_minimum_target_version(target_versions: set[str]) -> str: + """Get the minimum target version from a set of target versions.""" + nums_and_tgts = ((get_version_num(tgt), tgt) for tgt in target_versions) + matches = ((Decimal(match.group()), tgt) for match, tgt in nums_and_tgts if match) + return min(matches)[1] + + +class BlackCompatibleConfig(FormatterConfig, total=False): + """Type definition for configuration dictionaries of Black compatible formatters.""" config: str exclude: Pattern[str] @@ -21,3 +53,21 @@ class BlackConfig(FormatterConfig, total=False): skip_string_normalization: bool skip_magic_trailing_comma: bool preview: bool + + +def read_black_compatible_cli_args( + args: Namespace, config: BlackCompatibleConfig +) -> None: + """Read Black-compatible configuration from command line arguments.""" + if args.config: + config["config"] = args.config + if getattr(args, "line_length", None): + config["line_length"] = args.line_length + if getattr(args, "target_version", None): + config["target_version"] = {args.target_version} + if getattr(args, "skip_string_normalization", None) is not None: + config["skip_string_normalization"] = args.skip_string_normalization + if getattr(args, "skip_magic_trailing_comma", None) is not None: + config["skip_magic_trailing_comma"] = args.skip_magic_trailing_comma + if getattr(args, "preview", None): + config["preview"] = args.preview diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py new file mode 100644 index 000000000..5d4b31065 --- /dev/null +++ b/src/darker/formatters/ruff_formatter.py @@ -0,0 +1,198 @@ +"""Re-format Python source code using Ruff. + +In examples below, a simple two-line snippet is used. +The first line will be reformatted by Ruff, and the second left intact:: + + >>> from pathlib import Path + >>> from unittest.mock import Mock + >>> src = Path("dummy/file/path.py") + >>> src_content = TextDocument.from_lines( + ... [ + ... "for i in range(5): print(i)", + ... 'print("done")', + ... ] + ... ) + +First, `RuffFormatter.run` uses Ruff to reformat the contents of a given file. +Reformatted lines are returned e.g.:: + + >>> from darker.formatters.ruff_formatter import RuffFormatter + >>> dst = RuffFormatter().run(src_content) + >>> dst.lines + ('for i in range(5):', ' print(i)', 'print("done")') + +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 logging +import sys +from pathlib import Path +from subprocess import PIPE, run # nosec +from typing import TYPE_CHECKING, Collection + +from darker.formatters.base_formatter import BaseFormatter, HasConfig +from darker.formatters.formatter_config import ( + BlackCompatibleConfig, + get_minimum_target_version, + read_black_compatible_cli_args, + validate_target_versions, +) +from darkgraylib.config import ConfigurationError +from darkgraylib.utils import TextDocument + +if sys.version_info >= (3, 11): + try: + import tomllib + except ImportError: + # Help users on older Python 3.11 alphas + import tomli as tomllib # type: ignore[no-redef,import-not-found] +else: + import tomli as tomllib + +if TYPE_CHECKING: + from argparse import Namespace + from typing import Pattern + +logger = logging.getLogger(__name__) + + +class RuffFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]): + """Ruff code formatter plugin interface.""" + + config: BlackCompatibleConfig # type: ignore[assignment] + + name = "ruff format" + + def run(self, content: TextDocument) -> TextDocument: + """Run the Ruff code re-formatter for the Python source code given as a string. + + :param content: The source code + :return: The reformatted content + + """ + # Collect relevant Ruff configuration options from ``self.config`` in order to + # pass them to Ruff's ``format_str()``. File exclusion options aren't needed + # since at this point we already have a single file's content to work on. + # Ignore ISC001 (single-line-implicit-string-concatenation) since it conflicts + # with Black's string formatting + args = ['--config=lint.ignore=["ISC001"]'] + if "line_length" in self.config: + args.append(f"--line-length={self.config['line_length']}") + 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 = get_minimum_target_version(target_versions_in) + args.append(f"--target-version={target_version}") + if self.config.get("skip_magic_trailing_comma", False): + args.append('--config="format.skip-magic-trailing-comma=true"') + args.append('--config="lint.isort.split-on-trailing-comma=false"') + if self.config.get("skip_string_normalization", False): + args.append('''--config=format.quote-style="preserve"''') + if self.config.get("preview", False): + args.append("--preview") + + # The custom handling of empty and all-whitespace files below will be + # unnecessary if https://github.com/psf/ruff/pull/2484 lands in Ruff. + contents_for_ruff = content.string_with_newline("\n") + dst_contents = _ruff_format_stdin(contents_for_ruff, args) + return TextDocument.from_str( + dst_contents, + encoding=content.encoding, + override_newline=content.newline, + ) + + def _read_config_file(self, config_path: str) -> None: + """Read Ruff configuration from a configuration file. + + :param config_path: Path to the configuration file + :raises ConfigurationError: If the configuration file cannot be read or parsed + + """ + try: + with Path(config_path).open(mode="rb") as config_file: + raw_config = tomllib.load(config_file).get("tool", {}).get("ruff", {}) + if "line-length" in raw_config: + self.config["line_length"] = int(raw_config["line-length"]) + except (OSError, ValueError, tomllib.TOMLDecodeError) as exc: + message = f"Failed to read Ruff config: {exc}" + raise ConfigurationError(message) from exc + + def _read_cli_args(self, args: Namespace) -> None: + return read_black_compatible_cli_args(args, self.config) + + def get_config_path(self) -> str | None: + """Get the path of the configuration file.""" + return self.config.get("config") + + # pylint: disable=duplicate-code + def get_line_length(self) -> int | None: + """Get the ``line-length`` Ruff configuration option value.""" + return self.config.get("line_length") + + # pylint: disable=duplicate-code + def get_exclude(self, default: Pattern[str]) -> Pattern[str]: + """Get the ``exclude`` Ruff configuration option value.""" + return self.config.get("exclude", default) + + # pylint: disable=duplicate-code + def get_extend_exclude(self) -> Pattern[str] | None: + """Get the ``extend_exclude`` Ruff configuration option value.""" + return self.config.get("extend_exclude") + + # pylint: disable=duplicate-code + def get_force_exclude(self) -> Pattern[str] | None: + """Get the ``force_exclude`` Ruff configuration option value.""" + return self.config.get("force_exclude") + + +def _get_supported_target_versions() -> set[str]: + """Get the supported target versions for Ruff. + + Calls ``ruff config target-version`` as a subprocess, looks for the line looking + like ``Type: "py38" | "py39" | "py310"``, and returns the target versions as a set + of strings. + + """ + cmdline = "ruff config target-version" + output = run( # noqa: S603 # nosec + cmdline.split(), stdout=PIPE, check=True, text=True + ).stdout + type_lines = [line for line in output.splitlines() if line.startswith('Type: "py')] + if not type_lines: + message = f"`{cmdline}` returned no target versions on a 'Type: \"py...' line" + raise ConfigurationError(message) + quoted_targets = type_lines[0][6:].split(" | ") + if any(tgt_ver[0] != '"' or tgt_ver[-1] != '"' for tgt_ver in quoted_targets): + message = f"`{cmdline}` returned invalid target versions {type_lines[0]!r}" + raise ConfigurationError(message) + return {tgt_ver[1:-1] for tgt_ver in quoted_targets} + + +def _ruff_format_stdin(contents: str, args: Collection[str]) -> str: + """Run the contents through ``ruff format``. + + :param contents: The source code to be reformatted + :param args: Additional command line arguments to pass to Ruff + :return: The reformatted source code + + """ + cmdline = ["ruff", "format", *args, "-"] + logger.debug("Running %s", " ".join(cmdline)) + result = run( # noqa: S603 # nosec + cmdline, input=contents, stdout=PIPE, check=True, text=True, encoding="utf-8" + ) + return result.stdout diff --git a/src/darker/tests/test_formatters_black.py b/src/darker/tests/test_formatters_black.py index 94332a03f..48403d70c 100644 --- a/src/darker/tests/test_formatters_black.py +++ b/src/darker/tests/test_formatters_black.py @@ -36,7 +36,7 @@ import tomli as tomllib if TYPE_CHECKING: - from darker.formatters.formatter_config import BlackConfig + from darker.formatters.formatter_config import BlackCompatibleConfig @dataclass @@ -230,7 +230,7 @@ def test_filter_python_files( # pylint: disable=too-many-arguments paths = {tmp_path / name for name in names} for path in paths: path.touch() - black_config: BlackConfig = { + black_config: BlackCompatibleConfig = { "exclude": regex.compile(exclude) if exclude else DEFAULT_EXCLUDE_RE, "extend_exclude": regex.compile(extend_exclude) if extend_exclude else None, "force_exclude": regex.compile(force_exclude) if force_exclude else None, From 67b2238f977ab3308f0c5bd2dd1e51d98b9b35fd Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:54:17 +0200 Subject: [PATCH 41/77] docs: --formatter explanations in README --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 5698f72ef..9e6a81e50 100644 --- a/README.rst +++ b/README.rst @@ -167,6 +167,9 @@ You can enable additional features with command line options: - ``-f`` / ``--flynt``: Also convert string formatting to use f-strings using the ``flynt`` package +If you only want to run those tools without reformatting with Black, +use the ``--formatter=none`` option. + *New in version 1.1.0:* The ``-L`` / ``--lint`` option. *New in version 1.2.2:* Package available in conda-forge_. @@ -176,6 +179,8 @@ You can enable additional features with command line options: *New in version 3.0.0:* Removed the ``-L`` / ``--lint`` functionality and moved it into the Graylint_ package. +*New in version 3.0.0:* The ``--formatter`` option. + .. _Conda: https://conda.io/ .. _conda-forge: https://conda-forge.org/ From 3f712d719e695c16a7e2ff6fc44e453b53ea8a36 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:37:39 +0300 Subject: [PATCH 42/77] docs: comment about tomllib/tomli and the Python version Thanks @clintonsteiner! --- src/darker/formatters/ruff_formatter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py index 5d4b31065..29821ef3b 100644 --- a/src/darker/formatters/ruff_formatter.py +++ b/src/darker/formatters/ruff_formatter.py @@ -53,12 +53,14 @@ from darkgraylib.utils import TextDocument if sys.version_info >= (3, 11): + # On Python 3.11+, we can use the `tomllib` module from the standard library. try: import tomllib except ImportError: # Help users on older Python 3.11 alphas import tomli as tomllib # type: ignore[no-redef,import-not-found] else: + # On older Pythons, we must use the backport. import tomli as tomllib if TYPE_CHECKING: From 1567e7ac94fbf733ad4fb928a4e4124946779fe5 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 9 Oct 2024 18:08:39 +0300 Subject: [PATCH 43/77] refactor: replace magic number with length of prefix string Thanks @clintonsteiner! --- src/darker/formatters/ruff_formatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py index 29821ef3b..75b107089 100644 --- a/src/darker/formatters/ruff_formatter.py +++ b/src/darker/formatters/ruff_formatter.py @@ -177,7 +177,7 @@ def _get_supported_target_versions() -> set[str]: if not type_lines: message = f"`{cmdline}` returned no target versions on a 'Type: \"py...' line" raise ConfigurationError(message) - quoted_targets = type_lines[0][6:].split(" | ") + quoted_targets = type_lines[0][len('Type: '):].split(" | ") if any(tgt_ver[0] != '"' or tgt_ver[-1] != '"' for tgt_ver in quoted_targets): message = f"`{cmdline}` returned invalid target versions {type_lines[0]!r}" raise ConfigurationError(message) From 3a6828aed5bbcdec465a7ed8d887d5d7910957f9 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:54:50 +0300 Subject: [PATCH 44/77] feat: store numeric Black target versions internally This will make it easier to add support for pyupgrade. --- src/darker/formatters/black_formatter.py | 16 ++++++++++---- src/darker/formatters/formatter_config.py | 27 ++++++++--------------- src/darker/tests/test_command_line.py | 2 +- src/darker/tests/test_formatters_black.py | 23 ++++++++++--------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index d22a52b2e..5cfffafac 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -117,10 +117,15 @@ def _read_config_file(self, config_path: str) -> None: # noqa: C901 if "target_version" in raw_config: target_version = raw_config["target_version"] if isinstance(target_version, str): - self.config["target_version"] = target_version + self.config["target_version"] = ( + int(target_version[2]), + int(target_version[3:]), + ) elif isinstance(target_version, list): - # Convert TOML list to a Python set - self.config["target_version"] = set(target_version) + # Convert TOML list to a Python set of int-tuples + self.config["target_version"] = { + (int(v[2]), int(v[3:])) for v in target_version + } else: message = ( f"Invalid target-version = {target_version!r} in {config_path}" @@ -183,7 +188,10 @@ def _make_black_options(self) -> Mode: if "line_length" in self.config: mode["line_length"] = self.config["line_length"] if "target_version" in self.config: - all_target_versions = {tgt_v.name.lower(): tgt_v for tgt_v in TargetVersion} + all_target_versions = { + (int(tgt_v.name[2]), int(tgt_v.name[3:])): tgt_v + for tgt_v in TargetVersion + } target_versions_in = validate_target_versions( self.config["target_version"], all_target_versions ) diff --git a/src/darker/formatters/formatter_config.py b/src/darker/formatters/formatter_config.py index a9e4da86a..5bea1f300 100644 --- a/src/darker/formatters/formatter_config.py +++ b/src/darker/formatters/formatter_config.py @@ -2,8 +2,6 @@ from __future__ import annotations -import re -from decimal import Decimal from typing import TYPE_CHECKING, Iterable, Pattern, TypedDict from darkgraylib.config import ConfigurationError @@ -17,11 +15,12 @@ class FormatterConfig(TypedDict): def validate_target_versions( - value: str | set[str], valid_target_versions: Iterable[str] -) -> set[str]: + value: tuple[int, int] | set[tuple[int, int]], + valid_target_versions: Iterable[tuple[int, int]], +) -> set[tuple[int, int]]: """Validate the target-version configuration option value.""" - target_versions_in = {value} if isinstance(value, str) else value - if not isinstance(value, (str, set)): + target_versions_in = value if isinstance(value, set) else {value} + if not isinstance(value, (tuple, set)): message = f"Invalid target version(s) {value!r}" # type: ignore[unreachable] raise ConfigurationError(message) bad_target_versions = target_versions_in - set(valid_target_versions) @@ -31,16 +30,6 @@ def validate_target_versions( return target_versions_in -get_version_num = re.compile(r"\d+").search - - -def get_minimum_target_version(target_versions: set[str]) -> str: - """Get the minimum target version from a set of target versions.""" - nums_and_tgts = ((get_version_num(tgt), tgt) for tgt in target_versions) - matches = ((Decimal(match.group()), tgt) for match, tgt in nums_and_tgts if match) - return min(matches)[1] - - class BlackCompatibleConfig(FormatterConfig, total=False): """Type definition for configuration dictionaries of Black compatible formatters.""" @@ -48,7 +37,7 @@ class BlackCompatibleConfig(FormatterConfig, total=False): exclude: Pattern[str] extend_exclude: Pattern[str] | None force_exclude: Pattern[str] | None - target_version: str | set[str] + target_version: tuple[int, int] | set[tuple[int, int]] line_length: int skip_string_normalization: bool skip_magic_trailing_comma: bool @@ -64,7 +53,9 @@ def read_black_compatible_cli_args( if getattr(args, "line_length", None): config["line_length"] = args.line_length if getattr(args, "target_version", None): - config["target_version"] = {args.target_version} + config["target_version"] = { + (int(args.target_version[2]), int(args.target_version[3:])) + } if getattr(args, "skip_string_normalization", None) is not None: config["skip_string_normalization"] = args.skip_string_normalization if getattr(args, "skip_magic_trailing_comma", None) is not None: diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 9609516ac..24cc30072 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -821,7 +821,7 @@ def options_repo(request, tmp_path_factory): {Path("a.py")}, Exclusions(isort={"**/*"}, flynt={"**/*"}), RevisionRange("HEAD", ":WORKTREE:"), - {"target_version": {"py39"}}, + {"target_version": {(3, 9)}}, ), ), dict( diff --git a/src/darker/tests/test_formatters_black.py b/src/darker/tests/test_formatters_black.py index 48403d70c..47ae70fb1 100644 --- a/src/darker/tests/test_formatters_black.py +++ b/src/darker/tests/test_formatters_black.py @@ -119,21 +119,22 @@ def test_formatter_without_black(caplog): ), dict(config_lines=["target-version ="], expect=tomllib.TOMLDecodeError()), dict(config_lines=["target-version = false"], expect=ConfigurationError()), - dict(config_lines=["target-version = 'py37'"], expect={"target_version": "py37"}), + dict(config_lines=["target-version = 'py37'"], expect={"target_version": (3, 7)}), dict( - config_lines=["target-version = ['py37']"], expect={"target_version": {"py37"}} + config_lines=["target-version = ['py37']"], + expect={"target_version": {(3, 7)}}, ), dict( config_lines=["target-version = ['py39']"], - expect={"target_version": {"py39"}}, + expect={"target_version": {(3, 9)}}, ), dict( config_lines=["target-version = ['py37', 'py39']"], - expect={"target_version": {"py37", "py39"}}, + expect={"target_version": {(3, 7), (3, 9)}}, ), dict( config_lines=["target-version = ['py39', 'py37']"], - expect={"target_version": {"py39", "py37"}}, + expect={"target_version": {(3, 9), (3, 7)}}, ), dict(config_lines=[r"include = '\.pyi$'"], expect={}), dict( @@ -326,27 +327,27 @@ def test_run_all_whitespace_input(src_content, expect): @pytest.mark.kwparametrize( dict(black_config={}), dict( - black_config={"target_version": "py37"}, + black_config={"target_version": (3, 7)}, expect_target_versions={TargetVersion.PY37}, ), dict( - black_config={"target_version": "py39"}, + black_config={"target_version": (3, 9)}, expect_target_versions={TargetVersion.PY39}, ), dict( - black_config={"target_version": {"py37"}}, + black_config={"target_version": {(3, 7)}}, expect_target_versions={TargetVersion.PY37}, ), dict( - black_config={"target_version": {"py39"}}, + black_config={"target_version": {(3, 9)}}, expect_target_versions={TargetVersion.PY39}, ), dict( - black_config={"target_version": {"py37", "py39"}}, + black_config={"target_version": {(3, 7), (3, 9)}}, expect_target_versions={TargetVersion.PY37, TargetVersion.PY39}, ), dict( - black_config={"target_version": {"py39", "py37"}}, + black_config={"target_version": {(3, 9), (3, 7)}}, expect_target_versions={TargetVersion.PY37, TargetVersion.PY39}, ), dict( From d67aba8a65658648abe1b34bcb19c66c93ef9ff5 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:55:09 +0300 Subject: [PATCH 45/77] feat: store numeric Ruff target versions internally This will make it easier to add support for pyupgrade. --- src/darker/formatters/ruff_formatter.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py index 75b107089..ab66208b4 100644 --- a/src/darker/formatters/ruff_formatter.py +++ b/src/darker/formatters/ruff_formatter.py @@ -45,7 +45,6 @@ from darker.formatters.base_formatter import BaseFormatter, HasConfig from darker.formatters.formatter_config import ( BlackCompatibleConfig, - get_minimum_target_version, read_black_compatible_cli_args, validate_target_versions, ) @@ -97,8 +96,8 @@ def run(self, content: TextDocument) -> TextDocument: target_versions_in = validate_target_versions( self.config["target_version"], supported_target_versions ) - target_version = get_minimum_target_version(target_versions_in) - args.append(f"--target-version={target_version}") + target_version_str = supported_target_versions[min(target_versions_in)] + args.append(f"--target-version={target_version_str}") if self.config.get("skip_magic_trailing_comma", False): args.append('--config="format.skip-magic-trailing-comma=true"') args.append('--config="lint.isort.split-on-trailing-comma=false"') @@ -161,7 +160,7 @@ def get_force_exclude(self) -> Pattern[str] | None: return self.config.get("force_exclude") -def _get_supported_target_versions() -> set[str]: +def _get_supported_target_versions() -> dict[tuple[int, int], str]: """Get the supported target versions for Ruff. Calls ``ruff config target-version`` as a subprocess, looks for the line looking @@ -181,7 +180,10 @@ def _get_supported_target_versions() -> set[str]: if any(tgt_ver[0] != '"' or tgt_ver[-1] != '"' for tgt_ver in quoted_targets): message = f"`{cmdline}` returned invalid target versions {type_lines[0]!r}" raise ConfigurationError(message) - return {tgt_ver[1:-1] for tgt_ver in quoted_targets} + return { + (int(tgt_ver[3]), int(tgt_ver[4:-1])): tgt_ver[1:-1] + for tgt_ver in quoted_targets + } def _ruff_format_stdin(contents: str, args: Collection[str]) -> str: From 460c6d28b642543b488dda28f089ab682dfbe34d Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:32:11 +0300 Subject: [PATCH 46/77] refactor: more readable _get_supported_target_versions() Thanks @clintonsteiner! --- src/darker/formatters/ruff_formatter.py | 63 +++++++++++++++++-------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py index ab66208b4..5fcc9e7fe 100644 --- a/src/darker/formatters/ruff_formatter.py +++ b/src/darker/formatters/ruff_formatter.py @@ -39,7 +39,7 @@ import logging import sys from pathlib import Path -from subprocess import PIPE, run # nosec +from subprocess import PIPE, SubprocessError, run # nosec from typing import TYPE_CHECKING, Collection from darker.formatters.base_formatter import BaseFormatter, HasConfig @@ -160,30 +160,55 @@ def get_force_exclude(self) -> Pattern[str] | None: return self.config.get("force_exclude") +TYPE_PREFIX = 'Type: "' +VER_PREFIX = "py" + + def _get_supported_target_versions() -> dict[tuple[int, int], str]: """Get the supported target versions for Ruff. Calls ``ruff config target-version`` as a subprocess, looks for the line looking - like ``Type: "py38" | "py39" | "py310"``, and returns the target versions as a set - of strings. + like ``Type: "py38" | "py39" | "py310"``, and returns the target versions as a dict + of int-tuples mapped to version strings. + :returns: A dictionary mapping Python version tuples to their string + representations. For example: ``{(3, 8): "py38", (3, 9): "py39"}`` + :raises ConfigurationError: If target versions cannot be determined from Ruff output """ - cmdline = "ruff config target-version" - output = run( # noqa: S603 # nosec - cmdline.split(), stdout=PIPE, check=True, text=True - ).stdout - type_lines = [line for line in output.splitlines() if line.startswith('Type: "py')] - if not type_lines: - message = f"`{cmdline}` returned no target versions on a 'Type: \"py...' line" - raise ConfigurationError(message) - quoted_targets = type_lines[0][len('Type: '):].split(" | ") - if any(tgt_ver[0] != '"' or tgt_ver[-1] != '"' for tgt_ver in quoted_targets): - message = f"`{cmdline}` returned invalid target versions {type_lines[0]!r}" - raise ConfigurationError(message) - return { - (int(tgt_ver[3]), int(tgt_ver[4:-1])): tgt_ver[1:-1] - for tgt_ver in quoted_targets - } + try: + cmdline = "ruff config target-version" + output = run( # noqa: S603 # nosec + cmdline.split(), stdout=PIPE, check=True, text=True + ).stdout.splitlines() + # Find a line like: Type: "py37" | "py38" | "py39" | "py310" | "py311" | "py312" + type_lines = [ + line + for line in output + if line.startswith(TYPE_PREFIX + VER_PREFIX) and line.endswith('"') + ] + if not type_lines: + message = ( + f"`{cmdline}` returned no target versions on a" + f" '{TYPE_PREFIX}{VER_PREFIX}...' line" + ) + raise ConfigurationError(message) + # Drop 'Type:' prefix and the initial and final double quotes + delimited_versions = type_lines[0][len(TYPE_PREFIX) : -len('"')] + # Now we have: py37" | "py38" | "py39" | "py310" | "py311" | "py312 + # Split it by '" | "' (turn strs to lists since Mypy disallows str unpacking) + py_versions = [ + list(py_version) for py_version in delimited_versions.split('" | "') + ] + # Now we have: [("p", "y", "3", "7"), ("p", "y", "3", "8"), ...] + # Turn it into {(3, 7): "py37", (3, 8): "py38", (3, 9): "py39", ...} + return { + (int(major), int("".join(minor))): f"{VER_PREFIX}{major}{''.join(minor)}" + for _p, _y, major, *minor in py_versions + } + + except (OSError, ValueError, SubprocessError) as exc: + message = f"Failed to get Ruff target versions: {exc}" + raise ConfigurationError(message) from exc def _ruff_format_stdin(contents: str, args: Collection[str]) -> str: From 10152ae292a427b43379ec8c2c1123b0dc812a55 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:31:42 +0300 Subject: [PATCH 47/77] test: unit tests for ruff formatter --- src/darker/tests/test_formatters_ruff.py | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/darker/tests/test_formatters_ruff.py diff --git a/src/darker/tests/test_formatters_ruff.py b/src/darker/tests/test_formatters_ruff.py new file mode 100644 index 000000000..e9169cb1d --- /dev/null +++ b/src/darker/tests/test_formatters_ruff.py @@ -0,0 +1,66 @@ +"""Unit tests for `darker.formatters.ruff_formatter`.""" + +# pylint: disable=redefined-outer-name + +from subprocess import run # nosec +from textwrap import dedent +from unittest.mock import patch + +import pytest + +from darker.formatters import ruff_formatter + + +def test_get_supported_target_versions(): + """`ruff_formatter._get_supported_target_versions` runs Ruff, gets py versions.""" + with patch.object(ruff_formatter, "run") as run_mock: + run_mock.return_value.stdout = dedent( + """ + Default value: "py38" + Type: "py37" | "py38" | "py39" | "py310" | "py311" | "py312" + Example usage: + """ + ) + + # pylint: disable=protected-access + result = ruff_formatter._get_supported_target_versions() # noqa: SLF001 + + assert result == { + (3, 7): "py37", + (3, 8): "py38", + (3, 9): "py39", + (3, 10): "py310", + (3, 11): "py311", + (3, 12): "py312", + } + + +@pytest.fixture +def ruff(): + """Make a Ruff call and return the `subprocess.CompletedProcess` instance.""" + cmdline = [ + "ruff", + "format", + "--force-exclude", # apply `exclude =` from conffile even with stdin + "--stdin-filename=myfile.py", # allow to match exclude patterns + '--config=lint.ignore=["ISC001"]', + "-", + ] + return run( # noqa: S603 # nosec + cmdline, input="print( 1)\n", capture_output=True, check=False, text=True + ) + + +def test_ruff_returncode(ruff): + """A basic Ruff subprocess call returns a zero returncode.""" + assert ruff.returncode == 0 + + +def test_ruff_stderr(ruff): + """A basic Ruff subprocess call prints nothing on standard error.""" + assert ruff.stderr == "" + + +def test_ruff_stdout(ruff): + """A basic Ruff subprocess call prints the reformatted file on standard output.""" + assert ruff.stdout == "print(1)\n" From dafde396728ec1e5c8fe15212b8fbe0d1bb6d5ff Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:16:57 +0300 Subject: [PATCH 48/77] feat: pass file path to formatter run() method This allows formatters to exclude based on file path even if we're passing file contents on stdin and using --stdin-filename. --- src/darker/__main__.py | 2 +- src/darker/formatters/base_formatter.py | 3 ++- src/darker/formatters/black_formatter.py | 9 +++++++-- src/darker/formatters/none_formatter.py | 7 ++++++- src/darker/formatters/ruff_formatter.py | 23 ++++++++++++++++++----- src/darker/tests/test_formatters_black.py | 4 ++-- 6 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index d9d23a2da..9b471cc55 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -293,7 +293,7 @@ def _maybe_reformat_single_file( if glob_any(relpath_in_rev2, exclude): # File was excluded by Black configuration, don't reformat return fstringified - return formatter.run(fstringified) + return formatter.run(fstringified, relpath_in_rev2) def _drop_changes_on_unedited_lines( diff --git a/src/darker/formatters/base_formatter.py b/src/darker/formatters/base_formatter.py index b8c02ef1e..15e10d833 100644 --- a/src/darker/formatters/base_formatter.py +++ b/src/darker/formatters/base_formatter.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from argparse import Namespace + from pathlib import Path from darkgraylib.utils import TextDocument @@ -41,7 +42,7 @@ def read_config(self, src: tuple[str, ...], args: Namespace) -> None: self._read_config_file(config_path) self._read_cli_args(args) - def run(self, content: TextDocument) -> TextDocument: + def run(self, content: TextDocument, path_from_cwd: Path) -> TextDocument: """Reformat the content.""" raise NotImplementedError diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index 5cfffafac..f836814a6 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -17,7 +17,7 @@ Reformatted lines are returned e.g.:: >>> from darker.formatters.black_formatter import BlackFormatter - >>> dst = BlackFormatter().run(src_content) + >>> dst = BlackFormatter().run(src_content, src) >>> dst.lines ('for i in range(5):', ' print(i)', 'print("done")') @@ -51,6 +51,7 @@ if TYPE_CHECKING: from argparse import Namespace + from pathlib import Path from typing import Pattern from black import FileMode as Mode @@ -145,10 +146,14 @@ def _read_config_file(self, config_path: str) -> None: # noqa: C901 def _read_cli_args(self, args: Namespace) -> None: return read_black_compatible_cli_args(args, self.config) - def run(self, content: TextDocument) -> TextDocument: + def run( + self, content: TextDocument, path_from_cwd: Path # noqa: ARG002 + ) -> TextDocument: """Run the Black code re-formatter for the Python source code given as a string. :param content: The source code + :param path_from_cwd: The path to the source code file being reformatted, either + absolute or relative to the current working directory :return: The reformatted content """ diff --git a/src/darker/formatters/none_formatter.py b/src/darker/formatters/none_formatter.py index 650acd492..549fdde59 100644 --- a/src/darker/formatters/none_formatter.py +++ b/src/darker/formatters/none_formatter.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from argparse import Namespace + from pathlib import Path from darkgraylib.utils import TextDocument @@ -17,10 +18,14 @@ class NoneFormatter(BaseFormatter): name = "dummy reformat" - def run(self, content: TextDocument) -> TextDocument: + def run( + self, content: TextDocument, path_from_cwd: Path # noqa: ARG002 + ) -> TextDocument: """Return the Python source code unmodified. :param content: The source code + :param path_from_cwd: The path to the source code file being reformatted, either + absolute or relative to the current working directory :return: The source code unmodified """ diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py index 5fcc9e7fe..2bcb195c0 100644 --- a/src/darker/formatters/ruff_formatter.py +++ b/src/darker/formatters/ruff_formatter.py @@ -17,7 +17,7 @@ Reformatted lines are returned e.g.:: >>> from darker.formatters.ruff_formatter import RuffFormatter - >>> dst = RuffFormatter().run(src_content) + >>> dst = RuffFormatter().run(src_content, src) >>> dst.lines ('for i in range(5):', ' print(i)', 'print("done")') @@ -76,10 +76,12 @@ class RuffFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]): name = "ruff format" - def run(self, content: TextDocument) -> TextDocument: + 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. :param content: The source code + :param path_from_cwd: The path to the file being reformatted, either absolute or + relative to the current working directory :return: The reformatted content """ @@ -109,7 +111,7 @@ def run(self, content: TextDocument) -> TextDocument: # The custom handling of empty and all-whitespace files below will be # unnecessary if https://github.com/psf/ruff/pull/2484 lands in Ruff. contents_for_ruff = content.string_with_newline("\n") - dst_contents = _ruff_format_stdin(contents_for_ruff, args) + dst_contents = _ruff_format_stdin(contents_for_ruff, path_from_cwd, args) return TextDocument.from_str( dst_contents, encoding=content.encoding, @@ -211,15 +213,26 @@ def _get_supported_target_versions() -> dict[tuple[int, int], str]: raise ConfigurationError(message) from exc -def _ruff_format_stdin(contents: str, args: Collection[str]) -> str: +def _ruff_format_stdin( + contents: str, path_from_cwd: Path, args: Collection[str] +) -> str: """Run the contents through ``ruff format``. :param contents: The source code to be reformatted + :param path_from_cwd: The path to the file being reformatted, either absolute or + relative to the current working directory :param args: Additional command line arguments to pass to Ruff :return: The reformatted source code """ - cmdline = ["ruff", "format", *args, "-"] + cmdline = [ + "ruff", + "format", + "--force-exclude", # apply `exclude =` from conffile even with stdin + f"--stdin-filename={path_from_cwd}", # allow to match exclude patterns + *args, + "-", + ] logger.debug("Running %s", " ".join(cmdline)) result = run( # noqa: S603 # nosec cmdline, input=contents, stdout=PIPE, check=True, text=True, encoding="utf-8" diff --git a/src/darker/tests/test_formatters_black.py b/src/darker/tests/test_formatters_black.py index 47ae70fb1..d083df4b1 100644 --- a/src/darker/tests/test_formatters_black.py +++ b/src/darker/tests/test_formatters_black.py @@ -297,7 +297,7 @@ def test_run_ignores_excludes(): "force_exclude": regex.compile(r".*"), } - result = formatter.run(src) + result = formatter.run(src, Path("a.py")) assert result.string == "a = 1\n" @@ -398,7 +398,7 @@ def test_run_configuration( formatter = BlackFormatter() formatter.config = black_config - check(formatter.run(src)) + check(formatter.run(src, Path("a.py"))) assert format_str.call_count == 1 mode = format_str.call_args[1]["mode"] From 3a628cca67026d9217a037625bcd46910709a00d Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 6 Oct 2024 19:56:52 +0300 Subject: [PATCH 49/77] feat: add formatter TOML config sections to make testing easier --- src/darker/formatters/black_formatter.py | 1 + src/darker/formatters/ruff_formatter.py | 1 + src/darker/tests/test_formatters_black_compatible.py | 0 3 files changed, 2 insertions(+) create mode 100644 src/darker/tests/test_formatters_black_compatible.py diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index f836814a6..ff3127473 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -80,6 +80,7 @@ class BlackFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]): config: BlackCompatibleConfig # type: ignore[assignment] name = "black" + config_section = "tool.black" def read_config(self, src: tuple[str, ...], args: Namespace) -> None: """Read Black configuration from ``pyproject.toml``. diff --git a/src/darker/formatters/ruff_formatter.py b/src/darker/formatters/ruff_formatter.py index 2bcb195c0..667079d5f 100644 --- a/src/darker/formatters/ruff_formatter.py +++ b/src/darker/formatters/ruff_formatter.py @@ -75,6 +75,7 @@ class RuffFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]): config: BlackCompatibleConfig # type: ignore[assignment] name = "ruff format" + config_section = "tool.ruff" 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. diff --git a/src/darker/tests/test_formatters_black_compatible.py b/src/darker/tests/test_formatters_black_compatible.py new file mode 100644 index 000000000..e69de29bb From a415a79a6534b259d72c47513128860a78293fbc Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:00:38 +0300 Subject: [PATCH 50/77] test: both Black and Ruff in same test when applicable --- src/darker/tests/test_formatters_black.py | 76 ++------- .../tests/test_formatters_black_compatible.py | 144 ++++++++++++++++++ 2 files changed, 153 insertions(+), 67 deletions(-) diff --git a/src/darker/tests/test_formatters_black.py b/src/darker/tests/test_formatters_black.py index d083df4b1..9f2a1273f 100644 --- a/src/darker/tests/test_formatters_black.py +++ b/src/darker/tests/test_formatters_black.py @@ -9,7 +9,7 @@ from importlib import reload from pathlib import Path from typing import TYPE_CHECKING -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest import regex @@ -92,15 +92,8 @@ def test_formatter_without_black(caplog): ] +@pytest.mark.parametrize("option_name_delimiter", ["-", "_"]) @pytest.mark.kwparametrize( - dict( - config_path=None, config_lines=["line-length = 79"], expect={"line_length": 79} - ), - dict( - config_path="custom.toml", - config_lines=["line-length = 99"], - expect={"line_length": 99}, - ), dict( config_lines=["skip-string-normalization = true"], expect={"skip_string_normalization": True}, @@ -136,7 +129,6 @@ def test_formatter_without_black(caplog): config_lines=["target-version = ['py39', 'py37']"], expect={"target_version": {(3, 9), (3, 7)}}, ), - dict(config_lines=[r"include = '\.pyi$'"], expect={}), dict( config_lines=[r"exclude = '\.pyx$'"], expect={"exclude": RegexEquality("\\.pyx$")}, @@ -155,12 +147,16 @@ def test_formatter_without_black(caplog): ), config_path=None, ) -def test_read_config(tmpdir, config_path, config_lines, expect): - """`BlackFormatter.read_config` reads Black config correctly from a TOML file.""" +def test_read_config(tmpdir, option_name_delimiter, config_path, config_lines, expect): + """``read_config()`` reads Black config correctly from a TOML file.""" + # Test both hyphen and underscore delimited option names + config = "\n".join( + line.replace("-", option_name_delimiter) for line in config_lines + ) tmpdir = Path(tmpdir) src = tmpdir / "src.py" toml = tmpdir / (config_path or "pyproject.toml") - toml.write_text("[tool.black]\n{}\n".format("\n".join(config_lines))) + toml.write_text(f"[tool.black]\n{config}\n") with raises_or_matches(expect, []): formatter = BlackFormatter() args = Namespace() @@ -255,38 +251,6 @@ def test_filter_python_files( # pylint: disable=too-many-arguments assert result == expect_paths -@pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-1"]) -@pytest.mark.parametrize("newline", ["\n", "\r\n"]) -def test_run(encoding, newline): - """Running Black through its Python internal API gives correct results""" - src = TextDocument.from_lines( - [f"# coding: {encoding}", "print ( 'touché' )"], - encoding=encoding, - newline=newline, - ) - - result = BlackFormatter().run(src) - - assert result.lines == ( - f"# coding: {encoding}", - 'print("touché")', - ) - assert result.encoding == encoding - assert result.newline == newline - - -@pytest.mark.parametrize("newline", ["\n", "\r\n"]) -def test_run_always_uses_unix_newlines(newline): - """Content is always passed to Black with Unix newlines""" - src = TextDocument.from_str(f"print ( 'touché' ){newline}") - with patch("darker.formatters.black_wrapper.format_str") as format_str: - format_str.return_value = 'print("touché")\n' - - _ = BlackFormatter().run(src) - - format_str.assert_called_once_with("print ( 'touché' )\n", mode=ANY) - - def test_run_ignores_excludes(): """Black's exclude configuration is ignored by `BlackFormatter.run`.""" src = TextDocument.from_str("a=1\n") @@ -302,28 +266,6 @@ def test_run_ignores_excludes(): assert result.string == "a = 1\n" -@pytest.mark.parametrize( - "src_content, expect", - [ - ("", ""), - ("\n", "\n"), - ("\r\n", "\r\n"), - (" ", ""), - ("\t", ""), - (" \t", ""), - (" \t\n", "\n"), - (" \t\r\n", "\r\n"), - ], -) -def test_run_all_whitespace_input(src_content, expect): - """All-whitespace files are reformatted correctly""" - src = TextDocument.from_str(src_content) - - result = BlackFormatter().run(src) - - assert result.string == expect - - @pytest.mark.kwparametrize( dict(black_config={}), dict( diff --git a/src/darker/tests/test_formatters_black_compatible.py b/src/darker/tests/test_formatters_black_compatible.py index e69de29bb..f0afd1b96 100644 --- a/src/darker/tests/test_formatters_black_compatible.py +++ b/src/darker/tests/test_formatters_black_compatible.py @@ -0,0 +1,144 @@ +"""Unit tests for Black compatible formatter plugins.""" + +# pylint: disable=use-dict-literal + +from argparse import Namespace +from pathlib import Path +from unittest.mock import patch + +import pytest + +from darker.formatters import ruff_formatter +from darker.formatters.black_formatter import BlackFormatter +from darker.formatters.ruff_formatter import RuffFormatter +from darkgraylib.testtools.helpers import raises_or_matches +from darkgraylib.utils import TextDocument + + +@pytest.mark.parametrize( + "formatter_setup", + [(BlackFormatter, "-"), (BlackFormatter, "_"), (RuffFormatter, "-")], +) +@pytest.mark.kwparametrize( + dict( + config_path=None, config_lines=["line-length = 79"], expect={"line_length": 79} + ), + dict( + config_path="custom.toml", + config_lines=["line-length = 99"], + expect={"line_length": 99}, + ), + dict(config_lines=[r"include = '\.pyi$'"], expect={}), + config_path=None, +) +def test_read_config_black_and_ruff( + tmpdir, formatter_setup, config_path, config_lines, expect +): + """``read_config()`` reads Black and Ruff config correctly from a TOML file.""" + formatter_class, option_name_delimiter = formatter_setup + # For Black, we test both hyphen and underscore delimited option names + config = "\n".join( # pylint: disable=duplicate-code + line.replace("-", option_name_delimiter) for line in config_lines + ) + tmpdir = Path(tmpdir) + src = tmpdir / "src.py" + toml = tmpdir / (config_path or "pyproject.toml") + section = formatter_class.config_section + toml.write_text(f"[{section}]\n{config}\n") + with raises_or_matches(expect, []): + formatter = formatter_class() + args = Namespace() + args.config = config_path and str(toml) + if config_path: + expect["config"] = str(toml) + + # pylint: disable=duplicate-code + formatter.read_config((str(src),), args) + + assert formatter.config == expect + + +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) +@pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-1"]) +@pytest.mark.parametrize("newline", ["\n", "\r\n"]) +def test_run(formatter_class, encoding, newline): + """Running formatter through their plugin ``run`` method gives correct results.""" + src = TextDocument.from_lines( + [f"# coding: {encoding}", "print ( 'touché' )"], + encoding=encoding, + newline=newline, + ) + + result = formatter_class().run(src, Path("a.py")) + + assert result.lines == ( + f"# coding: {encoding}", + 'print("touché")', + ) + assert result.encoding == encoding + assert result.newline == newline + + +@pytest.mark.parametrize( + "formatter_setup", + [ + (BlackFormatter, "darker.formatters.black_wrapper.format_str"), + (RuffFormatter, "darker.formatters.ruff_formatter._ruff_format_stdin"), + ], +) +@pytest.mark.parametrize("newline", ["\n", "\r\n"]) +def test_run_always_uses_unix_newlines(formatter_setup, newline): + """Content is always passed to Black and Ruff with Unix newlines.""" + formatter_class, formatter_func_name = formatter_setup + src = TextDocument.from_str(f"print ( 'touché' ){newline}") + with patch(formatter_func_name) as formatter_func: + formatter_func.return_value = 'print("touché")\n' + + _ = formatter_class().run(src, Path("a.py")) + + (formatter_func_call,) = formatter_func.call_args_list + assert formatter_func_call.args[0] == "print ( 'touché' )\n" + + +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) +@pytest.mark.parametrize( + ("src_content", "expect"), + [ + ("", ""), + ("\n", "\n"), + ("\r\n", "\r\n"), + (" ", ""), + ("\t", ""), + (" \t", ""), + (" \t\n", "\n"), + (" \t\r\n", "\r\n"), + ], +) +def test_run_all_whitespace_input(formatter_class, src_content, expect): + """All-whitespace files are reformatted correctly.""" + src = TextDocument.from_str(src_content) + + result = formatter_class().run(src, Path("a.py")) + + assert result.string == expect + + +@pytest.mark.kwparametrize( + dict(formatter_config={}, expect=[]), + dict(formatter_config={"line_length": 80}, expect=["--line-length=80"]), +) +def test_run_configuration(formatter_config, expect): + """`RuffFormatter.run` passes correct configuration to Ruff.""" + src = TextDocument.from_str("import os\n") + with patch.object(ruff_formatter, "_ruff_format_stdin") as format_stdin: + format_stdin.return_value = "import os\n" + formatter = RuffFormatter() + formatter.config = formatter_config + + formatter.run(src, Path("a.py")) + + format_stdin.assert_called_once_with( + "import os\n", + Path("a.py"), + ['--config=lint.ignore=["ISC001"]', *expect], + ) From 4f7c378b0a61d01766221fb33f09d2555a51f52d Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:10:45 +0300 Subject: [PATCH 51/77] feat: remove global Black imports --- src/darker/formatters/black_formatter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py index ff3127473..fe89b3465 100644 --- a/src/darker/formatters/black_formatter.py +++ b/src/darker/formatters/black_formatter.py @@ -57,8 +57,6 @@ from black import FileMode as Mode from black import TargetVersion - from darker.formatters.formatter_config import BlackConfig - logger = logging.getLogger(__name__) From 36dcd1c2b1f8a6d83798d01adfebbe4b4f40be70 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:33:25 +0300 Subject: [PATCH 52/77] test: add Ruff command line option tests --- src/darker/tests/test_command_line_ruff.py | 102 +++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/darker/tests/test_command_line_ruff.py diff --git a/src/darker/tests/test_command_line_ruff.py b/src/darker/tests/test_command_line_ruff.py new file mode 100644 index 000000000..40c85526f --- /dev/null +++ b/src/darker/tests/test_command_line_ruff.py @@ -0,0 +1,102 @@ +"""Unit tests for Ruff related parts of `darker.command_line`.""" + +# pylint: disable=no-member,redefined-outer-name,unused-argument,use-dict-literal + +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent +from unittest.mock import patch + +import pytest + +from darker.__main__ import main +from darker.formatters import ruff_formatter +from darkgraylib.testtools.git_repo_plugin import GitRepoFixture + + +@pytest.fixture(scope="module") +def ruff_options_files(request, tmp_path_factory): + """Fixture for the `ruff_black_options` test.""" + with GitRepoFixture.context(request, tmp_path_factory) as repo: + (repo.root / "pyproject.toml").write_bytes(b"[tool.ruff]\n") + (repo.root / "ruff.cfg").write_text( + dedent( + """ + [tool.ruff] + line-length = 81 + skip-string-normalization = false + target-version = 'py38' + """ + ) + ) + yield repo.add({"main.py": 'print("Hello World!")\n'}, commit="Initial commit") + + +@pytest.mark.kwparametrize( + dict(options=[]), + dict(options=["-c", "ruff.cfg"], expect_opts=["--line-length=81"]), + dict(options=["--config", "ruff.cfg"], expect_opts=["--line-length=81"]), + dict( + options=["-S"], + expect_opts=['--config=format.quote-style="preserve"'], + ), + dict( + options=["--skip-string-normalization"], + expect_opts=['--config=format.quote-style="preserve"'], + ), + dict(options=["-l", "90"], expect_opts=["--line-length=90"]), + dict(options=["--line-length", "90"], expect_opts=["--line-length=90"]), + dict( + options=["-c", "ruff.cfg", "-S"], + expect_opts=["--line-length=81", '--config=format.quote-style="preserve"'], + ), + dict( + options=["-c", "ruff.cfg", "-l", "90"], + expect_opts=["--line-length=90"], + ), + dict( + options=["-l", "90", "-S"], + expect_opts=["--line-length=90", '--config=format.quote-style="preserve"'], + ), + dict( + options=["-c", "ruff.cfg", "-l", "90", "-S"], + expect_opts=["--line-length=90", '--config=format.quote-style="preserve"'], + ), + dict(options=["-t", "py39"], expect_opts=["--target-version=py39"]), + dict(options=["--target-version", "py39"], expect_opts=["--target-version=py39"]), + dict( + options=["-c", "ruff.cfg", "-t", "py39"], + expect_opts=["--line-length=81", "--target-version=py39"], + ), + dict( + options=["-t", "py39", "-S"], + expect_opts=[ + "--target-version=py39", + '--config=format.quote-style="preserve"', + ], + ), + dict( + options=["-c", "ruff.cfg", "-t", "py39", "-S"], + expect_opts=[ + "--line-length=81", + "--target-version=py39", + '--config=format.quote-style="preserve"', + ], + ), + dict(options=["--preview"], expect_opts=["--preview"]), + expect_opts=[], +) +def test_ruff_options(monkeypatch, ruff_options_files, options, expect_opts): + """Ruff options from the command line are passed correctly to Ruff.""" + ruff_options_files["main.py"].write_bytes(b'print ("Hello World!")\n') + with patch.object(ruff_formatter, "_ruff_format_stdin") as format_stdin: + format_stdin.return_value = 'print("Hello World!")\n' + + main([*options, "--formatter=ruff", str(ruff_options_files["main.py"])]) + + format_stdin.assert_called_once_with( + 'print ("Hello World!")\n', + Path("main.py"), + ['--config=lint.ignore=["ISC001"]', *expect_opts], + ) From 095dab0b484120211fa7043f8543a4e5094beded Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 6 Oct 2024 21:59:38 +0300 Subject: [PATCH 53/77] test: add Ruff config file + CLI option test --- src/darker/tests/test_command_line_ruff.py | 50 +++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/darker/tests/test_command_line_ruff.py b/src/darker/tests/test_command_line_ruff.py index 40c85526f..68c9ada7d 100644 --- a/src/darker/tests/test_command_line_ruff.py +++ b/src/darker/tests/test_command_line_ruff.py @@ -6,13 +6,14 @@ from pathlib import Path from textwrap import dedent -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from darker.__main__ import main from darker.formatters import ruff_formatter from darkgraylib.testtools.git_repo_plugin import GitRepoFixture +from darkgraylib.utils import joinlines @pytest.fixture(scope="module") @@ -100,3 +101,50 @@ def test_ruff_options(monkeypatch, ruff_options_files, options, expect_opts): Path("main.py"), ['--config=lint.ignore=["ISC001"]', *expect_opts], ) + + +@pytest.mark.kwparametrize( + dict(config=[], options=[], expect=[]), + dict(options=["--line-length=50"], expect=["--line-length=50"]), + dict(config=["line-length = 60"], expect=["--line-length=60"]), + dict( + config=["line-length = 60"], + options=["--line-length=50"], + expect=["--line-length=50"], + ), + dict( + options=["--skip-string-normalization"], + expect=['--config=format.quote-style="preserve"'], + ), + dict(options=["--no-skip-string-normalization"], expect=[]), + dict( + options=["--skip-magic-trailing-comma"], + expect=[ + '--config="format.skip-magic-trailing-comma=true"', + '--config="lint.isort.split-on-trailing-comma=false"', + ], + ), + dict(options=["--target-version", "py39"], expect=["--target-version=py39"]), + dict(options=["--preview"], expect=["--preview"]), + config=[], + options=[], +) +def test_ruff_config_file_and_options(git_repo, config, options, expect): + """Ruff configuration file and command line options are combined correctly.""" + # Only line length is both supported as a command line option and read by Darker + # from Ruff configuration. + added_files = git_repo.add( + {"main.py": "foo", "pyproject.toml": joinlines(["[tool.ruff]", *config])}, + commit="Initial commit", + ) + added_files["main.py"].write_bytes(b"a = [1, 2,]") + # Speed up tests by mocking `_ruff_format_stdin` to skip running Ruff + format_stdin = Mock(return_value="a = [1, 2,]") + with patch.object(ruff_formatter, "_ruff_format_stdin", format_stdin): + # end of test setup, now run the test: + + main([*options, "--formatter=ruff", str(added_files["main.py"])]) + + format_stdin.assert_called_once_with( + "a = [1, 2,]", Path("main.py"), ['--config=lint.ignore=["ISC001"]', *expect] + ) From 00b07641ecb6f5a356d8266c233a15d0aecd298b Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:20:41 +0300 Subject: [PATCH 54/77] test: ruff tests in test_main.py --- src/darker/tests/test_main.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index d8d1850f5..d2b188d52 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -104,6 +104,9 @@ def main_repo(request, tmp_path_factory): yield fixture +@pytest.mark.parametrize( + "formatter_arguments", [[], ["--formatter=black"], ["--formatter=ruff"]] +) @pytest.mark.kwparametrize( dict(arguments=["--diff"], expect_stdout=A_PY_DIFF_BLACK), dict(arguments=["--isort"], expect_a_py=A_PY_BLACK_ISORT), @@ -150,6 +153,8 @@ def main_repo(request, tmp_path_factory): pyproject_toml=""" [tool.black] exclude = 'a.py' + [tool.ruff.format] + exclude = ['a.py'] """, expect_a_py=A_PY, ), @@ -158,6 +163,8 @@ def main_repo(request, tmp_path_factory): pyproject_toml=""" [tool.black] exclude = 'a.py' + [tool.ruff.format] + exclude = ['a.py'] """, expect_stdout=[], ), @@ -166,6 +173,8 @@ def main_repo(request, tmp_path_factory): pyproject_toml=""" [tool.black] extend_exclude = 'a.py' + [tool.ruff] + extend-exclude = ['a.py'] """, expect_a_py=A_PY, ), @@ -174,6 +183,8 @@ def main_repo(request, tmp_path_factory): pyproject_toml=""" [tool.black] extend_exclude = 'a.py' + [tool.ruff] + extend-exclude = ['a.py'] """, expect_stdout=[], ), @@ -182,6 +193,10 @@ def main_repo(request, tmp_path_factory): pyproject_toml=""" [tool.black] force_exclude = 'a.py' + [tool.ruff.format] + exclude = ['a.py'] + [tool.ruff] + force-exclude = true # redundant, always passed to ruff anyway """, expect_a_py=A_PY, ), @@ -190,6 +205,10 @@ def main_repo(request, tmp_path_factory): pyproject_toml=""" [tool.black] force_exclude = 'a.py' + [tool.ruff.format] + exclude = ['a.py'] + [tool.ruff] + force-exclude = true # redundant, always passed to ruff anyway """, expect_stdout=[], ), @@ -211,6 +230,7 @@ def test_main( main_repo, monkeypatch, capsys, + formatter_arguments, arguments, newline, pyproject_toml, @@ -233,7 +253,9 @@ def test_main( repo.paths["subdir/a.py"].write_bytes(newline.join(A_PY).encode("ascii")) repo.paths["b.py"].write_bytes(f"print(42 ){newline}".encode("ascii")) - retval = darker.__main__.main(arguments + [str(pwd / "subdir")]) + retval = darker.__main__.main( + [*formatter_arguments, *arguments, str(pwd / "subdir")] + ) stdout = capsys.readouterr().out.replace(str(repo.root), "") diff_output = stdout.splitlines(False) From 1bc74b7ba9aec954949cb54f0d439eca822f6d20 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:56:55 +0300 Subject: [PATCH 55/77] test: exercise ruff formatter in three more test_main tests --- src/darker/tests/test_main.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index d2b188d52..6ef0b77ba 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -278,7 +278,8 @@ def test_main( assert retval == expect_retval -def test_main_in_plain_directory(tmp_path, capsys): +@pytest.mark.parametrize("formatter", [[], ["--formatter=black"], ["--formatter=ruff"]]) +def test_main_in_plain_directory(tmp_path, capsys, formatter): """Darker works also in a plain directory tree""" subdir_a = tmp_path / "subdir_a" subdir_c = tmp_path / "subdir_b/subdir_c" @@ -289,7 +290,7 @@ def test_main_in_plain_directory(tmp_path, capsys): (subdir_c / "another python file.py").write_text("a =5") retval = darker.__main__.main( - ["--diff", "--check", "--isort", "--lint", "dummy", str(tmp_path)], + [*formatter, "--diff", "--check", "--isort", "--lint", "dummy", str(tmp_path)], ) assert retval == 1 @@ -319,18 +320,19 @@ def test_main_in_plain_directory(tmp_path, capsys): ) +@pytest.mark.parametrize("formatter", [[], ["--formatter=black"], ["--formatter=ruff"]]) @pytest.mark.parametrize( "encoding, text", [(b"utf-8", b"touch\xc3\xa9"), (b"iso-8859-1", b"touch\xe9")] ) @pytest.mark.parametrize("newline", [b"\n", b"\r\n"]) -def test_main_encoding(git_repo, encoding, text, newline): +def test_main_encoding(git_repo, formatter, encoding, text, newline): """Encoding and newline of the file is kept unchanged after reformatting""" paths = git_repo.add({"a.py": newline.decode("ascii")}, commit="Initial commit") edited = [b"# coding: ", encoding, newline, b's="', text, b'"', newline] expect = [b"# coding: ", encoding, newline, b's = "', text, b'"', newline] paths["a.py"].write_bytes(b"".join(edited)) - retval = darker.__main__.main(["a.py"]) + retval = darker.__main__.main([*formatter, "a.py"]) result = paths["a.py"].read_bytes() assert retval == 0 @@ -409,7 +411,8 @@ def test_main_historical_pre_commit(git_repo, monkeypatch): darker.__main__.main(["--revision=:PRE-COMMIT:", "a.py"]) -def test_main_vscode_tmpfile(git_repo, capsys): +@pytest.mark.parametrize("formatter", [[], ["--formatter=black"], ["--formatter=ruff"]]) +def test_main_vscode_tmpfile(git_repo, capsys, formatter): """Main function handles VSCode `.py..tmp` files correctly""" _ = git_repo.add( {"a.py": "print ( 'reformat me' ) \n"}, @@ -417,7 +420,7 @@ def test_main_vscode_tmpfile(git_repo, capsys): ) (git_repo.root / "a.py.hash.tmp").write_text("print ( 'reformat me now' ) \n") - retval = darker.__main__.main(["--diff", "a.py.hash.tmp"]) + retval = darker.__main__.main([*formatter, "--diff", "a.py.hash.tmp"]) assert retval == 0 outerr = capsys.readouterr() From b32cf7f07c5bfda0a1b74aab9f11446bb4e7ec19 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:07:04 +0300 Subject: [PATCH 56/77] test: ruff in tests for format_edited_parts() --- .../tests/test_main_format_edited_parts.py | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/darker/tests/test_main_format_edited_parts.py b/src/darker/tests/test_main_format_edited_parts.py index 2fa93ab35..9160950f2 100644 --- a/src/darker/tests/test_main_format_edited_parts.py +++ b/src/darker/tests/test_main_format_edited_parts.py @@ -16,6 +16,7 @@ import darker.verification from darker.config import Exclusions from darker.formatters.black_formatter import BlackFormatter +from darker.formatters.ruff_formatter import RuffFormatter from darker.tests.examples import A_PY, A_PY_BLACK, A_PY_BLACK_FLYNT, A_PY_BLACK_ISORT from darker.tests.helpers import unix_and_windows_newline_repos from darker.verification import NotEquivalentError @@ -71,7 +72,7 @@ def format_edited_parts_repo(request, tmp_path_factory): expect=[A_PY_BLACK_ISORT_FLYNT], ), dict( - black_config={"skip_string_normalization": True}, + formatter_config={"skip_string_normalization": True}, black_exclude=set(), expect=[A_PY_BLACK_UNNORMALIZE], ), @@ -84,18 +85,20 @@ def format_edited_parts_repo(request, tmp_path_factory): isort_exclude=set(), expect=[A_PY_ISORT], ), - black_config={}, + formatter_config={}, black_exclude={"**/*"}, isort_exclude={"**/*"}, flynt_exclude={"**/*"}, ) +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) @pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["unix", "windows"]) def test_format_edited_parts( format_edited_parts_repo, - black_config, + formatter_config, black_exclude, isort_exclude, flynt_exclude, + formatter_class, newline, expect, ): @@ -106,8 +109,8 @@ def test_format_edited_parts( :func:`~darker.__main__.format_edited_parts`. """ - formatter = BlackFormatter() - formatter.config = black_config + formatter = formatter_class() + formatter.config = formatter_config result = darker.__main__.format_edited_parts( Path(format_edited_parts_repo[newline].root), @@ -195,9 +198,10 @@ def format_edited_parts_stdin_repo(request, tmp_path_factory): ], ), ) +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) @pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["unix", "windows"]) def test_format_edited_parts_stdin( - format_edited_parts_stdin_repo, newline, rev1, rev2, expect + format_edited_parts_stdin_repo, rev1, rev2, expect, formatter_class, newline ): """`format_edited_parts` with ``--stdin-filename``.""" repo = format_edited_parts_stdin_repo[newline] @@ -216,7 +220,7 @@ def test_format_edited_parts_stdin( {Path("a.py")}, Exclusions(formatter=set(), isort=set()), RevisionRange(rev1, rev2), - BlackFormatter(), + formatter_class(), report_unmodified=False, ), ) @@ -232,12 +236,15 @@ def test_format_edited_parts_stdin( assert result == expect -def test_format_edited_parts_all_unchanged(git_repo, monkeypatch): +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) +def test_format_edited_parts_all_unchanged(git_repo, monkeypatch, formatter_class): """``format_edited_parts()`` yields nothing if no reformatting was needed.""" monkeypatch.chdir(git_repo.root) paths = git_repo.add({"a.py": "pass\n", "b.py": "pass\n"}, commit="Initial commit") - paths["a.py"].write_bytes(b'"properly"\n"formatted"\n') - paths["b.py"].write_bytes(b'"not"\n"checked"\n') + # Note: `ruff format` likes to add a blank line between strings, Black not + # - but since black won't remove it either, this works for our test: + paths["a.py"].write_bytes(b'"properly"\n\n"formatted"\n') + paths["b.py"].write_bytes(b'"not"\n\n"checked"\n') result = list( darker.__main__.format_edited_parts( @@ -245,7 +252,7 @@ def test_format_edited_parts_all_unchanged(git_repo, monkeypatch): {Path("a.py"), Path("b.py")}, Exclusions(), RevisionRange("HEAD", ":WORKTREE:"), - BlackFormatter(), + formatter_class(), report_unmodified=False, ), ) @@ -253,7 +260,8 @@ def test_format_edited_parts_all_unchanged(git_repo, monkeypatch): assert result == [] -def test_format_edited_parts_ast_changed(git_repo, caplog): +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) +def test_format_edited_parts_ast_changed(git_repo, caplog, formatter_class): """``darker.__main__.format_edited_parts()`` when reformatting changes the AST.""" caplog.set_level(logging.DEBUG, logger="darker.__main__") paths = git_repo.add({"a.py": "1\n2\n3\n4\n5\n6\n7\n8\n"}, commit="Initial commit") @@ -270,7 +278,7 @@ def test_format_edited_parts_ast_changed(git_repo, caplog): {Path("a.py")}, Exclusions(isort={"**/*"}), RevisionRange("HEAD", ":WORKTREE:"), - BlackFormatter(), + formatter_class(), report_unmodified=False, ), ) @@ -292,7 +300,8 @@ def test_format_edited_parts_ast_changed(git_repo, caplog): ] -def test_format_edited_parts_isort_on_already_formatted(git_repo): +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) +def test_format_edited_parts_isort_on_already_formatted(git_repo, formatter_class): """An already correctly formatted file after ``isort`` is simply skipped.""" before = [ "import a", @@ -314,7 +323,7 @@ def test_format_edited_parts_isort_on_already_formatted(git_repo): {Path("a.py")}, Exclusions(), RevisionRange("HEAD", ":WORKTREE:"), - BlackFormatter(), + formatter_class(), report_unmodified=False, ) @@ -368,8 +377,9 @@ def format_edited_parts_historical_repo(request, tmp_path_factory): dict(rev1="HEAD^", rev2=WORKTREE, expect=[(":WORKTREE:", "reformatted")]), dict(rev1="HEAD", rev2=WORKTREE, expect=[(":WORKTREE:", "reformatted")]), ) +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) def test_format_edited_parts_historical( - format_edited_parts_historical_repo, rev1, rev2, expect + format_edited_parts_historical_repo, rev1, rev2, expect, formatter_class ): """``format_edited_parts()`` is correct for different commit pairs.""" repo = format_edited_parts_historical_repo @@ -379,7 +389,7 @@ def test_format_edited_parts_historical( {Path("a.py")}, Exclusions(), RevisionRange(rev1, rev2), - BlackFormatter(), + formatter_class(), report_unmodified=False, ) From d4d87b0d39f4409eeae92d436da36ce998451400 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:09:54 +0300 Subject: [PATCH 57/77] test: ruff in tests for reformat_and_flynt_single_file() --- .../test_main_reformat_and_flynt_single_file.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/darker/tests/test_main_reformat_and_flynt_single_file.py b/src/darker/tests/test_main_reformat_and_flynt_single_file.py index 0b1756a77..7e14d6e1a 100644 --- a/src/darker/tests/test_main_reformat_and_flynt_single_file.py +++ b/src/darker/tests/test_main_reformat_and_flynt_single_file.py @@ -10,6 +10,7 @@ from darker.__main__ import _reformat_and_flynt_single_file from darker.config import Exclusions from darker.formatters.black_formatter import BlackFormatter +from darker.formatters.ruff_formatter import RuffFormatter from darker.git import EditedLinenumsDiffer from darkgraylib.git import RevisionRange from darkgraylib.testtools.git_repo_plugin import GitRepoFixture @@ -69,6 +70,7 @@ def reformat_and_flynt_single_file_repo(request, tmp_path_factory): exclusions=Exclusions(), expect="import original\nprint( original )\n", ) +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) def test_reformat_and_flynt_single_file( reformat_and_flynt_single_file_repo, relative_path, @@ -76,6 +78,7 @@ def test_reformat_and_flynt_single_file( rev2_isorted, exclusions, expect, + formatter_class, ): """Test for `_reformat_and_flynt_single_file`.""" repo = reformat_and_flynt_single_file_repo @@ -88,13 +91,14 @@ def test_reformat_and_flynt_single_file( TextDocument(rev2_content), TextDocument(rev2_isorted), has_isort_changes=False, - formatter=BlackFormatter(), + formatter=formatter_class(), ) assert result.string == expect -def test_blacken_and_flynt_single_file_common_ancestor(git_repo): +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) +def test_blacken_and_flynt_single_file_common_ancestor(git_repo, formatter_class): """`_blacken_and_flynt_single_file` diffs to common ancestor of ``rev1...rev2``.""" a_py_initial = dedent( """\ @@ -151,7 +155,7 @@ def test_blacken_and_flynt_single_file_common_ancestor(git_repo): rev2_content=worktree, rev2_isorted=worktree, has_isort_changes=False, - formatter=BlackFormatter(), + formatter=formatter_class(), ) assert result.lines == ( @@ -163,7 +167,8 @@ def test_blacken_and_flynt_single_file_common_ancestor(git_repo): ) -def test_reformat_single_file_docstring(git_repo): +@pytest.mark.parametrize("formatter_class", [BlackFormatter, RuffFormatter]) +def test_reformat_single_file_docstring(git_repo, formatter_class): """`_blacken_and_flynt_single_file()` handles docstrings as one contiguous block.""" initial = dedent( '''\ @@ -210,7 +215,7 @@ def docstring_func(): rev2_content=TextDocument.from_str(modified), rev2_isorted=TextDocument.from_str(modified), has_isort_changes=False, - formatter=BlackFormatter(), + formatter=formatter_class(), ) assert result.lines == tuple(expect.splitlines()) From 2da8687fdd4f6a87df20519b2a970942928440e2 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:12:03 +0300 Subject: [PATCH 58/77] test: ruff in tests for --stdin-filename --- src/darker/tests/test_main_stdin_filename.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/darker/tests/test_main_stdin_filename.py b/src/darker/tests/test_main_stdin_filename.py index 87914037d..aab1d189c 100644 --- a/src/darker/tests/test_main_stdin_filename.py +++ b/src/darker/tests/test_main_stdin_filename.py @@ -2,9 +2,10 @@ # pylint: disable=no-member,redefined-outer-name,too-many-arguments,use-dict-literal +from __future__ import annotations + from io import BytesIO from types import SimpleNamespace -from typing import List, Optional from unittest.mock import Mock, patch import pytest @@ -148,14 +149,16 @@ def main_stdin_filename_repo(request, tmp_path_factory): expect=0, expect_a_py="original\n", ) +@pytest.mark.parametrize("formatter", [[], ["--formatter=black"], ["--formatter=ruff"]]) def test_main_stdin_filename( main_stdin_filename_repo: SimpleNamespace, - config_src: Optional[List[str]], - src: List[str], - stdin_filename: Optional[str], - revision: Optional[str], + config_src: list[str] | None, + src: list[str], + stdin_filename: str | None, + revision: str | None, expect: int, expect_a_py: str, + formatter: list[str], ) -> None: """Tests for `darker.__main__.main` and the ``--stdin-filename`` option""" repo = main_stdin_filename_repo @@ -177,7 +180,7 @@ def test_main_stdin_filename( ), raises_if_exception(expect): # end of test setup - retval = darker.__main__.main(arguments) + retval = darker.__main__.main([*formatter, *arguments]) assert retval == expect assert repo.paths["a.py"].read_text() == expect_a_py From 2beb0ade28979513ed76c629ae5c3748a05ab3de Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 9 Oct 2024 23:19:37 +0300 Subject: [PATCH 59/77] test: Black compatible Flake8 configuration [ruff-plugin] --- setup.cfg | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.cfg b/setup.cfg index d8ee464d5..f30182804 100644 --- a/setup.cfg +++ b/setup.cfg @@ -97,8 +97,14 @@ ignore = D400 # D415 First line should end with a period, question mark, or exclamation point D415 + # E203 Whitespace before ':' + E203 # E231 missing whitespace after ',' E231 + # E501 Line too long (82 > 79 characters) + E501 + # E701 Multiple statements on one line (colon) + E701 # W503 line break before binary operator W503 # Darglint options when run as a Flake8 plugin: From 3678ad9e14494568c8251b127836d6df0484f201 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:14:27 +0200 Subject: [PATCH 60/77] ci: disable dependabot version updates Enforcing newest versions of Darker's dependencies may cause a problem for those users who like to install Darker in the same environment as the package they are reformatting. Their package may have an upper version limit for a dependency, e.g. to prevent an unintended update to an incompatible major version. If Darker then requires a minimum version newer than that limit, the installation of Darker will fail. It's reasonable to keep Dependabot security updates (based on the Github Advisory Database) enabled, but for the above reasons, Dependabot version updates should in my opinion be disabled. We should support oldest possible non-vulnerable versions of our dependencies, and yet in a fresh environment users will still get the newest versions automatically installed. See also #788 --- .github/dependabot.yml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 7d25a84b5..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - ignore: - - dependency-name: "black" From 78fbed86cad375a17023c683a74c132e69d5edcf Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:14:52 +0200 Subject: [PATCH 61/77] docs: update the change log --- CHANGES.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b2ce13132..fe9d3980c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,6 @@ These features will be included in the next release: Added ----- -- Dependabot configuration for automatically opening prs for package version - upgrades - New exit codes 2 for file not found, 3 for invalid command line arguments, 4 for missing dependencies and 123 for unknown failures. - Display exit code in parentheses after error message. From ba5bd89894eda993e8f52dbb60b8adc9ce7d7e75 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:56:30 +0300 Subject: [PATCH 62/77] docs(contr): add @shane-kearns contributor --- contributors.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 16f13d158..465e5c34c 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -186,6 +186,8 @@ seweissman: - {link_type: issues, type: Bug reports} sgaist: - {link_type: pulls-author, type: Code} +shane-kearns: + - {link_type: issues, type: Bug reports} shangxiao: - {link_type: pulls-author, type: Code} - {link_type: issues, type: Bug reports} From b8acd9f8394d3c25f6d82ea85f0eda4189b96f3e Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:12:27 +0300 Subject: [PATCH 63/77] docs(contr): more contribution types for oldies --- contributors.yaml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 465e5c34c..acce8463b 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -1,6 +1,7 @@ --- AckslD: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} Ashblaze: - {link_type: search-discussions, type: Bug reports} Asuskf: @@ -9,14 +10,20 @@ Carreau: - {link_type: commits, type: Code} - {link_type: commits, type: Documentation} - {link_type: pulls-reviewed, type: Reviewed Pull Requests} + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: pulls-author, type: Code} + - {link_type: search-comments, type: Reviewed Pull Requests} CorreyL: - {link_type: commits, type: Code} - {link_type: commits, type: Documentation} - {link_type: pulls-reviewed, type: Reviewed Pull Requests} + - {link_type: search-comments, type: Bug reports} DavidCDreher: - {link_type: issues, type: Bug reports} DylanYoung: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} Eyobkibret15: - {link_type: search-discussions, type: Bug reports} Hainguyen1210: @@ -27,10 +34,14 @@ Krischtopp: - {link_type: issues, type: Bug reports} MatthijsBurgh: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} + - {link_type: pulls-author, type: Code} Mystic-Mirage: - {link_type: commits, type: Code} - {link_type: commits, type: Documentation} - {link_type: pulls-reviewed, type: Reviewed Pull Requests} + - {link_type: search-comments, type: Bug reports} Pacu2: - {link_type: pulls-author, type: Code} - {link_type: pulls-reviewed, type: Reviewed Pull Requests} @@ -40,8 +51,12 @@ RishiKumarRay: - {link_type: search, type: Bug reports} Svenito: - {link_type: pulls-author, type: Code} + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} Timple: - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} agandra: - {link_type: issues, type: Bug reports} akaihola: @@ -50,6 +65,10 @@ akaihola: - {link_type: commits, type: Documentation} - {link_type: pulls-reviewed, type: Reviewed Pull Requests} - {link_type: commits, type: Maintenance} + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: pulls-author, type: Code} + - {link_type: search-comments, type: Reviewed Pull Requests} aljazerzen: - {link_type: commits, type: Code} baod-rate: @@ -70,8 +89,12 @@ chrisdecker1201: - {link_type: issues, type: Bug reports} clintonsteiner: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: pulls-author, type: Code} + - {link_type: search-comments, type: Reviewed Pull Requests} deadkex: - {link_type: search-discussions, type: Bug reports} + - {link_type: search-comments, type: Bug reports} dhrvjha: - {link_type: search-comments, type: Bug reports} - {link_type: pulls-author, type: Code} @@ -79,6 +102,7 @@ dkeraudren: - {link_type: search-comments, type: Bug reports} dshemetov: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} dsmanl: - {link_type: issues, type: Bug reports} dwt: @@ -88,6 +112,9 @@ falkben: - {link_type: search-discussions, type: Bug reports} felixvd: - {link_type: pulls-author, type: Code} + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} fizbin: - {link_type: issues, type: Bug reports} flying-sheep: @@ -100,12 +127,17 @@ gergelypolonkai: - {link_type: issues, type: Bug reports} gesslerpd: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} guettli: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} hauntsaninja: - {link_type: issues, type: Bug reports} hugovk: - {link_type: pulls-author, type: Code} + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} ioggstream: - {link_type: search-comments, type: Bug reports} irynahryshanovich: @@ -122,15 +154,20 @@ jasleen19: - {link_type: pulls-reviewed, type: Reviewed Pull Requests} jedie: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} + - {link_type: pulls-author, type: Code} jenshnielsen: - {link_type: search, type: Bug reports} jsuit: - {link_type: search-discussions, type: Bug reports} jvacek: - {link_type: search, type: Bug reports} + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} k-dominik: - {link_type: search-comments, type: Bug reports} - {link_type: search-discussions, type: Bug reports} + - {link_type: issues, type: Bug reports} kedhammar: - {link_type: search-discussions, type: Bug reports} - {link_type: issues, type: Bug reports} @@ -148,6 +185,8 @@ markddavidoff: martinRenou: - {link_type: conda-issues, type: Code} - {link_type: pulls-reviewed, type: Reviewed Pull Requests} + - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} matclayton: - {link_type: issues, type: Bug reports} mayk0gan: @@ -176,18 +215,23 @@ qubidt: rogalski: - {link_type: pulls-author, type: Code} - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} roniemartinez: - {link_type: issues, type: Bug reports} rossbar: - {link_type: issues, type: Bug reports} samoylovfp: - {link_type: pulls-reviewed, type: Reviewed Pull Requests} + - {link_type: search-comments, type: Bug reports} seweissman: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} sgaist: - {link_type: pulls-author, type: Code} + - {link_type: issues, type: Bug reports} shane-kearns: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} shangxiao: - {link_type: pulls-author, type: Code} - {link_type: issues, type: Bug reports} @@ -216,11 +260,13 @@ virtuald: - {link_type: issues, type: Bug reports} wasdee: - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} wjdp: - {link_type: issues, type: Bug reports} - {link_type: pulls-author, type: Documentation} wkentaro: - {link_type: issues, type: Bug reports} + - {link_type: pulls-author, type: Code} wnoise: - {link_type: issues, type: Bug reports} wpnbos: From 4e77c6531532b6e37d9632fd17a4e7329693b80b Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:13:17 +0300 Subject: [PATCH 64/77] docs(contr): add @DeinAlptraum as contributor --- contributors.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index acce8463b..44f9b4f2c 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -21,6 +21,9 @@ CorreyL: - {link_type: search-comments, type: Bug reports} DavidCDreher: - {link_type: issues, type: Bug reports} +DeinAlptraum: + - {link_type: search-comments, type: Bug reports} + - {link_type: search-discussions, type: Bug reports} DylanYoung: - {link_type: issues, type: Bug reports} - {link_type: search-comments, type: Bug reports} From b9c3c3e1256dba9f1815770469094f7d891a5ac0 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:13:34 +0300 Subject: [PATCH 65/77] docs(contr): add @Garfounkel as contributor --- contributors.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 44f9b4f2c..893fc1ec7 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -29,6 +29,8 @@ DylanYoung: - {link_type: search-comments, type: Bug reports} Eyobkibret15: - {link_type: search-discussions, type: Bug reports} +Garfounkel: + - {link_type: search-comments, type: Bug reports} Hainguyen1210: - {link_type: issues, type: Bug reports} KangOl: From 75de9cd1e7120dbc3e81ead405be182d4f174128 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:13:43 +0300 Subject: [PATCH 66/77] docs(contr): add @Will-Ruddick as contributor --- contributors.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 893fc1ec7..be00a2a3e 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -62,6 +62,8 @@ Svenito: Timple: - {link_type: search-comments, type: Bug reports} - {link_type: search-comments, type: Reviewed Pull Requests} +Will-Ruddick: + - {link_type: search-comments, type: Bug reports} agandra: - {link_type: issues, type: Bug reports} akaihola: From dd1965a0fddd5c3a59777c5baa43cc85e78c8b82 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:13:55 +0300 Subject: [PATCH 67/77] docs(contr): add @baodrate as contributor --- contributors.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index be00a2a3e..9743c24a2 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -80,6 +80,10 @@ aljazerzen: - {link_type: commits, type: Code} baod-rate: - {link_type: pulls-author, type: Code} +baodrate: + - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} + - {link_type: pulls-author, type: Code} bdperkin: - {link_type: issues, type: Bug reports} brtknr: From a61c609d6a52cce7d50ffae6aae437ac829a00d6 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:14:10 +0300 Subject: [PATCH 68/77] docs(contr): add @brettcannon as contributor --- contributors.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 9743c24a2..07e7b64e3 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -86,6 +86,8 @@ baodrate: - {link_type: pulls-author, type: Code} bdperkin: - {link_type: issues, type: Bug reports} +brettcannon: + - {link_type: search-comments, type: Bug reports} brtknr: - {link_type: pulls-reviewed, type: Reviewed Pull Requests} casio: From 745dbf069e8a4db20c764185a672f7f6ea6b270c Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:14:23 +0300 Subject: [PATCH 69/77] docs(contr): add @tehunter as contributor --- contributors.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 07e7b64e3..afc63575c 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -262,6 +262,9 @@ talhajunaidd: - {link_type: commits, type: Code} tapted: - {link_type: issues, type: Bug reports} +tehunter: + - {link_type: issues, type: Bug reports} + - {link_type: pulls-author, type: Code} tgross35: - {link_type: issues, type: Bug reports} tkolleh: From 222479be0c3e9a4f9a2f5f5351948d7ca2f7db91 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:46:25 +0200 Subject: [PATCH 70/77] docs(contr): add @anakinxc as a contributor --- contributors.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index afc63575c..5c604e99d 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -78,6 +78,8 @@ akaihola: - {link_type: search-comments, type: Reviewed Pull Requests} aljazerzen: - {link_type: commits, type: Code} +anakinxc: + - {link_type: issues, type: Bug reports} baod-rate: - {link_type: pulls-author, type: Code} baodrate: From 0314e6791792a0c03bf73b6bf645f2d71c7866ce Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:46:44 +0200 Subject: [PATCH 71/77] docs(contr): add @artel1992 as a contributor --- contributors.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 5c604e99d..d7c148239 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -80,6 +80,9 @@ aljazerzen: - {link_type: commits, type: Code} anakinxc: - {link_type: issues, type: Bug reports} +artel1992: + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} baod-rate: - {link_type: pulls-author, type: Code} baodrate: From accae982e54e428598e040394a2aa47e01fce0dc Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:46:59 +0200 Subject: [PATCH 72/77] docs(contr): add @brtkwr as a contributor --- contributors.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index d7c148239..bbd8a5b8f 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -95,6 +95,8 @@ brettcannon: - {link_type: search-comments, type: Bug reports} brtknr: - {link_type: pulls-reviewed, type: Reviewed Pull Requests} +brtkwr: + - {link_type: search-comments, type: Bug reports} casio: - {link_type: issues, type: Bug reports} cclauss: From b989f0a849a156cb69ac6f2f8bfe3af07e8f37ff Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:47:32 +0200 Subject: [PATCH 73/77] docs(contr): add @haohu321 as a contributor --- contributors.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index bbd8a5b8f..e888a4d12 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -151,6 +151,9 @@ gesslerpd: guettli: - {link_type: issues, type: Bug reports} - {link_type: search-comments, type: Bug reports} +haohu321: + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Bug reports} hauntsaninja: - {link_type: issues, type: Bug reports} hugovk: From 0f4197a2201c629679929e2d660d69637c6beaab Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:47:43 +0200 Subject: [PATCH 74/77] docs(contr): add @ranelpadon as a contributor --- contributors.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index e888a4d12..606fec424 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -235,6 +235,8 @@ phitoduck: - {link_type: issues, type: Bug reports} qubidt: - {link_type: issues, type: Bug reports} +ranelpadon: + - {link_type: issues, type: Bug reports} rogalski: - {link_type: pulls-author, type: Code} - {link_type: issues, type: Bug reports} From 1af712a7bde830540e7a7bb567c0106a2f5745e8 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:48:02 +0200 Subject: [PATCH 75/77] docs(contr): add @zmeir as a contributor --- contributors.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 606fec424..a51aaf6ff 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -306,3 +306,7 @@ yoursvivek: - {link_type: commits, type: Documentation} zachnorton4C: - {link_type: issues, type: Bug reports} +zmeir: + - {link_type: search-comments, type: Bug reports} + - {link_type: issues, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} From 7e2b63fd476c46d302139f0964b9cc3683a1c7c7 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:48:41 +0200 Subject: [PATCH 76/77] docs(contr): add more link types for existing contributors --- contributors.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index a51aaf6ff..2dd8e8742 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -76,6 +76,7 @@ akaihola: - {link_type: search-comments, type: Bug reports} - {link_type: pulls-author, type: Code} - {link_type: search-comments, type: Reviewed Pull Requests} + - {link_type: search-discussions, type: Bug reports} aljazerzen: - {link_type: commits, type: Code} anakinxc: @@ -227,6 +228,9 @@ njhuffman: - {link_type: commits, type: Code} okuuva: - {link_type: search-comments, type: Bug reports} + - {link_type: issues, type: Bug reports} + - {link_type: pulls-author, type: Code} + - {link_type: search-comments, type: Reviewed Pull Requests} overratedpro: - {link_type: issues, type: Bug reports} philipgian: @@ -257,6 +261,7 @@ sgaist: shane-kearns: - {link_type: issues, type: Bug reports} - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} shangxiao: - {link_type: pulls-author, type: Code} - {link_type: issues, type: Bug reports} @@ -264,14 +269,18 @@ sherbie: - {link_type: pulls-reviewed, type: Reviewed Pull Requests} simgunz: - {link_type: search-comments, type: Reviewed Pull Requests} + - {link_type: search-discussions, type: Bug reports} simonf-dev: - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} soxofaan: - {link_type: pulls-author, type: Documentation} strzonnek: - {link_type: issues, type: Bug reports} talhajunaidd: - {link_type: commits, type: Code} + - {link_type: search-comments, type: Bug reports} + - {link_type: search-comments, type: Reviewed Pull Requests} tapted: - {link_type: issues, type: Bug reports} tehunter: From b4fb2c50e39e77ccf726eb9c0fc420641a026a9f Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:49:24 +0200 Subject: [PATCH 77/77] docs(contr): update human-readable contributor lists --- CONTRIBUTORS.rst | 13 ++ README.rst | 534 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 386 insertions(+), 161 deletions(-) diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 85896699a..3ca77895a 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -13,14 +13,18 @@ - Alfred Kedhammar (@kedhammar) - Aljaž Mur Eržen (@aljazerzen) - Antti Kaihola (@akaihola) +- Artem Uk (@artel1992) - Ashblaze (@Ashblaze) - August Masquelier (@levouh) - Axel Dahlberg (@AckslD) - Bao (@baod-rate) +- Bao (@baodrate) - Bao (@qubidt) - Ben Falk (@falkben) +- Bharat (@brtkwr) - Bharat Kunwar (@brtknr) - Brandon Perkins (@bdperkin) +- Brett Cannon (@brettcannon) - Carsten Kraus (@casio) - Cedric (@mrfroggg) - Chmouel Boudjnah (@chmouel) @@ -45,13 +49,16 @@ - Felix von Drigalski (@felixvd) - Filippos Giannakos (@philipgian) - Fox_white (@foxwhite25) +- Garfounkel (@Garfounkel) - Georges Discry (@gdiscry) - Gergely Polonkai (@gergelypolonkai) - Giel van Schijndel (@muggenhor) +- Hao Hu (@haohu321) - Hugo Dupras (@jabesq) - Hugo van Kemenade (@hugovk) - Iryna (@irynahryshanovich) - Jairo Llopis (@yajo) +- Jannick Kremer (@DeinAlptraum) - Jasleen Kaur (@jasleen19) - Jens Diemer (@jedie) - Jens Hedegaard Nielsen (@jenshnielsen) @@ -82,6 +89,7 @@ - Paul Ivanov (@ivanov) - Peter Gessler (@gesslerpd) - Philipp A. (@flying-sheep) +- ranelpadon (@ranelpadon) - Rishi Kumar Ray (@RishiKumarRay) - Roberto Polli (@ioggstream) - Ronie Martinez (@roniemartinez) @@ -91,6 +99,7 @@ - Sarah (@seweissman) - Sean Hammond (@sherbie) - sfoucek (@simonf-dev) +- shane-kearns (@shane-kearns) - Shantanu (@hauntsaninja) - Simone Gaiarin (@simgunz) - Stefaan Lippens (@soxofaan) @@ -98,6 +107,7 @@ - Sven Steinbauer (@Svenito) - Talha Juanid (@talhajunaidd) - Thomas Güttler (@guettli) +- Thomas H (@tehunter) - Tim Clephas (@Timple) - TJ Kolleh (@tkolleh) - Tobias Diez (@tobiasdiez) @@ -107,6 +117,9 @@ - Vivek Kushwaha (@yoursvivek) - Will (@Hainguyen1210) - Will Pimblett (@wjdp) +- Will-Ruddick (@Will-Ruddick) - William Bos (@wpnbos) +- Yancheng Zheng (@anakinxc) - Zach Norton (@zachnorton4C) +- Zohar Meir (@zmeir) - Łukasz Rogalski (@rogalski) diff --git a/README.rst b/README.rst index 9e6a81e50..7a79d3d6c 100644 --- a/README.rst +++ b/README.rst @@ -986,7 +986,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -997,7 +997,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1008,8 +1008,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 - 🐛 + 🐛 + 🐛 @@ -1020,7 +1020,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 @@ -1031,12 +1031,31 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💬 - 💻 - 📖 - 👀 - 🚧 + 💬 + 💻 + 📖 + 👀 + 🚧 + 🐛 + 🐛 + 💻 + 👀 + 🐛 + + + @artel1992 +
+ + Artem Uk + +
+
+ 🐛 + 🐛 + + + @Ashblaze @@ -1046,10 +1065,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 - - @levouh @@ -1059,8 +1076,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 🐛 + 💻 + 🐛 @@ -1071,7 +1088,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 @@ -1082,7 +1100,20 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 + + + + @baodrate +
+ + Bao + +
+
+ 🐛 + 👀 + 💻 @@ -1093,8 +1124,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + @falkben @@ -1104,8 +1137,19 @@ Thanks goes to these wonderful people (`emoji key`_):
- 📖 - 🐛 + 📖 + 🐛 + + + + @brtkwr +
+ + Bharat + +
+
+ 🐛 @@ -1116,10 +1160,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 👀 + 👀 - - @bdperkin @@ -1129,7 +1171,18 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + + + @brettcannon +
+ + Brett Cannon + +
+
+ 🐛 @@ -1140,8 +1193,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + @mrfroggg @@ -1151,7 +1206,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1162,8 +1217,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 🐛 + 💻 + 🐛 @@ -1174,7 +1229,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 @@ -1185,11 +1240,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 🐛 + 💻 + 🐛 - - @KangOl @@ -1199,7 +1252,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1210,10 +1263,13 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 📖 - 👀 + 💻 + 📖 + 👀 + 🐛 + + @dkeraudren @@ -1223,7 +1279,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1234,7 +1290,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1245,7 +1301,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1256,11 +1312,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 🐛 + 💻 + 🐛 - - @dhrvjha @@ -1270,8 +1324,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 - 💻 + 🐛 + 💻 @@ -1282,8 +1336,11 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + + @k-dominik @@ -1293,8 +1350,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 - 🐛 + 🐛 + 🐛 + 🐛 @@ -1305,7 +1363,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1316,7 +1374,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 @@ -1327,10 +1386,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 - - @Eyobkibret15 @@ -1340,7 +1397,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1351,8 +1408,13 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 + 🐛 + 🐛 + 👀 + + @philipgian @@ -1362,7 +1424,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 @@ -1373,7 +1435,18 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + + + @Garfounkel +
+ + Garfounkel + +
+
+ 🐛 @@ -1384,7 +1457,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 @@ -1395,10 +1468,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 - - @muggenhor @@ -1408,7 +1479,21 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 + + + + + + @haohu321 +
+ + Hao Hu + +
+
+ 🐛 + 🐛 @@ -1419,8 +1504,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 🐛 + 💻 + 🐛 @@ -1431,7 +1516,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 + 🐛 + 🐛 + 👀 @@ -1442,7 +1530,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1453,8 +1541,22 @@ Thanks goes to these wonderful people (`emoji key`_):
- 👀 + 👀 + + + + @DeinAlptraum +
+ + Jannick Kremer + +
+
+ 🐛 + 🐛 + + @jasleen19 @@ -1464,11 +1566,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 - 👀 + 🐛 + 👀 - - @jedie @@ -1478,7 +1578,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + 💻 @@ -1489,7 +1591,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1500,7 +1602,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + 🐛 @@ -1511,7 +1615,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 💻 @@ -1522,8 +1627,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + @Krischtopp @@ -1533,10 +1640,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 - - @leotrs @@ -1546,7 +1651,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1557,7 +1662,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1568,7 +1673,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1579,7 +1684,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1590,8 +1695,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + @Carreau @@ -1601,12 +1708,14 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 📖 - 👀 + 💻 + 📖 + 👀 + 🐛 + 🐛 + 💻 + 👀 - - @MatthijsBurgh @@ -1616,7 +1725,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + 👀 + 💻 @@ -1627,7 +1739,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 @@ -1638,7 +1750,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1649,9 +1761,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 📖 - 👀 + 💻 + 📖 + 👀 + 🐛 @@ -1662,9 +1775,11 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 - 💻 + 🐛 + 💻 + + @wasdee @@ -1674,10 +1789,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 - - @Pacu2 @@ -1687,8 +1801,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 👀 + 💻 + 👀 @@ -1699,7 +1813,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1710,9 +1824,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 🐛 - 👀 + 💻 + 🐛 + 👀 @@ -1723,7 +1837,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 @@ -1734,8 +1849,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + @RishiKumarRay @@ -1745,10 +1862,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 - - @ioggstream @@ -1758,7 +1873,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1769,7 +1884,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1780,7 +1895,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1791,7 +1906,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 + 🐛 @@ -1802,8 +1918,11 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + + @sherbie @@ -1813,10 +1932,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 👀 + 👀 - - @hauntsaninja @@ -1826,7 +1943,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1837,7 +1954,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 👀 + 👀 + 🐛 @@ -1848,7 +1966,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 📖 + 📖 @@ -1859,7 +1977,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1870,8 +1988,13 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 + 🐛 + 🐛 + 👀 + + @tkolleh @@ -1881,10 +2004,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 - - @talhajunaidd @@ -1894,7 +2015,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 + 💻 + 🐛 + 👀 @@ -1905,7 +2028,20 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + + + + @tehunter +
+ + Thomas H + +
+
+ 🐛 + 💻 @@ -1916,7 +2052,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 👀 @@ -1928,6 +2065,8 @@ Thanks goes to these wonderful people (`emoji key`_):
+ + @tapted @@ -1937,7 +2076,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1948,10 +2087,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 - - @victorcui96 @@ -1961,7 +2098,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1972,8 +2109,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 - 📖 + 🐛 + 📖 @@ -1984,7 +2121,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -1995,8 +2132,21 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 - 📖 + 🐛 + 📖 + + + + + + @Will-Ruddick +
+ + Will-Ruddick + +
+
+ 🐛 @@ -2007,7 +2157,18 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + + + @anakinxc +
+ + Yancheng Zheng + +
+
+ 🐛 @@ -2018,10 +2179,21 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + + + @zmeir +
+ + Zohar Meir + +
+
+ 🐛 + 🐛 + 👀 - - @clintonsteiner @@ -2031,8 +2203,13 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + 💻 + 👀 + + @deadkex @@ -2042,7 +2219,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 @@ -2053,7 +2231,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -2064,7 +2242,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -2075,7 +2253,7 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 @@ -2086,11 +2264,11 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 👀 + 💻 + 👀 + 🐛 + 👀 - - @mayk0gan @@ -2100,8 +2278,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + @okuuva @@ -2111,7 +2291,10 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 🐛 + 💻 + 👀 @@ -2122,7 +2305,18 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + + + + @ranelpadon +
+ + ranelpadon + +
+
+ 🐛 @@ -2133,7 +2327,8 @@ Thanks goes to these wonderful people (`emoji key`_):
- 👀 + 👀 + 🐛 @@ -2144,8 +2339,24 @@ Thanks goes to these wonderful people (`emoji key`_):
- 🐛 + 🐛 + 👀 + + + + @shane-kearns +
+ + shane-kearns + +
+
+ 🐛 + 🐛 + 👀 + + @rogalski @@ -2155,8 +2366,9 @@ Thanks goes to these wonderful people (`emoji key`_):
- 💻 - 🐛 + 💻 + 🐛 + 🐛