14
\$\begingroup\$

Background

This is a feature that I have wanted to see in Jupyter notebook for quite some time. I'm sure that others have built similar things but I haven't been able to find them on SO, Github, CodeReview or the rest of the searchable internet.

Approach

I used ipywidgets.Button as a base class and added a traitlet called files. This traitlet receives a list of files as strings from tkinter.filedialog.askopenfilename. When the button is clicked the file dialog pops up the user selects files and then can access that list of strings as an attribute/traitlet on the button instance.

import traitlets
from ipywidgets import widgets
from IPython.display import display
from tkinter import Tk, filedialog


class SelectFilesButton(widgets.Button):
    """A file widget that leverages tkinter.filedialog."""

    def __init__(self):
        super(SelectFilesButton, self).__init__()
        # Add the selected_files trait
        self.add_traits(files=traitlets.traitlets.List())
        # Create the button.
        self.description = "Select Files"
        self.icon = "square-o"
        self.style.button_color = "orange"
        # Set on click behavior.
        self.on_click(self.select_files)

    @staticmethod
    def select_files(b):
        """Generate instance of tkinter.filedialog.

        Parameters
        ----------
        b : obj:
            An instance of ipywidgets.widgets.Button 
        """
        # Create Tk root
        root = Tk()
        # Hide the main window
        root.withdraw()
        # Raise the root to the top of all windows.
        root.call('wm', 'attributes', '.', '-topmost', True)
        # List of selected fileswill be set to b.value
        b.files = filedialog.askopenfilename(multiple=True)

        b.description = "Files Selected"
        b.icon = "check-square-o"
        b.style.button_color = "lightgreen"

Example Usage

Displaying the button

my_button = SelectFilesButton()
my_button # This will display the button in the context of Jupyter Notebook

Retrieving the file list from the button.

# In a different cell of the same Jupyter Notebook You can access the file list by using the following:
my_button.files

Questions

These are just some suggestions.

1) Does this work on your machine? If so throw me an upvote! If not please specify what versions you are working with so I can extend.

2) Are there any general overall improvements that can be made?

3) Is there any redundancy that I missed?

4) Does this code follow best practices?

\$\endgroup\$
2
  • \$\begingroup\$ It would be nice to have a solution without Tk which only works together with an X server. \$\endgroup\$
    – Soerendip
    Commented Jul 21, 2020 at 17:31
  • \$\begingroup\$ Would be nice to be able to re-use it, in case you have multiple files from separate directories (save you wanted to compare 2 files from different directories). If you hit the button multiple times it will add the files, instead of replace them in the "list" of files (obviously determining which file is which would have to be done elsewhere.) \$\endgroup\$
    – Biaspoint
    Commented Dec 5, 2022 at 18:43

2 Answers 2

6
\$\begingroup\$

It is a nice piece of code. I have tried it in both jupyter notebook and jupyter lab. It works quite well in the notebook, but it does not work that well in lab.

In jupyter lab, it shows the button, but does not execute anything when you push it. I have adapted your code to be able to run it in lab.

Code:

import traitlets
from ipywidgets import widgets
from IPython.display import display
from tkinter import Tk, filedialog


class SelectFilesButton(widgets.Button):
    """A file widget that leverages tkinter.filedialog."""

    def __init__(self):
        super(SelectFilesButton, self).__init__()
        # Add the selected_files trait
        self.add_traits(files=traitlets.traitlets.List())
        # Create the button.
        self.description = "Select Files"
        self.icon = "square-o"
        self.style.button_color = "orange"
        # Set on click behavior.
        self.on_click(self.select_files)

    @staticmethod
    def select_files(b):
        """Generate instance of tkinter.filedialog.

        Parameters
        ----------
        b : obj:
            An instance of ipywidgets.widgets.Button 
        """
        with out:
            try:
                # Create Tk root
                root = Tk()
                # Hide the main window
                root.withdraw()
                # Raise the root to the top of all windows.
                root.call('wm', 'attributes', '.', '-topmost', True)
                # List of selected fileswill be set to b.value
                b.files = filedialog.askopenfilename(multiple=True)

                b.description = "Files Selected"
                b.icon = "check-square-o"
                b.style.button_color = "lightgreen"
            except:
                pass
out = widgets.Output()
raw = SelectFilesButton()
widgets.VBox([raw, out])
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Could you explain the steps you took to refactor OP's code to work on lab? \$\endgroup\$
    – dfhwze
    Commented Sep 30, 2019 at 4:31
  • \$\begingroup\$ Ok, I will try. I saw that the posted code had two problems. The first one: It just worked on the notebook. And, as stated by @MaciekS, It has trouble when the user does not select any file. So, for the first issue, I followed the suggestions made on this post. It use [Output] (ipywidgets.readthedocs.io/en/latest/examples/…) and VBox. I tried to use widgets.Output() inside the class, but it did not work for me. \$\endgroup\$ Commented Sep 30, 2019 at 4:54
  • \$\begingroup\$ The second issue was simpler to solve, just a try - except did the trick. \$\endgroup\$ Commented Sep 30, 2019 at 4:55
  • \$\begingroup\$ I just tried it in JupyterLab version 3.3.4 on Python 3.9 and it works great. It was not obvious at first how to get the files selected out of it, but then I tried calling it with the my_button.files code above and it worked beautifully. Is there a way to have it also pass out the folder / directory surrounding the selected files? \$\endgroup\$ Commented Apr 27, 2022 at 4:56
3
\$\begingroup\$

I like your idea. This is a feature I often need. I tried this on Python 2.7.

The first problem was no tkinter module (I have Tkinter) and no filedialog present in it. I had to modify this (according to this and this):

from Tkinter import Tk
import tkFileDialog as filedialog

Then I had to remove the style changes (no style member).

I've got an error when clicked OK without selecting any file:

TraitError: The 'files' trait of a SelectFilesButton instance must be a list, but a value of type 'str' (i.e. '') was specified.

\$\endgroup\$
10
  • \$\begingroup\$ Thanks for the input I'll add some version friendly features. Did you try to update ipywidgets that should help? \$\endgroup\$ Commented Jun 2, 2017 at 11:52
  • \$\begingroup\$ Can you move the content out of the link, and explain why you consider it to be much simpler and easier? \$\endgroup\$ Commented Mar 13, 2018 at 15:29
  • \$\begingroup\$ Also, as-is this doesn't feel like a review, but more of a "here's some stuff that didn't work for me". I think rephrasing to make portability a suggestion, with the specific details of how you might accomplish that, would be much more valuable. \$\endgroup\$ Commented Mar 13, 2018 at 15:30
  • \$\begingroup\$ @Dannno agreed this is not really answer. My question has been tagged as python3.x the entire time so complaints about version compatibility b/t 2.x and 3.x are pretty irrelevant IMO. Also the PyQt based solution can hardly be called simpler in terms of installation and implementation. I'd caution against the use of PyQt for anyone interested in commercial development because of it's ridiculous license. \$\endgroup\$ Commented Mar 13, 2018 at 17:52
  • 1
    \$\begingroup\$ Wow! Isn't this a code review?! There was no feedback for almost a month, so I decided to test and write some comments. I find the other solution easier, so that's what I wrote (Answers that merely provide an alternate solution with no explanation [...] -HelpCenter > Answering). I stated my comments are for using in Python 2 (in case the Author wants to support it). I did not intend this to be the answer, but just an input. First, the "answer" was appreciated, and then its downvote by the same person. Does this make sense? Is it really better not to give any feedback? \$\endgroup\$
    – MaciekS
    Commented Mar 14, 2018 at 15:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.