rajah9
rajah9

Reputation: 12339

Cannot patch a magic attribute

I'd like to be able to patch a magic attribute.

The main file, which I'd like to test, is FileUtil.py, containing a class definition of FileUtil:

class FileUtil:
    @logit(showArgs=True, showRetVal=True)
    def exec_file_path(self) -> str:
        """
        Get the full dir and file path we're executing from using __file__, or if not available (probably because you're using a Jupyter notebook)
        use os.path.abspath.
        """
        try:
            logger.debug('exec_file_path is about to attempt accessing __file__.')
            return __file__
        except NameError as err:
            logger.debug('Exception message: {msg}'.format(msg=err))
            return abspath('')

I also have a local test in mockTest.py, containing a class AltFileUtil. To distinguish which one is being called, fUtil instantiates AltFileUtil and gUtil instantiates FileUtil.

The method under test uses __file__, but barring that, uses absdir.

from os.path import abspath
from unittest.mock import MagicMock, PropertyMock, patch

class AltFileUtil:
    def exec_file_path(self) -> str:
        """
        Get the full dir and file path we're executing from using __file__, or if not available (probably because you're using a Jupyter notebook)
        use os.path.abspath.
        """
        try:
            return __file__
        except NameError as err:
            return abspath('')

fUtil = AltFileUtil()
print (f'Part 1. function returns: {fUtil.exec_file_path()}')

patches = ['__main__.AltFileUtil', 'mockTest.AltFileUtil', ]
for p in patches:
    print (f'Using patch of {p}.')
    with patch(p) as mockFUtil:
        type(mockFUtil.return_value).__file__ = PropertyMock(return_value='mockfilename')
        x = mockFUtil.exec_file_path()
        try:
            print (f'Success! Got {x.__file__}.')
        except (ValueError, AttributeError, TypeError) as e:
            print(f'Got a {type(e)} error: {e}')

from FileUtil import FileUtil
gUtil = FileUtil()
print (f'Part 2. Using function from FileUtil.py, which returns: {gUtil.exec_file_path()}')

patches = ['FileUtil.FileUtil',  ]
for p in patches:
    print (f'Using patch of {p}.')
    with patch(p) as mockFUtil:
        type(mockFUtil.return_value).__file__ = PropertyMock(return_value='mockfilename')
        x = mockFUtil.exec_file_path()
        try:
            print (f'Success! Got {x.__file__}.')
        except (ValueError, AttributeError, TypeError) as e:
            print(f'Got a {type(e)} error: {e}')

The output is

C:\Users\Owner\PycharmProjects\Utilities\venv\Scripts\python.exe C:/Users/Owner/.PyCharm2019.2/config/scratches/mockTest.py
Part 1. function returns: C:/Users/Owner/.PyCharm2019.2/config/scratches/mockTest.py
Using patch of __main__.AltFileUtil.
Got a <class 'AttributeError'> error: __file__
Using patch of mockTest.AltFileUtil.
Part 1. function returns: C:\Users\Owner\.PyCharm2019.2\config\scratches\mockTest.py
Using patch of __main__.AltFileUtil.
Got a <class 'AttributeError'> error: __file__
Using patch of mockTest.AltFileUtil.
Got a <class 'AttributeError'> error: __file__
Part 2. Using function from FileUtil.py, which returns: C:\Users\Owner\PycharmProjects\Utilities\FileUtil.py
Using patch of FileUtil.FileUtil.
Got a <class 'AttributeError'> error: __file__
Got a <class 'AttributeError'> error: __file__
Part 2. Using function from FileUtil.py, which returns: C:\Users\Owner\PycharmProjects\Utilities\FileUtil.py
Using patch of FileUtil.FileUtil.
2019-10-08 07:42:13,797 DEBUG Entering exec_file_path.
Got a <class 'AttributeError'> error: __file__
2019-10-08 07:42:13,797 DEBUG >> 0. <FileUtil.FileUtil object at 0x03721230>
2019-10-08 07:42:13,797 DEBUG exec_file_path is about to attempt accessing __file__.
2019-10-08 07:42:13,797 DEBUG >> Return value is C:\Users\Owner\PycharmProjects\Utilities\FileUtil.py.
2019-10-08 07:42:13,797 DEBUG Exiting exec_file_path.
2019-10-08 07:42:13,797 DEBUG Entering exec_file_path.
2019-10-08 07:42:13,797 DEBUG >> 0. <FileUtil.FileUtil object at 0x02E64050>
2019-10-08 07:42:13,797 DEBUG exec_file_path is about to attempt accessing __file__.
2019-10-08 07:42:13,797 DEBUG >> Return value is C:\Users\Owner\PycharmProjects\Utilities\FileUtil.py.
2019-10-08 07:42:13,797 DEBUG Exiting exec_file_path.

Process finished with exit code 0

I think it should have given me mockfilename instead of an attribute error.

Upvotes: 0

Views: 730

Answers (2)

rajah9
rajah9

Reputation: 12339

I've broken out the __file__ statement into a separate, patchable member.

class FileUtil:
    def executing_file(self):
        return globals()['__file__']

    def exec_file_path(self) -> str:
        try:
            ex_file = self.executing_file()
            return ex_file
        except NameError as err:
            logger.debug('Exception message: {msg}'.format(msg=err))
            return abspath('')

Here's how I can patch the test and change the return value.

@mock.patch.object(FileUtil, 'executing_file')
def test_exec_file_path(self, mock_obj):
    fUtil = FileUtil()
    mock_obj.return_value = r'C:\mockpath\mockfile.py'
    self.assertEqual(r'C:\mockpath\mockfile.py', fUtil.exec_file_path())

Here's how to patch it so that it throws a NameError (and will use the patched abspath).

@mock.patch('FileUtil.abspath')
@mock.patch.object(FileUtil, 'executing_file')
def test_exec_file_path_err(self, mock_obj, mock_abs):
    fUtil = FileUtil()
    mock_abs.return_value = r'c:\abspath\mockfile.py'
    mock_obj.side_effect = NameError(mock_obj, 'mock name error')
    self.assertEqual(r'c:\abspath\mockfile.py', fUtil.exec_file_path()) 

Upvotes: 0

wim
wim

Reputation: 363616

With patch() it's important to patch objects in the namespace where they are looked up. See where to patch in the docs.

In your case, it's because the line

return __file__

does not resolve the name __file__ by looking for an attribute on the instance. It's resolved in the module namespace.

Therefore, you would need to patch it in the correct place (i.e. on the module in which FileUtil was defined).

Upvotes: 1

Related Questions