Reputation: 12339
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
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
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