Skip to content

feat(api)!: ListMixin.list typing overload #3122

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

Conversation

igorp-collabora
Copy link
Contributor

@igorp-collabora igorp-collabora commented Feb 6, 2025

BREAKING CHANGE: The http_list method will no longer
ignore the iterator argument if the page argument is not
None. The ListMixin.list() will now return either list or
iterator based on the iterator argument. This will make
certain type narrowing checks redundant.

Overload ListMixin.list method typing to return either
RESTObjectList or list based on the iterator argument.
By default then iterator is False return a list otherwise
return an iterator.

Provide 3 overloads:

  1. iterator: Literal[False] - this is the default and returns a list.
  2. iterator: Literal[True] - return an iterator.
  3. iterator: bool - return either list or iterator. It is useful when
    the list function is being extended by another function that can
    also take either True or False for the iterator argument.

Make page argument to http_list not override the iterator
to make the function signatures more straight forward. This
also makes it easier to unpack **kwargs as only iterator
argument will control if a list or iterator is returned so the
**kwargs can no longer have a hidden page argument.

OLD

Questions:

  1. Should calling without get_all or iterator raise deprecation warning? (mypy needs a fix Overloaded deprecated method use is not reported python/mypy#18474)
  2. Is it get_all or all? Documentation still uses all but apparently it was replaced in favour of get_all?
  3. **kwargs are used for all options. Should at least some options become keyword arguments? This will improve code completion tools.

@nejch
Copy link
Member

nejch commented Feb 7, 2025

Thanks for your work here as well @igorp-collabora!

To answer your questions:

Should calling without get_all or iterator raise deprecation warning? (mypy needs a fix python/mypy#18474)

We issue a user warning without it as it's really helped avoid frequent user questions, but I'm not sure if we should fully deprecate it/require passing it. Does it make things very difficult otherwise? /cc @JohnVillalovos

Is it get_all or all? Documentation still uses all but apparently it was replaced in favour of get_all?
**kwargs are used for all options.

all was often conflicting with API parameters, so we added get_all, yes, and silently deprecated all plus, I thought, removed any references to it in the docs. Maybe it's time we add an explicit deprecation warning to it or simply remove it in a breaking change.

Should at least some options become keyword arguments? This will improve code completion tools.

It might be a good opportunity to do this now and be more explicit. However I think we should maybe then also track removal of custom list methods (maybe we can make the list helper more generic to accommodate deviations that require custom methods now). Otherwise we'll have a lot of duplication to cover all of them.

@igorp-collabora igorp-collabora force-pushed the list-mixin-typing-overload branch from 8cbf8a9 to 716a905 Compare February 7, 2025 16:24
@igorp-collabora
Copy link
Contributor Author

I changed the overload to be around the iterator=. Looking at the client.py it looks like then iterator=True and page is None then the RESTObjectList is instantly returned. In all other cases the list is returned. This bypasses the get_all vs all issue.

Copy link

codecov bot commented Feb 7, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 97.30%. Comparing base (beb2f24) to head (33b4a70).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3122      +/-   ##
==========================================
+ Coverage   97.28%   97.30%   +0.01%     
==========================================
  Files          97       97              
  Lines        5977     6012      +35     
==========================================
+ Hits         5815     5850      +35     
  Misses        162      162              
Flag Coverage Δ
api_func_v4 83.48% <94.44%> (-0.06%) ⬇️
cli_func_v4 84.61% <90.74%> (+0.10%) ⬆️
unit 90.13% <90.74%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
gitlab/client.py 98.72% <100.00%> (-0.01%) ⬇️
gitlab/mixins.py 91.48% <100.00%> (+0.14%) ⬆️
gitlab/v4/cli.py 91.85% <100.00%> (ø)
gitlab/v4/objects/ldap.py 82.75% <100.00%> (+4.49%) ⬆️
gitlab/v4/objects/snippets.py 100.00% <100.00%> (ø)
gitlab/v4/objects/users.py 99.17% <100.00%> (+0.02%) ⬆️

@nejch
Copy link
Member

nejch commented Feb 7, 2025

OK, this is really concise now! Focusing on the iterator arg also works great as that's where our custom iterator comes from as well. IMO, we could also add a breaking change trailer here to say that we no longer have union return but always a specific type, WDYT?

@igorp-collabora
Copy link
Contributor Author

Turns out that if you want to have overloaded methods extended by some other method you need to add an extra overload with original signature:

import typing


@typing.overload
def foo(bar: typing.Literal[True] = True) -> int: ...


@typing.overload
def foo(bar: typing.Literal[False]) -> str: ...

#
# @typing.overload
# def foo(bar: bool) -> str | int: ...


def foo(bar: bool = True) -> str | int:
    return 12345 if bar else "12345"


def test() -> None:
    typing.reveal_type(foo())
    typing.reveal_type(foo(True))
    typing.reveal_type(foo(False))


@typing.overload
def baz(foz: typing.Literal[True] = True) -> int: ...


@typing.overload
def baz(foz: typing.Literal[False]) -> str: ...


def baz(foz: bool = True) -> int | str:
    return foo(foz)


def test2() -> None:
    typing.reveal_type(baz())
    typing.reveal_type(baz(True))
    typing.reveal_type(baz(False))

Without the 3rd overloaded signature on the foo the following error will be raised:

typing_overloads.py:35: error: Returning Any from function declared to return "int | str"  [no-any-return]
typing_overloads.py:35: error: No overload variant of "foo" matches argument type "bool"  [call-overload]
typing_overloads.py:35: note: Possible overload variants:
typing_overloads.py:35: note:     def foo(bar: Literal[True] = ...) -> int
typing_overloads.py:35: note:     def foo(bar: Literal[False]) -> str

I will adjust the list method overload accordingly because it is being extended in a few places. For example, SnippetManager.list_public.

@igorp-collabora
Copy link
Contributor Author

Or you define the use the union of two literals to describe the extended function args type:

def baz(foz: typing.Literal[True] | typing.Literal[False] = True) -> int | str:
    return foo(foz)

This passed the type check. It looks like the bool is not the same as typing.Literal[True] | typing.Literal[False].

@igorp-collabora
Copy link
Contributor Author

I think I will add the 3rd overload instead of Literal[True, False] because it will be less disruptive and more extendable for downstream users. Otherwise everyone will have to use Literal[True, False] instead of bool.

@igorp-collabora
Copy link
Contributor Author

@nejch if this will be a breaking change what do you think about making the page argument not override the iterator? Right now it will fall back to a list if page is set even if iterator=True. (it also emits a warning) If the iterator will be the only argument that controls the list vs iterator then the function signature and its documentation will be cleaner.

BREAKING CHANGE: The `http_list` method will no longer
ignore the `iterator` argument if the `page` argument is not
None. The `ListMixin.list()` will now return either list or
iterator based on the `iterator` argument. This will make
certain type narrowing checks redundant.

Overload `ListMixin.list` method typing to return either
`RESTObjectList` or `list` based on the `iterator` argument.
By default then `iterator` is False return a list otherwise
return an iterator.

Provide 3 overloads:

1. `iterator: Literal[False]` - this is the default and returns a list.
2. `iterator: Literal[True]` - return an iterator.
3. `iterator: bool` - return either list or iterator. It is useful when
   the list function is being extended by another function that can
   also take either True or False for the `iterator` argument.

Make `page` argument to `http_list` not override the `iterator`
to make the function signatures more straight forward. This
also makes it easier to unpack `**kwargs` as only `iterator`
argument will control if a list or iterator is returned so the
`**kwargs` can no longer have a hidden page argument.
@igorp-collabora igorp-collabora force-pushed the list-mixin-typing-overload branch from 716a905 to 33b4a70 Compare February 11, 2025 13:45
@igorp-collabora igorp-collabora changed the title Draft: ListMixin.list typing overload feat(api)!: ListMixin.list typing overload Feb 11, 2025
@igorp-collabora
Copy link
Contributor Author

I made the iterator argument be the only one that controls what gets returned. This makes it easier to use with the **kwargs as it type checker will not assume that page argument can be in kwargs.

See the gitlab/v4/cli.py do_list. Because we specifically call using iterator=False the type checker will know that return will be list. If page argument was still able to control the return type we would have to pass page=None or type checker will fail to match the overload as **self.args could have the page argument.

I added BREAKING CHANGES to the commit message, the exclamation mark and undrafted PR.

(also the http_list is used in a lot of managers and objects directly and they can use a return type overloads)

@nejch
Copy link
Member

nejch commented Feb 12, 2025

Thanks @igorp-collabora for looking into all the options and @JohnVillalovos for taking a look as well 🙇

@nejch nejch merged commit 6eee494 into python-gitlab:main Feb 12, 2025
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Overloaded deprecated method use is not reported
3 participants