forked from mahmoud/awesome-python-applications
-
Notifications
You must be signed in to change notification settings - Fork 0
/
generate_docs.py
283 lines (222 loc) · 9.53 KB
/
generate_docs.py
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import attr
from ruamel import yaml
from boltons.dictutils import OMD
from boltons.fileutils import iter_find_files, atomic_save
TOOLS_PATH = os.path.dirname(os.path.abspath(__file__))
TEMPLATES_PATH = os.path.dirname(os.path.abspath(__file__)) + '/templates/'
# house = u"\u2302"
BULLET = '1.'
INDENT = ' ' * 4
@attr.s(frozen=True)
class TagEntry(object):
tag = attr.ib()
tag_type = attr.ib()
title = attr.ib()
desc = attr.ib(default='')
subtags = attr.ib(default=(), repr=False)
tag_path = attr.ib(default=(), repr=False)
fq_tag = attr.ib(default=None)
@property
def is_fq(self):
return self.tag == self.fq_tag
@attr.s(frozen=True)
class Project(object):
name = attr.ib()
desc = attr.ib(default='')
tags = attr.ib(default=())
urls = attr.ib(default=())
@classmethod
def from_dict(cls, d):
kwargs = dict(d)
kwargs['tags'] = tuple(kwargs.get('tags', ()))
cur_urls = ()
for k in list(kwargs):
if not k.endswith('_url'):
continue
cur_urls += ((k[:-4], kwargs.pop(k)),)
kwargs['urls'] = cur_urls
return cls(**kwargs)
def _unwrap_dict(d):
if not len(d) == 1:
raise ValueError('expected single-member dict')
return list(d.items())[0]
class ProjectList(object):
def __init__(self, project_list, tagsonomy):
self.project_list = []
self.tagsonomy = tagsonomy
self.tag_registry = OMD()
for tag_group in ('topic', 'platform'): # TODO: framework, license
for tag in self.tagsonomy[tag_group]:
self.register_tag(tag_group, tag)
for project in project_list:
new_tags = soft_sorted(project.get('tags', []), first=self.tag_registry.keys())
project['tags'] = new_tags
self.project_list.append(Project.from_dict(project))
@classmethod
def from_path(cls, path):
data = yaml.safe_load(open(path, encoding='utf-8'))
return cls(data['projects'], data['tagsonomy'])
def register_tag(self, tag_type, tag_entry, tag_path=()):
if isinstance(tag_entry, str):
tag, tag_entry = tag_entry, {}
else:
tag, tag_entry = _unwrap_dict(tag_entry)
tag_entry = dict(tag_entry)
tag_entry['tag'] = tag
tag_entry['tag_type'] = tag_type
if not tag_entry.get('title'):
tag_entry["title"] = tag.replace('_', ' ').title()
subtags = []
for subtag_entry in tag_entry.pop('subtags', []):
st = self.register_tag(tag_type, subtag_entry,
tag_path=(tag,) if not tag_path else tag_path + (tag,))
subtags.append(st)
tag_entry['subtags'] = tuple(subtags)
tag_entry['fq_tag'] = '.'.join(tag_path + (tag,))
if not tag_path:
ret = TagEntry(**tag_entry)
else:
ret = TagEntry(tag_path=tag_path, **tag_entry)
self.tag_registry[tag] = ret
return ret
def get_projects_by_type(self, type_name):
ret = OMD()
for tag, tag_entry in self.tag_registry.items():
if tag_entry.tag_type != type_name:
continue
ret[tag_entry] = []
for project in self.project_list:
if tag in project.tags:
ret[tag_entry].append(project)
ret[tag_entry].sort(key=lambda x: x.name.lower())
return ret
# sort of document the expected ones, even when they match the
# .title() pattern
_URL_LABEL_MAP = {'wp': 'WP',
'home': 'Home',
'repo': 'Repo',
'docs': 'Docs',
'pypi': 'PyPI'}
_URL_ORDER = ['repo', 'home', 'wp', 'docs']
def soft_sorted(iterable, first=None, last=None, key=None, reverse=False):
"""For when you care about the order of some elements, but not about
others.
Use this to float to the top and/or sink to the bottom a specific
ordering, while sorting the rest of the elements according to
normal :func:`sorted` rules.
>>> soft_sorted(['two', 'b', 'one', 'a'], first=['one', 'two'])
['one', 'two', 'a', 'b']
>>> soft_sorted(range(7), first=[6, 15], last=[2, 4], reverse=True)
[6, 5, 3, 1, 0, 2, 4]
Args:
iterable (list): A list or other iterable to sort.
first (list): A sequence to enforce for elements which should
appear at the beginning of the returned list.
last (list): A sequence to enforce for elements which should
appear at the end of the returned list.
key (callable): Callable used to generate a comparable key for
each item to be sorted, same as the key in
:func:`sorted`. Note that entries in *first* and *last*
should be the keys for the items. Defaults to
passthrough/the identity function.
reverse (bool): Whether or not elements not explicitly ordered
by *first* and *last* should be in reverse order or not.
Returns a new list in sorted order.
"""
first = first or []
last = last or []
key = key or (lambda x: x)
seq = list(iterable)
other = [x for x in seq if not ((first and key(x) in first) or (last and key(x) in last))]
other.sort(key=key, reverse=reverse)
if first:
first = sorted([x for x in seq if key(x) in first], key=lambda x: first.index(key(x)))
if last:
last = sorted([x for x in seq if key(x) in last], key=lambda x: last.index(key(x)))
return first + other + last
def _format_url_name(name):
return _URL_LABEL_MAP.get(name, name.title())
def format_category(project_map, tag_entry):
lines = []
append = lines.append
def _format_tag(project_map, tag_entry, level=2):
append('%s <a id="tag-%s" href="#tag-%s">%s</a>' %
('#' * level, tag_entry.fq_tag or tag_entry.tag, tag_entry.fq_tag or tag_entry.tag, tag_entry.title))
append('')
if tag_entry.desc:
append(tag_entry.desc)
append('')
if tag_entry.subtags:
append('')
for subtag_entry in tag_entry.subtags:
_format_tag(project_map, subtag_entry, level=level + 1)
append('%s <a id="tag-%s-other" href="#tag-%s-other">Other %s projects</a>' %
('#' * (level + 1), tag_entry.tag, tag_entry.tag, tag_entry.title))
for project in project_map[tag_entry]:
tmpl = ' {bullet} **{name}** - ({links}) {desc}'
links = ', '.join(['[%s](%s)' % (_format_url_name(name), url) for name, url
in soft_sorted(project.urls, key=lambda x: x[0], first=_URL_ORDER[:-1], last=_URL_ORDER[-1:])])
line = tmpl.format(bullet=BULLET, name=project.name, links=links, desc=project.desc)
if len(project.tags) > 1:
other_tags = [t for t in project.tags if t != tag_entry.tag]
line += ' `(%s)`' % ', '.join(other_tags)
lines.append(line)
append('')
return '\n'.join(lines)
return _format_tag(project_map, tag_entry)
def format_tag_toc(project_map):
lines = []
def _format_tag_toc(tag_entries, path=()):
for te in tag_entries:
if te.tag_path != path:
continue
entry_count = len(project_map[te])
if te.subtags:
entry_count = len(project_map[te]) + len(set.union(*[set(project_map[st]) for st in te.subtags]))
link_text = '<a href="#tag-%s">%s</a> *(%s)*' % (te.fq_tag or te.tag, te.title, entry_count)
lines.append((INDENT * len(te.tag_path)) + BULLET + ' ' + link_text)
if te.subtags:
_format_tag_toc(te.subtags, path=path + (te.tag,))
if len(project_map[te]):
link_text = ('<a href="#tag-%s-other">Other %s projects</a> *(%s)*'
% (te.fq_tag or te.tag, te.title, len(project_map[te])))
lines.append((INDENT * (len(te.tag_path) + 1)) + BULLET + ' ' + link_text)
return
_format_tag_toc(project_map.keys())
return '\n'.join(lines)
def format_all_categories(project_map):
parts = []
for tag_entry in project_map:
if tag_entry.tag_path:
continue
if not project_map[tag_entry] and not tag_entry.subtags:
continue # TODO: some message, inviting additions
text = format_category(project_map, tag_entry)
parts.append(text)
return '\n'.join(parts)
def main():
plist = ProjectList.from_path('projects.yaml')
print([p for p in plist.project_list if not p.desc])
topic_map = plist.get_projects_by_type('topic')
topic_toc_text = format_tag_toc(topic_map)
projects_by_topic = format_all_categories(topic_map)
plat_map = plist.get_projects_by_type('platform')
plat_toc_text = format_tag_toc(plat_map)
projects_by_plat = format_all_categories(plat_map)
context = {'TOPIC_TOC': topic_toc_text,
'TOPIC_TEXT': projects_by_topic,
'PLATFORM_TOC': plat_toc_text,
'PLATFORM_TEXT': projects_by_plat,
'TOTAL_COUNT': len(plist.project_list)}
for filename in iter_find_files(TEMPLATES_PATH, '*.tmpl.md'):
tmpl_text = open(filename).read()
target_filename = os.path.split(filename)[1].replace('.tmpl', '')
output_text = tmpl_text.format(**context)
with atomic_save(target_filename) as f:
f.write(output_text.encode('utf8'))
return
if __name__ == '__main__':
main()