Repository: pypa/pip
File: src/pip/_internal/utils/filesystem.py (Python)

Code before:

def file_size(path: str) -> int | float:
    # If it's a symlink, return 0.
    if os.path.islink(path):
        return 0
    return os.path.getsize(path)


def format_file_size(path: str) -> str:
    return format_size(file_size(path))


def directory_size(path: str) -> int | float:
    size = 0.0
    for root, _dirs, files in os.walk(path):
        for filename in files:
            file_path = os.path.join(root, filename)
            size += file_size(file_path)
    return size


def format_directory_size(path: str) -> str:
    return format_size(directory_size(path))


def subdirs_without_files(path: str) -> Generator[Path]:
    """Yields every subdirectory of +path+ that has no files under it."""

    def inner(path: Path, parents: list[Path]) -> Generator[Path]:
        path_obj = Path(path)

        if not path_obj.exists():
            return

        subdirs = []
        for item in path_obj.iterdir():
            if item.is_dir():
                subdirs.append(item)
            else:
                # If we find a file, we want to preserve the whole subtree,
                # so bail immediately.
                return

        # If we get to this point, we didn't find a file yet.

        if parents is None:
            parents = []
        else:
            parents += [path_obj]

        if subdirs:

Review comment (bug):
I hate to break it to you, but this seems to be buggy still. The root of the problem is that when it discards a subtree because a file is present, we need to discard the entire _chain of parent directories_. This logic seems to instead discard (skip) the inner subdirectories of the non-empty directory. 

Imagine we have this structure:

```
root
├── d
│   ├── d
│   │   └── 3
│   │       └── uh.txt
│   └── f
│       └── d
```

When the code reaches `root/f/d/d` and discovers it's empty, it will return all of its parents, but we should only delete the `root/d/f/d` and `root/d/f` parents. `root` and `root/d` still contain a file (at `root/d/d/3/uh.txt`).

I spent some working on an alternative implementation. I haven't thoroughly tested this, but it seems to be more robust:

```python
def subdirs_without_files(path: str) -> Generator[Path]:
    """Yields every subdirectory of +path+ that has no files under it."""

    directories = []
    non_empty = set()

    for root, _, filenames in os.walk(path, topdown=False):
        root_path = Path(root)
        if filenames:
            # This directory contains a file, mark it and its parent
            # directories (but not ".") as non empty.
            non_empty.update(root_path.parents[:-1])
            non_empty.add(root_path)

        directories.append(root_path)

    for d in directories:
        if d not in non_empty:
            yield d
```

The gist is that we walk the entire directory structure looking for directories which contain a file, all while building a list of all directories. If a file is found, then the containing directory and its parents are marked as non-empty. Afterwards, we go through the directories and yield any we haven't marked earlier. And sine we're walking bottom-up, they will be naturally yielded in reverse order.

Code after:

def file_size(path: str) -> int | float:
    # If it's a symlink, return 0.
    if os.path.islink(path):
        return 0
    return os.path.getsize(path)


def format_file_size(path: str) -> str:
    return format_size(file_size(path))


def directory_size(path: str) -> int | float:
    size = 0.0
    for root, _dirs, files in os.walk(path):
        for filename in files:
            file_path = os.path.join(root, filename)
            size += file_size(file_path)
    return size


def format_directory_size(path: str) -> str:
    return format_size(directory_size(path))


def copy_directory_permissions(directory: str, target_file: BinaryIO) -> None:
    mode = (
        os.stat(directory).st_mode & 0o666  # select read/write permissions of directory
        | 0o600  # set owner read/write permissions
    )
    # Change permissions only if there is no risk of following a symlink.
    if os.chmod in os.supports_fd:
        os.chmod(target_file.fileno(), mode)
    elif os.chmod in os.supports_follow_symlinks:
        os.chmod(target_file.name, mode, follow_symlinks=False)


def _subdirs_without_generic(
    path: str, predicate: Callable[[str, list[str]], bool]
) -> Generator[Path]:
    """Yields every subdirectory of +path+ that has no files matching the
    predicate under it."""

    directories = []
    excluded = set()

    for root_str, _, filenames in os.walk(Path(path).resolve()):
        root = Path(root_str)
        if predicate(root_str, filenames):
            # This directory should be excluded, so exclude it and all of its
            # parent directories.