Skip to content

Commit

Permalink
Hide the SocketType class
Browse files Browse the repository at this point in the history
It's not really an analogue to the stdlib SocketType (which is the raw
_socket.SocketType that socket.socket subclasses), and having it be
public makes it quite difficult to add fake sockets (see python-triogh-170).

Instead, we expose a new function trio.socket.is_trio_socket, and
rework the docs accordingly.
  • Loading branch information
njsmith committed Jul 25, 2017
1 parent 4188f1f commit d662056
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 98 deletions.
2 changes: 1 addition & 1 deletion docs/source/reference-core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ kind of issue looks like in real life, consider this function::
async def recv_exactly(sock, nbytes):
data = bytearray()
while nbytes > 0:
# SocketType.recv() reads up to 'nbytes' bytes each time
# recv() reads up to 'nbytes' bytes each time
chunk += await sock.recv(nbytes)
if not chunk:
raise RuntimeError("socket unexpected closed")
Expand Down
110 changes: 91 additions & 19 deletions docs/source/reference-io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ I/O in Trio

.. note::

Please excuse our dust! `geocities-construction-worker.gif
<http://www.textfiles.com/underconstruction/>`__
Please excuse our dust! `[insert geocities construction worker gif
here] <http://www.textfiles.com/underconstruction/>`__

You're looking at the documentation for trio's development branch,
which is currently about half-way through implementing a proper
Expand Down Expand Up @@ -162,11 +162,11 @@ create a :class:`SSLStream`:
:members:


Low-level sockets and networking
--------------------------------

.. module:: trio.socket

Low-level networking with :mod:`trio.socket`
---------------------------------------------

The :mod:`trio.socket` module provides trio's basic low-level
networking API. If you're doing ordinary things with stream-oriented
connections over IPv4/IPv6/Unix domain sockets, then you probably want
Expand All @@ -176,22 +176,35 @@ get direct access to all the quirky bits of your system's networking
API, then you're in the right place.


:mod:`trio.socket`: top-level exports
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Top-level exports
~~~~~~~~~~~~~~~~~

Generally, the API exposed by :mod:`trio.socket` mirrors that of the
standard library :mod:`socket` module. Most constants (like
``SOL_SOCKET``) and simple utilities (like :func:`~socket.inet_aton`)
are simply re-exported unchanged. But there are also some differences,
which are described here.

All functions that return socket objects (e.g. :func:`socket.socket`,
:func:`socket.socketpair`, ...) are modified to return trio socket
objects instead. In addition, there is a new function to directly
convert a standard library socket into a trio socket:
.. function:: socket(...)
socketpair(...)
fromfd(...)
fromshare(...)

Trio provides analogues to all the standard library functions that
return socket objects; their interface is identical, except that
they're modified to return trio socket objects instead.

In addition, there is a new function to directly convert a standard
library socket into a trio socket:

.. autofunction:: from_stdlib_socket

Unlike :func:`socket.socket`, :func:`trio.socket.socket` is a
function, not a class; if you want to check whether an object is a
trio socket, use:

.. autofunction:: is_trio_socket

For name lookup, Trio provides the standard functions, but with some
changes:

Expand Down Expand Up @@ -237,7 +250,7 @@ broken features:
Socket objects
~~~~~~~~~~~~~~

.. class:: SocketType()
.. interface:: The trio socket object interface

Trio socket objects are overall very similar to the :ref:`standard
library socket objects <python:socket-objects>`, with a few
Expand Down Expand Up @@ -279,9 +292,27 @@ Socket objects
this can be easily accomplished by calling either
:meth:`resolve_local_address` or :meth:`resolve_remote_address`.

.. automethod:: resolve_local_address
.. method:: resolve_local_address(address)

Resolve the given address into a numeric address suitable for
passing to :meth:`bind`.

This performs the same address resolution that the standard library
:meth:`~socket.socket.bind` call would do, taking into account the
current socket's settings (e.g. if this is an IPv6 socket then it
returns IPv6 addresses). In particular, a hostname of ``None`` is
mapped to the wildcard address.

.. automethod:: resolve_remote_address
.. method:: resolve_remote_address(address)

Resolve the given address into a numeric address suitable for
passing to :meth:`connect` or similar.

This performs the same address resolution that the standard library
:meth:`~socket.socket.connect` call would do, taking into account the
current socket's settings (e.g. if this is an IPv6 socket then it
returns IPv6 addresses). In particular, a hostname of ``None`` is
mapped to the localhost address.

**Modern defaults:** And finally, we took the opportunity to update
the defaults for several socket options that were stuck in the
Expand Down Expand Up @@ -326,14 +357,54 @@ Socket objects
See `issue #72 <https://github.com/python-trio/trio/issues/72>`__ for
discussion of these defaults.

The following methods are similar, but not identical, to the
equivalents in :func:`socket.socket`:
The following methods are similar to the equivalents in
:func:`socket.socket`, but have some trio-specific quirks:

.. method:: bind

.. automethod:: bind
Bind this socket to the given address.

.. automethod:: connect
Unlike the stdlib :meth:`~socket.socket.bind`, this method
requires a pre-resolved address. See
:meth:`resolve_local_address`.

.. automethod:: sendall
.. method:: connect
:async:

Connect the socket to a remote address.

Similar to :meth:`socket.socket.connect`, except async and
requiring a pre-resolved address. See
:meth:`resolve_remote_address`.

.. warning::

Due to limitations of the underlying operating system APIs, it is
not always possible to properly cancel a connection attempt once it
has begun. If :meth:`connect` is cancelled, and is unable to
abort the connection attempt, then it will:

1. forcibly close the socket to prevent accidental re-use
2. raise :exc:`~trio.Cancelled`.

tl;dr: if :meth:`connect` is cancelled then the socket is
left in an unknown state – possibly open, and possibly
closed. The only reasonable thing to do is to close it.

.. method:: sendall(data, flags=0)
:async:

Send the data to the socket, blocking until all of it has been
accepted by the operating system.

``flags`` are passed on to ``send``.

Most low-level operations in trio provide a guarantee: if they raise
:exc:`trio.Cancelled`, this means that they had no effect, so the
system remains in a known state. This is **not true** for
:meth:`sendall`. If this operation raises :exc:`trio.Cancelled` (or
any other exception for that matter), then it may have sent some, all,
or none of the requested data, and there is no way to know which.

.. method:: sendfile

Expand Down Expand Up @@ -374,6 +445,7 @@ Socket objects
* :meth:`~socket.socket.set_inheritable`
* :meth:`~socket.socket.get_inheritable`


Asynchronous disk I/O
---------------------

Expand Down
10 changes: 5 additions & 5 deletions trio/_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class SocketStream(HalfCloseableStream):
interface based on a raw network socket.
Args:
sock (trio.socket.SocketType): The trio socket object to wrap. Must have
type ``SOCK_STREAM``, and be connected.
sock: The trio socket object to wrap. Must have type ``SOCK_STREAM``,
and be connected.
By default, :class:`SocketStream` enables ``TCP_NODELAY``, and (on
platforms where it's supported) enables ``TCP_NOTSENT_LOWAT`` with a
Expand All @@ -50,12 +50,12 @@ class SocketStream(HalfCloseableStream):
.. attribute:: socket
The :class:`trio.socket.SocketType` object that this stream wraps.
The Trio socket object that this stream wraps.
"""
def __init__(self, sock):
if not isinstance(sock, tsocket.SocketType):
raise TypeError("SocketStream requires trio.socket.SocketType")
if not tsocket.is_trio_socket(sock):
raise TypeError("SocketStream requires trio socket object")
if sock._real_type != tsocket.SOCK_STREAM:
raise ValueError("SocketStream requires a SOCK_STREAM socket")
try:
Expand Down
88 changes: 20 additions & 68 deletions trio/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def from_stdlib_socket(sock):
"""Convert a standard library :func:`socket.socket` into a trio socket.
"""
return SocketType(sock)
return _SocketType(sock)
__all__.append("from_stdlib_socket")

@_wraps(_stdlib_socket.fromfd, assigned=(), updated=())
Expand All @@ -192,9 +192,8 @@ def fromshare(*args, **kwargs):

@_wraps(_stdlib_socket.socketpair, assigned=(), updated=())
def socketpair(*args, **kwargs):
return tuple(
from_stdlib_socket(s)
for s in _stdlib_socket.socketpair(*args, **kwargs))
left, right = _stdlib_socket.socketpair(*args, **kwargs)
return (from_stdlib_socket(left), from_stdlib_socket(right))
__all__.append("socketpair")

@_wraps(_stdlib_socket.socket, assigned=(), updated=())
Expand All @@ -204,7 +203,19 @@ def socket(*args, **kwargs):


################################################################
# SocketType
# Type checking
################################################################

def is_trio_socket(obj):
"""Check whether the given object is a trio socket.
"""
return isinstance(obj, _SocketType)

__all__.append("is_trio_socket")

################################################################
# _SocketType
################################################################

# sock.type gets weird stuff set in it, in particular on Linux:
Expand All @@ -218,7 +229,7 @@ def socket(*args, **kwargs):
getattr(_stdlib_socket, "SOCK_NONBLOCK", 0)
| getattr(_stdlib_socket, "SOCK_CLOEXEC", 0))

class SocketType:
class _SocketType:
def __init__(self, sock):
if type(sock) is not _stdlib_socket.socket:
# For example, ssl.SSLSocket subclasses socket.socket, but we
Expand Down Expand Up @@ -302,15 +313,9 @@ def dup(self):
"""Same as :meth:`socket.socket.dup`.
"""
return SocketType(self._sock.dup())
return _SocketType(self._sock.dup())

def bind(self, address):
"""Bind this socket to the given address.
Unlike the stdlib :meth:`~socket.socket.connect`, this method requires
a pre-resolved address. See :meth:`resolve_local_address`.
"""
self._check_address(address, require_resolved=True)
return self._sock.bind(address)

Expand Down Expand Up @@ -418,30 +423,10 @@ async def _resolve_address(self, address, flags):

# Returns something appropriate to pass to bind()
async def resolve_local_address(self, address):
"""Resolve the given address into a numeric address suitable for
passing to :meth:`bind`.
This performs the same address resolution that the standard library
:meth:`~socket.socket.bind` call would do, taking into account the
current socket's settings (e.g. if this is an IPv6 socket then it
returns IPv6 addresses). In particular, a hostname of ``None`` is
mapped to the wildcard address.
"""
return await self._resolve_address(address, AI_PASSIVE)

# Returns something appropriate to pass to connect()/sendto()/sendmsg()
async def resolve_remote_address(self, address):
"""Resolve the given address into a numeric address suitable for
passing to :meth:`connect` or similar.
This performs the same address resolution that the standard library
:meth:`~socket.socket.connect` call would do, taking into account the
current socket's settings (e.g. if this is an IPv6 socket then it
returns IPv6 addresses). In particular, a hostname of ``None`` is
mapped to the localhost address.
"""
return await self._resolve_address(address, 0)

async def _nonblocking_helper(self, fn, args, kwargs, wait_fn):
Expand Down Expand Up @@ -510,28 +495,10 @@ async def accept(self):
################################################################

async def connect(self, address):
"""Connect the socket to a remote address.
Similar to :meth:`socket.socket.connect`, except async and requiring a
pre-resolved address. See :meth:`resolve_remote_address`.
.. warning::
Due to limitations of the underlying operating system APIs, it is
not always possible to properly cancel a connection attempt once it
has begun. If :meth:`connect` is cancelled, and is unable to
abort the connection attempt, then it will:
1. forcibly close the socket to prevent accidental re-use
2. raise :exc:`~trio.Cancelled`.
tl;dr: if :meth:`connect` is cancelled then you should throw away
that socket and make a new one.
"""
# nonblocking connect is weird -- you call it to start things
# off, then the socket becomes writable as a completion
# notification. This means it isn't really cancellable...
# notification. This means it isn't really cancellable... we close the
# socket if cancelled, to avoid confusion.
async with _try_sync():
self._check_address(address, require_resolved=True)
# An interesting puzzle: can a non-blocking connect() return EINTR
Expand Down Expand Up @@ -693,19 +660,6 @@ async def sendmsg(self, *args):
################################################################

async def sendall(self, data, flags=0):
"""Send the data to the socket, blocking until all of it has been
accepted by the operating system.
``flags`` are passed on to ``send``.
Most low-level operations in trio provide a guarantee: if they raise
:exc:`trio.Cancelled`, this means that they had no effect, so the
system remains in a known state. This is **not true** for
:meth:`sendall`. If this operation raises :exc:`trio.Cancelled` (or
any other exception for that matter), then it may have sent some, all,
or none of the requested data, and there is no way to know which.
"""
with memoryview(data) as data:
if not data:
await _core.yield_briefly()
Expand All @@ -730,8 +684,6 @@ async def sendall(self, data, flags=0):
# settimeout
# timeout

__all__.append("SocketType")


################################################################
# create_connection
Expand Down
Loading

0 comments on commit d662056

Please sign in to comment.