]> git.madduck.net Git - etc/vim.git/commitdiff

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Support pyproject.toml
authorŁukasz Langa <lukasz@langa.pl>
Wed, 6 Jun 2018 22:30:02 +0000 (15:30 -0700)
committerŁukasz Langa <lukasz@langa.pl>
Thu, 7 Jun 2018 00:02:10 +0000 (17:02 -0700)
Fixes #65

README.md
black.py
docs/index.rst
docs/pyproject_toml.md [new symlink]
pyproject.toml [new file with mode: 0644]
tests/data/empty_pyproject.toml [new file with mode: 0644]
tests/test_black.py

index 00179a93198d4d1dfebff5bd5fac8f3c5c2a52a0..831401b3d256d8af2b6d002b619c4ef1e52e7771 100644 (file)
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ possible.
 
 *Contents:* **[Installation and usage](#installation-and-usage)** |
 **[The *Black* code style](#the-black-code-style)** |
 
 *Contents:* **[Installation and usage](#installation-and-usage)** |
 **[The *Black* code style](#the-black-code-style)** |
+**[pyproject.toml](#pyproject.toml)** |
 **[Editor integration](#editor-integration)** |
 **[Version control integration](#version-control-integration)** |
 **[Ignoring unmodified files](#ignoring-unmodified-files)** |
 **[Editor integration](#editor-integration)** |
 **[Version control integration](#version-control-integration)** |
 **[Ignoring unmodified files](#ignoring-unmodified-files)** |
@@ -103,6 +104,7 @@ Options:
                               that were not changed or were ignored due to
                               --exclude=.
   --version                   Show the version and exit.
                               that were not changed or were ignored due to
                               --exclude=.
   --version                   Show the version and exit.
+  --config PATH               Read configuration from PATH.
   --help                      Show this message and exit.
 ```
 
   --help                      Show this message and exit.
 ```
 
@@ -487,6 +489,98 @@ a future version of the formatter:
 * use `float` instead of `Union[int, float]`.
 
 
 * use `float` instead of `Union[int, float]`.
 
 
+## pyproject.toml
+
+*Black* is able to read project-specific default values for its
+command line options from a `pyproject.toml` file.  This is
+especially useful for specifying custom `--include` and `--exclude`
+patterns for your project.
+
+**Pro-tip**: If you're asking yourself "Do I need to configure anything?"
+the answer is "No".  *Black* is all about sensible defaults.
+
+
+### What on Earth is a `pyproject.toml` file?
+
+[PEP 518](https://www.python.org/dev/peps/pep-0518/) defines
+`pyproject.toml` as a configuration file to store build system
+requirements for Python projects.  With the help of tools
+like [Poetry](https://poetry.eustace.io/) or
+[Flit](https://flit.readthedocs.io/en/latest/) it can fully replace the
+need for `setup.py` and `setup.cfg` files.
+
+
+### Where *Black* looks for the file
+
+By default *Black* looks for `pyproject.toml` starting from the common
+base directory of all files and directories passed on the command line.
+If it's not there, it looks in parent directories.  It stops looking
+when it finds the file, or a `.git` directory, or a `.hg` directory,
+or the root of the file system, whichever comes first.
+
+If you're formatting standard input, *Black* will look for configuration
+starting from the current working directory.
+
+You can also explicitly specify the path to a particular file that you
+want with `--config`.  In this situation *Black* will not look for any
+other file.
+
+If you're running with `--verbose`, you will see a blue message if
+a file was found and used.
+
+
+### Configuration format
+
+As the file extension suggests, `pyproject.toml` is a [TOML](https://github.com/toml-lang/toml) file.  It contains separate
+sections for different tools.  *Black* is using the `[tool.black]`
+section.  The option keys are the same as long names of options on
+the command line.
+
+Note that you have to use single-quoted strings in TOML for regular
+expressions. It's the equivalent of r-strings in Python.  Multiline
+strings are treated as verbose regular expressions by Black.  Use `[ ]`
+to denote a significant space character.
+
+<details>
+<summary>Example `pyproject.toml`</summary>
+
+```toml
+[tool.black]
+line-length = 88
+py36 = true
+include = '\.pyi?$'
+exclude = '''
+/(
+    \.git
+  | \.hg
+  | \.mypy_cache
+  | \.tox
+  | \.venv
+  | _build
+  | buck-out
+  | build
+  | dist
+
+  # The following are specific to Black, you probably don't want those.
+  | blib2to3
+  | tests/data
+)/
+'''
+```
+
+</details>
+
+### Lookup hierarchy
+
+Command-line options have defaults that you can see in `--help`.
+A `pyproject.toml` can override those defaults.  Finally, options
+provided by the user on the command line override both.
+
+*Black* will only ever use one `pyproject.toml` file during an entire
+run. It doesn't look for multiple files, and doesn't compose
+configuration from different levels of the file hierarchy.
+
+
 ## Editor integration
 
 ### Emacs
 ## Editor integration
 
 ### Emacs
@@ -632,16 +726,18 @@ repos:
     rev: stable
     hooks:
     - id: black
     rev: stable
     hooks:
     - id: black
-      args: [--line-length=88, --safe]
       language_version: python3.6
 ```
 Then run `pre-commit install` and you're ready to go.
 
       language_version: python3.6
 ```
 Then run `pre-commit install` and you're ready to go.
 
-`args` in the above config is optional but shows you how you can change
-the line length if you really need to.  If you're already using Python
-3.7, switch the `language_version` accordingly. Finally, `stable` is a tag
-that is pinned to the latest release on PyPI.  If you'd rather run on
-master, this is also an option.
+Avoid using `args` in the hook.  Instead, store necessary configuration
+in `pyproject.toml` so that editors and command-line usage of Black all
+behave consistently for your project.  See *Black*'s own `pyproject.toml`
+for an example.
+
+If you're already using Python 3.7, switch the `language_version`
+accordingly. Finally, `stable` is a tag that is pinned to the latest
+release on PyPI.  If you'd rather run on master, this is also an option.
 
 
 ## Ignoring unmodified files
 
 
 ## Ignoring unmodified files
@@ -714,6 +810,8 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md).
 
 ### 18.6b2
 
 
 ### 18.6b2
 
+* added `--config` (#65)
+
 * fixed improper unmodified file caching when `-S` was used
 
 
 * fixed improper unmodified file caching when `-S` was used
 
 
@@ -1039,18 +1137,3 @@ Multiple contributions by:
 * [Stavros Korokithakis](mailto:hi@stavros.io)
 * [Sunil Kapil](mailto:snlkapil@gmail.com)
 * [Vishwas B Sharma](mailto:sharma.vishwas88@gmail.com)
 * [Stavros Korokithakis](mailto:hi@stavros.io)
 * [Sunil Kapil](mailto:snlkapil@gmail.com)
 * [Vishwas B Sharma](mailto:sharma.vishwas88@gmail.com)
-
----
-
-*Contents:*
-**[Installation and Usage](#installation-and-usage)** |
-**[The *Black* code style](#the-black-code-style)** |
-**[Editor integration](#editor-integration)** |
-**[Version control integration](#version-control-integration)** |
-**[Ignoring unmodified files](#ignoring-unmodified-files)** |
-**[Testimonials](#testimonials)** |
-**[Show your style](#show-your-style)** |
-**[License](#license)** |
-**[Contributing](#contributing-to-black)** |
-**[Change Log](#change-log)** |
-**[Authors](#authors)**
index 80401f5e5c5e4d2d6cc297351b7564350615b0ed..dd7fe3911e630990cbbd547ee156ff830a37be4e 100644 (file)
--- a/black.py
+++ b/black.py
@@ -3,7 +3,7 @@ from asyncio.base_events import BaseEventLoop
 from concurrent.futures import Executor, ProcessPoolExecutor
 from datetime import datetime
 from enum import Enum, Flag
 from concurrent.futures import Executor, ProcessPoolExecutor
 from datetime import datetime
 from enum import Enum, Flag
-from functools import partial, wraps
+from functools import lru_cache, partial, wraps
 import io
 import keyword
 import logging
 import io
 import keyword
 import logging
@@ -38,6 +38,7 @@ from typing import (
 from appdirs import user_cache_dir
 from attr import dataclass, Factory
 import click
 from appdirs import user_cache_dir
 from attr import dataclass, Factory
 import click
+import toml
 
 # lib2to3 fork
 from blib2to3.pytree import Node, Leaf, type_repr
 
 # lib2to3 fork
 from blib2to3.pytree import Node, Leaf, type_repr
@@ -156,6 +157,40 @@ class FileMode(Flag):
         return mode
 
 
         return mode
 
 
+def read_pyproject_toml(
+    ctx: click.Context, param: click.Parameter, value: Union[str, int, bool, None]
+) -> Optional[str]:
+    """Inject Black configuration from "pyproject.toml" into defaults in `ctx`.
+
+    Returns the path to a successfully found and read configuration file, None
+    otherwise.
+    """
+    assert not isinstance(value, (int, bool)), "Invalid parameter type passed"
+    if not value:
+        root = find_project_root(ctx.params.get("src", ()))
+        path = root / "pyproject.toml"
+        if path.is_file():
+            value = str(path)
+        else:
+            return None
+
+    try:
+        pyproject_toml = toml.load(value)
+        config = pyproject_toml.get("tool", {}).get("black", {})
+    except (toml.TomlDecodeError, OSError) as e:
+        raise click.BadOptionUsage(f"Error reading configuration file: {e}", ctx)
+
+    if not config:
+        return None
+
+    if ctx.default_map is None:
+        ctx.default_map = {}
+    ctx.default_map.update(  # type: ignore  # bad types in .pyi
+        {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
+    )
+    return value
+
+
 @click.command()
 @click.option(
     "-l",
 @click.command()
 @click.option(
     "-l",
@@ -257,6 +292,16 @@ class FileMode(Flag):
     type=click.Path(
         exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
     ),
     type=click.Path(
         exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
     ),
+    is_eager=True,
+)
+@click.option(
+    "--config",
+    type=click.Path(
+        exists=False, file_okay=True, dir_okay=False, readable=True, allow_dash=False
+    ),
+    is_eager=True,
+    callback=read_pyproject_toml,
+    help="Read configuration from PATH.",
 )
 @click.pass_context
 def main(
 )
 @click.pass_context
 def main(
@@ -272,26 +317,29 @@ def main(
     verbose: bool,
     include: str,
     exclude: str,
     verbose: bool,
     include: str,
     exclude: str,
-    src: List[str],
+    src: Tuple[str],
+    config: Optional[str],
 ) -> None:
     """The uncompromising code formatter."""
     write_back = WriteBack.from_configuration(check=check, diff=diff)
     mode = FileMode.from_configuration(
         py36=py36, pyi=pyi, skip_string_normalization=skip_string_normalization
     )
 ) -> None:
     """The uncompromising code formatter."""
     write_back = WriteBack.from_configuration(check=check, diff=diff)
     mode = FileMode.from_configuration(
         py36=py36, pyi=pyi, skip_string_normalization=skip_string_normalization
     )
-    report = Report(check=check, quiet=quiet, verbose=verbose)
-    sources: Set[Path] = set()
+    if config and verbose:
+        out(f"Using configuration from {config}.", bold=False, fg="blue")
     try:
     try:
-        include_regex = re.compile(include)
+        include_regex = re_compile_maybe_verbose(include)
     except re.error:
         err(f"Invalid regular expression for include given: {include!r}")
         ctx.exit(2)
     try:
     except re.error:
         err(f"Invalid regular expression for include given: {include!r}")
         ctx.exit(2)
     try:
-        exclude_regex = re.compile(exclude)
+        exclude_regex = re_compile_maybe_verbose(exclude)
     except re.error:
         err(f"Invalid regular expression for exclude given: {exclude!r}")
         ctx.exit(2)
     except re.error:
         err(f"Invalid regular expression for exclude given: {exclude!r}")
         ctx.exit(2)
+    report = Report(check=check, quiet=quiet, verbose=verbose)
     root = find_project_root(src)
     root = find_project_root(src)
+    sources: Set[Path] = set()
     for s in src:
         p = Path(s)
         if p.is_dir():
     for s in src:
         p = Path(s)
         if p.is_dir():
@@ -307,9 +355,8 @@ def main(
         if verbose or not quiet:
             out("No paths given. Nothing to do 😴")
         ctx.exit(0)
         if verbose or not quiet:
             out("No paths given. Nothing to do 😴")
         ctx.exit(0)
-        return
 
 
-    elif len(sources) == 1:
+    if len(sources) == 1:
         reformat_one(
             src=sources.pop(),
             line_length=line_length,
         reformat_one(
             src=sources.pop(),
             line_length=line_length,
@@ -2894,7 +2941,7 @@ def gen_python_files_in_dir(
             normalized_path += "/"
         exclude_match = exclude.search(normalized_path)
         if exclude_match and exclude_match.group(0):
             normalized_path += "/"
         exclude_match = exclude.search(normalized_path)
         if exclude_match and exclude_match.group(0):
-            report.path_ignored(child, f"matches --exclude={exclude.pattern}")
+            report.path_ignored(child, f"matches the --exclude regular expression")
             continue
 
         if child.is_dir():
             continue
 
         if child.is_dir():
@@ -2906,7 +2953,8 @@ def gen_python_files_in_dir(
                 yield child
 
 
                 yield child
 
 
-def find_project_root(srcs: List[str]) -> Path:
+@lru_cache()
+def find_project_root(srcs: Iterable[str]) -> Path:
     """Return a directory containing .git, .hg, or pyproject.toml.
 
     That directory can be one of the directories passed in `srcs` or their
     """Return a directory containing .git, .hg, or pyproject.toml.
 
     That directory can be one of the directories passed in `srcs` or their
@@ -3164,6 +3212,16 @@ def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str:
     return regex.sub(replacement, regex.sub(replacement, original))
 
 
     return regex.sub(replacement, regex.sub(replacement, original))
 
 
+def re_compile_maybe_verbose(regex: str) -> Pattern[str]:
+    """Compile a regular expression string in `regex`.
+
+    If it contains newlines, use verbose mode.
+    """
+    if "\n" in regex:
+        regex = "(?x)" + regex
+    return re.compile(regex)
+
+
 def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]:
     """Like `reversed(enumerate(sequence))` if that were possible."""
     index = len(sequence) - 1
 def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]:
     """Like `reversed(enumerate(sequence))` if that were possible."""
     index = len(sequence) - 1
index 30c24a3ab52810d85294ebe6055bb6ef06b43786..da60f7b2725e9f08fdfac1cfc4769f19e25e1037 100644 (file)
@@ -48,10 +48,12 @@ Contents
 
    installation_and_usage
    the_black_code_style
 
    installation_and_usage
    the_black_code_style
+   pyproject_toml
    editor_integration
    version_control_integration
    ignoring_unmodified_files
    contributing
    editor_integration
    version_control_integration
    ignoring_unmodified_files
    contributing
+   show_your_style
    change_log
    reference/reference_summary
    authors
    change_log
    reference/reference_summary
    authors
diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md
new file mode 120000 (symlink)
index 0000000..57c8664
--- /dev/null
@@ -0,0 +1 @@
+_build/generated/pyproject_toml.md
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644 (file)
index 0000000..cbb530c
--- /dev/null
@@ -0,0 +1,28 @@
+# Example configuration for Black.
+
+# NOTE: you have to use single-quoted strings in TOML for regular expressions.
+# It's the equivalent of r-strings in Python.  Multiline strings are treated as
+# verbose regular expressions by Black.  Use [ ] to denote a significant space
+# character.
+
+[tool.black]
+line-length = 88
+py36 = true
+include = '\.pyi?$'
+exclude = '''
+/(
+    \.git
+  | \.hg
+  | \.mypy_cache
+  | \.tox
+  | \.venv
+  | _build
+  | buck-out
+  | build
+  | dist
+
+  # The following are specific to Black, you probably don't want those.
+  | blib2to3
+  | tests/data
+)/
+'''
diff --git a/tests/data/empty_pyproject.toml b/tests/data/empty_pyproject.toml
new file mode 100644 (file)
index 0000000..8c5274d
--- /dev/null
@@ -0,0 +1,2 @@
+# Empty pyproject.toml to use with some tests that depend on Python 3.6 autodiscovery
+# and so on.
index e555ba96c4bed56967ef811db8e998895d6c0f17..b8af711d384272e4fb1aca9bde6772c9691fe68f 100644 (file)
@@ -176,10 +176,10 @@ class BlackTestCase(unittest.TestCase):
         )
         source, _ = read_data("expression.py")
         expected, _ = read_data("expression.diff")
         )
         source, _ = read_data("expression.py")
         expected, _ = read_data("expression.diff")
+        config = THIS_DIR / "data" / "empty_pyproject.toml"
         stderrbuf = BytesIO()
         stderrbuf = BytesIO()
-        result = BlackRunner(stderrbuf).invoke(
-            black.main, ["-", "--fast", f"--line-length={ll}", "--diff"], input=source
-        )
+        args = ["-", "--fast", f"--line-length={ll}", "--diff", f"--config={config}"]
+        result = BlackRunner(stderrbuf).invoke(black.main, args, input=source)
         self.assertEqual(result.exit_code, 0)
         actual = diff_header.sub("[Deterministic header]", result.output)
         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
         self.assertEqual(result.exit_code, 0)
         actual = diff_header.sub("[Deterministic header]", result.output)
         actual = actual.rstrip() + "\n"  # the diff output has a trailing space