Skip to content
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

memories: test/fix interactions between SopelIdentifierMemorydict #2525

Merged
merged 13 commits into from
Nov 7, 2023

Conversation

dgw
Copy link
Member

@dgw dgw commented Oct 20, 2023

Description

Tin. Will resolve #2524.

Checklist

  • I have read CONTRIBUTING.md
  • I can and do license this contribution under the EFLv2
  • No issues are reported by make qa (runs make lint and make test)
  • I have tested the functionality of the things this change touches
    • Copiously.

Notes

Started as a partial, untested fix from my tablet to reserve the PR number. New day, new task list:

  • del d['KeY']
  • d.clear() (appears to work, but the object becomes a regular dict and loses the Identifier magic)
  • d.copy()
    Made a copy m and added another item to it, verifying that the new item did not appear in the original d.
    However, the new object m is a dict, not a SopelIdentifierMemory.
  • d.get('KeY')
    Returns the default passed to .get(), not key's value.
  • d.pop('KeY') (currently raises KeyError)
  • d.pop('KeY', 'default') (currently always returns 'default')
  • d.pop('NotInDict')
    Returns None but should raise KeyError.
  • d.setdefault('KeY', 'default')
    With 'key' in d, returns 'default' & adds 'KeY' to d as a regular string, not Identifier.
  • d.update(a_plain_dict)
    If d has Identifier('thing1') as a key, and a_plain_dict has 'ThinG1' (a regular string), they will not be merged correctly and d will now also have a regular string key called 'ThinG1'.
  • d | a_plain_dict
    Same problem as d.update(a_plain_dict).
  • d |= a_plain_dict
    Same problem as d.update(a_plain_dict) and d | a_plain_dict.
  • [ ] d == a_plain_dict
    If a_plain_dict has the same key/value pairs but the key capitalization doesn't match Identifier's canonical (lowercase) internal representation, the equality check returns False.
    Decided to NAK this one and let them be unequal. It's an easily reversible decision, since this just preserves the existing behavior (so there's no worry about plugin authors screaming that we keep changing the rules 😁).
  • result = SopelIdentifierMemory({'KeY': 'foo'})
    result is of type SopelIdentifierMemory, but without the magic. Existing keys are not converted to Identifier, and 'key' in result doesn't work as expected for that type.
    In fact, even 'KeY' in result returns False for the example above, and result['KeY'] raises KeyError`. Even basic functionality of the resulting fake identifier memory is completely broken.

@dgw dgw added Medium Priority Bugfix Generally, PRs that reference (and fix) one or more issue(s) labels Oct 20, 2023
@dgw dgw added this to the 8.0.0 milestone Oct 20, 2023
Copy link
Contributor

@SnoopJ SnoopJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix for pop() looks reasonable to me. Agree that we want some tests to enforce the expected behavior, subclassing dict is tricky!

@dgw dgw force-pushed the SopelIdentifierMemory-casts branch 2 times, most recently from 079df7a to dd14707 Compare October 21, 2023 03:47
@dgw
Copy link
Member Author

dgw commented Oct 21, 2023

Fix for pop() looks reasonable to me.

Fun fact: It wasn't reasonable, and in fact didn't raise KeyError if the key is missing and no default was provided.

Fixed that, and checked off some more of my task list w/fixes for .copy(), del memory['KeY'], and .get().

Still draft, though. I have several more behaviors to retest and then fix if necessary.

@dgw dgw force-pushed the SopelIdentifierMemory-casts branch from a2038d1 to 1d5ecea Compare October 21, 2023 06:46
@dgw
Copy link
Member Author

dgw commented Oct 21, 2023

Think I'm about burned out on this for the day. But if my notes are correct, only __eq__()/__ne__() and setdefault() are left to check/unit-test/maybe-fix. 🎉

@dgw
Copy link
Member Author

dgw commented Oct 21, 2023

Turns out I lied about being done for the day. setdefault() was a pretty simple one, and then after toying with __eq__() for a bit I just decided not to bother.

Objects of different types probably should be considered unequal, unless there's a really good use-case I'm missing. (Anyone who really wants to force a comparison between a regular dict and a SopelIdentifierMemory can test identifier_memory == SopelIdentifierMemory(my_normal_dict) instead, since this patch fixes that behavior.)

@dgw dgw marked this pull request as ready for review October 21, 2023 07:41
@dgw dgw changed the title memories: cast key when needed in SopelIdentifierMemory memories: test/fix interactions between SopelIdentifierMemory and regular dict Oct 21, 2023
@dgw dgw changed the title memories: test/fix interactions between SopelIdentifierMemory and regular dict memories: test/fix interactions between SopelIdentifierMemorydict Oct 21, 2023
@dgw dgw requested a review from a team October 21, 2023 07:51
sopel/tools/memories.py Show resolved Hide resolved
sopel/tools/memories.py Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
Copy link
Member Author

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not quite time for you to look at the code again, since I haven't pushed any tweaks yet, but I thought it'd be useful to publish my comment responses before I get really into addressing the bigger line notes that will lead to any refactoring.

sopel/tools/memories.py Show resolved Hide resolved
sopel/tools/memories.py Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Fixed Show fixed Hide fixed
test/tools/test_tools_memories.py Fixed Show fixed Hide fixed
test/tools/test_tools_memories.py Fixed Show fixed Hide fixed
test/tools/test_tools_memories.py Fixed Show fixed Hide fixed
test/tools/test_tools_memories.py Fixed Show fixed Hide fixed
@dgw
Copy link
Member Author

dgw commented Oct 24, 2023

Aha, need to decide what to do about those "assert statement has a side effect" errors from CodeQL. Now that I eliminated the intermediate variables from those tests, I don't really want to put them back. 😅

Copy link
Member Author

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batched responses related to my upcoming push. Will look forward to the next round of feedback tomorrow or later in the week 🙂

sopel/tools/memories.py Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
@Exirel
Copy link
Contributor

Exirel commented Oct 28, 2023

Objects of different types probably should be considered unequal

In this case, absolutely, yes. A SopelIdentifierMemory is a very specific structure that has a very specific behavior. If people are going to do weird stuff with it, that weird stuff shouldn't work.

@dgw
Copy link
Member Author

dgw commented Oct 29, 2023

Welp, looks like no more CI builds on this branch until I rebase. Y'all mind if I squash out the fixups at the same time?

@Exirel
Copy link
Contributor

Exirel commented Oct 29, 2023

Yes go ahead, this will fix the conflict too.

@dgw
Copy link
Member Author

dgw commented Oct 29, 2023

this will fix the conflict too

Yep, that's why I wanted to rebase. Hard to continue bikeshedding new test cases if CI won't run any more. 😝

Ran 3.10 & 3.8 tests locally; pushing in a moment, once I review the final history for rebase mistakes… I wound up doing three passes on it so there'd be less chance of putting fixups in the wrong order by accident, and I'm still not quite positive that I put everything in the right order. 😅

@dgw dgw force-pushed the SopelIdentifierMemory-casts branch from ae6c337 to 7c48831 Compare October 29, 2023 15:45
@dgw dgw force-pushed the SopelIdentifierMemory-casts branch 2 times, most recently from 353e3d0 to e546b8e Compare October 29, 2023 16:01
@dgw dgw requested review from Exirel and SnoopJ October 31, 2023 14:24
Copy link
Contributor

@Exirel Exirel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, that sound pretty good so far.

Aside from various recommendations or requests, my repeating points are:

  • you can use memory[key] == dict[key] to check that the value is the same/expected when you create from dict or by using copy
  • check that a copy doesn't alter the original, and vice versa

And I agree with SnoopJ on the left over "key not in memory", they don't look relevant.

test/tools/test_tools_memories.py Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
@dgw dgw requested a review from Exirel November 1, 2023 19:55
@dgw
Copy link
Member Author

dgw commented Nov 1, 2023

I have another rebase in my future, apparently. Hopefully @SnoopJ can revisit the one or two open threads he started by the time Exi comes back from being "AFK" (as he said) for a few days, and I can rebase one last time with all the feedback/fixups then.

Copy link
Contributor

@SnoopJ SnoopJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a few more nits, but this is good enough to get a ✔️ from me

sopel/tools/memories.py Show resolved Hide resolved
sopel/tools/memories.py Show resolved Hide resolved
test/tools/test_tools_memories.py Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
test/tools/test_tools_memories.py Outdated Show resolved Hide resolved
sopel/tools/memories.py Outdated Show resolved Hide resolved
@dgw
Copy link
Member Author

dgw commented Nov 2, 2023

The fixups I just pushed (plus one technically unrelated new test I'm shoehorning in) catch even more edge cases that were missed, which @SnoopJ's nitpicks nudged me to find either directly or indirectly. Another very helpful review, thanks!

Copy link
Contributor

@Exirel Exirel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to be honest: the tests look good. The code looks good. Is there anything missing? Maybe, but I can't tell. All I know is that it looks very thorough, and if a bug arises... then we'll fix it. I don't see anything that would prevent us from fixing any issue, as the design sounds sane to me, and that's the most important part.

Note: if you really really want a nitpick at this point, I still don't think it's necessary to do any assertion about how a builtin dict works. But it's already here and it works, so there is no argument to have here.

dgw and others added 13 commits November 7, 2023 11:40
Resolves `KeyError` currently raised if the `key` parameter (`str`)
doesn't match the internal (lowercased) representation of `Identifier`.

That is, `del an_identifier_memory['key']` might have worked, but doing
`del an_identifier_memory['KeY']` would not. This is now fixed.
Calling `.copy()` on a `SopelIdentifierMemory` now returns the correct
type of object (`SopelIdentifierMemory`, instead of a plain `dict`).

Co-authored-by: SnoopJ <[email protected]>
I'm timid about changing the actual method signature for `__init__()`,
but could be talked into letting Python itself raise the `TypeError` for
us if this approach looks too dumb. (All of the memory types can be
cleaned up at once later, which is another reason I chose not to change
the signature for just one of them.)

But here we are: `SopelIdentifierMemory.__init__()` now handles being
passed an iterable of `(key, value)` 2-tuples (whether the iterable
itself is a list or a tuple), a regular `dict`, and an existing
`SopelIdentifierMemory` object.

In that last case, it's doing some unnecessary work in most cases by
always casting the keys to whatever the `identifier_factory` function
returns, but there are *probably* a few obscure cases where that might
actually be useful behavior so I'll leave it.
Constructing a `SopelMemory` by passing kwargs as key-value pairs is not
currently supported. This applies to all derived types, and since there
are now a bunch of tests for `SopelIdentifierMemory`, it gets to be the
guinea pig for an xfail-marked test of that fact.

The same limitation applies to `SopelMemoryWithDefault`, since it also
shares `SopelMemory`'s constructor method signature (which does not
accept any kwargs).

Co-authored-by: Exirel <[email protected]>
Prior to some of the recent work done on this class, calling `.clear()`
on a `SopelIdentifierMemory` instance would turn that object into a
normal `dict`, according to my tests a little while ago.

That appears to no longer be the case, and though I didn't explicitly
work on that problem yet, a unit test to make sure the behavior isn't
broken again will do us good.
Comes with a slight bonus refactoring to share the argument-processing
code previously inlined in `__init__()` with `update()`.

Co-authored-by: SnoopJ <[email protected]>
New `|` / `|=` operators added in Python 3.9 for dicts. However, because
we're now defining their dunder methods, they will also work in 3.8 for
SopelIdentifierMemory objects.

Co-authored-by: SnoopJ <[email protected]>
Co-authored-by: Exirel <[email protected]>
Comparing two `SopelIdentifierMemory` instances using `==` already
worked, but if one side of the comparison was a plain `dict` it would
be almost impossible for the two to be considered equal.

It's obviously doable to override `__eq__()`/`__ne__()` and make the two
types test as equal if they have equal values associated with keys-that-
are-equivalent-when-compared-as-`Identifier`s, but we probably shouldn't
do that. I'm perfectly happy to consider objects of different types as
"not equal" even if they contain equivalent key-value pairs.

Therefore, these new tests codify the `==`/`!=` behavior as follows:

* `SopelIdentifierMemory` objects will compare as equal if their keys
  and values compare as equal, even if they use different
  `IdentifierFactory` (the `make_identifier` parameter) implementations.
* A `SopelIdentifierMemory` and a `dict[Identifier]` MAY compare as
  equal if the `Identifier` keys in each were created by compatible (but
  not necessarily identical) `IdentifierFactory` implementations.
* A `SopelIdentifierMemory` and a `dict[str]` (or any other `dict` with
  non-`Identifier` keys) are not expected to compare as equal.
Since the whole point of this type is to have a thread-safe version of
`collections.defaultdict` and not have to check for `'key' in SMWD`
before accessing something, the `__contains__()` method (supporting the
`in` keyword) is never exercised in regular code.

The rest of `SopelMemoryWithDefault` is covered by other test cases for
code that *uses* the type, so I'll just keep this patch small.

Now the only uncovered code in `sopel/tools/memories.py` is for variadic
argument handling that we plan to remove anyway, so 98% is fine for now.
@dgw dgw force-pushed the SopelIdentifierMemory-casts branch from 11ea3a8 to a463d16 Compare November 7, 2023 18:40
@dgw
Copy link
Member Author

dgw commented Nov 7, 2023

Latest force-push to squash out all the fixups moved one of the tests within the file, but meh. I made sure that the commit order is sane using git rebase master --exec '<pip3 deps> && <run lint/test>' and all the intermediate stages pass. I verified each commit's diff manually to be even more sure that the intermediate steps make logical sense. Can't imagine what else I could double check at this point, so let's :shipit:

Note: if you really really want a nitpick at this point, I still don't think it's necessary to do any assertion about how a builtin dict works.

I reduced the number of those, if not completely eliminated them. You're right that it isn't worth delaying the patch any longer.

@dgw dgw merged commit d212dd1 into master Nov 7, 2023
15 checks passed
@dgw dgw deleted the SopelIdentifierMemory-casts branch November 7, 2023 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bugfix Generally, PRs that reference (and fix) one or more issue(s) hacktoberfest-accepted Medium Priority
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SopelIdentifierMemory pop() does not cast
3 participants