Source code for pykern.pkio
# -*- coding: utf-8 -*-
u"""Useful I/O operations
:copyright: Copyright (c) 2015 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
from __future__ import absolute_import, division, print_function
from pykern import pkcompat
import contextlib
import copy
import errno
import glob
import io
import locale
import os
import os.path
import py
import re
import shutil
import six
#: used during unit testing see ``pykern.pkunit.save_chdir``
pkunit_prefix = None
[docs]def exception_is_not_found(exc):
"""True if exception is IOError and ENOENT
Args:
exc (BaseException): to check
Returns:
bool: True if is a file not found exception.
"""
return isinstance(exc, IOError) and exc.errno == errno.ENOENT or isinstance(exc, py.error.ENOENT)
[docs]def expand_user_path(path):
"""Calls expanduser on path
If `pkunit_prefix` is set, will prefix, too.
Args:
path (str): path to expand
Returns:
py.path.Local: expanded path
"""
return py_path(path)
[docs]def has_file_extension(filename, to_check):
"""if matches any of the file extensions
Args:
filename (str|py.path.local): what to check
to_check (str|tuple|list): is without '.' and lower
Returns:
bool: if any of the extensions matches
"""
if isinstance(to_check, six.string_types):
to_check = (to_check)
e = py_path(filename).ext[1:].lower()
return e in to_check
[docs]def mkdir_parent(path):
"""Create the directories and their parents (if necessary)
Args:
path (str): dir to create
Returns:
py.path.local: path
"""
return py_path(path).ensure(dir=True)
[docs]def mkdir_parent_only(path):
"""Create the paths' parent directories.
Args:
path (str): children of dir to create
Returns:
py.path.local: parent directory of path
"""
return mkdir_parent(py_path(path).dirname)
[docs]def py_path(path=None):
"""Creates a py.path.Local object
Will expanduser, if needed.
If `pkunit_prefix` is set, will prefix, too.
Args:
path (str): path to convert (or None for current dir)
Returns:
py.path.Local: path
"""
global pkunit_prefix
res = py.path.local(path, expanduser=True)
if pkunit_prefix:
# Allow for <test>_work and <test>_data so we don't add
# prefix if there's a common parent directory.
if not str(res).startswith(pkunit_prefix.dirname):
res = pkunit_prefix.join(res)
py.path.local(res.dirname).ensure(dir=True)
return res
[docs]def read_text(filename):
"""Open file, read with preferred encoding text, and close.
Args:
filename (str or py.path.Local): File to open
Returns:
str: contest of `filename`
"""
fn = py_path(filename)
with io.open(str(fn), encoding=locale.getpreferredencoding()) as f:
return f.read();
[docs]@contextlib.contextmanager
def save_chdir(dirname, mkdir=False, is_pkunit_prefix=False):
"""Save current directory, change to directory, and restore.
Args:
dirname (str): directory to change to
mkdir (bool): Make the directory?
is_pkunit_prefix (bool): If True, sets pkunit_prefix.
Returns:
str: current directory before `chdir`
"""
global pkunit_prefix
prev_d = py.path.local().realpath()
prev_ppp = pkunit_prefix
try:
if is_pkunit_prefix:
d = py.path.local(dirname)
else:
d = py_path(dirname)
if mkdir and not d.check(dir=1):
mkdir_parent(d)
os.chdir(str(d))
if is_pkunit_prefix:
pkunit_prefix = py.path.local(d)
yield d.realpath()
finally:
os.chdir(str(prev_d))
if is_pkunit_prefix:
pkunit_prefix = prev_ppp
[docs]def sorted_glob(path):
"""sorted list of py.path.Local objects, non-recursive
Args:
path (py.path.Local or str): pattern
Returns:
list: py.path.Local objects
"""
return sorted(py_path(f) for f in glob.glob(str(path)))
[docs]def unchecked_remove(*paths):
"""Remove files or directories, ignoring OSError.
Will not remove '/' or '.'
Args:
paths (str): paths to remove
"""
cwd = py_path()
for a in paths:
p = py_path(a)
assert len(p.parts()) > 1, \
'{}: will not remove root directory'.format(p)
assert cwd != p, \
'{}: will not remove current directory'.format(p)
try:
os.remove(str(a))
except OSError:
try:
shutil.rmtree(str(a), ignore_errors=True)
except OSError:
pass
[docs]def walk_tree(dirname, file_re=None):
"""Return list files (only) as py.path's, top down, sorted
If you want to go bottom up, just reverse the list.
Args:
dirname (str): directory to walk
match_re (re or str): Optionally, only return files which match file_re
Yields:
py.path.local: paths in sorted order
"""
fr = file_re
if fr and not hasattr(fr, 'search'):
fr = re.compile(fr)
dirname = py_path(dirname).realpath()
dn = str(dirname)
res = []
for r, d, files in os.walk(dn, topdown=True, onerror=None, followlinks=False):
for f in files:
p = py_path(r).join(f)
if fr and not fr.search(dirname.bestrelpath(p)):
continue
res.append(p)
# Not an iterator, but works as one. Don't assume always will return list
return sorted(res)
[docs]def write_text(filename, contents):
"""Open file, write text with preferred encoding, and close.
Args:
filename (str or py.path.Local): File to open
contents (str): New contents
Returns:
py.path.local: `filename` as :class:`py.path.Local`
"""
fn = py_path(filename)
with io.open(str(fn), 'w', encoding=locale.getpreferredencoding()) as f:
f.write(pkcompat.locale_str(contents))
return fn