Skip to content

Instantly share code, notes, and snippets.

@mpkocher
Last active February 22, 2019 01:05
Show Gist options
  • Save mpkocher/4e261b38d8b1c76a7f6dd8e9a95a4873 to your computer and use it in GitHub Desktop.
Save mpkocher/4e261b38d8b1c76a7f6dd8e9a95a4873 to your computer and use it in GitHub Desktop.
Functional Python Techniques Part 3
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Functional Programming Techniques in Python: Part 3\n",
"\n",
"In [Part 1](https://mpkocher.github.io/2019/01/30/Functional-Python-Part-1/), we introduced core Functional Programming techniques in Python.\n",
"\n",
"In [Part 2](https://mpkocher.github.io/2019/02/02/Functional-Python-Part-2/), we're designed a REST API client using Functional Programming Techniques (FPT). \n",
"\n",
"In Part 3, we're going to do another concrete example and build a commandline tool interface. \n",
"\n",
"This will focus on ironing out some friction points when interfacing with the standard lib (e.g., argparse) while also aiming for a DRY model. \n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import datetime\n",
"import functools\n",
"import itertools\n",
"import argparse\n",
"import sys\n",
"import logging"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Today is 2019-02-21 16:59:11.100236\n"
]
}
],
"source": [
"print(\"Today is {}\".format(datetime.datetime.now()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Utils\n",
"\n",
"In [Part 1](https://mpkocher.github.io/2019/01/30/Functional-Python-Part-1/), we defined a few utils. We'll reproduce the `compose` util here. "
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"def compose(*funcs):\n",
" \"\"\"Functional composition\n",
" [f, g, h] will be f(g(h(x)))\n",
" \"\"\"\n",
" def compose_two(f, g):\n",
" def c(x):\n",
" return f(g(x))\n",
" return c\n",
" return functools.reduce(compose_two, funcs)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Example Using argparse from the Python Standard Lib\n",
"\n",
"[Official Python 3 argparse Docs](https://docs.python.org/3/library/argparse.html)\n",
"\n",
"Let's start from the common example from [RealPython](https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/)\n",
"\n",
"Slightly modified from their example."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"def hello(args):\n",
" print('Hello, {0}!'.format(args.name))\n",
"\n",
"def goodbye(args):\n",
" print('Goodbye, {0}!'.format(args.name))\n",
"\n",
"def get_parser(version=\"0.1.0\"):\n",
" parser = argparse.ArgumentParser()\n",
" parser.add_argument('--version', action='version', version=version)\n",
" \n",
" subparsers = parser.add_subparsers()\n",
"\n",
" hello_parser = subparsers.add_parser('hello')\n",
" hello_parser.add_argument('name') # add the name argument\n",
" hello_parser.set_defaults(func=hello) # set the default function to hello\n",
"\n",
" goodbye_parser = subparsers.add_parser('goodbye')\n",
" goodbye_parser.add_argument('name')\n",
" goodbye_parser.set_defaults(func=goodbye)\n",
" return parser\n",
"\n",
"def runner(argv):\n",
" parser = get_parser()\n",
" args = parser.parse_args(argv)\n",
" return(args.func(args))\n",
"\n",
"#if __name__ == '__main__':\n",
"# args = parser.parse_args()\n",
"# args.func(args) # call the default function"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's define a util for testing."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"ARGS = [[\"hello\", 'my-name'],\n",
" [\"goodbye\", 'my-new-name'],\n",
" [\"hello\", '--help'], # ipython won't play nicely with the argparse useage of sys.exit\n",
" ]\n",
"\n",
"def run_example(runner_func, args=ARGS):\n",
" for arg in args:\n",
" print(\"Running with args {}\".format(arg))\n",
" runner_func(arg)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running with args ['hello', 'my-name']\n",
"Hello, my-name!\n",
"Running with args ['goodbye', 'my-new-name']\n",
"Goodbye, my-new-name!\n",
"Running with args ['hello', '--help']\n",
"usage: ipykernel_launcher.py hello [-h] name\n",
"\n",
"positional arguments:\n",
" name\n",
"\n",
"optional arguments:\n",
" -h, --help show this help message and exit\n"
]
},
{
"ename": "SystemExit",
"evalue": "0",
"output_type": "error",
"traceback": [
"An exception has occurred, use %tb to see the full traceback.\n",
"\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/mkocher/anaconda2/envs/core37/lib/python3.7/site-packages/IPython/core/interactiveshell.py:3275: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.\n",
" warn(\"To exit: use 'exit', 'quit', or Ctrl-D.\", stacklevel=1)\n"
]
}
],
"source": [
"run_example(runner)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running with args --version\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"usage: ipykernel_launcher.py [-h] [--version] {hello,goodbye} ...\n",
"ipykernel_launcher.py: error: invalid choice: '-' (choose from 'hello', 'goodbye')\n"
]
},
{
"ename": "SystemExit",
"evalue": "2",
"output_type": "error",
"traceback": [
"An exception has occurred, use %tb to see the full traceback.\n",
"\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 2\n"
]
}
],
"source": [
"run_example(runner, [('--version')])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Comments\n",
"\n",
"It looks like a reasonable interface. However, it's a bit verbose, specifically for the subparser example. There's also a bit of magic with the `.set_defaults(func=my_func)` that isn't particularly obvious. \n",
"\n",
"Depending on your point of view, this is feature utilizing the `dict` nature of the implementation details, or it's a lackluster design yielding an amorphous interface. Regardless, this ends up with the following pattern to call a specific subparser.\n",
"\n",
"```python\n",
"p = get_parser()\n",
"pargs = p.parse_args(sys.argv[1:])\n",
"return pargs.func(pargs)\n",
"```\n",
"\n",
"**Let's see if we can take our Functional Programming Techniques (FPT) and apply them to smooth out some of friction in the argparse interface while also adding more features and extensibility.** \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Thin Wrapping Layer to argparse\n",
"\n",
"Let's try to smooth out some of the duplication and try to extend the API in \"natural\" ways, such as to adding a setup hook and error handling mechanism.\n",
"\n",
"### Goals of Each Iteration using FPT\n",
"\n",
"- 1. Cleanup some low hanging duplication\n",
"- 2. Add an option to a subparser and add sharing option(s) functionality between subparsers\n",
"- 3. Extend: Add logging setup functionality. Add running a post execution hook.\n",
"- 4. Extend: Add error handling to map/handle errors to exit code\n",
"\n",
"The overall goal in each iteration is to demonstrate leveraging functions and closures as a core pattern yielding a simple and extensible design. Different use cases would be appropriate for each iteration. For example, Iteration #1 and #2 might be useful for one off use cases, whereas Iteration #3 or #4 would be more appropriate for production code. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Iteration #1\n",
"\n",
"For the first iteration let's try to remove some of the duplication and do a bit of cleanup.\n",
"\n",
"Specifically:\n",
"\n",
"- decouple the `argparse` IO layer from the lib code in our package. This will avoid leaking the amorphous parsed args into our lib code.\n",
"- remove a bit duplication in the `name` option\n",
"- define a utility function to help add a subparser\n",
"\n",
"\n",
"Let's define an function within `get_parser` to reduce a bit of biolerplate when adding subparsers. "
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"# Ideally, this should from my lib code and shouldn't\n",
"# mix the IO layer from CLI interface\n",
"\n",
"def lib_hello(name):\n",
" print('Hello, {0}!'.format(name))\n",
" \n",
"def lib_goodbye(name):\n",
" print('Goodbye, {0}!'.format(name))\n",
" \n",
"\n",
"# CLI. Wrapper funcs of (arg) -> f(a, b, c)\n",
"def run_hello(args):\n",
" return lib_hello(args.name)\n",
"\n",
"def run_goodbye(args):\n",
" return lib_goodbye(args.name)\n",
"\n",
"def _add_opt_name(p):\n",
" p.add_argument('name', help=\"User Name\")\n",
" return p\n",
"\n",
"\n",
"def get_parser(version='0.1.0'): \n",
"\n",
" parser = argparse.ArgumentParser(description=\"Help for Tool\")\n",
" parser.add_argument('--version', action='version', version=version)\n",
" subparsers = parser.add_subparsers()\n",
" \n",
" def _add_to_sp(name, add_opts, func):\n",
" p = subparsers.add_parser(name)\n",
" add_opts(p)\n",
" p.set_defaults(func=func)\n",
" return p\n",
"\n",
" _add_to_sp('hello', _add_opt_name, run_hello)\n",
" _add_to_sp('goodbye', _add_opt_name, run_goodbye)\n",
" \n",
" return parser\n",
"\n",
"def runner(argv):\n",
" p = get_parser()\n",
" pargs = p.parse_args(argv)\n",
" return pargs.func(pargs)\n",
"\n",
"#if __name__ == '__main__':\n",
"# sys.exit(runner(sys.argv[1:]))"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running with args ['hello', 'my-name']\n",
"Hello, my-name!\n",
"Running with args ['goodbye', 'my-new-name']\n",
"Goodbye, my-new-name!\n",
"Running with args ['hello', '--help']\n",
"usage: ipykernel_launcher.py hello [-h] name\n",
"\n",
"positional arguments:\n",
" name User Name\n",
"\n",
"optional arguments:\n",
" -h, --help show this help message and exit\n"
]
},
{
"ename": "SystemExit",
"evalue": "0",
"output_type": "error",
"traceback": [
"An exception has occurred, use %tb to see the full traceback.\n",
"\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n"
]
}
],
"source": [
"run_example(runner)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Iteration #2\n",
"\n",
"Let's cleanup a bit more and extend the `hello` subparser by adding a new option (`--num-times`)."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"def lib_hello(name, num_times=1):\n",
" for _ in range(0, num_times):\n",
" print('Hello, {0}!'.format(name))\n",
" \n",
"def run_hello(args):\n",
" return lib_hello(args.name, args.num_times)\n",
"\n",
"def add_opt_hello(p):\n",
" _add_opt_name(p)\n",
" p.add_argument('--num-times', help=\"Number of times to print hello\", default=1, type=int)\n",
" return p\n",
"\n",
"\n",
"def get_parser(version='0.1.0'): \n",
"\n",
" parser = argparse.ArgumentParser(description=\"Help for Tool\")\n",
" parser.add_argument('--version', action='version', version=version)\n",
" subparsers = parser.add_subparsers()\n",
" \n",
" def _add_to_sp(name, add_opts, func):\n",
" p = subparsers.add_parser(name)\n",
" add_opts(p)\n",
" p.set_defaults(func=func)\n",
" return p\n",
"\n",
" _add_to_sp('hello', add_opt_hello, run_hello)\n",
" _add_to_sp('goodbye', _add_opt_name, run_goodbye)\n",
" \n",
" return parser\n",
"\n",
"def runner(argv):\n",
" p = get_parser()\n",
" pargs = p.parse_args(argv)\n",
" return pargs.func(pargs)\n",
"\n",
"#if __name__ == '__main__':\n",
"# sys.exit(runner(sys.argv[1:]))"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running with args ['hello', 'My-Name', '--num-times', '2']\n",
"Hello, My-Name!\n",
"Hello, My-Name!\n",
"Running with args ['hello', '--help']\n",
"usage: ipykernel_launcher.py hello [-h] [--num-times NUM_TIMES] name\n",
"\n",
"positional arguments:\n",
" name User Name\n",
"\n",
"optional arguments:\n",
" -h, --help show this help message and exit\n",
" --num-times NUM_TIMES\n",
" Number of times to print hello\n"
]
},
{
"ename": "SystemExit",
"evalue": "0",
"output_type": "error",
"traceback": [
"An exception has occurred, use %tb to see the full traceback.\n",
"\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n"
]
}
],
"source": [
"run_example(runner, [['hello', 'My-Name','--num-times', '2'], ['hello', '--help']])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Iteration #3 \n",
"\n",
"In this iteration we're going to extend the current API to add two new features.\n",
"\n",
"- Add logging options and logging setup setup (This can be thought of a specific case of a pre-start hook).\n",
"- Add enabling of running an epilogue after our lib function has run (This is can a thought of as a post execution hook).\n",
"\n",
"Before we add this fuctionality, let's define a new pattern to help pass functions around more clearly.\n",
"\n",
"Instead of using `None` when passing a function as an arg/kw to a function, let's define an identity or 'null' functions that capture the core interface of the functions that will be passed into our driver function (e.g., `runner`). \n",
"\n",
"More concretely, instead of this:\n",
"\n",
"```python\n",
"def setup_logger(level, output_file=None):\n",
" # implementation omitted for avoid chatter\n",
" pass\n",
"\n",
"def runner(args, setup_logger=None):\n",
" if setup_logger is not None:\n",
" setup_logger(args.level, args.output_file)\n",
"```\n",
"\n",
"Well use `typing` from Python 3 combined with a null or identity function to clearly define the interface.\n",
"\n",
"\n",
"```python\n",
"from typing import Optional\n",
"\n",
"def setup_logger_null(level:str, output_file:Optional[str]=None) -> None:\n",
" # implementation omitted for avoid chatter\n",
" pass\n",
"\n",
"def runner(args, setup_logger=setup_logger_null) -> int:\n",
" setup_logger(args.level, args.output_file)\n",
"```\n",
"\n",
"Using the approach is useful for a few key reasons.\n",
"\n",
"1. Makes the API extendible by passing the function to the driver that will setup the logger or run the epilogue post execution hook.\n",
"2. Provides users with a concrete definition of the function interface (e.g., F(int, int) -> None)) for users to plugin into the API in a clear defined mechanism"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's add a concrete example with both a `setup_logger` and `run_epilogue` passed into the driver (`runner`) function.\n",
"\n",
"From an OO perspective, this can be thought of as similar to the [Strategy Pattern](https://en.wikipedia.org/wiki/Strategy_pattern)."
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"from typing import Optional\n",
"\n",
"logger = logging.getLogger(__name__)\n",
" \n",
"# util funcs for our CLI \n",
"def null_setup_logger(level:str, output_file:Optional[str]=None) -> None:\n",
" pass\n",
"\n",
"def setup_logger(level:str, output_file:Optional[str]=None):\n",
" # implementation omitted\n",
" print(\"mock setting up logger level={} file={}\".format(level, output_file))\n",
"\n",
"def null_run_epilogue(exit_code:int, started_at) -> None:\n",
" pass\n",
"\n",
"def run_epilogue(exit_code:int, started_at):\n",
" dt = datetime.datetime.now() - started_at\n",
" print(\"completed exit-code={} runtime:{:.2f} sec\".format(exit_code, dt.total_seconds()))\n",
" \n",
" \n",
"def _add_opt_logging(p):\n",
" f = p.add_argument\n",
" \n",
" f('--level', help=\"Logging Level\", default=\"INFO\")\n",
" f('--log-file', help=\"Write log to specific output file\", default=None)\n",
" return p\n",
"\n",
"\n",
"def get_parser(version='0.1.0'): \n",
"\n",
" parser = argparse.ArgumentParser(description=\"Help for Tool\")\n",
" parser.add_argument('--version', action='version', version=version)\n",
" subparsers = parser.add_subparsers()\n",
" \n",
" def _add_to_sp(name, add_opts, func):\n",
" p = subparsers.add_parser(name)\n",
" # For consistency we want the logging opts to be\n",
" # added to all subparsers\n",
" f = compose(add_opts, _add_opt_logging)\n",
" _ = f(p)\n",
" p.set_defaults(func=func)\n",
" return p\n",
"\n",
" _add_to_sp('hello', add_opt_hello, run_hello)\n",
" _add_to_sp('goodbye', _add_opt_name, run_goodbye)\n",
" \n",
" return parser\n",
"\n",
"def runner(argv, \n",
" setup_logger=setup_logger, \n",
" run_epilogue=run_epilogue) -> int:\n",
" \n",
" exit_code = 1\n",
" started_at = datetime.datetime.now()\n",
" p = get_parser()\n",
" pargs = p.parse_args(argv)\n",
" \n",
" # This is strongly coupled to _add_opt_logging\n",
" # This can be decoupled, by changing the func\n",
" # signature to f(args) -> Unit\n",
" setup_logger(pargs.level, pargs.log_file)\n",
" \n",
" # the pre exe could be passed in\n",
" logger.debug(pargs)\n",
" \n",
" try:\n",
" out = pargs.func(pargs)\n",
" exit_code = 0\n",
" except Exception as ex:\n",
" # we'll fix this in the next iteration\n",
" logger.error(ex)\n",
"\n",
" run_epilogue(exit_code, started_at)\n",
" return(out)\n",
"\n",
"#if __name__ == '__main__':\n",
"# sys.exit(runner(sys.argv[1:]))"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running with args ['hello', 'my-name']\n",
"mock setting up logger level=INFO file=None\n",
"Hello, my-name!\n",
"completed exit-code=0 runtime:0.00 sec\n",
"Running with args ['goodbye', 'my-new-name']\n",
"mock setting up logger level=INFO file=None\n",
"Goodbye, my-new-name!\n",
"completed exit-code=0 runtime:0.00 sec\n",
"Running with args ['hello', '--help']\n",
"usage: ipykernel_launcher.py hello [-h] [--level LEVEL] [--log-file LOG_FILE]\n",
" [--num-times NUM_TIMES]\n",
" name\n",
"\n",
"positional arguments:\n",
" name User Name\n",
"\n",
"optional arguments:\n",
" -h, --help show this help message and exit\n",
" --level LEVEL Logging Level\n",
" --log-file LOG_FILE Write log to specific output file\n",
" --num-times NUM_TIMES\n",
" Number of times to print hello\n"
]
},
{
"ename": "SystemExit",
"evalue": "0",
"output_type": "error",
"traceback": [
"An exception has occurred, use %tb to see the full traceback.\n",
"\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n"
]
}
],
"source": [
"run_example(runner)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note, because the the `runner` return type has changed (the exception is being caught) and now requires changing the main hook to explicitly exit from `sys`.\n",
"\n",
"```python\n",
"if __name__ == '__main__':\n",
" sys.exit(runner(sys.argv[1:]))\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Iteration #4\n",
"\n",
"Let's add custom error handling to map Python exceptions to integer return codes using a similar pattern that was used in Iteration #3."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"def error_handler(ex, default_error_code=1) -> int:\n",
" \"\"\"Responsible for mapping the Exception to an Int\n",
" \n",
" Note, this should also write the stacktrace to log or stderr.\n",
" \"\"\"\n",
" d = {'ZeroDivisionError': 7, \n",
" 'IOError':3,\n",
" 'ValueError': 4\n",
" }\n",
" return d.get(ex.__class__.__name__, default_error_code)\n",
"\n",
"\n",
"def runner(argv, \n",
" setup_logger=setup_logger,\n",
" error_handler=error_handler,\n",
" run_epilogue=run_epilogue) -> int:\n",
" \n",
" exit_code = 1\n",
" started_at = datetime.datetime.now()\n",
" \n",
" p = get_parser()\n",
" pargs = p.parse_args(argv)\n",
" \n",
" setup_logger(pargs.level, pargs.log_file)\n",
" logger.debug(pargs)\n",
" \n",
" try:\n",
" out = pargs.func(pargs)\n",
" exit_code = 0\n",
" except Exception as ex:\n",
" exit_code = error_handler(ex)\n",
"\n",
" run_epilogue(exit_code, started_at)\n",
" return(exit_code)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running with args ['hello', 'my-name']\n",
"mock setting up logger level=INFO file=None\n",
"Hello, my-name!\n",
"completed exit-code=0 runtime:0.00 sec\n",
"Running with args ['goodbye', 'my-new-name']\n",
"mock setting up logger level=INFO file=None\n",
"Goodbye, my-new-name!\n",
"completed exit-code=0 runtime:0.00 sec\n",
"Running with args ['hello', '--help']\n",
"usage: ipykernel_launcher.py hello [-h] [--level LEVEL] [--log-file LOG_FILE]\n",
" [--num-times NUM_TIMES]\n",
" name\n",
"\n",
"positional arguments:\n",
" name User Name\n",
"\n",
"optional arguments:\n",
" -h, --help show this help message and exit\n",
" --level LEVEL Logging Level\n",
" --log-file LOG_FILE Write log to specific output file\n",
" --num-times NUM_TIMES\n",
" Number of times to print hello\n"
]
},
{
"ename": "SystemExit",
"evalue": "0",
"output_type": "error",
"traceback": [
"An exception has occurred, use %tb to see the full traceback.\n",
"\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n"
]
}
],
"source": [
"run_example(runner)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Summary and Misc Comments\n",
"\n",
"We've accomplished a few items\n",
"\n",
"- Broke up the original argparse layer into a core orthagonal components, such as `get_parser`, `runner`. \n",
"- Shared common options using simple functions\n",
"- Smoothed out lack of expressiveness (resulting in boilerplate and duplication) of subparsers in argparse by using a few functions as core composable units\n",
"- Adding `setup_logger` and `run_epilogue` by passing functions into the driver. \n",
"\n",
"This concludes Part 3. Hopefully, the examples here provided insight into how to leverage FPT in smooth out duplication and boilerplate when interfacing to existing APIs. \n",
"\n",
"Best to you and your Python'ing.\n",
"\n",
"## Futher Reading\n",
"\n",
"- Other CLI libs: [click](https://click.palletsprojects.com/en/7.x/) and [docopt](https://github.com/docopt/docopt)\n",
"- [Discussion of Sharing Options](https://github.com/pallets/click/issues/108#issuecomment-194465429) using closures and functions in `click`. This is similar to spirit of the thin layer we've added to argparse. \n",
"- [Click has several nice examples](https://github.com/pallets/click/blob/master/click/decorators.py#L92) of decorators and functions are core design principles\n",
"- The patterns discussed in Part 3 are used extensively in [pbcommand](https://github.com/mpkocher/pbcommand/blob/master/pbcommand/cli/quick.py#L319)\n"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Today is 2019-02-21 16:59:28.615358\n"
]
}
],
"source": [
"print(\"Today is {}\".format(datetime.datetime.now()))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment