Skip to content

Commit

Permalink
Pathlike (#49)
Browse files Browse the repository at this point in the history
* pathlike support

* tests for fscompat

* version bump

* str fix

* official release

* docstring fix
  • Loading branch information
willmcgugan authored Jun 11, 2017
1 parent 6b78709 commit 9489fdc
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 20 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
## [2.0.4] - 2017-06-11

### Added

- Opener extension mechanism contributed by Martin Larralde.
- Support for pathlike objects.

### Fixed

Expand All @@ -19,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- More specific error when `validatepath` throws an error about the path
argument being the wrong type, and changed from a ValueError to a
TypeError.
- Deprecated `encoding` parameter in OSFS.

## [2.0.3] - 2017-04-22

Expand Down
73 changes: 73 additions & 0 deletions fs/_fscompat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import sys

import six

try:
from os import fsencode, fsdecode
except ImportError:
def _fscodec():
encoding = sys.getfilesystemencoding()
errors = 'strict' if encoding == 'mbcs' else 'surrogateescape'

def fsencode(filename):
"""
Encode filename 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).
"""
if isinstance(filename, bytes):
return filename
elif isinstance(filename, six.text_type):
return filename.encode(encoding, errors)
else:
raise TypeError("expect string type, not %s" % type(filename).__name__)

def fsdecode(filename):
"""
Decode filename 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).
"""
if isinstance(filename, six.text_type):
return filename
elif isinstance(filename, bytes):
return filename.decode(encoding, errors)
else:
raise TypeError("expect string type, not %s" % type(filename).__name__)

return fsencode, fsdecode

fsencode, fsdecode = _fscodec()
del _fscodec

try:
from os import fspath
except ImportError:
def fspath(path):
"""Return the path representation of a path-like object.
If str or bytes is passed in, it is returned unchanged. Otherwise the
os.PathLike interface is used to get the path representation. If the
path representation is not str or bytes, TypeError is raised. If the
provided path is not str, bytes, or os.PathLike, TypeError is raised.
"""
if isinstance(path, (six.text_type, bytes)):
return path

# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path_repr = path_type.__fspath__(path)
except AttributeError:
if hasattr(path_type, '__fspath__'):
raise
else:
raise TypeError("expected string type or os.PathLike object, "
"not " + path_type.__name__)
if isinstance(path_repr, (six.text_type, bytes)):
return path_repr
else:
raise TypeError("expected {}.__fspath__() to return string type "
"not {}".format(path_type.__name__,
type(path_repr).__name__))
2 changes: 1 addition & 1 deletion fs/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.4a2"
__version__ = "2.0.4"
28 changes: 10 additions & 18 deletions fs/osfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
from .errors import FileExists
from .base import FS
from .enums import ResourceType
from ._fscompat import fsencode, fsdecode, fspath
from .info import Info
from .path import abspath, basename, normpath
from .path import basename
from .permissions import Permissions
from .error_tools import convert_os_errors
from .mode import Mode, validate_open_mode
Expand All @@ -43,19 +44,16 @@ class OSFS(FS):
"""
Create an OSFS.
:param root_path: An OS path to the location on your HD you
wish to manage.
:type root_path: str
:param root_path: An OS path or path-like object to the location on
your HD you wish to manage.
:type root_path: str or path-like
:param create: Set to ``True`` to create the root directory if it
does not already exist, otherwise the directory should exist
prior to creating the ``OSFS`` instance.
:type create: bool
:param int create_mode: The permissions that will be used to create
the directory if ``create`` is True and the path doesn't exist,
defaults to ``0o777``.
:param encoding: The encoding to use for paths, or ``None``
(default) to auto-detect.
:type encoding: str
:raises `fs.errors.CreateFailed`: If ``root_path`` does not
exists, or could not be created.
Expand All @@ -71,13 +69,11 @@ class OSFS(FS):
def __init__(self,
root_path,
create=False,
create_mode=0o777,
encoding=None):
create_mode=0o777):
"""Create an OSFS instance."""

super(OSFS, self).__init__()
self.encoding = encoding or sys.getfilesystemencoding()

root_path = fsdecode(fspath(root_path))
_root_path = os.path.expanduser(os.path.expandvars(root_path))
_root_path = os.path.normpath(os.path.abspath(_root_path))
self.root_path = _root_path
Expand Down Expand Up @@ -113,21 +109,17 @@ def __init__(self,
_meta["invalid_path_chars"] = '\0'

if 'PC_PATH_MAX' in os.pathconf_names:
root_path_safe = _root_path.encode(self.encoding) \
if six.PY2 and isinstance(_root_path, six.text_type) \
else _root_path
_meta['max_sys_path_length'] = (
os.pathconf(
root_path_safe,
fsencode(_root_path),
os.pathconf_names['PC_PATH_MAX']
)
)

def __repr__(self):
_fmt = "{}({!r}, encoding={!r})"
_fmt = "{}({!r})"
return _fmt.format(self.__class__.__name__,
self.root_path,
self.encoding)
self.root_path)

def __str__(self):
fmt = "<{} '{}'>"
Expand Down
61 changes: 61 additions & 0 deletions tests/test_fscompat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import unicode_literals

import unittest

import six

from fs._fscompat import fsencode, fsdecode, fspath


class PathMock(object):
def __init__(self, path):
self._path = path
def __fspath__(self):
return self._path


class BrokenPathMock(object):
def __init__(self, path):
self._path = path
def __fspath__(self):
return self.broken


class TestFSCompact(unittest.TestCase):

def test_fspath(self):
path = PathMock('foo')
self.assertEqual(fspath(path), 'foo')
path = PathMock(b'foo')
self.assertEqual(fspath(path), b'foo')
path = 'foo'
assert path is fspath(path)

with self.assertRaises(TypeError):
fspath(100)

with self.assertRaises(TypeError):
fspath(PathMock(5))

with self.assertRaises(AttributeError):
fspath(BrokenPathMock('foo'))

def test_fsencode(self):
encode_bytes = fsencode(b'foo')
assert isinstance(encode_bytes, bytes)
self.assertEqual(encode_bytes, b'foo')

encode_bytes = fsencode('foo')
assert isinstance(encode_bytes, bytes)
self.assertEqual(encode_bytes, b'foo')

with self.assertRaises(TypeError):
fsencode(5)

def test_fsdecode(self):
decode_text = fsdecode(b'foo')
assert isinstance(decode_text, six.text_type)
decode_text = fsdecode('foo')
assert isinstance(decode_text, six.text_type)
with self.assertRaises(TypeError):
fsdecode(5)

0 comments on commit 9489fdc

Please sign in to comment.