Repository: psf/black

Issue:
Error Path handling: `ValueError: '//src' does not start with '/'`
This happens within a docker container running at the root level.

For context:

```
root@2d592a60ac50:/# echo $PWD
/

root@2d592a60ac50:/# ls -l src/lib3to6/
total 160
-rw-r--r-- 1 root root   428 Aug 26 17:34 __init__.py
-rwxr-xr-x 1 root root  4181 Aug 26 17:34 __main__.py
...

root@2d592a60ac50:/# /opt/conda/envs/lib3to6_py38/bin/black --version
black, version 20.8b1

root@2d592a60ac50:/# /opt/conda/envs/lib3to6_py38/bin/python --version
Python 3.8.5

root@2d592a60ac50:/# uname -a
Linux 2d592a60ac50 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 GNU/Linux
```

When I run `black src/` I get this error.

```
root@2d592a60ac50:/# /opt/conda/envs/lib3to6_py38/bin/black src/
Traceback (most recent call last):
  File "/opt/conda/envs/lib3to6_py38/bin/black", line 8, in <module>
    sys.exit(patched_main())
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/black/__init__.py", line 6607, in patched_main
    main()
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/click/decorators.py", line 21, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/black/__init__.py", line 528, in main
    sources = get_sources(
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/black/__init__.py", line 604, in get_sources
    sources.update(
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/black/__init__.py", line 5868, in gen_python_files
    normalized_path = normalize_path_maybe_ignore(child, root, report)
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/site-packages/black/__init__.py", line 5835, in normalize_path_maybe_ignore
    normalized_path = path.resolve().relative_to(root).as_posix()
  File "/opt/conda/envs/lib3to6_py38/lib/python3.8/pathlib.py", line 904, in relative_to
    raise ValueError("{!r} does not start with {!r}"
ValueError: '//src/lib3to6' does not start with '/'
```

It works fine if I run it with the absolute path `black /src/`.

```
root@2d592a60ac50:/# /opt/conda/envs/lib3to6_py38/bin/black /src/
reformatted /src/lib3to6/__init__.py
reformatted /src/lib3to6/fixer_base.py
...
All done! ✨ 🍰 ✨
16 files reformatted, 1 file left unchanged.
```

Editing the local file, I see these values in the function:

```
root: PosixPath('/')
path: PosixPath('src/lib3to6')
path.resolve(): PosixPath('//src/lib3to6')
```

I feel very stupid saying this, but this may be an issue with the `pathlib.Path.resolve` function. You may nonetheless wish to implement a workaround by calling `path.absolute()` first:

```
root@2d592a60ac50:/# /opt/conda/envs/lib3to6_py38/bin/ipython
Python 3.8.5 | packaged by conda-forge | (default, Aug 21 2020, 18:21:27)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.17.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from pathlib import Path

In [2]: Path("src")
Out[2]: PosixPath('src')

In [3]: Path("src").resolve()
Out[3]: PosixPath('//src')

In [4]: Path("src").resolve().absolute()
Out[4]: PosixPath('//src')

In [5]: Path("src").absolute().resolve()
Out[5]: PosixPath('/src')

In [6]: root = Path("/")

In [7]: Path("src").absolute().resolve().relative_to(root)
Out[7]: PosixPath('src')
```


Discussion:
I see the `Path.absolute()` function is undocumented and the alternative is to use this `Path.cwd()`. So perhaps something like this:

```python
def normalize_path_maybe_ignore(
    path: Path, root: Path, report: "Report"
) -> Optional[str]:
    """Normalize `path`. May return `None` if `path` was ignored.

    `report` is where "path ignored" output goes.
    """
    try:
        abspath = path if path.is_absolute() else Path.cwd() / path
        normalized_path = abspath.resolve().relative_to(root).as_posix()
    except OSError as e:
        report.path_ignored(path, f"cannot be read because {e}")
        return None

```

Related python issue: https://bugs.python.org/issue33660. Whatever the WA will be, it might be worth mentioning that in a comment.

Last time Łukasz made the following WA: 75d2af2e3a0d0cf85b1e0c510c626d1ee1938074. This case certainly craves for a test.

Sigh, that's frustrating. Thank you for jumping in @mbarkhau. I'll take care of BPO-33660 but for the time being we'll need an internal workaround. Care to add a PR to Black for that?
Would be happy to. I'll go forward with the above change and see if I can add a test.

Resolving patch:
diff --git a/src/black/__init__.py b/src/black/__init__.py
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -5831,7 +5831,8 @@ def normalize_path_maybe_ignore(
     `report` is where "path ignored" output goes.
     """
     try:
-        normalized_path = path.resolve().relative_to(root).as_posix()
+        abspath = path if path.is_absolute() else Path.cwd() / path
+        normalized_path = abspath.resolve().relative_to(root).as_posix()
     except OSError as e:
         report.path_ignored(path, f"cannot be read because {e}")
         return None


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -9,6 +9,7 @@
 from io import BytesIO, TextIOWrapper
 import os
 from pathlib import Path
+from platform import system
 import regex as re
 import sys
 from tempfile import TemporaryDirectory
@@ -1939,6 +1940,23 @@ def test_find_project_root(self) -> None:
             self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
             self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
 
+    def test_bpo_33660_workaround(self) -> None:
+        if system() == "Windows":
+            return
+
+        # https://bugs.python.org/issue33660
+
+        old_cwd = Path.cwd()
+        try:
+            root = Path("/")
+            os.chdir(str(root))
+            path = Path("workspace") / "project"
+            report = black.Report(verbose=True)
+            normalized_path = black.normalize_path_maybe_ignore(path, root, report)
+            self.assertEqual(normalized_path, "workspace/project")
+        finally:
+            os.chdir(str(old_cwd))
+
 
 class BlackDTestCase(AioHTTPTestCase):
     async def get_application(self) -> web.Application: