Skip to content

Commit e801c65

Browse files
committed
Fixed handling of byte paths
- path entries are always saved as strings - byte paths are converted to string paths for most functions - fixes #517
1 parent 025f06c commit e801c65

File tree

7 files changed

+63
-42
lines changed

7 files changed

+63
-42
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ the proposed changes so you can be ready.
2020
functions
2121

2222
#### Fixes
23+
* Fixed handling of byte string paths
24+
(see [#517](../../issues/517))
2325
* Fixed `os.walk` if path ends with path separator
2426
(see [#512](../../issues/512))
2527
* Fixed handling of empty path in `os.makedirs`

pyfakefs/fake_filesystem.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
from pyfakefs.helpers import (
113113
FakeStatResult, FileBufferIO, NullFileBufferIO,
114114
is_int_type, is_byte_string, is_unicode_string,
115-
make_string_path, IS_WIN)
115+
make_string_path, IS_WIN, to_string)
116116

117117
__pychecker__ = 'no-reimportself'
118118

@@ -640,10 +640,11 @@ def add_entry(self, path_object):
640640
not self.filesystem.is_windows_fs):
641641
raise OSError(errno.EACCES, 'Permission Denied', self.path)
642642

643-
if path_object.name in self.contents:
643+
path_object_name = to_string(path_object.name)
644+
if path_object_name in self.contents:
644645
self.filesystem.raise_os_error(errno.EEXIST, self.path)
645646

646-
self.contents[path_object.name] = path_object
647+
self.contents[path_object_name] = path_object
647648
path_object.parent_dir = self
648649
self.st_nlink += 1
649650
path_object.st_nlink += 1
@@ -665,7 +666,7 @@ def get_entry(self, pathname_name):
665666
KeyError: if no child exists by the specified name.
666667
"""
667668
pathname_name = self._normalized_entryname(pathname_name)
668-
return self.contents[pathname_name]
669+
return self.contents[to_string(pathname_name)]
669670

670671
def _normalized_entryname(self, pathname_name):
671672
if not self.filesystem.is_case_sensitive:
@@ -712,7 +713,7 @@ def remove_entry(self, pathname_name, recursive=True):
712713
entry.st_nlink -= 1
713714
assert entry.st_nlink >= 0
714715

715-
del self.contents[pathname_name]
716+
del self.contents[to_string(pathname_name)]
716717

717718
@property
718719
def size(self):
@@ -1736,12 +1737,6 @@ def exists(self, file_path, check_link=False):
17361737
return False
17371738
return True
17381739

1739-
@staticmethod
1740-
def _to_string(path):
1741-
if isinstance(path, bytes):
1742-
path = path.decode(locale.getpreferredencoding(False))
1743-
return path
1744-
17451740
def resolve_path(self, file_path, allow_fd=False, raw_io=True):
17461741
"""Follow a path, resolving symlinks.
17471742
@@ -1787,7 +1782,6 @@ def resolve_path(self, file_path, allow_fd=False, raw_io=True):
17871782
if file_path is None:
17881783
# file.open(None) raises TypeError, so mimic that.
17891784
raise TypeError('Expected file system path string, received None')
1790-
file_path = self._to_string(file_path)
17911785
if not file_path or not self._valid_relative_path(file_path):
17921786
# file.open('') raises OSError, so mimic that, and validate that
17931787
# all parts of a relative path exist.
@@ -2793,6 +2787,7 @@ def makedirs(self, dir_name, mode=PERM_DEF, exist_ok=False):
27932787
"""
27942788
if not dir_name:
27952789
self.raise_os_error(errno.ENOENT, '')
2790+
dir_name = to_string(dir_name)
27962791
ends_with_sep = self.ends_with_path_separator(dir_name)
27972792
dir_name = self.absnormpath(dir_name)
27982793
if (ends_with_sep and self.is_macos and
@@ -4128,6 +4123,7 @@ def makedirs(self, name, mode=PERM_DEF, exist_ok=None):
41284123

41294124
def _path_with_dir_fd(self, path, fct, dir_fd):
41304125
"""Return the path considering dir_fd. Raise on invalid parameters."""
4126+
path = to_string(path)
41314127
if dir_fd is not None:
41324128
# check if fd is supported for the built-in real function
41334129
real_fct = getattr(os, fct.__name__)

pyfakefs/fake_filesystem_unittest.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -436,20 +436,24 @@ def _is_fs_function(self, fct):
436436
return False
437437

438438
def _def_values(self, item):
439+
"""Find default arguments that are file-system functions to be
440+
patched in top-level functions and members of top-level classes."""
439441
# check for module-level functions
440-
if inspect.isfunction(item) and item.__defaults__:
441-
for i, d in enumerate(item.__defaults__):
442-
if self._is_fs_function(d):
443-
yield item, i, d
444-
if inspect.isclass(item):
442+
if inspect.isfunction(item):
443+
if item.__defaults__:
444+
for i, d in enumerate(item.__defaults__):
445+
if self._is_fs_function(d):
446+
yield item, i, d
447+
elif inspect.isclass(item):
445448
# check for methods in class (nested classes are ignored for now)
446449
try:
447450
for m in inspect.getmembers(item,
448451
predicate=inspect.isfunction):
449-
if m[1].__defaults__:
450-
for i, d in enumerate(m[1].__defaults__):
452+
m = m[1]
453+
if m.__defaults__:
454+
for i, d in enumerate(m.__defaults__):
451455
if self._is_fs_function(d):
452-
yield m[1], i, d
456+
yield m, i, d
453457
except ImportError:
454458
# Ignore ImportError: No module named '_gdbm'
455459
pass

pyfakefs/fake_scandir.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import sys
2121

2222
from pyfakefs.extra_packages import use_scandir_package
23+
from pyfakefs.helpers import to_string
2324

2425
if sys.version_info >= (3, 6):
2526
BaseClass = os.PathLike
@@ -131,7 +132,7 @@ def __init__(self, filesystem, path):
131132
self.path = ''
132133
else:
133134
self.abspath = self.filesystem.absnormpath(path)
134-
self.path = path
135+
self.path = to_string(path)
135136
contents = self.filesystem.confirmdir(self.abspath).contents
136137
self.contents_iter = iter(contents)
137138

@@ -249,7 +250,7 @@ def do_walk(top_dir, top_most=False):
249250
if not topdown:
250251
yield top_contents
251252

252-
return do_walk(top, top_most=True)
253+
return do_walk(to_string(top), top_most=True)
253254

254255

255256
class FakeScanDirModule:

pyfakefs/helpers.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def make_string_path(dir_name):
4949
return dir_name
5050

5151

52+
def to_string(path):
53+
"""Return the string representation of a byte string using the preferred
54+
encoding, or the string itself if path is a str."""
55+
if isinstance(path, bytes):
56+
return path.decode(locale.getpreferredencoding(False))
57+
return path
58+
59+
5260
class FakeStatResult:
5361
"""Mimics os.stat_result for use as return type of `stat()` and similar.
5462
This is needed as `os.stat_result` has no possibility to set
@@ -85,17 +93,17 @@ def use_float(self, val):
8593

8694
def __eq__(self, other):
8795
return (
88-
isinstance(other, FakeStatResult) and
89-
self._st_atime_ns == other._st_atime_ns and
90-
self._st_ctime_ns == other._st_ctime_ns and
91-
self._st_mtime_ns == other._st_mtime_ns and
92-
self.st_size == other.st_size and
93-
self.st_gid == other.st_gid and
94-
self.st_uid == other.st_uid and
95-
self.st_nlink == other.st_nlink and
96-
self.st_dev == other.st_dev and
97-
self.st_ino == other.st_ino and
98-
self.st_mode == other.st_mode
96+
isinstance(other, FakeStatResult) and
97+
self._st_atime_ns == other._st_atime_ns and
98+
self._st_ctime_ns == other._st_ctime_ns and
99+
self._st_mtime_ns == other._st_mtime_ns and
100+
self.st_size == other.st_size and
101+
self.st_gid == other.st_gid and
102+
self.st_uid == other.st_uid and
103+
self.st_nlink == other.st_nlink and
104+
self.st_dev == other.st_dev and
105+
self.st_ino == other.st_ino and
106+
self.st_mode == other.st_mode
99107
)
100108

101109
def __ne__(self, other):
@@ -272,6 +280,7 @@ class FileBufferIO:
272280
Uses an io.BytesIO stream for the raw data and adds handling of encoding
273281
and newlines.
274282
"""
283+
275284
def __init__(self, contents=None, linesep='\n', binary=False,
276285
newline=None, encoding=None, errors='strict'):
277286
self._newline = newline
@@ -418,5 +427,6 @@ def __getattr__(self, name):
418427

419428
class NullFileBufferIO(FileBufferIO):
420429
"""Special stream for null device. Does nothing on writing."""
430+
421431
def putvalue(self, s):
422432
pass

pyfakefs/tests/fake_os_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,18 @@ def test_lstat_trailing_sep(self):
335335
self.assertEqual(stat, self.os.lstat(
336336
self.base_path + self.path_separator() + self.path_separator()))
337337

338+
def test_stat_with_byte_string(self):
339+
stat_str = self.os.stat(self.base_path)
340+
base_path_bytes = self.base_path.encode('utf8')
341+
stat_bytes = self.os.stat(base_path_bytes)
342+
self.assertEqual(stat_bytes, stat_str)
343+
344+
def test_lstat_with_byte_string(self):
345+
stat_str = self.os.lstat(self.base_path)
346+
base_path_bytes = self.base_path.encode('utf8')
347+
stat_bytes = self.os.lstat(base_path_bytes)
348+
self.assertEqual(stat_bytes, stat_str)
349+
338350
def test_exists_with_trailing_sep(self):
339351
# regression test for #364
340352
file_path = self.make_path('alpha')

pyfakefs/tests/test_utils.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import unittest
2626

2727
from pyfakefs import fake_filesystem
28-
from pyfakefs.helpers import is_byte_string
28+
from pyfakefs.helpers import is_byte_string, to_string
2929

3030

3131
class DummyTime:
@@ -265,17 +265,13 @@ def make_path(self, *args):
265265
Always use to compose absolute paths for tests also running in the
266266
real FS.
267267
"""
268-
if isinstance(args[0], bytes):
269-
base_path = self.base_path.encode()
270-
else:
271-
base_path = self.base_path
272-
273268
if isinstance(args[0], (list, tuple)):
274-
path = base_path
269+
path = self.base_path
275270
for arg in args[0]:
276-
path = self.os.path.join(path, arg)
271+
path = self.os.path.join(path, to_string(arg))
277272
return path
278-
return self.os.path.join(base_path, *args)
273+
args = [to_string(arg) for arg in args]
274+
return self.os.path.join(self.base_path, *args)
279275

280276
def create_dir(self, dir_path):
281277
"""Create the directory at `dir_path`, including subdirectories.

0 commit comments

Comments
 (0)