From 4c0bd4f04178e74e128d5b218078b51c96a2004b Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Wed, 8 Jul 2026 19:28:05 +0800 Subject: [PATCH 1/3] 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 5f75b8d245b3e99..92f6829c3aa3e4c 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 83d11a7695ffbe3..4fa28cc5f5d35ce 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 000000000000000..680c7d49e575d6b --- /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/3] 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 4fa28cc5f5d35ce..a194a6199bc4d8c 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 680c7d49e575d6b..6fcbd590d0d9d38 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. From 5e9d4233ae8ee9d6ab3e94e4c4845085fafdb17b Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sat, 11 Jul 2026 09:43:23 +0800 Subject: [PATCH 3/3] gh-153333: Move readprofile test to TkTest and cover two encodings --- Lib/test/test_tkinter/test_misc.py | 39 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 92f6829c3aa3e4c..10fbf8356671c56 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -5,7 +5,6 @@ import sys import textwrap import unittest -import warnings import weakref import tkinter from tkinter import TclError @@ -366,24 +365,6 @@ 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') @@ -829,6 +810,26 @@ def test_iterable_protocol(self): class TkTest(AbstractTkTest, unittest.TestCase): + def test_readprofile(self): + # gh-153333: profile scripts are decoded with their own coding cookie, + # not the locale encoding. Two cookies so no locale can mask the bug. + profiles = { + '.RpClass.py': ('latin-1', "self._rp_latin1 = 'caf\xe9'"), + '.rpbase.py': ('utf-8', "self._rp_utf8 = 'caf\xe9'"), + } + self.addCleanup(self.root.__dict__.pop, '_rp_latin1', None) + self.addCleanup(self.root.__dict__.pop, '_rp_utf8', None) + with (os_helper.temp_dir() as home, + os_helper.EnvironmentVarGuard() as env): + env['HOME'] = home + for filename, (encoding, body) in profiles.items(): + script = '# -*- coding: %s -*-\n%s\n' % (encoding, body) + with open(os.path.join(home, filename), 'wb') as f: + f.write(script.encode(encoding)) + self.root.readprofile('rpbase', 'RpClass') + self.assertEqual(self.root._rp_latin1, 'caf\xe9') + self.assertEqual(self.root._rp_utf8, 'caf\xe9') + def test_className(self): # The className argument sets the class of the root window. Tk # title-cases it: the first letter is upper-cased, the rest lower-cased.