Skip to content

Instantly share code, notes, and snippets.

@minrk
Created December 19, 2018 10:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save minrk/2e14bec8e842e5664101c1310b3d7bdb to your computer and use it in GitHub Desktop.
Save minrk/2e14bec8e842e5664101c1310b3d7bdb to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "# Making IPython magics from click functions\n\nTwo fun facts:\n\n- click makes nice command-line APIs in Python\n- IPython uses magics to expose Python functions via a shell-like syntax\n\nFirst, let's create the hello world click command from the click docs:"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "import click\n\n@click.command()\n@click.option('--count', default=1, help='Number of greetings.')\n@click.option('--name', prompt='Your name',\n help='The person to greet.')\ndef hello(count, name):\n \"\"\"Simple program that greets NAME for a total of COUNT times.\"\"\"\n for x in range(count):\n click.echo('Hello %s!' % click.style(name, fg='green'))",
"execution_count": 1,
"outputs": []
},
{
"metadata": {},
"cell_type": "markdown",
"source": "which we can call:"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "hello(['--count', '2'])",
"execution_count": 2,
"outputs": [
{
"output_type": "stream",
"text": "Your name: min\nHello min!\nHello min!\n",
"name": "stdout"
},
{
"output_type": "error",
"ename": "SystemExit",
"evalue": "0",
"traceback": [
"An exception has occurred, use %tb to see the full traceback.\n",
"\u001b[1;31mSystemExit\u001b[0m\u001b[1;31m:\u001b[0m 0\n"
]
},
{
"output_type": "stream",
"text": "/Users/benjaminrk/dev/ip/ipython/IPython/core/interactiveshell.py:3283: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.\n warn(\"To exit: use 'exit', 'quit', or Ctrl-D.\", stacklevel=1)\n",
"name": "stderr"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "Two things:\n\n1. it raises `SystemExit`, and\n2. `min` is not green\n\nThe latter is because click checks if sys.stdin is a tty, which it isn't in the notebook,\nand only enables colors if it is.\n\nThe check is in `click.utils.should_strip_ansi`:"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "s = click.style('Hello World!', fg='green')\ns",
"execution_count": 3,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 3,
"data": {
"text/plain": "'\\x1b[32mHello World!\\x1b[0m'"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "click.utils.should_strip_ansi??",
"execution_count": 4,
"outputs": [
{
"output_type": "display_data",
"data": {
"text/plain": "\u001b[1;31mSignature:\u001b[0m \u001b[0mclick\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mutils\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mshould_strip_ansi\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mstream\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mNone\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcolor\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;31mDocstring:\u001b[0m <no docstring>\n\u001b[1;31mSource:\u001b[0m \n\u001b[1;32mdef\u001b[0m \u001b[0mshould_strip_ansi\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mstream\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mNone\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcolor\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\n\u001b[1;33m\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mcolor\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\n\u001b[1;33m\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mstream\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\n\u001b[1;33m\u001b[0m \u001b[0mstream\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0msys\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstdin\u001b[0m\u001b[1;33m\u001b[0m\n\u001b[1;33m\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0misatty\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mstream\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\n\u001b[1;33m\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mcolor\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;31mFile:\u001b[0m ~/conda/lib/python3.6/site-packages/click/_compat.py\n\u001b[1;31mType:\u001b[0m function\n"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "click.echo(s)",
"execution_count": 5,
"outputs": [
{
"output_type": "stream",
"text": "Hello World!\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "We can force click not to check this and then echo should produce colored output:"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "try:\n from unittest import mock\nexcept ImportError: # py2\n import mock\n\nwith mock.patch(\n 'click.utils.should_strip_ansi',\n lambda *args, **kwargs: False\n):\n click.echo(s)",
"execution_count": 6,
"outputs": [
{
"output_type": "stream",
"text": "\u001b[32mHello World!\u001b[0m\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "Next, we can write the transform to turn a click command into an IPython magic.\nAn IPython magic is a Python function called where the rest of the line\nis passed as a simple string.\nThe click function wants a list of arguments, `sys.argv`-style.\n\nSo we need to do the following things:\n\n1. split the line into arguments with [`shlex.split`](https://docs.python.org/3/library/shlex.html#shlex.split)\n2. tell the command that it's name is `%magicname`\n3. tell `click.echo` that it should always color output\n4. catch `SystemExit` errors and turning them into IPython magic `UsageError`s\n5. finally, register the magic via IPython's [`register_magic_function`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.interactiveshell.html#IPython.core.interactiveshell.InteractiveShell.register_magic_function)"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "import shlex\ntry:\n from unittest import mock\nexcept ImportError: # py2\n import mock\n\nfrom IPython.core.error import UsageError\n\ndef click_magic(click_command, name=None):\n if name is None:\n name = click_command.name\n\n def magic_func(line):\n args = shlex.split(line)\n # bypass click's check for whether colors should be enabled\n with mock.patch(\n 'click.utils.should_strip_ansi',\n lambda *args, **kwargs: False\n ):\n try:\n click_command(\n shlex.split(line),\n prog_name='%' + name,\n )\n except SystemExit as e:\n if e.code != 0:\n raise UsageError(\"Command exited with status=%s\" % e.code)\n \n get_ipython().register_magic_function(magic_func, magic_name=name)\n",
"execution_count": 7,
"outputs": []
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "click_magic(hello)",
"execution_count": 8,
"outputs": []
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "%hello --help",
"execution_count": 9,
"outputs": [
{
"output_type": "stream",
"text": "Usage: %hello [OPTIONS]\n\n Simple program that greets NAME for a total of COUNT times.\n\nOptions:\n --count INTEGER Number of greetings.\n --name TEXT The person to greet.\n --help Show this message and exit.\n",
"name": "stdout"
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "%hello --count 2 --name myname",
"execution_count": 10,
"outputs": [
{
"output_type": "stream",
"text": "Hello \u001b[32mmyname\u001b[0m!\nHello \u001b[32mmyname\u001b[0m!\n",
"name": "stdout"
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "%hello --unrecognized",
"execution_count": 11,
"outputs": [
{
"output_type": "stream",
"text": "Error: no such option: --unrecognized\nUsageError: Command exited with status=2\n",
"name": "stderr"
}
]
}
],
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3",
"language": "python"
},
"language_info": {
"name": "python",
"version": "3.6.5",
"mimetype": "text/x-python",
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"pygments_lexer": "ipython3",
"nbconvert_exporter": "python",
"file_extension": ".py"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment