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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -636,6 +642,7 @@ jobs:
- build-windows
- build-macos
- build-ubuntu
- build-ubuntu-installed
- build-ubuntu-ssltests
- build-ios
- build-emscripten
Expand Down Expand Up @@ -685,6 +692,7 @@ jobs:
!fromJSON(needs.build-context.outputs.run-ubuntu)
&& '
build-ubuntu,
build-ubuntu-installed,
build-ubuntu-ssltests,
test-hypothesis,
build-asan,
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/reusable-install.yml
Original file line number Diff line number Diff line change
@@ -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
34 changes: 17 additions & 17 deletions Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -668,12 +668,12 @@ Eager task factory
Shielding from cancellation
===========================

.. awaitablefunction:: shield(aw)
.. awaitablefunction:: shield(arg)

Protect an :ref:`awaitable object <asyncio-awaitables>`
from being :meth:`cancelled <Task.cancel>`.

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::

Expand Down Expand Up @@ -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.


Expand Down Expand Up @@ -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 <asyncio-awaitables>`
Wait for the *fut* :ref:`awaitable <asyncio-awaitables>`
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
Expand All @@ -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:

Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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 <asyncio-awaitables>` in the *aws* iterable
Run :ref:`awaitable objects <asyncio-awaitables>` in the *fs* iterable
concurrently. The returned object can be iterated to obtain the results
of the awaitables as they finish.

Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Lib/logging/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
9 changes: 8 additions & 1 deletion Lib/test/test_os/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand Down
28 changes: 28 additions & 0 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_urllib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix error handling in :func:`socket.gethostbyaddr` and
:func:`socket.gethostbyname_ex` when hostname resolution fails.
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading