Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tkinter appears not to pass file extension to file writing correctly #129534

Closed
elkym opened this issue Jan 31, 2025 · 6 comments
Closed

Tkinter appears not to pass file extension to file writing correctly #129534

elkym opened this issue Jan 31, 2025 · 6 comments
Labels
stdlib Python modules in the Lib dir topic-tkinter type-bug An unexpected behavior, bug, or error

Comments

@elkym
Copy link

elkym commented Jan 31, 2025

Bug description:

Requirements: pandas

Testing this on Windows 11, Python 3.13, pandas 2.2.3
Also tested on Python 3.11.9

File selection dialog custom class has added method _get_file_type for tkinter's asksaveasfilename for diagnostic prints.

The tkinter file dialog class has an internal method _fixresult-- this will call other methods and pass along the correct file ending, but somehow, my script will not append it.

If the user selects an existing file that has a file extension the ending is included in the file name, and the script correctly writes a file.
If the user manually adds a correct file extension from the list, then python treats that as the correct file extension and ignores the dropdown selection.

But if the user does not manually add a file ending by selection of an existing file, or entering data in the dialog box, no ending is passed to the function, and therefore the file fails to write.

Is this actually not an intended feature? My reading of the source code and documentation, along with some consulting of CoPilot, leads me to believe that the ending selected in the dropdown is intended to be appended to any filename that lacks an ending, but if I've misunderstood, then perhaps this should be reclassified as a new feature request. But my understanding is that it should do this. Help me out if you can. Thanks.

+-----------------------------+
| asksaveasfilename Function |
| |
| - Calls SaveAs.show() |
+-------------+---------------+
|
v
+-------------+---------------+
| SaveAs Class |
| |
| - Inherits from _Dialog |
| - Uses _Dialog.show() |
+-------------+---------------+
|
v
+-------------+---------------+
| _Dialog Class |
| |
| - Defines show() |
| - Calls tk.call() |
| - Passes result to |
| _fixresult() |
+-------------+---------------+
|
v
+-------------+---------------+
| tk.call Method |
| |
| - Executes Tcl command |
| - Returns selected file path |
+-------------+---------------+
|
v
+-------------+---------------+
| _fixresult Method |
| |
| - Processes result |
| - Ensures correct file path |
| and extension |
+-----------------------------+

CODE:

import tkinter as tk
from tkinter import filedialog
import os
import pandas as pd

class FileDialogTest:
    def __init__(self, file_extensions):
        self.file_extensions = file_extensions
        self.selected_file_type = None

    def _format_file_extensions(self):
        return [(f"{ext} files", ext) for ext in self.file_extensions]

    def _get_file_type(self, file_path):
        _, ext = os.path.splitext(file_path)
        return ext

    def select_file_to_save(self, default_name="testfile"):
        root = tk.Tk()
        root.withdraw()
        file_path = filedialog.asksaveasfilename(
            title="Select a file to save",
            filetypes=self._format_file_extensions(),
            initialfile=default_name
        )
        if not file_path:
            raise FileNotFoundError("No file selected.")
        print(f"[After Selection] Selected file path: {file_path}") # Print for identifying file extension issues
        self.selected_file_type = self._get_file_type(file_path)
        if not self.selected_file_type:
            # Append the default extension based on the selected file type
            self.selected_file_type = self.file_extensions[0]  # Default to the first extension if none is selected
            file_path += self.selected_file_type
        elif self.selected_file_type not in self.file_extensions:
            # Correct the file extension if it doesn't match the selected type
            file_path += self.file_extensions[self.file_extensions.index(self.selected_file_type)]
        print(f"[After Extension Check] Final file path: {file_path}") # Print for identifying file extension issues
        print(f"[After Extension Check] Selected file type: {self.selected_file_type}") # Print for identifying file extension issues
        return file_path, self.selected_file_type

# Define file extensions for testing
file_extensions = ['.txt', '.csv', '.parquet', '.xlsx']

# Initialize FileDialogTest with file extensions
file_dialog_test = FileDialogTest(file_extensions)

# Test select_file_to_save method
try:
    file_path, selected_file_type = file_dialog_test.select_file_to_save()
    print(f"[Main] Selected file path: {file_path}")  # Print for identifying file extension issues
    print(f"[Main] Selected file type: {selected_file_type}")  # Print for identifying file extension issues
    
    # Sample data to save as DataFrame
    sample_data = {
        "Column1": ["Value 1", "Value 2"],
        "Column2": [123, 456],
        "Column3": ["Another string", "Yet another string"],
        "Column4": [456.78, 789.01]
    }
    
    df = pd.DataFrame(sample_data)
    
    # Save the DataFrame to the selected file
    print(f"[Before Saving] Saving DataFrame to {file_path} as {selected_file_type}") # Print for identifying file extension issues
    if selected_file_type == '.csv':
        df.to_csv(file_path, index=False)
    elif selected_file_type == '.txt':
        df.to_csv(file_path, sep='\t', index=False)
    elif selected_file_type == '.parquet':
        df.to_parquet(file_path, index=False)
    elif selected_file_type == '.xlsx':
        df.to_excel(file_path, index=False)
    print(f"[After Saving] Data saved to {file_path}") # Print for identifying file extension issues
except FileNotFoundError as e:
    print(e)

CPython versions tested on:

3.13

Operating systems tested on:

Windows

Linked PRs

@elkym elkym added the type-bug An unexpected behavior, bug, or error label Jan 31, 2025
@picnixz picnixz added stdlib Python modules in the Lib dir topic-tkinter labels Jan 31, 2025
@ericvsmith
Copy link
Member

This would be easier for someone to test and troubleshoot if you had a simpler example with no external dependencies.

@elkym
Copy link
Author

elkym commented Feb 3, 2025

At the time of posting another test indicated that a workaround seemed possible for simpler files like csv and txt (I believe I overwrote and lost this version-- I should probably have been more methodical).

But my intended usage is with dataframes, so I included those file types deliberately, and the bug persisted when using my preferred dataframe write methods no matter my attempts using the xlsxwriter to_excel() function and the pandas to_parquet() function. In retrospect, I'm pretty certain this was a fluke.

A friend helped me discover a bit more though. It seems that when using tkinter's file selection dialog, appending the selected file extension will not occur unless a default extension has been specified.

It would seem that a default file type being required to pass the selected file type would make sense if only one file type were permitted-- but this does seem like unintended behavior as it currently stands.

Suggestion:
Update tkinter's class(es) so that if no default file type is specified, if there is a file type list present, or a single file type in the parameters, the first in these parameters is used as the default type. I'm not yet sure how this would be implemented but would appreciate input from those more familiar with the inner workings of the tkinter classes and functions involved.

@anjuraghuwanshi
Copy link

Hi @elkym ,

I came across this issue and submitted a fix in PR #129682. The fix ensures that when no file extension is provided by the user, asksaveasfilename appends the first extension from the filetypes list, even if defaultextension is not explicitly set.

I've tested this with multiple file types (.txt, .csv, .parquet, .xlsx), and the behavior is now more consistent with user expectations. Let me know if you have any thoughts or if there's anything else you'd like to see addressed!

Thanks for reporting this! 🚀

@terryjreedy
Copy link
Member

@serhiy-storchaka This issue needs you opinion as to whether a tkinter change is needed or at least desirable and whether the PR is right approach.

@serhiy-storchaka
Copy link
Member

The asksaveasfilename() function and the SaveAs class is just a thin wrapper around the Tk's tk_getSaveFile command. If you want to change its behavior, make a request to the Tk core developing team. In your case you can just add a code to process the result of asksaveasfilename() in your program, no changes in tk_getSaveFile are needed.

There are also issues with the proposed implementation in PR #129682, but since we will not make any changes anyway, I am not going to discuss them here.

@anjuraghuwanshi
Copy link

Thank you for the clarification! I now understand that asksaveasfilename() is intended to stay a thin wrapper around Tk's tk_getSaveFile, and that any filename processing should be done at the application level.
I appreciate your time, and I'll handle filename extensions in my own program instead.
I'll go ahead and close the PR. Thanks again for your feedback!

@terryjreedy terryjreedy closed this as not planned Won't fix, can't repro, duplicate, stale Feb 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir topic-tkinter type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

6 participants