From: Serhiy Storchaka Date: Mon, 29 Jun 2026 20:49:19 +0000 (+0300) Subject: gh-103878: Return a consistent empty value from cancelled file dialogs (GH-152435) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=edcc07d985c41523ee8386d8c8880e10d0990db9;p=thirdparty%2FPython%2Fcpython.git 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 Co-Authored-By: Claude Opus 4.8 --- diff --git a/Doc/library/dialog.rst b/Doc/library/dialog.rst index c3f62117ed61..a576cdcfaa2b 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 054e719a0f88..758ef1479ef2 100644 --- a/Lib/test/test_tkinter/test_filedialog.py +++ b/Lib/test/test_tkinter/test_filedialog.py @@ -37,6 +37,39 @@ class NativeDialogTest(AbstractTkTest, unittest.TestCase): 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 e2eff98e601c..23cb19249a92 100644 --- a/Lib/tkinter/filedialog.py +++ b/Lib/tkinter/filedialog.py @@ -311,7 +311,9 @@ class _Dialog(commondialog.Dialog): 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 000000000000..dd5b8c2545fe --- /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.