|
29 | 29 | "\n",
|
30 | 30 | "import re\n",
|
31 | 31 | "from tokenize import tokenize,COMMENT\n",
|
32 |
| - "from ast import parse,FunctionDef\n", |
| 32 | + "from ast import parse,FunctionDef,AnnAssign\n", |
33 | 33 | "from io import BytesIO\n",
|
34 | 34 | "from textwrap import dedent\n",
|
35 | 35 | "from types import SimpleNamespace\n",
|
36 | 36 | "from inspect import getsource,isfunction,isclass,signature,Parameter\n",
|
| 37 | + "from dataclasses import dataclass, is_dataclass\n", |
37 | 38 | "from fastcore.utils import *\n",
|
38 | 39 | "\n",
|
39 | 40 | "from fastcore import docscrape\n",
|
|
217 | 218 | "outputs": [],
|
218 | 219 | "source": [
|
219 | 220 | "#export\n",
|
| 221 | + "def isdataclass(s):\n", |
| 222 | + " \"Check if `s` is a dataclass but not a dataclass' instance\"\n", |
| 223 | + " return is_dataclass(s) and isclass(s)\n", |
| 224 | + "\n", |
| 225 | + "def get_dataclass_source(s):\n", |
| 226 | + " \"Get source code for dataclass `s`\"\n", |
| 227 | + " return getsource(s) if not getattr(s, \"__module__\") == '__main__' else \"\"\n", |
| 228 | + "\n", |
| 229 | + "def get_source(s):\n", |
| 230 | + " \"Get source code for string, function object or dataclass `s`\"\n", |
| 231 | + " return getsource(s) if isfunction(s) else get_dataclass_source(s) if isdataclass(s) else s\n", |
| 232 | + "\n", |
220 | 233 | "def _parses(s):\n",
|
221 |
| - " \"Parse Python code in string or function object `s`\"\n", |
222 |
| - " return parse(dedent(getsource(s) if isfunction(s) else s))\n", |
| 234 | + " \"Parse Python code in string, function object or dataclass `s`\"\n", |
| 235 | + " return parse(dedent(get_source(s)))\n", |
223 | 236 | "\n",
|
224 | 237 | "def _tokens(s):\n",
|
225 | 238 | " \"Tokenize Python code in string or function object `s`\"\n",
|
226 |
| - " if isfunction(s): s = getsource(s)\n", |
| 239 | + " s = get_source(s)\n", |
227 | 240 | " return tokenize(BytesIO(s.encode('utf-8')).readline)\n",
|
228 | 241 | "\n",
|
229 | 242 | "_clean_re = re.compile('^\\s*#(.*)\\s*$')\n",
|
|
234 | 247 | "def _param_locs(s, returns=True):\n",
|
235 | 248 | " \"`dict` of parameter line numbers to names\"\n",
|
236 | 249 | " body = _parses(s).body\n",
|
237 |
| - " if len(body)!=1 or not isinstance(body[0], FunctionDef): return None\n", |
238 |
| - " defn = body[0]\n", |
239 |
| - " res = {arg.lineno:arg.arg for arg in defn.args.args}\n", |
240 |
| - " if returns and defn.returns: res[defn.returns.lineno] = 'return'\n", |
241 |
| - " return res" |
| 250 | + " if len(body)==1: #or not isinstance(body[0], FunctionDef): return None\n", |
| 251 | + " defn = body[0]\n", |
| 252 | + " if isinstance(defn, FunctionDef):\n", |
| 253 | + " res = {arg.lineno:arg.arg for arg in defn.args.args}\n", |
| 254 | + " if returns and defn.returns: res[defn.returns.lineno] = 'return'\n", |
| 255 | + " return res\n", |
| 256 | + " elif isdataclass(s):\n", |
| 257 | + " res = {arg.lineno:arg.target.id for arg in defn.body if isinstance(arg, AnnAssign)}\n", |
| 258 | + " return res\n", |
| 259 | + " return None" |
242 | 260 | ]
|
243 | 261 | },
|
244 | 262 | {
|
|
302 | 320 | "def docments(s, full=False, returns=True, eval_str=False):\n",
|
303 | 321 | " \"`dict` of parameter names to 'docment-style' comments in function or string `s`\"\n",
|
304 | 322 | " nps = parse_docstring(s)\n",
|
305 |
| - " if isclass(s): s = s.__init__ # Constructor for a class\n", |
| 323 | + " if isclass(s) and not is_dataclass(s): s = s.__init__ # Constructor for a class\n", |
306 | 324 | " comments = {o.start[0]:_clean_comment(o.string) for o in _tokens(s) if o.type==COMMENT}\n",
|
307 |
| - " parms = _param_locs(s, returns=returns)\n", |
| 325 | + " parms = _param_locs(s, returns=returns) or {}\n", |
308 | 326 | " docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()}\n",
|
309 | 327 | "\n",
|
310 | 328 | " if isinstance(s,str): s = eval(s)\n",
|
|
737 | 755 | "docments(add_mixed, full=True)"
|
738 | 756 | ]
|
739 | 757 | },
|
| 758 | + { |
| 759 | + "cell_type": "markdown", |
| 760 | + "metadata": {}, |
| 761 | + "source": [ |
| 762 | + "You can use docments with dataclasses:" |
| 763 | + ] |
| 764 | + }, |
| 765 | + { |
| 766 | + "cell_type": "code", |
| 767 | + "execution_count": null, |
| 768 | + "metadata": {}, |
| 769 | + "outputs": [ |
| 770 | + { |
| 771 | + "data": { |
| 772 | + "text/markdown": [ |
| 773 | + "```json\n", |
| 774 | + "{'age': None, 'name': None, 'return': None, 'weight': None}\n", |
| 775 | + "```" |
| 776 | + ], |
| 777 | + "text/plain": [ |
| 778 | + "{'name': None, 'age': None, 'weight': None, 'return': None}" |
| 779 | + ] |
| 780 | + }, |
| 781 | + "execution_count": null, |
| 782 | + "metadata": {}, |
| 783 | + "output_type": "execute_result" |
| 784 | + } |
| 785 | + ], |
| 786 | + "source": [ |
| 787 | + "@dataclass\n", |
| 788 | + "class Person:\n", |
| 789 | + " name:str # The name of the person\n", |
| 790 | + " age:int # The age of the person\n", |
| 791 | + " weight:float # The weight of the person\n", |
| 792 | + "\n", |
| 793 | + "docments(Person)" |
| 794 | + ] |
| 795 | + }, |
| 796 | + { |
| 797 | + "cell_type": "markdown", |
| 798 | + "metadata": {}, |
| 799 | + "source": [ |
| 800 | + "Caveat: if class was defined in online notebook, docments will not contain parameters' comments. This is because the source code is not available in the notebook. After converting the notebook to a script, the docments will be available. Thus, documentation will have correct parameters' comments." |
| 801 | + ] |
| 802 | + }, |
| 803 | + { |
| 804 | + "cell_type": "code", |
| 805 | + "execution_count": null, |
| 806 | + "metadata": {}, |
| 807 | + "outputs": [], |
| 808 | + "source": [ |
| 809 | + "tmp = Path('person.py')\n", |
| 810 | + "tmp.write_text('''\n", |
| 811 | + "from dataclasses import dataclass\n", |
| 812 | + "@dataclass\n", |
| 813 | + "class Person:\n", |
| 814 | + " name:str # The name of the person\n", |
| 815 | + " age:int # The age of the person\n", |
| 816 | + " weight:float # The weight of the person\n", |
| 817 | + "''')\n", |
| 818 | + "import person\n", |
| 819 | + "tst_dict = { \n", |
| 820 | + " 'age': 'The age of the person',\n", |
| 821 | + " 'name': 'The name of the person',\n", |
| 822 | + " 'return': None,\n", |
| 823 | + " 'weight': 'The weight of the person'}\n", |
| 824 | + "assert tst_dict == docments(person.Person)\n", |
| 825 | + "try: # to conform to python 3.6\n", |
| 826 | + " tmp.unlink()\n", |
| 827 | + "except FileNotFoundError:\n", |
| 828 | + " pass" |
| 829 | + ] |
| 830 | + }, |
740 | 831 | {
|
741 | 832 | "cell_type": "markdown",
|
742 | 833 | "metadata": {},
|
|
784 | 875 | ],
|
785 | 876 | "metadata": {
|
786 | 877 | "kernelspec": {
|
787 |
| - "display_name": "Python 3 (ipykernel)", |
| 878 | + "display_name": "Python 3.9.12 ('base')", |
788 | 879 | "language": "python",
|
789 | 880 | "name": "python3"
|
790 | 881 | }
|
|
0 commit comments