diff --git a/CHANGES/10665.feature.rst b/CHANGES/10665.feature.rst new file mode 100644 index 00000000000..b40dec951e1 --- /dev/null +++ b/CHANGES/10665.feature.rst @@ -0,0 +1 @@ +Added :py:attr:`~aiohttp.web.TCPSite.port` accessor for dynamic port allocations in :class:`~aiohttp.web.TCPSite` -- by :user:`twhittock-disguise`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e3ddd3e3d6a..1179c7452f3 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -344,6 +344,7 @@ Thomas Forbes Thomas Grainger Tim Menninger Tolga Tezel +Tom Whittock Tomasz Trebski Toshiaki Tanaka Trinh Hoang Nhu diff --git a/aiohttp/web_runner.py b/aiohttp/web_runner.py index 11f692ce07e..920bb568559 100644 --- a/aiohttp/web_runner.py +++ b/aiohttp/web_runner.py @@ -113,6 +113,11 @@ def name(self) -> str: host = "0.0.0.0" if not self._host else self._host return str(URL.build(scheme=scheme, host=host, port=self._port)) + @property + def port(self) -> int: + """Return the port number the server is bound to, useful for the dynamically allocated port (0).""" + return self._port + async def start(self) -> None: await super().start() loop = asyncio.get_event_loop() @@ -127,6 +132,10 @@ async def start(self) -> None: reuse_address=self._reuse_address, reuse_port=self._reuse_port, ) + if self._port == 0: + # Port 0 means bind to any port, so we need to set the attribute + # to the port the server was actually bound to. + self._port = self._server.sockets[0].getsockname()[1] class UnixSite(BaseSite): diff --git a/docs/web_advanced.rst b/docs/web_advanced.rst index 76fc3ea57f1..a06777e3921 100644 --- a/docs/web_advanced.rst +++ b/docs/web_advanced.rst @@ -1181,6 +1181,28 @@ the middleware might use :meth:`BaseRequest.clone`. for modifying *scheme*, *host* and *remote* attributes according to ``Forwarded`` and ``X-Forwarded-*`` HTTP headers. +Deploying with a dynamic port +----------------------------- + +When deploying aiohttp with zero-configuration networking, it may be useful +to have the server bind to a dynamic port. This can be done by +using the ``0`` port number. This will cause the OS to assign a +free port to the server. The assigned port can be retrieved +using the :attr:`TCPSite.port` property after the server has started. + +For example:: + + app = web.Application() + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, 'localhost', 0) + await site.start() + + print(f"Server started on port {site.port}") + while True: + await asyncio.sleep(3600) # sleep forever + + Swagger support --------------- diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 7f661f44f71..b16a784279b 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -2788,6 +2788,17 @@ application on specific TCP or Unix socket, e.g.:: this flag when being created. This option is not supported on Windows. + .. attribute:: port + + The port number the server is bound to. This is useful when the port + number is not known in advance, such as when constructing with + port 0 to request an ephemeral port or when not supplying the port + to the constructor. + This attribute is read-only and should not be modified. + This attribute is only guaranteed to be correct after the server has + been started. + + .. class:: UnixSite(runner, path, *, \ shutdown_timeout=60.0, ssl_context=None, \ backlog=128) diff --git a/tests/test_web_runner.py b/tests/test_web_runner.py index d75e68ee153..60b3e13e954 100644 --- a/tests/test_web_runner.py +++ b/tests/test_web_runner.py @@ -256,9 +256,21 @@ 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) + + 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