Skip to content

Let remote.push return a PushInfoList #1360

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

Merged
merged 8 commits into from
Nov 13, 2021

Conversation

Sjord
Copy link
Contributor

@Sjord Sjord commented Oct 12, 2021

List-like, so that it's backward compatible. But it has a new method
raise_on_error, that throws an exception if anything failed to push.

Related to #621

This is more of a proposal for this design. Is this actually backward-compatible? Should we check the flags of all PushInfos in raise_on_error? Is this better than a new method push_really_or_else_raise() or push(raise=True)? What if a PushInfo is missing from the result because it totally failed?

@Byron
Copy link
Member

Byron commented Oct 13, 2021

Thanks for keeping the ball rolling on solving the matter.

Changing the type of the return value could probably easily be a backward incompatible change unless (probably impossible) care is taken to make the type equivalent even in the presence of instanceof checks.

Hence I think having an entirely new method for it that can do what it wants is the only safe way.

@Sjord
Copy link
Contributor Author

Sjord commented Oct 14, 2021

Changing the type of the return value could probably easily be a backward incompatible change unless (probably impossible) care is taken to make the type equivalent even in the presence of instanceof checks.

Well, the new PushInfoList extends IterableList and list. So isinstance(..., list) would still return true, and PushInfoList can still be iterated over to retrieve PushInfo items. It's true that the return type is different, but I thought that would have limited impact.

@Yobmod, what do you think?

@Byron
Copy link
Member

Byron commented Oct 14, 2021

Well, the new PushInfoList extends IterableList and list. So isinstance(..., list) would still return true, and PushInfoList can still be iterated over to retrieve PushInfo items. It's true that the return type is different, but I thought that would have limited impact.

Thanks for the clarification. Could you post an example invocation for me to understand how one would enable exceptions using the return type? My intuition here is that a separate method with different semantics will be similarly opt-in, but inherently more straightforward to use.

@Sjord
Copy link
Contributor Author

Sjord commented Nov 7, 2021

The most straightforward usage is like this:

remote.push().raise_on_error()

The return value of push() is backwards compatible, making it possible to combine the ability to raise exceptions with checking flags.

push_info = remote.push()
if not push_info[0].flags & push_info.NO_MATCH:
    push_info.raise_on_error()

This adds more flexibility for custom error handling, compared to a new method.

A similar pattern is used in requests' raise_for_status, which raises an error when a HTTP request returns an error status code.

@Byron
Copy link
Member

Byron commented Nov 7, 2021

Thanks for the clarification! I think my biggest stumbling block was the naming, specifically …on_error() which made me think it's a capability that is turned on to, then, raise if an error occurs. Of course it didn't compute that this would be set on a return value.

Maybe it will help most to rename it to raise_for_error() instead to go with the mainstream, even though my intuition would have been raise_if_error().

Since the the respective field is called exception, I think I would be confused if the actual value is not stored in result.error. Maybe it's better to either not expose this by making the field private, or to rename either method or field for consistency. Since built-in error types are usually called …Error, like IOError, I suppose the field should be named similarly to match.

Once the naming is clear, I think there shouldn't be anything else holding this PR back.

Sjord added 2 commits November 8, 2021 16:21
List-like, so that it's backward compatible. But it has a new method
raise_on_error, that throws an exception if anything failed to push.

Related to gitpython-developers#621
@Sjord Sjord force-pushed the PushInfoList.raise_on_error branch from b705587 to 997a3b6 Compare November 8, 2021 17:06
@Byron
Copy link
Member

Byron commented Nov 8, 2021

@Sjord Please feel free to mark this PR as ready when ready. If there are problems with types, [AT]Yobmod might be able to provide some clues.

@Sjord Sjord force-pushed the PushInfoList.raise_on_error branch from 997a3b6 to 808179e Compare November 9, 2021 11:42
@Sjord Sjord marked this pull request as ready for review November 9, 2021 12:01
Copy link
Member

@Byron Byron left a comment

Choose a reason for hiding this comment

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

This looks great, thanks a lot.

I think once we can prove that the type system won't break by this change either it's definitely ready to be merged.

Thanks a lot

git/remote.py Outdated
@@ -116,6 +116,23 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non
return progress


class PushInfoList(IterableList):
Copy link
Member

Choose a reason for hiding this comment

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

The strange thing is that I would expect IterableList[PushInfo] to be the base class, or at least something in this type should indicate that PushInfos are iterated over.

That way, PushInfoList would be fitting in place of IterableList[PushInfo], which might even be a test to add to assure the type system stays compatible, too.

Something like…

def f(l: IterableList[PushInfo]):
    pass

f(PushInfoList())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, but I had trouble getting this to work with mypy.

The list with pushinfos was created like this:

output: IterableList[PushInfo] = IterableList('push_infos')

I am not sure what the push_infos argument is for, but I tried moving it into PushInfoList:

class PushInfoList(IterableList[PushInfo]):
    def __new__(cls) -> 'PushInfoList':
        return super().__new__(cls, 'push_infos')

    def __init__(self) -> None:
        super().__init__('push_infos')
        self.error: Optional[Exception] = None

However, mypy doesn't agree with this. The types don't match up in __new__. super(IterableList[PushInfo], cls) doesn't work, so now I have a call explicitly to IterableList.__new__. Is that OK?

class PushInfoList(IterableList[PushInfo]):
    def __new__(cls) -> 'PushInfoList':
        return cast(PushInfoList, IterableList.__new__(cls, 'push_infos'))

Copy link
Member

Choose a reason for hiding this comment

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

"push_infos" is supposed to be an id_attribute, which definitely isn't useful here as it is used like this.

Maybe @Yobmod can help us clarify if this .__new__ calling convention to pacify mypi is the expected way, or if there are more idiomatic ways to do this.

From my point of view, it's certainly OK even though I can't tell if a future patch release of mypi is going to stop accepting it.

@Sjord Sjord force-pushed the PushInfoList.raise_on_error branch from eec1b97 to a270deb Compare November 10, 2021 14:05
Copy link
Member

@Byron Byron left a comment

Choose a reason for hiding this comment

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

Thanks for the improvement to the types, much appreciated.

Is it possible to add a test for this type, too, to validate that the new type now passes for the old if used in a typed function like this?

def f(l: IterableList[PushInfo]):
    pass

f(PushInfoList())

@Sjord
Copy link
Contributor Author

Sjord commented Nov 11, 2021

Is it possible to add a test for this type, too, to validate that the new type now passes for the old if used in a typed function like this?

This is not so easy. I tried the folllowing, but isinstance and issubclass don't work on generic types:

self.assertIsInstance(results, IterableList[PushInfo])

Another possibility would be to let mypy do the testing; mypy would raise an error on your example code if PushInfoList is no longer compatible with IterableList[PushInfo]. But the unittest code is not checked with mypy, so putting it there won't have any effect. I am also not comfortable with putting dead code in git/remote.py just for mypy testing sake.

@Byron
Copy link
Member

Byron commented Nov 12, 2021

Another possibility would be to let mypy do the testing; mypy would raise an error on your example code if PushInfoList is no longer compatible with IterableList[PushInfo]. But the unittest code is not checked with mypy, so putting it there won't have any effect. I am also not comfortable with putting dead code in git/remote.py just for mypy testing sake.

Thanks for having given it a shot. I wasn't even aware that the tests aren't yet checked with mypi and can imagine that trying to do that would result in a slew of errors that aren't worth fixing.

If you can tell me something like the code I suggested worked in your local tests, I think we are ready to merge, having tried our best to not break anything.

Thank you

@Sjord
Copy link
Contributor Author

Sjord commented Nov 12, 2021

If you can tell me something like the code I suggested worked in your local tests

Yes, I temporarily put the example you supplied in remote.py, and mypy agrees that a PushInfoList instance is acceptable for a IterableList[PushInfo] parameter.

Copy link
Member

@Byron Byron left a comment

Choose a reason for hiding this comment

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

Thanks a lot for all the work that went into this and for hanging in there :).

@Byron Byron added this to the v3.1.25 - Bugfixes milestone Nov 13, 2021
@Byron Byron merged commit 35f7e94 into gitpython-developers:main Nov 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants