A comment on the GitHub issue also points out how ACL masks are restrictive (&=
, not =
).
These two comments have made me re-think, and I agree to the closing of the issue: The required functionality would need two separate contexts: set_umask
and increase_umask
.
My initial usecases were:
- setting the umask to a ‘higher value’, say
0o077
for private-file creation
- setting the umask to
0o000
for public socket creation. I don’t think I mentioned this one but it was in my head all along.
But what if one wanted to further restrict only the ‘world’ permissions to 0, ie increase the umask to 0oXY7
, whilst leaving the group umask field Y
untouched? One could naively guess that 0o022
is the default pre-existing umask, and so Y=2
leading to 0o027
could be sensible, but what if the pre-existing umask is 0o077
? Setting the umask to 0o027
actually weakens the umask! Mutatis mutandis setting X=0
is purely an assumption that it too was 0 before.
So two separate context managers would be required.
class set_umask(ModificationContext):
"""Non thread-safe context manager to set the process's umask."""
def _apply(self):
return os.umask(self._applied_value)
def _revert(self, previous_value):
os.umask(previous_value)
class increase_umask(ModificationContext):
"""Non thread-safe context manager to increase the process's umask."""
def _apply(self):
old_mask = os.umask(0)
new_mask = self._applied_value | old_mask
os.umask(new_mask)
return old_mask
def _revert(self, previous_value):
os.umask(previous_value)
set_umask
cannot perform the behavior of monotonically-increasingly restricting the umask, &=
, as the comments point out.
increase_umask
cannot perform the behavior of loosening the umask, =
, as would be needed eg for making public sockets.
Both are required for specific usecases, and noobs may well use the wrong context manager; we could no longer say in the documentation that ‘this is a simple wrapper around umask’.
Further technical details, as man 2 umask
acknowledges:
It is impossible to use umask() to fetch a process’s umask without at the same time changing it. A second call to umask() would then be needed to restore the umask. The nonatomicity of these two steps provides the potential for races in multithreaded programs.
If a KeyboardInterrupt
occured between the umask
calls in increase_umask._apply
that seems like a mess too! That would need to be acknowledged.
These two belong in a separate PyPI package, not in the stdlib, which I’ll make myself
As for the addition of ModificationContext
to contextlib, I’d be against it:
- Since no
shutil.umask
is being added, and ModificationContext
is only used by contextlib internally, is it worth exposing another ABC to save a few lines of code?
- Noobs may say ‘I thought all context managers modified something to an extent?’
- It may spread the same issue as
umask
; I have the following in one of my codebases:
@contextmanager
def flocked(f, operation = fcntl.LOCK_EX):
"""
Context manager for locking a file via `flock`
"""
fcntl.flock(f, operation)
try:
yield
finally:
fcntl.flock(f, fcntl.LOCK_UN)
If it were to be refactored into a ModificationContext
, should flocked
try to set the lock to operation
, or increase to operation
; If one called flocked(f, fcntl.LOCK_SH)
should that replace an exclusive lock with a shared one, or do nothing if an exclusive lock is already present, and we want to keep flocked
monotonically increasing in the strictness of its effect. (n.b. I only do file locking in one place in reality, anything else is asking for trouble, this is just for arguments sake and to illustrate the pattern emerging)
ModificationContext
can’t implicitly convey whether the context performs an &=
restriction, or =
substitution. The same would occur for a replace_attr
CM to an extent too: if one wanted to replace the timeout
(ms) attribute of a class with timeout + 5000
. In this case it’s ‘restriction-like’ but +=
not &=
.
ModificationContext
|
`- IncrementalContext # relatively set value / attribute;
| # `+=`, `-=`, `&=`, `|=`,
| # aka RestrictionContext
|
`- SubstitutionContext # absolutely set value / attribute;
# `=`
# aka replace_attribute
Not worth the clutter, and it’s anti-beginner friendly, making them learn all this. “Just give me my @contextmanager
!”