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

typecheck _socket and _core._local #2705

Merged
merged 23 commits into from
Jul 29, 2023

Conversation

jakkdl
Copy link
Member

@jakkdl jakkdl commented Jul 14, 2023

This one's a bit messier, and I failed to resolve what to do with _SocketType. The stubs in typeshed for socket aren't the cleanest either, but I managed to mess around enough to satisfy pyright --verifytypes stringent requirements of unambigious and specify the actual default value - and I think/hope everything's correct, though no big harm if they aren't.

Typing RunVar with no_value = object() seemed not very possible, but I think this change shouldn't affect behaviour.

I'll do another push to figure out if there's any decent way of resolving autodoc, otherwise I'll just spam nitpick_ignore in these three PRs. _SocketType is the main one here that might merit documenting.

@jakkdl jakkdl requested a review from TeamSpen210 July 14, 2023 16:08
@codecov
Copy link

codecov bot commented Jul 14, 2023

Codecov Report

Merging #2705 (975da8a) into master (2fd005b) will increase coverage by 0.00%.
The diff coverage is 100.00%.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2705   +/-   ##
=======================================
  Coverage   98.89%   98.89%           
=======================================
  Files         113      113           
  Lines       16552    16600   +48     
  Branches     3006     3014    +8     
=======================================
+ Hits        16369    16417   +48     
  Misses        126      126           
  Partials       57       57           
Files Changed Coverage Δ
trio/socket.py 100.00% <ø> (ø)
trio/_core/_local.py 98.24% <100.00%> (+0.13%) ⬆️
trio/_socket.py 100.00% <100.00%> (ø)
trio/_sync.py 100.00% <100.00%> (ø)
trio/_tests/test_socket.py 99.83% <100.00%> (+<0.01%) ⬆️
trio/_threads.py 100.00% <100.00%> (ø)

@Fuyukai
Copy link
Member

Fuyukai commented Jul 14, 2023

Typing _socket makes it look legitimate and not something that needs to be put on a road and stomped in the head repeatedly.

Copy link
Contributor

@TeamSpen210 TeamSpen210 left a comment

Choose a reason for hiding this comment

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

Found a bunch of minor things. _SocketType seems tricky though, we wouldn't want to require users to use that private name for their annotations, but SocketType is empty so you can't do anything with it...

trio/_core/_local.py Outdated Show resolved Hide resolved
trio/_sync.py Outdated Show resolved Hide resolved
trio/_core/_local.py Outdated Show resolved Hide resolved
trio/_threads.py Show resolved Hide resolved
trio/_socket.py Outdated Show resolved Hide resolved
trio/_socket.py Outdated Show resolved Hide resolved
trio/_socket.py Show resolved Hide resolved
trio/_socket.py Outdated
assert len(normed) == 4
# typechecking certainly doesn't like this logic, but given just how broad
# Address is, it's kind of impossible to write the below without type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

This might be possible to type, if you define TypeGuard functions for the two len checks here, then change each branch to just build a whole tuple instead of converting to list (so we preserve the heterogeneity). Or unpack it into four variables.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ooh, I didn't try with TypeGuard. Though I don't think it's terribly important to fix these lines, unless those typeguard functions can be reused elsewhere.

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

oremanj commented Jul 14, 2023

I think the issue with SocketType vs _SocketType is an excellent reason to use a stub file _socket.pyi here. These interfaces don't change often (almost at all, really) so I'm not as concerned about shear between the interface and the implementation.

The other option if you don't like that would be to make SocketType be an ABC with abstractmethods for all the public-facing socket methods. _SocketType has the implementations, but we only use the interface name SocketType in public-facing types.

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

jakkdl commented Jul 16, 2023

I think the issue with SocketType vs _SocketType is an excellent reason to use a stub file _socket.pyi here. These interfaces don't change often (almost at all, really) so I'm not as concerned about shear between the interface and the implementation.

The other option if you don't like that would be to make SocketType be an ABC with abstractmethods for all the public-facing socket methods. _SocketType has the implementations, but we only use the interface name SocketType in public-facing types.

I think I'll just leave _SocketType as is for this PR, and open an issue discussing possible solutions and then a PR dedicated for that. Making SocketType abstract with a bunch of abstract methods will probably break downstream code that inherits from it and just happens not to implement one of the methods made abstract.

trio/_socket.py Show resolved Hide resolved
@A5rocks A5rocks added the typing Adding static types to trio's interface label Jul 18, 2023
@A5rocks A5rocks mentioned this pull request Jul 18, 2023
@jakkdl jakkdl requested review from TeamSpen210 and A5rocks July 20, 2023 10:32
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 look too much but this should be most of my comments.

"""Sets the value of this :class:`RunVar` for this current run
call.

"""
try:
old_value = self.get()
except LookupError:
token = _RunVarToken.empty(self)
token = RunVarToken[T]._empty(self)
Copy link
Contributor

Choose a reason for hiding this comment

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

Huh, I feel like mypy should infer T.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it doesn't backtrack from return token to determine token: RunVarToken[T]. Might be better to just add a variable annotation before the try block for it, then it should be able to infer.

Copy link
Member Author

Choose a reason for hiding this comment

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

Something something inheritance seems to break the typevar (or maybe because one is trio._socket.T and one is trio._util.T), so line 72 needs an explicit T (otherwise I get Incompatible types in assignment (expression has type "RunVarToken[T]", variable has type "RunVarToken[T]")) and a variable annotation before the try doesn't help. But I could remove the one at line 70.

trio/_socket.py Outdated Show resolved Hide resolved
trio/_socket.py Outdated
@@ -178,9 +225,10 @@ def numeric_only_failure(exc):
# idna.encode will error out if the hostname has Capital Letters
# in it; with uts46=True it will lowercase them instead.
host = _idna.encode(host, uts46=True)
hr = _resolver.get(None)
hr: HostnameResolver | None = _resolver.get(None)
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 these type hints should also be inferred?

Copy link
Member Author

Choose a reason for hiding this comment

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

yep, removing it has no effect. Though idk if having redundant explicit type hints is a bad thing.

trio/_socket.py Show resolved Hide resolved
trio/_socket.py Outdated Show resolved Hide resolved
trio/_socket.py Outdated
return self._sock.getsockname()

@overload
def getsockopt(self, __level: int, __optname: int) -> int:
Copy link
Contributor

Choose a reason for hiding this comment

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

We dropped support for 3.7

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'm not sure what you're referring to here, I'm not finding what's changed and this is what's in typeshed.

Copy link
Contributor

Choose a reason for hiding this comment

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

3.8 introduced / pos arguments, so we can use that instead of the __arg hack.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed it in a bunch of places, but I failed to remove them from sendto, and from the ones where _make_simple_sock_method_wrapper is used to verify the signature.


################################################################
# sendto
################################################################

@overload
async def sendto(
self, __data: Buffer, __address: tuple[Any, ...] | str | Buffer
Copy link
Contributor

Choose a reason for hiding this comment

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

tuple[Any, ...]...?

Copy link
Member Author

Choose a reason for hiding this comment

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

taken from typeshed, I've been thinking I should improve them upstream.

Copy link
Contributor

Choose a reason for hiding this comment

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

Go ahead, typeshed loves contributions! (If you haven't submitted a PR there, it's an experience: they merge them so quickly!)

@jakkdl jakkdl requested a review from A5rocks July 21, 2023 13:52
@python-trio python-trio deleted a comment from jakkdl Jul 21, 2023
@jakkdl jakkdl mentioned this pull request Jul 23, 2023
trio/_socket.py Outdated
) -> None:
if optlen is None:
return self._sock.setsockopt(level, optname, cast("int|Buffer", value))
return self._sock.setsockopt(level, optname, cast(None, value), optlen)
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like we need a test using value = None. Instead of the casts here, perhaps it’d better to add an a check in each side of the branch to verify that value is or is not None, so that can be eliminated from the union. Better to be a raise instead of assert since it’s user code that would be wrong.

Copy link
Member Author

Choose a reason for hiding this comment

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

Implemented 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

The first exception message is wrong, optlen isn’t set there?

@jakkdl jakkdl requested a review from TeamSpen210 July 27, 2023 12:29
Copy link
Contributor

@TeamSpen210 TeamSpen210 left a comment

Choose a reason for hiding this comment

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

Aside from the sockopt message (and failing test on PyPy), everything looks good here.

@jakkdl
Copy link
Member Author

jakkdl commented Jul 28, 2023

I skimmed through socket documentation on windows and failed to find something that seemed interesting - might even be none, so I'll just skip testing that.

It looks like pypy doesn't support optlen at all. I feel like a comment at the call site that will pop up in the traceback should be good enough if it ever pops up that somebody runs pypy (uncommon) and tries to use the optlen argument (very uncommon - it wasn't even tested for in the trio tests previously). But could also wrap the call in a try/except. Typeshed doesn't make a distinction for pypy, nor do type checkers support guards using sys.implementation.name afaik

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.

Looks good to me other than these. I don't think I need to rereview again!

trio/_core/_local.py Outdated Show resolved Hide resolved
def listen(self, /, backlog: int = min(_stdlib_socket.SOMAXCONN, 128)) -> None:
return self._sock.listen(backlog)

def get_inheritable(self) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why some methods (e.g. listen, setsockopt) have pos-only self params, whereas some (e.g. get_inheritable, set_inheritable) don't.

Copy link
Member Author

Choose a reason for hiding this comment

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

Uh, neither listen nor setsockopt have pos-only self params? The only ones that do are ones that use __self to match typeshed.

I guess there's some inconsistency with whether methods use pos-only at all, but that inconsistency seems to be there in typeshed as well (set_inheritable doesn't have pos-only in typeshed) - and there were some mismatches previously (bind has pos-only address in typeshed, but previous versions of trio didn't, so I don't think I should change that and possibly break existing code).

Comment on lines +722 to +727
# args and kwargs must be starred, otherwise pyright complains:
# '"args" member of ParamSpec is valid only when used with *args parameter'
# '"kwargs" member of ParamSpec is valid only when used with **kwargs parameter'
# wait_fn and fn must also be first in the signature
# 'Keyword parameter cannot appear in signature after ParamSpec args parameter'

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW this isn't just a pyright thing as both these requirements come directly from the Parameter Specification PEP.

Copy link
Member Author

Choose a reason for hiding this comment

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

Huh, weird that mypy didn't complain about it then.

if TYPE_CHECKING:

async def recv(self, buffersize: int, flags: int = 0) -> bytes:
def recv(__self, __buflen: int, __flags: int = 0) -> Awaitable[bytes]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't forget /!

if TYPE_CHECKING:

def recv_into(
__self, buffer: Buffer, nbytes: int = 0, flags: int = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Another pos-only parameter than can use /: I won't comment on any others, as you can just ctrl+f "__"

Copy link
Member Author

Choose a reason for hiding this comment

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

As I mentioned above (but gosh, navigating discussion threads in long PR's are impossible):

Changed it in a bunch of places, but I failed to remove them from sendto, and from the ones where _make_simple_sock_method_wrapper is used to verify the signature.

As it checks if the signature is identical to typeshed it will require updating those. I'll add a comment in the code

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, I do have a comment for that already.

trio/trio/_socket.py

Lines 866 to 868 in 8a8b0b7

# _make_simple_sock_method_wrapper is typed, so this checks that the above is correct
# this requires that we refrain from using `/` to specify pos-only
# args, or mypy thinks the signature differs from typeshed.


################################################################
# sendto
################################################################

@overload
async def sendto(
self, __data: Buffer, __address: tuple[Any, ...] | str | Buffer
Copy link
Contributor

Choose a reason for hiding this comment

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

Go ahead, typeshed loves contributions! (If you haven't submitted a PR there, it's an experience: they merge them so quickly!)

trio/_sync.py Outdated Show resolved Hide resolved
jakkdl and others added 2 commits July 29, 2023 12:20
make `_NoValue` `Final`

Co-authored-by: EXPLOSION <git@helvetica.moe>
@jakkdl jakkdl dismissed A5rocks’s stale review July 29, 2023 10:29

You said you didn't need to re-review, and need to dismiss in order to be able to merge.

@jakkdl jakkdl enabled auto-merge (squash) July 29, 2023 10:29
@jakkdl jakkdl merged commit cf1f3c7 into python-trio:master Jul 29, 2023
@jakkdl jakkdl deleted the typing_improvements_socket branch July 29, 2023 13:42
@jakkdl
Copy link
Member Author

jakkdl commented Jul 31, 2023

https://typing.readthedocs.io/en/latest/source/stubs.html#unsupported-features

Currently, the following features are not supported by all type checkers and should not be used in stubs:

  • Positional-only argument syntax (PEP 570 7). Instead, use the syntax described in the section Functions and Methods. 15

This might be why typeshed is doing __param, though idk if that note is up-to-date

@jakkdl
Copy link
Member Author

jakkdl commented Jul 31, 2023

and even more relevant: python/typeshed#10113

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.

5 participants