diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fdecd468645f52..d6d61029b89d90d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -245,6 +245,12 @@ jobs: os: ${{ matrix.os }} test-opts: ${{ matrix.test-opts || '' }} + build-ubuntu-installed: + name: 'Ubuntu (installed)' + needs: build-context + if: needs.build-context.outputs.run-ubuntu == 'true' + uses: ./.github/workflows/reusable-install.yml + build-ubuntu-ssltests: name: 'Ubuntu SSL tests' runs-on: ${{ matrix.os }} @@ -636,6 +642,7 @@ jobs: - build-windows - build-macos - build-ubuntu + - build-ubuntu-installed - build-ubuntu-ssltests - build-ios - build-emscripten @@ -685,6 +692,7 @@ jobs: !fromJSON(needs.build-context.outputs.run-ubuntu) && ' build-ubuntu, + build-ubuntu-installed, build-ubuntu-ssltests, test-hypothesis, build-asan, diff --git a/.github/workflows/reusable-install.yml b/.github/workflows/reusable-install.yml new file mode 100644 index 000000000000000..337da684882d29f --- /dev/null +++ b/.github/workflows/reusable-install.yml @@ -0,0 +1,44 @@ +name: Reusable Ubuntu (installed) + +on: + workflow_call: + +permissions: + contents: read + +env: + FORCE_COLOR: 1 + +jobs: + build-install-test: + name: build, install and test + runs-on: ubuntu-26.04-arm + timeout-minutes: 60 + env: + PYTHONSTRICTEXTENSIONBUILD: 1 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Register gcc problem matcher + run: echo "::add-matcher::.github/problem-matchers/gcc.json" + - name: Set install dir + run: + echo "INSTALL_DIR=$(realpath "${GITHUB_WORKSPACE}/../installed-python")" >> "$GITHUB_ENV" + - name: Install dependencies + run: sudo ./.github/workflows/posix-deps-apt.sh + - name: Configure CPython + run: ./configure --config-cache --prefix="$INSTALL_DIR" + - name: Build CPython + run: make -j + - name: Install CPython + run: make install + - name: Set installed interpreter + run: | + ldversion=$(./python -c 'import sysconfig; print(sysconfig.get_config_var("LDVERSION"))') + echo "INSTALLED_PYTHON=${INSTALL_DIR}/bin/python${ldversion}" >> "$GITHUB_ENV" + - name: Display build info + run: | + "$INSTALLED_PYTHON" -m test.pythoninfo + - name: Test the installed Python + run: xvfb-run "$INSTALLED_PYTHON" -m test --fast-ci --timeout=900 diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 38364138a17d790..596cb7565a66e7d 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -668,12 +668,12 @@ Eager task factory Shielding from cancellation =========================== -.. awaitablefunction:: shield(aw) +.. awaitablefunction:: shield(arg) Protect an :ref:`awaitable object ` from being :meth:`cancelled `. - If *aw* is a coroutine it is automatically scheduled as a Task. + If *arg* is a coroutine it is automatically scheduled as a Task. The statement:: @@ -714,7 +714,7 @@ Shielding from cancellation Removed the *loop* parameter. .. deprecated:: 3.10 - Deprecation warning is emitted if *aw* is not Future-like object + Deprecation warning is emitted if *arg* is not Future-like object and there is no running event loop. @@ -837,13 +837,13 @@ Timeouts .. versionadded:: 3.11 -.. function:: wait_for(aw, timeout) +.. function:: wait_for(fut, timeout) :async: - Wait for the *aw* :ref:`awaitable ` + Wait for the *fut* :ref:`awaitable ` to complete with a timeout. - If *aw* is a coroutine it is automatically scheduled as a Task. + If *fut* is a coroutine it is automatically scheduled as a Task. *timeout* can either be ``None`` or a float or int number of seconds to wait for. If *timeout* is ``None``, block until the future @@ -859,7 +859,7 @@ Timeouts so the total wait time may exceed the *timeout*. If an exception happens during cancellation, it is propagated. - If the wait is cancelled, the future *aw* is also cancelled. + If the wait is cancelled, the future *fut* is also cancelled. .. _asyncio_example_waitfor: @@ -884,8 +884,8 @@ Timeouts # timeout! .. versionchanged:: 3.7 - When *aw* is cancelled due to a timeout, ``wait_for`` waits - for *aw* to be cancelled. Previously, it raised + When *fut* is cancelled due to a timeout, ``wait_for`` waits + for *fut* to be cancelled. Previously, it raised :exc:`TimeoutError` immediately. .. versionchanged:: 3.10 @@ -898,20 +898,20 @@ Timeouts Waiting primitives ================== -.. function:: wait(aws, *, timeout=None, return_when=ALL_COMPLETED) +.. function:: wait(fs, *, timeout=None, return_when=ALL_COMPLETED) :async: - Run :class:`~asyncio.Future` and :class:`~asyncio.Task` instances in the *aws* + Run :class:`~asyncio.Future` and :class:`~asyncio.Task` instances in the *fs* iterable concurrently and block until the condition specified by *return_when*. - The *aws* iterable must not be empty. + The *fs* iterable must not be empty. Returns two sets of Tasks/Futures: ``(done, pending)``. Usage:: - done, pending = await asyncio.wait(aws) + done, pending = await asyncio.wait(fs) *timeout* (a float or int), if specified, can be used to control the maximum number of seconds to wait before returning. @@ -943,7 +943,7 @@ Waiting primitives Unlike :func:`~asyncio.wait_for`, ``wait()`` does not cancel the futures when a timeout occurs. - If ``wait()`` is cancelled, the futures in *aws* are not cancelled + If ``wait()`` is cancelled, the futures in *fs* are not cancelled and continue to run. .. versionchanged:: 3.10 @@ -956,9 +956,9 @@ Waiting primitives Added support for generators yielding tasks. -.. function:: as_completed(aws, *, timeout=None) +.. function:: as_completed(fs, *, timeout=None) - Run :ref:`awaitable objects ` in the *aws* iterable + Run :ref:`awaitable objects ` in the *fs* iterable concurrently. The returned object can be iterated to obtain the results of the awaitables as they finish. @@ -1011,7 +1011,7 @@ Waiting primitives Removed the *loop* parameter. .. deprecated:: 3.10 - Deprecation warning is emitted if not all awaitable objects in the *aws* + Deprecation warning is emitted if not all awaitable objects in the *fs* iterable are Future-like objects and there is no running event loop. .. versionchanged:: 3.12 diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index c78a30763d9fa08..fb6b5f3b411b227 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -902,7 +902,11 @@ def __init__(self, address=('localhost', SYSLOG_UDP_PORT), self.socktype = socktype self.timeout = timeout self.socket = None - self.createSocket() + # The address is resolved again when emitting an event. + try: + self.createSocket() + except socket.gaierror: + pass def _connect_unixsocket(self, address): use_socktype = self.socktype diff --git a/Lib/subprocess.py b/Lib/subprocess.py index a14fede00c391c9..d38cc756ec479f2 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -1688,8 +1688,9 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, close_fds = False if shell: - startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = _winapi.SW_HIDE + if not startupinfo.dwFlags & _winapi.STARTF_USESHOWWINDOW: + startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = _winapi.SW_HIDE if not executable: # gh-101283: without a fully-qualified path, before Windows # checks the system directories, it first looks in the diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 90c38557d2e21b4..7cd0df3ea0b62d2 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -2211,6 +2211,24 @@ def tearDown(self): self.server_class.address_family = socket.AF_INET super(IPv6SysLogHandlerTest, self).tearDown() +@support.requires_working_socket() +class UnresolvableSysLogAddressTest(BaseTest): + + """Test for SysLogHandler with a temporarily unresolvable address.""" + + @patch('socket.getaddrinfo') + def test_unresolvable_address(self, mock_getaddrinfo): + # The address can be unresolvable when the handler is created. + mock_getaddrinfo.side_effect = socket.gaierror + hdlr = logging.handlers.SysLogHandler(('localhost', 514)) + self.addCleanup(hdlr.close) + self.assertIsNone(hdlr.socket) + # It is resolved again when a record is emitted. + calls = mock_getaddrinfo.call_count + with support.captured_stderr(): + hdlr.emit(logging.makeLogRecord({'msg': 'sp\xe4m'})) + self.assertGreater(mock_getaddrinfo.call_count, calls) + @support.requires_working_socket() @threading_helper.requires_working_threading() class HTTPHandlerTest(BaseTest): diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py index 8743b0bf0bc4939..f3d67027ad37277 100644 --- a/Lib/test/test_os/test_posix.py +++ b/Lib/test/test_os/test_posix.py @@ -71,7 +71,7 @@ def testNoArgFunctions(self): NO_ARG_FUNCTIONS = [ "ctermid", "getcwd", "getcwdb", "uname", "times", "getloadavg", "getegid", "geteuid", "getgid", "getgroups", - "getpid", "getpgrp", "getppid", "getuid", "sync", + "getpid", "getpgrp", "getppid", "getuid", ] for name in NO_ARG_FUNCTIONS: @@ -81,6 +81,13 @@ def testNoArgFunctions(self): posix_func() self.assertRaises(TypeError, posix_func, 1) + # gh-102184: sync() can block for a long time. + @support.requires_resource('walltime') + @unittest.skipUnless(hasattr(posix, 'sync'), 'test needs posix.sync()') + def test_sync(self): + posix.sync() + self.assertRaises(TypeError, posix.sync, 1) + @unittest.skipUnless(hasattr(posix, 'getresuid'), 'test needs posix.getresuid()') def test_getresuid(self): diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index d1840e97d0f2f7c..4cea07b3d2c7745 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -3777,6 +3777,34 @@ def test_startupinfo_copy(self): self.assertEqual(startupinfo.wShowWindow, subprocess.SW_HIDE) self.assertEqual(startupinfo.lpAttributeList, {"handle_list": []}) + def test_startupinfo_shell_show_window(self): + # gh-85028: shell=True must not override wShowWindow set by the caller + import _winapi + SW_MAXIMIZE = 3 + used = [] + create_process = _winapi.CreateProcess + + def spy(*args): + # The startup info is the last argument of CreateProcess() + used.append(args[-1]) + return create_process(*args) + + startupinfo = subprocess.STARTUPINFO( + dwFlags=subprocess.STARTF_USESHOWWINDOW, + wShowWindow=SW_MAXIMIZE) + with mock.patch.object(_winapi, 'CreateProcess', spy): + rc = subprocess.call(ZERO_RETURN_CMD, shell=True, + startupinfo=startupinfo) + self.assertEqual(rc, 0) + rc = subprocess.call(ZERO_RETURN_CMD, shell=True) + self.assertEqual(rc, 0) + + requested, default = used + self.assertEqual(requested.wShowWindow, SW_MAXIMIZE) + # Without STARTF_USESHOWWINDOW the shell window is still hidden. + self.assertTrue(default.dwFlags & subprocess.STARTF_USESHOWWINDOW) + self.assertEqual(default.wShowWindow, subprocess.SW_HIDE) + # CREATE_NEW_CONSOLE creates a "popup" window. @support.requires_resource('gui') def test_creationflags(self): diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 1e5f79998e7cab2..ab59727a8fd1820 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -807,6 +807,61 @@ def test_short_content_raises_ContentTooShortError_without_reporthook(self): self.unfakehttp() +class urlcleanup_Tests(unittest.TestCase, FakeHTTPMixin): + """Test urllib.request.urlcleanup()""" + + def setUp(self): + self.addCleanup(urllib.request.urlcleanup) + + def urlretrieve(self): + self.fakehttp(b'HTTP/1.1 200 OK\r\n\r\ndata') + try: + filename, headers = urllib.request.urlretrieve( + support.TEST_HTTP_URL) + finally: + self.unfakehttp() + self.addCleanup(os_helper.unlink, filename) + return filename + + def fake_urlopen(self, data): + self.fakehttp(b'HTTP/1.1 200 OK\r\n\r\n' + data) + try: + with urllib.request.urlopen(support.TEST_HTTP_URL) as fp: + return fp.read() + finally: + self.unfakehttp() + + def test_temporary_files(self): + filename = self.urlretrieve() + self.assertTrue(os.path.exists(filename)) + + urllib.request.urlcleanup() + self.assertFalse(os.path.exists(filename)) + + # A file created after the cleanup is not deleted. + os_helper.create_empty_file(filename) + urllib.request.urlcleanup() + self.assertTrue(os.path.exists(filename)) + + def test_opener(self): + # The implicitly created opener supports http. + self.assertEqual(self.fake_urlopen(b'first'), b'first') + + # An installed opener replaces it and supports only its handlers. + opener = urllib.request.OpenerDirector() + opener.add_handler(urllib.request.DataHandler()) + opener.add_handler(urllib.request.UnknownHandler()) + urllib.request.install_opener(opener) + with urllib.request.urlopen('data:,hello') as fp: + self.assertEqual(fp.read(), b'hello') + with self.assertRaises(urllib.error.URLError): + self.fake_urlopen(b'') + + # urlcleanup() resets the opener. + urllib.request.urlcleanup() + self.assertEqual(self.fake_urlopen(b'second'), b'second') + + class QuotingTests(unittest.TestCase): r"""Tests for urllib.quote() and urllib.quote_plus() diff --git a/Misc/NEWS.d/next/Library/2026-07-22-19-21-20.gh-issue-82535.METFqd.rst b/Misc/NEWS.d/next/Library/2026-07-22-19-21-20.gh-issue-82535.METFqd.rst new file mode 100644 index 000000000000000..e027f81256b703e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-19-21-20.gh-issue-82535.METFqd.rst @@ -0,0 +1,3 @@ +:class:`logging.handlers.SysLogHandler` no longer fails +if the address cannot be resolved when the handler is created. +The address is resolved again when a record is emitted. diff --git a/Misc/NEWS.d/next/Library/2026-08-10-09-00-18.gh-issue-155336.vrTXoU.rst b/Misc/NEWS.d/next/Library/2026-08-10-09-00-18.gh-issue-155336.vrTXoU.rst new file mode 100644 index 000000000000000..87f8a7f4a21e66d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-10-09-00-18.gh-issue-155336.vrTXoU.rst @@ -0,0 +1,2 @@ +Fix error handling in :func:`socket.gethostbyaddr` and +:func:`socket.gethostbyname_ex` when hostname resolution fails. diff --git a/Misc/NEWS.d/next/Windows/2020-06-19-00-14-52.bpo-40851.0-3EJP.rst b/Misc/NEWS.d/next/Windows/2020-06-19-00-14-52.bpo-40851.0-3EJP.rst new file mode 100644 index 000000000000000..70f5efad398b037 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2020-06-19-00-14-52.bpo-40851.0-3EJP.rst @@ -0,0 +1,3 @@ +:class:`subprocess.Popen` with ``shell=True`` on Windows now honors +``wShowWindow`` of the *startupinfo* argument, so the console window of the +started program can be shown. Previously it was always hidden. diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 73ae1c942daba48..f9c77c631b5d2af 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -6041,7 +6041,7 @@ sock_decode_hostname(const char *name) static PyObject * gethost_common(socket_state *state, struct hostent *h, struct sockaddr *addr, - size_t alen, int af) + size_t alen, int af, int h_error) { char **pch; PyObject *rtn_tuple = (PyObject *)NULL; @@ -6052,7 +6052,7 @@ gethost_common(socket_state *state, struct hostent *h, struct sockaddr *addr, if (h == NULL) { /* Let's get real error message to return */ - set_herror(state, h_errno); + set_herror(state, h_error); return NULL; } @@ -6188,6 +6188,7 @@ static PyObject * socket_gethostbyname_ex(PyObject *self, PyObject *args) { char *name; + int h_error; struct hostent *h; sock_addr_t addr; struct sockaddr *sa; @@ -6199,7 +6200,6 @@ socket_gethostbyname_ex(PyObject *self, PyObject *args) #else char buf[16384]; int buf_len = (sizeof buf) - 1; - int errnop; #endif #ifdef HAVE_GETHOSTBYNAME_R_3_ARG int result; @@ -6218,14 +6218,14 @@ socket_gethostbyname_ex(PyObject *self, PyObject *args) Py_BEGIN_ALLOW_THREADS #ifdef HAVE_GETHOSTBYNAME_R #if defined(HAVE_GETHOSTBYNAME_R_6_ARG) - gethostbyname_r(name, &hp_allocated, buf, buf_len, - &h, &errnop); + gethostbyname_r(name, &hp_allocated, buf, buf_len, &h, &h_error); #elif defined(HAVE_GETHOSTBYNAME_R_5_ARG) - h = gethostbyname_r(name, &hp_allocated, buf, buf_len, &errnop); + h = gethostbyname_r(name, &hp_allocated, buf, buf_len, &h_error); #else /* HAVE_GETHOSTBYNAME_R_3_ARG */ memset((void *) &data, '\0', sizeof(data)); result = gethostbyname_r(name, &hp_allocated, &data); h = (result != 0) ? NULL : &hp_allocated; + h_error = h_errno; #endif #else /* not HAVE_GETHOSTBYNAME_R */ #ifdef USE_GETHOSTBYNAME_LOCK @@ -6235,6 +6235,7 @@ socket_gethostbyname_ex(PyObject *self, PyObject *args) _Py_COMP_DIAG_IGNORE_DEPR_DECLS h = gethostbyname(name); _Py_COMP_DIAG_POP + h_error = h_errno; #endif /* HAVE_GETHOSTBYNAME_R */ Py_END_ALLOW_THREADS /* Some C libraries would require addr.__ss_family instead of @@ -6243,7 +6244,7 @@ socket_gethostbyname_ex(PyObject *self, PyObject *args) access sa_family. */ sa = SAS2SA(&addr); ret = gethost_common(state, h, SAS2SA(&addr), sizeof(addr), - sa->sa_family); + sa->sa_family, h_error); #ifdef USE_GETHOSTBYNAME_LOCK PyMutex_Unlock(&netdb_lock); #endif @@ -6282,7 +6283,6 @@ socket_gethostbyaddr(PyObject *self, PyObject *args) to maintain this alignment. */ _Py_ALIGNED_DEF(8, char) buf[16384]; int buf_len = (sizeof buf) - 1; - int errnop; #endif #ifdef HAVE_GETHOSTBYNAME_R_3_ARG int result; @@ -6291,6 +6291,7 @@ socket_gethostbyaddr(PyObject *self, PyObject *args) const char *ap; int al; int af; + int h_error; if (!PyArg_ParseTuple(args, "et:gethostbyaddr", "idna", &ip_num)) return NULL; @@ -6323,16 +6324,14 @@ socket_gethostbyaddr(PyObject *self, PyObject *args) Py_BEGIN_ALLOW_THREADS #ifdef HAVE_GETHOSTBYNAME_R #if defined(HAVE_GETHOSTBYNAME_R_6_ARG) - gethostbyaddr_r(ap, al, af, - &hp_allocated, buf, buf_len, - &h, &errnop); + gethostbyaddr_r(ap, al, af, &hp_allocated, buf, buf_len, &h, &h_error); #elif defined(HAVE_GETHOSTBYNAME_R_5_ARG) - h = gethostbyaddr_r(ap, al, af, - &hp_allocated, buf, buf_len, &errnop); + h = gethostbyaddr_r(ap, al, af, &hp_allocated, buf, buf_len, &h_error); #else /* HAVE_GETHOSTBYNAME_R_3_ARG */ memset((void *) &data, '\0', sizeof(data)); result = gethostbyaddr_r(ap, al, af, &hp_allocated, &data); h = (result != 0) ? NULL : &hp_allocated; + h_error = h_errno; #endif #else /* not HAVE_GETHOSTBYNAME_R */ #ifdef USE_GETHOSTBYNAME_LOCK @@ -6342,9 +6341,10 @@ socket_gethostbyaddr(PyObject *self, PyObject *args) _Py_COMP_DIAG_IGNORE_DEPR_DECLS h = gethostbyaddr(ap, al, af); _Py_COMP_DIAG_POP + h_error = h_errno; #endif /* HAVE_GETHOSTBYNAME_R */ Py_END_ALLOW_THREADS - ret = gethost_common(state, h, SAS2SA(&addr), sizeof(addr), af); + ret = gethost_common(state, h, SAS2SA(&addr), sizeof(addr), af, h_error); #ifdef USE_GETHOSTBYNAME_LOCK PyMutex_Unlock(&netdb_lock); #endif