https://github.com/python/cpython/commit/edcc07d985c41523ee8386d8c8880e10d0990db9 commit: edcc07d985c41523ee8386d8c8880e10d0990db9 branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-06-29T20:49:19Z summary:
gh-103878: Return a consistent empty value from cancelled file dialogs (GH-152435) On cancellation Tcl may report the empty result as '', () or b'' depending on the platform and Tk version. Normalize it so that askopenfilename(), asksaveasfilename() and askdirectory() always return '' and askopenfilenames() always returns (). Open._fixresult() now distinguishes single from multiple results by the 'multiple' option rather than the result type. Co-authored-by: Christopher Chavez <[email protected]> Co-Authored-By: Claude Opus 4.8 <[email protected]> files: A Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst M Doc/library/dialog.rst M Lib/test/test_tkinter/test_filedialog.py M Lib/tkinter/filedialog.py diff --git a/Doc/library/dialog.rst b/Doc/library/dialog.rst index c3f62117ed61d7..a576cdcfaa2b1e 100644 --- a/Doc/library/dialog.rst +++ b/Doc/library/dialog.rst @@ -159,10 +159,8 @@ listed below: The below functions when called create a modal, native look-and-feel dialog, wait for the user's selection, and return it. The exact return value depends on the function (see below); when the dialog is -cancelled it is an empty string, an empty tuple or ``None``. -The precise type of this empty value may vary between platforms and Tk -versions, so test the result for truth rather than comparing it with a -specific value. +cancelled it is the empty value documented for that function -- an empty +string, an empty tuple, an empty list or ``None``. .. function:: askopenfile(mode="r", **options) askopenfiles(mode="r", **options) @@ -171,7 +169,7 @@ specific value. :func:`askopenfile` returns the opened file object, or ``None`` if the dialog is cancelled. :func:`askopenfiles` returns a list of the opened file objects, or an empty - tuple if cancelled. + list if cancelled. The files are opened in mode *mode* (read-only ``'r'`` by default). .. function:: asksaveasfile(mode="w", **options) diff --git a/Lib/test/test_tkinter/test_filedialog.py b/Lib/test/test_tkinter/test_filedialog.py index 054e719a0f883d..758ef1479ef2cf 100644 --- a/Lib/test/test_tkinter/test_filedialog.py +++ b/Lib/test/test_tkinter/test_filedialog.py @@ -37,6 +37,39 @@ def test_directory(self): self.check(filedialog.Directory, 'tk_chooseDirectory') +class CancelResultTest(AbstractTkTest, unittest.TestCase): + # On cancellation Tcl may report the empty result as '', () or b'' + # (gh-103878). _fixresult() normalizes it to the documented empty value: + # '' for the filename dialogs and () for the multiple-selection dialog. + + def check(self, dialog, expected): + for empty in ('', (), b''): + with self.subTest(empty=empty): + result = dialog._fixresult(self.root, empty) + self.assertEqual(result, expected) + self.assertIs(type(result), type(expected)) + + def test_open(self): + self.check(filedialog.Open(self.root), '') + + def test_saveas(self): + self.check(filedialog.SaveAs(self.root), '') + + def test_directory(self): + self.check(filedialog.Directory(self.root), '') + + def test_openfilenames(self): + self.check(filedialog.Open(self.root, multiple=1), ()) + + def test_results_preserved(self): + # A real selection is returned unchanged. + single = filedialog.Open(self.root) + self.assertEqual(single._fixresult(self.root, '/a/spam'), '/a/spam') + multiple = filedialog.Open(self.root, multiple=1) + self.assertEqual(multiple._fixresult(self.root, ('/a', '/b')), + ('/a', '/b')) + + class FileDialogTest(AbstractTkTest, unittest.TestCase): # The pure-Python FileDialog runs its own modal loop in go(); its logic is # exercised here without entering the loop. diff --git a/Lib/tkinter/filedialog.py b/Lib/tkinter/filedialog.py index e2eff98e601c07..23cb19249a9283 100644 --- a/Lib/tkinter/filedialog.py +++ b/Lib/tkinter/filedialog.py @@ -311,7 +311,9 @@ def _fixoptions(self): pass def _fixresult(self, widget, result): - if result: + if not result: + result = '' # normalize the cancelled result (gh-103878) + else: # keep directory and filename until next time # convert Tcl path objects to strings try: @@ -335,17 +337,16 @@ class Open(_Dialog): command = "tk_getOpenFile" def _fixresult(self, widget, result): - if isinstance(result, tuple): - # multiple results: + if self.options.get("multiple"): + # multiple results: a tuple of filenames + if not isinstance(result, tuple): + result = widget.tk.splitlist(result) result = tuple([getattr(r, "string", r) for r in result]) if result: path, file = os.path.split(result[0]) self.options["initialdir"] = path # don't set initialfile or filename, as we have multiple of these return result - if not widget.tk.wantobjects() and "multiple" in self.options: - # Need to split result explicitly - return self._fixresult(widget, widget.tk.splitlist(result)) return _Dialog._fixresult(self, widget, result) @@ -362,7 +363,9 @@ class Directory(commondialog.Dialog): command = "tk_chooseDirectory" def _fixresult(self, widget, result): - if result: + if not result: + result = '' # normalize the cancelled result (gh-103878) + else: # convert Tcl path objects to strings try: result = result.string @@ -420,12 +423,7 @@ def askopenfiles(mode = "r", **options): """ files = askopenfilenames(**options) - if files: - ofiles=[] - for filename in files: - ofiles.append(open(filename, mode)) - files=ofiles - return files + return [open(filename, mode) for filename in files] def asksaveasfile(mode = "w", **options): diff --git a/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst b/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst new file mode 100644 index 00000000000000..dd5b8c2545fe65 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst @@ -0,0 +1,8 @@ +The :mod:`tkinter.filedialog` functions that return a filename +(:func:`~tkinter.filedialog.askopenfilename`, +:func:`~tkinter.filedialog.asksaveasfilename` and +:func:`~tkinter.filedialog.askdirectory`) now consistently return an empty +string when the dialog is cancelled, instead of an empty tuple or ``b''`` +on some platforms. :func:`~tkinter.filedialog.askopenfilenames` likewise +always returns an empty tuple, and :func:`~tkinter.filedialog.askopenfiles` +an empty list. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
