Skip to content

Commit 241d225

Browse files
c00wpytorchmergebot
authored andcommitted
torch/config: fix mock behaviour (pytorch#140779)
Mock only replaces the value that was removed, if after deletion, it does not see the attribute. Pull Request resolved: pytorch#140779 Approved by: https://github.com/ezyang
1 parent 878a849 commit 241d225

File tree

2 files changed

+26
-0
lines changed

2 files changed

+26
-0
lines changed

test/test_utils_config_module.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Owner(s): ["module: unknown"]
22
import os
33
import pickle
4+
from unittest.mock import patch
45

56

67
os.environ["ENV_TRUE"] = "1"
@@ -320,6 +321,14 @@ def test_make_closur_patcher(self):
320321
revert()
321322
self.assertTrue(config.e_bool)
322323

324+
def test_unittest_patch(self):
325+
with patch("torch.testing._internal.fake_config_module.e_bool", False):
326+
with patch("torch.testing._internal.fake_config_module.e_bool", False):
327+
self.assertFalse(config.e_bool)
328+
# unittest.mock has some very weird semantics around deletion of attributes when undoing patches
329+
self.assertFalse(config.e_bool)
330+
self.assertTrue(config.e_bool)
331+
323332

324333
if __name__ == "__main__":
325334
run_tests()

torch/utils/_config_module.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,18 @@ class _ConfigEntry:
213213
# environment variables are read at install time
214214
env_value_force: Any = _UNSET_SENTINEL
215215
env_value_default: Any = _UNSET_SENTINEL
216+
# Used to work arounds bad assumptions in unittest.mock.patch
217+
# The code to blame is
218+
# https://github.com/python/cpython/blob/94a7a4e22fb8f567090514785c69e65298acca42/Lib/unittest/mock.py#L1637
219+
# Essentially, mock.patch requires, that if __dict__ isn't accessible
220+
# (which it isn't), that after delattr is called on the object, the
221+
# object must throw when hasattr is called. Otherwise, it doesn't call
222+
# setattr again.
223+
# Technically we'll have an intermediate state of hiding the config while
224+
# mock.patch is unpatching itself, but it calls setattr after the delete
225+
# call so the final state is correct. It's just very unintuitive.
226+
# upstream bug - python/cpython#126886
227+
hide: bool = False
216228

217229
def __init__(self, config: Config):
218230
self.default = config.default
@@ -253,11 +265,15 @@ def __setattr__(self, name: str, value: object) -> None:
253265
else:
254266
self._config[name].user_override = value
255267
self._is_dirty = True
268+
self._config[name].hide = False
256269

257270
def __getattr__(self, name: str) -> Any:
258271
try:
259272
config = self._config[name]
260273

274+
if config.hide:
275+
raise AttributeError(f"{self.__name__}.{name} does not exist")
276+
261277
if config.env_value_force is not _UNSET_SENTINEL:
262278
return config.env_value_force
263279

@@ -288,6 +304,7 @@ def __delattr__(self, name: str) -> None:
288304
# must support delete because unittest.mock.patch deletes
289305
# then recreate things
290306
self._config[name].user_override = _UNSET_SENTINEL
307+
self._config[name].hide = True
291308

292309
def _is_default(self, name: str) -> bool:
293310
return self._config[name].user_override is _UNSET_SENTINEL

0 commit comments

Comments
 (0)