-
Notifications
You must be signed in to change notification settings - Fork 64
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
Relax Representation Invariant Checking Logic for Recursive Classes #1162
Conversation
for more information, see https://pre-commit.ci
Converted to draft because the failing test |
Pull Request Test Coverage Report for Build 13983965613Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
Not a current collaborator so can't directly request a reviewer. @david-yz-liu PR size: small |
Thank you @ckatsube! It's great to see you active on here. :) I will review soon. BTW, if you want to join the new SDS slack, you're welcome to join by following the instructions under "Slack" at https://www.cs.toronto.edu/~david/sds/welcome-to-sds.html. |
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.
Thank you, @ckatsube! I left some comments. Also it would be good to add a test case to illustrate the interaction with inheritance.
python_ta/contracts/__init__.py
Outdated
@@ -221,6 +225,12 @@ def new_setattr(self: klass, name: str, value: Any) -> None: | |||
else: | |||
super(klass, self).__delattr__(name) | |||
raise AssertionError(str(e)) from None | |||
else: | |||
caller_klass = type(caller_self) | |||
if "__mutated_instances__" in caller_klass.__dict__: |
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.
Use hasattr
and getattr
instead of accessing __dict__
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.
Sounds good!
python_ta/contracts/__init__.py
Outdated
else: | ||
caller_klass = type(caller_self) | ||
if "__mutated_instances__" in caller_klass.__dict__: | ||
mutable_instances = caller_klass.__dict__["__mutated_instances__"] |
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.
The variable should probably be called mutated_instances
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.
Thanks for the consistency check 👍
python_ta/contracts/__init__.py
Outdated
@@ -221,6 +225,12 @@ def new_setattr(self: klass, name: str, value: Any) -> None: | |||
else: | |||
super(klass, self).__delattr__(name) | |||
raise AssertionError(str(e)) from None | |||
else: |
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.
I think this else
is a bit too broad: if caller_self is self
it doesn't need to be added to __mutated_instances__
, as the RIs should be checked on self
at the end of the calling method.
Also, it's worth adding a comment to explain what's going on (parallel to the comment in the if branch).
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.
Sounds good! Thanks for the keen eye
python_ta/contracts/__init__.py
Outdated
# Only validating if the attribute is not being set in a instance/class method | ||
# AND self is an instance of caller_self's type -- aka caller_self is equal to |
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.
The check for isinstance(caller_self, type(self))
is good (it's fine for an instance of a child class to violate an RI on another instance of the a parent class).
In the comment, the wording should be reversed (caller_self is an instance of self's type); the part including and after the --
is not necessary and can be removed.
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.
Thanks for following up on the question!
python_ta/contracts/__init__.py
Outdated
except PyTAContractError as e: | ||
raise AssertionError(str(e)) from None | ||
else: | ||
return r | ||
finally: | ||
setattr(instance_klass, "__mutated_instances__", mutated_instances_to_restore) |
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.
The restoring is generally good; however, we should have different cases for whether __mutated_instances__
was an attribute or not originally. If it wasn't, we should use delattr
to remove the instance attribute altogether, so it remains a temporary attribute.
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.
Sounds like a plan--added a branch for deleting the temporary attribute
def test_violate_ri_in_other_instance(person, person_2) -> None: | ||
""" | ||
Call a method that changes age of another instance of the same class to something invalid | ||
Excepts the RI to be violated hence an AssertionError to be thrown. |
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.
"Excepts" -> "Expects"
"thrown" -> "raised"
Also, please add a period to the end of the sentence on the previous line.
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.
Done!
Resolved the review comments and added the requested tests. Additionally modified the logic around invariant checking the mutated instances to use the klass/klass_mod of the mutated instance to prevent child class RIs from being enforced on mutated parent class instances (this was caught by the new tests). |
Since the checks passed, this is ready for re-review! |
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.
Thanks, @ckatsube!
Motivation & Proposal
The Representation Invariant checker timing is currently too strict. In many data structures, it's common for instances to mutate other instances of the same type (i.e. graphs/trees or recursive data structures). The current logic enforces RI checks in two places: 1) when an attribute is set EXCEPT for instance methods 2) after the instance method is called. The problem is that Path 1 is executed inside instance methods that set the attributes of different instances of the same type (motivating example below).
The proposed change is to additionally exempt instances of the same type as
self
during the Path 1 check and push checking these RIs until after the instance method has returned, alongside the Path 2 check.More practical motivating example with RecursiveList
Note: this specific example can technically be avoided by having a better constructor for RecursiveList but the bug still stands regardless.
Changes
Modified the Path 1 check to also exempt RI checking if the type of the parent frame's
self
matches the type ofself
Accumulated all of the instances that were exempt in the Path 1 check to later check at Path 2
Modified the Path 2 logic to also check RIs of the accumulated instance attributes
Added unit tests to
test_class_contracts
for both the attribute mutation and function arg mutation cases.Type of Change
(Write an
X
or a brief description next to the type or types that best describe your changes.)Checklist
(Complete each of the following items for your pull request. Indicate that you have completed an item by changing the
[ ]
into a[x]
in the raw text, or by clicking on the checkbox in the rendered description on GitHub.)Before opening your pull request:
I have updated the project documentation, if applicable.If this is my first contribution, I have added myself to the list of contributors.After opening your pull request:
Questions and Comments
I made a minor logical nuance where subclasses should be able to temporarily violate parent class instances' Representation Invariants in its instance methods (
Child.modify(parent)
). If you think that Child instances should not be allowed to temporarily violate Parent RIs and this check should be strictly equal class types (only Child==Child and Parent == Parent but no Child modifies Parent), let me know! It would effectively just be changingnot isinstance(caller_self, type(self))
tonot type(caller_self) == type(self)