Repository navigation
Conversation
|
In GitLab by @woutdenolf on Mar 10, 2023, 16:34 GMT+1: added 1 commit
Compare with previous version (link unavailable after force-push) |
|
In GitLab by @woutdenolf on Mar 10, 2023, 16:35 GMT+1: marked this merge request as draft |
8dcd50a to
b58fb90
Compare
c693228 to
e15bcf0
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
4c2f270 to
6bfdb46
Compare
6bfdb46 to
bf69894
Compare
There was a problem hiding this comment.
I can understand that the hashing mechanism can be quite complex depending on the value type and I don't know if I am missing information on how hashing works, but I really hit a wall when reviewing this.
I talked about the tests in an inline comment but also, I really feel like the comments do not help: they are verbose but still ambiguous.
The implementation of _uhash looks sensible but I'd suggest to review the inline comments. Perhaps adding some semantic value by renaming some variables and/or extracting some util functions may be useful as well (e.g numpy arrays, datetime).
|
|
||
| obj = Myclass(1) | ||
| assert hashing.uhash(obj) == obj.__uhash__() | ||
| assert hashing.uhash([obj]) != hashing.uhash([obj.__uhash__()]) |
There was a problem hiding this comment.
What is the point of this check?
There was a problem hiding this comment.
I'll expand a bit more on this response than on the others as an illustration of the more general respone.
- Test that when the top-level value's
__uhash__hands back aUniversalHash, it is returned verbatim:
assert uhash(obj) == obj.__uhash__()- Test in addition that the verbatim returning does not apply to the nesting (list in this case):
assert hashing.uhash([obj]) != hashing.uhash([obj.__uhash__()])If the verbatim logic is implemented wrong and it would be mangled in with the nesting then this would fail. In the current code you could make it fail like this
diff --git a/src/ewokscore/hashing.py b/src/ewokscore/hashing.py
index bcd62d2..2cb91da 100644
--- a/src/ewokscore/hashing.py
+++ b/src/ewokscore/hashing.py
@@ -157,8 +157,7 @@ def _uhash(value) -> UniversalHash:
# Any other return value is the content of the value
at_root = False
- _hash.update(_classhashdata(type(value)))
- _expand(stack, open_depths, value, (nested,))
+ stack.append(nested)
continueThis shows exactly my point of "covering the holes". A human would test only 1). The AI understands that the verbatim logic itself should not enter other branches of the code, not now and not in the future when the code gets changed.
This also illustrates my dilemma: on the one hard I don't want to delete this hardened tests, on the other hand good luck reading it all during a review (also when creating the PR, I probably spend much more time on the tests than the actual code) or when you add new features and you need to know where you new test needs to fit in and how the tests of the other branches are affected.
| assert a == a.uhash | ||
| assert a != c.uhash |
There was a problem hiding this comment.
Should it not check a == b.uhash here ?
There was a problem hiding this comment.
Because it's a duplicate of a == a.uhash, not an independent property.
class HasUhash(ABC):
def __eq__(self, other):
return uhash(self) == uhash(other)|
|
||
|
|
||
| def test_hashing_uncomparable_keys(): | ||
| """Mapping keys and set items need no total order between them.""" |
There was a problem hiding this comment.
What does "need no total order" mean?
There was a problem hiding this comment.
These keys have no total order:
sorted([(1, 2), (1, "b")])
TypeError: '<' not supported between instances of 'str' and 'int'
I replace "total order" by "comparable". It is for sorting purposes, which "total order" would include, but "comparable" is more intuitive.
| """Mapping keys and set items need no total order between them.""" | ||
| adict = {(1, 2): "a", (1, "b"): "c"} | ||
|
|
||
| assert hashing.uhash(adict) == hashing.uhash(dict(reversed(adict.items()))) |
There was a problem hiding this comment.
Well, last comment for this file. I think that the tests are too complex.
For example, I had to run this in the Python console to see the output to understand what the test was trying to achieve.
I'd suggest to review the tests and simplify them by:
- more obvious generation of the tested data that leaves room for second guessing.
- document the aim of the tests more clearly through the test name
- breaking down longer tests. That should help with the point above.
| # Hashing `[1, [2]]` pops values from the stack until it is empty. Expanding a | ||
| # value replaces it by the marker `)` that closes it, followed by its items in | ||
| # reverse, so that the items are popped in order and the marker last: | ||
| # | ||
| # stack (top last) popped value hashed | ||
| # ---------------- ------------ -------------------- | ||
| # [1, [2]] [1, [2]] "builtins.list" | ||
| # ), [2], 1 1 "builtins.int", "0x1" | ||
| # ), [2] [2] "builtins.list" | ||
| # ), ), 2 2 "builtins.int", "0x2" | ||
| # ), ) ) ")" | ||
| # ) ) ")" |
There was a problem hiding this comment.
I thought I understood _expand until I read that.
What is popped values? What is (top last) ?
Is the table showing different examples or subsequent steps?
There was a problem hiding this comment.
I first wanted to add this example to the PR but then decided to put it in the code (since it is rather complex). At least the LIFO concept with container expansion is covered by that example. Not the more intricate corner cases like handling circular references (e.g. for this _EndOfNesting holds references for the id's in open_depths are valid).
This is not a single _expand call, this shows how [1, [2]] is hashed.
I should either clarify that or move it somewhere else. The example is mainly there to illustrate the LIFO with _expand logic.
popped values: popped from the stack in each iteration of the _uhash() function. I see now that putting it with _expand() really set you on the wrong track.
(top last) should be removed, it is wrong. Left-over from a previous approach that was abandoned.
| # ), ) ) ")" | ||
| # ) ) ")" | ||
| def _expand( | ||
| stack: List[Any], open_depths: Dict[int, int], value: Any, nested: Iterable |
There was a problem hiding this comment.
| stack: List[Any], open_depths: Dict[int, int], value: Any, nested: Iterable | |
| stack: List[Any], open_depths: Dict[int, int], value: Any, value_items: Iterable |
There was a problem hiding this comment.
I went for parent and parent_items. I also reviewed all usage of "nested" and replaced it.
| elif isinstance(value, UniversalHash): | ||
| _hash.update(repr(value).encode()) | ||
| elif isinstance(value, enum.Enum): | ||
| # The name identifies the member, except for a combination of flags |
There was a problem hiding this comment.
What does "except for a combination of flags" mean? What to do in this case?
There was a problem hiding this comment.
This is the reason we need name and value
import enum
class Perm(enum.Flag):
R = 1
W = 2
X = 4
combos = [Perm.R | Perm.W, Perm.R | Perm.X, Perm.W | Perm.X]
print([c.name for c in combos])
# python 3.12: ['R|W', 'R|X', 'W|X']
# python <3.12: [None, None, None]Otherwise we could take only the name (as one would expect for an enum).
This is also something I learned (name only would be enough, hence only the flags are mentioned)
class Color(enum.Enum):
RED = 1
ALIAS = 1 # not a new member
assert Color.ALIAS is Color.RED
assert Color.ALIAS.name == "RED"|
Yes the tests are a problem. Doing this by hand makes it more readable but leaves lots of untested holes. Letting AI cover those holes leads to unreadable tests. A pattern that appears not just in this PR. Up to now I was thinking to better fill the holes but now I'm on the fence. Or I should find a better way in guiding the AI in the "filling the holes". The problem is that for complex code there are so many possible permutations of branches that if you fill all holes it might always be a struggle to read through the tests. |
…t of an entire hashing
714f95d to
1182ffa
Compare
Co-authored-by: Loïc Huder <42204205+loichuder@users.noreply.github.com>
In GitLab by @woutdenolf on Mar 10, 2023, 15:31 GMT+1:
The universal hashing is use for output caching. Expand hashing support and allow registering hash functions for third-party types.
__uhash__becomes universally hashableuhashcalls and replace with LIFO stack (depth-first search)uhashclasshashdataandmultitype_sortedprivate"ewoks.hashing"for types that do not have a__uhash__methods and are not natively supportedAI used (Claude) for splitting commits, fixing recursion limit (LIFO solution) and circular references (open_depth) and adding support for more builtin types.
NOTE: this would need bumping the major version again. Perhaps wait together with #490 and #491 for 6.0 release.
Assignees: @woutdenolf
Migrated from GitLab: https://gitlab.esrf.fr/workflow/ewoks/ewokscore/-/merge_requests/180