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

Even more mypy type checking #1238

Closed

Conversation

doshitan
Copy link
Contributor

@doshitan doshitan commented Mar 2, 2020

It started out a little more disciplined then became a big ball of changes, apologies.

Also some of this might be overkill.

Included the stricter mypy config that this passes for reference, but it's maybe we don't want to require it yet.

Running mypy (with stricter config):
image

The mypy report part errors out early, but what it does get to:

+--------------------------+-------------------+----------+
| Module                   | Imprecision       | Lines    |
+--------------------------+-------------------+----------+
| nixops                   |   0.00% imprecise |    0 LOC |
| nixops.ansi              |   0.00% imprecise |   18 LOC |
| nixops.diff              |  15.17% imprecise |  211 LOC |
| nixops.known_hosts       |  11.43% imprecise |   70 LOC |
| nixops.logger            |   0.00% imprecise |  158 LOC |
| nixops.nix_expr          |  27.53% imprecise |  385 LOC |
| nixops.parallel          |   9.48% imprecise |  116 LOC |
| nixops.plugins           |  58.33% imprecise |   12 LOC |
| nixops.plugins.hookspecs |  15.38% imprecise |   26 LOC |
| nixops.resources         |  15.31% imprecise |  320 LOC |
| nixops.script_defs       |  28.99% imprecise | 1128 LOC |
| nixops.ssh_util          |   5.62% imprecise |  409 LOC |
| nixops.state             |  18.07% imprecise |   83 LOC |
| nixops.util              |  10.83% imprecise |  600 LOC |
+--------------------------+-------------------+----------+
| Total                    |  18.30% imprecise | 3536 LOC |
+--------------------------+-------------------+----------+

Tests:
image

@grahamc saw your tweet about working on this and having some experience hitting python over the head with mypy, figured I'd make a pass (^_^)

@grahamc
Copy link
Member

grahamc commented Mar 2, 2020

This is amazing! There are some module import errors which I can help track down and fix. Thank you!

Comment on lines +43 to +45
) -> FinalResult[Result]:
task_queue: queue.Queue[Task] = queue.Queue()
result_queue: queue.Queue[WorkerResult] = queue.Queue()
result_queue: queue.Queue[WorkerResult[Result]] = queue.Queue()
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit surprised -- is FinalResult / WorkerResult a generic type?

Copy link
Contributor

Choose a reason for hiding this comment

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

WorkerResult is defined a little further up, it's basically an Either from Haskell, or std::variant from C++.

nixops/logger.py Outdated Show resolved Hide resolved
Comment on lines +186 to +202
@overload
def _enc_str(node: str, for_attribute: Literal[True]) -> str:
...

@overload
def _enc_str(
node: str, for_attribute: Literal[False]
) -> Union[RawValue, Container]:
...

@overload
def _enc_str(node: str) -> Union[RawValue, Container]:
...

def _enc_str(
node: str, for_attribute: bool = False
) -> Union[str, RawValue, Container]:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious to try this overall PR without the Literal import, given that it's only used in two places. If that works, we can merge the rest and make this it's own PR and figure out if/how python does dependency management.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, I'd kinda prefer separate functions with it clear in the name what they do/return, rather than relying on a special parameter, but the pattern was present in a variety of places and I don't know if there are calls to this stuff (particularly the logged_exec calls/wrappers) outside of this code base, so was hesitant to change function names.

If we can refactor and do away with all the Literal junk without also adding a bunch of casts, I think that'd be a win.

grahamc added a commit to grahamc/nixops that referenced this pull request Mar 2, 2020
An alternative implementation of NixOS#1238 from GitHub user doshitan which
used Literal[...] as a sort of dependent type:

    +    @overload
    +    def run_command(
    +        self,
    +        command: Command,
    +        flags: List[str] = ...,
    +        timeout: Optional[int] = ...,
    +        logged: bool = ...,
    +        allow_ssh_args: bool = ...,
    +        user: Optional[str] = ...,
    +        *,
    +        capture_stdout: Literal[False],
    +        **kwargs: Any
    +    ) -> int:
    +        ...
    +
    +    @overload
    +    def run_command(
    +        self,
    +        command: Command,
    +        flags: List[str] = ...,
    +        timeout: Optional[int] = ...,
    +        logged: bool = ...,
    +        allow_ssh_args: bool = ...,
    +        user: Optional[str] = ...,
    +        *,
    +        capture_stdout: Literal[True],
    +        **kwargs: Any
    +    ) -> str:
    +        ...
    +
    +    @overload
    +    def run_command(
    +        self,
    +        command: Command,
    +        flags: List[str] = [],
    +        timeout: Optional[int] = None,
    +        logged: bool = True,
    +        allow_ssh_args: bool = False,
    +        user: Optional[str] = None,
    +        **kwargs: Any
    +    ) -> Union[str, int]:
    +        ...

however, I think this option is much simpler and improves the API.
Possibly in the future we will add this more complex type.
grahamc added a commit that referenced this pull request Mar 2, 2020
An alternative implementation of #1238 from GitHub user doshitan which
used Literal[...] as a sort of dependent type:

    +    @overload
    +    def run_command(
    +        self,
    +        command: Command,
    +        flags: List[str] = ...,
    +        timeout: Optional[int] = ...,
    +        logged: bool = ...,
    +        allow_ssh_args: bool = ...,
    +        user: Optional[str] = ...,
    +        *,
    +        capture_stdout: Literal[False],
    +        **kwargs: Any
    +    ) -> int:
    +        ...
    +
    +    @overload
    +    def run_command(
    +        self,
    +        command: Command,
    +        flags: List[str] = ...,
    +        timeout: Optional[int] = ...,
    +        logged: bool = ...,
    +        allow_ssh_args: bool = ...,
    +        user: Optional[str] = ...,
    +        *,
    +        capture_stdout: Literal[True],
    +        **kwargs: Any
    +    ) -> str:
    +        ...
    +
    +    @overload
    +    def run_command(
    +        self,
    +        command: Command,
    +        flags: List[str] = [],
    +        timeout: Optional[int] = None,
    +        logged: bool = True,
    +        allow_ssh_args: bool = False,
    +        user: Optional[str] = None,
    +        **kwargs: Any
    +    ) -> Union[str, int]:
    +        ...

however, I think this option is much simpler and improves the API.
Possibly in the future we will add this more complex type.
Copy link
Member

@grahamc grahamc left a comment

Choose a reason for hiding this comment

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

There is definitely good stuff in here, though it became a bit tricky to merge the later commits, for sure. I picked out some of the easy-to-check, easy to break-out changes and merged those. I'm hoping to merge more from this PR, too.

return " " * level + self.value

def __repr__(self):
def __repr__(self) -> str:
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit unsure about this -- we say we're returning a str, but above we accept an Any in the constructor. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding of Python is that __repr__ should always return a string (and typeshed seems to agree). But yea, strange that it just returns the raw value and I don't know if the behavior is relied on somewhere :/

It'll take more types to handle things like `{("a", "b"): 1, "a": 2}`
properly, for now, loosen types/go back to more like it was before.
For Literal support in Python 3.7 (though we could also just drop usage of Literal)
…start-with-types

* origin/start-with-types:
  ssh_util: typed run_command wrapper
  ssh_util.py
  logger.py
  nixops: make the op required
@doshitan
Copy link
Contributor Author

doshitan commented Mar 3, 2020

Welp, down to just one coverage test error (due to more import cycle shenanigans).

I don't know why this was done back in 2015 or how it's used, but it
fixes the coverage tests locally.
grahamc added a commit that referenced this pull request Mar 9, 2020
@grahamc
Copy link
Member

grahamc commented Apr 15, 2020

I'm remiss to close this, but it is a bit out of date from master and the remaining commits are tricky to merge. Maybe the work here could be broken up in to a few smaller and related chunks?

@doshitan
Copy link
Contributor Author

Yea, I've not had a chance to get back to this lately, if you've already lifted what useful bits you could then feel free to close! I may have time this weekend to rebase/pick out any standalone chunks, but can't promise anything.

@grahamc grahamc closed this Apr 20, 2020
@grahamc
Copy link
Member

grahamc commented Apr 20, 2020

If you rebase, go ahead and reopen? Thank you!

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.

None yet

3 participants