Skip to content

Commit

Permalink
Handle skipped pathlib.Path.open calls
Browse files Browse the repository at this point in the history
- if a module is skipped using additional_skip_names,
  Path.open is now handled the same way as io.open
  to use real fs calls
  • Loading branch information
mrbean-bremen committed May 12, 2024
1 parent 71044c9 commit 68ed3b9
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 42 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ The released versions correspond to PyPI releases.
and their path-parsing behaviors are now consistent regardless of runtime platform
and/or faked filesystem customization (see [#1006](../../issues/1006)).

### Fixes
* correctly use real open calls in pathlib for skipped modules (see [#1012](../../issues/1012))

## [Version 5.4.1](https://pypi.python.org/pypi/pyfakefs/5.4.0) (2024-04-11)
Fixes a regression.

Expand Down
46 changes: 11 additions & 35 deletions pyfakefs/fake_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@

import _io # pytype: disable=import-error
import io
import os
import sys
import traceback
from enum import Enum
from typing import (
List,
Expand All @@ -34,7 +32,7 @@
)

from pyfakefs.fake_file import AnyFileWrapper
from pyfakefs.fake_open import FakeFileOpen
from pyfakefs.fake_open import fake_open
from pyfakefs.helpers import IS_PYPY

if TYPE_CHECKING:
Expand Down Expand Up @@ -92,39 +90,17 @@ def open(
"""Redirect the call to FakeFileOpen.
See FakeFileOpen.call() for description.
"""
# workaround for built-in open called from skipped modules (see #552)
# as open is not imported explicitly, we cannot patch it for
# 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 from_open_code or any(
[
module_name == sn or module_name.endswith("." + sn)
for sn in self.skip_names
]
):
return io.open( # pytype: disable=wrong-arg-count
file,
mode,
buffering,
encoding,
errors,
newline,
closefd,
opener,
)
fake_open = FakeFileOpen(self.filesystem)
return fake_open(
file, mode, buffering, encoding, errors, newline, closefd, opener
self.filesystem,
self.skip_names,
file,
mode,
buffering,
encoding,
errors,
newline,
closefd,
opener,
)

if sys.version_info >= (3, 8):
Expand Down
56 changes: 54 additions & 2 deletions pyfakefs/fake_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"""A fake open() function replacement. See ``fake_filesystem`` for usage."""

import errno
import io
import os
import sys
import traceback
from stat import (
S_ISDIR,
)
Expand All @@ -28,6 +30,9 @@
cast,
AnyStr,
TYPE_CHECKING,
Callable,
IO,
List,
)

from pyfakefs import helpers
Expand Down Expand Up @@ -63,6 +68,54 @@
}


def fake_open(
filesystem: "FakeFilesystem",
skip_names: List[str],
file: Union[AnyStr, int],
mode: str = "r",
buffering: int = -1,
encoding: Optional[str] = None,
errors: Optional[str] = None,
newline: Optional[str] = None,
closefd: bool = True,
opener: Optional[Callable] = None,
) -> Union[AnyFileWrapper, IO[Any]]:
"""Redirect the call to FakeFileOpen.
See FakeFileOpen.call() for description.
"""
# workaround for built-in open called from skipped modules (see #552)
# as open is not imported explicitly, we cannot patch it for
# specific modules; instead we check if the caller is a skipped
# module (should work in most cases)
stack = traceback.extract_stack(limit=3)
# 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 from_open_code or any(
[module_name == sn or module_name.endswith("." + sn) for sn in skip_names]
):
return io.open( # pytype: disable=wrong-arg-count
file,
mode,
buffering,
encoding,
errors,
newline,
closefd,
opener,
)
fake_file_open = FakeFileOpen(filesystem)
return fake_file_open(
file, mode, buffering, encoding, errors, newline, closefd, opener
)


class FakeFileOpen:
"""Faked `file()` and `open()` function replacements.
Expand Down Expand Up @@ -288,7 +341,6 @@ def _init_file_object(
if open_modes.can_write:
if open_modes.truncate:
file_object.set_contents("")
file_object
else:
if open_modes.must_exist:
self.filesystem.raise_os_error(errno.ENOENT, file_path)
Expand Down Expand Up @@ -344,7 +396,7 @@ def _handle_file_arg(
can_write,
)

# open a file file by path
# open a file by path
file_path = cast(AnyStr, file_) # pytype: disable=invalid-annotation
if file_path == self.filesystem.dev_null.name:
file_object = self.filesystem.dev_null
Expand Down
25 changes: 21 additions & 4 deletions pyfakefs/fake_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@
import re
import sys
from pathlib import PurePath
from typing import Callable
from typing import Callable, List
from urllib.parse import quote_from_bytes as urlquote_from_bytes

from pyfakefs import fake_scandir
from pyfakefs.fake_filesystem import FakeFilesystem
from pyfakefs.fake_open import FakeFileOpen
from pyfakefs.fake_open import FakeFileOpen, fake_open
from pyfakefs.fake_os import FakeOsModule, use_original_os
from pyfakefs.helpers import IS_PYPY

Expand Down Expand Up @@ -532,6 +532,7 @@ class FakePath(pathlib.Path):

# the underlying fake filesystem
filesystem = None
skip_names: List[str] = []

def __new__(cls, *args, **kwargs):
"""Creates the correct subclass based on OS."""
Expand Down Expand Up @@ -631,8 +632,15 @@ def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None)
or permission is denied.
"""
self._raise_on_closed()
return FakeFileOpen(self.filesystem)(
self._path(), mode, buffering, encoding, errors, newline
return fake_open(
self.filesystem,
self.skip_names,
self._path(),
mode,
buffering,
encoding,
errors,
newline,
)

def read_bytes(self):
Expand Down Expand Up @@ -879,6 +887,15 @@ def __init__(self, filesystem=None):
if self.fake_pathlib is None:
self.__class__.fake_pathlib = FakePathlibModule(filesystem)

@property
def skip_names(self):
return [] # not used, here to allow a setter

@skip_names.setter
def skip_names(self, value):
# this is set from the patcher and passed to the fake Path class
self.fake_pathlib.Path.skip_names = value

def __call__(self, *args, **kwargs):
return self.fake_pathlib.Path(*args, **kwargs)

Expand Down
10 changes: 9 additions & 1 deletion pyfakefs/tests/fake_open_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from pyfakefs import fake_filesystem, helpers
from pyfakefs.helpers import is_root, IS_PYPY, get_locale_encoding
from pyfakefs.fake_io import FakeIoModule
from pyfakefs.fake_filesystem_unittest import PatchMode
from pyfakefs.fake_filesystem_unittest import PatchMode, Patcher
from pyfakefs.tests.skip_open import read_open
from pyfakefs.tests.test_utils import RealFsTestCase


Expand Down Expand Up @@ -2104,5 +2105,12 @@ def use_real_fs(self):
return True


class SkipOpenTest(unittest.TestCase):
def test_open_in_skipped_module(self):
with Patcher(additional_skip_names=["skip_open"]):
contents = read_open("skip_open.py")
self.assertTrue(contents.startswith("# Licensed under the Apache License"))


if __name__ == "__main__":
unittest.main()
10 changes: 10 additions & 0 deletions pyfakefs/tests/fake_pathlib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@

from pyfakefs import fake_pathlib, fake_filesystem, fake_filesystem_unittest, fake_os
from pyfakefs.fake_filesystem import OSType
from pyfakefs.fake_filesystem_unittest import Patcher
from pyfakefs.helpers import IS_PYPY, is_root
from pyfakefs.tests.skip_open import read_pathlib
from pyfakefs.tests.test_utils import RealFsTestMixin

is_windows = sys.platform == "win32"
Expand Down Expand Up @@ -1312,5 +1314,13 @@ def test_posix_pure_path_parsing(self):
)


class SkipOpenTest(unittest.TestCase):
def test_open_pathlib_in_skipped_module(self):
# regression test for #1012
with Patcher(additional_skip_names=["skip_open"]):
contents = read_pathlib("skip_open.py")
self.assertTrue(contents.startswith("# Licensed under the Apache License"))


if __name__ == "__main__":
unittest.main(verbosity=2)
26 changes: 26 additions & 0 deletions pyfakefs/tests/skip_open.py
Original file line number Diff line number Diff line change
@@ -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.
"""
Provides functions for testing additional_skip_names functionality.
"""

import os
from pathlib import Path


def read_pathlib(file_name):
return (Path(__file__).parent / file_name).open("r").read()


def read_open(file_name):
with open(os.path.join(os.path.dirname(__file__), file_name)) as f:
return f.read()

0 comments on commit 68ed3b9

Please sign in to comment.