]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-103878: Return a consistent empty value from cancelled file dialogs (GH-152435)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 29 Jun 2026 20:49:19 +0000 (23:49 +0300)
committerGitHub <noreply@github.com>
Mon, 29 Jun 2026 20:49:19 +0000 (20:49 +0000)
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 <chrischavez@gmx.us>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Doc/library/dialog.rst
Lib/test/test_tkinter/test_filedialog.py
Lib/tkinter/filedialog.py
Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst [new file with mode: 0644]

index c3f62117ed61d78fa41731239e162591f9f08eb9..a576cdcfaa2b1e9ee7536d8d71379461101b65ad 100644 (file)
@@ -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)
index 054e719a0f883d98d04c1d4fc96bd1ee7d30aeed..758ef1479ef2cfad1a2f17aed8eac8698da5a0d3 100644 (file)
@@ -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.
index e2eff98e601c07c00baf66a4797915c47866acc3..23cb19249a9283587b258369728722456f983d70 100644 (file)
@@ -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 (file)
index 0000000..dd5b8c2
--- /dev/null
@@ -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.