Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix breaking pathlib change in 3.10 beta1 #594

Merged
merged 9 commits into from
May 14, 2021
24 changes: 17 additions & 7 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macOS-latest, windows-2016]
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-beta.1]
include:
- python-version: pypy3
os: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

Expand Down Expand Up @@ -79,18 +79,28 @@ jobs:
shell: bash
- name: Install extra dependencies
run: |
pip install -r extra_requirements.txt
# some extra dependencies are not avaialble in 3.10 Beta yet
# so we exclude it from all tests on extra dependencies
if [[ '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
pip install -r extra_requirements.txt
fi
shell: bash
- name: Run unit tests with extra packages as non-root user
run: |
python -m pyfakefs.tests.all_tests
if [[ '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
python -m pyfakefs.tests.all_tests
fi
shell: bash
- name: Run pytest tests
run: |
export PY_VERSION=${{ matrix.python-version }}
$GITHUB_WORKSPACE/.github/workflows/run_pytest.sh
if [[ '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
export PY_VERSION=${{ matrix.python-version }}
$GITHUB_WORKSPACE/.github/workflows/run_pytest.sh
fi
shell: bash
- name: Run performance tests
run: |
if [[ '${{ matrix.os }}' != 'macOS-latest' ]]; then
if [[ '${{ matrix.os }}' != 'macOS-latest' && '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
export TEST_PERFORMANCE=1
python -m pyfakefs.tests.performance_test
fi
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The released versions correspond to PyPi releases.
### Fixes
* correctly handle byte paths in `os.path.exists`
(see [#595](../../issues/595))
* Update `fake_pathlib` to support changes coming in Python 3.10
([see](https://github.com/python/cpython/pull/19342))

## [Version 4.4.0](https://pypi.python.org/pypi/pyfakefs/4.4.0) (2021-02-24)
Adds better support for Python 3.8 / 3.9.
Expand Down
15 changes: 9 additions & 6 deletions pyfakefs/fake_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
from pyfakefs.helpers import (
FakeStatResult, BinaryBufferIO, TextBufferIO,
is_int_type, is_byte_string, is_unicode_string,
make_string_path, IS_WIN, to_string, matching_string
make_string_path, IS_WIN, to_string, matching_string, real_encoding
)
from pyfakefs import __version__ # noqa: F401 for upwards compatibility

Expand Down Expand Up @@ -293,7 +293,7 @@ def __init__(self, name, st_mode=S_IFREG | PERM_DEF_FILE,
if st_mode >> 12 == 0:
st_mode |= S_IFREG
self.stat_result.st_mode = st_mode
self.encoding = encoding
self.encoding = real_encoding(encoding)
self.errors = errors or 'strict'
self._byte_contents = self._encode_contents(contents)
self.stat_result.st_size = (
Expand Down Expand Up @@ -430,7 +430,7 @@ def set_contents(self, contents, encoding=None):
OSError: if `st_size` is not a non-negative integer,
or if it exceeds the available file system space.
"""
self.encoding = encoding
self.encoding = real_encoding(encoding)
changed = self._set_initial_contents(contents)
if self._side_effect is not None:
self._side_effect(self)
Expand Down Expand Up @@ -1177,9 +1177,12 @@ def stat(self, entry_path, follow_symlinks=True):
OSError: if the filesystem object doesn't exist.
"""
# stat should return the tuple representing return value of os.stat
file_object = self.resolve(
entry_path, follow_symlinks,
allow_fd=True, check_read_perm=False)
try:
file_object = self.resolve(
entry_path, follow_symlinks,
allow_fd=True, check_read_perm=False)
except TypeError:
file_object = self.resolve(entry_path)
if not is_root():
# make sure stat raises if a parent dir is not readable
parent_dir = file_object.parent_dir
Expand Down
64 changes: 50 additions & 14 deletions pyfakefs/fake_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def init_module(filesystem):

def _wrap_strfunc(strfunc):
@functools.wraps(strfunc)
def _wrapped(pathobj, *args):
return strfunc(pathobj.filesystem, str(pathobj), *args)
def _wrapped(pathobj, *args, **kwargs):
return strfunc(pathobj.filesystem, str(pathobj), *args, **kwargs)

return staticmethod(_wrapped)

Expand Down Expand Up @@ -94,19 +94,24 @@ class _FakeAccessor(accessor):

listdir = _wrap_strfunc(FakeFilesystem.listdir)

chmod = _wrap_strfunc(FakeFilesystem.chmod)

if use_scandir:
scandir = _wrap_strfunc(fake_scandir.scandir)

if hasattr(os, "lchmod"):
lchmod = _wrap_strfunc(lambda fs, path, mode: FakeFilesystem.chmod(
fs, path, mode, follow_symlinks=False))
chmod = _wrap_strfunc(FakeFilesystem.chmod)
else:
def lchmod(self, pathobj, mode):
def lchmod(self, pathobj, *args, **kwargs):
"""Raises not implemented for Windows systems."""
raise NotImplementedError("lchmod() not available on this system")

def chmod(self, pathobj, *args, **kwargs):
if "follow_symlinks" in kwargs and not kwargs["follow_symlinks"]:
raise NotImplementedError(
"lchmod() not available on this system")
return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs)

mkdir = _wrap_strfunc(FakeFilesystem.makedir)

unlink = _wrap_strfunc(FakeFilesystem.remove)
Expand All @@ -124,13 +129,21 @@ def lchmod(self, pathobj, mode):
FakeFilesystem.create_symlink(fs, file_path, link_target,
create_missing_dirs=False))

if sys.version_info >= (3, 8):
if (3, 8) <= sys.version_info < (3, 10):
link_to = _wrap_binary_strfunc(
lambda fs, file_path, link_target:
FakeFilesystem.link(fs, file_path, link_target))

if sys.version_info >= (3, 9):
readlink = _wrap_strfunc(FakeFilesystem.readlink)
if sys.version_info >= (3, 10):
link = _wrap_binary_strfunc(
lambda fs, file_path, link_target:
FakeFilesystem.link(fs, file_path, link_target))

# this will use the fake filesystem because os is patched
def getcwd(self):
return os.getcwd()

readlink = _wrap_strfunc(FakeFilesystem.readlink)

utime = _wrap_strfunc(FakeFilesystem.utime)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the above section I am 99% sure I am doing something wrong/bad here -- but the gist of this is it looks like we needed to add some methods to the FakeAccessor object -- I am assuming(?) its not a great idea to just attach os methods here because we would want to have the accessor using the patched methods? But this does seem to get more of the tests passing so figured id commit it to try to be as helpful as I can... (even if it isn't much :))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it accordingly. Regarding getcwd - if using os inside the function, as I do it now, it will be patched, so it will behave correctly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed realpath as it doesn't seem to be really needed, but maybe I'm wrong here.

Expand Down Expand Up @@ -461,19 +474,42 @@ def __new__(cls, *args, **kwargs):
cls = (FakePathlibModule.WindowsPath
if cls.filesystem.is_windows_fs
else FakePathlibModule.PosixPath)
self = cls._from_parts(args, init=True)
self = cls._from_parts(args)
return self

def _path(self):
"""Returns the underlying path string as used by the fake filesystem.
"""
return str(self)
@classmethod
def _from_parts(cls, args, init=False): # pylint: disable=unused-argument
# Overwritten to call _init to set the fake accessor,
# which is not done since Python 3.10
self = object.__new__(cls)
self._init()
drv, root, parts = self._parse_args(args)
self._drv = drv
self._root = root
self._parts = parts
return self

@classmethod
def _from_parsed_parts(cls, drv, root, parts):
# Overwritten to call _init to set the fake accessor,
# which is not done since Python 3.10
self = object.__new__(cls)
self._init()
self._drv = drv
self._root = root
self._parts = parts
return self

def _init(self, template=None):
"""Initializer called from base class."""
self._accessor = _fake_accessor
self._closed = False

def _path(self):
"""Returns the underlying path string as used by the fake filesystem.
"""
return str(self)

@classmethod
def cwd(cls):
"""Return a new path pointing to the current working directory
Expand Down Expand Up @@ -722,7 +758,7 @@ def __new__(cls, *args, **kwargs):
if cls is RealPathlibModule.Path:
cls = (RealPathlibModule.WindowsPath if os.name == 'nt'
else RealPathlibModule.PosixPath)
self = cls._from_parts(args, init=True)
self = cls._from_parts(args)
return self


Expand Down
9 changes: 9 additions & 0 deletions pyfakefs/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ def to_string(path):
return path


def real_encoding(encoding):
"""Since Python 3.10, the new function ``io.text_encoding`` returns
"locale" as the encoding if None is defined. This will be handled
as no encoding in pyfakefs."""
if sys.version_info >= (3, 10):
return encoding if encoding != "locale" else None
return encoding


def matching_string(matched, string):
"""Return the string as byte or unicode depending
on the type of matched, assuming string is an ASCII string.
Expand Down
27 changes: 22 additions & 5 deletions pyfakefs/tests/fake_pathlib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,11 @@ def test_chmod(self):
# we get stat.S_IFLNK | 0o755 under MacOs
self.assertEqual(link_stat.st_mode, stat.S_IFLNK | 0o777)

@unittest.skipIf(sys.platform == 'darwin',
'Different behavior under MacOs')
def test_lchmod(self):
self.skip_if_symlink_not_supported()
if (sys.version_info >= (3, 10) and self.use_real_fs() and
'chmod' not in os.supports_follow_symlinks):
raise unittest.SkipTest('follow_symlinks not available for chmod')
file_stat = self.os.stat(self.file_path)
link_stat = self.os.lstat(self.file_link_path)
if not hasattr(os, "lchmod"):
Expand All @@ -390,8 +391,9 @@ def test_lchmod(self):
else:
self.path(self.file_link_path).lchmod(0o444)
self.assertEqual(file_stat.st_mode, stat.S_IFREG | 0o666)
# we get stat.S_IFLNK | 0o755 under MacOs
self.assertEqual(link_stat.st_mode, stat.S_IFLNK | 0o444)
# the exact mode depends on OS and Python version
self.assertEqual(link_stat.st_mode & 0o777700,
stat.S_IFLNK | 0o700)

def test_resolve(self):
self.create_dir(self.make_path('antoine', 'docs'))
Expand Down Expand Up @@ -968,7 +970,22 @@ def test_symlink(self):
def test_stat(self):
path = self.make_path('foo', 'bar', 'baz')
self.create_file(path, contents='1234567')
self.assertEqual(self.os.stat(path), self.os.stat(self.path(path)))
self.assertEqual(self.os.stat(path), self.path(path).stat())

@unittest.skipIf(sys.version_info < (3, 10), "New in Python 3.10")
def test_stat_follow_symlinks(self):
self.check_posix_only()
directory = self.make_path('foo')
base_name = 'bar'
file_path = self.path(self.os.path.join(directory, base_name))
link_path = self.path(self.os.path.join(directory, 'link'))
contents = "contents"
self.create_file(file_path, contents=contents)
self.create_symlink(link_path, base_name)
self.assertEqual(len(contents),
link_path.stat(follow_symlinks=True)[stat.ST_SIZE])
self.assertEqual(len(base_name),
link_path.stat(follow_symlinks=False)[stat.ST_SIZE])

def test_utime(self):
path = self.make_path('some_file')
Expand Down