Source code for xonsh.commands_cache
# -*- coding: utf-8 -*-
import os
import builtins
import collections.abc as cabc
from xonsh.dirstack import _get_cwd
from xonsh.platform import ON_WINDOWS
from xonsh.tools import executables_in
[docs]class CommandsCache(cabc.Mapping):
"""A lazy cache representing the commands available on the file system.
The keys are the command names and the values a tuple of (loc, has_alias)
where loc is either a str pointing to the executable on the file system or
None (if no executable exists) and has_alias is a boolean flag for whether
the command has an alias.
"""
def __init__(self):
self._cmds_cache = {}
self._path_checksum = None
self._alias_checksum = None
self._path_mtime = -1
def __contains__(self, key):
return key in self.all_commands
def __iter__(self):
return iter(self.all_commands)
def __len__(self):
return len(self.all_commands)
def __getitem__(self, key):
return self.all_commands[key]
[docs] def is_empty(self):
"""Returns whether the cache is populated or not."""
return len(self._cmds_cache) == 0
@staticmethod
[docs] def get_possible_names(name):
"""Generates the possible `PATHEXT` extension variants of a given executable
name on Windows as a list, conserving the ordering in `PATHEXT`.
Returns a list as `name` being the only item in it on other platforms."""
if ON_WINDOWS:
name = name.upper()
return [
name + ext
for ext in ([''] + builtins.__xonsh_env__['PATHEXT'])
]
else:
return [name]
@property
def all_commands(self):
paths = builtins.__xonsh_env__.get('PATH', [])
pathset = frozenset(x for x in paths if os.path.isdir(x))
# did PATH change?
path_hash = hash(pathset)
cache_valid = path_hash == self._path_checksum
self._path_checksum = path_hash
# did aliases change?
alss = getattr(builtins, 'aliases', set())
al_hash = hash(frozenset(alss))
cache_valid = cache_valid and al_hash == self._alias_checksum
self._alias_checksum = al_hash
# did the contents of any directory in PATH change?
max_mtime = 0
for path in pathset:
mtime = os.stat(path).st_mtime
if mtime > max_mtime:
max_mtime = mtime
cache_valid = cache_valid and (max_mtime <= self._path_mtime)
self._path_mtime = max_mtime
if cache_valid:
return self._cmds_cache
allcmds = {}
for path in reversed(paths):
# iterate backwards so that entries at the front of PATH overwrite
# entries at the back.
for cmd in executables_in(path):
key = cmd.upper() if ON_WINDOWS else cmd
allcmds[key] = (os.path.join(path, cmd), cmd in alss)
only_alias = (None, True)
for cmd in alss:
if cmd not in allcmds:
allcmds[cmd] = only_alias
self._cmds_cache = allcmds
return allcmds
[docs] def lazyin(self, key):
"""Checks if the value is in the current cache without the potential to
update the cache. It just says whether the value is known *now*. This
may not reflect precisely what is on the $PATH.
"""
return key in self._cmds_cache
[docs] def lazyiter(self):
"""Returns an iterator over the current cache contents without the
potential to update the cache. This may not reflect what is on the
$PATH.
"""
return iter(self._cmds_cache)
[docs] def lazylen(self):
"""Returns the length of the current cache contents without the
potential to update the cache. This may not reflect precisely
what is on the $PATH.
"""
return len(self._cmds_cache)
[docs] def lazyget(self, key, default=None):
"""A lazy value getter."""
return self._cmds_cache.get(key, default)
[docs] def locate_binary(self, name):
"""Locates an executable on the file system using the cache."""
# make sure the cache is up to date by accessing the property
_ = self.all_commands
return self.lazy_locate_binary(name)
[docs] def lazy_locate_binary(self, name):
"""Locates an executable in the cache, without checking its validity."""
possibilities = self.get_possible_names(name)
if ON_WINDOWS:
# Windows users expect to be able to execute files in the same
# directory without `./`
cwd = _get_cwd()
local_bin = next((
full_name for full_name in possibilities
if os.path.isfile(full_name)
), None)
if local_bin:
if os.path.splitdrive(cwd)[0] != os.path.splitdrive(local_bin)[0]: # if cwd not on same drive as bin
return os.path.abspath(local_bin) # avoid ValueError in relpath()
else:
return os.path.abspath(os.path.relpath(local_bin, cwd))
cached = next((cmd for cmd in possibilities if cmd in self._cmds_cache), None)
if cached:
return self._cmds_cache[cached][0]
elif os.path.isfile(name) and name != os.path.basename(name):
return name