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
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ repos:
entry: Space found in path, move to Misc/NEWS.d/next/Core_and_Builtins/
files: Misc/NEWS.d/next/Core and Builtins/20.*.rst

- repo: local
hooks:
- id: check-capi-macros
name: Check C API macros start with Py
language: python
entry: python Tools/build/check_capi_macros.py
pass_filenames: false
files: ^(Include/(cpython/)?[^/]+\.h|pyconfig\.h\.in|Tools/build/check_capi_macros)

- repo: meta
hooks:
- id: check-hooks-apply
Expand Down
2 changes: 1 addition & 1 deletion Doc/library/ipaddress.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ write code that handles both IP versions correctly. Address objects are

.. attribute:: ipv6_mapped

:class:`IPv4Address` object representing the IPv4-mapped IPv6 address. See :RFC:`4291`.
:class:`IPv6Address` object representing the IPv4-mapped IPv6 address. See :RFC:`4291`.

.. versionadded:: 3.13

Expand Down
10 changes: 10 additions & 0 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,16 @@ Special read-only attributes
* - .. attribute:: codeobject.co_firstlineno
- The line number of the first line of the function

* - .. attribute:: codeobject.co_linetable
- A :class:`bytes` object containing encoded source location information.
The exact format is an implementation detail and may change between
Python versions. Use :meth:`~codeobject.co_lines` and
:meth:`~codeobject.co_positions` for supported access to line and
position information. To create a modified copy of a code object,
use :meth:`~codeobject.replace`.

.. versionadded:: 3.10

* - .. attribute:: codeobject.co_stacksize
- The required stack size of the code object

Expand Down
4 changes: 4 additions & 0 deletions Lib/curses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

from _curses import *
import _curses
import os as _os
import sys as _sys

Expand All @@ -33,6 +34,7 @@ def initscr():
if key.startswith(('ACS_', 'WACS_')) or key in ('LINES', 'COLS'):
setattr(curses, key, value)
return stdscr
initscr.__doc__ = _curses.initscr.__doc__

# newterm() is wrapped for the same reason as initscr(): the ACS_* and WACS_*
# constants and LINES/COLS only become available once a terminal is
Expand All @@ -50,6 +52,7 @@ def newterm(type=None, fd=None, infd=None, /):
if key.startswith(('ACS_', 'WACS_')) or key in ('LINES', 'COLS'):
setattr(curses, key, value)
return screen
newterm.__doc__ = _curses.newterm.__doc__

# This is a similar wrapper for start_color(), which adds the COLORS and
# COLOR_PAIRS variables which are only available after start_color() is
Expand All @@ -60,6 +63,7 @@ def start_color():
_curses.start_color()
curses.COLORS = _curses.COLORS
curses.COLOR_PAIRS = _curses.COLOR_PAIRS
start_color.__doc__ = _curses.start_color.__doc__

# Import Python has_key() implementation if _curses doesn't contain has_key()

Expand Down
11 changes: 9 additions & 2 deletions Modules/_cursesmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -5536,7 +5536,14 @@ static PyMethodDef PyCursesScreen_methods[] = {
{NULL, NULL} /* sentinel */
};

PyDoc_STRVAR(PyCursesScreen_Type_doc,
"A curses screen.\n"
"\n"
"Screen objects are returned by newterm() and new_prescr(), and represent\n"
"a terminal together with its standard window, stdscr.");

static PyType_Slot PyCursesScreen_Type_slots[] = {
{Py_tp_doc, (void *)PyCursesScreen_Type_doc},
{Py_tp_methods, PyCursesScreen_methods},
{Py_tp_getset, PyCursesScreen_getsets},
{Py_tp_dealloc, PyCursesScreen_dealloc},
Expand Down Expand Up @@ -6931,12 +6938,12 @@ _curses.initscr

Initialize the library.

Return a WindowObject which represents the whole screen.
Return a window object which represents the whole screen.
[clinic start generated code]*/

static PyObject *
_curses_initscr_impl(PyObject *module)
/*[clinic end generated code: output=619fb68443810b7b input=514f4bce1821f6b5]*/
/*[clinic end generated code: output=619fb68443810b7b input=e2bf3a061b7d948a]*/
{
WINDOW *win;

Expand Down
4 changes: 2 additions & 2 deletions Modules/clinic/_cursesmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 170 additions & 0 deletions Tools/build/check_capi_macros.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""
Check that all macros defined by the Python C API have a name starting with
"Py". Ignore names listed by check_capi_macros_ignored.txt: macros with an
invalid name, added before this script was created.

Python C API:

* Include/*.h
* Include/cpython/*.h
* pyconfig.h.in
"""

import difflib
import glob
import os.path
import re
import sys

EXCLUDE_HEADERS = {
# Header files not included by Python.h and ignored by this script
'dynamic_annotations.h',
'errcode.h',
'opcode.h',
'opcode_ids.h',
'osdefs.h',
'pyexpat.h',
'structmember.h',

# Header files not included by Python.h but parsed by this script:
# - datetime.h
# - frameobject.h
# - marshal.h
# - py_curses.h
# - pydtrace.h
}

TOOLS_BUILD_DIR = os.path.abspath(os.path.dirname(__file__))
SRC_DIR = os.path.dirname(os.path.dirname(TOOLS_BUILD_DIR))
IGNORED_FILENAME = os.path.join(TOOLS_BUILD_DIR, 'check_capi_macros_ignored.txt')

DEFINE_REGEX = re.compile(r'\s*#\s*(?P<directive>define|undef)\s+(?P<macro>.*)')
PYTHON_PREFIX = re.compile(r'(Py|PY|_Py|_PY)')
NAME_REGEX = re.compile(r'([A-Za-z_][A-Za-z0-9_]*)\b')

CAUSES_BY_DIRECTIVE = {
"define": "defined",
"undef": "undefined",
}


def parse_file(filename, names):
with open(filename, encoding='utf8') as fp:
for lineno, line in enumerate(fp, start=1):
# Check for '#define MACRO'
match = DEFINE_REGEX.match(line)
if not match:
continue
macro = match['macro']
directive = match['directive']
cause = CAUSES_BY_DIRECTIVE.get(directive, directive)

if PYTHON_PREFIX.match(macro):
continue

match = NAME_REGEX.match(macro)
if not match:
print(f"ERROR: {filename}: Unable to parse {line!r}")
sys.exit(1)
name = match[1]
names.append((name, filename, lineno, cause))


def get_ignored_names():
ignored = []
with open(IGNORED_FILENAME, encoding='utf8') as fp:
for line in fp:
name = line.strip()
if name.startswith('#'):
# Ignore comment
continue
if name:
ignored.append(name)
return ignored


def main():
failure = False

# Parse header files
include_dir = os.path.join(SRC_DIR, 'Include')
files = glob.glob(os.path.join(include_dir, '*.h'))
files = [filename for filename in files
if os.path.basename(filename) not in EXCLUDE_HEADERS]
files.extend(glob.glob(os.path.join(include_dir, 'cpython', '*.h')))
files.append(os.path.join(SRC_DIR, 'pyconfig.h.in'))
names = [] # list of (name: str, filename: str, lineno: int, cause: str)
for filename in files:
parse_file(filename, names)

# Parse ignore list
ignored = get_ignored_names()

# Check if the sorted list has duplicated entries
if len(set(ignored)) != len(ignored):
print(f"ERROR: {IGNORED_FILENAME} list contains duplicated entries:")
print()
seen = set()
for name in ignored:
if name not in seen:
seen.add(name)
continue
print(f"- {name}")
print()
failure = True

# Check if the sorted list is sorted
ignored_sorted = sorted(ignored)
if ignored_sorted != ignored:
print(f"ERROR: {IGNORED_FILENAME} list is not sorted")
print()
diff = difflib.unified_diff(ignored, ignored_sorted,
fromfile=IGNORED_FILENAME,
tofile=IGNORED_FILENAME,
lineterm='')
for line in diff:
print(line)
print()
failure = True

# Check for outdated ignore list
names_set = {name for name, filename, lineno, cause in names}
ignored = set(ignored)
outdated = ignored - names_set
if outdated:
print(f"ERROR: {IGNORED_FILENAME} is outdated, "
"the following macros can be removed:")
print()
for name in sorted(outdated):
print(f"- {name}")
print()
print(f"Total: {len(outdated)} macros")
print()
failure = True

# Check for new macros
new_macros = names_set - ignored
if new_macros:
print('ERROR: the Python C API defines the following new macros:')
print()
count = 0
for name, filename, lineno, cause in sorted(names):
if name in ignored:
continue
print(f"- {name} {cause} at {filename}:{lineno}")
count += 1
print()
print(f"Total: {count} macros")
failure = True

if not failure:
print("OK: the ignore list is up to date and sorted")
print("OK: the Python C API only defines macros with names "
f"starting with Py (ignoring {len(ignored)} macros)")
sys.exit(0)

sys.exit(1)


if __name__ == "__main__":
main()
Loading
Loading