-
Notifications
You must be signed in to change notification settings - Fork 3
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
Allow composing using '+' instead of explicit compose() calls. #1
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -59,6 +59,15 @@ def __call__(self, /, *args, **kwargs): | |||||||||||||||||||||||
result = function(result) | ||||||||||||||||||||||||
return result | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def __add__(self, other): | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we did implement this as an operator overload on the Mainly, because if
(Incidentally, this is a strong reminder that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks to the thoughts triggered by this, I went ahead and fixed |
||||||||||||||||||||||||
"""Compose using '+' operator.""" | ||||||||||||||||||||||||
if isinstance(other, compose): | ||||||||||||||||||||||||
return compose(*self.functions, *other.functions) | ||||||||||||||||||||||||
elif callable(other): | ||||||||||||||||||||||||
return compose(*self.functions, other) | ||||||||||||||||||||||||
Comment on lines
+65
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a class instance constructs another instance of itself, it should use whatever class it actually is, not hardcode the class:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that |
||||||||||||||||||||||||
else: | ||||||||||||||||||||||||
raise ValueError("Can only compose callables and 'compose' instances.") | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python operator overloads are special, so instead of raising an exception in such cases, they should Also, if we were returning an exception here, the appropriate exception would be
Comment on lines
+64
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to detect
Comment on lines
+64
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So putting together all the code comments, if we do implement this as an overload on this class, I would change the whole function body to just this:
Suggested change
P.S. Since
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, also, unless code is expecting that its error message will be given directly to a non-technical end-user, its error messages should be optimized for pasting into other text, not for as-is presentation to humans, so:
Suggested change
You're thinking of the error message as a stand-alone thing, which is absolutely the right mindset at the user interface boundary to a human, but at the level of utility code like try:
clean_up(...)
except Exception as error:
raise CleanupError(`f"cleaning up: {error}`) If
Whereas if |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
@_recursive_repr_if_available | ||||||||||||||||||||||||
def __repr__(self): | ||||||||||||||||||||||||
"""Represent the composed function as an unambiguous string.""" | ||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -11,6 +11,46 @@ def g(s): | |||||||||||||||||||||||||||||||||||||||||||||||
assert f_g.functions == (g, f) # functions exposed in execution order | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
def test_compose_add_compose(): | ||||||||||||||||||||||||||||||||||||||||||||||||
def f(s): | ||||||||||||||||||||||||||||||||||||||||||||||||
return s + 'f' | ||||||||||||||||||||||||||||||||||||||||||||||||
def g(s): | ||||||||||||||||||||||||||||||||||||||||||||||||
return s + 'g' | ||||||||||||||||||||||||||||||||||||||||||||||||
f_wrapped = compose(f) | ||||||||||||||||||||||||||||||||||||||||||||||||
g_wrapped = compose(g) | ||||||||||||||||||||||||||||||||||||||||||||||||
f_g_wrapped = f_wrapped + g_wrapped | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
assert f_g_wrapped('') == 'gf' # Remember that it's f(g(...)) so g runs first | ||||||||||||||||||||||||||||||||||||||||||||||||
assert f_g_wrapped.functions == (g, f) # functions exposed in execution order | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
def test_compose_add_function(): | ||||||||||||||||||||||||||||||||||||||||||||||||
def f(s): | ||||||||||||||||||||||||||||||||||||||||||||||||
return s + 'f' | ||||||||||||||||||||||||||||||||||||||||||||||||
def g(s): | ||||||||||||||||||||||||||||||||||||||||||||||||
return s + 'g' | ||||||||||||||||||||||||||||||||||||||||||||||||
f_wrapped = compose(f) | ||||||||||||||||||||||||||||||||||||||||||||||||
f_g_composed = f_wrapped + g | ||||||||||||||||||||||||||||||||||||||||||||||||
assert f_g_composed('') == 'gf' # Remember that it's f(g(...)) so g runs first | ||||||||||||||||||||||||||||||||||||||||||||||||
assert f_g_composed.functions == (g, f) # functions exposed in execution order | ||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+14
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we find ourselves having this much of this kind of repetition in our tests, that suggests we're testing the wrong thing, and should think more precisely about how to test just what we're adding on top of the already-tested behavior. In this case, the assert (compose(f) + f) == compose(f, f) but then I remembered that we don't have assert (compose(f) + f).functions == compose(f, f).functions Key point is that we want to eliminate all the irrelevant stuff. Why have both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also I think if I thought about this more, I would probably have other refactors to suggest for these tests, but I don't want to spend more time on thinking and describing how I would further refine the tests for how it is now, given my "big picture" leanings. |
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
def test_compose_add_neither_compose_nor_function(): | ||||||||||||||||||||||||||||||||||||||||||||||||
def f(s): | ||||||||||||||||||||||||||||||||||||||||||||||||
return s + 'f' | ||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+39
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the actual function is never used, I would probably use the closest thing to an appropriately "nil" function here:
Suggested change
(Of course, a completely nil function has zero arguments and no explicit return value, but function composition logically requires every function except the last one returns something, and every function except the first one takes exactly one argument, so the closest to a nil function which still fits every possible position in a function composition takes one argument and returns something.) |
||||||||||||||||||||||||||||||||||||||||||||||||
f_wrapped = compose(f) | ||||||||||||||||||||||||||||||||||||||||||||||||
g = 'string' | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is just a very minor matter, almost just personal taste, but I would've used |
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
raised = False | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||||||
f_g_wrapped = f_wrapped + g | ||||||||||||||||||||||||||||||||||||||||||||||||
except ValueError as err: | ||||||||||||||||||||||||||||||||||||||||||||||||
raised = True | ||||||||||||||||||||||||||||||||||||||||||||||||
finally: | ||||||||||||||||||||||||||||||||||||||||||||||||
assert raised | ||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+44
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Because of a bunch of tiny optimizations of various factors: Simpler (less logical operations, less indirection, less things, less complecting of things together), shorter (so more relevant information fits within the detailed central spot of human vision, so you pay less saccades and human working memory for reading the same code behavior), produces a relevant self-explanatory failure message, not naming/storing things which we're not using as if we're going to use them (so the reader has more immediate local cues that they eliminate or discount the possibility that they are used later), the error type matches what Python normally raises in these cases. Actually, testing if an error is raised is not the right test - Python is responsible for raising that
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I think these tests only felt like they belonged in this spot of the file because the first two tests start out by re-testing everything that the first test tests. But once we clear that up, it becomes more obvious that these tests are not particularly related to the first test at all, and thus are just testing a distinct feature. And in fact they're breaking up the flow of the tests, because the tests that immediately follow are still testing |
||||||||||||||||||||||||||||||||||||||||||||||||
def test_inlining(): | ||||||||||||||||||||||||||||||||||||||||||||||||
def f(_): | ||||||||||||||||||||||||||||||||||||||||||||||||
return None | ||||||||||||||||||||||||||||||||||||||||||||||||
|
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.
When overriding operators, make sure to also override the reverse function (in this case
__radd__
).