Skip to content

Commit edcc07d

Browse files
serhiy-storchakachrstphrchvzclaude
authored
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 <chrischavez@gmx.us> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0a29d84 commit edcc07d

4 files changed

Lines changed: 55 additions & 18 deletions

File tree

Doc/library/dialog.rst

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,8 @@ listed below:
159159
The below functions when called create a modal, native look-and-feel dialog,
160160
wait for the user's selection, and return it.
161161
The exact return value depends on the function (see below); when the dialog is
162-
cancelled it is an empty string, an empty tuple or ``None``.
163-
The precise type of this empty value may vary between platforms and Tk
164-
versions, so test the result for truth rather than comparing it with a
165-
specific value.
162+
cancelled it is the empty value documented for that function -- an empty
163+
string, an empty tuple, an empty list or ``None``.
166164

167165
.. function:: askopenfile(mode="r", **options)
168166
askopenfiles(mode="r", **options)
@@ -171,7 +169,7 @@ specific value.
171169
:func:`askopenfile` returns the opened file object, or ``None`` if the
172170
dialog is cancelled.
173171
:func:`askopenfiles` returns a list of the opened file objects, or an empty
174-
tuple if cancelled.
172+
list if cancelled.
175173
The files are opened in mode *mode* (read-only ``'r'`` by default).
176174

177175
.. function:: asksaveasfile(mode="w", **options)

Lib/test/test_tkinter/test_filedialog.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,39 @@ def test_directory(self):
3737
self.check(filedialog.Directory, 'tk_chooseDirectory')
3838

3939

40+
class CancelResultTest(AbstractTkTest, unittest.TestCase):
41+
# On cancellation Tcl may report the empty result as '', () or b''
42+
# (gh-103878). _fixresult() normalizes it to the documented empty value:
43+
# '' for the filename dialogs and () for the multiple-selection dialog.
44+
45+
def check(self, dialog, expected):
46+
for empty in ('', (), b''):
47+
with self.subTest(empty=empty):
48+
result = dialog._fixresult(self.root, empty)
49+
self.assertEqual(result, expected)
50+
self.assertIs(type(result), type(expected))
51+
52+
def test_open(self):
53+
self.check(filedialog.Open(self.root), '')
54+
55+
def test_saveas(self):
56+
self.check(filedialog.SaveAs(self.root), '')
57+
58+
def test_directory(self):
59+
self.check(filedialog.Directory(self.root), '')
60+
61+
def test_openfilenames(self):
62+
self.check(filedialog.Open(self.root, multiple=1), ())
63+
64+
def test_results_preserved(self):
65+
# A real selection is returned unchanged.
66+
single = filedialog.Open(self.root)
67+
self.assertEqual(single._fixresult(self.root, '/a/spam'), '/a/spam')
68+
multiple = filedialog.Open(self.root, multiple=1)
69+
self.assertEqual(multiple._fixresult(self.root, ('/a', '/b')),
70+
('/a', '/b'))
71+
72+
4073
class FileDialogTest(AbstractTkTest, unittest.TestCase):
4174
# The pure-Python FileDialog runs its own modal loop in go(); its logic is
4275
# exercised here without entering the loop.

Lib/tkinter/filedialog.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,9 @@ def _fixoptions(self):
311311
pass
312312

313313
def _fixresult(self, widget, result):
314-
if result:
314+
if not result:
315+
result = '' # normalize the cancelled result (gh-103878)
316+
else:
315317
# keep directory and filename until next time
316318
# convert Tcl path objects to strings
317319
try:
@@ -335,17 +337,16 @@ class Open(_Dialog):
335337
command = "tk_getOpenFile"
336338

337339
def _fixresult(self, widget, result):
338-
if isinstance(result, tuple):
339-
# multiple results:
340+
if self.options.get("multiple"):
341+
# multiple results: a tuple of filenames
342+
if not isinstance(result, tuple):
343+
result = widget.tk.splitlist(result)
340344
result = tuple([getattr(r, "string", r) for r in result])
341345
if result:
342346
path, file = os.path.split(result[0])
343347
self.options["initialdir"] = path
344348
# don't set initialfile or filename, as we have multiple of these
345349
return result
346-
if not widget.tk.wantobjects() and "multiple" in self.options:
347-
# Need to split result explicitly
348-
return self._fixresult(widget, widget.tk.splitlist(result))
349350
return _Dialog._fixresult(self, widget, result)
350351

351352

@@ -362,7 +363,9 @@ class Directory(commondialog.Dialog):
362363
command = "tk_chooseDirectory"
363364

364365
def _fixresult(self, widget, result):
365-
if result:
366+
if not result:
367+
result = '' # normalize the cancelled result (gh-103878)
368+
else:
366369
# convert Tcl path objects to strings
367370
try:
368371
result = result.string
@@ -420,12 +423,7 @@ def askopenfiles(mode = "r", **options):
420423
"""
421424

422425
files = askopenfilenames(**options)
423-
if files:
424-
ofiles=[]
425-
for filename in files:
426-
ofiles.append(open(filename, mode))
427-
files=ofiles
428-
return files
426+
return [open(filename, mode) for filename in files]
429427

430428

431429
def asksaveasfile(mode = "w", **options):
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The :mod:`tkinter.filedialog` functions that return a filename
2+
(:func:`~tkinter.filedialog.askopenfilename`,
3+
:func:`~tkinter.filedialog.asksaveasfilename` and
4+
:func:`~tkinter.filedialog.askdirectory`) now consistently return an empty
5+
string when the dialog is cancelled, instead of an empty tuple or ``b''``
6+
on some platforms. :func:`~tkinter.filedialog.askopenfilenames` likewise
7+
always returns an empty tuple, and :func:`~tkinter.filedialog.askopenfiles`
8+
an empty list.

0 commit comments

Comments
 (0)