from base64 import b64encode
from functools import partial
from IPython.display import Javascript, display
from ipywidgets import Box, Button, Layout
from pathlib import Path
def create_code_cell(code):
= b64encode(code.encode()).decode()
encoded_code f"""
display(Javascript( var code = IPython.notebook.insert_cell_below('code');
code.set_text(atob("{encoded_code}"));
code.execute();
code.focus_cell()"""))
def on_click_dir(path, button): create_code_cell(f"ls('{path}')")
def on_click_file(path, button): create_code_cell(f"Path('{path}')")
def ls(root=Path()):
if isinstance(root, str): root = Path(root).expanduser()
= sorted(root.iterdir())
paths if not paths: return
= Layout(width='fit-content')
button_layout = []
buttons for path in paths:
= Button(description=str(path.relative_to(root)), layout=button_layout)
button if path.is_dir() else on_click_file, path))
button.on_click(partial(on_click_dir
buttons.append(button)= Layout(overflow='scroll hidden', height='500px', display='flex',
box_layout ='column wrap', align_content='flex-start')
flex_flowreturn Box(buttons, layout=box_layout)
Point and click directory navigation inside a Jupyter notebook
Here’s a tiny demo of a point-and-click navigation interface with rich output powered by Jupyter notebooks and ipywidgets! I originally mentioned the idea in a previous TIL. I also tweeted about it which is the best place to leave any comments or questions if you’d like.
TL;DR
If all you need is a copy-pastable code snippet, here you go. Read on for a more in-depth description.
Minimal implementation
We start by defining a function to create and execute a code cell below the focused cell (see my previous TIL if you’d like more detail on this part):
def create_code_cell(code):
= b64encode(code.encode()).decode()
encoded_code f"""
display(Javascript( var code = IPython.notebook.insert_cell_below('code');
code.set_text(atob("{encoded_code}"));
code.execute();
code.focus_cell()
"""))
We’re going to be using button widgets, which expect an on-click callback, so let’s define those next. The callback is expected to be a function accepting a single argument, button
, to which the button object itself is passed - although we won’t be using it. We need to know the path that was clicked on as well, so we’ll have to partial
that in later:
def on_click_dir(path, button): create_code_cell(f"ls('{path}')")
def on_click_file(path, button): create_code_cell(f"Path('{path}')")
Test if it works:
'point-and-click-directory-navigation-inside-a-jupyter-notebook.ipynb'), None) on_click_file(Path(
'point-and-click-directory-navigation-inside-a-jupyter-notebook.ipynb') Path(
PosixPath('point-and-click-directory-navigation-inside-a-jupyter-notebook.ipynb')
Neat! The cell above this was created by calling on_click_file
.
Finally, we implement a straightforward minimal ls
function using Button
widgets for Path
s, and wrapping those in a VBox
widget:
from ipywidgets import VBox
def ls(root=Path()):
if isinstance(root, str): root = Path(root).expanduser()
= sorted(root.iterdir())
paths if not paths: return
= []
buttons for path in paths:
= Button(description=str(path))
button if path.is_dir() else on_click_file, path))
button.on_click(partial(on_click_dir
buttons.append(button)return VBox(buttons)
Improved styling
I don’t like how the implementation above is styled, so here is another with a few purely stylistic improvements:
def ls(root=Path()):
if isinstance(root, str): root = Path(root).expanduser()
= sorted(root.iterdir())
paths if not paths: return
= Layout(width='fit-content')
button_layout = []
buttons for path in paths:
= Button(description=str(path.relative_to(root)), layout=button_layout)
button if path.is_dir() else on_click_file, path))
button.on_click(partial(on_click_dir
buttons.append(button)= Layout(overflow='scroll hidden', height='500px', display='flex',
box_layout ='column wrap', align_content='flex-start')
flex_flowreturn Box(buttons, layout=box_layout)
'~/code/fastai') ls(
Unfortunately, my current blog setup doesn’t support widgets, but you should be able to run this locally. You can also check out the demo video in my tweet.
If you pay close attention to the demo video, you’ll notice that it’s still styled slightly differently to what we’ve built here. Some styles can’t be changed through ipywidget’s style
interface, so that was achieved by manually writing CSS with the %%html
magic command followed by a <style>...</style>
tag, and then assigning a class to the buttons and boxes using their add_class
method. I also implemented a custom widget with a small render
JavaScript function that resized the output grid until it fit the width of the screen.
I’m really excited with how this turned out! And it was far simpler than I’d expected. I’ll definitely be exploring the point-and-click navigation pattern more. I’m thinking about trying it out for exploring documentation about Python objects.