Skip to content
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

QueryBuilder: add flat keyword to first method #5410

Merged

Conversation

sphuber
Copy link
Contributor

@sphuber sphuber commented Mar 6, 2022

Fixes #5408

This keyword already exists for the all method and it will likewise be
useful for first when only a single quantity is projected. In that
case, often the caller doesn't want a list as a return value but simply
the projected quantity. Allowing to get this directly from the method
call as opposed to manually dereferencing the first item from the
returned list often makes for cleaner code.

@sphuber sphuber force-pushed the fix/5408/query-builder-first-flat branch from 3bedc9b to 0a92c72 Compare March 7, 2022 18:03
def first(self) -> Optional[List[Any]]:
"""Executes the query, asking for the first row of results.
def first(self, flat: bool = False) -> Optional[Union[List[Any], Any]]:
"""Return the first result of the query.
Copy link
Member

Choose a reason for hiding this comment

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

I still think it is important to indicate this actually executes a query, i.e. connects to the storage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a comment in docstring in vein of SQLA docs

@chrisjsewell
Copy link
Member

I'd note, this is a little similar to https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=scalar#sqlalchemy.orm.Query.scalar (plus thing like one and one_or_none)

I'm tempted to say this should be a separate method, because it is never real ideal when you are returning unions of different types, although actually at least add overload typing, i.e.

from typing import Any, List, Literal, overload

    @overload
    def first(self, flat: Literal[False]) -> Optional[List[Any]]:
        ...
    @overload
    def first(self, flat: Literal[True]) -> Optional[Any]:
        ...

@sphuber sphuber force-pushed the fix/5408/query-builder-first-flat branch 2 times, most recently from 5c0564d to 5400aee Compare March 9, 2022 14:05
@sphuber
Copy link
Contributor Author

sphuber commented Mar 9, 2022

@chrisjsewell should be good for second review

@sphuber
Copy link
Contributor Author

sphuber commented Mar 9, 2022

Are the new pre-commit failures due to the use of the overload or the from __future__ import annotations?

@chrisjsewell
Copy link
Member

chrisjsewell commented Mar 9, 2022

Are the new pre-commit failures due to the use of the overload or the from __future__ import annotations?

Not due to the __future__, just due to the types, i.e. surfacing "bugs" that already existed, whereby the code is assuming a list is always returned, when actually None can also be returned.
For some reason, pylint was not picking these up before.

FYI, when you are adding from __future__ import annotations, I would suggest running pyupgrade against the file, which will convert all the old style annotation to the new style: https://github.com/asottile/pyupgrade#pep-585-typing-rewrites

@sphuber
Copy link
Contributor Author

sphuber commented Mar 9, 2022

Not due to the future, just due to the types, i.e. surfacing "bugs" that already existed, whereby the code is assuming a list is always returned, when actually None can also be returned.

But those are false positives surely? The default for flat is False, so all those calls should simply return lists and the unsubscriptable-object therefore is simply wrong. Please tell me that I don't have to start adding ignore statements for all of them just because pylint doesn't properly read the typing. In that case I see little to no benefit of adding the overloaded definitions just for the sake of the typing if all it causes us is trouble

@chrisjsewell
Copy link
Member

But those are false positives surely? The default for flat is False, so all those calls should simply return lists

No, the return type is Optional[List[str]], i.e. a list[str] | None

Comment on lines 1017 to 1018
if result is None:
return None
Copy link
Member

Choose a reason for hiding this comment

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

@sphuber it can return None here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I saw that, but take the first hit aiida/orm/nodes/data/upf.py:136:27:

        existing_upf = builder.first()

        if existing_upf is None:
            ...
        else:
            existing_upf = existing_upf[0]

That looks like a false positive to me. If anything this would be the perfect candidate for first(flat=True).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same goes for the next "hits"

tests/orm/test_querybuilder.py:724:26: E1136: Value 'result' is unsubscriptable (unsubscriptable-object)
tests/orm/test_querybuilder.py:725:26: E1136: Value 'result' is unsubscriptable (unsubscriptable-object)

Also false positives.

Copy link
Member

Choose a reason for hiding this comment

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

Fair, then IMO, I would just disable unsubscriptable-object, because pylint does not seem to be doing a good job with them: pylint-dev/pylint#1498
I don't feel pylint should be trying to involve itself in type-checking, since that is what mypy is for, that does it a lot better

Copy link
Member

Choose a reason for hiding this comment

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

Eurgh could also be overload, could be future annotations: pylint-dev/pylint#5189, pylint-dev/pylint#3979, pylint-dev/pylint#4369
Silly pylint

This keyword already exists for the `all` method and it will likewise be
useful for `first` when only a single quantity is projected. In that
case, often the caller doesn't want a list as a return value but simply
the projected quantity. Allowing to get this directly from the method
call as opposed to manually dereferencing the first item from the
returned list often makes for cleaner code.
@sphuber sphuber force-pushed the fix/5408/query-builder-first-flat branch from 5400aee to 952ed5a Compare March 9, 2022 19:47
@sphuber
Copy link
Contributor Author

sphuber commented Mar 9, 2022

@chrisjsewell pre-commit now passes. Should be good to go

Copy link
Member

@chrisjsewell chrisjsewell left a comment

Choose a reason for hiding this comment

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

Cheers

def first(self, flat: Literal[True]) -> Optional[Any]:
...

def first(self, flat: bool = False) -> Optional[list[Any] | Any]:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def first(self, flat: bool = False) -> Optional[list[Any] | Any]:
def first(self, flat: bool = False) -> None | list[Any] | Any:

Will approve anyway, but you are kind of mixing old and new type annotations in these changes

@sphuber sphuber merged commit 5b10cd3 into aiidateam:develop Mar 9, 2022
@sphuber sphuber deleted the fix/5408/query-builder-first-flat branch March 9, 2022 22:18
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.

Add the flat keyword to QueryBuilder.first
2 participants