Skip to content

Instantly share code, notes, and snippets.

@hvnsweeting
Created November 17, 2019 07:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hvnsweeting/4cc161ea3915bcb0cd8fada43de3c12e to your computer and use it in GitHub Desktop.
Save hvnsweeting/4cc161ea3915bcb0cd8fada43de3c12e to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# AST - Abstract Syntax Tree"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## How python code run?\n",
"\n",
"```\n",
"Source code --- (parser) ---> AST --- (compiled) ---> bytecode ----> VM runs\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- https://docs.python.org/3/glossary.html#term-bytecode\n",
"- https://docs.python.org/3.7/reference/datamodel.html#index-55"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import ast"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Help on module ast:\n",
"\n",
"NAME\n",
" ast\n",
"\n",
"MODULE REFERENCE\n",
" https://docs.python.org/3.6/library/ast\n",
" \n",
" The following documentation is automatically generated from the Python\n",
" source files. It may be incomplete, incorrect or include features that\n",
" are considered implementation detail and may vary between Python\n",
" implementations. When in doubt, consult the module reference at the\n",
" location listed above.\n",
"\n",
"DESCRIPTION\n",
" ast\n",
" ~~~\n",
" \n",
" The `ast` module helps Python applications to process trees of the Python\n",
" abstract syntax grammar. The abstract syntax itself might change with\n",
" each Python release; this module helps to find out programmatically what\n",
" the current grammar looks like and allows modifications of it.\n",
" \n",
" An abstract syntax tree can be generated by passing `ast.PyCF_ONLY_AST` as\n",
" a flag to the `compile()` builtin function or by using the `parse()`\n",
" function from this module. The result will be a tree of objects whose\n",
" classes all inherit from `ast.AST`.\n",
" \n",
" A modified abstract syntax tree can be compiled into a Python code object\n",
" using the built-in `compile()` function.\n",
" \n",
" Additionally various helper functions are provided that make working with\n",
" the trees simpler. The main intention of the helper functions and this\n",
" module in general is to provide an easy to use interface for libraries\n",
" that work tightly with the python syntax (template engines for example).\n",
" \n",
" \n",
" :copyright: Copyright 2008 by Armin Ronacher.\n",
" :license: Python License.\n",
"\n",
"CLASSES\n",
" builtins.object\n",
" NodeVisitor\n",
" NodeTransformer\n",
" \n",
" class NodeTransformer(NodeVisitor)\n",
" | A :class:`NodeVisitor` subclass that walks the abstract syntax tree and\n",
" | allows modification of nodes.\n",
" | \n",
" | The `NodeTransformer` will walk the AST and use the return value of the\n",
" | visitor methods to replace or remove the old node. If the return value of\n",
" | the visitor method is ``None``, the node will be removed from its location,\n",
" | otherwise it is replaced with the return value. The return value may be the\n",
" | original node in which case no replacement takes place.\n",
" | \n",
" | Here is an example transformer that rewrites all occurrences of name lookups\n",
" | (``foo``) to ``data['foo']``::\n",
" | \n",
" | class RewriteName(NodeTransformer):\n",
" | \n",
" | def visit_Name(self, node):\n",
" | return copy_location(Subscript(\n",
" | value=Name(id='data', ctx=Load()),\n",
" | slice=Index(value=Str(s=node.id)),\n",
" | ctx=node.ctx\n",
" | ), node)\n",
" | \n",
" | Keep in mind that if the node you're operating on has child nodes you must\n",
" | either transform the child nodes yourself or call the :meth:`generic_visit`\n",
" | method for the node first.\n",
" | \n",
" | For nodes that were part of a collection of statements (that applies to all\n",
" | statement nodes), the visitor may also return a list of nodes rather than\n",
" | just a single node.\n",
" | \n",
" | Usually you use the transformer like this::\n",
" | \n",
" | node = YourTransformer().visit(node)\n",
" | \n",
" | Method resolution order:\n",
" | NodeTransformer\n",
" | NodeVisitor\n",
" | builtins.object\n",
" | \n",
" | Methods defined here:\n",
" | \n",
" | generic_visit(self, node)\n",
" | Called if no explicit visitor function exists for a node.\n",
" | \n",
" | ----------------------------------------------------------------------\n",
" | Methods inherited from NodeVisitor:\n",
" | \n",
" | visit(self, node)\n",
" | Visit a node.\n",
" | \n",
" | ----------------------------------------------------------------------\n",
" | Data descriptors inherited from NodeVisitor:\n",
" | \n",
" | __dict__\n",
" | dictionary for instance variables (if defined)\n",
" | \n",
" | __weakref__\n",
" | list of weak references to the object (if defined)\n",
" \n",
" class NodeVisitor(builtins.object)\n",
" | A node visitor base class that walks the abstract syntax tree and calls a\n",
" | visitor function for every node found. This function may return a value\n",
" | which is forwarded by the `visit` method.\n",
" | \n",
" | This class is meant to be subclassed, with the subclass adding visitor\n",
" | methods.\n",
" | \n",
" | Per default the visitor functions for the nodes are ``'visit_'`` +\n",
" | class name of the node. So a `TryFinally` node visit function would\n",
" | be `visit_TryFinally`. This behavior can be changed by overriding\n",
" | the `visit` method. If no visitor function exists for a node\n",
" | (return value `None`) the `generic_visit` visitor is used instead.\n",
" | \n",
" | Don't use the `NodeVisitor` if you want to apply changes to nodes during\n",
" | traversing. For this a special visitor exists (`NodeTransformer`) that\n",
" | allows modifications.\n",
" | \n",
" | Methods defined here:\n",
" | \n",
" | generic_visit(self, node)\n",
" | Called if no explicit visitor function exists for a node.\n",
" | \n",
" | visit(self, node)\n",
" | Visit a node.\n",
" | \n",
" | ----------------------------------------------------------------------\n",
" | Data descriptors defined here:\n",
" | \n",
" | __dict__\n",
" | dictionary for instance variables (if defined)\n",
" | \n",
" | __weakref__\n",
" | list of weak references to the object (if defined)\n",
"\n",
"FUNCTIONS\n",
" copy_location(new_node, old_node)\n",
" Copy source location (`lineno` and `col_offset` attributes) from\n",
" *old_node* to *new_node* if possible, and return *new_node*.\n",
" \n",
" dump(node, annotate_fields=True, include_attributes=False)\n",
" Return a formatted dump of the tree in *node*. This is mainly useful for\n",
" debugging purposes. The returned string will show the names and the values\n",
" for fields. This makes the code impossible to evaluate, so if evaluation is\n",
" wanted *annotate_fields* must be set to False. Attributes such as line\n",
" numbers and column offsets are not dumped by default. If this is wanted,\n",
" *include_attributes* can be set to True.\n",
" \n",
" fix_missing_locations(node)\n",
" When you compile a node tree with compile(), the compiler expects lineno and\n",
" col_offset attributes for every node that supports them. This is rather\n",
" tedious to fill in for generated nodes, so this helper adds these attributes\n",
" recursively where not already set, by setting them to the values of the\n",
" parent node. It works recursively starting at *node*.\n",
" \n",
" get_docstring(node, clean=True)\n",
" Return the docstring for the given node or None if no docstring can\n",
" be found. If the node provided does not have docstrings a TypeError\n",
" will be raised.\n",
" \n",
" increment_lineno(node, n=1)\n",
" Increment the line number of each node in the tree starting at *node* by *n*.\n",
" This is useful to \"move code\" to a different location in a file.\n",
" \n",
" iter_child_nodes(node)\n",
" Yield all direct child nodes of *node*, that is, all fields that are nodes\n",
" and all items of fields that are lists of nodes.\n",
" \n",
" iter_fields(node)\n",
" Yield a tuple of ``(fieldname, value)`` for each field in ``node._fields``\n",
" that is present on *node*.\n",
" \n",
" literal_eval(node_or_string)\n",
" Safely evaluate an expression node or a string containing a Python\n",
" expression. The string or node provided may only consist of the following\n",
" Python literal structures: strings, bytes, numbers, tuples, lists, dicts,\n",
" sets, booleans, and None.\n",
" \n",
" parse(source, filename='<unknown>', mode='exec')\n",
" Parse the source into an AST node.\n",
" Equivalent to compile(source, filename, mode, PyCF_ONLY_AST).\n",
" \n",
" walk(node)\n",
" Recursively yield all descendant nodes in the tree starting at *node*\n",
" (including *node* itself), in no specified order. This is useful if you\n",
" only want to modify nodes in place and don't care about the context.\n",
"\n",
"DATA\n",
" PyCF_ONLY_AST = 1024\n",
"\n",
"FILE\n",
" /usr/lib/python3.6/ast.py\n",
"\n",
"\n"
]
}
],
"source": [
"help(ast)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"ast.parse?"
]
},
{
"cell_type": "code",
"execution_count": 76,
"metadata": {},
"outputs": [],
"source": [
"tree = ast.parse(\"\"\"\n",
"def foo(a, b):\n",
" '''Calculates product of numbers in range'''\n",
" p = 0\n",
" for i in range(a, b):\n",
" p = p + i\n",
" return p\n",
" \n",
"foo(2, 10)\n",
"\n",
"\"\"\")"
]
},
{
"cell_type": "code",
"execution_count": 77,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"_ast.Module"
]
},
"execution_count": 77,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"type(tree)"
]
},
{
"cell_type": "code",
"execution_count": 78,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"\"Module(body=[FunctionDef(name='foo', args=arguments(args=[arg(arg='a', annotation=None), arg(arg='b', annotation=None)], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Expr(value=Str(s='Calculates product of numbers in range')), Assign(targets=[Name(id='p', ctx=Store())], value=Num(n=0)), For(target=Name(id='i', ctx=Store()), iter=Call(func=Name(id='range', ctx=Load()), args=[Name(id='a', ctx=Load()), Name(id='b', ctx=Load())], keywords=[]), body=[Assign(targets=[Name(id='p', ctx=Store())], value=BinOp(left=Name(id='p', ctx=Load()), op=Add(), right=Name(id='i', ctx=Load())))], orelse=[]), Return(value=Name(id='p', ctx=Load()))], decorator_list=[], returns=None), Expr(value=Call(func=Name(id='foo', ctx=Load()), args=[Num(n=2), Num(n=10)], keywords=[]))])\""
]
},
"execution_count": 78,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ast.dump(tree)"
]
},
{
"cell_type": "code",
"execution_count": 79,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[<_ast.FunctionDef at 0x7f722841fef0>, <_ast.Expr at 0x7f722845c748>]"
]
},
"execution_count": 79,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tree.body"
]
},
{
"cell_type": "code",
"execution_count": 80,
"metadata": {},
"outputs": [],
"source": [
"func, expr = tree.body"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## A sample function"
]
},
{
"cell_type": "code",
"execution_count": 81,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<_ast.FunctionDef at 0x7f722841fef0>"
]
},
"execution_count": 81,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"func"
]
},
{
"cell_type": "code",
"execution_count": 82,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'name': 'foo',\n",
" 'args': <_ast.arguments at 0x7f722841f7b8>,\n",
" 'body': [<_ast.Expr at 0x7f72285c5f98>,\n",
" <_ast.Assign at 0x7f72284d6fd0>,\n",
" <_ast.For at 0x7f72284d65f8>,\n",
" <_ast.Return at 0x7f72284004a8>],\n",
" 'decorator_list': [],\n",
" 'returns': None,\n",
" 'lineno': 2,\n",
" 'col_offset': 0}"
]
},
"execution_count": 82,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vars(func)"
]
},
{
"cell_type": "code",
"execution_count": 83,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"foo <_ast.arguments object at 0x7f722841f7b8>\n"
]
}
],
"source": [
"print(func.name, func.args)"
]
},
{
"cell_type": "code",
"execution_count": 84,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['a', 'b']\n"
]
}
],
"source": [
"print([a.arg for a in func.args.args])"
]
},
{
"cell_type": "code",
"execution_count": 85,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Calculates product of numbers in range'"
]
},
"execution_count": 85,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ast.get_docstring(func)"
]
},
{
"cell_type": "code",
"execution_count": 86,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[<_ast.Expr at 0x7f72285c5f98>,\n",
" <_ast.Assign at 0x7f72284d6fd0>,\n",
" <_ast.For at 0x7f72284d65f8>,\n",
" <_ast.Return at 0x7f72284004a8>]"
]
},
"execution_count": 86,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"func.body"
]
},
{
"cell_type": "code",
"execution_count": 87,
"metadata": {},
"outputs": [],
"source": [
"assign = func.body[1]"
]
},
{
"cell_type": "code",
"execution_count": 88,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'targets': [<_ast.Name at 0x7f72284d69e8>],\n",
" 'value': <_ast.Num at 0x7f72284d67f0>,\n",
" 'lineno': 4,\n",
" 'col_offset': 4}"
]
},
"execution_count": 88,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vars(assign)"
]
},
{
"cell_type": "code",
"execution_count": 89,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'p'"
]
},
"execution_count": 89,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"assign.targets[0].id"
]
},
{
"cell_type": "code",
"execution_count": 90,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0"
]
},
"execution_count": 90,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"assign.value.n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Run the code"
]
},
{
"cell_type": "code",
"execution_count": 91,
"metadata": {},
"outputs": [],
"source": [
"stree = ast.parse(\"print('Hello PYMIERs!')\")"
]
},
{
"cell_type": "code",
"execution_count": 92,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<_ast.Module at 0x7f72284222b0>"
]
},
"execution_count": 92,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stree"
]
},
{
"cell_type": "code",
"execution_count": 93,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<code object <module> at 0x7f72284240c0, file \"<ast>\", line 1>"
]
},
"execution_count": 93,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"code = compile(source=stree, filename='<ast>', mode='exec')\n",
"code"
]
},
{
"cell_type": "code",
"execution_count": 94,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hello PYMIERs!\n"
]
}
],
"source": [
"exec(code)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visit tree nodes"
]
},
{
"cell_type": "code",
"execution_count": 101,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"foo\n"
]
}
],
"source": [
"class FuncListor(ast.NodeVisitor):\n",
" def visit_FunctionDef(self, node):\n",
" print(node.name)\n",
" self.generic_visit(node)\n",
" \n",
"FuncListor().visit(tree)"
]
},
{
"cell_type": "code",
"execution_count": 99,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"\"Module(body=[FunctionDef(name='foo', args=arguments(args=[arg(arg='a', annotation=None), arg(arg='b', annotation=None)], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Expr(value=Str(s='Calculates product of numbers in range')), Assign(targets=[Name(id='p', ctx=Store())], value=Num(n=0)), For(target=Name(id='i', ctx=Store()), iter=Call(func=Name(id='range', ctx=Load()), args=[Name(id='a', ctx=Load()), Name(id='b', ctx=Load())], keywords=[]), body=[Assign(targets=[Name(id='p', ctx=Store())], value=BinOp(left=Name(id='p', ctx=Load()), op=Add(), right=Name(id='i', ctx=Load())))], orelse=[]), Return(value=Name(id='p', ctx=Load()))], decorator_list=[], returns=None), Expr(value=Call(func=Name(id='foo', ctx=Load()), args=[Num(n=2), Num(n=10)], keywords=[]))])\""
]
},
"execution_count": 99,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ast.dump(tree)"
]
},
{
"cell_type": "code",
"execution_count": 102,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"foo\n"
]
}
],
"source": [
"for node in ast.walk(tree):\n",
" if isinstance(node, ast.FunctionDef):\n",
" print(node.name)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 113,
"metadata": {},
"outputs": [
{
"ename": "ValueError",
"evalue": "not enough values to unpack (expected 3, got 2)",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-113-24077ba23852>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mc\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mValueError\u001b[0m: not enough values to unpack (expected 3, got 2)"
]
}
],
"source": [
"a, b, c = (1, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## How to detect above error without running it? "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Grep?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Or just check by AST"
]
},
{
"cell_type": "code",
"execution_count": 114,
"metadata": {},
"outputs": [],
"source": [
"ut = ast.parse(\"\"\"\n",
"a, b, c = (1, 2)\n",
"\"\"\", 'unbalance_tuple.py', 'exec')"
]
},
{
"cell_type": "code",
"execution_count": 115,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<_ast.Module at 0x7f72283c81d0>"
]
},
"execution_count": 115,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ut"
]
},
{
"cell_type": "code",
"execution_count": 116,
"metadata": {},
"outputs": [],
"source": [
"a = ut.body[0]"
]
},
{
"cell_type": "code",
"execution_count": 117,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"([<_ast.Tuple at 0x7f72283c80f0>], <_ast.Tuple at 0x7f72284516d8>)"
]
},
"execution_count": 117,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"a.targets, a.value"
]
},
{
"cell_type": "code",
"execution_count": 123,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3"
]
},
"execution_count": 123,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(a.targets[0].elts)"
]
},
{
"cell_type": "code",
"execution_count": 120,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"2"
]
},
"execution_count": 120,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(a.value.elts)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Pylint comes to rescue the world !"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"$ pylint unbalanced_tuple.py \n",
"************* Module unbalanced_tuple\n",
"unbalanced_tuple.py:1:0: W0632: Possible unbalanced tuple unpacking with sequence (1, 2): left side has 3 label(s), right side has 2 value(s) (unbalanced-tuple-unpacking)\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## A Checker"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Thank you!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## References\n",
"- https://docs.python.org/3/library/ast.html\n",
"- https://greentreesnakes.readthedocs.io/en/latest/"
]
}
],
"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.6.8"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment