Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop black dependency #759

Merged
merged 10 commits into from
Jan 1, 2025
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down
23 changes: 16 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

..
Expand All @@ -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 <myfile.py>`` or ``darker <directory>`` command
reads the original file(s),
formats them using Black_,
Expand Down Expand Up @@ -478,7 +480,7 @@ PyCharm/IntelliJ IDEA

1. Install ``darker``::

$ pip install darker
$ pip install 'darker[black]'

2. Locate your ``darker`` installation folder.

Expand Down Expand Up @@ -540,7 +542,7 @@ Visual Studio Code

1. Install ``darker``::

$ pip install darker
$ pip install 'darker[black]'

2. Locate your ``darker`` installation folder.

Expand Down Expand Up @@ -683,16 +685,20 @@ 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
- repo: https://github.com/akaihola/darker
rev: v2.1.1
hooks:
- id: darker
args: [--isort]
args:
- --formatter=none
- --isort
additional_dependencies:
- isort~=5.9
Expand Down Expand Up @@ -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
===================
Expand Down
2 changes: 1 addition & 1 deletion action/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]}"
Expand Down
16 changes: 9 additions & 7 deletions action/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -208,15 +210,15 @@ 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,
encoding="utf-8",
)
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)

Expand Down
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down
3 changes: 1 addition & 2 deletions src/darker/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/darker/configuration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Configuration and command line handling."""
19 changes: 19 additions & 0 deletions src/darker/configuration/target_version.py
Original file line number Diff line number Diff line change
@@ -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
153 changes: 111 additions & 42 deletions src/darker/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -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)
Loading
Loading