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

QF refactor and mypy cleanup #635

Merged
merged 4 commits into from
Jul 18, 2023
Merged

QF refactor and mypy cleanup #635

merged 4 commits into from
Jul 18, 2023

Conversation

Nathanjp91
Copy link
Contributor

Problem

QueryFilter objects are a bit unweildy atm, having to call them with a quite verbose dictionary like syntax

Solution

Added in ability to create filter objects from kwargs while keeping existing functionality, also supports modifier instantiation using 'modifier:value' syntax

QueryFilter._from_args(test='test1') # QueryFilter(name='test', param='test1')
QueryFilter._from_args(test='>=:6') # QueryFilter(name='test', param='6', modifier='>=')

Also cleaned up Query code further to handle generalized and inherited where(*args, **kwargs) that creates these filter objects and doesn't require overwriting in meta query code.

Changelog

Streamlined creation and usage of QueryFilters

@@ -24,6 +24,7 @@ def from_api_key(cls, api_key: str, datasets_dir: Optional[Path] = None) -> Meta
config = DarwinConfig.from_api_key_with_defaults(api_key=api_key)
client = Client(config) # create a temporary client to get the default team
token_info = client.get("/users/token_info")
assert isinstance(token_info, dict)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

overcomes a mypy typing thing

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this would've also done it:

token_info = client.get("/users/token_info", dict())

but fair play.

filters = QueryFilter._from_args(*args, **kwargs)
for item in filters:
self += item
return self
Copy link
Contributor Author

@Nathanjp91 Nathanjp91 Jul 14, 2023

Choose a reason for hiding this comment

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

__add__ function now calls the underlying self.__class__ constructor and passes through all the necessary values, so no longer any need to overwrite where in specific implementations

Copy link
Contributor

Choose a reason for hiding this comment

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

Because if there are filters the class instance has become the class in question?

I don't get how this implements our edge cases though, which override where?

Copy link
Contributor Author

@Nathanjp91 Nathanjp91 Jul 17, 2023

Choose a reason for hiding this comment

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

So the problem was that for specific objects, like DatasetQuery, where(...) needs to call the constructor of DatasetQuery rather than cls.init() for mypy typing, all of our implementations basically just looked like

class DatasetQuery(Query[DatasetMeta]):
    def where(self, ...) -> Query[T]
        filter = QueryFilter(...)
        return DatasetQuery(filter)
        

but using the self.__class__() in the __add__() (or anywhere else we want to implement this), that satisfies mypy's type restraint on the return, while removing a chunk of code we have to write for every query.

Copy link
Contributor

@owencjones owencjones left a comment

Choose a reason for hiding this comment

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

Yeah, looks good - would be good to go over the points I've raised because they're largely points I could do with reviewing.

return QueryFilter(name=d["name"], param=str(d["param"]), modifier=modifier)

@classmethod
def _from_args(cls, *args: object, **kwargs: str) -> List[QueryFilter]:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this typing will cause issues in Mypy with calling the function, but using *args: str, **kwargs: int doesn't IIRC.

If I've got this right, the issue with this one, is that invocation will currently work within the function itself, but running something like:

QueryFilter._from_args(c="a", f=2)

will lead to a complaint from mypy that c and f aren't objects.

Ignore if this is wrong!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so object overcomes the mypy typing constraints as everything in python is an object, for mypy it's basically Any but when you actually mean "I accept anything" and not just "I cbf typing this input".

Interestingly, while researching typing for mypy and args/kwargs functions, object and Any are weird, inherited objects of each other. It's a strange, one off relationship that python defines specifically.

In terms of for this though, the intention was that I could provide an array, as well as a dict, but I actually just settled on checking for dict and didn't replace this typing. I could make it stricter and replace with

def _from_args(cls, *args: Dict[str, object], **kwargs: str) -> List[QueryFilter]

In your example though the f=2 would cause a typing issue as I've defined only str. There's scope here to allow object instead and then cast to str for the QueryFilter and I'm not fussed, I think explicitly saying str here though does give better type hint about the intention as it's basically signalling that you need to be able to cast it as a str for the filtering object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

filters = QueryFilter._from_args(*args, **kwargs)
for item in filters:
self += item
return self
Copy link
Contributor

Choose a reason for hiding this comment

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

Because if there are filters the class instance has become the class in question?

I don't get how this implements our edge cases though, which override where?

@@ -24,6 +24,7 @@ def from_api_key(cls, api_key: str, datasets_dir: Optional[Path] = None) -> Meta
config = DarwinConfig.from_api_key_with_defaults(api_key=api_key)
client = Client(config) # create a temporary client to get the default team
token_info = client.get("/users/token_info")
assert isinstance(token_info, dict)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this would've also done it:

token_info = client.get("/users/token_info", dict())

but fair play.

@Nathanjp91 Nathanjp91 merged commit a07556b into master Jul 18, 2023
9 checks passed
@Nathanjp91 Nathanjp91 deleted the QF_refactor branch November 8, 2023 10:42
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.

2 participants