Skip to content

Commit 9489fdc

Browse files
authored
Pathlike (#49)
* pathlike support * tests for fscompat * version bump * str fix * official release * docstring fix
1 parent 6b78709 commit 9489fdc

File tree

5 files changed

+148
-20
lines changed

5 files changed

+148
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7-
## [Unreleased]
7+
## [2.0.4] - 2017-06-11
88

99
### Added
1010

1111
- Opener extension mechanism contributed by Martin Larralde.
12+
- Support for pathlike objects.
1213

1314
### Fixed
1415

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

2325
## [2.0.3] - 2017-04-22
2426

fs/_fscompat.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import sys
2+
3+
import six
4+
5+
try:
6+
from os import fsencode, fsdecode
7+
except ImportError:
8+
def _fscodec():
9+
encoding = sys.getfilesystemencoding()
10+
errors = 'strict' if encoding == 'mbcs' else 'surrogateescape'
11+
12+
def fsencode(filename):
13+
"""
14+
Encode filename to the filesystem encoding with 'surrogateescape' error
15+
handler, return bytes unchanged. On Windows, use 'strict' error handler if
16+
the file system encoding is 'mbcs' (which is the default encoding).
17+
"""
18+
if isinstance(filename, bytes):
19+
return filename
20+
elif isinstance(filename, six.text_type):
21+
return filename.encode(encoding, errors)
22+
else:
23+
raise TypeError("expect string type, not %s" % type(filename).__name__)
24+
25+
def fsdecode(filename):
26+
"""
27+
Decode filename from the filesystem encoding with 'surrogateescape' error
28+
handler, return str unchanged. On Windows, use 'strict' error handler if
29+
the file system encoding is 'mbcs' (which is the default encoding).
30+
"""
31+
if isinstance(filename, six.text_type):
32+
return filename
33+
elif isinstance(filename, bytes):
34+
return filename.decode(encoding, errors)
35+
else:
36+
raise TypeError("expect string type, not %s" % type(filename).__name__)
37+
38+
return fsencode, fsdecode
39+
40+
fsencode, fsdecode = _fscodec()
41+
del _fscodec
42+
43+
try:
44+
from os import fspath
45+
except ImportError:
46+
def fspath(path):
47+
"""Return the path representation of a path-like object.
48+
49+
If str or bytes is passed in, it is returned unchanged. Otherwise the
50+
os.PathLike interface is used to get the path representation. If the
51+
path representation is not str or bytes, TypeError is raised. If the
52+
provided path is not str, bytes, or os.PathLike, TypeError is raised.
53+
"""
54+
if isinstance(path, (six.text_type, bytes)):
55+
return path
56+
57+
# Work from the object's type to match method resolution of other magic
58+
# methods.
59+
path_type = type(path)
60+
try:
61+
path_repr = path_type.__fspath__(path)
62+
except AttributeError:
63+
if hasattr(path_type, '__fspath__'):
64+
raise
65+
else:
66+
raise TypeError("expected string type or os.PathLike object, "
67+
"not " + path_type.__name__)
68+
if isinstance(path_repr, (six.text_type, bytes)):
69+
return path_repr
70+
else:
71+
raise TypeError("expected {}.__fspath__() to return string type "
72+
"not {}".format(path_type.__name__,
73+
type(path_repr).__name__))

fs/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.4a2"
1+
__version__ = "2.0.4"

fs/osfs.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
from .errors import FileExists
2626
from .base import FS
2727
from .enums import ResourceType
28+
from ._fscompat import fsencode, fsdecode, fspath
2829
from .info import Info
29-
from .path import abspath, basename, normpath
30+
from .path import basename
3031
from .permissions import Permissions
3132
from .error_tools import convert_os_errors
3233
from .mode import Mode, validate_open_mode
@@ -43,19 +44,16 @@ class OSFS(FS):
4344
"""
4445
Create an OSFS.
4546
46-
:param root_path: An OS path to the location on your HD you
47-
wish to manage.
48-
:type root_path: str
47+
:param root_path: An OS path or path-like object to the location on
48+
your HD you wish to manage.
49+
:type root_path: str or path-like
4950
:param create: Set to ``True`` to create the root directory if it
5051
does not already exist, otherwise the directory should exist
5152
prior to creating the ``OSFS`` instance.
5253
:type create: bool
5354
:param int create_mode: The permissions that will be used to create
5455
the directory if ``create`` is True and the path doesn't exist,
5556
defaults to ``0o777``.
56-
:param encoding: The encoding to use for paths, or ``None``
57-
(default) to auto-detect.
58-
:type encoding: str
5957
:raises `fs.errors.CreateFailed`: If ``root_path`` does not
6058
exists, or could not be created.
6159
@@ -71,13 +69,11 @@ class OSFS(FS):
7169
def __init__(self,
7270
root_path,
7371
create=False,
74-
create_mode=0o777,
75-
encoding=None):
72+
create_mode=0o777):
7673
"""Create an OSFS instance."""
7774

7875
super(OSFS, self).__init__()
79-
self.encoding = encoding or sys.getfilesystemencoding()
80-
76+
root_path = fsdecode(fspath(root_path))
8177
_root_path = os.path.expanduser(os.path.expandvars(root_path))
8278
_root_path = os.path.normpath(os.path.abspath(_root_path))
8379
self.root_path = _root_path
@@ -113,21 +109,17 @@ def __init__(self,
113109
_meta["invalid_path_chars"] = '\0'
114110

115111
if 'PC_PATH_MAX' in os.pathconf_names:
116-
root_path_safe = _root_path.encode(self.encoding) \
117-
if six.PY2 and isinstance(_root_path, six.text_type) \
118-
else _root_path
119112
_meta['max_sys_path_length'] = (
120113
os.pathconf(
121-
root_path_safe,
114+
fsencode(_root_path),
122115
os.pathconf_names['PC_PATH_MAX']
123116
)
124117
)
125118

126119
def __repr__(self):
127-
_fmt = "{}({!r}, encoding={!r})"
120+
_fmt = "{}({!r})"
128121
return _fmt.format(self.__class__.__name__,
129-
self.root_path,
130-
self.encoding)
122+
self.root_path)
131123

132124
def __str__(self):
133125
fmt = "<{} '{}'>"

tests/test_fscompat.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import unicode_literals
2+
3+
import unittest
4+
5+
import six
6+
7+
from fs._fscompat import fsencode, fsdecode, fspath
8+
9+
10+
class PathMock(object):
11+
def __init__(self, path):
12+
self._path = path
13+
def __fspath__(self):
14+
return self._path
15+
16+
17+
class BrokenPathMock(object):
18+
def __init__(self, path):
19+
self._path = path
20+
def __fspath__(self):
21+
return self.broken
22+
23+
24+
class TestFSCompact(unittest.TestCase):
25+
26+
def test_fspath(self):
27+
path = PathMock('foo')
28+
self.assertEqual(fspath(path), 'foo')
29+
path = PathMock(b'foo')
30+
self.assertEqual(fspath(path), b'foo')
31+
path = 'foo'
32+
assert path is fspath(path)
33+
34+
with self.assertRaises(TypeError):
35+
fspath(100)
36+
37+
with self.assertRaises(TypeError):
38+
fspath(PathMock(5))
39+
40+
with self.assertRaises(AttributeError):
41+
fspath(BrokenPathMock('foo'))
42+
43+
def test_fsencode(self):
44+
encode_bytes = fsencode(b'foo')
45+
assert isinstance(encode_bytes, bytes)
46+
self.assertEqual(encode_bytes, b'foo')
47+
48+
encode_bytes = fsencode('foo')
49+
assert isinstance(encode_bytes, bytes)
50+
self.assertEqual(encode_bytes, b'foo')
51+
52+
with self.assertRaises(TypeError):
53+
fsencode(5)
54+
55+
def test_fsdecode(self):
56+
decode_text = fsdecode(b'foo')
57+
assert isinstance(decode_text, six.text_type)
58+
decode_text = fsdecode('foo')
59+
assert isinstance(decode_text, six.text_type)
60+
with self.assertRaises(TypeError):
61+
fsdecode(5)

0 commit comments

Comments
 (0)