From 4c0bd4f04178e74e128d5b218078b51c96a2004b Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Wed, 8 Jul 2026 19:28:05 +0800 Subject: [PATCH 1/2] gh-153333: Read tkinter profile scripts with the source file's encoding Tk.readprofile execed the user's ~/.CLASSNAME.py and ~/.BASENAME.py profile scripts read via open() with no encoding argument, i.e. the locale default encoding. That emits an EncodingWarning under -X warn_default_encoding and mis-decodes a profile whose source carries a PEP 263 coding cookie. Read them with tokenize.open() instead, which decodes using the encoding detected from the file (its coding cookie, else UTF-8), matching how Python reads source. As an incidental effect of using the context-manager form, each script is now closed promptly after reading instead of at garbage collection, removing a ResourceWarning. --- Lib/test/test_tkinter/test_misc.py | 20 +++++++++++++++++++ Lib/tkinter/__init__.py | 7 +++++-- ...-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 5f75b8d245b3e9..92f6829c3aa3e4 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -1,9 +1,11 @@ import collections.abc import functools +import os import platform import sys import textwrap import unittest +import warnings import weakref import tkinter from tkinter import TclError @@ -364,6 +366,24 @@ def test_getdouble(self): self.assertEqual(self.root.getdouble(3), 3.0) self.assertRaises(ValueError, self.root.getdouble, 'spam') + def test_readprofile(self): + # gh-153333: readprofile() reads the profile scripts with the source + # file's encoding (here a Latin-1 coding cookie), not the locale + # default encoding. + name = 'readprofiletest' + script = ("# -*- coding: latin-1 -*-\n" + "self.readprofile_result = 'caf\xe9'\n") + self.addCleanup(self.root.__dict__.pop, 'readprofile_result', None) + with os_helper.temp_dir() as home: + with open(os.path.join(home, '.%s.py' % name), 'wb') as f: + f.write(script.encode('latin-1')) + with os_helper.EnvironmentVarGuard() as env: + env['HOME'] = home + with warnings.catch_warnings(): + warnings.simplefilter('error', EncodingWarning) + self.root.readprofile(name, name) + self.assertEqual(self.root.readprofile_result, 'caf\xe9') + def test_getvar(self): self.root.setvar('test_var', 'hello') self.assertEqual(self.root.getvar('test_var'), 'hello') diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 83d11a7695ffbe..4fa28cc5f5d35c 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -2739,6 +2739,7 @@ def readprofile(self, baseName, className): the Tcl Interpreter and calls exec on the contents of .BASENAME.py and .CLASSNAME.py if such a file exists in the home directory.""" import os + import tokenize if 'HOME' in os.environ: home = os.environ['HOME'] else: home = os.curdir class_tcl = os.path.join(home, '.%s.tcl' % className) @@ -2750,11 +2751,13 @@ def readprofile(self, baseName, className): if os.path.isfile(class_tcl): self.tk.call('source', class_tcl) if os.path.isfile(class_py): - exec(open(class_py).read(), dir) + with tokenize.open(class_py) as f: + exec(f.read(), dir) if os.path.isfile(base_tcl): self.tk.call('source', base_tcl) if os.path.isfile(base_py): - exec(open(base_py).read(), dir) + with tokenize.open(base_py) as f: + exec(f.read(), dir) def report_callback_exception(self, exc, val, tb): """Report callback exception on sys.stderr. diff --git a/Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst b/Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst new file mode 100644 index 00000000000000..680c7d49e575d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst @@ -0,0 +1,2 @@ +The ``readprofile`` method of :class:`tkinter.Tk` now reads the user's +profile scripts using :func:`tokenize.open`. From edb85b61f443fae7361d7ce06beee1a1730ab01c Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Thu, 9 Jul 2026 16:04:21 +0800 Subject: [PATCH 2/2] Open profile scripts in binary mode Pass the raw bytes to exec() so the compiler applies the source file's encoding declaration, instead of detecting it with tokenize.open(). --- Lib/tkinter/__init__.py | 5 ++--- .../Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 4fa28cc5f5d35c..a194a6199bc4d8 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -2739,7 +2739,6 @@ def readprofile(self, baseName, className): the Tcl Interpreter and calls exec on the contents of .BASENAME.py and .CLASSNAME.py if such a file exists in the home directory.""" import os - import tokenize if 'HOME' in os.environ: home = os.environ['HOME'] else: home = os.curdir class_tcl = os.path.join(home, '.%s.tcl' % className) @@ -2751,12 +2750,12 @@ def readprofile(self, baseName, className): if os.path.isfile(class_tcl): self.tk.call('source', class_tcl) if os.path.isfile(class_py): - with tokenize.open(class_py) as f: + with open(class_py, 'rb') as f: exec(f.read(), dir) if os.path.isfile(base_tcl): self.tk.call('source', base_tcl) if os.path.isfile(base_py): - with tokenize.open(base_py) as f: + with open(base_py, 'rb') as f: exec(f.read(), dir) def report_callback_exception(self, exc, val, tb): diff --git a/Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst b/Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst index 680c7d49e575d6..6fcbd590d0d9d3 100644 --- a/Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst +++ b/Misc/NEWS.d/next/Library/2026-07-08-21-15-00.gh-issue-153333.Kp3mZq.rst @@ -1,2 +1,3 @@ The ``readprofile`` method of :class:`tkinter.Tk` now reads the user's -profile scripts using :func:`tokenize.open`. +profile scripts using the encoding declared in the file, instead of the +locale encoding.