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

Add Python type annotation for base Compute API #1306

Merged
merged 22 commits into from
Nov 24, 2019

Conversation

Kami
Copy link
Member

@Kami Kami commented Jul 9, 2019

The goal of this pull request is to add type annotations to the base APIs in an incremental manner.

Adding type annotations to an existing project is much harder (and takes much more time) compared to adding it as you develop a new project and that's why I will take an incremental approach.

I plan to start with the base APIs and see how it goes. It's likely that those type annotations will require various issues in the driver code which will be hard and time consuming to fix (and that's also the reason why I plan to start with the base APIs).

@Kami
Copy link
Member Author

Kami commented Jul 9, 2019

It turns out this will be quite a challenging and painful task.

Existing type annotations I added already uncovered some issues with sub-classes not handling parent types correctly, etc.

We don't have much choice here, but to use a type annotation work around in such scenarios (supporting multiple types aka Union), because we can't break the API.

In addition to that, it will also be hard to come up with good type annotations for our dynamic get_driver and other factory methods.

@Kami
Copy link
Member Author

Kami commented Jul 10, 2019

In addition to my comment above - I also decided I will start with an API per pull request to make sure the PRs don't grow too large.

In this PR I will work on the compute API and other common code which is needed to get that to work.

As far as type annotations for our dynamically returned provider drivers go - I still haven't been able to figure this out.

We may just need to go with returning a reference to the base NodeDriver. It's not ideal since it won't know about any provider class specific methods and arguments, but I'm not even sure yet if we can correctly annotate our code which does our dynamic driver class loading (we may need a MyPy plugin or it may even not be possible).

@Kami
Copy link
Member Author

Kami commented Jul 10, 2019

@tonybaloney ^ maybe you have an idea if we can get type annotations to work for our dynamically loaded and returned driver classes.

For now, I have this - f5a2016. As mentioned above, it's not ideal since it won't be aware of any provider driver class specific methods and arguments.

I did quite a lot of research and experimentation and I couldn't get it to work (aka annotate get_driver method that it returns a reference to provider driver class which is dependent on the value of provided provider argument constant).

I believe we may only be able to achieve that using a custom MyPy plugin (but I could be wrong).

@Kami Kami changed the title [WIP] Add Python type annotation for base APIs [WIP] Add Python type annotation for base Compute API Jul 10, 2019
@Kami Kami changed the title [WIP] Add Python type annotation for base Compute API Add Python type annotation for base Compute API Jul 18, 2019
@Kami
Copy link
Member Author

Kami commented Jul 19, 2019

This is "mostly" working with limitations mentioned above (get_driver always returns reference to the base driver).

I still need to fix enum handling (I needed to change our custom ENUM type so it works correctly with MyPy), then I can hopefully merge it into trunk.

Kami added 3 commits July 19, 2019 13:40
Those are needed for backward compatibility reasons to ensure it works
with old code which treats types as simple strings.

Update affected code.
@Kami
Copy link
Member Author

Kami commented Jul 19, 2019

I finally managed to get all the tests to pass.

Sadly it wasn't that straightforward. One of the main offenders was our custom "enum" type and making sure it works with type annotations and it's also fully backward compatible. For that, I needed to implement a bunch of "glue" code - 9604610

@codecov-io
Copy link

codecov-io commented Jul 19, 2019

Codecov Report

Merging #1306 into trunk will decrease coverage by <.01%.
The diff coverage is 90.51%.

Impacted file tree graph

@@            Coverage Diff             @@
##            trunk    #1306      +/-   ##
==========================================
- Coverage   86.34%   86.33%   -0.01%     
==========================================
  Files         372      372              
  Lines       76203    76232      +29     
  Branches     6976     6979       +3     
==========================================
+ Hits        65798    65817      +19     
- Misses       7608     7614       +6     
- Partials     2797     2801       +4
Impacted Files Coverage Δ
libcloud/common/brightbox.py 53.65% <ø> (ø) ⬆️
libcloud/common/hostvirtual.py 59.37% <ø> (ø) ⬆️
libcloud/common/gridscale.py 83.92% <ø> (ø) ⬆️
libcloud/storage/drivers/cloudfiles.py 79.45% <ø> (ø) ⬆️
libcloud/compute/providers.py 85.71% <ø> (ø) ⬆️
libcloud/compute/drivers/ec2.py 74.95% <ø> (ø) ⬆️
libcloud/common/liquidweb.py 96.49% <ø> (-0.07%) ⬇️
libcloud/utils/loggingconnection.py 61.84% <ø> (ø) ⬆️
libcloud/common/base.py 90.48% <100%> (ø) ⬆️
libcloud/common/luadns.py 100% <100%> (ø) ⬆️
... and 26 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9f2d8d6...8e25d98. Read the comment docs.

@Kami
Copy link
Member Author

Kami commented Jul 26, 2019

@c-w maybe you have an idea on how (if possible) to handle that in mypy^ - #1306 (comment)

@c-w
Copy link
Member

c-w commented Jul 26, 2019

@Kami I'm not sure this is currently possible in mypy since we only know the type of the returned driver value at runtime based on the provider argument.

The current approach looks good to me and may even encourage more users to stick to the methods defined in the base API (a win in my mind since it'll make code more easily cross-cloud portable). We do lose mypy coverage for any ex_ methods, but if a user has code that relies on those, they can always work around it like this:

driver: MySpecificDriver = get_driver(...)

@Kami
Copy link
Member Author

Kami commented Jul 28, 2019

Thanks.

And yes, I believe what you said should work (I will test it and document it).

I plan to release release a candidate / beta version with those changes (probably after the next release) to make sure it doesn't break anything. Only thing I'm really worried about is the enum changes I made.

@stale
Copy link

stale bot commented Oct 26, 2019

Thanks for contributing to this issue. As it has been 90 days since the last activity, we are automatically marking is as stale. If this issue is not relevant or applicable anymore (problem has been fixed in a new version or similar), please close the issue or let us know so we can close it. On the contrary, if the issue is still relevant, there is nothing you need to do, but if you have any additional details or context which would help us when working on this issue, please include it as a comment to this issue.

@stale stale bot added the stale label Oct 26, 2019
@Kami
Copy link
Member Author

Kami commented Nov 6, 2019

I'll work on a patch release first (2.6.1) and after that's out, merge this into trunk and prepare release candidate / beta releases with those changes for people to test them out.

@stale stale bot removed the stale label Nov 6, 2019
@Kami
Copy link
Member Author

Kami commented Nov 24, 2019

@c-w That's what I came up with - 428c3b6.

For some reason, mypy doesn't recognize driver classes (e.g. EC2NodeDriver to be a subclass of NodeDriver), so the following doesn't work:

EC2 = get_driver(Provider.EC2)  # type: Type[EC2NodeDriver]
Rackspace = get_driver(Provider.RACKSPACE)  # type: Type[RackspaceNodeDriver]

The approach I used is not ideal, but I confirmed it works.

I'm not sure why it doesn't recognize it (I didn't dig in too much yet), I assume it's something to do with scoping.

Might even need to open mypy issue / question about it, but I assume they will come back to us with that our code already relies on too much "magic" and meta programming :D

@Kami Kami merged commit 011e53f into apache:trunk Nov 24, 2019
@Kami
Copy link
Member Author

Kami commented Nov 29, 2019

It turns out this PR inadvertently introduced dependency on requests when running setup.py install and we didn't have any install tests which would have caught that.

I believe I fixed that in 4e42ea3, 5159bab and added a check in 75f07bd.

EDIT: Due to libcloud now also depending on enum and typing package, this needed more work - 21a78a1.

This approach is much safer anyway. We shouldn't have setup.py importing any code from libcloud package which is to be installed...

This should also fix read the docs build.

@Kami
Copy link
Member Author

Kami commented Dec 5, 2019

Just a heads up - the plan is to add py.typed file which declares Python packages includes type annotations once type annotations are included for all the base APIs.

@Kami
Copy link
Member Author

Kami commented Mar 1, 2020

@c-w I just found out today that casting to a driver type doesn't work as expected - python/mypy#3903 :/

MyPy won't inherit type annotations from the base class if subclass overrides the method (it doesn't matter if it preserves the parent method signature).

Pretty much only option we have at this point is to add type annotations to all the subclassed methods.

This is obviously far from ideal :/


I think only reasonable option we have right now is to simply rely on type annotations from the base driver class.

Going forward, we will probably need to use something like this - python/mypy#7548 and also add type annotations to any subclass methods where signature differs from the parent / base class method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants