1{
2 "cells": [
3  {
4   "cell_type": "raw",
5   "metadata": {
6    "raw_mimetype": "text/restructuredtext"
7   },
8   "source": [
9    "====================\n",
10    "Parsing and Visiting\n",
11    "====================\n",
12    "\n",
13    "LibCST provides helpers to parse source code string as concrete syntax tree. In order to perform static analysis to identify patterns in the tree or modify the tree programmatically, we can use visitor pattern to traverse the tree. In this tutorial, we demonstrate a common three-step-workflow to build an automated refactoring (codemod) application:\n",
14    "\n",
15    "1. `Parse Source Code <#Parse-Source-Code>`_\n",
16    "2. `Build Visitor or Transformer <#Build-Visitor-or-Transformer>`_\n",
17    "3. `Generate Source Code <#Generate-Source-Code>`_\n",
18    "\n",
19    "Parse Source Code\n",
20    "=================\n",
21    "LibCST provides various helpers to parse source code as concrete syntax tree: :func:`~libcst.parse_module`, :func:`~libcst.parse_expression` and :func:`~libcst.parse_statement` (see :doc:`Parsing <parser>` for more detail). The default :class:`~libcst.CSTNode` repr provides pretty print formatting for reading the tree easily."
22   ]
23  },
24  {
25   "cell_type": "code",
26   "execution_count": null,
27   "metadata": {
28    "nbsphinx": "hidden"
29   },
30   "outputs": [],
31   "source": [
32    "import sys\n",
33    "sys.path.append(\"../../\")"
34   ]
35  },
36  {
37   "cell_type": "code",
38   "execution_count": null,
39   "metadata": {},
40   "outputs": [],
41   "source": [
42    "import libcst as cst\n",
43    "\n",
44    "cst.parse_expression(\"1 + 2\")"
45   ]
46  },
47  {
48   "cell_type": "raw",
49   "metadata": {
50    "raw_mimetype": "text/restructuredtext"
51   },
52   "source": [
53    "Example: add typing annotation from pyi stub file to Python source\n",
54    "------------------------------------------------------------------\n",
55    "Python `typing annotation <https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html>`_ was added in Python 3.5. Some Python applications add typing annotations in separate ``pyi`` stub files in order to support old Python versions. When applications decide to stop supporting old Python versions, they'll want to automatically copy the type annotation from a pyi file to a source file. Here we demonstrate how to do that easliy using LibCST. The first step is to parse the pyi stub and source files as trees."
56   ]
57  },
58  {
59   "cell_type": "code",
60   "execution_count": null,
61   "metadata": {},
62   "outputs": [],
63   "source": [
64    "py_source = '''\n",
65    "class PythonToken(Token):\n",
66    "    def __repr__(self):\n",
67    "        return ('TokenInfo(type=%s, string=%r, start_pos=%r, prefix=%r)' %\n",
68    "                self._replace(type=self.type.name))\n",
69    "\n",
70    "def tokenize(code, version_info, start_pos=(1, 0)):\n",
71    "    \"\"\"Generate tokens from a the source code (string).\"\"\"\n",
72    "    lines = split_lines(code, keepends=True)\n",
73    "    return tokenize_lines(lines, version_info, start_pos=start_pos)\n",
74    "'''\n",
75    "\n",
76    "pyi_source = '''\n",
77    "class PythonToken(Token):\n",
78    "    def __repr__(self) -> str: ...\n",
79    "\n",
80    "def tokenize(\n",
81    "    code: str, version_info: PythonVersionInfo, start_pos: Tuple[int, int] = (1, 0)\n",
82    ") -> Generator[PythonToken, None, None]: ...\n",
83    "'''\n",
84    "\n",
85    "source_tree = cst.parse_module(py_source)\n",
86    "stub_tree = cst.parse_module(pyi_source)"
87   ]
88  },
89  {
90   "cell_type": "raw",
91   "metadata": {
92    "raw_mimetype": "text/restructuredtext"
93   },
94   "source": [
95    "Build Visitor or Transformer\n",
96    "============================\n",
97    "For traversing and modifying the tree, LibCST provides Visitor and Transformer classes similar to the `ast module <https://docs.python.org/3/library/ast.html#ast.NodeVisitor>`_. To implement a visitor (read only) or transformer (read/write), simply implement a subclass of :class:`~libcst.CSTVisitor` or :class:`~libcst.CSTTransformer` (see :doc:`Visitors <visitors>` for more detail).\n",
98    "In the typing example, we need to implement a visitor to collect typing annotation from the stub tree and a transformer to copy the annotation to the function signature. In the visitor, we implement ``visit_FunctionDef`` to collect annotations. Later in the transformer, we implement ``leave_FunctionDef`` to add the collected annotations."
99   ]
100  },
101  {
102   "cell_type": "code",
103   "execution_count": null,
104   "metadata": {},
105   "outputs": [],
106   "source": [
107    "from typing import List, Tuple, Dict, Optional\n",
108    "\n",
109    "\n",
110    "class TypingCollector(cst.CSTVisitor):\n",
111    "    def __init__(self):\n",
112    "        # stack for storing the canonical name of the current function\n",
113    "        self.stack: List[Tuple[str, ...]] = []\n",
114    "        # store the annotations\n",
115    "        self.annotations: Dict[\n",
116    "            Tuple[str, ...],  # key: tuple of canonical class/function name\n",
117    "            Tuple[cst.Parameters, Optional[cst.Annotation]],  # value: (params, returns)\n",
118    "        ] = {}\n",
119    "\n",
120    "    def visit_ClassDef(self, node: cst.ClassDef) -> Optional[bool]:\n",
121    "        self.stack.append(node.name.value)\n",
122    "\n",
123    "    def leave_ClassDef(self, node: cst.ClassDef) -> None:\n",
124    "        self.stack.pop()\n",
125    "\n",
126    "    def visit_FunctionDef(self, node: cst.FunctionDef) -> Optional[bool]:\n",
127    "        self.stack.append(node.name.value)\n",
128    "        self.annotations[tuple(self.stack)] = (node.params, node.returns)\n",
129    "        return (\n",
130    "            False\n",
131    "        )  # pyi files don't support inner functions, return False to stop the traversal.\n",
132    "\n",
133    "    def leave_FunctionDef(self, node: cst.FunctionDef) -> None:\n",
134    "        self.stack.pop()\n",
135    "\n",
136    "\n",
137    "class TypingTransformer(cst.CSTTransformer):\n",
138    "    def __init__(self, annotations):\n",
139    "        # stack for storing the canonical name of the current function\n",
140    "        self.stack: List[Tuple[str, ...]] = []\n",
141    "        # store the annotations\n",
142    "        self.annotations: Dict[\n",
143    "            Tuple[str, ...],  # key: tuple of canonical class/function name\n",
144    "            Tuple[cst.Parameters, Optional[cst.Annotation]],  # value: (params, returns)\n",
145    "        ] = annotations\n",
146    "\n",
147    "    def visit_ClassDef(self, node: cst.ClassDef) -> Optional[bool]:\n",
148    "        self.stack.append(node.name.value)\n",
149    "\n",
150    "    def leave_ClassDef(\n",
151    "        self, original_node: cst.ClassDef, updated_node: cst.ClassDef\n",
152    "    ) -> cst.CSTNode:\n",
153    "        self.stack.pop()\n",
154    "        return updated_node\n",
155    "\n",
156    "    def visit_FunctionDef(self, node: cst.FunctionDef) -> Optional[bool]:\n",
157    "        self.stack.append(node.name.value)\n",
158    "        return (\n",
159    "            False\n",
160    "        )  # pyi files don't support inner functions, return False to stop the traversal.\n",
161    "\n",
162    "    def leave_FunctionDef(\n",
163    "        self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef\n",
164    "    ) -> cst.CSTNode:\n",
165    "        key = tuple(self.stack)\n",
166    "        self.stack.pop()\n",
167    "        if key in self.annotations:\n",
168    "            annotations = self.annotations[key]\n",
169    "            return updated_node.with_changes(\n",
170    "                params=annotations[0], returns=annotations[1]\n",
171    "            )\n",
172    "        return updated_node\n",
173    "\n",
174    "\n",
175    "visitor = TypingCollector()\n",
176    "stub_tree.visit(visitor)\n",
177    "transformer = TypingTransformer(visitor.annotations)\n",
178    "modified_tree = source_tree.visit(transformer)"
179   ]
180  },
181  {
182   "cell_type": "raw",
183   "metadata": {
184    "raw_mimetype": "text/restructuredtext"
185   },
186   "source": [
187    "Generate Source Code\n",
188    "====================\n",
189    "Generating the source code from a cst tree is as easy as accessing the :attr:`~libcst.Module.code` attribute on :class:`~libcst.Module`. After the code generation, we often use `ufmt <https://ufmt.omnilib.dev/en/stable/>`_ to reformate the code to keep a consistent coding style."
190   ]
191  },
192  {
193   "cell_type": "code",
194   "execution_count": null,
195   "metadata": {},
196   "outputs": [],
197   "source": [
198    "print(modified_tree.code)"
199   ]
200  },
201  {
202   "cell_type": "code",
203   "execution_count": null,
204   "metadata": {},
205   "outputs": [],
206   "source": [
207    "# Use difflib to show the changes to verify type annotations were added as expected.\n",
208    "import difflib\n",
209    "\n",
210    "print(\n",
211    "    \"\".join(\n",
212    "        difflib.unified_diff(py_source.splitlines(1), modified_tree.code.splitlines(1))\n",
213    "    )\n",
214    ")"
215   ]
216  },
217  {
218   "cell_type": "raw",
219   "metadata": {
220    "raw_mimetype": "text/restructuredtext"
221   },
222   "source": [
223    "For the sake of efficiency, we don't want to re-write the file when the transformer doesn't change the source code. We can use :meth:`~libcst.CSTNode.deep_equals` to check whether two trees have the same source code. Note that ``==`` checks the identity of tree object instead of representation."
224   ]
225  },
226  {
227   "cell_type": "code",
228   "execution_count": null,
229   "metadata": {},
230   "outputs": [],
231   "source": [
232    "if not modified_tree.deep_equals(source_tree):\n",
233    "    ...  # write to file"
234   ]
235  }
236 ],
237 "metadata": {
238  "celltoolbar": "Edit Metadata",
239  "kernelspec": {
240   "display_name": "Python 3",
241   "language": "python",
242   "name": "python3"
243  },
244  "language_info": {
245   "codemirror_mode": {
246    "name": "ipython",
247    "version": 3
248   },
249   "file_extension": ".py",
250   "mimetype": "text/x-python",
251   "name": "python",
252   "nbconvert_exporter": "python",
253   "pygments_lexer": "ipython3",
254   "version": "3.7.3"
255  }
256 },
257 "nbformat": 4,
258 "nbformat_minor": 2
259}
260