diff --git a/Doc/library/json.rst b/Doc/library/json.rst index 5e8c452a5ab9a1..ddd12a002f7416 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -261,6 +261,8 @@ Basic Usage into JSON and then back into a dictionary, the dictionary may not equal the original one. That is, ``loads(dumps(x)) != x`` if x has non-string keys. + *sort_keys* sorts the keys before they are coerced to strings, + so numeric keys are sorted by value, not by their string representation. .. function:: load(fp, *, cls=None, object_hook=None, parse_float=None, \ parse_int=None, parse_constant=None, \ diff --git a/Doc/library/tk.rst b/Doc/library/tk.rst index e27af48ba7ac48..9ca26a5dfa7659 100644 --- a/Doc/library/tk.rst +++ b/Doc/library/tk.rst @@ -34,7 +34,7 @@ alternative `GUI frameworks and tools ', ('html', None, None)), + (']>', ('html', None, None)), + ('', ('html', None, 'a.dtd')), + (']>', + ('html', None, 'a.dtd')), + ('', ('html', '-//P', 'a.dtd')), + ("", + ('html', '-//P', 'a.dtd')), + ]: + with self.subTest(doctype=doctype): + parser = ET.XMLParser(target=DoctypeParser()) + parser.feed(doctype + '') + self.assertEqual(parser.close(), expected) + def test_builder_lookup_errors(self): class RaisingBuilder: def __init__(self, raise_in=None, what=ValueError): diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index 5b663a4e456456..1349e49fbd68a5 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -104,7 +104,8 @@ def __init__(self, root=None, font=None, name=None, exists=False, if exists: self.name = name # confirm font exists - if self.name not in tk.splitlist(tk.call("font", "names")): + name = getattr(name, 'string', name) # can be a Tcl object + if name not in tk.splitlist(tk.call("font", "names")): raise tkinter._tkinter.TclError( "named font %s does not already exist" % (self.name,)) # if font config info supplied, apply it @@ -123,11 +124,11 @@ def __init__(self, root=None, font=None, name=None, exists=False, self._call = tk.call def __str__(self): - # A wrapped description is a list or tuple, not a string; format it as - # a Tcl word so it can be used as an option value (as ttk does). - if isinstance(self.name, str): - return self.name - return tkinter._join(self.name) + # A wrapped description can be a list or tuple; format it as a Tcl + # word so it can be used as an option value (as ttk does). + if isinstance(self.name, (list, tuple)): + return tkinter._join(self.name) + return str(self.name) def __repr__(self): return f"<{self.__class__.__module__}.{self.__class__.__qualname__}" \ @@ -136,7 +137,13 @@ def __repr__(self): def __eq__(self, other): if not isinstance(other, Font): return NotImplemented - return self.name == other.name and self._tk == other._tk + name = self.name + other_name = other.name + if type(name) is not type(other_name): + # A Tcl object does not compare equal to a string. + name = getattr(name, 'string', name) + other_name = getattr(other_name, 'string', other_name) + return name == other_name and self._tk == other._tk def __getitem__(self, key): return self.cget(key) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index bed8c27df5a384..ce98e4dc24a0d3 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -1591,10 +1591,10 @@ def __init__(self, *, target=None, encoding=None): parser.CommentHandler = target.comment if hasattr(target, 'pi'): parser.ProcessingInstructionHandler = target.pi + parser.StartDoctypeDeclHandler = self._start_doctype # Configure pyexpat: buffering, new-style attribute handling. parser.buffer_text = 1 parser.ordered_attributes = 1 - self._doctype = None self.entity = {} try: self.version = "Expat %d.%d.%d" % expat.version_info @@ -1713,38 +1713,15 @@ def _default(self, text): err.lineno = self.parser.ErrorLineNumber err.offset = self.parser.ErrorColumnNumber raise err - elif prefix == "<" and text[:9] == "": - self._doctype = None - return - text = text.strip(_XML_WHITESPACE) - if not text: - return - self._doctype.append(text) - n = len(self._doctype) - if n > 2: - type = self._doctype[1] - if type == "PUBLIC" and n == 4: - name, type, pubid, system = self._doctype - if pubid: - pubid = pubid[1:-1] - elif type == "SYSTEM" and n == 3: - name, type, system = self._doctype - pubid = None - else: - return - if hasattr(self.target, "doctype"): - self.target.doctype(name, pubid, system[1:-1]) - elif hasattr(self, "doctype"): - warnings.warn( - "The doctype() method of XMLParser is ignored. " - "Define doctype() method on the TreeBuilder target.", - RuntimeWarning) - - self._doctype = None + + def _start_doctype(self, name, system, pubid, has_internal_subset): + if hasattr(self.target, "doctype"): + self.target.doctype(name, pubid, system) + elif hasattr(self, "doctype"): + warnings.warn( + "The doctype() method of XMLParser is ignored. " + "Define doctype() method on the TreeBuilder target.", + RuntimeWarning) def feed(self, data): """Feed encoded data to parser.""" diff --git a/Misc/NEWS.d/next/Documentation/2026-06-18-16-58-52.gh-issue-59551.oNkjTG.rst b/Misc/NEWS.d/next/Documentation/2026-06-18-16-58-52.gh-issue-59551.oNkjTG.rst new file mode 100644 index 00000000000000..9842e85f9c2c21 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2026-06-18-16-58-52.gh-issue-59551.oNkjTG.rst @@ -0,0 +1 @@ +Rename Doc/library/dialog.rst to tkinter.dialogs.rst. diff --git a/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst b/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst new file mode 100644 index 00000000000000..9df2868c30f16d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst @@ -0,0 +1,3 @@ +Fix :func:`tkinter.font.nametofont` and the :class:`tkinter.font.Font` +constructor for a font name or description returned by Tk as a Tcl object, +for example by :meth:`ttk.Style.lookup() `. diff --git a/Misc/NEWS.d/next/Library/2026-09-11-12-00-00.gh-issue-156955.bLtmAp.rst b/Misc/NEWS.d/next/Library/2026-09-11-12-00-00.gh-issue-156955.bLtmAp.rst new file mode 100644 index 00000000000000..2a452a2a6e1652 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-11-12-00-00.gh-issue-156955.bLtmAp.rst @@ -0,0 +1,3 @@ +Speed up :func:`csv.writer` by caching the set of characters that need +quoting or escaping in the dialect. Writing long fields is now up to 5 times +faster. diff --git a/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst b/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst new file mode 100644 index 00000000000000..53f2bf561eefbb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst @@ -0,0 +1,4 @@ +Fix the Python implementation of :class:`xml.etree.ElementTree.XMLParser`: +the ``doctype()`` method of the target is now called for a document type +declaration without an external identifier, like ````, +as in the C implementation. diff --git a/Modules/_csv.c b/Modules/_csv.c index c640f2d36a8464..6af66c3f09a03b 100644 --- a/Modules/_csv.c +++ b/Modules/_csv.c @@ -117,7 +117,12 @@ typedef struct { Py_UCS4 quotechar; /* quote character */ Py_UCS4 escapechar; /* escape character */ PyObject *lineterminator; /* string to write between records */ - + /* Cache for the writer: bit c is set if the ASCII character c needs + quoting or escaping (delimiter, quotechar, escapechar, '\r', '\n' + and the characters of lineterminator). */ + uint64_t special_chars[2]; + /* Whether any of the special characters is non-ASCII. */ + bool nonascii_special; } DialectObj; typedef struct { @@ -332,6 +337,54 @@ _set_str(const char *name, PyObject **target, PyObject *src, const char *dflt) return 0; } +static void +dialect_add_special_char(DialectObj *self, Py_UCS4 c) +{ + if (c == NOT_SET) { + return; + } + if (c < 128) { + self->special_chars[c / 64] |= (uint64_t)1 << (c % 64); + } + else { + self->nonascii_special = true; + } +} + +static void +dialect_init_special_chars_cache(DialectObj *self) +{ + self->special_chars[0] = self->special_chars[1] = 0; + self->nonascii_special = false; + dialect_add_special_char(self, self->delimiter); + dialect_add_special_char(self, self->quotechar); + dialect_add_special_char(self, self->escapechar); + dialect_add_special_char(self, '\r'); + dialect_add_special_char(self, '\n'); + PyObject *lt = self->lineterminator; + for (Py_ssize_t i = 0; i < PyUnicode_GET_LENGTH(lt); i++) { + dialect_add_special_char(self, PyUnicode_READ_CHAR(lt, i)); + } +} + +/* Whether the character needs quoting or escaping by the writer. */ +static inline bool +dialect_is_special_char(DialectObj *self, Py_UCS4 c) +{ + if (c < 128) { + return (self->special_chars[c / 64] >> (c % 64)) & 1; + } + if (!self->nonascii_special) { + return false; + } + return (c == self->delimiter || + c == self->quotechar || + c == self->escapechar || + PyUnicode_FindChar(self->lineterminator, c, 0, + PyUnicode_GET_LENGTH(self->lineterminator), + 1) >= 0); +} + static int dialect_check_quoting(int quoting) { @@ -558,6 +611,7 @@ dialect_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { goto err; } + dialect_init_special_chars_cache(self); ret = Py_NewRef(self); err: @@ -1208,14 +1262,7 @@ join_append_data(WriterObj *self, int field_kind, const void *field_data, Py_UCS4 c = PyUnicode_READ(field_kind, field_data, i); int want_escape = 0; - if (c == dialect->delimiter || - c == dialect->escapechar || - c == dialect->quotechar || - c == '\n' || - c == '\r' || - PyUnicode_FindChar( - dialect->lineterminator, c, 0, - PyUnicode_GET_LENGTH(dialect->lineterminator), 1) >= 0) { + if (dialect_is_special_char(dialect, c)) { if (dialect->quoting == QUOTE_NONE) want_escape = 1; else {