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

Typing SocketType, test_socket, test_highlevel_[socket, open_tcp_stream, open_tcp_listeners] #2774

Merged
merged 25 commits into from
Sep 29, 2023

Conversation

jakkdl
Copy link
Member

@jakkdl jakkdl commented Aug 25, 2023

Resolves #2720

Quite messy, and looks like there's some signatures in _socket.py that might warrant changing. Will write a summary when I've finished up test_highlevel_open_tcp_listeners

TODO:

  • test generic-ness raises type errors
  • document how users might want to approach the generic type

@codecov
Copy link

codecov bot commented Aug 25, 2023

Codecov Report

Merging #2774 (a20a909) into master (4ee5413) will increase coverage by 0.12%.
Report is 2 commits behind head on master.
The diff coverage is 100.00%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2774      +/-   ##
==========================================
+ Coverage   98.97%   99.10%   +0.12%     
==========================================
  Files         115      115              
  Lines       17117    17208      +91     
  Branches     3079     3084       +5     
==========================================
+ Hits        16942    17054     +112     
+ Misses        121      106      -15     
+ Partials       54       48       -6     
Files Coverage Δ
trio/_abc.py 100.00% <ø> (ø)
trio/_core/_io_epoll.py 100.00% <100.00%> (ø)
trio/_core/_io_kqueue.py 87.20% <100.00%> (+1.48%) ⬆️
trio/_dtls.py 96.60% <100.00%> (ø)
trio/_highlevel_open_tcp_listeners.py 100.00% <ø> (ø)
trio/_highlevel_open_tcp_stream.py 97.64% <100.00%> (ø)
trio/_highlevel_socket.py 100.00% <ø> (ø)
trio/_socket.py 100.00% <100.00%> (ø)
trio/_tests/test_highlevel_open_tcp_listeners.py 98.46% <100.00%> (+0.13%) ⬆️
trio/_tests/test_highlevel_open_tcp_stream.py 100.00% <100.00%> (ø)
... and 5 more

... and 1 file with indirect coverage changes

@A5rocks A5rocks added the typing Adding static types to trio's interface label Aug 26, 2023
@jakkdl jakkdl mentioned this pull request Aug 26, 2023
@jakkdl jakkdl marked this pull request as ready for review August 29, 2023 14:17
@jakkdl
Copy link
Member Author

jakkdl commented Aug 29, 2023

Hm, looking at https://docs.python.org/3/library/socket.html#socket-families I now understand why methods like recv have Any in their return type when they're returning a socket address. Might warrant changing a couple signatures to use Any and/or object

@jakkdl jakkdl changed the title [WIP] Typing SocketType, test_socket, test_highlevel_[socket, open_tcp_stream, open_tcp_listeners] Typing SocketType, test_socket, test_highlevel_[socket, open_tcp_stream, open_tcp_listeners] Aug 29, 2023
@jakkdl jakkdl requested review from A5rocks, CoolCat467 and TeamSpen210 and removed request for CoolCat467 August 29, 2023 14:56
Copy link
Member

@CoolCat467 CoolCat467 left a comment

Choose a reason for hiding this comment

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

Looks good other than a few formatting things and a question about Any vs object usage. I also think it would be good to annotate the __init__ return types while we are at it so we don't have to do it later.

trio/_highlevel_open_tcp_stream.py Outdated Show resolved Hide resolved
trio/_socket.py Outdated Show resolved Hide resolved
@jakkdl
Copy link
Member Author

jakkdl commented Sep 2, 2023

I also think it would be good to annotate the __init__ return types while we are at it so we don't have to do it later.

I have repeatedly asked you why you think this, while giving you explanations why it doesn't. It does not matter for pyright --verifytypes and it does not matter for mypy. Would you please elaborate, and show it with a reproducible sample + program invocation, so we can settle this?
They don't hurt in any way, but I don't see any reason to add them, and I have yet to find any check that can be turned on in any type-checker that flags either, when you have parameters that are typed.

@jakkdl jakkdl requested a review from CoolCat467 September 16, 2023 10:07
Copy link
Member

@CoolCat467 CoolCat467 left a comment

Choose a reason for hiding this comment

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

Looks good to me, but I would replace the Address -> Any change with Address -> AddressFormat alias. One of the review comments has more details why I think this should be done.

trio/socket.py Show resolved Hide resolved
trio/_highlevel_open_tcp_stream.py Show resolved Hide resolved
trio/_highlevel_open_tcp_stream.py Show resolved Hide resolved
trio/_dtls.py Show resolved Hide resolved
trio/_dtls.py Show resolved Hide resolved
trio/_dtls.py Show resolved Hide resolved
trio/_dtls.py Show resolved Hide resolved
trio/_dtls.py Show resolved Hide resolved
trio/_dtls.py Show resolved Hide resolved
trio/_tests/test_highlevel_open_tcp_stream.py Outdated Show resolved Hide resolved
trio/testing/_fake_net.py Outdated Show resolved Hide resolved
Copy link
Member Author

@jakkdl jakkdl left a comment

Choose a reason for hiding this comment

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

uh, turns out I had a very old review-in-progress and it's a mess to find where I commented on what, so I'll submit this now and it might be outdated.

docs/source/reference-io.rst Outdated Show resolved Hide resolved
Comment on lines 211 to 220
@abstractmethod
def socket(
self,
family: socket.AddressFamily | int | None = None,
type: socket.SocketKind | int | None = None,
proto: int | None = None,
) -> _SocketType:
family: socket.AddressFamily | int = socket.AF_INET,
type: socket.SocketKind | int = socket.SOCK_STREAM,
proto: int = 0,
) -> SocketType:
"""Create and return a socket object.

Your socket object must inherit from :class:`trio.socket.SocketType`,
Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't want these types to be | None, as that affects the signature of all subclasses of SocketFactory. But this is a change in behaviour, so it might be better to just do a type: ignore

Copy link
Member

Choose a reason for hiding this comment

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

I think having them default to the values they would have became originally is a good 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 is fine. I approach type hints as not bound by semver (i.e. can narrow them or expand them as long as we're not passing something with a new type in) and subclasses can still default to None. Hopefully that approach is sane...

Copy link
Member Author

Choose a reason for hiding this comment

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

the issue is not the type hint, but the default value being changed from None -> socket.AF_INET etc etc.
although thinking about it a bit more, I'm having trouble coming up with any code that actually changes in behaviour from this. If you're subclassing SocketFactory and defining your own SocketFactory.socket, the default values in SocketFactory.socket shouldn't modify the behaviour of MyClass.socket in any way .. I think?

trio/_socket.py Outdated Show resolved Hide resolved
@jakkdl
Copy link
Member Author

jakkdl commented Sep 20, 2023

I also think it would be good to annotate the __init__ return types while we are at it so we don't have to do it later.

I have repeatedly asked you why you think this, while giving you explanations why it doesn't. It does not matter for pyright --verifytypes and it does not matter for mypy. Would you please elaborate, and show it with a reproducible sample + program invocation, so we can settle this? They don't hurt in any way, but I don't see any reason to add them, and I have yet to find any check that can be turned on in any type-checker that flags either, when you have parameters that are typed.

@CoolCat467

@A5rocks
Copy link
Contributor

A5rocks commented Sep 20, 2023

(It might just be cause I haven't fully woken up yet, but quoted message feels a little bit too aggressive... it's alright and can stay since it's obviously not meant to be such: just for CoolCat, don't take it in an aggressive way ^^)

I'll make sure to actually review this in a while!! Sorry about the delay.

Copy link
Member

@CoolCat467 CoolCat467 left a comment

Choose a reason for hiding this comment

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

With the issues with AddressFormat explained, I think this looks good!

@CoolCat467
Copy link
Member

I also think it would be good to annotate the __init__ return types while we are at it so we don't have to do it later.

I have repeatedly asked you why you think this, while giving you explanations why it doesn't. It does not matter for pyright --verifytypes and it does not matter for mypy. Would you please elaborate, and show it with a reproducible sample + program invocation, so we can settle this? They don't hurt in any way, but I don't see any reason to add them, and I have yet to find any check that can be turned on in any type-checker that flags either, when you have parameters that are typed.

@CoolCat467

# example.py

class ClassName:
    def __init__(self):
        pass
dmypy run "example.py" -- --disallow-untyped-defs

yields the following:

Daemon started
example.py:4: error: Function is missing a return type annotation  [no-untyped-def]
example.py:4: note: Use "-> None" if function does not return a value
Found 1 error in 1 file (checked 40 source files)

@TeamSpen210
Copy link
Contributor

That's a special case where -> None must be used, since the __init__() method has no parameters defined at all other than self. Try def __init__(self, a: int, b: str): and you'll see there should be no complaints. Alternatively perhaps it's a problem with daemon mode?

@CoolCat467
Copy link
Member

That's a special case where -> None must be used, since the __init__() method has no parameters defined at all other than self. Try def __init__(self, a: int, b: str): and you'll see there should be no complaints. Alternatively perhaps it's a problem with daemon mode?

It indeed does not raise errors when it has arguments in dmypy 1.5.1, but I feel like this was not always the case. I always make sure it has the return type annotated mostly because I don't like the idea of special cases. Everything else has the return types annotated, so why shouldn't this? And plus, then any code that looks at the __annotations__ of it has to have special exceptions, and things aren't nearly as clean.

Confirming this is one of the comments in PEP-484:

(Note that the return type of __init__ ought to be annotated with -> None. The reason for this is subtle. If __init__ assumed a return annotation of -> None, would that mean that an argument-less, un-annotated __init__ method should still be type-checked? Rather than leaving this ambiguous or introducing an exception to the exception, we simply say that __init__ ought to have a return annotation; the default behavior is thus the same as for other methods.)

@jakkdl
Copy link
Member Author

jakkdl commented Sep 21, 2023

I am sorry for the overly aggressive message, but I did get a bit frustrated about it having come up several times :) 1 2 3 4

pyright:
https://github.com/microsoft/pyright/blob/main/docs/typed-libraries.md#type-completeness

Type annotations can be omitted in a few specific cases where the type is obvious from the context:
...
The return type for an __init__ method does not need to be specified, since it is always None.

mypy:

https://mypy.readthedocs.io/en/stable/class_basics.html#annotating-init-methods

The __init__ method is somewhat special – it doesn’t return a value. This is best expressed as -> None. However, since many feel this is redundant, it is allowed to omit the return type declaration on __init__ methods if at least one argument is annotated.

The comment in PEP-484 is specifically about "whether a type checker should check the function, or treat it as an untyped function and ignore the content", and not any confusion about what the return type actually is.
This is the norm in the most widely used type checkers, and in all projects I know of (as it isn't even possible to enable a warning for not having it!) so anybody inspecting __annotations__ for whatever reason would have to special-case this behavior anyway.

(returning non-None will also raise a TypeError: https://docs.python.org/3/reference/datamodel.html?highlight=init#object.__init__ )

Copy link
Contributor

@A5rocks A5rocks left a comment

Choose a reason for hiding this comment

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

I didn't review the tests yet but generally I'm just gonna trust you're doing the right thing 😅

Comment on lines 211 to 220
@abstractmethod
def socket(
self,
family: socket.AddressFamily | int | None = None,
type: socket.SocketKind | int | None = None,
proto: int | None = None,
) -> _SocketType:
family: socket.AddressFamily | int = socket.AF_INET,
type: socket.SocketKind | int = socket.SOCK_STREAM,
proto: int = 0,
) -> SocketType:
"""Create and return a socket object.

Your socket object must inherit from :class:`trio.socket.SocketType`,
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 is fine. I approach type hints as not bound by semver (i.e. can narrow them or expand them as long as we're not passing something with a new type in) and subclasses can still default to None. Hopefully that approach is sane...

trio/_core/_io_kqueue.py Show resolved Hide resolved
trio/_socket.py Outdated Show resolved Hide resolved
trio/_socket.py Show resolved Hide resolved
not TYPE_CHECKING and hasattr(_stdlib_socket.socket, "share")
):

def share(self, /, process_id: int) -> bytes:
Copy link
Contributor

Choose a reason for hiding this comment

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

This coverage warning here also doesn't make sense given we're running the tests on Windows...?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah no clue :<

Copy link
Member Author

Choose a reason for hiding this comment

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

on a rerun there were no coverage warning, so seems like a temporary glitch.

trio/_socket.py Show resolved Hide resolved
trio/_socket.py Show resolved Hide resolved
trio/_socket.py Show resolved Hide resolved
@jakkdl
Copy link
Member Author

jakkdl commented Sep 26, 2023

uh, thought for a minute that _public could be skipped in coverage ... but since the implementation should get called by the generated files that's certainly not the case.

@jakkdl jakkdl merged commit 24226c3 into python-trio:master Sep 29, 2023
@jakkdl jakkdl deleted the typing_sockettype branch September 29, 2023 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typing Adding static types to trio's interface
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Handling _SocketType and SocketType
4 participants