Skip to content

Make FakePathlibModule.Path compare equal to pathlib.Path #478

Description

@Aran-Fey

Sometimes it happens that a test case compares a fake path to a real path - usually when you've created a pathlib.Path in pytest.mark.parametrize, like so:

def my_function():
    return pathlib.Path()

@pytest.mark.parametrize('expected_result', [pathlib.Path()])
def test_my_function(fs, expected_result):
    assert my_function() == expected_result

This fails with AssertionError: assert WindowsPath('.') == WindowsPath('.').

This could of course be avoided by passing in a string and converting it to a path inside the test, but in reality there isn't always such a convenient workaround. Here's a more realistic example, where an object with loads of attributes - some of which are Paths - is loaded from disk and compared to the expected result:

import pytest
from pathlib import Path
from my_module import MyClass

@pytest.mark.parametrize('attrs', [
    {'foo': False,
     'origin': Path(),
     'bar': 17,
     'paths': [Path('/'), Path('somewhere')]},
])
def test_serialization(fs, attrs):
    obj = MyClass(**attrs)
    obj.dump('myobj.dump')
    
    loaded_obj = MyClass.load_file('myobj.dump')
    assert vars(loaded_obj) == attrs

As I'm sure you can see, having to convert all of those attributes to Paths inside the test is rather annoying. For this reason it would be very convenient if fake Paths would compare equal to real Paths.

Activity

  1. mrbean-bremen commented on Apr 28, 2019

    @mrbean-bremen
    Member

    Thanks for the report! I see your problem, and I think there is an easy fix for this (overwriting __eq__), as long as the comparison is this way around, e.g. comparing the fake path against a real path. Would that be sufficient for you?

  2. Aran-Fey commented on Apr 28, 2019

    @Aran-Fey
    Author

    Hmm, I think that would probably be sufficient, yes. I'm not sure if it's a good idea to have an asymmetrical __eq__ though. Might cause some confusing errors if someone ever puts the fake path on the wrong side of the comparison. Wouldn't it be better to go all the way and monkeypatch pathlib.Path.__eq__?

  3. mrbean-bremen commented on Apr 28, 2019

    @mrbean-bremen
    Member

    Well, the question is where to do this. The actual problem is that the pytest.parametrize decorator is read before any patching occurs - this is one of the instances that are mentioned in the documentation, where patching won't work. I agree that overwriting __eq__ is not the best way to handle this. The corrrect way would be to ensure that the pathlib used in the parameter is patched - I will think about this...

  4. Aran-Fey commented on Apr 29, 2019

    @Aran-Fey
    Author

    For now, I've worked around the problem by adding this monkey patch to my conftest.py:

    # monkeypatch Path.__eq__ so that pyfakefs FakePaths compare equal to real pathlib.Paths
    from pathlib import Path
    from pyfakefs.fake_pathlib import FakePath
    
    # Path somehow gets monkeypatched during testing, so in order to have access
    # to the original class we'll simply create an instance of it
    PATH = object.__new__(Path)
    
    def path_eq(self, other):
        Path = type(PATH)
    
        if isinstance(other, (Path, FakePath)):
            return str(self) == str(other)
    
        return super(Path, self).__eq__(other)
    
    Path.__eq__ = path_eq
  5. mrbean-bremen commented on Apr 29, 2019

    @mrbean-bremen
    Member

    Yes, this is a valid solution, at least for your case. I have been thinking about adding the same monkeypatching to the pytest plugin, but I'm somewhat reluctant to do this, as generally there could be other non-patched fs calls in a parametrize decorator. I will have another look some time later.

  6. mrbean-bremen commented on May 23, 2021

    @mrbean-bremen
    Member

    Getting back to this, I now think that the correct way to resolve this would be to reload the sut module during the test (for example using modules_to_reload in the test setup), as this is the only way that ensures that the code that is read at load time is correctly patched -- see the documentation for how to do this with pytest.
    Actually, the fixture in the documentation is too complicated, it could look like this:

    import pytest
    from pyfakefs.fake_filesystem_unittest import Patcher
    import sut
    
    @pytest.fixture
    def fs_reload_sut():
        with Patcher(modules_to_reload=[sut]) as p:
            yield p.fs
  7. mrbean-bremen commented on May 23, 2021

    @mrbean-bremen
    Member

    @jmcgeheeiv - instead of closing the issue, I would like to to convert this (and other issues that may be helpful) into a Q/A discussion issue. If you agree, could you enable discussions in the pyfakefs configuration?

  8. jmcgeheeiv commented on May 23, 2021

    @jmcgeheeiv
    Contributor

    Great idea, @mrbean-bremen. Done.

  9. locked and limited conversation to collaborators on May 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions