Source code for disambigufile.disambigufile
"""
Class with file-like interface to a file found in provided search path
See top level package docstring for documentation
"""
import logging
import os
import pathlib
import re
import typing
import attr
myself = pathlib.Path(__file__).stem
logger = logging.getLogger(myself)
logging.getLogger(myself).addHandler(logging.NullHandler())
try:
import optini
logger.debug('loaded optional module optini')
except ModuleNotFoundError as e:
logger.debug(f"optional module not found: {e}")
########################################################################
# exceptions
[docs]class Error(Exception):
pass
[docs]class NoMatchError(Error):
pass
[docs]class AmbiguousMatchError(Error):
def __init__(self, found):
self.found = found
self.message = f"matches found: {found}"
########################################################################
[docs]@attr.s(auto_attribs=True)
class DisFile:
"""
Class with file-like interface to a file found in provided search path
- To get filename of disambiguated file, evaluate in string context
- Supports `with` context statements
- Raises exceptions if file is ambiguous
- All module exceptions inherit from disambigufile.Error
See `help(disambigufile)` for examples
Attributes
----------
pattern : str
Regular expression describing desired match
expand : bool, default=True
Expand ~ and environment variables in path components
path : str, default=None
Directories to search (colon-separated)
subpattern : str, default=None
Regular expression describing secondary match
pathopt : str, default='path'
Option name when using configuration data
Raises
------
NoMatchError
AmbiguousMatchError
"""
pattern: str
expand: bool = True
path: str = None
subpattern: str = None
pathopt: str = 'path'
pathlist: typing.List[str] = attr.Factory(list)
def __attrs_post_init__(self):
"""Constructor"""
self._determine_pathlist()
self._search()
if len(self.found) == 0:
raise NoMatchError
if len(self.found) > 1:
raise AmbiguousMatchError(self.found)
# if no exceptions, there will be an unambiguous match
# use hit() or open() to interact with the item found
def _determine_pathlist(self):
"""Determine final path to search"""
# add any directories provided by parameter
if self.path is not None:
self.pathlist += self.path.split(':')
# add paths from options if present
try:
if self.pathopt in optini.opt:
paths = optini.opt.path.split(':')
logger.debug(f"adding paths from options: {paths}")
self.pathlist += paths
except (AttributeError, NameError):
pass
# strip out trailing slashes
self.pathlist = map(lambda x: x.rstrip('/'), self.pathlist)
# expand ~ and environment variables
if self.expand:
self.pathlist = self._expandpath(self.pathlist)
def _expandstr(self, x):
"""Expand ~ and then expand environment variables"""
logger.debug(f"expanding {x}")
return os.path.expandvars(os.path.expanduser(x))
def _expandpath(self, pathlist):
"""Expand elements of path"""
return list(map(lambda x: self._expandstr(x), pathlist))
def _search_path_for_file(self, pattern, pathlist):
"""Return list of files matching pattern in a path"""
# filter out missing directories
pathlist = filter(lambda x: os.path.isdir(x), pathlist)
files = []
for dir in pathlist:
for file in os.listdir(dir):
if re.search(pattern, file):
files.append(f"{dir}/{file}")
return(files)
def _search(self):
"""Search path for matching files"""
logger.debug(f"considering {self.pathlist}")
# search for matches with each element of path
found = self._search_path_for_file(self.pattern, self.pathlist)
if self.subpattern is not None:
# if unambiguous, found will be a single-item list
# however, directories might have multiple matches & 1 sub-match
# example, pattern = asdf, subpattern = data
# if asdf1/data and asdf2 both exist, asdf1/data is unique
newfound = []
for x in found:
if os.path.isdir(x):
submatches = filter(
lambda x: re.search(self.subpattern, x),
os.listdir(x),
)
for submatch in submatches:
newfound.append(f"{x}/{submatch}")
else:
newfound.append(x)
found = newfound
self.found = found
[docs] def hit(self):
"""return filename of disambiguated file"""
logger.debug(f"hit: {self.found[0]}")
return self.found[0]
[docs] def open(self, mode='r'):
"""open disambiguated file and return file-like object"""
self._f = open(self.hit(), mode)
return self._f
def __str__(self):
return self.hit()
def __enter__(self):
return self.open()
def __exit__(self):
self._f.close()