-
Notifications
You must be signed in to change notification settings - Fork 16
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
WIP: Refactor decorators in git utils #296
WIP: Refactor decorators in git utils #296
Conversation
Hi @aguschin , here a first draft of what I am changing, for now just worked on the cloning decorator. I am writing few points to guide you:
If you think that this change looks good, I can apply the same to all other decorators. |
On a second thought, I believe that passing the controller and its arguments to the cloning decorator is too much. However, while working it out, I realized that all decorators in the
Probably I could abstract it away to avoid code repetition. What do you think? |
gto/api.py
Outdated
@clone(repo_arg="repo", controller=is_url_of_remote_repo, controller_args=["repo"]) | ||
def check_ref(repo: Union[str, Repo], ref: str): | ||
"""Find out what have been registered/assigned in the provided ref""" | ||
reg = GitRegistry.from_repo(repo) | ||
return reg.check_ref(ref) |
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 may be missing something here, but what is preventing us to do something like follows?
@clone(repo_arg="repo", controller=is_url_of_remote_repo, controller_args=["repo"]) | |
def check_ref(repo: Union[str, Repo], ref: str): | |
"""Find out what have been registered/assigned in the provided ref""" | |
reg = GitRegistry.from_repo(repo) | |
return reg.check_ref(ref) | |
def check_ref(repo: Union[str, Repo], ref: str): | |
"""Find out what have been registered/assigned in the provided ref""" | |
with open_repo(repo) as r: | |
reg = GitRegistry.from_repo(r) | |
return reg.check_ref(ref) |
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.
Don't you mean to remove decorator at all here? So open_repo
will actually clone repo to temp dir if needed? Or didn't get you right here?
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.
Sorry, fixed.
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.
We had a long discussion about it in #25. We decided it's better to use decorator instead of writing this. It looked like a good way to do this at that moment. Maybe it's time to revisit that decision.
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.
Btw, if we need to also pass params like --commit
and --push
(to automatically commit and push commit or tag), could it work like with open_repo(repo, commit=commit, push=push) as r: ...
?
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.
@francesco086, do you think it would make the tests much easier? I didn't have time to dig into your tests or try to rewrite them, so relying on your opinion here.
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 am sorry but I am lost, what would make tests easier? Option 1, 2, or 3 in #296 (comment)? Could you perhaps rephrase? I am not sure I understand what is it that you mean now.
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 last suggestion that you formulated, i.e. use with gto.registry.GitRegistry.from_repo(r) as reg:
instead of decorators. Asking because tests complexity + "it's not right" feeling were main motivations for this discussion IMO)
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.
Ok, now I understand, thanks :)
The answer is: it depends on how we are going to add the remote functionalities to GitRegistry.from_repo(...)
. For what concern the testing difficulties, this change means only to move the target (if we keep the decorators) :D
This would reduce the number of tests for the cloning functionality, because that would happen only in from_repo()
. All other functionalities (commit, push) will have to be checked on all (or at least, most) of the GitRegistry
methods. And if we keep using decorators, there will be the same difficulties.
But, to be honest, when I look at the code itself, I very much like the decorator approach, because there is a very neat separation of concern. What I find very cumbersome is to test them, but maybe it's me, I don't know how to do it in the right way. Perhaps this could be formulated in this way (in simple terms):
@g
def f():
...
Suppose we are working TDD. Suppose that I fully tested f
when g
does nothing (e.g. in our case, if the repo is not remote) and I tested that when I decorate any function with g
, everything works as expected. So, I can trust f
and g
taken singularly. What is the "right" way to ensure that f
has been actually decorated with g
? The tests I have before would pass whether f
is decorated or not. What is the smallest test I need to add that would fail, and therefore would force me to decorate f
with g
?
So far the only answer to this question seemed to be inspect.getsource
and check that there is a decorator. But I find this approach of parsing the source code a little weird...
One thing more (I hope not to confuse you, please just skip it if this is the case): I talked about TDD according to the London school. If we take the Chicago school, then probably we could say "we don't check the decorators with unittest because they are private methods, and rather check the functionalities in the public ones (api
)." In practice this would mean to leave the tests of the api
as they were, and delete the tests of the decorators in git_utils
. Even though this may sound simple, it actually would lead to a higher complexity if one wants to test everything properly (one should check all possible combinations of inputs, and by adding remote_or_not
, commit
, and push
, means 8 times more test). Much easier to test 2 cases (remote_or_not
), then other 2 cases (commit
), then other 2 cases (push
), separately, and then make sure that they combine properly.
In short, if we change the decorators to context managers (notice: external to api
and registry
, so not by including them in GitRegistry
), tests may get simpler. At the price of changing the code. If someone has a good idea to solve the testing problem I explained above with f
and g
, that would be ideal...
With this said, I have nothing against "moving the target" and decorate GitRegistry
instead of the api
functions, if this helps performance etc.
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.
My suggestion, considering all:
- move the remote repo functionalities into
GitRegistry
directly (so, suggestion from @skshetry will be realized) - I am unsure whether to keep decorators or to switch to context managers. When I am unsure, I prefer to be lazy, so eventually I think I would stick to the decorators.
But I leave the decision to you :)
What about grouping tests into classes, is that ok? Like in here: https://github.com/iterative/gto/pull/296/files#diff-dca8d5901c13c8fdb3912ed1fa77246f037bdda082b903fac553851476779773R31
Thanks for great summary @francesco086. It helped a lot. WDYT about @skshetry suggestion above? |
At the beginning I liked very much the idea of the decorator because it is not intrusive (I didn't need to change the functions in api). If I remember right, this was the reason why we decided to go for decorators, we didn't want to add a However, I found quite painful to work with decorators, especially for what concerns the tests. But perhaps it comes from my inexperience with them. In the end I don't know what is best :D I would let you decide |
PS: instead of I'm not very convinced about the name |
Yes, this was one of the reasons as I remember it. Besides CLI commands, Studio will have to refactor a lot of code, if using Ok, let's see what @skshetry have in mind. I'll play around with this in meantime to see how it can be like. |
One question/suggestion: what if Basically it would be a generalized Contra I see: tight binding with gitpython (which is there already, but this would reinforce it). |
That's what I meant by |
Hi @aguschin ! I am living with a bad conscience about the code I left behind me. Any decision on which direction to take? Just let me know what you consider best. |
Hi @francesco086! Sorry, simply didn't have time to investigate this. Will take a look soon. |
Codecov ReportBase: 86.71% // Head: 86.71% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## main #296 +/- ##
==========================================
- Coverage 86.71% 86.71% -0.01%
==========================================
Files 16 16
Lines 2025 2032 +7
==========================================
+ Hits 1756 1762 +6
- Misses 269 270 +1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
7f03654
to
ca243a9
Compare
Hi @aguschin , first of all I wanted to thank you for the discussion we had, it is really expanding my understandings :) I pushed a minor change with a simplification of the Following our discussion I thought "for now we took a functional programming style", let's try to see how it would work if pushed to its consequences. Don't know if this will help us, just thought it was interesting enough to share :) |
...aaaaaand I think I got it! This is how you can assert that a decorator has been called: https://github.com/francesco086/gto/blob/test_decorator_call/tests/fp_test.py#L17 I don't know what do you think. In my opinion, it was interesting to see, but I am getting the strong impression that python is not really functional programming friendly :/ For each function you need its "core" implementation separated, eventually using the decorator notation ( Shall we switch to object oriented (and in this case, context managers)? |
Sorry, I'm not that fast 😂 was trying to understand your examples 😅
Could you maybe share something for a novice to read on that? Would be great to get some basic knowledge about TDD and these nuances, if they're important :) |
Sure, I would recommend this one about the difference between London and Chicago school: https://youtu.be/_S5iUf0ANyQ I first learned it from Uncle Bob in his video lectures on Clean Code, but don't have a O'Reilly account anymore and can't point to it anymore sry :( |
In my stubbornness, I came across at again another "discovery" about decorators. I was wondering: can you reference a non-decorated function? How? ...and when you are stubborn enough to check the python-lang source code, you find out that the reference to the original function (the from functools import wraps
def g(f):
@wraps(f)
def wrapped_f(*args, **kwargs):
print("decorated!")
return f(*args, **kwargs)
return wrapped_f
@g
def f():
print("I am f")
f.__wrapped__()
>> I am f Here we go! I think this is good to know |
The other piece I needed for what I wanted to do (actually, the most important), a import ast
import inspect
from dataclasses import dataclass
from functools import wraps
from typing import Callable
def g(a):
def wrap(f):
@wraps(f)
def wrapped_f(*args, **kwargs):
return f(*args, **kwargs)
return wrapped_f
return wrap
@g("x")
@g(a="x")
def f(x):
pass
@dataclass
class AppliedDecorator:
f: str
args: list
kwargs: dict
def get_applied_decorators(f: Callable):
o = ast.parse(inspect.getsource(f))
ast_f = [c for c in ast.iter_child_nodes(o) if c.name == f.__name__][0]
return [
AppliedDecorator(f=d.func.id,
args=[a.value for a in d.args],
kwargs={k.arg: k.value.value for k in d.keywords})
for d in ast_f.decorator_list
]
dec = get_applied_decorators(f)
print(dec)
>> [AppliedDecorator(f='g', args=['x'], kwargs={}), AppliedDecorator(f='g', args=[], kwargs={'a': 'x'})] Unfortunately it requires parsing the source code :( But thanks to the module |
… git_utils.clone to take repo_arg
ca243a9
to
7978a8e
Compare
Hi @aguschin , I started working out the alternative approach, here a draft PR where you can have a look at the changes: Notice how many rather intrusive changes were required. The most annoying part is Notice also that the same exact changes were required for How does it look in your opinion? Do you think it's a promising direction? |
I close this one in favor of #309 |
Contributes to #25