From d12c9f6866ff0f4cd8e1a9344d663451efe98a89 Mon Sep 17 00:00:00 2001 From: Carl Montanari Date: Mon, 10 May 2021 10:48:10 -0700 Subject: [PATCH 1/9] fix breaking pathlib change in 3.10 beta1 --- pyfakefs/fake_pathlib.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index b43b1784..3c8e26b4 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -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) @@ -461,7 +461,8 @@ 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) + self._accessor = _fake_accessor return self def _path(self): @@ -469,11 +470,6 @@ def _path(self): """ return str(self) - def _init(self, template=None): - """Initializer called from base class.""" - self._accessor = _fake_accessor - self._closed = False - @classmethod def cwd(cls): """Return a new path pointing to the current working directory @@ -722,7 +718,8 @@ 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) + self._accessor = _fake_accessor return self From 311ec4cb581fc8c3bbf5a8bdc9c174371cb6fd95 Mon Sep 17 00:00:00 2001 From: Carl Montanari Date: Mon, 10 May 2021 11:51:35 -0700 Subject: [PATCH 2/9] update changelog, add 3.10-beta1 to actions ci, ensure _closed is marked false --- .github/workflows/pythonpackage.yml | 2 +- CHANGES.md | 4 ++++ pyfakefs/fake_pathlib.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 32b8cce6..8eab201d 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -29,7 +29,7 @@ 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 diff --git a/CHANGES.md b/CHANGES.md index 85a2ea64..438bb6de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,10 @@ The released versions correspond to PyPi releases. * correctly handle byte paths in `os.path.exists` (see [#595](../../issues/595)) +### Fixes + * 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. diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index 3c8e26b4..f7b9c535 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -463,6 +463,7 @@ def __new__(cls, *args, **kwargs): else FakePathlibModule.PosixPath) self = cls._from_parts(args) self._accessor = _fake_accessor + self._closed = False return self def _path(self): @@ -720,6 +721,7 @@ def __new__(cls, *args, **kwargs): else RealPathlibModule.PosixPath) self = cls._from_parts(args) self._accessor = _fake_accessor + self._closed = False return self From 39ac5fabb787bcc419ce506f554178e861ebbba2 Mon Sep 17 00:00:00 2001 From: Carl Montanari Date: Mon, 10 May 2021 11:59:32 -0700 Subject: [PATCH 3/9] bump tests ci job setup-python to v2 --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 8eab201d..28a70768 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -37,7 +37,7 @@ jobs: 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 }} From 204e3cb1d3d65075913a0b8aa52ea5d4e2b7063f Mon Sep 17 00:00:00 2001 From: Carl Montanari Date: Mon, 10 May 2021 12:49:01 -0700 Subject: [PATCH 4/9] restore `_init` method --- pyfakefs/fake_pathlib.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index f7b9c535..1003ee1b 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -462,10 +462,16 @@ def __new__(cls, *args, **kwargs): if cls.filesystem.is_windows_fs else FakePathlibModule.PosixPath) self = cls._from_parts(args) - self._accessor = _fake_accessor - self._closed = False + self._init() return self + def _init(self, template=None): + """Initializer called from base class.""" + # template is an unused holdover + _ = template + self._accessor = _fake_accessor + self._closed = False + def _path(self): """Returns the underlying path string as used by the fake filesystem. """ @@ -720,8 +726,7 @@ def __new__(cls, *args, **kwargs): cls = (RealPathlibModule.WindowsPath if os.name == 'nt' else RealPathlibModule.PosixPath) self = cls._from_parts(args) - self._accessor = _fake_accessor - self._closed = False + self._init() return self From d6ae60b5b31543b962b6c1d26fec2fc81feb7a60 Mon Sep 17 00:00:00 2001 From: Carl Montanari Date: Mon, 10 May 2021 14:08:52 -0700 Subject: [PATCH 5/9] add methods to fake accessor --- pyfakefs/fake_filesystem.py | 9 ++++++--- pyfakefs/fake_pathlib.py | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 1e9bfc8a..102561e2 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -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 diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index 1003ee1b..d39d6861 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -54,7 +54,8 @@ def init_module(filesystem): def _wrap_strfunc(strfunc): @functools.wraps(strfunc) def _wrapped(pathobj, *args, **kwargs): - return strfunc(pathobj.filesystem, str(pathobj), *args, **kwargs) + p = pathobj.filesystem if hasattr(pathobj, "filesystem") else pathobj + return strfunc(p, str(pathobj), *args, **kwargs) return staticmethod(_wrapped) @@ -131,9 +132,17 @@ def lchmod(self, pathobj, mode): if sys.version_info >= (3, 9): readlink = _wrap_strfunc(FakeFilesystem.readlink) + else: + # why? and shouldnt this not be *os* -- but something patched (FakeOsModule probably)? + readlink = staticmethod(os.readlink) + pass utime = _wrap_strfunc(FakeFilesystem.utime) + # same comment as above for these two + realpath = staticmethod(os.path.realpath) + getcwd = staticmethod(os.getcwd) + _fake_accessor = _FakeAccessor() @@ -720,6 +729,13 @@ class RealPath(pathlib.Path): itself is not. """ + def _init(self, template=None): + """Initializer called from base class.""" + # template is an unused holdover + _ = template + self._accessor = _fake_accessor + self._closed = False + def __new__(cls, *args, **kwargs): """Creates the correct subclass based on OS.""" if cls is RealPathlibModule.Path: From 02d5f6930284e4f13b1af4daef5cf2d3c493317c Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Wed, 12 May 2021 21:51:14 +0200 Subject: [PATCH 6/9] Overwrite _from_parts / _from_parsed_parts to call _init - fix link_to for Python 3.10 - revert some of the probably unneeded changes --- pyfakefs/fake_pathlib.py | 59 ++++++++++++++++------------- pyfakefs/tests/fake_pathlib_test.py | 9 ++--- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index d39d6861..2e9f6ef7 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -54,8 +54,7 @@ def init_module(filesystem): def _wrap_strfunc(strfunc): @functools.wraps(strfunc) def _wrapped(pathobj, *args, **kwargs): - p = pathobj.filesystem if hasattr(pathobj, "filesystem") else pathobj - return strfunc(p, str(pathobj), *args, **kwargs) + return strfunc(pathobj.filesystem, str(pathobj), *args, **kwargs) return staticmethod(_wrapped) @@ -125,23 +124,19 @@ 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) - else: - # why? and shouldnt this not be *os* -- but something patched (FakeOsModule probably)? - readlink = staticmethod(os.readlink) - pass + if sys.version_info >= (3, 10): + link = _wrap_binary_strfunc( + lambda fs, file_path, link_target: + FakeFilesystem.link(fs, file_path, link_target)) - utime = _wrap_strfunc(FakeFilesystem.utime) + readlink = _wrap_strfunc(FakeFilesystem.readlink) - # same comment as above for these two - realpath = staticmethod(os.path.realpath) - getcwd = staticmethod(os.getcwd) + utime = _wrap_strfunc(FakeFilesystem.utime) _fake_accessor = _FakeAccessor() @@ -471,15 +466,35 @@ def __new__(cls, *args, **kwargs): if cls.filesystem.is_windows_fs else FakePathlibModule.PosixPath) self = cls._from_parts(args) + return 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.""" - # template is an unused holdover - _ = template - self._accessor = _fake_accessor - self._closed = False + """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. @@ -729,20 +744,12 @@ class RealPath(pathlib.Path): itself is not. """ - def _init(self, template=None): - """Initializer called from base class.""" - # template is an unused holdover - _ = template - self._accessor = _fake_accessor - self._closed = False - def __new__(cls, *args, **kwargs): """Creates the correct subclass based on OS.""" if cls is RealPathlibModule.Path: cls = (RealPathlibModule.WindowsPath if os.name == 'nt' else RealPathlibModule.PosixPath) self = cls._from_parts(args) - self._init() return self diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py index 5dcc57f9..1d289601 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -378,20 +378,19 @@ 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() file_stat = self.os.stat(self.file_path) link_stat = self.os.lstat(self.file_link_path) - if not hasattr(os, "lchmod"): + if not hasattr(os, "lchmod") and sys.version_info < (3, 10): with self.assertRaises(NotImplementedError): self.path(self.file_link_path).lchmod(0o444) 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')) From affc4f3595628992eaefc6137e1dcf008b0ca8ac Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Thu, 13 May 2021 12:23:43 +0200 Subject: [PATCH 7/9] Handle pathlib.Path.getcwd for Python 3.10 - handle dummy encoding "locale" introduced in Python 3.10 --- pyfakefs/fake_filesystem.py | 6 +++--- pyfakefs/fake_pathlib.py | 3 +++ pyfakefs/helpers.py | 9 +++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 102561e2..971dc93d 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -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 @@ -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 = ( @@ -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) diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index 2e9f6ef7..97e15dac 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -134,6 +134,9 @@ def lchmod(self, pathobj, mode): lambda fs, file_path, link_target: FakeFilesystem.link(fs, file_path, link_target)) + # this will use the fake filesystem because os is patched + getcwd = lambda p: os.getcwd() + readlink = _wrap_strfunc(FakeFilesystem.readlink) utime = _wrap_strfunc(FakeFilesystem.utime) diff --git a/pyfakefs/helpers.py b/pyfakefs/helpers.py index aa3959d9..08962fce 100644 --- a/pyfakefs/helpers.py +++ b/pyfakefs/helpers.py @@ -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. From b88851fb992585b0f179c339fbf195e30acd2e25 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Thu, 13 May 2021 13:44:45 +0200 Subject: [PATCH 8/9] Adapt handling of pathlib.Path.lchmod - it calls chmod() in Python 3.10, so this has to handled --- pyfakefs/fake_pathlib.py | 14 ++++++++++---- pyfakefs/tests/fake_pathlib_test.py | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index 97e15dac..09933fa0 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -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) @@ -135,7 +140,8 @@ def lchmod(self, pathobj, mode): FakeFilesystem.link(fs, file_path, link_target)) # this will use the fake filesystem because os is patched - getcwd = lambda p: os.getcwd() + def getcwd(self): + return os.getcwd() readlink = _wrap_strfunc(FakeFilesystem.readlink) diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py index 1d289601..61ffb409 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -380,9 +380,12 @@ def test_chmod(self): 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") and sys.version_info < (3, 10): + if not hasattr(os, "lchmod"): with self.assertRaises(NotImplementedError): self.path(self.file_link_path).lchmod(0o444) else: From 86bb5f772543018ef3169a4e692b14846e83a700 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Thu, 13 May 2021 14:14:54 +0200 Subject: [PATCH 9/9] Do not test extra dependencies with Python 3.10 - some dependencies are not available for the beta version - comment out docker tests - setup currently fails, has to be handled separately --- .github/workflows/pythonpackage.yml | 20 +++++++++++++++----- CHANGES.md | 2 -- pyfakefs/tests/fake_pathlib_test.py | 17 ++++++++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 28a70768..df96fa49 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -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 diff --git a/CHANGES.md b/CHANGES.md index 438bb6de..4fbf3556 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,8 +12,6 @@ The released versions correspond to PyPi releases. ### Fixes * correctly handle byte paths in `os.path.exists` (see [#595](../../issues/595)) - -### Fixes * Update `fake_pathlib` to support changes coming in Python 3.10 ([see](https://github.com/python/cpython/pull/19342)) diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py index 61ffb409..efea509a 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -970,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')