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 Doc/c-api/bytes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ called with a non-bytes parameter.
The function is :term:`soft deprecated`,
use the :c:type:`PyBytesWriter` API instead.

.. _pybyteswriter:

PyBytesWriter
-------------
Expand Down
6 changes: 3 additions & 3 deletions Doc/library/getpass.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ The :mod:`getpass` module provides two functions:

The *echo_char* argument controls how user input is displayed while typing.
If *echo_char* is ``None`` (default), input remains hidden. Otherwise,
*echo_char* must be a printable ASCII string and each typed character
is replaced by it. For example, ``echo_char='*'`` will display
asterisks instead of the actual input.
*echo_char* must be a single printable ASCII character and each
typed character is replaced by it. For example, ``echo_char='*'`` will
display asterisks instead of the actual input.

If echo free input is unavailable getpass() falls back to printing
a warning message to *stream* and reading from ``sys.stdin`` and
Expand Down
15 changes: 6 additions & 9 deletions Doc/library/sysconfig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -382,22 +382,19 @@ Other functions

Examples of returned values:

- linux-i586
- linux-alpha (?)
- solaris-2.6-sun4u

Windows will return one of:
Windows:

- win-amd64 (64-bit Windows on AMD64, aka x86_64, Intel64, and EM64T)
- win-arm64 (64-bit Windows on ARM64, aka AArch64)
- win32 (all others - specifically, sys.platform is returned)

macOS can return:
POSIX based OS:

- macosx-10.6-ppc
- macosx-10.4-ppc64
- macosx-10.3-i386
- macosx-10.4-fat
- linux-x86_64
- macosx-15.5-arm64
- macosx-26.0-universal2 (macOS on Apple Silicon or Intel)
- android-24-arm64_v8a

For other non-POSIX platforms, currently just returns :data:`sys.platform`.

Expand Down
60 changes: 32 additions & 28 deletions Doc/reference/compound_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -335,15 +335,29 @@ stored in the :mod:`sys` module is reset to its previous value::
:keyword:`!except*` clause
--------------------------

The :keyword:`!except*` clause(s) are used for handling
:exc:`ExceptionGroup`\s. The exception type for matching is interpreted as in
the case of :keyword:`except`, but in the case of exception groups we can have
partial matches when the type matches some of the exceptions in the group.
This means that multiple :keyword:`!except*` clauses can execute,
each handling part of the exception group.
Each clause executes at most once and handles an exception group
of all matching exceptions. Each exception in the group is handled by at most
one :keyword:`!except*` clause, the first that matches it. ::
The :keyword:`!except*` clause(s) specify one or more handlers for groups of
exceptions (:exc:`BaseExceptionGroup` instances). A :keyword:`try` statement
can have either :keyword:`except` or :keyword:`!except*` clauses, but not both.
The exception type for matching is mandatory in the case of :keyword:`!except*`,
so ``except*:`` is a syntax error. The type is interpreted as in the case of
:keyword:`!except`, but matching is performed on the exceptions contained in the
group that is being handled. An :exc:`TypeError` is raised if a matching
type is a subclass of :exc:`!BaseExceptionGroup`, because that would have
ambiguous semantics.

When an exception group is raised in the try block, each :keyword:`!except*`
clause splits (see :meth:`~BaseExceptionGroup.split`) it into the subgroups
of matching and non-matching exceptions. If the matching subgroup is not empty,
it becomes the handled exception (the value returned from :func:`sys.exception`)
and assigned to the target of the :keyword:`!except*` clause (if there is one).
Then, the body of the :keyword:`!except*` clause executes. If the non-matching
subgroup is not empty, it is processed by the next :keyword:`!except*` in the
same manner. This continues until all exceptions in the group have been matched,
or the last :keyword:`!except*` clause has run.

After all :keyword:`!except*` clauses execute, the group of unhandled exceptions
is merged with any exceptions that were raised or re-raised from within
:keyword:`!except*` clauses. This merged exception group propagates on.::

>>> try:
... raise ExceptionGroup("eg",
Expand All @@ -356,22 +370,18 @@ one :keyword:`!except*` clause, the first that matches it. ::
caught <class 'ExceptionGroup'> with nested (TypeError(2),)
caught <class 'ExceptionGroup'> with nested (OSError(3), OSError(4))
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg
| File "<doctest default[0]>", line 2, in <module>
| raise ExceptionGroup("eg",
| [ValueError(1), TypeError(2), OSError(3), OSError(4)])
| ExceptionGroup: eg (1 sub-exception)
+-+---------------- 1 ----------------
| ValueError: 1
+------------------------------------


Any remaining exceptions that were not handled by any :keyword:`!except*`
clause are re-raised at the end, along with all exceptions that were
raised from within the :keyword:`!except*` clauses. If this list contains
more than one exception to reraise, they are combined into an exception
group.

If the raised exception is not an exception group and its type matches
one of the :keyword:`!except*` clauses, it is caught and wrapped by an
exception group with an empty message string. ::
If the exception raised from the :keyword:`try` block is not an exception group
and its type matches one of the :keyword:`!except*` clauses, it is caught and
wrapped by an exception group with an empty message string. This ensures that the
type of the target ``e`` is consistently :exc:`BaseExceptionGroup`::

>>> try:
... raise BlockingIOError
Expand All @@ -380,13 +390,7 @@ exception group with an empty message string. ::
...
ExceptionGroup('', (BlockingIOError()))

An :keyword:`!except*` clause must have a matching expression; it cannot be ``except*:``.
Furthermore, this expression cannot contain exception group types, because that would
have ambiguous semantics.

It is not possible to mix :keyword:`except` and :keyword:`!except*`
in the same :keyword:`try`.
The :keyword:`break`, :keyword:`continue`, and :keyword:`return` statements
:keyword:`break`, :keyword:`continue` and :keyword:`return`
cannot appear in an :keyword:`!except*` clause.


Expand Down
3 changes: 2 additions & 1 deletion Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,8 @@ New features
and :c:data:`Py_mod_abi`.
(Contributed by Petr Viktorin in :gh:`137210`.)

* Implement :pep:`782`, the :c:type:`PyBytesWriter` API. Add functions:
* Implement :pep:`782`, the :ref:`PyBytesWriter API <pybyteswriter>`.
Add functions:

* :c:func:`PyBytesWriter_Create`
* :c:func:`PyBytesWriter_Discard`
Expand Down
2 changes: 1 addition & 1 deletion InternalDocs/asyncio.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ the current task is found and returned. If no matching thread state is
found, `None` is returned.

In free-threading, it avoids contention on a global dictionary as
threads can access the current task of thier running loop without any
threads can access the current task of their running loop without any
locking.

---
Expand Down
17 changes: 15 additions & 2 deletions Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,14 @@ def prepare(self):
raw.lflag |= termios.ISIG
raw.cc[termios.VMIN] = 1
raw.cc[termios.VTIME] = 0
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
try:
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
except termios.error as e:
if e.args[0] != errno.EIO:
# gh-135329: when running under external programs (like strace),
# tcsetattr may fail with EIO. We can safely ignore this
# and continue with default terminal settings.
raise

# In macOS terminal we need to deactivate line wrap via ANSI escape code
if self.is_apple_terminal:
Expand Down Expand Up @@ -372,7 +379,11 @@ def restore(self):
self.__disable_bracketed_paste()
self.__maybe_write_code(self._rmkx)
self.flushoutput()
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
try:
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
except termios.error as e:
if e.args[0] != errno.EIO:
raise

if self.is_apple_terminal:
os.write(self.output_fd, b"\033[?7h")
Expand Down Expand Up @@ -411,6 +422,8 @@ def get_event(self, block: bool = True) -> Event | None:
return self.event_queue.get()
else:
continue
elif err.errno == errno.EIO:
raise SystemExit(errno.EIO)
else:
raise
else:
Expand Down
21 changes: 15 additions & 6 deletions Lib/getpass.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
prompt: Written on stream to ask for the input. Default: 'Password: '
stream: A writable file object to display the prompt. Defaults to
the tty. If no tty is available defaults to sys.stderr.
echo_char: A string used to mask input (e.g., '*'). If None, input is
hidden.
echo_char: A single ASCII character to mask input (e.g., '*').
If None, input is hidden.
Returns:
The seKr3t input.
Raises:
Expand Down Expand Up @@ -144,10 +144,19 @@ def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None):


def _check_echo_char(echo_char):
# ASCII excluding control characters
if echo_char and not (echo_char.isprintable() and echo_char.isascii()):
raise ValueError("'echo_char' must be a printable ASCII string, "
f"got: {echo_char!r}")
# Single-character ASCII excluding control characters
if echo_char is None:
return
if not isinstance(echo_char, str):
raise TypeError("'echo_char' must be a str or None, not "
f"{type(echo_char).__name__}")
if not (
len(echo_char) == 1
and echo_char.isprintable()
and echo_char.isascii()
):
raise ValueError("'echo_char' must be a single printable ASCII "
f"character, got: {echo_char!r}")


def _raw_input(prompt="", stream=None, input=None, echo_char=None):
Expand Down
22 changes: 13 additions & 9 deletions Lib/sysconfig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,18 +645,22 @@ def get_platform():
isn't particularly important.

Examples of returned values:
linux-i586
linux-alpha (?)
solaris-2.6-sun4u

Windows will return one of:
win-amd64 (64-bit Windows on AMD64 (aka x86_64, Intel64, EM64T, etc)
win-arm64 (64-bit Windows on ARM64 (aka AArch64)
win32 (all others - specifically, sys.platform is returned)

For other non-POSIX platforms, currently just returns 'sys.platform'.
Windows:

"""
- win-amd64 (64-bit Windows on AMD64, aka x86_64, Intel64, and EM64T)
- win-arm64 (64-bit Windows on ARM64, aka AArch64)
- win32 (all others - specifically, sys.platform is returned)

POSIX based OS:

- linux-x86_64
- macosx-15.5-arm64
- macosx-26.0-universal2 (macOS on Apple Silicon or Intel)
- android-24-arm64_v8a

For other non-POSIX platforms, currently just returns :data:`sys.platform`."""
if os.name == 'nt':
if 'amd64' in sys.version.lower():
return 'win-amd64'
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -4887,7 +4887,7 @@ class Color(Enum):
def _generate_next_value_(name, start, count, last):
return name

def test_auto_order_wierd(self):
def test_auto_order_weird(self):
weird_auto = auto()
weird_auto.value = 'pathological case'
class Color(Enum):
Expand Down
36 changes: 36 additions & 0 deletions Lib/test/test_getpass.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,5 +201,41 @@ def test_control_chars_with_echo_char(self):
self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue())


class GetpassEchoCharTest(unittest.TestCase):

def test_accept_none(self):
getpass._check_echo_char(None)

@support.subTests('echo_char', ["*", "A", " "])
def test_accept_single_printable_ascii(self, echo_char):
getpass._check_echo_char(echo_char)

def test_reject_empty_string(self):
self.assertRaises(ValueError, getpass.getpass, echo_char="")

@support.subTests('echo_char', ["***", "AA", "aA*!"])
def test_reject_multi_character_strings(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)

@support.subTests('echo_char', [
'\N{LATIN CAPITAL LETTER AE}', # non-ASCII single character
'\N{HEAVY BLACK HEART}', # non-ASCII multibyte character
])
def test_reject_non_ascii(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)

@support.subTests('echo_char', [
ch for ch in map(chr, range(0, 128))
if not ch.isprintable()
])
def test_reject_non_printable_characters(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)

# TypeError Rejection
@support.subTests('echo_char', [b"*", 0, 0.0, [], {}])
def test_reject_non_string(self, echo_char):
self.assertRaises(TypeError, getpass.getpass, echo_char=echo_char)


if __name__ == "__main__":
unittest.main()
20 changes: 10 additions & 10 deletions Lib/test/test_json/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class FloatNum(float, Enum):
NEG_INF = float('-inf')
NAN = float('nan')

class WierdNum(float, Enum):
class WeirdNum(float, Enum):
inf = INF
neg_inf = NEG_INF
nan = NAN
Expand All @@ -40,7 +40,7 @@ def test_floats(self):
self.assertEqual(self.loads(self.dumps(enum)), enum)

def test_weird_floats(self):
for enum, expected in zip(WierdNum, ('Infinity', '-Infinity', 'NaN')):
for enum, expected in zip(WeirdNum, ('Infinity', '-Infinity', 'NaN')):
self.assertEqual(self.dumps(enum), expected)
if not isnan(enum):
self.assertEqual(float(self.dumps(enum)), enum)
Expand All @@ -64,16 +64,16 @@ def test_list(self):
str([E, PI, TAU]))
self.assertEqual(self.loads(self.dumps(list(FloatNum))),
list(FloatNum))
self.assertEqual(self.dumps(list(WierdNum)),
self.assertEqual(self.dumps(list(WeirdNum)),
'[Infinity, -Infinity, NaN]')
self.assertEqual(self.loads(self.dumps(list(WierdNum)))[:2],
list(WierdNum)[:2])
self.assertTrue(isnan(self.loads(self.dumps(list(WierdNum)))[2]))
self.assertEqual(self.loads(self.dumps(list(WeirdNum)))[:2],
list(WeirdNum)[:2])
self.assertTrue(isnan(self.loads(self.dumps(list(WeirdNum)))[2]))

def test_dict_keys(self):
s, b, h, r = BigNum
e, p, t = FloatNum
i, j, n = WierdNum
i, j, n = WeirdNum
d = {
s:'tiny', b:'large', h:'larger', r:'largest',
e:"Euler's number", p:'pi', t:'tau',
Expand All @@ -100,9 +100,9 @@ def test_dict_values(self):
e=FloatNum.e,
pi=FloatNum.pi,
tau=FloatNum.tau,
i=WierdNum.inf,
j=WierdNum.neg_inf,
n=WierdNum.nan,
i=WeirdNum.inf,
j=WeirdNum.neg_inf,
n=WeirdNum.nan,
)
nd = self.loads(self.dumps(d))
self.assertEqual(nd['tiny'], SMALL)
Expand Down
Loading
Loading