diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 4e8b599b..f93700c8 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -54,7 +54,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ matrix.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/extra_requirements.txt') }} + key: ${{ matrix.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/extra_requirements.txt') }}-${{ hashFiles('**/legacy_requirements.txt') }} restore-keys: | ${{ matrix.os }}-${{ matrix.python-version }}-pip- @@ -82,6 +82,7 @@ jobs: if: ${{ matrix.python-version != 'pypy-3.10' }} run: | pip install -r extra_requirements.txt + pip install -r legacy_requirements.txt pip install zstandard cffi # needed to test #910 shell: bash - name: Run unit tests with extra packages as non-root user @@ -148,6 +149,7 @@ jobs: run: | pip install -r requirements.txt pip install -r extra_requirements.txt + pip install -r legacy_requirements.txt pip install pytest-find-dependencies - name: Check dependencies run: python -m pytest --find-dependencies pyfakefs/tests diff --git a/pyfakefs/extra_packages.py b/pyfakefs/extra_packages.py deleted file mode 100644 index 0b061f13..00000000 --- a/pyfakefs/extra_packages.py +++ /dev/null @@ -1,53 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Imports external packages that replace or emulate internal packages. -If the external module is not present, the built-in module is imported. -""" - -import warnings - -try: - import pathlib2 -except ImportError: - pathlib2 = None - -try: - import scandir - - use_scandir_package = True - use_builtin_scandir = False -except ImportError: - try: - from os import scandir # noqa: F401 - - use_builtin_scandir = True - use_scandir_package = False - except ImportError: - use_builtin_scandir = False - use_scandir_package = False - - -def legacy_warning(module_name): - warnings.resetwarnings() - warnings.warn( - f"You have installed the legacy package '{module_name}'." - "Patching this package will no longer be supported in pyfakefs >= 6", - category=DeprecationWarning, - stacklevel=2, - ) - - -if pathlib2 is not None: - legacy_warning("pathlib2") -if use_scandir_package: - legacy_warning("scandir") diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index e9757230..45202a14 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -83,11 +83,11 @@ from importlib import reload from pyfakefs import fake_filesystem, fake_io, fake_os, fake_open, fake_path, fake_file +from pyfakefs import fake_legacy_modules from pyfakefs import fake_filesystem_shutil from pyfakefs import fake_pathlib from pyfakefs import mox3_stubout -from pyfakefs.extra_packages import pathlib2 -from pyfakefs import fake_scandir +from pyfakefs.legacy_packages import pathlib2, scandir OS_MODULE = "nt" if sys.platform == "win32" else "posix" PATH_MODULE = "ntpath" if sys.platform == "win32" else "posixpath" @@ -699,12 +699,15 @@ def _init_fake_module_classes(self) -> None: self._class_modules["Path"] = ["pathlib"] self._unfaked_module_classes["pathlib"] = fake_pathlib.RealPathlibModule if pathlib2: - self._fake_module_classes["pathlib2"] = fake_pathlib.FakePathlibModule + self._fake_module_classes["pathlib2"] = ( + fake_legacy_modules.FakePathlib2Module + ) self._class_modules["Path"].append("pathlib2") self._unfaked_module_classes["pathlib2"] = fake_pathlib.RealPathlibModule + if scandir: + self._fake_module_classes["scandir"] = fake_legacy_modules.FakeScanDirModule self._fake_module_classes["Path"] = fake_pathlib.FakePathlibPathModule self._unfaked_module_classes["Path"] = fake_pathlib.RealPathlibPathModule - self._fake_module_classes["scandir"] = fake_scandir.FakeScanDirModule def _init_fake_module_functions(self) -> None: # handle patching function imported separately like diff --git a/pyfakefs/fake_legacy_modules.py b/pyfakefs/fake_legacy_modules.py new file mode 100644 index 00000000..7c25b98f --- /dev/null +++ b/pyfakefs/fake_legacy_modules.py @@ -0,0 +1,110 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import warnings + + +from pyfakefs.fake_pathlib import FakePathlibModule +from pyfakefs.fake_scandir import scandir, walk + + +def legacy_warning(module_name): + msg = ( + f"You are using the legacy package '{module_name}' instead of the " + f"built-in module." + "Patching this package will no longer be supported in pyfakefs >= 6" + ) + warnings.warn(msg, category=DeprecationWarning) + + +class FakePathlib2Module(FakePathlibModule): + """Uses FakeFilesystem to provide a fake pathlib module replacement. + for the `pathlib2` package available on PyPi. + The usage of `pathlib2` is deprecated and will no longer be supported + in future pyfakefs versions. + """ + + has_warned = False + + def __getattribute__(self, name): + attr = object.__getattribute__(self, name) + if hasattr(attr, "__call__") and not FakePathlib2Module.has_warned: + FakePathlib2Module.has_warned = True + legacy_warning("pathlib2") + return attr + + +class FakeScanDirModule: + """Uses FakeFilesystem to provide a fake module replacement + for the `scandir` package available on PyPi. + + The usage of the `scandir` package is deprecated and will no longer be supported + in future pyfakefs versions. + + You need a fake_filesystem to use this: + `filesystem = fake_filesystem.FakeFilesystem()` + `fake_scandir_module = fake_filesystem.FakeScanDirModule(filesystem)` + """ + + @staticmethod + def dir(): + """Return the list of patched function names. Used for patching + functions imported from the module. + """ + return "scandir", "walk" + + def __init__(self, filesystem): + self.filesystem = filesystem + + has_warned = False + + def scandir(self, path="."): + """Return an iterator of DirEntry objects corresponding to the entries + in the directory given by path. + + Args: + path: Path to the target directory within the fake filesystem. + + Returns: + an iterator to an unsorted list of os.DirEntry objects for + each entry in path. + + Raises: + OSError: if the target is not a directory. + """ + if not self.has_warned: + self.__class__.has_warned = True + legacy_warning("scandir") + return scandir(self.filesystem, path) + + def walk(self, top, topdown=True, onerror=None, followlinks=False): + """Perform a walk operation over the fake filesystem. + + Args: + top: The root directory from which to begin walk. + topdown: Determines whether to return the tuples with the root as + the first entry (`True`) or as the last, after all the child + directory tuples (`False`). + onerror: If not `None`, function which will be called to handle the + `os.error` instance provided when `os.listdir()` fails. + followlinks: If `True`, symbolic links are followed. + + Yields: + (path, directories, nondirectories) for top and each of its + subdirectories. See the documentation for the builtin os module + for further details. + """ + if not self.has_warned: + self.__class__.has_warned = True + legacy_warning("scandir") + + return walk(self.filesystem, top, topdown, onerror, followlinks) diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index a5dea10a..5a3f46d3 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -772,8 +772,6 @@ def is_reserved(self): class FakePathlibModule: """Uses FakeFilesystem to provide a fake pathlib module replacement. - Can be used to replace both the standard `pathlib` module and the - `pathlib2` package available on PyPi. You need a fake_filesystem to use this: `filesystem = fake_filesystem.FakeFilesystem()` diff --git a/pyfakefs/fake_scandir.py b/pyfakefs/fake_scandir.py index 75f8caa4..b9f7690f 100644 --- a/pyfakefs/fake_scandir.py +++ b/pyfakefs/fake_scandir.py @@ -20,16 +20,10 @@ import os import sys -from pyfakefs.extra_packages import use_scandir_package from pyfakefs.helpers import to_string, make_string_path -if sys.version_info >= (3, 6): - BaseClass = os.PathLike -else: - BaseClass = object - -class DirEntry(BaseClass): +class DirEntry(os.PathLike): """Emulates os.DirEntry. Note that we did not enforce keyword only arguments.""" @@ -133,9 +127,7 @@ class ScanDirIter: def __init__(self, filesystem, path): self.filesystem = filesystem if isinstance(path, int): - if not use_scandir_package and ( - sys.version_info < (3, 7) or self.filesystem.is_windows_fs - ): + if self.filesystem.is_windows_fs: raise NotImplementedError( "scandir does not support file descriptor " "path argument" ) @@ -263,61 +255,3 @@ def do_walk(top_dir, top_most=False): yield top_contents return do_walk(make_string_path(to_string(top)), top_most=True) - - -class FakeScanDirModule: - """Uses FakeFilesystem to provide a fake `scandir` module replacement. - - .. Note:: The ``scandir`` function is a part of the standard ``os`` module - since Python 3.5. This class handles the separate ``scandir`` module - that is available on pypi. - - You need a fake_filesystem to use this: - `filesystem = fake_filesystem.FakeFilesystem()` - `fake_scandir_module = fake_filesystem.FakeScanDirModule(filesystem)` - """ - - @staticmethod - def dir(): - """Return the list of patched function names. Used for patching - functions imported from the module. - """ - return "scandir", "walk" - - def __init__(self, filesystem): - self.filesystem = filesystem - - def scandir(self, path="."): - """Return an iterator of DirEntry objects corresponding to the entries - in the directory given by path. - - Args: - path: Path to the target directory within the fake filesystem. - - Returns: - an iterator to an unsorted list of os.DirEntry objects for - each entry in path. - - Raises: - OSError: if the target is not a directory. - """ - return scandir(self.filesystem, path) - - def walk(self, top, topdown=True, onerror=None, followlinks=False): - """Perform a walk operation over the fake filesystem. - - Args: - top: The root directory from which to begin walk. - topdown: Determines whether to return the tuples with the root as - the first entry (`True`) or as the last, after all the child - directory tuples (`False`). - onerror: If not `None`, function which will be called to handle the - `os.error` instance provided when `os.listdir()` fails. - followlinks: If `True`, symbolic links are followed. - - Yields: - (path, directories, nondirectories) for top and each of its - subdirectories. See the documentation for the builtin os module - for further details. - """ - return walk(self.filesystem, top, topdown, onerror, followlinks) diff --git a/pyfakefs/legacy_packages.py b/pyfakefs/legacy_packages.py new file mode 100644 index 00000000..465b464b --- /dev/null +++ b/pyfakefs/legacy_packages.py @@ -0,0 +1,26 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Imports external packages that replace or emulate internal packages. +These packages are not needed with any current Python version, +and their support in pyfakefs will be removed in a an upcoming release. +""" + +try: + import pathlib2 +except ImportError: + pathlib2 = None + +try: + import scandir as scandir +except ImportError: + scandir = None diff --git a/pyfakefs/tests/all_tests.py b/pyfakefs/tests/all_tests.py index 36945afa..2042884c 100644 --- a/pyfakefs/tests/all_tests.py +++ b/pyfakefs/tests/all_tests.py @@ -32,6 +32,7 @@ fake_pathlib_test, fake_tempfile_test, patched_packages_test, + fake_legacy_modules_test, mox3_stubout_test, ) @@ -57,6 +58,7 @@ def suite(self): # pylint: disable-msg=C6409 loader.loadTestsFromModule(dynamic_patch_test), loader.loadTestsFromModule(fake_pathlib_test), loader.loadTestsFromModule(patched_packages_test), + loader.loadTestsFromModule(fake_legacy_modules_test), ] ) return self diff --git a/pyfakefs/tests/all_tests_without_extra_packages.py b/pyfakefs/tests/all_tests_without_extra_packages.py index fa7c93b6..822e7ce7 100644 --- a/pyfakefs/tests/all_tests_without_extra_packages.py +++ b/pyfakefs/tests/all_tests_without_extra_packages.py @@ -16,13 +16,10 @@ import sys import unittest -from pyfakefs import extra_packages +from pyfakefs import legacy_packages -if extra_packages.use_scandir_package: - extra_packages.use_scandir_package = False - from os import scandir - - extra_packages.scandir = scandir +legacy_packages.scandir = None +legacy_packages.pathlib2 = None from pyfakefs.tests.all_tests import AllTests # noqa: E402 diff --git a/pyfakefs/tests/example_test.py b/pyfakefs/tests/example_test.py index 126bbe74..03de633a 100644 --- a/pyfakefs/tests/example_test.py +++ b/pyfakefs/tests/example_test.py @@ -32,7 +32,7 @@ import unittest from pyfakefs import fake_filesystem_unittest -from pyfakefs.extra_packages import use_scandir_package +from pyfakefs.legacy_packages import scandir from pyfakefs.tests import example # The module under test @@ -143,9 +143,7 @@ def test_os_scandir(self): self.assertTrue(entries[1].is_symlink()) self.assertTrue(entries[2].is_file()) - @unittest.skipIf( - not use_scandir_package, "Testing only if scandir module is installed" - ) + @unittest.skipIf(scandir is None, "Testing only if scandir module is installed") def test_scandir_scandir(self): """Test example.scandir() which uses `scandir.scandir()`. diff --git a/pyfakefs/tests/fake_legacy_modules_test.py b/pyfakefs/tests/fake_legacy_modules_test.py new file mode 100644 index 00000000..276e8467 --- /dev/null +++ b/pyfakefs/tests/fake_legacy_modules_test.py @@ -0,0 +1,118 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyfakefs.fake_filesystem_unittest import TestCase +from pyfakefs.fake_legacy_modules import FakeScanDirModule +from pyfakefs.legacy_packages import pathlib2, scandir +from pyfakefs.tests.fake_os_test import FakeScandirTest +from pyfakefs.tests.fake_pathlib_test import ( + FakePathlibInitializationTest, + FakePathlibPathFileOperationTest, + FakePathlibFileObjectPropertyTest, + FakePathlibUsageInOsFunctionsTest, +) + + +class DeprecationWarningTest(TestCase): + def setUp(self): + self.setUpPyfakefs() + + @unittest.skipIf(scandir is None, "The scandir package is not installed") + def test_scandir_warning(self): + FakeScanDirModule.has_warned = False + with self.assertWarns(DeprecationWarning): + scandir.scandir("/") + + @unittest.skipIf(pathlib2 is None, "The pathlib2 package is not installed") + def test_pathlib2_warning(self): + FakeScanDirModule.has_warned = False + with self.assertWarns(DeprecationWarning): + pathlib2.Path("/foo") + + +@unittest.skipIf(scandir is None, "The scandir package is not installed") +class FakeScandirPackageTest(FakeScandirTest): + def used_scandir(self): + import pyfakefs.fake_legacy_modules + + def fake_scan_dir(p): + return pyfakefs.fake_legacy_modules.FakeScanDirModule( + self.filesystem + ).scandir(p) + + return fake_scan_dir + + def test_path_like(self): + unittest.skip("Path-like objects not available in scandir package") + + +class RealScandirPackageTest(FakeScandirPackageTest): + def used_scandir(self): + from scandir import scandir + + return scandir + + def use_real_fs(self): + return True + + +@unittest.skipIf(pathlib2 is None, "The pathlib2 package is not installed") +class FakePathlib2InitializationTest(FakePathlibInitializationTest): + def used_pathlib(self): + return pathlib2 + + +class RealPathlib2InitializationTest(FakePathlib2InitializationTest): + def use_real_fs(self): + return True + + +@unittest.skipIf(pathlib2 is None, "The pathlib2 package is not installed") +class FakePathlib2FileObjectPropertyTest(FakePathlibFileObjectPropertyTest): + def used_pathlib(self): + return pathlib2 + + +class RealPathlib2FileObjectPropertyTest(FakePathlib2FileObjectPropertyTest): + def use_real_fs(self): + return True + + +@unittest.skipIf(pathlib2 is None, "The pathlib2 package is not installed") +class FakePathlib2PathFileOperationTest(FakePathlibPathFileOperationTest): + def used_pathlib(self): + return pathlib2 + + def test_is_junction(self): + unittest.skip("is_junction not available in pathlib2") + + +class RealPathlibPath2FileOperationTest(FakePathlib2PathFileOperationTest): + def use_real_fs(self): + return True + + +@unittest.skipIf(pathlib2 is None, "The pathlib2 package is not installed") +class FakePathlib2UsageInOsFunctionsTest(FakePathlibUsageInOsFunctionsTest): + def used_pathlib(self): + return pathlib2 + + +class RealPathlib2UsageInOsFunctionsTest(FakePathlib2UsageInOsFunctionsTest): + def use_real_fs(self): + return True + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index 2a4ee55e..b17ddb86 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -21,8 +21,6 @@ import sys import unittest -from pyfakefs.helpers import IN_DOCKER, IS_PYPY, get_uid, get_gid, reset_ids - from pyfakefs import fake_filesystem, fake_os, fake_open, fake_file from pyfakefs.fake_filesystem import ( FakeFileOpen, @@ -30,11 +28,7 @@ set_uid, set_gid, ) -from pyfakefs.extra_packages import ( - use_scandir_package, - use_builtin_scandir, -) - +from pyfakefs.helpers import IN_DOCKER, IS_PYPY, get_uid, get_gid, reset_ids from pyfakefs.tests.test_utils import TestCase, RealFsTestCase @@ -5247,23 +5241,13 @@ class FakeScandirTest(FakeOsModuleTestBase): FILE_SIZE = 50 LINKED_FILE_SIZE = 10 + def used_scandir(self): + return self.os.scandir + def setUp(self): super().setUp() self.supports_symlinks = not self.is_windows or not self.use_real_fs() - - if use_scandir_package: - if self.use_real_fs(): - from scandir import scandir - else: - import pyfakefs.fake_scandir - - def fake_scan_dir(p): - return pyfakefs.fake_scandir.scandir(self.filesystem, p) - - scandir = fake_scan_dir - else: - scandir = self.os.scandir - self.scandir = scandir + self.scandir = self.used_scandir() self.directory = self.make_path("xyzzy", "plugh") link_dir = self.make_path("linked", "plugh") @@ -5501,10 +5485,6 @@ def test_stat_ino_dev(self): self.assertEqual(file_stat.st_ino, self.dir_entries[5].stat().st_ino) self.assertEqual(file_stat.st_dev, self.dir_entries[5].stat().st_dev) - @unittest.skipIf( - not use_builtin_scandir, - "Path-like objects not available in scandir package", - ) def test_path_like(self): self.assertTrue(isinstance(self.dir_entries[0], os.PathLike)) self.assertEqual( @@ -5543,7 +5523,6 @@ def use_real_fs(self): @unittest.skipIf(TestCase.is_windows, "dir_fd not supported for os.scandir in Windows") -@unittest.skipIf(use_scandir_package, "no dir_fd support for scandir package") class FakeScandirFdTest(FakeScandirTest): def tearDown(self): self.os.close(self.dir_fd) diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py index e8544450..145b0cdd 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -44,6 +44,9 @@ def __init__(self, methodName="runTest"): fake_filesystem_unittest.TestCase.__init__(self, methodName) RealFsTestMixin.__init__(self) + def used_pathlib(self): + return pathlib + def setUp(self): RealFsTestMixin.setUp(self) self.filesystem = None @@ -52,8 +55,8 @@ def setUp(self): self.setUpPyfakefs() self.filesystem = self.fs self.create_basepath() - self.pathlib = pathlib - self.path = pathlib.Path + self.pathlib = self.used_pathlib() + self.path = self.pathlib.Path self.os = os self.open = open