From 4e999910363caec2bdbcae65442af050274b369b Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Sun, 8 Oct 2023 16:12:35 +0200 Subject: [PATCH] Fix patching open for Python 3.12 - add patching _io, as this is used for io - add workaround for calling the real open_code (does not work yet under Windows) - add Python 3.12 to supported Python versions - fixes #836 --- .github/workflows/testsuite.yml | 2 +- CHANGES.md | 4 +++ README.md | 2 +- pyfakefs/fake_filesystem_unittest.py | 10 ++----- pyfakefs/fake_io.py | 9 ++++++- .../tests/fake_filesystem_unittest_test.py | 26 ++++++++++++------- setup.cfg | 1 + 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 6aa334aa..6c3750f5 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] include: - python-version: "pypy-3.7" os: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index 4d052b94..c8cd5627 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,11 +3,15 @@ The released versions correspond to PyPI releases. ## Unreleased +### Changes +* add official support for Python 3.12 + ### Fixes * removed a leftover debug print statement (see [#869](../../issues/869)) * make sure tests work without HOME environment set (see [#870](../../issues/870)) * automount drive or UNC path under Windows if needed for `pathlib.Path.mkdir()` (see [#890](../../issues/890)) +* adapt patching `io.open` to work with Python 3.12 (see [#836](../../issues/836)) ## [Version 5.2.3](https://pypi.python.org/pypi/pyfakefs/5.2.3) (2023-08-18) Fixes a rare problem on pytest shutdown. diff --git a/README.md b/README.md index 97b72131..b55c63d9 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ for more information about the limitations of pyfakefs. ### Continuous integration pyfakefs is currently automatically tested on Linux, macOS and Windows, with -Python 3.7 to 3.11, and with PyPy3 on Linux, using +Python 3.7 to 3.12, and with PyPy3 on Linux, using [GitHub Actions](https://github.com/pytest-dev/pyfakefs/actions). ### Running pyfakefs unit tests diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index 85764973..78371c0e 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -36,7 +36,6 @@ to `:py:class`pyfakefs.fake_filesystem_unittest.TestCase`. """ import _io # type:ignore[import] -import builtins import doctest import functools import genericpath @@ -562,7 +561,6 @@ def __init__( # save the original open function for use in pytest plugin self.original_open = open self.patch_open_code = patch_open_code - self.fake_open: fake_open.FakeFileOpen if additional_skip_names is not None: skip_names = [ @@ -655,8 +653,8 @@ def _init_fake_module_classes(self) -> None: "io": fake_io.FakeIoModule, "pathlib": fake_pathlib.FakePathlibModule, } - if IS_PYPY: - # in PyPy io.open, the module is referenced as _io + if IS_PYPY or sys.version_info >= (3, 12): + # in PyPy and later cpython versions, the module is referenced as _io self._fake_module_classes["_io"] = fake_io.FakeIoModule if sys.platform == "win32": self._fake_module_classes["nt"] = fake_path.FakeNtModule @@ -853,7 +851,6 @@ def _refresh(self) -> None: self.fs = fake_filesystem.FakeFilesystem(patcher=self, create_temp_dir=True) self.fs.patch_open_code = self.patch_open_code - self.fake_open = fake_open.FakeFileOpen(self.fs) for name in self._fake_module_classes: self.fake_modules[name] = self._fake_module_classes[name](self.fs) if hasattr(self.fake_modules[name], "skip_names"): @@ -932,9 +929,6 @@ def patch_modules(self) -> None: for module, attr in modules: if attr in self.unfaked_modules: self._stubs.smart_set(module, name, self.unfaked_modules[attr]) - if sys.version_info >= (3, 12): - # workaround for patching open - does not work with skip modules - self._stubs.smart_set(builtins, "open", self.fake_open) def patch_defaults(self) -> None: for fct, idx, ft in self.FS_DEFARGS: diff --git a/pyfakefs/fake_io.py b/pyfakefs/fake_io.py index fc719ad5..007f4c58 100644 --- a/pyfakefs/fake_io.py +++ b/pyfakefs/fake_io.py @@ -95,9 +95,16 @@ def open( # specific modules; instead we check if the caller is a skipped # module (should work in most cases) stack = traceback.extract_stack(limit=2) + # handle the case that we try to call the original `open_code` + # and get here instead (since Python 3.12) + from_open_code = ( + sys.version_info >= (3, 12) + and stack[0].name == "open_code" + and stack[0].line == "return self._io_module.open_code(path)" + ) module_name = os.path.splitext(stack[0].filename)[0] module_name = module_name.replace(os.sep, ".") - if any( + if from_open_code or any( [ module_name == sn or module_name.endswith("." + sn) for sn in self.skip_names diff --git a/pyfakefs/tests/fake_filesystem_unittest_test.py b/pyfakefs/tests/fake_filesystem_unittest_test.py index 60c240ee..156f543e 100644 --- a/pyfakefs/tests/fake_filesystem_unittest_test.py +++ b/pyfakefs/tests/fake_filesystem_unittest_test.py @@ -18,6 +18,7 @@ Test the :py:class`pyfakefs.fake_filesystem_unittest.TestCase` base class. """ import glob +import importlib.util import io import multiprocessing import os @@ -28,6 +29,8 @@ import tempfile import unittest import warnings +from contextlib import redirect_stdout +from io import StringIO from pathlib import Path from unittest import TestCase, mock @@ -228,14 +231,12 @@ def test_import_function_from_os_as_other_name(self): stat_result = pyfakefs.tests.import_as_example.file_stat2(file_path) self.assertEqual(3, stat_result.st_size) - @unittest.skipIf(sys.version_info >= (3, 12), "Currently not working in 3.12") def test_import_open_as_other_name(self): file_path = "/foo/bar" self.fs.create_file(file_path, contents=b"abc") contents = pyfakefs.tests.import_as_example.file_contents1(file_path) self.assertEqual("abc", contents) - @unittest.skipIf(sys.version_info >= (3, 12), "Currently not working in 3.12") def test_import_io_open_as_other_name(self): file_path = "/foo/bar" self.fs.create_file(file_path, contents=b"abc") @@ -398,10 +399,6 @@ def test_fake_path_does_not_exist7(self): self.fs.create_file("foo") self.assertFalse(pyfakefs.tests.import_as_example.check_if_exists7("foo")) - @unittest.skipIf( - sys.version_info >= (3, 12), - "Skip modules currently not working for open in 3.12", - ) def test_open_succeeds(self): pyfakefs.tests.import_as_example.open_this_file() @@ -447,10 +444,6 @@ def test_fake_path_does_not_exist7(self): self.fs.create_file("foo") self.assertFalse(pyfakefs.tests.import_as_example.check_if_exists7("foo")) - @unittest.skipIf( - sys.version_info >= (3, 12), - "Skip modules currently not working for open in 3.12", - ) def test_open_succeeds(self): pyfakefs.tests.import_as_example.open_this_file() @@ -787,6 +780,7 @@ def load_configs(configs): return retval +@unittest.skipIf(sys.version_info < (3, 8), "open_code new in Python 3.8") class AutoPatchOpenCodeTestCase(fake_filesystem_unittest.TestCase): """Test patching open_code in auto mode, see issue #554.""" @@ -806,6 +800,18 @@ def test_run_path(self): def test_run_module(self): load_configs([self.config_module]) + def import_foo(self): + spec = importlib.util.spec_from_file_location("bar", "/foo/bar.py") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + @unittest.skipIf(sys.platform == "win32", "Not yet working under Windows") + def test_exec_module_in_fake_fs(self): + self.fs.create_file("/foo/bar.py", contents="print('hello')") + with redirect_stdout(StringIO()) as stdout: + self.import_foo() + assert stdout.getvalue() == "hello\n" + class TestOtherFS(fake_filesystem_unittest.TestCase): def setUp(self): diff --git a/setup.cfg b/setup.cfg index be7953b7..d47921b1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Operating System :: POSIX