Repository: pytest-dev/pyfakefs
Pull request #1055 (2024-08-29 08:03:47)

Fix some problems with pathlib PosixPath/WindowsPath behavior
<!--
Please prefix your PR title with [WIP] for PRs that are still in progress.
-->

#### Describe the changes
Adding tests to document subtle pyfakefs differences described in issue 1053

#### Tasks
- [x] Unit tests added that reproduce the issue or prove feature is working
- [x] Fix or feature added
- [x] Entry to release notes added
- [x] Pre-commit CI shows no errors
- [x] Unit tests passing
- [ ] For documentation changes: The Read the Docs preview builds and looks as expected


mrbean-bremen: @camillobruni - I have added a commit that shall fix most of the issues, but I still got one failing test (test_real_file_with_home) under Posix and Python < 3.12, which I have disabled at the moment. 
Can you please check how this version works for you?
mrbean-bremen: > I haven't read all of this but wanted to offer feedback for what I did see.

Another pair of eyes is always helpful, especially as I rarely get reviews here. Given enough eyeballs...
camillobruni: Late reply back from vacation: thanks a lot for these fixes!
mrbean-bremen: @camillobruni - I added a workaround to get the failing test running, and I would merge this if you give your ok. Did you have a chance to test it?
Oh, and I forgot the release notes, will add these before merging, of course.
camillobruni: Cool, double checked locally and I got the tests working with this 👍 

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 038457a..57ad071 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -21,6 +21,8 @@ The released versions correspond to PyPI releases.
 
 ### Fixes
 * removing files while iterating over `scandir` results is now possible (see [#1051](../../issues/1051))
+* fake `pathlib.PosixPath` and `pathlib.WindowsPath` now behave more like in the real filesystem
+  (see [#1055](../../issues/1055))
 
 ## [Version 5.6.0](https://pypi.python.org/pypi/pyfakefs/5.6.0) (2024-07-12)
 Adds preliminary Python 3.13 support.
diff --git a/pyfakefs/fake_file.py b/pyfakefs/fake_file.py
index 5be1598..d43eb6d 100644
--- a/pyfakefs/fake_file.py
+++ b/pyfakefs/fake_file.py
@@ -419,8 +419,7 @@ class FakeFile:
 
 class FakeNullFile(FakeFile):
     def __init__(self, filesystem: "FakeFilesystem") -> None:
-        devnull = "nul" if filesystem.is_windows_fs else "/dev/null"
-        super().__init__(devnull, filesystem=filesystem, contents="")
+        super().__init__(filesystem.devnull, filesystem=filesystem, contents="")
 
     @property
     def byte_contents(self) -> bytes:
diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py
index 56f0e47..1aa5e48 100644
--- a/pyfakefs/fake_filesystem.py
+++ b/pyfakefs/fake_filesystem.py
@@ -81,6 +81,8 @@ True
 True
 """
 
+import contextlib
+import dataclasses
 import errno
 import heapq
 import os
@@ -101,7 +103,6 @@ from stat import (
 )
 from typing import (
     List,
-    Optional,
     Callable,
     Union,
     Any,
@@ -111,6 +112,7 @@ from typing import (
     AnyStr,
     overload,
     NoReturn,
+    Optional,
 )
 
 from pyfakefs import fake_file, fake_path, fake_io, fake_os, helpers, fake_open
@@ -122,6 +124,9 @@ from pyfakefs.helpers import (
     matching_string,
     AnyPath,
     AnyString,
+    WINDOWS_PROPERTIES,
+    POSIX_PROPERTIES,
+    FSType,
 )
 
 if sys.platform.startswith("linux"):
@@ -179,10 +184,6 @@ class FakeFilesystem:
     """Provides the appearance of a real directory tree for unit testing.
 
     Attributes:
-        path_separator: The path separator, corresponds to `os.path.sep`.
-        alternative_path_separator: Corresponds to `os.path.altsep`.
-        is_windows_fs: `True` in a real or faked Windows file system.
-        is_macos: `True` under MacOS, or if we are faking it.
         is_case_sensitive: `True` if a case-sensitive file system is assumed.
         root: The root :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` entry
             of the file system.
@@ -217,12 +218,8 @@ class FakeFilesystem:
         >>> filesystem = FakeFilesystem(path_separator='/')
 
         """
-        self.path_separator: str = path_separator
-        self.alternative_path_separator: Optional[str] = os.path.altsep
         self.patcher = patcher
         self.create_temp_dir = create_temp_dir
-        if path_separator != os.sep:
-            self.alternative_path_separator = None
 
         # is_windows_fs can be used to test the behavior of pyfakefs under
         # Windows fs on non-Windows systems and vice verse;
@@ -235,7 +232,19 @@ class FakeFilesystem:
 
         # is_case_sensitive can be used to test pyfakefs for case-sensitive
         # file systems on non-case-sensitive systems and vice verse
-        self.is_case_sensitive: bool = not (self.is_windows_fs or self._is_macos)
+        self.is_case_sensitive: bool = not (self._is_windows_fs or self._is_macos)
+
+        # by default, we use the configured filesystem
+        self.fs_type = FSType.DEFAULT
+        base_properties = (
+            WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
+        )
+        self.fs_properties = [
+            dataclasses.replace(base_properties),
+            POSIX_PROPERTIES,
+            WINDOWS_PROPERTIES,
+        ]
+        self.path_separator = path_separator
 
         self.root: FakeDirectory
         self._cwd = ""
@@ -262,30 +271,71 @@ class FakeFilesystem:
 
     @property
     def is_linux(self) -> bool:
+        """Returns `True` in a real or faked Linux file system."""
         return not self.is_windows_fs and not self.is_macos
 
     @property
     def is_windows_fs(self) -> bool:
-        return self._is_windows_fs
+        """Returns `True` in a real or faked Windows file system."""
+        return self.fs_type == FSType.WINDOWS or (
+            self.fs_type == FSType.DEFAULT and self._is_windows_fs
+        )
 
     @is_windows_fs.setter
     def is_windows_fs(self, value: bool) -> None:
         if self._is_windows_fs != value:
             self._is_windows_fs = value
+            if value:
+                self._is_macos = False
             self.reset()
             FakePathModule.reset(self)
 
     @property
     def is_macos(self) -> bool:
+        """Returns `True` in a real or faked macOS file system."""
         return self._is_macos
 
     @is_macos.setter
     def is_macos(self, value: bool) -> None:
         if self._is_macos != value:
             self._is_macos = value
+            if value:
+                self._is_windows_fs = False
             self.reset()
             FakePathModule.reset(self)
 
+    @property
+    def path_separator(self) -> str:
+        """Returns the path separator, corresponds to `os.path.sep`."""
+        return self.fs_properties[self.fs_type.value].sep
+
+    @path_separator.setter
+    def path_separator(self, value: str) -> None:
+        self.fs_properties[0].sep = value
+        if value != os.sep:
+            self.alternative_path_separator = None
+
+    @property
+    def alternative_path_separator(self) -> Optional[str]:
+        """Returns the alternative path separator, corresponds to `os.path.altsep`."""
+        return self.fs_properties[self.fs_type.value].altsep
+
+    @alternative_path_separator.setter
+    def alternative_path_separator(self, value: Optional[str]) -> None:
+        self.fs_properties[0].altsep = value
+
+    @property
+    def devnull(self) -> str:
+        return self.fs_properties[self.fs_type.value].devnull
+
+    @property
+    def pathsep(self) -> str:
+        return self.fs_properties[self.fs_type.value].pathsep
+
+    @property
+    def line_separator(self) -> str:
+        return self.fs_properties[self.fs_type.value].linesep
+
     @property
     def cwd(self) -> str:
         """Return the current working directory of the fake filesystem."""
@@ -334,8 +384,11 @@ class FakeFilesystem:
         self._is_windows_fs = value == OSType.WINDOWS
         self._is_macos = value == OSType.MACOS
         self.is_case_sensitive = value == OSType.LINUX
-        self.path_separator = "\\" if value == OSType.WINDOWS else "/"
-        self.alternative_path_separator = "/" if value == OSType.WINDOWS else None
+        self.fs_type = FSType.DEFAULT
+        base_properties = (
+            WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
+        )
+        self.fs_properties[0] = base_properties
         self.reset()
         FakePathModule.reset(self)
 
@@ -358,6 +411,15 @@ class FakeFilesystem:
 
             fake_pathlib.init_module(self)
 
+    @contextlib.contextmanager
+    def use_fs_type(self, fs_type: FSType):
+        old_fs_type = self.fs_type
+        try:
+            self.fs_type = fs_type
+            yield
+        finally:
+            self.fs_type = old_fs_type
+
     def _add_root_mount_point(self, total_size):
         mount_point = "C:" if self.is_windows_fs else self.path_separator
         self._cwd = mount_point
@@ -403,9 +465,6 @@ class FakeFilesystem:
         if self.patcher:
             self.patcher.clear_cache()
 
-    def line_separator(self) -> str:
-        return "\r\n" if self.is_windows_fs else "\n"
-
     def raise_os_error(
         self,
         err_no: int,
@@ -1144,8 +1203,9 @@ class FakeFilesystem:
         if isinstance(p, bytes):
             sep = self.path_separator.encode()
             altsep = None
-            if self.alternative_path_separator:
-                altsep = self.alternative_path_separator.encode()
+            alternative_path_separator = self.alternative_path_separator
+            if alternative_path_separator is not None:
+                altsep = alternative_path_separator.encode()
             colon = b":"
             unc_prefix = b"\\\\?\\UNC\\"
             empty = b""
@@ -1292,7 +1352,11 @@ class FakeFilesystem:
         if not path or path == self.get_path_separator(path):
             return []
         drive, path = self.splitdrive(path)
-        path_components = path.split(self.get_path_separator(path))
+        sep = self.get_path_separator(path)
+        # handle special case of Windows emulated under POSIX
+        if self.is_windows_fs and sys.platform != "win32":
+            path = path.replace(matching_string(sep, "\\"), sep)
+        path_components = path.split(sep)
         assert drive or path_components
         if not path_components[0]:
             if len(path_components) > 1 and not path_components[1]:
@@ -1438,7 +1502,7 @@ class FakeFilesystem:
             raise TypeError
         if not path:
             return False
-        if path == self.dev_null.name:
+        if path == self.devnull:
             return not self.is_windows_fs or sys.version_info >= (3, 8)
         try:
             if self.is_filepath_ending_with_separator(path):
@@ -1515,7 +1579,7 @@ class FakeFilesystem:
         path = self.replace_windows_root(path)
         if self._is_root_path(path):
             return path
-        if path == matching_string(path, self.dev_null.name):
+        if path == matching_string(path, self.devnull):
             return path
         path_components = self._path_components(path)
         resolved_components = self._resolve_components(path_components)
@@ -1661,7 +1725,7 @@ class FakeFilesystem:
         path = make_string_path(file_path)
         if path == matching_string(path, self.root.name):
             return self.root
-        if path == matching_string(path, self.dev_null.name):
+        if path == matching_string(path, self.devnull):
             return self.dev_null
 
         path = self._original_path(path)
diff --git a/pyfakefs/fake_path.py b/pyfakefs/fake_path.py
index 013b9e7..7719ef0 100644
--- a/pyfakefs/fake_path.py
+++ b/pyfakefs/fake_path.py
@@ -123,9 +123,9 @@ class FakePathModule:
     def reset(cls, filesystem: "FakeFilesystem") -> None:
         cls.sep = filesystem.path_separator
         cls.altsep = filesystem.alternative_path_separator
-        cls.linesep = filesystem.line_separator()
-        cls.devnull = "nul" if filesystem.is_windows_fs else "/dev/null"
-        cls.pathsep = ";" if filesystem.is_windows_fs else ":"
+        cls.linesep = filesystem.line_separator
+        cls.devnull = filesystem.devnull
+        cls.pathsep = filesystem.pathsep
 
     def exists(self, path: AnyStr) -> bool:
         """Determine whether the file object exists within the fake filesystem.
diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py
index 55942b8..c437eb5 100644
--- a/pyfakefs/fake_pathlib.py
+++ b/pyfakefs/fake_pathlib.py
@@ -42,14 +42,15 @@ import re
 import sys
 import warnings
 from pathlib import PurePath
-from typing import Callable, List
+from typing import Callable, List, Optional
 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 fake_open
 from pyfakefs.fake_os import FakeOsModule, use_original_os
-from pyfakefs.helpers import IS_PYPY, is_called_from_skipped_module
+from pyfakefs.fake_path import FakePathModule
+from pyfakefs.helpers import IS_PYPY, is_called_from_skipped_module, FSType
 
 
 def init_module(filesystem):
@@ -67,30 +68,27 @@ def init_module(filesystem):
         FakePathlibModule.PurePosixPath._flavour = fake_pure_posix_flavour
 
         # Pure Windows path separators must be filesystem-independent.
-        fake_pure_nt_flavour = _FakePosixFlavour(filesystem)
+        fake_pure_nt_flavour = _FakeWindowsFlavour(filesystem)
         fake_pure_nt_flavour.sep = "\\"
         fake_pure_nt_flavour.altsep = "/"
         FakePathlibModule.PureWindowsPath._flavour = fake_pure_nt_flavour
     else:
         # in Python > 3.11, the flavour is no longer a separate class,
         # but points to the os-specific path module (posixpath/ntpath)
-        fake_os = FakeOsModule(filesystem)
-        parser_name = "_flavour" if sys.version_info < (3, 13) else "parser"
+        fake_os_posix = FakeOsModule(filesystem)
+        if filesystem.is_windows_fs:
+            fake_os_posix.path = FakePosixPathModule(filesystem, fake_os_posix)
+        fake_os_windows = FakeOsModule(filesystem)
+        if not filesystem.is_windows_fs:
+            fake_os_windows.path = FakeWindowsPathModule(filesystem, fake_os_windows)
 
-        setattr(FakePathlibModule.PosixPath, parser_name, fake_os.path)
-        setattr(FakePathlibModule.WindowsPath, parser_name, fake_os.path)
+        parser_name = "_flavour" if sys.version_info < (3, 13) else "parser"
 
-        # Pure POSIX path separators must be filesystem independent.
-        fake_pure_posix_os = FakeOsModule(filesystem)
-        fake_pure_posix_os.path.sep = "/"
-        fake_pure_posix_os.path.altsep = None
-        setattr(FakePathlibModule.PurePosixPath, parser_name, fake_pure_posix_os.path)
+        # Pure POSIX path properties must be filesystem independent.
+        setattr(FakePathlibModule.PurePosixPath, parser_name, fake_os_posix.path)
 
-        # Pure Windows path separators must be filesystem independent.
-        fake_pure_nt_os = FakeOsModule(filesystem)
-        fake_pure_nt_os.path.sep = "\\"
-        fake_pure_nt_os.path.altsep = "/"
-        setattr(FakePathlibModule.PureWindowsPath, parser_name, fake_pure_nt_os.path)
+        # Pure Windows path properties must be filesystem independent.
+        setattr(FakePathlibModule.PureWindowsPath, parser_name, fake_os_windows.path)
 
 
 def _wrap_strfunc(fake_fct, original_fct):
@@ -242,9 +240,6 @@ if sys.version_info < (3, 12):
         """Fake Flavour implementation used by PurePath and _Flavour"""
 
         filesystem = None
-        sep = "/"
-        altsep = None
-        has_drv = False
 
         ext_namespace_prefix = "\\\\?\\"
 
@@ -254,9 +249,6 @@ if sys.version_info < (3, 12):
 
         def __init__(self, filesystem):
             self.filesystem = filesystem
-            self.sep = filesystem.path_separator
-            self.altsep = filesystem.alternative_path_separator
-            self.has_drv = filesystem.is_windows_fs
             super().__init__()
 
         @staticmethod
@@ -320,9 +312,13 @@ if sys.version_info < (3, 12):
 
         def splitroot(self, path, sep=None):
             """Split path into drive, root and rest."""
+            is_windows = isinstance(self, _FakeWindowsFlavour)
             if sep is None:
-                sep = self.filesystem.path_separator
-            if self.filesystem.is_windows_fs:
+                if is_windows == self.filesystem.is_windows_fs:
+                    sep = self.filesystem.path_separator
+                else:
+                    sep = self.sep
+            if is_windows:
                 return self._splitroot_with_drive(path, sep)
             return self._splitroot_posix(path, sep)
 
@@ -443,6 +439,9 @@ if sys.version_info < (3, 12):
             | {"COM%d" % i for i in range(1, 10)}
             | {"LPT%d" % i for i in range(1, 10)}
         )
+        sep = "\\"
+        altsep = "/"
+        has_drv = True
         pathmod = ntpath
 
         def is_reserved(self, parts):
@@ -519,6 +518,9 @@ if sys.version_info < (3, 12):
         independent of FakeFilesystem properties.
         """
 
+        sep = "/"
+        altsep: Optional[str] = None
+        has_drv = False
         pathmod = posixpath
 
         def is_reserved(self, parts):
@@ -551,6 +553,36 @@ if sys.version_info < (3, 12):
 
         def compile_pattern(self, pattern):
             return re.compile(fnmatch.translate(pattern)).fullmatch
+else:  # Python >= 3.12
+
+    class FakePosixPathModule(FakePathModule):
+        def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
+            super().__init__(filesystem, os_module)
+            with self.filesystem.use_fs_type(FSType.POSIX):
+                self.reset(self.filesystem)
+
+    class FakeWindowsPathModule(FakePathModule):
+        def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
+            super().__init__(filesystem, os_module)
+            with self.filesystem.use_fs_type(FSType.WINDOWS):
+                self.reset(self.filesystem)
+
+    def with_fs_type(f: Callable, fs_type: FSType) -> Callable:
+        """Decorator used for fake_path methods to ensure that
+        the correct filesystem type is used."""
+
+        @functools.wraps(f)
+        def wrapped(self, *args, **kwargs):
+            with self.filesystem.use_fs_type(fs_type):
+                return f(self, *args, **kwargs)
+
+        return wrapped
+
+    # decorate all public functions to use the correct fs type
+    for fct_name in FakePathModule.dir():
+        fn = getattr(FakePathModule, fct_name)
+        setattr(FakeWindowsPathModule, fct_name, with_fs_type(fn, FSType.WINDOWS))
+        setattr(FakePosixPathModule, fct_name, with_fs_type(fn, FSType.POSIX))
 
 
 class FakePath(pathlib.Path):
@@ -878,15 +910,11 @@ class FakePathlibModule:
         paths"""
 
         __slots__ = ()
-        if sys.version_info >= (3, 13):
-            parser = posixpath
 
     class PureWindowsPath(PurePath):
         """A subclass of PurePath, that represents Windows filesystem paths"""
 
         __slots__ = ()
-        if sys.version_info >= (3, 13):
-            parser = ntpath
 
     class WindowsPath(FakePath, PureWindowsPath):
         """A subclass of Path and PureWindowsPath that represents
@@ -1010,9 +1038,9 @@ if sys.version_info > (3, 10):
 
         return wrapped
 
-    for name, fn in inspect.getmembers(RealPath, inspect.isfunction):
-        if not name.startswith("__"):
-            setattr(RealPath, name, with_original_os(fn))
+    for fct_name, fn in inspect.getmembers(RealPath, inspect.isfunction):
+        if not fct_name.startswith("__"):
+            setattr(RealPath, fct_name, with_original_os(fn))
 
 
 class RealPathlibPathModule:
diff --git a/pyfakefs/helpers.py b/pyfakefs/helpers.py
index 674ce8f..652969d 100644
--- a/pyfakefs/helpers.py
+++ b/pyfakefs/helpers.py
@@ -24,6 +24,8 @@ import time
 import traceback
 from collections import namedtuple
 from copy import copy
+from dataclasses import dataclass
+from enum import Enum
 from stat import S_IFLNK
 from typing import Union, Optional, Any, AnyStr, overload, cast
 
@@ -205,6 +207,42 @@ def matching_string(  # type: ignore[misc]
     return string  # pytype: disable=bad-return-type
 
 
+@dataclass
+class FSProperties:
+    sep: str
+    altsep: Optional[str]
+    pathsep: str
+    linesep: str
+    devnull: str
+
+
+# pure POSIX file system properties, for use with PosixPath
+POSIX_PROPERTIES = FSProperties(
+    sep="/",
+    altsep=None,
+    pathsep=":",
+    linesep="\n",
+    devnull="/dev/null",
+)
+
+# pure Windows file system properties, for use with WindowsPath
+WINDOWS_PROPERTIES = FSProperties(
+    sep="\\",
+    altsep="/",
+    pathsep=";",
+    linesep="\r\n",
+    devnull="NUL",
+)
+
+
+class FSType(Enum):
+    """Defines which file system properties to use."""
+
+    DEFAULT = 0  # use current OS file system + modifications in fake file system
+    POSIX = 1  # pure POSIX properties, for use in PosixPath
+    WINDOWS = 2  # pure Windows properties, for use in WindowsPath
+
+
 class FakeStatResult:
     """Mimics os.stat_result for use as return type of `stat()` and similar.
     This is needed as `os.stat_result` has no possibility to set


Test patch:
diff --git a/pyfakefs/tests/fake_filesystem_unittest_test.py b/pyfakefs/tests/fake_filesystem_unittest_test.py
index 89f6783..4b3de17 100644
--- a/pyfakefs/tests/fake_filesystem_unittest_test.py
+++ b/pyfakefs/tests/fake_filesystem_unittest_test.py
@@ -850,7 +850,7 @@ class TestOtherFS(fake_filesystem_unittest.TestCase):
         self.assertEqual("/", os.altsep)
         self.assertEqual(";", os.pathsep)
         self.assertEqual("\r\n", os.linesep)
-        self.assertEqual("nul", os.devnull)
+        self.assertEqual("NUL", os.devnull)
 
     def test_linux(self):
         self.fs.os = OSType.LINUX
diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py
index afd3efe..afc438a 100644
--- a/pyfakefs/tests/fake_pathlib_test.py
+++ b/pyfakefs/tests/fake_pathlib_test.py
@@ -198,7 +198,7 @@ class FakePathlibInitializationWithDriveTest(RealPathlibTestCase):
         )
         self.assertEqual(path.parents[1], self.path("d:"))
 
-    @unittest.skipIf(not is_windows, "Windows-specifc behavior")
+    @unittest.skipIf(not is_windows, "Windows-specific behavior")
     def test_is_absolute(self):
         self.assertTrue(self.path("c:/a/b").is_absolute())
         self.assertFalse(self.path("/a/b").is_absolute())
@@ -445,7 +445,7 @@ class FakePathlibFileObjectPropertyTest(RealPathlibTestCase):
         self.create_dir(self.make_path("antoine", "docs"))
         self.create_file(self.make_path("antoine", "setup.py"))
         self.os.chdir(self.make_path("antoine"))
-        # use real path to handle symlink /var to /private/var in MacOs
+        # use real path to handle symlink /var to /private/var in macOS
         self.assert_equal_paths(
             self.path().resolve(),
             self.path(self.os.path.realpath(self.make_path("antoine"))),
@@ -503,7 +503,7 @@ class FakePathlibFileObjectPropertyTest(RealPathlibTestCase):
         assert len(list(directory.glob("*.txt"))) == 1
         assert list(directory.glob("*.txt"))[0] == file_path
 
-        # We cannot read files inside of the directory,
+        # We cannot read files inside the directory,
         # even if we have read access to the file
         with self.assertRaises(PermissionError):
             file_path.stat()
@@ -874,10 +874,10 @@ class FakePathlibUsageInOsFunctionsTest(RealPathlibTestCase):
     def test_join(self):
         dir1 = "foo"
         dir2 = "bar"
-        dir = self.os.path.join(dir1, dir2)
-        self.assertEqual(dir, self.os.path.join(self.path(dir1), dir2))
-        self.assertEqual(dir, self.os.path.join(dir1, self.path(dir2)))
-        self.assertEqual(dir, self.os.path.join(self.path(dir1), self.path(dir2)))
+        dir3 = self.os.path.join(dir1, dir2)
+        self.assertEqual(dir3, self.os.path.join(self.path(dir1), dir2))
+        self.assertEqual(dir3, self.os.path.join(dir1, self.path(dir2)))
+        self.assertEqual(dir3, self.os.path.join(self.path(dir1), self.path(dir2)))
 
     def test_normcase(self):
         dir1 = self.make_path("Foo", "Bar", "Baz")
@@ -1236,7 +1236,7 @@ class RealPathlibUsageInOsFunctionsTest(FakePathlibUsageInOsFunctionsTest):
 
 class FakeFilesystemPathLikeObjectTest(unittest.TestCase):
     def setUp(self):
-        self.filesystem = fake_filesystem.FakeFilesystem(path_separator="/")
+        self.filesystem = fake_filesystem.FakeFilesystem()
         self.pathlib = fake_pathlib.FakePathlibModule(self.filesystem)
         self.os = fake_os.FakeOsModule(self.filesystem)
 
@@ -1312,30 +1312,144 @@ class FakeFilesystemChmodTest(fake_filesystem_unittest.TestCase):
             path.is_file()
 
 
-class FakePathlibModulePurePathTest(unittest.TestCase):
-    def test_windows_pure_path_parsing(self):
-        """Verify faked pure Windows paths use filesystem-independent separators."""
-
+class FakePathlibModulePurePathTest(fake_filesystem_unittest.TestCase):
+    def test_windows_pure_path_parsing_backslash(self):
         path = r"C:\Windows\cmd.exe"
+        pure_result = pathlib.PureWindowsPath(path).stem
+        self.assertEqual("cmd", pure_result)
+
+        self.setUpPyfakefs()
         self.assertEqual(
-            fake_pathlib.FakePathlibModule.PureWindowsPath(path).stem,
-            pathlib.PureWindowsPath(path).stem,
+            pure_result, fake_pathlib.FakePathlibModule.PureWindowsPath(path).stem
         )
+        self.assertEqual(pure_result, pathlib.PureWindowsPath(path).stem)
 
+    def test_windows_pure_path_parsing_forward_slash(self):
         path = r"C:/Windows/cmd.exe"
+        pure_result = pathlib.PureWindowsPath(path).stem
+        self.assertEqual("cmd", pure_result)
+
+        self.setUpPyfakefs()
         self.assertEqual(
-            fake_pathlib.FakePathlibModule.PureWindowsPath(path).stem,
-            pathlib.PureWindowsPath(path).stem,
+            pure_result, fake_pathlib.FakePathlibModule.PureWindowsPath(path).stem
         )
+        self.assertEqual(pure_result, pathlib.PureWindowsPath(path).stem)
 
     def test_posix_pure_path_parsing(self):
-        """Verify faked pure POSIX paths use filesystem-independent separators."""
-
         path = r"/bin/bash"
+        pure_result = pathlib.PurePosixPath(path).stem
+        self.assertEqual("bash", pure_result)
+
+        self.setUpPyfakefs()
         self.assertEqual(
-            fake_pathlib.FakePathlibModule.PurePosixPath(path).stem,
-            pathlib.PurePosixPath(path).stem,
+            pure_result, fake_pathlib.FakePathlibModule.PurePosixPath(path).stem
         )
+        self.assertEqual(pathlib.PurePosixPath(path).stem, pure_result)
+
+    def test_windows_pure_path_str_backslash(self):
+        path = r"C:\Windows\cmd.exe"
+        pure_result = str(pathlib.PureWindowsPath(path))
+        self.assertEqual(r"C:\Windows\cmd.exe", pure_result)
+
+        self.setUpPyfakefs()
+        self.assertEqual(
+            pure_result, str(fake_pathlib.FakePathlibModule.PureWindowsPath(path))
+        )
+        self.assertEqual(str(pathlib.PureWindowsPath(path)), pure_result)
+
+    def test_windows_pure_path_str_forward_slash(self):
+        path = "C:/Windows/cmd.exe"
+        pure_result_win = str(pathlib.PureWindowsPath(path))
+        self.assertEqual(r"C:\Windows\cmd.exe", pure_result_win)
+        pure_result_posix_stem = str(pathlib.PurePosixPath(path).stem)
+        self.assertEqual("cmd", pure_result_posix_stem)
+
+        self.setUpPyfakefs()
+        self.assertEqual(
+            pure_result_win, str(fake_pathlib.FakePathlibModule.PureWindowsPath(path))
+        )
+        self.assertEqual(pure_result_win, str(pathlib.PureWindowsPath(path)))
+        self.assertEqual(pure_result_posix_stem, pathlib.PurePosixPath(path).stem)
+
+    def test_posix_pure_path_str_backslash(self):
+        path = r"\bin\bash"
+        pure_result = str(pathlib.PurePosixPath(path))
+        self.assertEqual(r"\bin\bash", pure_result)
+
+        self.setUpPyfakefs()
+        self.assertEqual(
+            pure_result, str(fake_pathlib.FakePathlibModule.PurePosixPath(path))
+        )
+        self.assertEqual(pure_result, str(pathlib.PurePosixPath(path)))
+
+    def test_posix_pure_path_str_forward_slash(self):
+        path = "/bin/bash"
+        pure_result = str(pathlib.PurePosixPath(path))
+        self.assertEqual(r"/bin/bash", pure_result)
+
+        self.setUpPyfakefs()
+        self.assertEqual(
+            pure_result, str(fake_pathlib.FakePathlibModule.PurePosixPath(path))
+        )
+        self.assertEqual(pure_result, str(pathlib.PurePosixPath(path)))
+
+    def check_posix_pure_path_is_absolute(self, path, expected_result):
+        pure_result = pathlib.PurePosixPath(path).is_absolute()
+        self.assertEqual(expected_result, pure_result)
+
+        self.setUpPyfakefs()
+        self.assertEqual(
+            pure_result,
+            fake_pathlib.FakePathlibModule.PurePosixPath(path).is_absolute(),
+        )
+        self.assertEqual(pure_result, pathlib.PurePosixPath(path).is_absolute())
+
+    def test_posix_pure_path_is_absolute_for_absolute_path(self):
+        self.check_posix_pure_path_is_absolute("/bin/bash", expected_result=True)
+
+    def test_posix_pure_path_is_absolute_for_local_path(self):
+        self.check_posix_pure_path_is_absolute("bin/bash", expected_result=False)
+
+    def test_posix_pure_path_is_absolute_for_relative_path(self):
+        self.check_posix_pure_path_is_absolute("../bin/bash", expected_result=False)
+
+    def check_windows_pure_path_is_absolute(self, path, expected_result):
+        pure_result = pathlib.PureWindowsPath(path).is_absolute()
+        self.assertEqual(expected_result, pure_result)
+
+        self.setUpPyfakefs()
+        self.assertEqual(
+            pure_result,
+            fake_pathlib.FakePathlibModule.PureWindowsPath(path).is_absolute(),
+        )
+        self.assertEqual(pure_result, pathlib.PureWindowsPath(path).is_absolute())
+
+    def test_windows_pure_path_is_absolute_for_absolute_path(self):
+        self.check_windows_pure_path_is_absolute("C:/Windows/cmd.exe", True)
+
+    def test_windows_pure_path_is_absolute_for_local_path(self):
+        self.check_windows_pure_path_is_absolute("./cmd.exe", expected_result=False)
+
+    def test_windows_pure_path_is_absolute_for_relative_path(self):
+        self.check_windows_pure_path_is_absolute("../cmd.exe", expected_result=False)
+
+
+class FakePathlibModulePurePathTestWindows(FakePathlibModulePurePathTest):
+    def setUpPyfakefs(self, **kwargs):
+        super().setUpPyfakefs(**kwargs)
+        self.fs.os = OSType.WINDOWS
+
+
+class FakePathlibModulePurePathTestMacos(FakePathlibModulePurePathTest):
+    def setUpPyfakefs(self, **kwargs):
+        super().setUpPyfakefs(**kwargs)
+        self.fs.os = OSType.MACOS
+
+
+class FakePathlibModulePurePathTestLinux(FakePathlibModulePurePathTest):
+    def setUpPyfakefs(self, **kwargs):
+        super().setUpPyfakefs(**kwargs)
+        self.fs.os = OSType.LINUX
 
 
 class SkipPathlibTest(fake_filesystem_unittest.TestCase):