Merge pull request #2004 from xonsh/vox-fs

PEP 519 for Vox
This commit is contained in:
Anthony Scopatz 2016-12-16 00:42:32 -08:00 committed by GitHub
commit 7d3892d1f0
5 changed files with 139 additions and 2 deletions

13
news/vox-fs.rst Normal file
View file

@ -0,0 +1,13 @@
**Added:** None
**Changed:**
* ``voxapi.Vox`` now supports ``pathlib.Path`` and ``PathLike`` objects as virtual environment identifiers
**Deprecated:** None
**Removed:** None
**Fixed:** None
**Security:** None

View file

@ -125,6 +125,30 @@ def test_crud_subdir(xonsh_builtins, tmpdir):
assert not tmpdir.join('spam', 'eggs').check()
try:
import pathlib
except ImportError:
pass
else:
@skip_if_on_conda
def test_crud_path(xonsh_builtins, tmpdir):
"""
Creates a virtual environment, gets it, enumerates it, and then deletes it.
"""
tmp = pathlib.Path(str(tmpdir))
vox = Vox()
vox.create(tmp)
assert stat.S_ISDIR(tmpdir.join('lib').stat().mode)
ve = vox[tmp]
assert ve.env == str(tmp)
assert os.path.isdir(ve.bin)
del vox[tmp]
assert not tmpdir.check()
@skip_if_on_conda
def test_crud_subdir(xonsh_builtins, tmpdir):

View file

@ -1,7 +1,7 @@
__version__ = '0.4.7'
# amalgamate exclude jupyter_kernel parser_table parser_test_table pyghooks
# amalgamate exclude winutils wizard pytest_plugin
# amalgamate exclude winutils wizard pytest_plugin fs
import os as _os
if _os.getenv('XONSH_DEBUG', ''):
pass

94
xonsh/fs.py Normal file
View file

@ -0,0 +1,94 @@
"""
Backported functions to implement the PEP 519 (Adding a file system path protocol) API.
"""
import abc
import sys
import io
import pathlib
try:
from os import PathLike, fspath, fsencode, fsdecode
except ImportError:
class PathLike(abc.ABC):
"""Abstract base class for implementing the file system path protocol."""
@abc.abstractmethod
def __fspath__(self):
"""Return the file system path representation of the object."""
raise NotImplementedError
PathLike.register(pathlib.Path)
def fspath(path):
"""Return the string representation of the path.
If str or bytes is passed in, it is returned unchanged. If __fspath__()
returns something other than str or bytes then TypeError is raised. If
this function is given something that is not str, bytes, or os.PathLike
then TypeError is raised.
"""
if isinstance(path, (str, bytes)):
return path
if isinstance(path, pathlib.Path):
return str(path)
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path = path_type.__fspath__(path)
except AttributeError:
if hasattr(path_type, '__fspath__'):
raise
else:
if isinstance(path, (str, bytes)):
return path
else:
raise TypeError("expected __fspath__() to return str or bytes, "
"not " + type(path).__name__)
raise TypeError("expected str, bytes or os.PathLike object, not "
+ path_type.__name__)
def _fscodec():
encoding = sys.getfilesystemencoding()
if encoding == 'mbcs':
errors = 'strict'
else:
errors = 'surrogateescape'
def fsencode(filename):
"""Encode filename (an os.PathLike, bytes, or str) to the filesystem
encoding with 'surrogateescape' error handler, return bytes unchanged.
On Windows, use 'strict' error handler if the file system encoding is
'mbcs' (which is the default encoding).
"""
filename = fspath(filename) # Does type-checking of `filename`.
if isinstance(filename, str):
return filename.encode(encoding, errors)
else:
return filename
def fsdecode(filename):
"""Decode filename (an os.PathLike, bytes, or str) from the filesystem
encoding with 'surrogateescape' error handler, return str unchanged. On
Windows, use 'strict' error handler if the file system encoding is
'mbcs' (which is the default encoding).
"""
filename = fspath(filename) # Does type-checking of `filename`.
if isinstance(filename, bytes):
return filename.decode(encoding, errors)
else:
return filename
return fsencode, fsdecode
fsencode, fsdecode = _fscodec()
del _fscodec
def open(file, *pargs, **kwargs):
if isinstance(file, PathLike):
file = fspath(file)
return io.open(file, *pargs, **kwargs)

View file

@ -16,6 +16,7 @@ import builtins
import collections.abc
from xonsh.platform import ON_POSIX, ON_WINDOWS
from xonsh.fs import PathLike, fspath
# This is because builtins aren't globally created during testing.
# FIXME: Is there a better way?
@ -132,7 +133,10 @@ class Vox(collections.abc.Mapping):
"""
# NOTE: clear=True is the same as delete then create.
# NOTE: upgrade=True is its own method
env_path = os.path.join(self.venvdir, name)
if isinstance(name, PathLike):
env_path = fspath(name)
else:
env_path = os.path.join(self.venvdir, name)
if not self._check_reserved(env_path):
raise ValueError("venv can't contain reserved names ({})".format(', '.join(_subdir_names())))
venv.create(
@ -194,6 +198,8 @@ class Vox(collections.abc.Mapping):
"""
if name is ...:
env_paths = [builtins.__xonsh_env__['VIRTUAL_ENV']]
elif isinstance(name, PathLike):
env_paths = [fspath(name)]
else:
if not self._check_reserved(name):
# Don't allow a venv that could be a venv special dir