Skip to content

Commit

Permalink
Merge pull request #36 from tr0yspradling/master
Browse files Browse the repository at this point in the history
Enhancements: cleaned up unused code, fixed linter warnings, using custom PathEntry instead of os.DirEntry…
  • Loading branch information
cafehaine authored Aug 9, 2023
2 parents 080e9b2 + 0bdc5d5 commit 24d694e
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 36 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# Idea configurations
.idea/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ xontrib load xlsd

### Registering an icon

In xlsd, icons are registered using a name. The name is then used by the different rules to get an icon for an `os.DirEntry`.
In xlsd, icons are registered using a name. The name is then used by the different rules to get an icon for an `PathEntry`.

You can view the built-in icons in [xlsd/icons.py](xlsd/icons.py#L99).

Expand Down Expand Up @@ -162,7 +162,7 @@ All the built-in columns are used in this config.

### Writing your own column

A column is a function taking for only argument an `os.DirEntry` and that outputs a string.
A column is a function taking for only argument an `PathEntry` and that outputs a string.

A simple filename column could be registered like this:
```python
Expand Down
22 changes: 14 additions & 8 deletions xlsd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from typing import Callable
from .path import PathEntry


COLORS = {
'symlink_target': "{CYAN}",
Expand All @@ -8,10 +9,11 @@
'size_unit': "{CYAN}",
}

XlsdSortMethod = Callable[[list[os.DirEntry]], list[os.DirEntry]]

XlsdSortMethod = Callable[[list[PathEntry]], list[PathEntry]]

XLSD_SORT_METHODS: dict[str, XlsdSortMethod] = {}
#XLSD_SORT_METHODS = {}


def xlsd_register_sort_method(name: str):
"""
Expand All @@ -22,16 +24,18 @@ def decorator(func: XlsdSortMethod):
return func
return decorator

def _direntry_lowercase_name(entry: os.DirEntry) -> str:

def _direntry_lowercase_name(entry: PathEntry) -> str:
"""
Return the lowercase name for a DirEntry.
This is used to sort a list of DirEntry by name.
"""
return entry.name.lower()


@xlsd_register_sort_method('directories_first')
def xlsd_sort_directories_first(entries: list[os.DirEntry]) -> list[os.DirEntry]:
def xlsd_sort_directories_first(entries: list[PathEntry]) -> list[PathEntry]:
"""
Sort the entries in alphabetical order, directories first.
"""
Expand All @@ -44,24 +48,26 @@ def xlsd_sort_directories_first(entries: list[os.DirEntry]) -> list[os.DirEntry]
directories.append(entry)
else:
files.append(entry)
except OSError: # Probably circular symbolic link
except OSError: # Probably circular symbolic link
directories.append(entry)

directories.sort(key=_direntry_lowercase_name)
files.sort(key=_direntry_lowercase_name)

return directories + files


@xlsd_register_sort_method('alphabetical')
def xlsd_sort_alphabetical(entries: list[os.DirEntry]) -> list[os.DirEntry]:
def xlsd_sort_alphabetical(entries: list[PathEntry]) -> list[PathEntry]:
"""
Sort the entries in alphabetical order.
"""
entries.sort(key=_direntry_lowercase_name)
return entries


@xlsd_register_sort_method('as_is')
def xlsd_sort_as_is(entries: list[os.DirEntry]) -> list[os.DirEntry]:
def xlsd_sort_as_is(entries: list[PathEntry]) -> list[PathEntry]:
"""
Keep the entries in the same order they were returned by the OS.
"""
Expand Down
1 change: 1 addition & 0 deletions xlsd/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

T = TypeVar('T')


class IconSet(Generic[T]):
"""
A storage for icons.
Expand Down
80 changes: 80 additions & 0 deletions xlsd/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import stat


class PathEntry:
"""
PathEntry is a utility class that represents a filesystem entry at a given path. The entry can represent a file,
directory, or symbolic link. The class provides methods for checking the type of the entry and obtaining file
statistics for it.
The class can be used to mirror the behavior of PathEntry, because PathEntry objects returned by os.scandir()
do not handle individual file paths and do not allow selective control over following symbolic links for
different operations.
"""

def __init__(self, path: str) -> None:
"""
Initialize a new instance of the PathEntry class.
:param path: A string containing a path to a file or directory.
:type path: str
"""
self.path = path
self.name = os.path.basename(path)

def is_dir(self, follow_symlinks: bool = True) -> bool:
"""
Check if the path represents a directory.
:param follow_symlinks: If True (default), symlinks are followed (i.e., if the path points to a directory
through a symlink, it will return True). If False, symlinks are not followed (i.e.,
if the path points to a directory through a symlink, it will return False).
:type follow_symlinks: bool
:return: True if the path represents a directory, False otherwise.
:rtype: bool
"""
if follow_symlinks:
return os.path.isdir(self.path)
else:
return stat.S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)

def is_file(self, follow_symlinks: bool = True) -> bool:
"""
Check if the path represents a file.
:param follow_symlinks: If True (default), symlinks are followed (i.e., if the path points to a file through a
symlink, it will return True). If False, symlinks are not followed (i.e., if the path
points to a file through a symlink, it will return False).
:type follow_symlinks: bool
:return: True if the path represents a file, False otherwise.
:rtype: bool
"""
if follow_symlinks:
return os.path.isfile(self.path)
else:
return stat.S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)

def is_symlink(self) -> bool:
"""
Check if the path represents a symbolic link.
:return: True if the path represents a symbolic link, False otherwise.
:rtype: bool
"""
return os.path.islink(self.path)

def stat(self, follow_symlinks: bool = True) -> os.stat_result:
"""
Perform a stat system call on the given path.
:param follow_symlinks: If True (default), symlinks are followed, similar to os.stat(). If False, symlinks are
not followed, similar to os.lstat().
:type follow_symlinks: bool
:return: The result of the stat or lstat call on the path.
:rtype: os.stat_result
"""
if follow_symlinks:
return os.stat(self.path)
else:
return os.lstat(self.path)
73 changes: 47 additions & 26 deletions xontrib/xlsd.xsh
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ An improved ls for xonsh, inspired by lsd.
Registers automatically as an alias for ls on load.
"""
from enum import Enum, auto
import math
import os
from typing import Callable, Dict, List, Optional, Tuple, Union
import math

from enum import Enum, auto
from typing import Callable, List, Optional

from xonsh.lazyasd import lazyobject
from xonsh.tools import format_color, print_color, is_string_seq

from xlsd.path import PathEntry


# Lazy imports
@lazyobject
def grp():
Expand Down Expand Up @@ -129,7 +133,7 @@ ${...}.register('XLSD_LIST_COLUMNS', validate=is_string_seq, convert=csv_to_list
${...}.register('XLSD_ICON_SOURCES', validate=is_string_seq, convert=csv_to_list,
detype=list_to_csv, default=['extension', 'libmagic'])

XlsdColumn = Callable[[os.DirEntry],str]
XlsdColumn = Callable[[PathEntry],str]

#TODO see with xonsh devs, imo shouldn't crash
#_XLSD_COLUMNS: Dict[str, Tuple[XlsdColumn, ColumnAlignment]] = {}
Expand All @@ -144,7 +148,7 @@ def xlsd_register_column(name: str, alignment: ColumnAlignment):
return func
return decorator

XlsdIconSource = Callable[[os.DirEntry], Optional[str]]
XlsdIconSource = Callable[[PathEntry], Optional[str]]

#TODO see with xonsh devs, imo shouldn't crash
#_XLSD_ICON_SOURCES: Dict[str, XlsdIconSource] = {}
Expand Down Expand Up @@ -205,7 +209,7 @@ def _format_size(size: int) -> str:
# the 'magic' lib might only be included in arch linux, it doesn't seem to work
# on macos.
@xlsd_register_icon_source('libmagic')
def _xlsd_icon_source_libmagic(direntry: os.DirEntry) -> Optional[str]:
def _xlsd_icon_source_libmagic(direntry: PathEntry) -> Optional[str]:
"""
Return the icon for a direntry using the file's mimetype.
"""
Expand All @@ -228,7 +232,7 @@ def _xlsd_icon_source_libmagic(direntry: os.DirEntry) -> Optional[str]:


@xlsd_register_icon_source('extension')
def _xlsd_icon_source_extension(direntry: os.DirEntry) -> Optional[str]:
def _xlsd_icon_source_extension(direntry: PathEntry) -> Optional[str]:
"""
Return the emoji for a direntry using the file extension.
"""
Expand All @@ -250,7 +254,7 @@ def _xlsd_icon_source_extension(direntry: os.DirEntry) -> Optional[str]:
# /Icon sources #
#################

def _icon_for_direntry(entry: os.DirEntry) -> str:
def _icon_for_direntry(entry: PathEntry) -> str:
"""
Return the icon for a direntry.
"""
Expand All @@ -266,7 +270,7 @@ def _icon_for_direntry(entry: os.DirEntry) -> str:
return icons.LS_ICONS.get(name)


def _get_color_for_direntry(entry: os.DirEntry) -> str:
def _get_color_for_direntry(entry: PathEntry) -> str:
"""Return one or multiple xonsh colors for the entry using $LS_COLORS."""
colors = []

Expand Down Expand Up @@ -311,11 +315,11 @@ def _get_color_for_direntry(entry: os.DirEntry) -> str:
return "".join("{"+color+"}" for color in colors)


def _format_direntry_name(entry: os.DirEntry, show_target: bool = True) -> str:
def _format_direntry_name(entry: PathEntry, show_target: bool = True) -> str:
"""
Return a string containing a bunch of ainsi escape codes as well as the "width" of the new name.
"""
path = entry.path if not entry.is_symlink() else os.readlink(entry.path)
# path = entry.path if not entry.is_symlink() else os.readlink(entry.path)
name = entry.name

# Show the icon
Expand Down Expand Up @@ -343,8 +347,20 @@ def _format_direntry_name(entry: os.DirEntry, show_target: bool = True) -> str:

return "".join(colors) + name

def _scan_dir(path: str):
"""
Scan the directory with the given path and yield PathEntry instances for each entry.
def _direntry_lowercase_name(entry: os.DirEntry) -> str:
:param path: A string containing a path to a directory.
:type path: str
:yield: PathEntry instance for each entry in path.
:rtype: Iterator[PathEntry]
"""
for entry in os.scandir(path):
yield PathEntry(entry.path)


def _direntry_lowercase_name(entry: PathEntry) -> str:
"""
Return the lowercase name for a DirEntry.
Expand All @@ -353,21 +369,26 @@ def _direntry_lowercase_name(entry: os.DirEntry) -> str:
return entry.name.lower()


def _get_entries(path: str, show_hidden: bool) -> List[os.DirEntry]:
def _get_entries(path: str, show_hidden: bool) -> List[PathEntry]:
"""
Return the list of DirEntrys for a path, sorted by name, directories first.
Return the list of PathEntry's for a path, sorted by name, directories first.
"""
entries = []
try:
with os.scandir(path) as iterator:
for entry in iterator:

path_entry = PathEntry(path)
if path_entry.is_dir(path):
try:
for entry in _scan_dir(path):
# Skip entries that start with a '.'
if not show_hidden and entry.name.startswith('.'):
continue

entries.append(entry)
except PermissionError:
pass
except PermissionError:
pass
elif path_entry.is_file(path):
# If the path is a file, create a DirEntry for it
entries.append(PathEntry(path))

sort_method = xlsd.XLSD_SORT_METHODS.get($XLSD_SORT_METHOD, lambda x: x)

Expand Down Expand Up @@ -503,7 +524,7 @@ def _show_table(columns: List[List[str]], column_alignments: List[ColumnAlignmen
################

@xlsd_register_column('mode', ColumnAlignment.LEFT)
def _xlsd_column_mode(direntry: os.DirEntry) -> str:
def _xlsd_column_mode(direntry: PathEntry) -> str:
"""
Format the mode from the stat structure for a file.
"""
Expand All @@ -515,15 +536,15 @@ def _xlsd_column_mode(direntry: os.DirEntry) -> str:


@xlsd_register_column('hardlinks', ColumnAlignment.RIGHT)
def _xlsd_column_hardlinks(direntry: os.DirEntry) -> str:
def _xlsd_column_hardlinks(direntry: PathEntry) -> str:
"""
Show the number of hardlinks for a file.
"""
return str(direntry.stat(follow_symlinks=False).st_nlink)


@xlsd_register_column('uid', ColumnAlignment.LEFT)
def _xlsd_column_uid(direntry: os.DirEntry) -> str:
def _xlsd_column_uid(direntry: PathEntry) -> str:
"""
Show the owner (user) of the file.
"""
Expand All @@ -532,7 +553,7 @@ def _xlsd_column_uid(direntry: os.DirEntry) -> str:


@xlsd_register_column('gid', ColumnAlignment.LEFT)
def _xlsd_column_gid(direntry: os.DirEntry) -> str:
def _xlsd_column_gid(direntry: PathEntry) -> str:
"""
Show the group that owns the file.
"""
Expand All @@ -541,23 +562,23 @@ def _xlsd_column_gid(direntry: os.DirEntry) -> str:


@xlsd_register_column('size', ColumnAlignment.RIGHT)
def _xlsd_column_size(direntry: os.DirEntry) -> str:
def _xlsd_column_size(direntry: PathEntry) -> str:
"""
Format the size of a file.
"""
return _format_size(direntry.stat().st_size)


@xlsd_register_column('mtime', ColumnAlignment.LEFT)
def _xlsd_column_mtime(direntry: os.DirEntry) -> str:
def _xlsd_column_mtime(direntry: PathEntry) -> str:
"""
Format the last modification date for a direntry.
"""
return time.strftime("%x %X", time.gmtime(direntry.stat().st_mtime))


@xlsd_register_column('name', ColumnAlignment.LEFT)
def _xlsd_column_name(direntry: os.DirEntry) -> str:
def _xlsd_column_name(direntry: PathEntry) -> str:
"""
Simply format the filename of the direntry.
"""
Expand Down

0 comments on commit 24d694e

Please sign in to comment.