Repository: psf/black
Pull request: Enhance `--verbose`
### Description

Black would now echo the location that it determined as the root path for the project if `--verbose` is enabled by the user, according to which it chooses the SRC paths, i.e. the absolute path of the project: `{root}/{src}`.

![Screenshot from 2021-10-07 13-40-59](https://user-images.githubusercontent.com/69356296/136345712-5d94d1ff-fd22-4c85-a49a-333a4c74429c.png)


Closes #1880

### Checklist - did you ...
- [x] Add a CHANGELOG entry if necessary?
- [X] Add / update tests if necessary?
- [X] Add new / update outdated documentation?

Problem statement:
`--verbose` mode should print location of project root
**Describe the bug** A clear and concise description of what the bug is.

Black 20.8b1 does not respect skip-string-normalization = true in the pyproject.toml

**To Reproduce** Steps to reproduce the behavior:

1. pyproject.toml
```
[tool.black]
target-version = ['py36']
line-length = 120
skip-string-normalization = true
exclude = '(.*/migrations/.*\.py)'
include = '.*/server/.*\.py$'
```

2. create a python file with single strings

```
example = 'This is an example'
```

3. run `black <filename> --check --diff` from same directory as pyproject.toml (which is root repo dir)

**Expected behavior** A clear and concise description of what you expected to happen.

No changes would be applied by black given our pyproject.toml. Note that if you pass `--config` to that same command it works. 

**Environment (please complete the following information):**

- Version: 20.8b1
- OS and Python version: macOS 10.15.7, Python 3.7.2 (but target version is 3.6)

**Does this bug also happen on master?** To answer this, you have two options:

Yes with:

> 2. Or run _Black_ on your machine:
>    - create a new virtualenv (make sure it's the same Python version);
>    - clone this repository;
>    - run `pip install -e .`;
>    - make sure it's sane by running `python -m unittest`; and
>    - run `black` like you did last time.
> 

**Additional context** Add any other context about the problem here.

We haven't normalized our strings yet because it's a ~30k line change (we plan to in the coming months). Noting that it seems to be a general "not respecting pyproject.toml" issue but string normalization was the most obvious thing.

Hints from issue discussion:
Thanks for reporting, but hmm I cannot reproduce:

```console
ichard26@acer-ubuntu:~/programming/testing$ mkdir black1880
ichard26@acer-ubuntu:~/programming/testing$ cd black1880/
ichard26@acer-ubuntu:~/programming/testing/black1880$ python -m virtualenv env/
ichard26@acer-ubuntu:~/programming/testing/black1880$ actenv
(env) ichard26@acer-ubuntu:~/programming/testing/black1880$ pip install black==20.8b1
(env) ichard26@acer-ubuntu:~/programming/testing/black1880$ vim pyproject.toml
(env) ichard26@acer-ubuntu:~/programming/testing/black1880$ vim test.py
(env) ichard26@acer-ubuntu:~/programming/testing/black1880$ black test.py --check --diff -v
Using configuration from /home/ichard26/programming/testing/black1880/pyproject.toml.
test.py already well formatted, good job.
All done! ✨ 🍰 ✨
1 file would be left unchanged.
(env) ichard26@acer-ubuntu:~/programming/testing/black1880$ cat pyproject.toml 
[tool.black]
target-version = ['py36']
line-length = 120
skip-string-normalization = true
exclude = '(.*/migrations/.*\.py)'
include = '.*/server/.*\.py$'
(env) ichard26@acer-ubuntu:~/programming/testing/black1880$ cat test.py
example = 'This is an example'
```
Hmm. I realize there's an implementation detail that mattered: the subdirectory we had the file in (server/) had a .git directory due to a docker quirk. Looking at find_pyproject_toml() and testing by removing the .git directory, it looks like black looks at the lowest directory with a .git and then uses that as the "root"?

new way to repro:

same files you had before, then do (in your test dir)

```mkdir subdir; mkdir subdir/.git/; mv test.py subdir/; black subdir/test.py --check --diff```

This is documented here: https://black.readthedocs.io/en/stable/pyproject_toml.html, so technically not an issue. 
Yeah, it's intended behaviour to first look in the common parent directory of the discovered or/and passed-in files for a "project root" indicator. Although, I'll admit it isn't exactly clear at first glance, even with `--verbose`. Perhaps it would be helpful to have Black print what folder it determined as the project root when verbose output is enabled? (I know that the root of the filesystem fallback doesn't really count as the root of any project but we can bikeshed over that later)
+1 more debug info. We can close this as "user shouldn't code after 6pm"
Got a file:
```
$ cat blacktest.py
example = 'This is an example'


def func():
    '''this is a docstring'''
    pass


def func2():
    '''
    this is a docstring
    '''
    pass
```

When run with black==19.10b and `skip-string-normalization = true` in pyproject.toml, nothing happens.

When I do the same with latest black (20.8b1) I get:
```
$ black blacktest.py --check --diff
would reformat blacktest.py
Oh no! 💥 💔 💥
1 file would be reformatted.
--- blacktest.py	2021-02-20 01:44:06.141640 +0000
+++ blacktest.py	2021-02-20 01:47:05.430516 +0000
@@ -5,9 +5,9 @@
     '''this is a docstring'''
     pass


 def func2():
-    '''
+    """
     this is a docstring
-    '''
+    """
     pass
```

I.e. it does seem like black 20.8b1 _is_ indeed converting single-quote docstrings into double-quote docstrings, at least when the docstring starts with a newline.

Should that still be considered a bug, or what do you think @ichard26 ?
@tomage yeah that would be considered a bug. Fortunately that's been reported and fixed already (see [commit](https://github.com/psf/black/commit/7fe19fac5bccc08dbcf21c293ab50f1fcda7716f), [PR](https://github.com/psf/black/pull/1637), and [issue](https://github.com/psf/black/issues/1634)). Unfortunately the fix hasn't been shipped in a release yet.
Ah - dang - should have noticed that one! Thanks for the update!
Going to rename this issue to reflect @ichard26's suggestion that `--verbose` should print more information.
Had my eyes on implementing this. The core issue seems to be getting `verbose` and `find_project_root` to communicate, because parsing the configuration is done in the option declaration rather than in the main CLI function. Just to steer the implementation: would you prefer moving parsing the configuration inside main from the option declaration, *or* set some global state that is then read in main for verbose printing?

Commit message: Remove quotes from project root verbose statement

Patch to review:
diff --git a/CHANGES.md b/CHANGES.md
index b454a73edab..39ec9c3059a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,12 @@
 # Change Log
 
+## Unreleased
+
+### _Black_
+
+- `--verbose` enhancements (project root, how is config found, root relative paths)
+  (#2526)
+
 ## 21.10b0
 
 ### _Black_
diff --git a/src/black/__init__.py b/src/black/__init__.py
index 831cda934a8..bd92cd76736 100644
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -9,6 +9,7 @@
 from multiprocessing import Manager, freeze_support
 import os
 from pathlib import Path
+from click.core import ParameterSource
 from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
 import regex as re
 import signal
@@ -44,7 +45,7 @@
 from black.cache import read_cache, write_cache, get_cache_info, filter_cached, Cache
 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.report import Report, Changed, NothingChanged, root_relative
 from black.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
 from black.files import gen_python_files, get_gitignore, normalize_path_maybe_ignore
 from black.files import wrap_stream_for_windows
@@ -399,7 +400,11 @@ def main(
 ) -> None:
     """The uncompromising code formatter."""
     if config and verbose:
-        out(f"Using configuration from {config}.", bold=False, fg="blue")
+        config_source = ctx.get_parameter_source("config")
+        if 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")
 
     error_msg = "Oh no! 💥 💔 💥"
     if required_version and required_version != __version__:
@@ -441,7 +446,7 @@ def main(
         )
     else:
         try:
-            sources = get_sources(
+            sources, root = get_sources(
                 ctx=ctx,
                 src=src,
                 quiet=quiet,
@@ -475,6 +480,7 @@ def main(
         else:
             reformat_many(
                 sources=sources,
+                root=root,
                 fast=fast,
                 write_back=write_back,
                 mode=mode,
@@ -501,10 +507,29 @@ def get_sources(
     force_exclude: Optional[Pattern[str]],
     report: "Report",
     stdin_filename: Optional[str],
-) -> Set[Path]:
-    """Compute the set of files to be formatted."""
+) -> Tuple[Set[Path], Path]:
+    """
+    Compute the set of files to be formatted.
+
+    Returns a tuple, with the first element as a set of paths which are to be
+    checked/formatted by black, and the second element as the project root
+    found out by black.
+    """
+    root, method = find_project_root(src)
+
+    if verbose:
+        if method:
+            out(
+                f"Identified `{root}` as project root containing a {method}.",
+                fg="blue",
+            )
+        else:
+            out(f"Identified `{root}` as project root.", fg="blue")
+        paths = '", "'.join(
+            str(root_relative(Path(source).absolute(), root)) for source in src
+        )
+        out(f'Sources to be formatted: "{paths}"', fg="blue")
 
-    root = find_project_root(src)
     sources: Set[Path] = set()
     path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx)
 
@@ -534,7 +559,9 @@ def get_sources(
             else:
                 force_exclude_match = None
             if force_exclude_match and force_exclude_match.group(0):
-                report.path_ignored(p, "matches the --force-exclude regular expression")
+                report.path_ignored(
+                    p, "matches the --force-exclude regular expression", root
+                )
                 continue
 
             if is_stdin:
@@ -564,8 +591,8 @@ def get_sources(
         elif s == "-":
             sources.add(p)
         else:
-            err(f"invalid path: {s}")
-    return sources
+            out(f"Invalid path: {root_relative(p, root)}", fg="red")
+    return sources, root
 
 
 def path_empty(
@@ -612,6 +639,8 @@ def reformat_one(
     `fast`, `write_back`, and `mode` options are passed to
     :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
     """
+    root, _ = find_project_root(str(src))
+
     try:
         changed = Changed.NO
 
@@ -648,15 +677,16 @@ def reformat_one(
                 write_back is WriteBack.CHECK and changed is Changed.NO
             ):
                 write_cache(cache, [src], mode)
-        report.done(src, changed)
+        report.done(src, changed, root)
     except Exception as exc:
         if report.verbose:
             traceback.print_exc()
-        report.failed(src, str(exc))
+        report.failed(src, str(exc), root)
 
 
 def reformat_many(
     sources: Set[Path],
+    root: Path,
     fast: bool,
     write_back: WriteBack,
     mode: Mode,
@@ -683,6 +713,7 @@ def reformat_many(
         loop.run_until_complete(
             schedule_formatting(
                 sources=sources,
+                root=root,
                 fast=fast,
                 write_back=write_back,
                 mode=mode,
@@ -699,6 +730,7 @@ def reformat_many(
 
 async def schedule_formatting(
     sources: Set[Path],
+    root: Path,
     fast: bool,
     write_back: WriteBack,
     mode: Mode,
@@ -718,7 +750,7 @@ async def schedule_formatting(
         cache = read_cache(mode)
         sources, cached = filter_cached(cache, sources)
         for src in sorted(cached):
-            report.done(src, Changed.CACHED)
+            report.done(src, Changed.CACHED, root)
     if not sources:
         return
 
@@ -761,7 +793,7 @@ async def schedule_formatting(
                     write_back is WriteBack.CHECK and changed is Changed.NO
                 ):
                     sources_to_cache.append(src)
-                report.done(src, changed)
+                report.done(src, changed, root)
     if cancelled:
         if sys.version_info >= (3, 7):
             await asyncio.gather(*cancelled, return_exceptions=True)
diff --git a/src/black/files.py b/src/black/files.py
index 4d7b47aaa9f..b7a8723c47b 100644
--- a/src/black/files.py
+++ b/src/black/files.py
@@ -30,7 +30,7 @@
 
 
 @lru_cache()
-def find_project_root(srcs: Sequence[str]) -> Path:
+def find_project_root(srcs: Sequence[str]) -> Tuple[Path, Optional[str]]:
     """Return a directory containing .git, .hg, or pyproject.toml.
 
     That directory will be a common parent of all files and directories
@@ -38,6 +38,11 @@ def find_project_root(srcs: Sequence[str]) -> Path:
 
     If no directory in the tree contains a marker that would specify it's the
     project root, the root of the file system is returned.
+
+    Returns a tuple with the first element as the project root path and the
+    second element as a string which describes the method by which black
+    found out the project root (None, if `None` of the registered method
+    was used).
     """
     if not srcs:
         srcs = [str(Path.cwd().resolve())]
@@ -57,20 +62,20 @@ def find_project_root(srcs: Sequence[str]) -> Path:
 
     for directory in (common_base, *common_base.parents):
         if (directory / ".git").exists():
-            return directory
+            return directory, ".git directory"
 
         if (directory / ".hg").is_dir():
-            return directory
+            return directory, ".hg directory"
 
         if (directory / "pyproject.toml").is_file():
-            return directory
+            return directory, "pyproject.toml"
 
-    return directory
+    return directory, None
 
 
 def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
     """Find the absolute filepath to a pyproject.toml if it exists"""
-    path_project_root = find_project_root(path_search_start)
+    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)
@@ -191,7 +196,7 @@ def gen_python_files(
 
         # First ignore files matching .gitignore, if passed
         if gitignore is not None and gitignore.match_file(normalized_path):
-            report.path_ignored(child, "matches the .gitignore file content")
+            report.path_ignored(child, "matches the .gitignore file content", root)
             continue
 
         # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options.
@@ -200,17 +205,19 @@ def gen_python_files(
             normalized_path += "/"
 
         if path_is_excluded(normalized_path, exclude):
-            report.path_ignored(child, "matches the --exclude regular expression")
+            report.path_ignored(child, "matches the --exclude regular expression", root)
             continue
 
         if path_is_excluded(normalized_path, extend_exclude):
             report.path_ignored(
-                child, "matches the --extend-exclude regular expression"
+                child, "matches the --extend-exclude regular expression", root
             )
             continue
 
         if path_is_excluded(normalized_path, force_exclude):
-            report.path_ignored(child, "matches the --force-exclude regular expression")
+            report.path_ignored(
+                child, "matches the --force-exclude regular expression", root
+            )
             continue
 
         if child.is_dir():
diff --git a/src/black/report.py b/src/black/report.py
index 7e1c8b4b87f..72d05b12b1e 100644
--- a/src/black/report.py
+++ b/src/black/report.py
@@ -1,6 +1,9 @@
 """
 Summarize Black runs to users.
 """
+import os
+from typing import Optional
+
 from dataclasses import dataclass
 from enum import Enum
 from pathlib import Path
@@ -10,6 +13,12 @@
 from black.output import out, err
 
 
+def root_relative(path: Path, root: Optional[Path]) -> Path:
+    if not root:
+        return path
+    return Path(os.path.relpath(path, start=Path(f"{root}/../")))
+
+
 class Changed(Enum):
     NO = 0
     CACHED = 1
@@ -32,30 +41,32 @@ class Report:
     same_count: int = 0
     failure_count: int = 0
 
-    def done(self, src: Path, changed: Changed) -> None:
+    def done(self, src: Path, changed: Changed, root: Optional[Path] = None) -> None:
         """Increment the counter for successful reformatting. Write out a message."""
         if changed is Changed.YES:
             reformatted = "would reformat" if self.check or self.diff else "reformatted"
             if self.verbose or not self.quiet:
-                out(f"{reformatted} {src}")
+                out(f"{reformatted} {root_relative(src, root)}")
             self.change_count += 1
         else:
             if self.verbose:
                 if changed is Changed.NO:
-                    msg = f"{src} already well formatted, good job."
+                    msg = "already well formatted, good job."
                 else:
-                    msg = f"{src} wasn't modified on disk since last run."
-                out(msg, bold=False)
+                    msg = "wasn't modified on disk since last run."
+                out(f"{root_relative(src, root)} {msg}", bold=False)
             self.same_count += 1
 
-    def failed(self, src: Path, message: str) -> None:
+    def failed(self, src: Path, message: str, root: Optional[Path] = None) -> None:
         """Increment the counter for failed reformatting. Write out a message."""
-        err(f"error: cannot format {src}: {message}")
+        err(f"error: cannot format {root_relative(src, root)}: {message}")
         self.failure_count += 1
 
-    def path_ignored(self, path: Path, message: str) -> None:
+    def path_ignored(
+        self, path: Path, message: str, root: Optional[Path] = None
+    ) -> None:
         if self.verbose:
-            out(f"{path} ignored: {message}", bold=False)
+            out(f"{root_relative(path, root)} ignored: {message}", bold=False)
 
     @property
     def return_code(self) -> int:
diff --git a/tests/test_black.py b/tests/test_black.py
index b96a5438557..728593002d1 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -1070,7 +1070,8 @@ def test_reformat_one_with_stdin(self) -> None:
                 report=report,
             )
             fsts.assert_called_once()
-            report.done.assert_called_with(path, black.Changed.YES)
+            root, _ = black.find_project_root(str(path))
+            report.done.assert_called_with(path, black.Changed.YES, root)
 
     def test_reformat_one_with_stdin_filename(self) -> None:
         with patch(
@@ -1092,7 +1093,8 @@ def test_reformat_one_with_stdin_filename(self) -> None:
                 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
             )
             # __BLACK_STDIN_FILENAME__ should have been stripped
-            report.done.assert_called_with(expected, black.Changed.YES)
+            root, _ = black.find_project_root(str(expected))
+            report.done.assert_called_with(expected, black.Changed.YES, root)
 
     def test_reformat_one_with_stdin_filename_pyi(self) -> None:
         with patch(
@@ -1116,7 +1118,8 @@ def test_reformat_one_with_stdin_filename_pyi(self) -> None:
                 mode=replace(DEFAULT_MODE, is_pyi=True),
             )
             # __BLACK_STDIN_FILENAME__ should have been stripped
-            report.done.assert_called_with(expected, black.Changed.YES)
+            root, _ = black.find_project_root(str(expected))
+            report.done.assert_called_with(expected, black.Changed.YES, root)
 
     def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
         with patch(
@@ -1140,7 +1143,8 @@ def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
                 mode=replace(DEFAULT_MODE, is_ipynb=True),
             )
             # __BLACK_STDIN_FILENAME__ should have been stripped
-            report.done.assert_called_with(expected, black.Changed.YES)
+            root, _ = black.find_project_root(str(expected))
+            report.done.assert_called_with(expected, black.Changed.YES, root)
 
     def test_reformat_one_with_stdin_and_existing_path(self) -> None:
         with patch(
@@ -1164,7 +1168,8 @@ def test_reformat_one_with_stdin_and_existing_path(self) -> None:
             )
             fsts.assert_called_once()
             # __BLACK_STDIN_FILENAME__ should have been stripped
-            report.done.assert_called_with(expected, black.Changed.YES)
+            root, _ = black.find_project_root(str(expected))
+            report.done.assert_called_with(expected, black.Changed.YES, root)
 
     def test_reformat_one_with_stdin_empty(self) -> None:
         output = io.StringIO()
@@ -1313,10 +1318,17 @@ def test_find_project_root(self) -> None:
             src_python.touch()
 
             self.assertEqual(
-                black.find_project_root((src_dir, test_dir)), root.resolve()
+                black.find_project_root((src_dir, test_dir)),
+                (root.resolve(), "pyproject.toml"),
+            )
+            self.assertEqual(
+                black.find_project_root((src_dir,)),
+                (src_dir.resolve(), "pyproject.toml"),
+            )
+            self.assertEqual(
+                black.find_project_root((src_python,)),
+                (src_dir.resolve(), "pyproject.toml"),
             )
-            self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
-            self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
 
     @patch(
         "black.files.find_user_pyproject_toml",
@@ -1744,7 +1756,7 @@ def assert_collected_sources(
         report=black.Report(),
         stdin_filename=stdin_filename,
     )
-    assert sorted(list(collected)) == sorted(gs_expected)
+    assert sorted(list(collected[0])) == sorted(gs_expected)
 
 
 class TestFileCollection:
@@ -1771,7 +1783,7 @@ def test_gitignore_used_as_default(self) -> None:
         src = [base / "b/"]
         assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_exclude_for_issue_1572(self) -> None:
         # Exclude shouldn't touch files that were explicitly given to Black through the
         # CLI. Exclude is supposed to only apply to the recursive discovery of files.
@@ -1953,13 +1965,13 @@ def test_symlink_out_of_root_directory(self) -> None:
         child.is_symlink.assert_called()
         assert child.is_symlink.call_count == 2
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin(self) -> None:
         src = ["-"]
         expected = ["-"]
         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename(self) -> None:
         src = ["-"]
         stdin_filename = str(THIS_DIR / "data/collections.py")
@@ -1971,7 +1983,7 @@ def test_get_sources_with_stdin_filename(self) -> None:
             stdin_filename=stdin_filename,
         )
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
         # Exclude shouldn't exclude stdin_filename since it is mimicking the
         # file being passed directly. This is the same as
@@ -1987,7 +1999,7 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
             stdin_filename=stdin_filename,
         )
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
         # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
         # file being passed directly. This is the same as
@@ -2003,7 +2015,7 @@ def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
             stdin_filename=stdin_filename,
         )
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
         # Force exclude should exclude the file when passing it through
         # stdin_filename


Code review comments:
diff_hunk: @@ -10,6 +13,12 @@
 from black.output import out, err
 
 
+def root_relative(path: Path, root: Optional[Path]) -> Path:
+    if not root:
+        return path
+    return Path(os.path.relpath(path, start=Path(f"{root}/../")))
path: src/black/report.py
text: @user1:
`pathlib.Path` does have `p.relative_to`. But this works fine too 👍 

@user1:
```suggestion
    return path.relative_to(root.parent)
```
would be the form. So why the parent?

@user1:
Oh I guess we want the root folder to be included?

@author:
Yeah

@user1:
The pathlib version is much cleaner to read as well, so maybe we should use it instead.