Skip to content

[redis] fix pipeline return types #4989

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 7 commits into from
Feb 23, 2021

Conversation

chdsbd
Copy link
Contributor

@chdsbd chdsbd commented Jan 30, 2021

Pipeline is a subclass of Redis, but instead of the commands returning results, they return Pipeline.

Previously the types said Redis().pipeline().rpush("mylist") returned int. The updated types now type the response as Pipeline.

With this change the following code now type checks.

from redis import Redis
r = Redis()
list_length, _ = r.pipeline().rpush("mylist").ltrim("mylist", 0, 100).execute()

Pipeline is a subclass of Redis, but instead of the commands returning results, they return Pipeline.

Previously the types incorrectly said `Redis().pipeline().rpush("mylist")` returned `int`. The updated types now type the response as `Pipeline`.

With this change the following code now type checks.
```
from redis import Redis
r = Redis()
list_length, _ = r.pipeline().rpush("mylist").ltrim("mylist", 0, 100).execute()
```
@chdsbd chdsbd changed the title fix pipeline return types [redis] fix pipeline return types Jan 30, 2021
def multi(self) -> None: ...
def execute_command(self, *args, **kwargs): ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

execute_command, along with parse_response, watch, and unwatch were duplicated in the methods I copied from Redis, so I deleted these and updated the copied ones below.

def script_load_for_pipeline(self, script): ...

class StrictPipeline(BasePipeline, StrictRedis): ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

StrictPipeline and BasePipeline don't exist in the library, so I think it's safe to use use the name Pipeline: https://github.com/andymccurdy/redis-py/blob/1a41cfd95a53a95b078084d8627be6b6fba3bb71/redis/client.py

Copy link
Collaborator

Choose a reason for hiding this comment

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

This probably was leftover from an old redis-py version.

Comment on lines 712 to 714
# methods from `Redis` client.
# arguments match `Redis`, but with some exceptions return types are
# `Pipeline` or `None`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Basically all the commands have the same arguments as Redis, but the return types are different.

Comment on lines 856 to 867
def sort(
self,
name: _Key,
start: Optional[int] = ...,
num: Optional[int] = ...,
by: Optional[_Key] = ...,
get: Optional[Union[_Key, Sequence[_Key]]] = ...,
desc: bool = ...,
alpha: bool = ...,
store: Optional[_Key] = ...,
groups: bool = ...,
) -> Pipeline: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Redis().sort() is overloaded to provide an accurate return type, but for Pipeline the return type is always Pipeline, so I removed the overloads.

@overload
def sort(
self,
name: _Key,
start: Optional[int] = ...,
num: Optional[int] = ...,
by: Optional[_Key] = ...,
get: Optional[Union[_Key, Sequence[_Key]]] = ...,
desc: bool = ...,
alpha: bool = ...,
store: None = ...,
groups: bool = ...,
) -> List[_StrType]: ...
@overload
def sort(
self,
name: _Key,
start: Optional[int] = ...,
num: Optional[int] = ...,
by: Optional[_Key] = ...,
get: Optional[Union[_Key, Sequence[_Key]]] = ...,
desc: bool = ...,
alpha: bool = ...,
*,
store: _Key,
groups: bool = ...,
) -> int: ...
@overload
def sort(
self,
name: _Key,
start: Optional[int],
num: Optional[int],
by: Optional[_Key],
get: Optional[Union[_Key, Sequence[_Key]]],
desc: bool,
alpha: bool,
store: _Key,
groups: bool = ...,
) -> int: ...

def smembers(self, name: _Key) -> Pipeline: ...
def smove(self, src, dst, value) -> Pipeline: ...
def spop(self, name, count: Optional[int] = ...) -> Pipeline: ...
def srandmember(self, name: _Key, number: Optional[int] = ...) -> Pipeline: ...
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 removed the overload here too for the same reason.

@overload
def srandmember(self, name: _Key, number: None = ...) -> Optional[_Value]: ...
@overload
def srandmember(self, name: _Key, number: int) -> List[_Value]: ...

groups: bool = ...,
) -> Pipeline: ...
def scan(self, cursor: int = ..., match: Optional[_Key] = ..., count: Optional[int] = ...) -> Pipeline: ...
def scan_iter(self, match: Optional[Text] = ..., count: Optional[int] = ...) -> Iterator[Any]: ...
Copy link
Contributor Author

@chdsbd chdsbd Jan 30, 2021

Choose a reason for hiding this comment

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

The scan_iter and other _iter methods return generators that explode when next() is called on them.

def script_flush(self) -> Pipeline: ...
def script_kill(self) -> Pipeline: ...
def script_load(self, script) -> Pipeline: ...
def register_script(self, script: Union[Text, _StrType]) -> Script: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

unlike other methods, this one actually returns Script

def pubsub_numpat(self) -> Pipeline: ...
def monitor(self) -> Monitor: ...
def cluster(self, cluster_arg: str, *args: Any) -> Pipeline: ...
def client(self) -> Any: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this method explodes when called

def pubsub_channels(self, pattern: _Key = ...) -> Pipeline: ...
def pubsub_numsub(self, *args: _Key) -> Pipeline: ...
def pubsub_numpat(self) -> Pipeline: ...
def monitor(self) -> Monitor: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This also returns Monitor instead of Pipeline

xx: bool = ...,
keepttl: bool = ...,
) -> Pipeline: ...
def __setitem__(self, name, value) -> None: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

should all the dunder methods be organized at the top?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Personally, I try to use the order the implementation uses. But it is your choice.

def object(self, infotype, key) -> Pipeline: ...
def ping(self) -> Pipeline: ...
def save(self) -> Pipeline: ...
def sentinel(self, *args) -> None: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

@srittau srittau left a comment

Choose a reason for hiding this comment

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

Thank you for this PR and sorry for the long delay. Only one issue below.

@@ -690,38 +690,297 @@ class PubSub:
def run_in_thread(self, sleep_time=...): ...
def ping(self, message: Optional[_Value] = ...) -> None: ...

class BasePipeline:
class Pipeline:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This devices from Redis in the implementation, so it should do so here as well. This will probably need # type: ignores where the signature of Pipeline does not match that of Redis. (But since Pipeline is a subtype of Redis that shouldn't be much of a concern.)

xx: bool = ...,
keepttl: bool = ...,
) -> Pipeline: ...
def __setitem__(self, name, value) -> None: ...
Copy link
Collaborator

Choose a reason for hiding this comment

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

Personally, I try to use the order the implementation uses. But it is your choice.

@chdsbd
Copy link
Contributor Author

chdsbd commented Feb 23, 2021

I updated Pipeline to inherit from Redis and added # type: ignore [override] to every method that returns Pipeline. Not all of these ignore statements are necessary because some methods in parent class are missing return types.

The ordering of methods should match the redis-py implementation. The Pipeline defines a few methods but inherits most methods from the Redis client.

@chdsbd chdsbd requested a review from srittau February 23, 2021 00:42
@srittau srittau merged commit 46ac6bf into python:master Feb 23, 2021
@Akuli
Copy link
Collaborator

Akuli commented Feb 23, 2021

I think # type: ignore[override] is a non-standard mypy thing? This PR however uses # type: ignore [override], with a space. Maybe type checkers understand that to be same as just # type: ignore?

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.

3 participants