I’m happy enough to go ahead with the *_context
suffix convention. While I prefer names to capture intention rather than describe aspects of the implementation, I also agree that ensure_*
has some ambiguity between asserting something is already true and actually temporarily making it true.
If we come up with a naming convention we like better before May (beta API freeze) we can still change it.
That said, I’d love to come up with an actual name for these kinds of apply-and-revert context managers, especially if we can make the reusable-and-reentrant pattern for implementing them easier to use.
Framing the naming problem that way inspired the following notion of “modification contexts” (which in turn leads to proposing modify_{NOUN}
as the general naming convention when there’s no more specific verb that applies, like replace_{NOUN}
or redirect_{NOUN}
):
class ModificationContext(ContextDecorator, AbstractContextManager):
"""Context manager to change a value on entry and revert it on exit."""
# Since these are inherently reusable-and-reentrant context managers,
# they're compatible with being used as function decorators
# in addition to being usable as regular context managers.
def __init__(self, value):
self._applied_value = value
self._values = []
@property
def applied_value(self):
return self._applied_value
def __repr__(self):
return f"{type(self).__qualname__}({self._applied_value!r})"
def apply(self):
self._values.append(self._apply())
return self._applied_value
def revert(self):
self._revert(self._values.pop())
@abstractmethod
def _apply(self):
# Actual implementation of applying the change and
# reporting the value to be restored
raise NotImplementedError
@abstractmethod
def _revert(self, previous_value):
# Separate reversion method is given the value to be restored
raise NotImplementedError
def __enter__(self):
return self.apply()
def __exit__(self, *exc_info):
self.revert()
With the following subclass in contextlib
(no changes needed to the public redirect_*
subclasses):
class _RedirectStream(ModificationContext):
_stream = None
def _apply(self):
previous_stream = getattr(sys, self._stream))
setattr(sys, self._stream, self._applied_value)
return previous_stream
def _revert(self, previous_stream):
setattr(sys, self._stream, previous_stream)
And these in shutil
:
class modify_cwd(ModificationContext):
@property
def path(self):
return self._applied_value
def _apply(self):
previous_cwd = os.getcwd()
os.chdir(self._applied_value)
return previous_cwd
_revert = staticmethod(os.chdir)
class modify_umask(ModificationContext):
# Convention: when the same word is used for both the noun and verb forms,
# use it as the value attribute and use `_apply_{NOUN}` as the method name
@property
def umask(self):
return self._applied_value
def _apply(self):
return os.umask(self._applied_value)
_revert = staticmethod(os.umask)
We could potentially even add the following generalisation of the
stream redirection modification to contextlib
:
class replace_attribute(ModificationContext):
# Similar in concept to `unittest.mock.patch`,
# just without all the testing related features
def __init__(self, target, attr, value):
self._target = target
self._attr = attr
super().__init__(value)
def _apply(self):
previous_value = getattr(self._target, self._attr)
setattr(self._target, self._attr, self._applied_value)
return previous_value
def _revert(self, previous_value):
setattr(self._target, self._attr, previous_value)
Edit: to be clear, the only part of this idea that directly affects the umask
CM proposal is calling it modify_umask
instead of umask_context
. (and maybe making the public access to umask
be via a read-only property)