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
7 changes: 6 additions & 1 deletion Doc/library/calendar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -580,9 +580,14 @@ The :mod:`!calendar` module defines the following exceptions:

.. exception:: IllegalMonthError(month)

A subclass of :exc:`ValueError`,
A subclass of :exc:`ValueError` and :exc:`IndexError`,
raised when the given month number is outside of the range 1-12 (inclusive).

.. versionchanged:: 3.12
:exc:`IllegalMonthError` is now also a subclass of
:exc:`ValueError`. New code should avoid catching
:exc:`IndexError`.

.. attribute:: month

The invalid month number.
Expand Down
36 changes: 34 additions & 2 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Format(enum.IntEnum):
"__cell__",
"__owner__",
"__stringifier_dict__",
"__resolved_str_cache__",
)


Expand Down Expand Up @@ -94,6 +95,7 @@ def __init__(
# value later.
self.__code__ = None
self.__ast_node__ = None
self.__resolved_str_cache__ = None

def __init_subclass__(cls, /, *args, **kwds):
raise TypeError("Cannot subclass ForwardRef")
Expand All @@ -113,7 +115,7 @@ def evaluate(
"""
match format:
case Format.STRING:
return self.__forward_arg__
return self.__resolved_str__
case Format.VALUE:
is_forwardref_format = False
case Format.FORWARDREF:
Expand Down Expand Up @@ -258,6 +260,24 @@ def __forward_arg__(self):
"Attempted to access '__forward_arg__' on an uninitialized ForwardRef"
)

@property
def __resolved_str__(self):
# __forward_arg__ with any names from __extra_names__ replaced
# with the type_repr of the value they represent
if self.__resolved_str_cache__ is None:
resolved_str = self.__forward_arg__
names = self.__extra_names__

if names:
visitor = _ExtraNameFixer(names)
ast_expr = ast.parse(resolved_str, mode="eval").body
node = visitor.visit(ast_expr)
resolved_str = ast.unparse(node)

self.__resolved_str_cache__ = resolved_str

return self.__resolved_str_cache__

@property
def __forward_code__(self):
if self.__code__ is not None:
Expand Down Expand Up @@ -321,7 +341,7 @@ def __repr__(self):
extra.append(", is_class=True")
if self.__owner__ is not None:
extra.append(f", owner={self.__owner__!r}")
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
return f"ForwardRef({self.__resolved_str__!r}{''.join(extra)})"


_Template = type(t"")
Expand Down Expand Up @@ -357,6 +377,7 @@ def __init__(
self.__cell__ = cell
self.__owner__ = owner
self.__stringifier_dict__ = stringifier_dict
self.__resolved_str_cache__ = None # Needed for ForwardRef

def __convert_to_ast(self, other):
if isinstance(other, _Stringifier):
Expand Down Expand Up @@ -1163,3 +1184,14 @@ def _get_dunder_annotations(obj):
if not isinstance(ann, dict):
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
return ann


class _ExtraNameFixer(ast.NodeTransformer):
"""Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation"""
def __init__(self, extra_names):
self.extra_names = extra_names

def visit_Name(self, node: ast.Name):
if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel:
node = ast.Name(id=type_repr(new_name))
return node
3 changes: 2 additions & 1 deletion Lib/json/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ def main():
infile = open(options.infile, encoding='utf-8')
try:
if options.json_lines:
objs = (json.loads(line) for line in infile)
lines = infile.readlines()
objs = (json.loads(line) for line in lines)
else:
objs = (json.load(infile),)
finally:
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1961,6 +1961,15 @@ def test_forward_repr(self):
"typing.List[ForwardRef('int', owner='class')]",
)

def test_forward_repr_extra_names(self):
def f(a: undefined | str): ...

annos = get_annotations(f, format=Format.FORWARDREF)

self.assertRegex(
repr(annos['a']), r"ForwardRef\('undefined \| str'.*\)"
)

def test_forward_recursion_actually(self):
def namespace1():
a = ForwardRef("A")
Expand Down Expand Up @@ -2037,6 +2046,17 @@ def test_evaluate_string_format(self):
fr = ForwardRef("set[Any]")
self.assertEqual(fr.evaluate(format=Format.STRING), "set[Any]")

def test_evaluate_string_format_extra_names(self):
# Test that internal extra_names are replaced when evaluating as strings
def f(a: unknown | str | int | list[str] | tuple[int, ...]): ...

fr = get_annotations(f, format=Format.FORWARDREF)['a']
# Test the cache is not populated before access
self.assertIsNone(fr.__resolved_str_cache__)

self.assertEqual(fr.evaluate(format=Format.STRING), "unknown | str | int | list[str] | tuple[int, ...]")
self.assertEqual(fr.__resolved_str_cache__, "unknown | str | int | list[str] | tuple[int, ...]")

def test_evaluate_forwardref_format(self):
fr = ForwardRef("undef")
evaluated = fr.evaluate(format=Format.FORWARDREF)
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 @@ -495,12 +495,17 @@ def test_formatmonth(self):
calendar.TextCalendar().formatmonth(0, 2),
result_0_02_text
)

def test_formatmonth_with_invalid_month(self):
with self.assertRaises(calendar.IllegalMonthError):
calendar.TextCalendar().formatmonth(2017, 13)
with self.assertRaises(calendar.IllegalMonthError):
calendar.TextCalendar().formatmonth(2017, -1)

def test_illegal_month_error_bases(self):
self.assertIsSubclass(calendar.IllegalMonthError, ValueError)
self.assertIsSubclass(calendar.IllegalMonthError, IndexError)

def test_formatmonthname_with_year(self):
self.assertEqual(
calendar.HTMLCalendar().formatmonthname(2004, 1, withyear=True),
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_json/json_lines.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"ingredients":["frog", "water", "chocolate", "glucose"]}
{"ingredients":["chocolate","steel bolts"]}
9 changes: 9 additions & 0 deletions Lib/test/test_json/test_tool.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import errno
import pathlib
import os
import sys
import textwrap
Expand Down Expand Up @@ -157,6 +158,14 @@ def test_jsonlines(self):
self.assertEqual(process.stdout, self.jsonlines_expect)
self.assertEqual(process.stderr, '')

@force_not_colorized
def test_jsonlines_from_file(self):
jsonl = pathlib.Path(__file__).parent / 'json_lines.jsonl'
args = sys.executable, '-m', self.module, '--json-lines', jsonl
process = subprocess.run(args, capture_output=True, text=True, check=True)
self.assertEqual(process.stdout, self.jsonlines_expect)
self.assertEqual(process.stderr, '')

def test_help_flag(self):
rc, out, err = assert_python_ok('-m', self.module, '-h',
PYTHON_COLORS='0')
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_xml_etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -3271,6 +3271,16 @@ def test_findtext_with_mutating(self):
e.extend([ET.Element('bar')])
e.findtext(cls(e, 'x'))

def test_findtext_with_mutating_non_none_text(self):
for cls in [MutationDeleteElementPath, MutationClearElementPath]:
with self.subTest(cls):
e = ET.Element('foo')
child = ET.Element('bar')
child.text = str(object())
e.append(child)
del child
repr(e.findtext(cls(e, 'x')))

def test_findtext_with_error(self):
e = ET.Element('foo')
e.extend([ET.Element('bar')])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Document that :class:`calendar.IllegalMonthError` is a subclass of both
:exc:`ValueError` and :exc:`IndexError` since Python 3.12.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix "I/O operation on closed file" when parsing JSON Lines file with
:mod:`JSON CLI <json.tool>`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:mod:`xml.etree.ElementTree`: Fix a use-after-free in
:meth:`Element.findtext <xml.etree.ElementTree.Element.findtext>` when the
element tree is mutated concurrently during the search.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``ForwardRef`` objects that contain internal names to represent known objects now show the ``type_repr`` of the known object rather than the internal ``__annotationlib_name_x__`` name when evaluated as strings.
22 changes: 9 additions & 13 deletions Modules/_elementtree.c
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ element_get_attrib(ElementObject* self)
LOCAL(PyObject*)
element_get_text(ElementObject* self)
{
/* return borrowed reference to text attribute */
/* return new reference to text attribute */

PyObject *res = self->text;

Expand All @@ -588,13 +588,13 @@ element_get_text(ElementObject* self)
}
}

return res;
return Py_NewRef(res);
}

LOCAL(PyObject*)
element_get_tail(ElementObject* self)
{
/* return borrowed reference to text attribute */
/* return new reference to tail attribute */

PyObject *res = self->tail;

Expand All @@ -609,7 +609,7 @@ element_get_tail(ElementObject* self)
}
}

return res;
return Py_NewRef(res);
}

static PyObject*
Expand Down Expand Up @@ -1359,9 +1359,9 @@ _elementtree_Element_findtext_impl(ElementObject *self, PyTypeObject *cls,
PyObject *text = element_get_text((ElementObject *)item);
Py_DECREF(item);
if (text == Py_None) {
Py_DECREF(text);
return Py_GetConstant(Py_CONSTANT_EMPTY_STR);
}
Py_XINCREF(text);
return text;
}
Py_DECREF(item);
Expand Down Expand Up @@ -2064,16 +2064,14 @@ static PyObject*
element_text_getter(PyObject *op, void *closure)
{
ElementObject *self = _Element_CAST(op);
PyObject *res = element_get_text(self);
return Py_XNewRef(res);
return element_get_text(self);
}

static PyObject*
element_tail_getter(PyObject *op, void *closure)
{
ElementObject *self = _Element_CAST(op);
PyObject *res = element_get_tail(self);
return Py_XNewRef(res);
return element_get_tail(self);
}

static PyObject*
Expand Down Expand Up @@ -2316,16 +2314,14 @@ elementiter_next(PyObject *op)
continue;

gettext:
Py_DECREF(elem);
if (!text) {
Py_DECREF(elem);
return NULL;
}
if (text == Py_None) {
Py_DECREF(elem);
Py_DECREF(text);
}
else {
Py_INCREF(text);
Py_DECREF(elem);
rc = PyObject_IsTrue(text);
if (rc > 0)
return text;
Expand Down
Loading