Repository: pytest-dev/pyfakefs (python)

Issue:
`PureWindowsPath.stem` changes behavior when `fs` fixture is loaded
**Describe the bug**

When the `fs` fixture is loaded, `pathlib.PureWindowsPath.stem` returns more than the file's stem.


**How To Reproduce**

```bash
mkdir demo
cd demo
python -m venv .venv
source .venv/bin/activate
pip install pytest pyfakefs
```

Create a file named `test_demo.py` with this content:

```python
import pathlib

def test_basic():
    assert pathlib.PureWindowsPath(r"C:\example\demo.py").stem == "demo"

def test_pyfakefs(fs):
    assert pathlib.PureWindowsPath(r"C:\example\demo.py").stem == "demo"
```

Then run:

```bash
pytest test_demo.py
```

You will see the following output:

```
$ pytest test_demo.py 
============================== test session starts ===============================
platform linux -- Python 3.12.3, pytest-8.2.0, pluggy-1.5.0
rootdir: /home/kurt/dev/test
configfile: pyproject.toml
plugins: pyfakefs-5.4.1
collected 2 items                                                                

test_demo.py .F                                                            [100%]

==================================== FAILURES ====================================
_________________________________ test_pyfakefs __________________________________

fs = <pyfakefs.fake_filesystem.FakeFilesystem object at 0x7dec1ffc4e90>

    def test_pyfakefs(fs):
>       assert pathlib.PureWindowsPath(r"C:\example\demo.py").stem == "demo"
E       AssertionError: assert 'C:\\example\\demo' == 'demo'
E         
E         - demo
E         + C:\example\demo

/home/kurt/dev/test/test_demo.py:8: AssertionError
============================ short test summary info =============================
FAILED test_demo.py::test_pyfakefs - AssertionError: assert 'C:\\example\\demo' == 'demo'
========================== 1 failed, 1 passed in 0.08s ===========================
```

**Your environment**
```
Linux-6.5.0-28-generic-x86_64-with-glibc2.35
Python 3.12.3 (main, Apr 10 2024, 14:08:50) [GCC 11.4.0]
pyfakefs 5.4.1
pytest 8.2.0
```


Pull request description:
Fix pure windows path behavior
<!--
Please prefix your PR title with [WIP] for PRs that are still in progress.
-->

#### Describe the changes
This PR fixes path separators for Windows paths.

> [!NOTE]
>
> A single regression test for #558 fails as a result of this change, and only on Python 3.12. I am uneasy touching this test because I harbor concerns that this test is enforcing behavior that might not be worthwhile to support.
>
> Please review this work, together with this test, and let me know how to proceed. My gut is to remove the failing regression test but I didn't include that here.

Fixes #1006 

#### Tasks
- [x] Unit tests added that reproduce the issue or prove feature is working
- [x] Fix or feature added
- [ ] Entry to release notes added
- [x] Pre-commit CI shows no errors
- [ ] Unit tests passing
- [ ] For documentation changes: The Read the Docs preview builds and looks as expected


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index fa7c9e9..34aefc3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -11,6 +11,9 @@ The released versions correspond to PyPI releases.
 * The usage of the `pathlib2` and `scandir` modules in pyfakefs is now deprecated.
   They will now cause deprecation warnings if still used. Support for patching
   these modules will be removed in pyfakefs 6.0.
+* `PureWindowsPath` and `PurePosixPath` now use filesystem-independent path separators,
+  and their path-parsing behaviors are now consistent regardless of runtime platform
+  and/or faked filesystem customization (see [#1006](../../issues/1006)).
 
 ## [Version 5.4.1](https://pypi.python.org/pypi/pyfakefs/5.4.0) (2024-04-11)
 Fixes a regression.
diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py
index 5a3f46d..dc474c8 100644
--- a/pyfakefs/fake_pathlib.py
+++ b/pyfakefs/fake_pathlib.py
@@ -55,15 +55,38 @@ def init_module(filesystem):
     # pylint: disable=protected-access
     FakePath.filesystem = filesystem
     if sys.version_info < (3, 12):
-        FakePathlibModule.PureWindowsPath._flavour = _FakeWindowsFlavour(filesystem)
-        FakePathlibModule.PurePosixPath._flavour = _FakePosixFlavour(filesystem)
+        FakePathlibModule.WindowsPath._flavour = _FakeWindowsFlavour(filesystem)
+        FakePathlibModule.PosixPath._flavour = _FakePosixFlavour(filesystem)
+
+        # Pure POSIX path separators must be filesystem-independent.
+        fake_pure_posix_flavour = _FakePosixFlavour(filesystem)
+        fake_pure_posix_flavour.sep = "/"
+        fake_pure_posix_flavour.altsep = None
+        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.sep = "\\"
+        fake_pure_nt_flavour.altsep = "/"
+        FakePathlibModule.PureWindowsPath._flavour = fake_pure_nt_flavour
     else:
         # in Python 3.12, the flavour is no longer an own class,
         # but points to the os-specific path module (posixpath/ntpath)
         fake_os = FakeOsModule(filesystem)
-        fake_path = fake_os.path
-        FakePathlibModule.PureWindowsPath._flavour = fake_path
-        FakePathlibModule.PurePosixPath._flavour = fake_path
+        FakePathlibModule.PosixPath._flavour = fake_os.path
+        FakePathlibModule.WindowsPath._flavour = fake_os.path
+
+        # 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
+        FakePathlibModule.PurePosixPath._flavour = fake_pure_posix_os.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 = "/"
+        FakePathlibModule.PureWindowsPath._flavour = fake_pure_nt_os.path
 
 
 def _wrap_strfunc(strfunc):


Test patch:
diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py
index 145b0cd..f142361 100644
--- a/pyfakefs/tests/fake_pathlib_test.py
+++ b/pyfakefs/tests/fake_pathlib_test.py
@@ -1286,5 +1286,31 @@ 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."""
+
+        path = r"C:\Windows\cmd.exe"
+        self.assertEqual(
+            fake_pathlib.FakePathlibModule.PureWindowsPath(path).stem,
+            pathlib.PureWindowsPath(path).stem,
+        )
+
+        path = r"C:/Windows/cmd.exe"
+        self.assertEqual(
+            fake_pathlib.FakePathlibModule.PureWindowsPath(path).stem,
+            pathlib.PureWindowsPath(path).stem,
+        )
+
+    def test_posix_pure_path_parsing(self):
+        """Verify faked pure POSIX paths use filesystem-independent separators."""
+
+        path = r"/bin/bash"
+        self.assertEqual(
+            fake_pathlib.FakePathlibModule.PurePosixPath(path).stem,
+            pathlib.PurePosixPath(path).stem,
+        )
+
+
 if __name__ == "__main__":
     unittest.main(verbosity=2)