Skip to content

Commit bf611a3

Browse files
Add tree method to display tree-like structure of the filesystem (#1750)
Co-authored-by: Martin Durant <[email protected]>
1 parent c36066c commit bf611a3

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed

fsspec/spec.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,6 +1576,141 @@ def modified(self, path):
15761576
"""Return the modified timestamp of a file as a datetime.datetime"""
15771577
raise NotImplementedError
15781578

1579+
def tree(
1580+
self,
1581+
path: str = "/",
1582+
recursion_limit: int = 2,
1583+
max_display: int = 25,
1584+
display_size: bool = False,
1585+
prefix: str = "",
1586+
is_last: bool = True,
1587+
first: bool = True,
1588+
indent_size: int = 4,
1589+
) -> str:
1590+
"""
1591+
Return a tree-like structure of the filesystem starting from the given path as a string.
1592+
1593+
Parameters
1594+
----------
1595+
path: Root path to start traversal from
1596+
recursion_limit: Maximum depth of directory traversal
1597+
max_display: Maximum number of items to display per directory
1598+
display_size: Whether to display file sizes
1599+
prefix: Current line prefix for visual tree structure
1600+
is_last: Whether current item is last in its level
1601+
first: Whether this is the first call (displays root path)
1602+
indent_size: Number of spaces by indent
1603+
1604+
Returns
1605+
-------
1606+
str: A string representing the tree structure.
1607+
1608+
Example
1609+
-------
1610+
>>> from fsspec import filesystem
1611+
1612+
>>> fs = filesystem('ftp', host='test.rebex.net', user='demo', password='password')
1613+
>>> tree = fs.tree(display_size=True, recursion_limit=3, indent_size=8, max_display=10)
1614+
>>> print(tree)
1615+
"""
1616+
1617+
def format_bytes(n: int) -> str:
1618+
"""Format bytes as text."""
1619+
for prefix, k in (
1620+
("P", 2**50),
1621+
("T", 2**40),
1622+
("G", 2**30),
1623+
("M", 2**20),
1624+
("k", 2**10),
1625+
):
1626+
if n >= 0.9 * k:
1627+
return f"{n / k:.2f} {prefix}b"
1628+
return f"{n}B"
1629+
1630+
result = []
1631+
1632+
if first:
1633+
result.append(path)
1634+
1635+
if recursion_limit:
1636+
indent = " " * indent_size
1637+
contents = self.ls(path, detail=True)
1638+
contents.sort(
1639+
key=lambda x: (x.get("type") != "directory", x.get("name", ""))
1640+
)
1641+
1642+
if max_display is not None and len(contents) > max_display:
1643+
displayed_contents = contents[:max_display]
1644+
remaining_count = len(contents) - max_display
1645+
else:
1646+
displayed_contents = contents
1647+
remaining_count = 0
1648+
1649+
for i, item in enumerate(displayed_contents):
1650+
is_last_item = (i == len(displayed_contents) - 1) and (
1651+
remaining_count == 0
1652+
)
1653+
1654+
branch = (
1655+
"└" + ("─" * (indent_size - 2))
1656+
if is_last_item
1657+
else "├" + ("─" * (indent_size - 2))
1658+
)
1659+
branch += " "
1660+
new_prefix = prefix + (
1661+
indent if is_last_item else "│" + " " * (indent_size - 1)
1662+
)
1663+
1664+
name = os.path.basename(item.get("name", ""))
1665+
1666+
if display_size and item.get("type") == "directory":
1667+
sub_contents = self.ls(item.get("name", ""), detail=True)
1668+
num_files = sum(
1669+
1 for sub_item in sub_contents if sub_item.get("type") == "file"
1670+
)
1671+
num_folders = sum(
1672+
1
1673+
for sub_item in sub_contents
1674+
if sub_item.get("type") == "directory"
1675+
)
1676+
1677+
if num_files == 0 and num_folders == 0:
1678+
size = " (empty folder)"
1679+
elif num_files == 0:
1680+
size = f" ({num_folders} subfolder{'s' if num_folders > 1 else ''})"
1681+
elif num_folders == 0:
1682+
size = f" ({num_files} file{'s' if num_files > 1 else ''})"
1683+
else:
1684+
size = f" ({num_files} file{'s' if num_files > 1 else ''}, {num_folders} subfolder{'s' if num_folders > 1 else ''})"
1685+
elif display_size and item.get("type") == "file":
1686+
size = f" ({format_bytes(item.get('size', 0))})"
1687+
else:
1688+
size = ""
1689+
1690+
result.append(f"{prefix}{branch}{name}{size}")
1691+
1692+
if item.get("type") == "directory" and recursion_limit > 0:
1693+
result.append(
1694+
self.tree(
1695+
path=item.get("name", ""),
1696+
recursion_limit=recursion_limit - 1,
1697+
max_display=max_display,
1698+
display_size=display_size,
1699+
prefix=new_prefix,
1700+
is_last=is_last_item,
1701+
first=False,
1702+
indent_size=indent_size,
1703+
)
1704+
)
1705+
1706+
if remaining_count > 0:
1707+
more_message = f"{remaining_count} more item(s) not displayed."
1708+
result.append(
1709+
f"{prefix}{'└' + ('─' * (indent_size - 2))} {more_message}"
1710+
)
1711+
1712+
return "\n".join(_ for _ in result if _)
1713+
15791714
# ------------------------------------------------------------------------
15801715
# Aliases
15811716

0 commit comments

Comments
 (0)