File size: 4,366 Bytes
fe5c39d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time    : 2024/3/11
@Author  : mashenquan
@File    : tree.py
@Desc    : Implement the same functionality as the `tree` command.
        Example:
            >>> print_tree(".")
            utils
            +-- serialize.py
            +-- project_repo.py
            +-- tree.py
            +-- mmdc_playwright.py
            +-- cost_manager.py
            +-- __pycache__
            |   +-- __init__.cpython-39.pyc
            |   +-- redis.cpython-39.pyc
            |   +-- singleton.cpython-39.pyc
            |   +-- embedding.cpython-39.pyc
            |   +-- make_sk_kernel.cpython-39.pyc
            |   +-- file_repository.cpython-39.pyc
            +-- file.py
            +-- save_code.py
            +-- common.py
            +-- redis.py
"""
from __future__ import annotations

import subprocess
from pathlib import Path
from typing import Callable, Dict, List

from gitignore_parser import parse_gitignore


def tree(root: str | Path, gitignore: str | Path = None, run_command: bool = False) -> str:
    """
    Recursively traverses the directory structure and prints it out in a tree-like format.

    Args:
        root (str or Path): The root directory from which to start traversing.
        gitignore (str or Path): The filename of gitignore file.
        run_command (bool): Whether to execute `tree` command. Execute the `tree` command and return the result if True,
            otherwise execute python code instead.

    Returns:
        str: A string representation of the directory tree.

    Example:
            >>> tree(".")
            utils
            +-- serialize.py
            +-- project_repo.py
            +-- tree.py
            +-- mmdc_playwright.py
            +-- __pycache__
            |   +-- __init__.cpython-39.pyc
            |   +-- redis.cpython-39.pyc
            |   +-- singleton.cpython-39.pyc
            +-- parse_docstring.py

            >>> tree(".", gitignore="../../.gitignore")
            utils
            +-- serialize.py
            +-- project_repo.py
            +-- tree.py
            +-- mmdc_playwright.py
            +-- parse_docstring.py

            >>> tree(".", gitignore="../../.gitignore", run_command=True)
            utils
            β”œβ”€β”€ serialize.py
            β”œβ”€β”€ project_repo.py
            β”œβ”€β”€ tree.py
            β”œβ”€β”€ mmdc_playwright.py
            └── parse_docstring.py


    """
    root = Path(root).resolve()
    if run_command:
        return _execute_tree(root, gitignore)

    git_ignore_rules = parse_gitignore(gitignore) if gitignore else None
    dir_ = {root.name: _list_children(root=root, git_ignore_rules=git_ignore_rules)}
    v = _print_tree(dir_)
    return "\n".join(v)


def _list_children(root: Path, git_ignore_rules: Callable) -> Dict[str, Dict]:
    dir_ = {}
    for i in root.iterdir():
        if git_ignore_rules and git_ignore_rules(str(i)):
            continue
        try:
            if i.is_file():
                dir_[i.name] = {}
            else:
                dir_[i.name] = _list_children(root=i, git_ignore_rules=git_ignore_rules)
        except (FileNotFoundError, PermissionError, OSError):
            dir_[i.name] = {}
    return dir_


def _print_tree(dir_: Dict[str:Dict]) -> List[str]:
    ret = []
    for name, children in dir_.items():
        ret.append(name)
        if not children:
            continue
        lines = _print_tree(children)
        for j, v in enumerate(lines):
            if v[0] not in ["+", " ", "|"]:
                ret = _add_line(ret)
                row = f"+-- {v}"
            else:
                row = f"    {v}"
            ret.append(row)
    return ret


def _add_line(rows: List[str]) -> List[str]:
    for i in range(len(rows) - 1, -1, -1):
        v = rows[i]
        if v[0] != " ":
            return rows
        rows[i] = "|" + v[1:]
    return rows


def _execute_tree(root: Path, gitignore: str | Path) -> str:
    args = ["--gitfile", str(gitignore)] if gitignore else []
    try:
        result = subprocess.run(["tree"] + args + [str(root)], capture_output=True, text=True, check=True)
        if result.returncode != 0:
            raise ValueError(f"tree exits with code {result.returncode}")
        return result.stdout
    except subprocess.CalledProcessError as e:
        raise e