-
Notifications
You must be signed in to change notification settings - Fork 383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make config read only #472
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great that we have progress on this! Also looking forward on using the ReadOnlyContainers in incense.
I now added some more unittests for the container types with and without the experiment. I didn't know that |
Also, currently |
These classes where designed for inheritance, so all the methods that access some internal state go through the dunder methods ( |
That makes sense. But it is apparently not correct that all methods that access the internal state go through the dunder methods, at least not for When used in interactive mode (ipyhton, jupyter notebook), it may make sense to overwrite all methods that can potentially change the contents to provide the user with a corresponding docstring. Another possibility would be to completely drop the inheritance from any dict or list type and write a wrapper around it that delegates the method calls to the wrapped object. |
The approach to drop the inheritance to all list/dict types would look something like this: class ReadOnlyList:
def __init__(self, *args, **kwargs):
self.data = list(*args, **kwargs)
def __repr__(self):
return repr(self.data)
def __lt__(self, other):
return self.data < self.__cast(other)
def __le__(self, other):
return self.data <= self.__cast(other)
def __eq__(self, other):
return self.data == self.__cast(other)
def __gt__(self, other):
return self.data > self.__cast(other)
def __ge__(self, other):
return self.data >= self.__cast(other)
def __cast(self, other):
return other.data if isinstance(other, self.__class__) else other
def __contains__(self, item):
return item in self.data
def __len__(self):
return len(self.data)
def __getitem__(self, i):
return self.data[i]
def __add__(self, other):
if isinstance(other, self.__class__):
return self.__class__(self.data + other.data)
elif isinstance(other, type(self.data)):
return self.__class__(self.data + other)
return self.__class__(self.data + list(other))
def __radd__(self, other):
if isinstance(other, self.__class__):
return self.__class__(other.data + self.data)
elif isinstance(other, type(self.data)):
return self.__class__(other + self.data)
return self.__class__(list(other) + self.data)
def __iadd__(self, other):
if isinstance(other, self.__class__):
self.data += other.data
elif isinstance(other, type(self.data)):
self.data += other
else:
self.data += list(other)
return self
def __mul__(self, n):
return self.__class__(self.data * n)
__rmul__ = __mul__
def __imul__(self, n):
self.data *= n
return self
def copy(self):
return self.__class__(self)
def count(self, item):
return self.data.count(item)
def index(self, item, *args):
return self.data.index(item, *args) which is basically a copy of |
Maybe two alternatives should be considered:
Advantages of the second:
When the ReadOnlyList does not inherit from list, why not using tuple? Tuple is an immutable (ReadOnly) list. |
Then,
Correct. Does not make sense to make
There are two possibilities: copy when (a) the configuration is initialized or (b) when the function is called.
If we assume that the only container types in the configuration are We could also save a copy of the configuration and check after each function call if any value was changed. This probably introduces less overhead than doing a I'm now completely unsure about what is the best solution for this. @JarnoRFB what is your opinion on this? |
This seems more complex than I initially thought. To throw something else in the arena we might consider using |
Definitely. I listed all options that currently are under debate below. What we might want to achieve:
No custom types
deepcopy on every call
deepcopy on initialization
deepcopy on initialization and equals checks after each function call
Special Types
pyrsistent
subclass of UserDict/UserList
subclass of list/dict
tuple/types.MappingProxyType
I think we shouldn't break |
What about something along these lines? (look into |
Thanks for the detailed and structured overview! After thinking a bit about it I would say that it is more or less impossible to guarantee immutability to data that is intrinsically mutable. The only solution that disables changes that persist between function calls is really deepcopy on every call. But as you said that might lead to large overheads. I believe deepcopy on initialization and equals checks after each function call that you implemented in your last PR will only work for custom types that implement
|
You are right. But also computing a hash of the configuration only works for types that implement I'll then implement your suggestions and squash the commits of this pull request in the following week. |
6eb4ee1
to
f159b36
Compare
Thanks for the change! Time to get this merged. I think the CI error could just be fixed by explicitly setting the seed in config.
Then just fix the flake8 errors and I think this is good to go. |
7203945
to
585153f
Compare
585153f
to
a93d46a
Compare
Python 2.7 is driving me crazy. And Codacity is complaining about unused variables in config scopes, intentional uses of assert in testcases and intentionally using |
@thequilo thanks! Looks good now. I hope to get rid of py 27 asap, but I can't make a release myself. If it takes to long, we could also just switch without a release... Codacy is bullshit anyway, most of the time just false alarms. Merging then! |
* 'master' of https://github.com/IDSIA/sacred: Release 0.7.5 update dependencies to safer version of SQLAlchemy fixed numpy deprecation warning fix yaml deprecation warning Make config read only (IDSIA#472) Remove broken codacy badge (IDSIA#486) Add queue based observer (IDSIA#471) Run CI against python 36 and 37 (IDSIA#485) Make failed mongo observer dump configurable (IDSIA#462) Make stale time longer (IDSIA#484) Fix a bug where a unicode char in README.rst would fail install (IDSIA#481)
Now overwriting entries in the config does not work, but when someone did this in the right way, this also does not work: import copy
def foo(_config):
_config = copy.deepcopy(_config)
_config['something'] = ... How about changing the datatype back to the original datatype when using copy? import copy
class ReadOnlyDict(dict):
...
def __copy__(self):
return {**self}
def __deepcopy__(self, memo):
d = dict(self)
return copy.deepcopy(d, memo=memo) >>> type(copy.copy(ReadOnlyDict()))
dict
>>> type(copy.deepcopy(ReadOnlyDict()))
dict |
@boeddeker sounds very reasonable to me. PR welcome! |
Hmm, this actually was in the list of features that the read-only contains should have ("provide a way to get a mutable copy of the config"), but it somehow didn't get implemented |
Another thing, is it intended to lose the original datatype in the config? >>> class MyDict(dict):
... pass
>>> isinstance(MyDict(), dict)
True Solutions:
I prefer the first. |
…d into run_kwargs_fix * 'run_kwargs_fix' of github.com-rueberger:rueberger/sacred: Release 0.7.5 update dependencies to safer version of SQLAlchemy fixed numpy deprecation warning fix yaml deprecation warning Make config read only (IDSIA#472) Remove broken codacy badge (IDSIA#486) Add queue based observer (IDSIA#471) Run CI against python 36 and 37 (IDSIA#485) Make failed mongo observer dump configurable (IDSIA#462) Make stale time longer (IDSIA#484) Fix a bug where a unicode char in README.rst would fail install (IDSIA#481)
Add new ReadOnlyContainer types and use them for the config. This solves #370.