diff --git a/Doc/library/dialog.rst b/Doc/library/dialog.rst index c3f62117ed61d78..a576cdcfaa2b1e9 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 054e719a0f883d9..758ef1479ef2cfa 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 e2eff98e601c07c..23cb19249a92835 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 000000000000000..dd5b8c2545fe653 --- /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.