Skip to content
2 changes: 1 addition & 1 deletion Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ and reliable way to wait for all tasks in the group to finish.

* call it from the task group body based on some condition or event
* pass the task group instance to child tasks via :meth:`create_task`, allowing a child
task to conditionally cancel the entire entire group
task to conditionally cancel the entire group
* pass the task group instance or bound :meth:`cancel` method to some other task *before*
opening the task group, allowing remote cancellation

Expand Down
2 changes: 1 addition & 1 deletion Doc/library/concurrent.interpreters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ thread) and calling a function in that execution context.
For concurrency, interpreters themselves (and this module) don't
provide much more than isolation, which on its own isn't useful.
Actual concurrency is available separately through
:mod:`threads <threading>` See `below <interp-concurrency_>`_
:mod:`threads <threading>` -- see `below <interp-concurrency_>`_.

.. seealso::

Expand Down
66 changes: 66 additions & 0 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3161,6 +3161,72 @@ fields, or any other data types containing pointer type fields.
that should be merged into a containing structure or union.


.. decorator:: struct(*, align=None, layout, endian='native', pack=None)
:module: ctypes.util

A :term:`decorator` that allows generating structure types using an
annotation-based syntax, similar to the :mod:`dataclasses` module.

For example:

.. code-block:: python

from ctypes.util import struct
from ctypes import c_int

@struct
class Point:
x: c_int
y: c_int

point = Point(1, 2)

*align*, *layout*, and *pack* supply the value for the :attr:`~ctypes.Structure._align_`,
:attr:`~ctypes.Structure._layout_`, and :attr:`~ctypes.Structure._pack_`
attributes, respectively.

*endian* controls which structure class will be used as the base.

- If *endian* is ``'native'``, :class:`~ctypes.Structure` will be used.
- If *endian* is ``'big'``, :class:`~ctypes.BigEndianStructure` will be used.
- If *endian* is ``'little'``, :class:`~ctypes.LittleEndianStructure` will be used.

Any other value will raise a :class:`ValueError`.

For controlling field-specific data, wrap the annotation in :class:`typing.Annotated`
with :class:`CFieldInfo` as the second argument, like so:

.. code-block:: python

@struct
class PyObject:
ob_refcnt: c_ssize_t
ob_type: c_void_p

@struct
class PyHovercraftObject:
ob_base: Annotated[PyObject, CFieldInfo(anonymous=True)]

.. versionadded:: next


.. class:: CFieldInfo(anonymous=False, bit_width=None)
:module: ctypes.util

Information regarding a structure field defined by the :func:`struct`
decorator. This should be used in the second argument of a
:class:`typing.Annotated` wrapping a ctypes type.

*anonymous* specifies whether the field will be present in the
:attr:`~ctypes.Structure._anonymous_` attribute of the generated class.

If *bit_width* is non-``None``, the annotated field will be *bit_width*
number of bits in the generated structure. This is equivalent to passing
a third item in :attr:`~ctypes.Structure._fields_`.

.. versionadded:: next


.. _ctypes-arrays-pointers:

Arrays and pointers
Expand Down
4 changes: 2 additions & 2 deletions Doc/library/filecmp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ The :mod:`!filecmp` module defines the following functions:
file changes. The entire cache may be cleared using :func:`clear_cache`.


.. function:: cmpfiles(dir1, dir2, common, shallow=True)
.. function:: cmpfiles(a, b, common, shallow=True)

Compare the files in the two directories *dir1* and *dir2* whose names are
Compare the files in the two directories *a* and *b* whose names are
given by *common*.

Returns three lists of file names: *match*, *mismatch*,
Expand Down
4 changes: 4 additions & 0 deletions Doc/library/wave.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ Wave_read Objects
This is one of :data:`WAVE_FORMAT_PCM`,
:data:`WAVE_FORMAT_IEEE_FLOAT`, or :data:`WAVE_FORMAT_EXTENSIBLE`.

.. versionadded:: 3.15


.. method:: getcomptype()

Expand Down Expand Up @@ -284,6 +286,8 @@ Wave_write Objects

Return the current frame format code.

.. versionadded:: 3.15


.. method:: setparams(tuple)

Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ curses
wide-character support.
(Contributed by Serhiy Storchaka in :gh:`133031`.)


ctypes
------

* Add :func:`ctypes.util.struct` for generating :class:`~ctypes.Structure` types
from an annotation-based syntax, similar to how the :mod:`dataclasses` module
is used.
(Contributed by Peter Bierma in :gh:`104533`.)


encodings
---------

Expand Down
1 change: 1 addition & 0 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ class Syntax(ThemeSection):
keyword: str = ANSIColors.BOLD_BLUE
keyword_constant: str = ANSIColors.BOLD_BLUE
builtin: str = ANSIColors.CYAN
command: str = ANSIColors.BOLD_CYAN
comment: str = ANSIColors.RED
string: str = ANSIColors.GREEN
number: str = ANSIColors.YELLOW
Expand Down
1 change: 1 addition & 0 deletions Lib/_pyrepl/historical_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
(r"\C-s", "forward-history-isearch"),
(r"\M-r", "restore-history"),
(r"\M-.", "yank-arg"),
(r"\M-_", "yank-arg"),
(r"\<page down>", "history-search-forward"),
(r"\x1b[6~", "history-search-forward"),
(r"\<page up>", "history-search-backward"),
Expand Down
1 change: 1 addition & 0 deletions Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def _clear_screen():
reader.scheduled_commands.append("clear_screen")


# Keep this in sync with _pyrepl.utils.COMMANDS
REPL_COMMANDS = {
"exit": _sitebuiltins.Quitter('exit', ''),
"quit": _sitebuiltins.Quitter('quit' ,''),
Expand Down
9 changes: 9 additions & 0 deletions Lib/_pyrepl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
IDENTIFIERS_AFTER = frozenset({"def", "class"})
KEYWORD_CONSTANTS = frozenset({"True", "False", "None"})
BUILTINS = frozenset({str(name) for name in dir(builtins) if not name.startswith('_')})
# Keep this in sync with _pyrepl.simple_interact.REPL_COMMANDS
COMMANDS = frozenset({"exit", "quit", "copyright", "help", "clear"})


def THEME(**kwargs):
Expand Down Expand Up @@ -235,6 +237,13 @@ def gen_colors_from_token_stream(
):
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "soft_keyword")
elif (
token.string in COMMANDS
and (not prev_token or prev_token.type == T.INDENT)
and (not next_token or next_token.type == T.NEWLINE)
):
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "command")
elif (
token.string in BUILTINS
and not (prev_token and prev_token.exact_type == T.DOT)
Expand Down
83 changes: 83 additions & 0 deletions Lib/ctypes/util.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import os
import sys

from dataclasses import dataclass

lazy import functools
lazy import shutil
lazy import subprocess

lazy import annotationlib
lazy from typing import Annotated, get_args, ClassVar, get_origin
lazy from ctypes import Structure, BigEndianStructure, LittleEndianStructure

# find_library(name) returns the pathname of a library, or None.
if os.name == "nt":

Expand Down Expand Up @@ -491,6 +498,82 @@ def dllist():
ctypes.byref(ctypes.py_object(libraries)))
return libraries


@dataclass(slots=True, frozen=True)
class CFieldInfo:
anonymous: bool = False
bit_width: int | None = None


def _process_struct(decorated_class, /, *, align, layout, endian, pack):
fields = []
anonymous = []
if issubclass(decorated_class, Structure):
fields.extend(decorated_class._fields_)
anonymous.extend(decorated_class._anonymous_)

for name, hint in annotationlib.get_annotations(decorated_class).items():
if get_origin(hint) is ClassVar:
continue

field = [name]
if get_origin(hint) is Annotated:
cls, field_info = get_args(hint)
field.append(cls)
if not isinstance(field_info, CFieldInfo):
raise TypeError(f"expected CFieldInfo in Annotated, got {field_info!r}")

if field_info.bit_width is not None:
field.append(field_info.bit_width)

if field_info.anonymous is True:
anonymous.append(name)
else:
field.append(hint)

fields.append(field)

if endian == 'big':
endian_class = BigEndianStructure
elif endian == 'little':
endian_class = LittleEndianStructure
elif endian == 'native':
endian_class = Structure
else:
raise ValueError(f"expected 'big', 'little', or 'native', but got {endian!r}")

@functools.wraps(decorated_class, updated=())
class _Struct(endian_class):
vars().update(vars(decorated_class))
if align is not None:
_align_ = align
if layout is not None:
_layout_ = layout
if pack is not None:
_pack_ = pack
_fields_ = fields
_anonymous_ = anonymous

return _Struct


def struct(class_or_none=None, /, *, align=None, layout=None, endian='native', pack=None):
process_the_struct = functools.partial(
_process_struct,
align=align,
layout=layout,
endian=endian,
pack=pack
)

if class_or_none is None:
def inner(decorated_class):
return process_the_struct(decorated_class)

return inner

return process_the_struct(class_or_none)

################################################################
# test code

Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,11 @@ def test_deprecation_warning(self):
"The 'January' attribute is deprecated, use 'JANUARY' instead"
):
calendar.January
with self.assertWarnsRegex(
DeprecationWarning,
"The 'February' attribute is deprecated, use 'FEBRUARY' instead"
):
calendar.February

def test_isleap(self):
# Make sure that the return is right for a few years, and
Expand Down
22 changes: 16 additions & 6 deletions Lib/test/test_ctypes/test_anon.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import unittest
import test.support
from ctypes import c_int, Union, Structure, sizeof
from ctypes.util import CFieldInfo, struct
from typing import Annotated
from ._support import StructCheckMixin


class AnonTest(unittest.TestCase, StructCheckMixin):

def test_anon(self):
@test.support.subTests("use_struct_util", [False, True])
def test_anon(self, use_struct_util):
class ANON(Union):
_fields_ = [("a", c_int),
("b", c_int)]
self.check_union(ANON)

class Y(Structure):
_fields_ = [("x", c_int),
("_", ANON),
("y", c_int)]
_anonymous_ = ["_"]
if use_struct_util:
@struct
class Y:
x: c_int
_: Annotated[ANON, CFieldInfo(anonymous=True)]
y: c_int
else:
class Y(Structure):
_fields_ = [("x", c_int),
("_", ANON),
("y", c_int)]
_anonymous_ = ["_"]
self.check_struct(Y)

self.assertEqual(Y.a.offset, sizeof(c_int))
Expand Down
22 changes: 16 additions & 6 deletions Lib/test/test_ctypes/test_bitfields.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
from typing import Annotated
import unittest
from ctypes import (CDLL, Structure, sizeof, POINTER, byref, alignment,
LittleEndianStructure, BigEndianStructure,
Expand All @@ -8,8 +9,9 @@
c_short, c_ushort, c_int, c_uint, c_long, c_ulong,
c_longlong, c_ulonglong,
Union)
from ctypes.util import struct, CFieldInfo
from test import support
from test.support import import_helper
from test.support import import_helper, subTests
from ._support import StructCheckMixin
_ctypes_test = import_helper.import_module("_ctypes_test")

Expand Down Expand Up @@ -126,11 +128,19 @@ def test_generic_checks(self):
self.check_struct(BITS_msvc)
self.check_struct(BITS_gcc)

def test_longlong(self):
class X(Structure):
_fields_ = [("a", c_longlong, 1),
("b", c_longlong, 62),
("c", c_longlong, 1)]
@subTests("use_struct_util", [False, True])
def test_longlong(self, use_struct_util):
if use_struct_util:
@struct
class X:
a: Annotated[c_longlong, CFieldInfo(bit_width=1)]
b: Annotated[c_longlong, CFieldInfo(bit_width=62)]
c: Annotated[c_longlong, CFieldInfo(bit_width=1)]
else:
class X(Structure):
_fields_ = [("a", c_longlong, 1),
("b", c_longlong, 62),
("c", c_longlong, 1)]
self.check_struct(X)

self.assertEqual(sizeof(X), sizeof(c_longlong))
Expand Down
Loading
Loading