Repository: psf/black

Issue:
Output claims the configuration came from project root (filesystem root) when in reality it was user-level config.
**Describe the bug**

The verbose would claim that the filesystem root was identified as the project root (this is correct) and the configuration file came from `/pyproject.toml` (this is false). 

**To Reproduce**

- Create a test file in a directory where is no pyproject.toml present (up the filesystem root)
- Format it with Black with `--verbose`
- Create a [user level config file](https://black.readthedocs.io/en/latest/usage_and_configuration/the_basics.html#where-black-looks-for-the-file) and configure line length to be something very small
- Reformat using the same command and observe the wrong output

```console
❯ black test.py --check -v
Identified `/` as project root containing a file system root.
Sources to be formatted: "home/ichard26/programming/oss/test.py"
test.py already well formatted, good job.

All done! ✨ 🍰 ✨
1 file would be left unchanged.

❯ echo "[tool.black]
  line-length = 10" > ~/.config/black

❯ black test.py --check -v
Identified `/` as project root containing a file system root.
Sources to be formatted: "home/ichard26/programming/oss/test.py"
Using configuration from project root.
would reformat test.py

Oh no! 💥 💔 💥
1 file would be reformatted.
```

**Expected behavior**

That the output would report either it came from user level configuration or at least not claim it was project configuration 'cause it's not.

**Environment**

- Black's version: 22.1.0 and d038a24ca200da9dacc1dcb05090c9e5b45b7869
- OS and Python version: CPython 3.8.5 under Ubuntu 20.04.03 LTS

**Additional context**

This made it _very_ confusing to debug an unrelated test suite issue :)

cc @Shivansh-007 

Discussion:
github-actions[bot]: **diff-shades** reports zero changes comparing this PR (0eaa13345ab7bffdb493728a597d5f14d7df8cb8) to main (31fe97e7ce1055debaa54bed9c63e252508a9a75).

---

[**What is this?**](https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades) | [Workflow run](https://github.com/psf/black/actions/runs/1785205203) | [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 4ad9e53..de85bd8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -20,6 +20,7 @@
 
 - Do not format `__pypackages__` directories by default (#2836)
 - Add support for specifying stable version with `--required-version` (#2832).
+- Avoid crashing when the user has no homedir (#2814)
 
 ### Documentation
 
@@ -34,6 +35,8 @@
 
 <!-- Changes to Black's terminal output and error messages -->
 
+- In verbose, mode, log when _Black_ is using user-level config (#2861)
+
 ### Packaging
 
 <!-- Changes to how Black is packaged, such as dependency requirements -->
diff --git a/README.md b/README.md
index eda07b1..8ba9d6c 100644
--- a/README.md
+++ b/README.md
@@ -130,10 +130,10 @@ code in compliance with many other _Black_ formatted projects.
 ## Used by
 
 The following notable open-source projects trust _Black_ with enforcing a consistent
-code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy,
-Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow,
-Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro,
-OpenOA, FLORIS, ORBIT, WOMBAT, and many more.
+code style: pytest, tox, Pyramid, Django, Django Channels, Hypothesis, attrs,
+SQLAlchemy, Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv),
+pandas, Pillow, Twisted, LocalStack, every Datadog Agent Integration, Home Assistant,
+Zulip, Kedro, OpenOA, FLORIS, ORBIT, WOMBAT, and many more.
 
 The following organizations use _Black_: Facebook, Dropbox, KeepTruckin, Mozilla, Quora,
 Duolingo, QuantumBlack, Tesla.
diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md
index 9938d81..bde99f7 100644
--- a/docs/guides/using_black_with_other_tools.md
+++ b/docs/guides/using_black_with_other_tools.md
@@ -210,7 +210,7 @@ mixed feelings about _Black_'s formatting style.
 #### Configuration
 
 ```
-disable = C0330, C0326
+disable = C0326, C0330
 max-line-length = 88
 ```
 
@@ -243,7 +243,7 @@ characters via `max-line-length = 88`.
 
 ```ini
 [MESSAGES CONTROL]
-disable = C0330, C0326
+disable = C0326, C0330
 
 [format]
 max-line-length = 88
@@ -259,7 +259,7 @@ max-line-length = 88
 max-line-length = 88
 
 [pylint.messages_control]
-disable = C0330, C0326
+disable = C0326, C0330
 ```
 
 </details>
@@ -269,7 +269,7 @@ disable = C0330, C0326
 
 ```toml
 [tool.pylint.messages_control]
-disable = "C0330, C0326"
+disable = "C0326, C0330"
 
 [tool.pylint.format]
 max-line-length = "88"
diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md
index 5d2f83a..1c7879b 100644
--- a/docs/integrations/editors.md
+++ b/docs/integrations/editors.md
@@ -189,10 +189,13 @@ If you need to do anything special to make your virtualenv work and install _Bla
 example you want to run a version from main), create a virtualenv manually and point
 `g:black_virtualenv` to it. The plugin will use it.
 
-To run _Black_ on save, add the following line to `.vimrc` or `init.vim`:
+To run _Black_ on save, add the following lines to `.vimrc` or `init.vim`:
 
 ```
-autocmd BufWritePre *.py execute ':Black'
+augroup black_on_save
+  autocmd!
+  autocmd BufWritePre *.py Black
+augroup end
 ```
 
 To run _Black_ on a key press (e.g. F9 below), add this:
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 01fea69..0b68542 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -3,5 +3,5 @@
 myst-parser==0.16.1
 Sphinx==4.4.0
 sphinxcontrib-programoutput==0.17
-sphinx_copybutton==0.4.0
+sphinx_copybutton==0.5.0
 furo==2022.1.2
diff --git a/docs/usage_and_configuration/file_collection_and_discovery.md b/docs/usage_and_configuration/file_collection_and_discovery.md
index bd90ccc..de1d5e6 100644
--- a/docs/usage_and_configuration/file_collection_and_discovery.md
+++ b/docs/usage_and_configuration/file_collection_and_discovery.md
@@ -24,8 +24,8 @@ as .pyi, and whether string normalization was omitted.
 
 To override the location of these files on all systems, set the environment variable
 `BLACK_CACHE_DIR` to the preferred location. Alternatively on macOS and Linux, set
-`XDG_CACHE_HOME` to you your preferred location. For example, if you want to put the
-cache in the directory you're running _Black_ from, set `BLACK_CACHE_DIR=.cache/black`.
+`XDG_CACHE_HOME` to your preferred location. For example, if you want to put the cache
+in the directory you're running _Black_ from, set `BLACK_CACHE_DIR=.cache/black`.
 _Black_ will then write the above files to `.cache/black`. Note that `BLACK_CACHE_DIR`
 will take precedence over `XDG_CACHE_HOME` if both are set.
 
diff --git a/src/black/__init__.py b/src/black/__init__.py
index 8c28b6b..b7bf822 100644
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -49,7 +49,12 @@
 from black.concurrency import cancel, shutdown, maybe_install_uvloop
 from black.output import dump_to_file, ipynb_diff, diff, color_diff, out, err
 from black.report import Report, Changed, NothingChanged
-from black.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
+from black.files import (
+    find_project_root,
+    find_pyproject_toml,
+    parse_pyproject_toml,
+    find_user_pyproject_toml,
+)
 from black.files import gen_python_files, get_gitignore, normalize_path_maybe_ignore
 from black.files import wrap_stream_for_windows
 from black.parsing import InvalidInput  # noqa F401
@@ -402,7 +407,7 @@ def validate_regex(
     help="Read configuration from FILE path.",
 )
 @click.pass_context
-def main(
+def main(  # noqa: C901
     ctx: click.Context,
     code: Optional[str],
     line_length: int,
@@ -469,7 +474,17 @@ def main(
 
         if config:
             config_source = ctx.get_parameter_source("config")
-            if config_source in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP):
+            user_level_config = str(find_user_pyproject_toml())
+            if config == user_level_config:
+                out(
+                    f"Using configuration from user-level config at "
+                    f"'{user_level_config}'.",
+                    fg="blue",
+                )
+            elif config_source in (
+                ParameterSource.DEFAULT,
+                ParameterSource.DEFAULT_MAP,
+            ):
                 out("Using configuration from project root.", fg="blue")
             else:
                 out(f"Using configuration in '{config}'.", fg="blue")
diff --git a/src/black/files.py b/src/black/files.py
index 18c8423..8348e0d 100644
--- a/src/black/files.py
+++ b/src/black/files.py
@@ -87,7 +87,7 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
             if path_user_pyproject_toml.is_file()
             else None
         )
-    except PermissionError as e:
+    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
@@ -111,6 +111,10 @@ def find_user_pyproject_toml() -> Path:
 
     This looks for ~\.black on Windows and ~/.config/black on Linux and other
     Unix systems.
+
+    May raise:
+    - RuntimeError: if the current user has no homedir
+    - PermissionError: if the current process cannot access the user's homedir
     """
     if sys.platform == "win32":
         # Windows


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
index cd38d9e..82abd47 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -10,7 +10,7 @@
 import types
 import unittest
 from concurrent.futures import ThreadPoolExecutor
-from contextlib import contextmanager
+from contextlib import contextmanager, redirect_stderr
 from dataclasses import replace
 from io import BytesIO
 from pathlib import Path
@@ -1358,6 +1358,21 @@ def test_find_project_root(self) -> None:
                 (src_dir.resolve(), "pyproject.toml"),
             )
 
+    @patch(
+        "black.files.find_user_pyproject_toml",
+    )
+    def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
+        find_user_pyproject_toml.side_effect = RuntimeError()
+
+        with redirect_stderr(io.StringIO()) as stderr:
+            result = black.files.find_pyproject_toml(
+                path_search_start=(str(Path.cwd().root),)
+            )
+
+        assert result is None
+        err = stderr.getvalue()
+        assert "Ignoring user configuration" in err
+
     @patch(
         "black.files.find_user_pyproject_toml",
         black.files.find_user_pyproject_toml.__wrapped__,
diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py
index d78a68c..473047a 100644
--- a/tests/test_ipynb.py
+++ b/tests/test_ipynb.py
@@ -24,6 +24,8 @@
 
 JUPYTER_MODE = Mode(is_ipynb=True)
 
+EMPTY_CONFIG = DATA_DIR / "empty_pyproject.toml"
+
 runner = CliRunner()
 
 
@@ -410,6 +412,7 @@ def test_ipynb_diff_with_change() -> None:
         [
             str(DATA_DIR / "notebook_trailing_newline.ipynb"),
             "--diff",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
@@ -422,6 +425,7 @@ def test_ipynb_diff_with_no_change() -> None:
         [
             str(DATA_DIR / "notebook_without_changes.ipynb"),
             "--diff",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
     expected = "1 file would be left unchanged."
@@ -440,13 +444,17 @@ def test_cache_isnt_written_if_no_jupyter_deps_single(
     monkeypatch.setattr(
         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
     )
-    result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")])
+    result = runner.invoke(
+        main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
+    )
     assert "No Python files are present to be formatted. Nothing to do" in result.output
     jupyter_dependencies_are_installed.cache_clear()
     monkeypatch.setattr(
         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
     )
-    result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")])
+    result = runner.invoke(
+        main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
+    )
     assert "reformatted" in result.output
 
 
@@ -462,13 +470,13 @@ def test_cache_isnt_written_if_no_jupyter_deps_dir(
     monkeypatch.setattr(
         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
     )
-    result = runner.invoke(main, [str(tmp_path)])
+    result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
     assert "No Python files are present to be formatted. Nothing to do" in result.output
     jupyter_dependencies_are_installed.cache_clear()
     monkeypatch.setattr(
         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
     )
-    result = runner.invoke(main, [str(tmp_path)])
+    result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
     assert "reformatted" in result.output
 
 
@@ -483,6 +491,7 @@ def test_ipynb_flag(tmp_path: pathlib.Path) -> None:
             str(tmp_nb),
             "--diff",
             "--ipynb",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
@@ -498,6 +507,7 @@ def test_ipynb_and_pyi_flags() -> None:
             "--pyi",
             "--ipynb",
             "--diff",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
     assert isinstance(result.exception, SystemExit)