# -*- coding: utf-8 -*-
"""
No project is complete without a utility module.
:author: Joe Joyce <joe@decafjoe.com>
:copyright: Copyright (c) Joe Joyce and contributors, 2016-2019.
:license: BSD
"""
import contextlib
import os
import shutil
import stat
import subprocess
import tempfile
from safe.compat import input
[docs]def expand_path(path):
"""
Return absolute path, with variables and ``~`` expanded.
:param str path: Path, possibly with variables and ``~``
:return: Absolute path with special sequences expanded
:rtype: str
"""
return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
[docs]def get_executable(name):
"""
Return the full path to executable named ``name``, if it exists.
:param str name: Name of the executable to find
:return: Full path to the executable or ``None``
:rtype: :class:`str` or ``None``
"""
directories = filter(None, os.environ.get('PATH', '').split(os.pathsep))
for directory in directories:
path = os.path.join(directory.strip('"'), name)
if os.path.isfile(path) and os.access(path, os.X_OK):
return path
[docs]def prompt_bool(prompt, default=False):
"""
Prompt user for a yes or no answer and return the result as a boolean.
:param str prompt: Prompt. ``' [y/n] '`` will be appended to this value
:param bool default: Default value if user enters nothing
:return: Boolean indicating user's choice
:rtype: :class:`bool`
"""
if default is True:
choices = ' [Y/n] '
flip_if = 'n'
else:
choices = ' [y/N] '
flip_if = 'y'
response = input(prompt + choices)
if response and response[0].lower() == flip_if:
return not default
return default
[docs]@contextlib.contextmanager
def temporary_directory():
"""
Context manager that creates a temporary directory for use in the body.
Example::
with temporary_directory() as tmp:
# do stuff with tmp
The temporary directory permissions are set to 0700 before handing control
over to the body.
"""
tmp = tempfile.mkdtemp()
os.chmod(tmp, stat.S_IRWXU)
try:
yield tmp
finally:
shutil.rmtree(tmp)
[docs]class Subprocess(subprocess.Popen):
"""Subclass whose :meth:`communicate` method turns bytes into strings."""
[docs] def communicate(self, stdin=None):
"""
Override parent to make sure bytes are decoded into strings.
:param stdin: Data to send to stdin
:type stdin: :class:`str` or ``None``
:return: 2-tuple, ``(stdout, stderr)``
:rtype: 2-:func:`tuple`
"""
if stdin is not None:
stdin = stdin.encode('utf-8')
stdout, stderr = super(Subprocess, self).communicate(stdin)
if stdout is not None:
stdout = stdout.decode('utf-8')
if stderr is not None:
stderr = stderr.decode('utf-8')
return stdout, stderr