11
\$\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\$
  • \$\begingroup\$ It would be nice to have a solution without Tk which only works together with an X server. \$\endgroup\$ – Sören Jul 21 at 17:31
3
\$\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])
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Could you explain the steps you took to refactor OP's code to work on lab? \$\endgroup\$ – dfhwze Sep 30 '19 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\$ – Juan David Argüello Plata Sep 30 '19 at 4:54
  • \$\begingroup\$ The second issue was simpler to solve, just a try - except did the trick. \$\endgroup\$ – Juan David Argüello Plata Sep 30 '19 at 4:55
2
\$\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.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for the input I'll add some version friendly features. Did you try to update ipywidgets that should help? \$\endgroup\$ – James Draper Jun 2 '17 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\$ – Dannnno Mar 13 '18 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\$ – Dannnno Mar 13 '18 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\$ – James Draper Mar 13 '18 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 Mar 14 '18 at 15:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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