File: tree.py

package info (click to toggle)
python-fs 2.4.16-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,944 kB
  • sloc: python: 13,048; makefile: 226; sh: 3
file content (172 lines) | stat: -rw-r--r-- 5,656 bytes parent folder | download | duplicates (2)
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# coding: utf-8
"""Render a FS object as text tree views.

Color is supported on UNIX terminals.
"""

from __future__ import print_function, unicode_literals

import sys
import typing

from fs.path import abspath, join, normpath

if typing.TYPE_CHECKING:
    from typing import List, Optional, Text, TextIO, Tuple

    from .base import FS
    from .info import Info


def render(
    fs,  # type: FS
    path="/",  # type: Text
    file=None,  # type: Optional[TextIO]
    encoding=None,  # type: Optional[Text]
    max_levels=5,  # type: int
    with_color=None,  # type: Optional[bool]
    dirs_first=True,  # type: bool
    exclude=None,  # type: Optional[List[Text]]
    filter=None,  # type: Optional[List[Text]]
):
    # type: (...) -> Tuple[int, int]
    """Render a directory structure in to a pretty tree.

    Arguments:
        fs (~fs.base.FS): A filesystem instance.
        path (str): The path of the directory to start rendering
            from (defaults to root folder, i.e. ``'/'``).
        file (io.IOBase): An open file-like object to render the
            tree, or `None` for stdout.
        encoding (str, optional): Unicode encoding, or `None` to
            auto-detect.
        max_levels (int, optional): Maximum number of levels to
            display, or `None` for no maximum.
        with_color (bool, optional): Enable terminal color output,
            or `None` to auto-detect terminal.
        dirs_first (bool): Show directories first.
        exclude (list, optional): Option list of directory patterns
            to exclude from the tree render.
        filter (list, optional): Optional list of files patterns to
            match in the tree render.

    Returns:
        (int, int): A tuple of ``(<directory count>, <file count>)``.

    """
    file = file or sys.stdout
    if encoding is None:
        encoding = getattr(file, "encoding", "utf-8") or "utf-8"
    is_tty = hasattr(file, "isatty") and file.isatty()

    if with_color is None:
        is_windows = sys.platform.startswith("win")
        with_color = False if is_windows else is_tty

    if encoding.lower() == "utf-8" and with_color:
        char_vertline = "│"
        char_newnode = "├"
        char_line = "──"
        char_corner = "└"
    else:
        char_vertline = "|"
        char_newnode = "|"
        char_line = "--"
        char_corner = "`"

    indent = " " * 4
    line_indent = char_vertline + " " * 3

    def write(line):
        # type: (Text) -> None
        """Write a line to the output."""
        print(line, file=file)

    # FIXME(@althonos): define functions using `with_color` and
    #      avoid checking `with_color` at every function call !

    def format_prefix(prefix):
        # type: (Text) -> Text
        """Format the prefix lines."""
        if not with_color:
            return prefix
        return "\x1b[32m%s\x1b[0m" % prefix

    def format_dirname(dirname):
        # type: (Text) -> Text
        """Format a directory name."""
        if not with_color:
            return dirname
        return "\x1b[1;34m%s\x1b[0m" % dirname

    def format_error(msg):
        # type: (Text) -> Text
        """Format an error."""
        if not with_color:
            return msg
        return "\x1b[31m%s\x1b[0m" % msg

    def format_filename(fname):
        # type: (Text) -> Text
        """Format a filename."""
        if not with_color:
            return fname
        if fname.startswith("."):
            fname = "\x1b[33m%s\x1b[0m" % fname
        return fname

    def sort_key_dirs_first(info):
        # type: (Info) -> Tuple[bool, Text]
        """Get the info sort function with directories first."""
        return (not info.is_dir, info.name.lower())

    def sort_key(info):
        # type: (Info) -> Text
        """Get the default info sort function using resource name."""
        return info.name.lower()

    counts = {"dirs": 0, "files": 0}

    def format_directory(path, levels):
        # type: (Text, List[bool]) -> None
        """Recursive directory function."""
        try:
            directory = sorted(
                fs.filterdir(path, exclude_dirs=exclude, files=filter),
                key=sort_key_dirs_first if dirs_first else sort_key,  # type: ignore
            )
        except Exception as error:
            prefix = (
                "".join(indent if last else line_indent for last in levels)
                + char_corner
                + char_line
            )
            write(
                "{} {}".format(
                    format_prefix(prefix), format_error("error ({})".format(error))
                )
            )
            return
        _last = len(directory) - 1
        for i, info in enumerate(directory):
            is_last_entry = i == _last
            counts["dirs" if info.is_dir else "files"] += 1
            prefix = "".join(indent if last else line_indent for last in levels)
            prefix += char_corner if is_last_entry else char_newnode
            if info.is_dir:
                write(
                    "{} {}".format(
                        format_prefix(prefix + char_line), format_dirname(info.name)
                    )
                )
                if max_levels is None or len(levels) < max_levels:
                    format_directory(join(path, info.name), levels + [is_last_entry])
            else:
                write(
                    "{} {}".format(
                        format_prefix(prefix + char_line), format_filename(info.name)
                    )
                )

    format_directory(abspath(normpath(path)), [])
    return counts["dirs"], counts["files"]