audrey.feldroy.com

The experimental notebooks of Audrey M. Roy Greenfeld. This website and all its notebooks are open-source at github.com/audreyfeldroy/audrey.feldroy.com


# Create a CLI Tool With fastcore.script

by Audrey M. Roy Greenfeld | Wed, Feb 5, 2025

Fastcore's call_parse decorator turns Python functions into CLI tools quickly. It's nowhere near as fancy as Typer or Click, but it's super quick to use.


Study the Example

From https://fastcore.fast.ai/script.html#example

#| export
from fastcore.script import *
from fastcore.utils import *
from pathlib import Path
import time
from nbdev.export import nb_export
@call_parse
def main(msg:str,     # The message
         upper:bool): # Convert to uppercase?
    "Print `msg`, optionally converting to uppercase"
    print(msg.upper() if upper else msg)

We can use the function right here, which is nice:

main("hey", upper=True)
main("hey", upper=False)

As for using it as a CLI tool, the docs say "If you copy that info a file and run it, you’ll see:


$ examples/test_fastcore.py --help
usage: test_fastcore.py [-h] [--upper] msg

Print `msg`, optionally converting to uppercase

positional arguments:
  msg          The message

optional arguments:
  -h, --help   show this help message and exit
  --upper      Convert to uppercase? (default: False)

"

Real-World Example: Prepending Notebook Filenames With Dates

I have many notebooks like Docker-Run.ipynb that I'd like prepended with the ISO 8601 date that they were last updated. My script will be prepend_nbs_with_dates.py because of the next line:

#| default_exp prepend_nbs_with_dates

I'll define a function to get all of them:

#| export
def get_undated_nbs(): return L(Path().glob('[!0-9]*.ipynb'))
nbs = get_undated_nbs()
nbs
(#14) [Path('Publish-Command-for-This-Blog.ipynb'),Path('Title-Generation.ipynb'),Path('ShellSage-Ghostty-and-Tmux.ipynb'),Path('Understanding-Gradient-Descent.ipynb'),Path('SVG-Animation-Via-CSS-Keyframes.ipynb'),Path('AI-Trajectory.ipynb'),Path('asyncio-in-Jupyter-Notebooks.ipynb'),Path('Writing-for-the-AIs.ipynb'),Path('ai_server_load_testing.ipynb'),Path('SVG-Paths-in-FastHTML.ipynb'),Path('fastai-WordTokenizer.ipynb'),Path('Matplotlib-Charts-in-FastHTML.ipynb'),Path('fastcore-and-anki.ipynb'),Path('Docker-Run.ipynb')]
nbs[1]
Path('Title-Generation.ipynb')

Then I'll define a function to prepend a date to a filename:

#| export
def prepend_date(fn):
    "Prepend ISO 8601 date to filename if not already present"
    if fn.stem[0].isdigit(): return  # Already has date
    mtime = fn.stat().st_mtime
    date = time.strftime('%Y-%m-%d', time.localtime(mtime))
    new_name = fn.parent/f"{date}-{fn.name}"
    fn.rename(new_name)
    return new_name
# prepend_date(nbs[1])

Finally, I'll define a main function that gets all undated notebooks and prepends dates to their filenames:

#| export
@call_parse
def main(dry_run:bool=True): # Don't actually rename if True
    "Prepend dates to notebook filenames"
    fns = get_undated_nbs()
    print(f"Found {len(fns)} undated notebooks")
    if dry_run:
        for f in fns:
            mtime = f.stat().st_mtime
            date = time.strftime('%Y-%m-%d', time.localtime(mtime))
            print(f"{f} -> {date}-{f.name}")
    else:
        for f in fns: prepend_date(f)

By default this does a dry run:

main()

Here we run it for real:

main(dry_run=False)

Now there are no more undated notebooks:

get_undated_nbs()
(#0) []

Export It

nb_export("2025-02-05-Create-a-CLI-Tool-With-Fastcore-Script.ipynb", lib_path="../scripts")
!ls ../scripts/

Run It

I ran it as a function earlier, so I don't have undated notebooks at the moment!

% python ../scripts/prepend_nbs_with_dates.py
Found 0 undated notebooks

Give me a few hours or days, and I'll update this post...

© 2024-2025 Audrey M. Roy Greenfeld