Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/10665.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added :py:attr:`~aiohttp.web.TCPSite.port` accessor for dynamic port allocations in :class:`~aiohttp.web.TCPSite` -- by :user:`twhittock-disguise` and :user:`rodrigobnogueira`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ Thomas Forbes
Thomas Grainger
Tim Menninger
Tolga Tezel
Tom Whittock
Tomasz Trebski
Toshiaki Tanaka
Trevor Gamblin
Expand Down
23 changes: 21 additions & 2 deletions aiohttp/web_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async def stop(self) -> None:


class TCPSite(BaseSite):
__slots__ = ("_host", "_port", "_reuse_address", "_reuse_port")
__slots__ = ("_host", "_port", "_bound_port", "_reuse_address", "_reuse_port")

def __init__(
self,
Expand All @@ -101,14 +101,29 @@ def __init__(
if port is None:
port = 8443 if self._ssl_context else 8080
self._port = port
self._bound_port: int | None = None
self._reuse_address = reuse_address
self._reuse_port = reuse_port

@property
def port(self) -> int:
"""The port the server is listening on.

If the server hasn't been started yet, this returns the requested port
(which might be 0 for a dynamic port).
After the server starts, it returns the actual bound port. This is
especially useful when port=0 was requested, as it allows retrieving the
dynamically assigned port after the site has started.
"""
if self._bound_port is not None:
return self._bound_port
return self._port

@property
def name(self) -> str:
scheme = "https" if self._ssl_context else "http"
host = "0.0.0.0" if not self._host else self._host
return str(URL.build(scheme=scheme, host=host, port=self._port))
return str(URL.build(scheme=scheme, host=host, port=self.port))

async def start(self) -> None:
await super().start()
Expand All @@ -124,6 +139,10 @@ async def start(self) -> None:
reuse_address=self._reuse_address,
reuse_port=self._reuse_port,
)
if self._server.sockets:
self._bound_port = self._server.sockets[0].getsockname()[1]
else:
self._bound_port = self._port


class UnixSite(BaseSite):
Expand Down
10 changes: 9 additions & 1 deletion docs/web_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2902,7 +2902,9 @@ application on specific TCP or Unix socket, e.g.::

:param str host: HOST to listen on, all interfaces if ``None`` (default).

:param int port: PORT to listed on, ``8080`` if ``None`` (default).
:param int port: PORT to listen on, ``8080`` if ``None`` (default).
Use ``0`` to let the OS assign a free ephemeral port
(see :attr:`port`).

:param float shutdown_timeout: a timeout used for both waiting on pending
tasks before application shutdown and for
Expand Down Expand Up @@ -2930,6 +2932,12 @@ application on specific TCP or Unix socket, e.g.::
this flag when being created. This option is not
supported on Windows.

.. attribute:: port

Read-only. The actual port number the server is bound to, only
guaranteed to be correct after the site has been started.


.. class:: UnixSite(runner, path, *, \
shutdown_timeout=60.0, ssl_context=None, \
backlog=128)
Expand Down
12 changes: 6 additions & 6 deletions examples/fake_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import socket
import ssl

from aiohttp import ClientSession, TCPConnector, test_utils, web
from aiohttp import ClientSession, TCPConnector, web
from aiohttp.abc import AbstractResolver, ResolveResult
from aiohttp.resolver import DefaultResolver

Expand Down Expand Up @@ -59,11 +59,12 @@ def __init__(self) -> None:
self.ssl_context.load_cert_chain(str(ssl_cert), str(ssl_key))

async def start(self) -> dict[str, int]:
port = test_utils.unused_port()
await self.runner.setup()
site = web.TCPSite(self.runner, "127.0.0.1", port, ssl_context=self.ssl_context)
site = web.TCPSite(
self.runner, "127.0.0.1", port=0, ssl_context=self.ssl_context
)
await site.start()
return {"graph.facebook.com": port}
return {"graph.facebook.com": site.port}

async def stop(self) -> None:
await self.runner.cleanup()
Expand Down Expand Up @@ -116,5 +117,4 @@ async def main() -> None:
await fake_facebook.stop()


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
asyncio.run(main())
2 changes: 2 additions & 0 deletions tests/test_run_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ def patched_loop(
) -> Iterator[asyncio.AbstractEventLoop]:
server = mock.create_autospec(asyncio.Server, spec_set=True, instance=True)
server.wait_closed.return_value = None
server.sockets = []
unix_server = mock.create_autospec(asyncio.Server, spec_set=True, instance=True)
unix_server.wait_closed.return_value = None
unix_server.sockets = []
with mock.patch.object(
loop, "create_server", autospec=True, spec_set=True, return_value=server
):
Expand Down
13 changes: 13 additions & 0 deletions tests/test_web_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,22 @@ async def test_tcpsite_empty_str_host(make_runner: _RunnerMaker) -> None:
runner = make_runner()
await runner.setup()
site = web.TCPSite(runner, host="")
assert site.port == 8080
assert site.name == "http://0.0.0.0:8080"


async def test_tcpsite_ephemeral_port(make_runner: _RunnerMaker) -> None:
runner = make_runner()
await runner.setup()
site = web.TCPSite(runner, port=0)
assert site.port == 0

await site.start()
assert site.port != 0
assert site.name.startswith("http://0.0.0.0:")
await site.stop()


def test_run_after_asyncio_run() -> None:
called = False

Expand Down
Loading