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

Inherit type signature from another method #2003

Closed
rowillia opened this issue Aug 9, 2016 · 8 comments
Closed

Inherit type signature from another method #2003

rowillia opened this issue Aug 9, 2016 · 8 comments

Comments

@rowillia
Copy link
Contributor

rowillia commented Aug 9, 2016

Is there a way to inherit a type signature from another method? The most common use case for this is when we've got some convenience method like requests.post

Below is a super simple version of https://github.com/kennethreitz/requests/blob/master/requests/api.py#L99 which eventually calls down into https://github.com/kennethreitz/requests/blob/master/requests/sessions.py#L397

def request(method: str, url: str, data: Optional[Dict[str, Any]]=None, headers: Optional[Dict[str, str]]=None) -> Response:
  pass

def post(url: str, **kwargs) -> Response:
  # Would like to be able to annotate that **kwargs should inherit from `request`
  return request('post', url, **kwargs)

post('http://foo.com', headers={'timeout': 32})  # Should fail.

Ideally I wouldn't have to copy and paste the type signature of request into post.

@rowillia rowillia changed the title Inherit type signature for pass through methods? Inherit type signature from another method Aug 9, 2016
@gnprice
Copy link
Collaborator

gnprice commented Aug 10, 2016

That'd be a useful thing to be able to do! I don't think there's a good way to do it right now, or in the existing PEP 484 type system. But this has a lot in common with the discussion of keyword arguments in python/typing#239 , in the context of higher-order functions. Do you think you'd be able to use the proposal there to do what you want?

@gnprice gnprice added this to the Questions milestone Aug 10, 2016
@JukkaL
Copy link
Collaborator

JukkaL commented Aug 10, 2016

None of the proposed approaches seem to quite do the right thing. I've thought about this previously, and if we had types for "dicts as structs" we could perhaps use those for **kwargs. However, this would require multiple PEP 484 changes:

  1. We'd need a way of defining "dict as struct" types and specify how they work in general. This sounds tricky and might not be worth the complexity. However, this would be useful also for things like JSON objects.
  2. The type for **kwargs is currently the value type for the dictionary. We'd need to change that somehow to allow specifying the type of the dictionary object instead.
  3. Potentially we'd also need a way of deriving a "dict as struct" type from a function signature. Otherwise, you'd have to use **kwargs in all instances to avoid duplicating the signature, and this would change semantics as you'd lose the option of using arguments positionally. Without this, we'd have to refactor the original example to use **kwargs in the definition of request, which looks like a pain if there are many keyword arguments.

@rowillia
Copy link
Contributor Author

rowillia commented Aug 10, 2016

@sixolet's proposal seems reasonable, but I think this is slightly different. In this case, we actually need to do some merging of arguments. For example, it'd also be totally valid for me to do this:

def request(method: str, url: str, data: Optional[Dict[str, Any]]=None, headers: Optional[Dict[str, str]]=None) -> Response:
  pass

def post(data: Optional[Dict[str, Any]]=None, *args, **kwargs) -> Response:
  return request('post', data=data, *args,**kwargs)

My ideal would be to have an annotation, something akin to the existing @overload

def request(method: str, url: str, data: Optional[Dict[str, str]]=None, headers: Optional[Dict[str, str]]=None) -> Response:
  pass

@inherit_signature(request)
def post(data: Optional[Dict[str, Any]]=None, *args, **kwargs) -> Response:
  new_data = {k: str(v) for k, v in data.items()}
  return request('post', data=new_data, *args,**kwargs)

post()  # Missing required parameter `url`
post('foo', headers={'abc': 123'})  # Wrong type for headers
post('foo') # OK
post('foo', data={'abc': 123}) # OK
post('foo', bad_param=roflcopter) # Unexpected parameter `bad_param`

And in this case *args and **kwargs without data would be inherited from request

@rowillia
Copy link
Contributor Author

@JukkaL For "Dict as struct" types take a look at Shapes from Hack - https://docs.hhvm.com/hack/shapes/introduction

@sixolet
Copy link
Collaborator

sixolet commented Aug 11, 2016

post seems a partial evaluation of request with the method fixed, and all the other arguments passed through.

With my proposal you can do something like this:

T = TypeVar('T')
R = TypeVar('R')
def curry(fix: T, f: Callable[[T, OtherArgs], R]) -> Callable[[OtherArgs], R]:
    def inner(*args, **kwargs):
        return f(fix, *args, **kwargs)
    return inner

def request(...): ... # Like you defined it

post = curry('post', request)

I still have no good answer about typechecking the body of curry without eliding any information about the signatures or making some kind of implicit casts.

I understand that this isn't what you asked for -- you asked for a way to make an explicit definition of post less tediously, and I wrote down a way to throw it together as a result of some higher-order functions.

@sixolet
Copy link
Collaborator

sixolet commented Aug 11, 2016

Oh, I think I did not understand your concept of "merging" -- post is not only currying in the method argument, but also transforming the data argument. I suppose you could have more higher-order functions to do that, too, but that's getting tedious.

@JukkaL
Copy link
Collaborator

JukkaL commented Aug 11, 2016

@rowillia Thanks for the link. I was aware that Hack has something like that but didn't remember the details.

@rowillia
Copy link
Contributor Author

@sixolet Currying would be a good start, but not sufficient. For example - https://github.com/kennethreitz/requests/blob/master/requests/api.py#L55-L56

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

No branches or pull requests

5 participants