Add support for some Python 3.10 changes
- new strict argument for os.path.realpath()
- new follow_symlinks argument for pathlib.Path.chmod()
- new method pathlib.Path.hardlink_to
- new 'newline' argument in pathlib.Path.write_text()
diff --git a/CHANGES.md b/CHANGES.md
index 4fbf355..88b2c42 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,6 +3,14 @@
## Version 4.5.0 (as yet unreleased)
+### New Features
+ * added support for some Python 3.10 features:
+ * new method `pathlib.Path.hardlink_to`
+ * new `newline` argument in `pathlib.Path.write_text`
+ * new `follow_symlinks` argument in `pathlib.Path.stat` and
+ `pathlib.Path.chmod`
+ * new 'strict' argument in `os.path.realpath`
+
### Changes
* `pathlib2` is still supported, but considered to have the same
functionality as `pathlib` and is no longer tested separately;
diff --git a/README.md b/README.md
index 135a7a4..0bea2fe 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@
### Continuous integration
pyfakefs is currently automatically tested on Linux, MacOS and Windows, with
-Python 3.6 to 3.9, and with PyPy3 on Linux, using
+Python 3.6 to 3.10, and with PyPy3 on Linux, using
[GitHub Actions](https://github.com/jmcgeheeiv/pyfakefs/actions).
### Running pyfakefs unit tests
diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py
index 971dc93..3765607 100644
--- a/pyfakefs/fake_filesystem.py
+++ b/pyfakefs/fake_filesystem.py
@@ -113,8 +113,8 @@
from pyfakefs.fake_scandir import scandir, walk
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, real_encoding
+ is_int_type, is_byte_string, is_unicode_string, make_string_path,
+ IS_WIN, IS_PYPY, to_string, matching_string, real_encoding
)
from pyfakefs import __version__ # noqa: F401 for upwards compatibility
@@ -3399,15 +3399,22 @@
path = self._os_path.relpath(path, start)
return path.replace(self._os_path.sep, self.filesystem.path_separator)
- def realpath(self, filename):
+ def realpath(self, filename, strict=None):
"""Return the canonical path of the specified filename, eliminating any
symbolic links encountered in the path.
"""
+ if strict is not None and sys.version_info < (3, 10):
+ raise TypeError("realpath() got an unexpected "
+ "keyword argument 'strict'")
+ if strict:
+ # raises in strict mode if the file does not exist
+ self.filesystem.resolve(filename)
if self.filesystem.is_windows_fs:
return self.abspath(filename)
filename = make_string_path(filename)
path, ok = self._joinrealpath(filename[:0], filename, {})
- return self.abspath(path)
+ path = self.abspath(path)
+ return path
def samefile(self, path1, path2):
"""Return whether path1 and path2 point to the same file.
@@ -4336,6 +4343,11 @@
follow_symlinks: (bool) If `False` and `path` points to a symlink,
the link itself is queried instead of the linked object.
"""
+ if (not follow_symlinks and
+ (os.chmod not in os.supports_follow_symlinks or IS_PYPY)):
+ raise NotImplementedError(
+ "`follow_symlinks` for chmod() is not available "
+ "on this system")
path = self._path_with_dir_fd(path, self.chmod, dir_fd)
self.filesystem.chmod(path, mode, follow_symlinks)
diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py
index 09933fa..1c6a960 100644
--- a/pyfakefs/fake_pathlib.py
+++ b/pyfakefs/fake_pathlib.py
@@ -97,19 +97,26 @@
if use_scandir:
scandir = _wrap_strfunc(fake_scandir.scandir)
+ chmod = _wrap_strfunc(FakeFilesystem.chmod)
+
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, *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")
+ if "follow_symlinks" in kwargs:
+ if sys.version_info < (3, 10):
+ raise TypeError("chmod() got an unexpected keyword "
+ "argument 'follow_synlinks'")
+ if (not kwargs["follow_symlinks"] and
+ os.chmod not in os.supports_follow_symlinks):
+ raise NotImplementedError(
+ "`follow_symlinks` for chmod() is not available "
+ "on this system")
return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs)
mkdir = _wrap_strfunc(FakeFilesystem.makedir)
@@ -129,7 +136,7 @@
FakeFilesystem.create_symlink(fs, file_path, link_target,
create_missing_dirs=False))
- if (3, 8) <= sys.version_info < (3, 10):
+ if (3, 8) <= sys.version_info:
link_to = _wrap_binary_strfunc(
lambda fs, file_path, link_target:
FakeFilesystem.link(fs, file_path, link_target))
@@ -592,7 +599,7 @@
with FakeFileOpen(self.filesystem)(self._path(), mode='wb') as f:
return f.write(view)
- def write_text(self, data, encoding=None, errors=None):
+ def write_text(self, data, encoding=None, errors=None, newline=None):
"""Open the fake file in text mode, write to it, and close
the file.
@@ -600,7 +607,9 @@
data: the string to be written
encoding: the encoding used for the string; if not given, the
default locale encoding is used
- errors: ignored
+ errors: (str) Defines how encoding errors are handled.
+ newline: Controls universal newlines, passed to stream object.
+ New in Python 3.10.
Raises:
TypeError: if data is not of type 'str'.
OSError: if the target object is a directory, the path is
@@ -609,10 +618,14 @@
if not isinstance(data, str):
raise TypeError('data must be str, not %s' %
data.__class__.__name__)
+ if newline is not None and sys.version_info < (3, 10):
+ raise TypeError("write_text() got an unexpected "
+ "keyword argument 'newline'")
with FakeFileOpen(self.filesystem)(self._path(),
mode='w',
encoding=encoding,
- errors=errors) as f:
+ errors=errors,
+ newline=newline) as f:
return f.write(data)
@classmethod
diff --git a/pyfakefs/tests/fake_filesystem_test.py b/pyfakefs/tests/fake_filesystem_test.py
index adbd50d..d5dd5c3 100644
--- a/pyfakefs/tests/fake_filesystem_test.py
+++ b/pyfakefs/tests/fake_filesystem_test.py
@@ -952,6 +952,18 @@
self.assertEqual('!george!washington!bridge',
self.os.path.realpath('bridge'))
+ @unittest.skipIf(sys.version_info < (3, 10), "'strict' new in Python 3.10")
+ def test_realpath_strict(self):
+ self.filesystem.create_file('!foo!bar')
+ self.filesystem.cwd = '!foo'
+ self.assertEqual('!foo!baz',
+ self.os.path.realpath('baz', strict=False))
+ self.assert_raises_os_error(errno.ENOENT,
+ self.os.path.realpath,
+ 'baz', strict=True)
+ self.assertEqual('!foo!bar',
+ self.os.path.realpath('bar', strict=True))
+
def test_samefile(self):
file_path1 = '!foo!bar!baz'
file_path2 = '!foo!bar!boo'
diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py
index c0bd9a9..de5bda1 100644
--- a/pyfakefs/tests/fake_os_test.py
+++ b/pyfakefs/tests/fake_os_test.py
@@ -23,7 +23,7 @@
import time
import unittest
-from pyfakefs.helpers import IN_DOCKER
+from pyfakefs.helpers import IN_DOCKER, IS_PYPY
from pyfakefs import fake_filesystem
from pyfakefs.fake_filesystem import FakeFileOpen, is_root
@@ -1934,8 +1934,6 @@
def test_chmod_follow_symlink(self):
self.check_posix_only()
- if self.use_real_fs() and 'chmod' not in os.supports_follow_symlinks:
- raise unittest.SkipTest('follow_symlinks not available')
path = self.make_path('some_file')
self.createTestFile(path)
link_path = self.make_path('link_to_some_file')
@@ -1945,22 +1943,24 @@
st = self.os.stat(link_path)
self.assert_mode_equal(0o6543, st.st_mode)
st = self.os.stat(link_path, follow_symlinks=False)
- self.assert_mode_equal(0o777, st.st_mode)
+ # the exact mode depends on OS and Python version
+ self.assertEqual(stat.S_IMODE(0o700), stat.S_IMODE(st.st_mode) & 0o700)
def test_chmod_no_follow_symlink(self):
self.check_posix_only()
- if self.use_real_fs() and 'chmod' not in os.supports_follow_symlinks:
- raise unittest.SkipTest('follow_symlinks not available')
path = self.make_path('some_file')
self.createTestFile(path)
link_path = self.make_path('link_to_some_file')
self.create_symlink(link_path, path)
- self.os.chmod(link_path, 0o6543, follow_symlinks=False)
-
- st = self.os.stat(link_path)
- self.assert_mode_equal(0o666, st.st_mode)
- st = self.os.stat(link_path, follow_symlinks=False)
- self.assert_mode_equal(0o6543, st.st_mode)
+ if os.chmod not in os.supports_follow_symlinks or IS_PYPY:
+ with self.assertRaises(NotImplementedError):
+ self.os.chmod(link_path, 0o6543, follow_symlinks=False)
+ else:
+ self.os.chmod(link_path, 0o6543, follow_symlinks=False)
+ st = self.os.stat(link_path)
+ self.assert_mode_equal(0o666, st.st_mode)
+ st = self.os.stat(link_path, follow_symlinks=False)
+ self.assert_mode_equal(0o6543, st.st_mode)
def test_lchmod(self):
"""lchmod shall behave like chmod with follow_symlinks=True."""
diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py
index efea509..ce0ea48 100644
--- a/pyfakefs/tests/fake_pathlib_test.py
+++ b/pyfakefs/tests/fake_pathlib_test.py
@@ -30,6 +30,7 @@
from pyfakefs.fake_filesystem import is_root
from pyfakefs import fake_pathlib, fake_filesystem
+from pyfakefs.helpers import IS_PYPY
from pyfakefs.tests.test_utils import RealFsTestCase
is_windows = sys.platform == 'win32'
@@ -380,9 +381,6 @@
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"):
@@ -395,6 +393,23 @@
self.assertEqual(link_stat.st_mode & 0o777700,
stat.S_IFLNK | 0o700)
+ @unittest.skipIf(sys.version_info < (3, 10),
+ "follow_symlinks argument new in Python 3.10")
+ def test_chmod_no_followsymlinks(self):
+ self.skip_if_symlink_not_supported()
+ file_stat = self.os.stat(self.file_path)
+ link_stat = self.os.lstat(self.file_link_path)
+ if os.chmod not in os.supports_follow_symlinks or IS_PYPY:
+ with self.assertRaises(NotImplementedError):
+ self.path(self.file_link_path).chmod(0o444,
+ follow_symlinks=False)
+ else:
+ self.path(self.file_link_path).chmod(0o444, follow_symlinks=False)
+ self.assertEqual(file_stat.st_mode, stat.S_IFREG | 0o666)
+ # 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'))
self.create_file(self.make_path('antoine', 'setup.py'))
@@ -526,6 +541,19 @@
self.assertTrue(self.os.path.exists(path_name))
self.check_contents(path_name, 'ανοησίες'.encode('greek'))
+ @unittest.skipIf(sys.version_info < (3, 10),
+ "newline argument new in Python 3.10")
+ def test_write_with_newline_arg(self):
+ path = self.path(self.make_path('some_file'))
+ path.write_text('1\r\n2\n3\r4', newline='')
+ self.check_contents(path, b'1\r\n2\n3\r4')
+ path.write_text('1\r\n2\n3\r4', newline='\n')
+ self.check_contents(path, b'1\r\n2\n3\r4')
+ path.write_text('1\r\n2\n3\r4', newline='\r\n')
+ self.check_contents(path, b'1\r\r\n2\r\n3\r4')
+ path.write_text('1\r\n2\n3\r4', newline='\r')
+ self.check_contents(path, b'1\r\r2\r3\r4')
+
def test_read_bytes(self):
path_name = self.make_path('binary_file')
self.create_file(path_name, contents=b'Binary file contents')
@@ -627,6 +655,20 @@
self.assertFalse(path.is_symlink())
self.assertEqual(2, self.os.stat(file_name).st_nlink)
+ @unittest.skipIf(sys.version_info < (3, 10),
+ 'hardlink_to new in Python 3.10')
+ def test_hardlink_to(self):
+ self.skip_if_symlink_not_supported()
+ file_name = self.make_path('foo', 'bar.txt')
+ self.create_file(file_name)
+ self.assertEqual(1, self.os.stat(file_name).st_nlink)
+ link_path = self.path(self.make_path('link_to_bar'))
+ path = self.path(file_name)
+ link_path.hardlink_to(path)
+ self.assertTrue(self.os.path.exists(link_path))
+ self.assertFalse(path.is_symlink())
+ self.assertEqual(2, self.os.stat(file_name).st_nlink)
+
@unittest.skipIf(sys.version_info < (3, 9),
'readlink new in Python 3.9')
def test_readlink(self):
diff --git a/setup.py b/setup.py
index b52bacb..a6a1508 100644
--- a/setup.py
+++ b/setup.py
@@ -42,6 +42,7 @@
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Operating System :: POSIX',