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

MyPy errors when using sync objects #491

Open
florianvazelle opened this issue Sep 11, 2024 · 4 comments
Open

MyPy errors when using sync objects #491

florianvazelle opened this issue Sep 11, 2024 · 4 comments
Labels
bug Something isn't working kr8s typing

Comments

@florianvazelle
Copy link
Contributor

Which project are you reporting a bug for?

kr8s

What happened?

It seems that MyPy continues to interpret methods as asynchronous for sync objects, even though at runtime they have been converted to synchronous methods with the sync decorator. This leads to MyPy errors like:

$ mypy test.py 
test.py:3: error: Value of type "Coroutine[Any, Any, APIObject | list[APIObject]]" must be used  [unused-coroutine]
test.py:3: note: Are you missing an await?

with code like:

# test.py
from kr8s.objects import Pod
Pod.list()

Anything else?

No response

@florianvazelle florianvazelle added the bug Something isn't working label Sep 11, 2024
@jacobtomlinson
Copy link
Member

Yeah I can reproduce that. I also see a similar error of I try and assign to a typed variable.

from kr8s.objects import Pod

pods: list[Pod] = Pod.list()
$ mypy test.py
test.py:3: error: Incompatible types in assignment (expression has type "Coroutine[Any, Any, APIObject | list[APIObject]]", variable has type "list[Pod]")  [assignment]
Found 1 error in 1 file (checked 1 source file)

I think there are two potential solutions here:

  • Find some way to tell mypy that the @sync wrapper changes the type
  • Provide a .pyi stub file for each sync API submodule to override the typing
    • I would want to find a way to add tests to ensure this stays in sync. Otherwise we just introduce a new class of bug where the stubs get out of sync with the code. I had a quick play around with stubgen and stubtest but I'm not sure if they will run into similar issues as mypy around incorrectly identifying the signatures of wrapped classes.

@jacobtomlinson
Copy link
Member

jacobtomlinson commented Sep 11, 2024

A third option could be to do away with the @sync wrapper and manually create each method and use run_sync() directly. This is what we do for functions like kr8s.get() today.

# kr8s/objects.py
from kr8s._objects import Pod as _Pod
from kr8s._async_utils import run_sync

class Pod(_Pod):
    """..."""

    ...

    @classmethod
    def list(cls, **kwargs) -> APIObject | list[APIObject]:
        """List objects in Kubernetes.

        Args:
            **kwargs: Keyword arguments to pass to :func:`kr8s.get`.

        Returns:
            A list of objects.
        """
        return run_sync(super().list)(**kwargs)

    ...

The upsides of this are:

  • Static analysers like mypy would have an easier time parsing the code
  • We could do away with the sphinx-autoapi plugin

But the downsides are:

  • We would need to duplicate all methods, including inherited methods from the base class
  • We would need to duplicate docstrings
  • This would potentially add thousands of lines of boilerplate code
  • We would need to find a way to keep everything in sync

@florianvazelle
Copy link
Contributor Author

I've tried several approaches, but none seem to work for the first option.

The third option is also problematic because:

  • MyPy doesn't allow overriding an async method with a sync one,
  • The best return type would be list[Pod], but super().list actually returns an async APIObject.

There doesn't seem to be an easy fix

@jacobtomlinson
Copy link
Member

jacobtomlinson commented Sep 16, 2024

Thanks for taking the time to dig into this.

I did think more about option 1 and I guess it's problematic because even if we write a plugin specifically for kr8s, all code that depends on kr8s would need the plugin, which is not something we can ask users to do.

I agree for option 3 it would be best to return list[Pod]. We could add a runtime assertion to make mypy happy and narrow the type. But as you say if you can't override async methods with sync ones then this option is not viable.

For option 2 I wonder if we need some custom stub generation script that we can invoke as part of the pre-commit hooks. This way we can explicitly tell mypy what to expect, but we don't need to worry about drift if they are auto-generated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working kr8s typing
Projects
None yet
Development

No branches or pull requests

2 participants