forked from JostTim/pGenUtils
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocs.py
More file actions
267 lines (209 loc) · 9.61 KB
/
docs.py
File metadata and controls
267 lines (209 loc) · 9.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# -*- coding: utf-8 -*-
"""Boilerplate:
A one line summary of the module or program, terminated by a period.
Leave one blank line. The rest of this docstring should contain an
overall description of the module or program. Optionally, it may also
contain a brief description of exported classes and functions and/or usage
examples.
<div id = "contributors">
Created on Wed Aug 25 10:40:20 2021
@author: Timothe
</div>
"""
import os, sys
from warnings import warn
import ast
import pathes
import strings
_EXCLUDE_BALISES = {"exclude_module":"EXCLUDE_MODULE_FROM_MKDOCSTRINGS","exclude_func_class":"EXCLUDE_FUNC_OR_CLASS_FROM_MKDOCSTRINGS"}
class mkds_pyfile_parser(ast.NodeVisitor):
"""
Attributes:
modulename str:
the name of the module without the .py extension
content dict:
a dictionnary with two keys : `functions` and `classes`,
each of wich containing a list of the classes and functions in the module
with the format : "module.fooclass" or "module.foofunction".
``content`` doesn't registers classes internal functions.
"""
def __init__(self, path):
"""
Constructor method of the class.
Args:
path (str): The full path to a python file that has been parsed.
Returns:
mkds_pyfile_parser : An instance of this class.
"""
# track context name and set of names marked as `global`
self.path = path
self.modulename = os.path.splitext(os.path.split(self.path)[1])[0]
self.context = [self.modulename]
self.content = {"functions":[],"classes":[]}
def is_empty(self):
"""
Returns:
bool: DESCRIPTION.
"""
if len(self.content["functions"]) == 0 and len(self.content["classes"]) == 0 :
return True
return False
def check_exclusion(self,node,exclusion_context):
node_doctring = ast.get_docstring(node)
if node_doctring is not None and "<" + _EXCLUDE_BALISES[exclusion_context] + ">" in node_doctring :
# hashtags are added around the balise to make this able to see this file even thou it contains the balise in it's text.
# Note : not usefull anymore as we check only the presence of the balises inside doctrings and not inside code.
# Let's keept this feature anyway to prevent any unexpected behavior trouble.
return True
return False
def aggreg_context(self):
aggregator = []
for item in self.context:
aggregator.append(item)
aggregator.append(".")
aggregator.pop()
agg = ''.join(aggregator)
return agg
def visit_FunctionDef(self, node):
self.context.append(node.name)
if len(self.context) == 2 and not self.check_exclusion(node,"exclude_func_class"):
#len(context) == 2 when we are inside module, and function. If we are inside module, class, function , we are == 3. And we don't want to register class functions.
self.content["functions"].append(self.aggreg_context())
self.generic_visit(node)
self.context.pop()
# treat coroutines the same way
visit_AsyncFunctionDef = visit_FunctionDef
def visit_ClassDef(self, node):
self.context.append(node.name)
if not self.check_exclusion(node,"exclude_func_class"):
self.content["classes"].append(self.aggreg_context())
self.generic_visit(node)
self.context.pop()
def visit_Lambda(self, node):
# lambdas are just functions, albeit with no statements, so no assignments.
# As they have no name though, they won't be documented
self.generic_visit(node)
def visit_Module(self, node):
if self.check_exclusion(node,"exclude_module"):
return None
self.generic_visit(node)
def visit(self,generic_arg=None):
"""
Takes as argument the output of ast.parse().
Itself takes as argument the str returned by reading a whole ``.py`` file.
This method is the way we visit every node of the file and on the way,
we register the classes and functions names.
Returns:
NoneType : Returns None. Use to be able to fill ``content`` with the classes and methods of the file.
Example:
```python
parser = mkds_pyfile_parser(filepath)
parser.visit()
print(parser.content)
```
Tip:
To explude functions/classes from being returned and lead to the creation of a mkdoctrings entry, use the balise :
``EXCLUDE_FUNC_OR_CLASS_FROM_MKDOCSTRINGS`` anywhere in the doctring of your function/class.
In the same way, a whole module can be excluded by including the balise
<``EXCLUDE_MODULE_FROM_MKDOCSTRINGS``> (with angle brackets) anywhere in the boilerplate at the top of your module.
"""
if generic_arg is None :
#This case is entered when a user externally calls visit with no argument.
with open(self.path,"r") as pyf:
parsed_data = ast.parse(pyf.read())
return super().visit(parsed_data)
else :
#This case is required when visit is called internally, by the super class ast.NodeVisitor.generic_visit(), with one argument.
#In that case, we just call the parent implementation of visit and return it's result transparently to not affect the class's behaviour.
return super().visit(generic_arg)
class mkds_markdown_indexfile():
"""TODO : make this class so we can add modules, their top doctring and their
child classes and function as list in a summary inside index.md in the doc folder or any project.
Also : add balises <div id = "contributors"> </div> around the boilerplate
'Created on Wed Aug 25 10:40:20 2021 @author: Timothe' to avoid putting it everywhere in the doc.
Add a balise to skip a part of the doctring <div id = "exclude_part_from_mkds"> </div>
Add a balise to skip the entire doctring <div id = "exclude_boilerplate_from_mkds">
Add a balise to specify the position of the content_index. <div id = "content_index">
Add links between summary items and their .md pages
"""
def __init__(self,path):
pass
def mkds_mod_mkdocs_yml_archi(path : str,appendings : list) -> None:
filepath = os.path.join(path,"mkdocs.yml")
original_content = []
with open(filepath,"r") as f :
for line in f.readlines():
original_content.append(line)
if "index.md" in line :
break
with open(filepath,"w") as f :
for line in original_content :
f.write(line)
for line in appendings :
f.write(line)
def mkds_markdownfile_content(item_name : str,item_type : str) -> str:
content = []
content.append("# \n")
content.append("::: ")
content.append(item_name + "\n")
if item_type == "functions" :
content.append(""" handler: python
rendering:
show_root_heading: true
show_root_full_path : false
show_category_heading : false
heading_level : 1""")
if item_type == "classes" :
content.append(""" handler: python
rendering:
show_root_heading: true
show_root_full_path : false
show_category_heading : true
show_root_members_full_path : true
show_if_no_docstring : false
heading_level : 1""")
return ''.join(content)
def mkds_make_docfiles(path : str) -> None:
def _entry_level(level : int,entry_name : str,quotes : bool = False) -> str :
line = " "*level + "- "
if quotes :
entry_name = _quoting(entry_name)
return line + entry_name + ": "
def _quoting(name : str) -> str:
return "'" + name + "'"
def _eol() -> str:
return "\n"
def _rel_path_md(names : str) -> str:
rel_path = ""
for name in names :
rel_path = rel_path + name + "\\"
rel_path = rel_path[:-1]
return _quoting(rel_path + ".md")
docpath = os.path.join(path,"doc")
pathes.is_or_makedir(docpath)
matched_py_files = pathes.re_folder_search(path,r".\.py$")
yml_new_architecture = []
for filepath in matched_py_files :
parser = mkds_pyfile_parser(filepath)
parser.visit()
if not parser.is_empty():
module_docpath = os.path.join(docpath,parser.modulename)
pathes.is_or_makedir(module_docpath)
matched_md_files = pathes.re_folder_search(module_docpath,r".\.md$")
if matched_md_files is not None :
for md_file_path in matched_md_files:
os.remove(md_file_path)
yml_new_architecture.append(_entry_level(1,parser.modulename)+_eol())
for func_type in ["classes","functions"]:
for func_item in parser.content[func_type]:
func_name = func_item.split(".")[1]
module_docfilename = os.path.join(module_docpath,func_name+".md")
yml_new_architecture.append(_entry_level(2,func_name,True)+_rel_path_md([parser.modulename,func_name])+_eol())
if os.path.isfile(module_docfilename):
warn("Warning : doc file have been overwritten")
with open(module_docfilename, "w") as mof :
mof.write(mkds_markdownfile_content(func_item,func_type))
mkds_mod_mkdocs_yml_archi(path,yml_new_architecture)
if __name__ == "__main__" :
test_path = r"C:\Users\Timothe\NasgoyaveOC\Professionnel\TheseUNIC\DevScripts\Python\__packages__\pGenUtils"
mkds_make_docfiles(test_path)