Skip to content

Commit a8a70bb

Browse files
Add support for Python 3.10 beta1 (#594)
- add Python 3.10-beta1 to CI - adapt fake pathlib, fix pathlib.Path methods link_to, getcwd, lchmod - handle dummy encoding "locale" introduced in Python 3.10 - do not test extra dependencies with Python 3.10 (some are not available)
1 parent 8ed31b7 commit a8a70bb

File tree

6 files changed

+109
-32
lines changed

6 files changed

+109
-32
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ jobs:
2929
fail-fast: false
3030
matrix:
3131
os: [ubuntu-latest, macOS-latest, windows-2016]
32-
python-version: [3.6, 3.7, 3.8, 3.9]
32+
python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-beta.1]
3333
include:
3434
- python-version: pypy3
3535
os: ubuntu-latest
3636

3737
steps:
3838
- uses: actions/checkout@v2
3939
- name: Set up Python ${{ matrix.python-version }}
40-
uses: actions/setup-python@v1
40+
uses: actions/setup-python@v2
4141
with:
4242
python-version: ${{ matrix.python-version }}
4343

@@ -79,18 +79,28 @@ jobs:
7979
shell: bash
8080
- name: Install extra dependencies
8181
run: |
82-
pip install -r extra_requirements.txt
82+
# some extra dependencies are not avaialble in 3.10 Beta yet
83+
# so we exclude it from all tests on extra dependencies
84+
if [[ '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
85+
pip install -r extra_requirements.txt
86+
fi
87+
shell: bash
8388
- name: Run unit tests with extra packages as non-root user
8489
run: |
85-
python -m pyfakefs.tests.all_tests
90+
if [[ '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
91+
python -m pyfakefs.tests.all_tests
92+
fi
93+
shell: bash
8694
- name: Run pytest tests
8795
run: |
88-
export PY_VERSION=${{ matrix.python-version }}
89-
$GITHUB_WORKSPACE/.github/workflows/run_pytest.sh
96+
if [[ '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
97+
export PY_VERSION=${{ matrix.python-version }}
98+
$GITHUB_WORKSPACE/.github/workflows/run_pytest.sh
99+
fi
90100
shell: bash
91101
- name: Run performance tests
92102
run: |
93-
if [[ '${{ matrix.os }}' != 'macOS-latest' ]]; then
103+
if [[ '${{ matrix.os }}' != 'macOS-latest' && '${{ matrix.python-version }}' != '3.10.0-beta.1' ]]; then
94104
export TEST_PERFORMANCE=1
95105
python -m pyfakefs.tests.performance_test
96106
fi

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ The released versions correspond to PyPi releases.
1212
### Fixes
1313
* correctly handle byte paths in `os.path.exists`
1414
(see [#595](../../issues/595))
15+
* Update `fake_pathlib` to support changes coming in Python 3.10
16+
([see](https://github.com/python/cpython/pull/19342))
1517

1618
## [Version 4.4.0](https://pypi.python.org/pypi/pyfakefs/4.4.0) (2021-02-24)
1719
Adds better support for Python 3.8 / 3.9.

pyfakefs/fake_filesystem.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
from pyfakefs.helpers import (
115115
FakeStatResult, BinaryBufferIO, TextBufferIO,
116116
is_int_type, is_byte_string, is_unicode_string,
117-
make_string_path, IS_WIN, to_string, matching_string
117+
make_string_path, IS_WIN, to_string, matching_string, real_encoding
118118
)
119119
from pyfakefs import __version__ # noqa: F401 for upwards compatibility
120120

@@ -293,7 +293,7 @@ def __init__(self, name, st_mode=S_IFREG | PERM_DEF_FILE,
293293
if st_mode >> 12 == 0:
294294
st_mode |= S_IFREG
295295
self.stat_result.st_mode = st_mode
296-
self.encoding = encoding
296+
self.encoding = real_encoding(encoding)
297297
self.errors = errors or 'strict'
298298
self._byte_contents = self._encode_contents(contents)
299299
self.stat_result.st_size = (
@@ -430,7 +430,7 @@ def set_contents(self, contents, encoding=None):
430430
OSError: if `st_size` is not a non-negative integer,
431431
or if it exceeds the available file system space.
432432
"""
433-
self.encoding = encoding
433+
self.encoding = real_encoding(encoding)
434434
changed = self._set_initial_contents(contents)
435435
if self._side_effect is not None:
436436
self._side_effect(self)
@@ -1177,9 +1177,12 @@ def stat(self, entry_path, follow_symlinks=True):
11771177
OSError: if the filesystem object doesn't exist.
11781178
"""
11791179
# stat should return the tuple representing return value of os.stat
1180-
file_object = self.resolve(
1181-
entry_path, follow_symlinks,
1182-
allow_fd=True, check_read_perm=False)
1180+
try:
1181+
file_object = self.resolve(
1182+
entry_path, follow_symlinks,
1183+
allow_fd=True, check_read_perm=False)
1184+
except TypeError:
1185+
file_object = self.resolve(entry_path)
11831186
if not is_root():
11841187
# make sure stat raises if a parent dir is not readable
11851188
parent_dir = file_object.parent_dir

pyfakefs/fake_pathlib.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def init_module(filesystem):
5353

5454
def _wrap_strfunc(strfunc):
5555
@functools.wraps(strfunc)
56-
def _wrapped(pathobj, *args):
57-
return strfunc(pathobj.filesystem, str(pathobj), *args)
56+
def _wrapped(pathobj, *args, **kwargs):
57+
return strfunc(pathobj.filesystem, str(pathobj), *args, **kwargs)
5858

5959
return staticmethod(_wrapped)
6060

@@ -94,19 +94,24 @@ class _FakeAccessor(accessor):
9494

9595
listdir = _wrap_strfunc(FakeFilesystem.listdir)
9696

97-
chmod = _wrap_strfunc(FakeFilesystem.chmod)
98-
9997
if use_scandir:
10098
scandir = _wrap_strfunc(fake_scandir.scandir)
10199

102100
if hasattr(os, "lchmod"):
103101
lchmod = _wrap_strfunc(lambda fs, path, mode: FakeFilesystem.chmod(
104102
fs, path, mode, follow_symlinks=False))
103+
chmod = _wrap_strfunc(FakeFilesystem.chmod)
105104
else:
106-
def lchmod(self, pathobj, mode):
105+
def lchmod(self, pathobj, *args, **kwargs):
107106
"""Raises not implemented for Windows systems."""
108107
raise NotImplementedError("lchmod() not available on this system")
109108

109+
def chmod(self, pathobj, *args, **kwargs):
110+
if "follow_symlinks" in kwargs and not kwargs["follow_symlinks"]:
111+
raise NotImplementedError(
112+
"lchmod() not available on this system")
113+
return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs)
114+
110115
mkdir = _wrap_strfunc(FakeFilesystem.makedir)
111116

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

127-
if sys.version_info >= (3, 8):
132+
if (3, 8) <= sys.version_info < (3, 10):
128133
link_to = _wrap_binary_strfunc(
129134
lambda fs, file_path, link_target:
130135
FakeFilesystem.link(fs, file_path, link_target))
131136

132-
if sys.version_info >= (3, 9):
133-
readlink = _wrap_strfunc(FakeFilesystem.readlink)
137+
if sys.version_info >= (3, 10):
138+
link = _wrap_binary_strfunc(
139+
lambda fs, file_path, link_target:
140+
FakeFilesystem.link(fs, file_path, link_target))
141+
142+
# this will use the fake filesystem because os is patched
143+
def getcwd(self):
144+
return os.getcwd()
145+
146+
readlink = _wrap_strfunc(FakeFilesystem.readlink)
134147

135148
utime = _wrap_strfunc(FakeFilesystem.utime)
136149

@@ -461,19 +474,42 @@ def __new__(cls, *args, **kwargs):
461474
cls = (FakePathlibModule.WindowsPath
462475
if cls.filesystem.is_windows_fs
463476
else FakePathlibModule.PosixPath)
464-
self = cls._from_parts(args, init=True)
477+
self = cls._from_parts(args)
465478
return self
466479

467-
def _path(self):
468-
"""Returns the underlying path string as used by the fake filesystem.
469-
"""
470-
return str(self)
480+
@classmethod
481+
def _from_parts(cls, args, init=False): # pylint: disable=unused-argument
482+
# Overwritten to call _init to set the fake accessor,
483+
# which is not done since Python 3.10
484+
self = object.__new__(cls)
485+
self._init()
486+
drv, root, parts = self._parse_args(args)
487+
self._drv = drv
488+
self._root = root
489+
self._parts = parts
490+
return self
491+
492+
@classmethod
493+
def _from_parsed_parts(cls, drv, root, parts):
494+
# Overwritten to call _init to set the fake accessor,
495+
# which is not done since Python 3.10
496+
self = object.__new__(cls)
497+
self._init()
498+
self._drv = drv
499+
self._root = root
500+
self._parts = parts
501+
return self
471502

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

508+
def _path(self):
509+
"""Returns the underlying path string as used by the fake filesystem.
510+
"""
511+
return str(self)
512+
477513
@classmethod
478514
def cwd(cls):
479515
"""Return a new path pointing to the current working directory
@@ -722,7 +758,7 @@ def __new__(cls, *args, **kwargs):
722758
if cls is RealPathlibModule.Path:
723759
cls = (RealPathlibModule.WindowsPath if os.name == 'nt'
724760
else RealPathlibModule.PosixPath)
725-
self = cls._from_parts(args, init=True)
761+
self = cls._from_parts(args)
726762
return self
727763

728764

pyfakefs/helpers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ def to_string(path):
5757
return path
5858

5959

60+
def real_encoding(encoding):
61+
"""Since Python 3.10, the new function ``io.text_encoding`` returns
62+
"locale" as the encoding if None is defined. This will be handled
63+
as no encoding in pyfakefs."""
64+
if sys.version_info >= (3, 10):
65+
return encoding if encoding != "locale" else None
66+
return encoding
67+
68+
6069
def matching_string(matched, string):
6170
"""Return the string as byte or unicode depending
6271
on the type of matched, assuming string is an ASCII string.

pyfakefs/tests/fake_pathlib_test.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,11 @@ def test_chmod(self):
378378
# we get stat.S_IFLNK | 0o755 under MacOs
379379
self.assertEqual(link_stat.st_mode, stat.S_IFLNK | 0o777)
380380

381-
@unittest.skipIf(sys.platform == 'darwin',
382-
'Different behavior under MacOs')
383381
def test_lchmod(self):
384382
self.skip_if_symlink_not_supported()
383+
if (sys.version_info >= (3, 10) and self.use_real_fs() and
384+
'chmod' not in os.supports_follow_symlinks):
385+
raise unittest.SkipTest('follow_symlinks not available for chmod')
385386
file_stat = self.os.stat(self.file_path)
386387
link_stat = self.os.lstat(self.file_link_path)
387388
if not hasattr(os, "lchmod"):
@@ -390,8 +391,9 @@ def test_lchmod(self):
390391
else:
391392
self.path(self.file_link_path).lchmod(0o444)
392393
self.assertEqual(file_stat.st_mode, stat.S_IFREG | 0o666)
393-
# we get stat.S_IFLNK | 0o755 under MacOs
394-
self.assertEqual(link_stat.st_mode, stat.S_IFLNK | 0o444)
394+
# the exact mode depends on OS and Python version
395+
self.assertEqual(link_stat.st_mode & 0o777700,
396+
stat.S_IFLNK | 0o700)
395397

396398
def test_resolve(self):
397399
self.create_dir(self.make_path('antoine', 'docs'))
@@ -968,7 +970,22 @@ def test_symlink(self):
968970
def test_stat(self):
969971
path = self.make_path('foo', 'bar', 'baz')
970972
self.create_file(path, contents='1234567')
971-
self.assertEqual(self.os.stat(path), self.os.stat(self.path(path)))
973+
self.assertEqual(self.os.stat(path), self.path(path).stat())
974+
975+
@unittest.skipIf(sys.version_info < (3, 10), "New in Python 3.10")
976+
def test_stat_follow_symlinks(self):
977+
self.check_posix_only()
978+
directory = self.make_path('foo')
979+
base_name = 'bar'
980+
file_path = self.path(self.os.path.join(directory, base_name))
981+
link_path = self.path(self.os.path.join(directory, 'link'))
982+
contents = "contents"
983+
self.create_file(file_path, contents=contents)
984+
self.create_symlink(link_path, base_name)
985+
self.assertEqual(len(contents),
986+
link_path.stat(follow_symlinks=True)[stat.ST_SIZE])
987+
self.assertEqual(len(base_name),
988+
link_path.stat(follow_symlinks=False)[stat.ST_SIZE])
972989

973990
def test_utime(self):
974991
path = self.make_path('some_file')

0 commit comments

Comments
 (0)