-
Notifications
You must be signed in to change notification settings - Fork 51
/
graph.py
4306 lines (3800 loc) · 194 KB
/
graph.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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf8 -*-
########################################################################################
# This file is part of exhale. Copyright (c) 2017-2024, Stephen McDowell. #
# Full BSD 3-Clause license available here: #
# #
# https://github.com/svenevs/exhale/blob/master/LICENSE #
########################################################################################
from __future__ import unicode_literals
from . import configs
from . import parse
from . import utils
import re
import os
import sys
import codecs
import hashlib
import itertools
from pathlib import Path
import platform
import textwrap
from bs4 import BeautifulSoup
try:
# Python 2 StringIO
from cStringIO import StringIO
except ImportError:
# Python 3 StringIO
from io import StringIO
__all__ = ["ExhaleRoot", "ExhaleNode"]
########################################################################################
#
##
###
####
##### Graph representation.
####
###
##
#
########################################################################################
class ExhaleNode(object):
'''
A wrapper class to track parental relationships, filenames, etc.
**Parameters**
``name`` (str)
The name of the compound.
``kind`` (str)
The kind of the compound (see :data:`~exhale.utils.AVAILABLE_KINDS`).
``refid`` (str)
The reference ID that Doxygen has associated with this compound.
**Attributes**
``kind`` (str)
The value of the ``kind`` parameter.
``name`` (str)
The value of the ``name`` parameter.
``refid`` (str)
The value of the ``refid`` parameter.
``children`` (list)
A potentially empty list of ``ExhaleNode`` object references that are
considered a child of this Node. Please note that a child reference in any
``children`` list may be stored in **many** other lists. Mutating a given
child will mutate the object, and therefore affect other parents of this
child. Lastly, a node of kind ``enum`` will never have its ``enumvalue``
children as it is impossible to rebuild that relationship without more
Doxygen xml parsing.
``parent`` (:class:`~exhale.graph.ExhaleNode`)
If an ExhaleNode is determined to be a child of another ExhaleNode, this
node will be added to its parent's ``children`` list, and a reference to
the parent will be in this field. Initialized to ``None``, make sure you
check that it is an object first.
.. warning::
Do not ever set the ``parent`` of a given node if the would-be parent's
kind is ``"file"``. Doing so will break many important relationships,
such as nested class definitions. Effectively, **every** node will be
added as a child to a file node at some point. The file node will track
this, but the child should not.
The following three member variables are stored internally, but managed
externally by the :class:`~exhale.graph.ExhaleRoot` class:
``file_name`` (str)
The name of the file to create. Set to ``None`` on creation, refer to
:func:`~exhale.graph.ExhaleRoot.initializeNodeFilenameAndLink`.
``link_name`` (str)
The name of the reStructuredText link that will be at the top of the file.
Set to ``None`` on creation, refer to
:func:`~exhale.graph.ExhaleRoot.initializeNodeFilenameAndLink`.
``title`` (str)
The title that will appear at the top of the reStructuredText file
``file_name``. When the reStructuredText document for this node is being
written, the root object will set this field.
The following two fields are used for tracking what has or has not already been
included in the hierarchy views. Things like classes or structs in the global
namespace will not be found by :func:`~exhale.graph.ExhaleNode.inClassHierarchy`,
and the ExhaleRoot object will need to track which ones were missed.
``in_class_hierarchy`` (bool)
Whether or not this node has already been incorporated in the class view.
``in_file_hierarchy`` (bool)
Whether or not this node has already been incorporated in the file view.
This class wields duck typing. If ``self.kind == "file"``, then the additional
member variables below exist:
``namespaces_used`` (list)
A list of namespace nodes that are either defined or used in this file.
``includes`` (list)
A list of strings that are parsed from the Doxygen xml for this file as
include directives.
``included_by`` (list)
A list of (refid, name) string tuples that are parsed from the Doxygen xml
for this file presenting all of the other files that include this file.
They are stored this way so that the root class can later link to that file
by its refid.
``location`` (str)
A string parsed from the Doxygen xml for this file stating where this file
is physically in relation to the *Doxygen* root.
``program_listing`` (list)
A list of strings that is the Doxygen xml <programlisting>, without the
opening or closing <programlisting> tags.
``program_file`` (list)
Managed externally by the root similar to ``file_name`` etc, this is the
name of the file that will be created to display the program listing if it
exists. Set to ``None`` on creation, refer to
:func:`~exhale.graph.ExhaleRoot.initializeNodeFilenameAndLink`.
``program_link_name`` (str)
Managed externally by the root similar to ``file_name`` etc, this is the
reStructuredText link that will be declared at the top of the
``program_file``. Set to ``None`` on creation, refer to
:func:`~exhale.graph.ExhaleRoot.initializeNodeFilenameAndLink`.
'''
def __init__(self, name, kind, refid):
self.name = os.path.normpath(name) if kind == 'dir' else name
self.kind = kind
self.refid = refid
self.root_owner = None # the ExhaleRoot owner
self.template_params = [] # only populated if found
# for inheritance
self.base_compounds = []
self.derived_compounds = []
# used for establishing a link to the file something was done in for leaf-like
# nodes conveniently, files also have this defined as their name making
# comparison easy :)
self.def_in_file = None
# la familia
self.children = [] # ExhaleNodes
self.parent = None # if reparented, will be an ExhaleNode
# managed externally
self.file_name = None
self.link_name = None
self.title = None
# representation of hierarchies
self.in_page_hierarchy = False
self.in_class_hierarchy = False
self.in_file_hierarchy = False
# kind-specific additional information
if self.kind == "file":
self.namespaces_used = [] # ExhaleNodes
self.includes = [] # strings
self.included_by = [] # (refid, name) tuples
self.language = ""
self.location = ""
self.program_listing = [] # strings
self.program_file = ""
self.program_link_name = ""
if self.kind == "function":
self.return_type = None # string (void, int, etc)
self.parameters = [] # list of strings: ["int", "int"] for foo(int x, int y)
self.template = None # list of strings
def __lt__(self, other):
'''
The ``ExhaleRoot`` class stores a bunch of lists of ``ExhaleNode`` objects.
When these lists are sorted, this method will be called to perform the sorting.
:Parameters:
``other`` (ExhaleNode)
The node we are comparing whether ``self`` is less than or not.
:Return (bool):
True if ``self`` is less than ``other``, False otherwise.
'''
# allows alphabetical sorting within types
if self.kind == other.kind:
if self.kind != "page":
return self.name.lower() < other.name.lower()
else:
# Arbitrarily stuff "indexpage" refid to the front. As doxygen presents
# things, it shows up last, but it does not matter since the sort we
# really care about will be with lists that do *NOT* have indexpage in
# them (for creating the page view hierarchy).
if self.refid == "indexpage":
return True
elif other.refid == "indexpage":
return False
# NOTE: kind of wasteful, but ordered_refs has ALL pages
# but realistically, there won't be *that* many pages. right? ;)
ordered_refs = [
p.refid for p in self.root_owner.index_xml_page_ordering
]
return ordered_refs.index(self.refid) < ordered_refs.index(other.refid)
# treat structs and classes as the same type
elif self.kind == "struct" or self.kind == "class":
if other.kind != "struct" and other.kind != "class":
return True
else:
if self.kind == "struct" and other.kind == "class":
return True
elif self.kind == "class" and other.kind == "struct":
return False
else:
return self.name.lower() < other.name.lower()
# otherwise, sort based off the kind
else:
return self.kind < other.kind
def __repr__(self):
# NOTE: there will never be a way to eval(repr()) anything from this! These are
# exclusively for developer debugging convenience.
prefix = self.kind.capitalize()
if self.kind == "function":
return f"{prefix}({self.full_signature()})"
if self.kind == "file":
prefix += f"({self.location}"
else:
prefix += f"({self.name}"
if self.template_params:
prefix += f", template=<"
last_comma_index = len(self.template_params) - 1
for idx, (param_t, decl_n, def_n) in enumerate(self.template_params):
_, typeid = param_t
prefix += f"{typeid}"
if decl_n:
prefix += f" {decl_n}"
if def_n:
prefix += f" = {def_n}"
if idx < last_comma_index:
prefix += ", "
prefix += ">"
return f"{prefix}, n_kids={len(self.children)})"
def set_owner(self, root):
"""Sets the :class:`~exhale.graph.ExhaleRoot` owner ``self.root_owner``."""
# needed to be able to track the page orderings as presented in index.xml
self.root_owner = root
def breathe_identifier(self):
"""
The unique identifier for breathe directives.
.. note::
This method is currently assumed to only be called for nodes that are
in :data:`exhale.utils.LEAF_LIKE_KINDS` (see also
:func:`exhale.graph.ExhaleRoot.generateSingleNodeRST` where it is used).
**Return**
:class:`python:str`
Usually, this will just be ``self.name``. However, for functions in
particular the signature must be included to distinguish overloads.
"""
if self.kind == "function":
# TODO: breathe bug with templates and overloads, don't know what to do...
return "{name}({parameters})".format(
name=self.name,
parameters=", ".join(self.parameters)
)
return self.name
def full_signature(self):
"""
The full signature of a ``"function"`` node.
**Return**
:class:`python:str`
The full signature of the function, including template, return type,
name, and parameter types.
**Raises**
:class:`python:RuntimeError`
If ``self.kind != "function"``.
"""
if self.kind == "function":
return "{template}{return_type} {name}({parameters})".format(
template="template <{0}> ".format(", ".join(self.template)) if self.template is not None else "",
return_type=self.return_type,
name=self.name,
parameters=", ".join(self.parameters)
)
raise RuntimeError(
"full_signature may only be called for a 'function', but {name} is a '{kind}' node.".format(
name=self.name, kind=self.kind
)
)
def templateParametersStringAsRestList(self, nodeByRefid):
'''
.. todo::
document this, create another method for creating this without the need for
generating links, to be used in making the node titles and labels
'''
if not self.template_params:
return None
else:
param_stream = StringIO()
for param_t, decl_n, def_n in self.template_params:
refid, typeid = param_t
# Say you wanted a custom link text 'custom', and somewhere
# else you had an internal link '.. _some_link:'. Then you do
# `custom <some_link_>`_
# LOL. RST is confusing
if refid:
# Easy case: the refid is something Exhale is explicitly documenting
if refid in nodeByRefid:
link = "{0}_".format(nodeByRefid[refid].link_name)
else:
# It's going to get generated by Breathe down the line, we need
# to reference the page the directive will appear on.
parent_refid = ""
for key in nodeByRefid:
if len(key) > len(parent_refid) and key in refid:
parent_refid = key
parent = nodeByRefid[parent_refid]
parent_page = os.path.basename(parent.file_name.replace(".rst", ".html"))
link = "{page}#{refid}".format(page=parent_page, refid=refid)
param_stream.write(
"#. `{typeid} <{link}>`_".format(
typeid=typeid,
# Not necessarily an ExhaleNode link, should be a link by
# the time Breathe is finished?
link=link
)
)
close_please = False
else:
param_stream.write("#. ``{typeid}".format(typeid=typeid))
close_please = True
# The type is in there, but when parsed it may have given something like
# `class X` for the typeid (meaning nothing else to write). For others,
# the decl_n is the declared name of the template parameter. E.g. it
# was parsed as `typeid <- class` and `decl_n <- X`.
if decl_n:
param_stream.write(" ")
if not close_please:
param_stream.write("``")
param_stream.write("{decl_n}".format(decl_n=decl_n))
close_please = True
# When templates provide a default value, `def_n` is it. When parsed,
# if the `decl_n` and `def_n` are the same, `def_n` is explicitly set
# to be None.
if def_n:
param_stream.write(" ")
if not close_please:
param_stream.write("``")
param_stream.write("= {def_n}``".format(def_n=def_n))
close_please = True
if close_please:
param_stream.write("``")
param_stream.write("\n")
param_stream.write("\n")
param_value = param_stream.getvalue()
param_stream.close()
return param_value
def baseOrDerivedListString(self, lst, nodeByRefid):
'''
.. todo:: long time from now: intersphinx should be possible here
'''
# lst should either be self.base_compounds or self.derived_compounds
if not lst:
return None
bod_stream = StringIO()
for prot, refid, string in lst:
bod_stream.write("- ")
# Include the prototype
if prot:
bod_stream.write("``{0}".format(prot))
please_close = True
else:
please_close = False
# Create the link, if possible
# TODO: how to do intersphinx links here?
# NOTE: refid is *NOT* guaranteed to be in nodeByRefid
# https://github.com/svenevs/exhale/pull/103
if refid and refid in nodeByRefid:
# TODO: why are these links not working????????????????????????????????
###########flake8breaks :/ :/ :/ :/ :/ :/ :/ :/ :/ :/ :/ :/ :/ :/ :/ :/
# if please_close:
# bod_stream.write("`` ") # close prototype
# bod_stream.write("`{name} <{link}_>`_".format(
# # name=string.replace("<", ">").replace(">", "<"),
# name=string.replace("<", "").replace(">", ""),
# link=nodeByRefid[refid].link_name
# ))
if not please_close:
bod_stream.write("``")
else:
bod_stream.write(" ")
bod_stream.write("{string}`` (:ref:`{link}`)".format(
string=string,
link=nodeByRefid[refid].link_name
))
else:
if not please_close:
bod_stream.write("``")
else:
bod_stream.write(" ")
bod_stream.write("{0}``".format(string))
bod_stream.write("\n")
bod_value = bod_stream.getvalue()
bod_stream.close()
return bod_value
def findNestedNamespaces(self, lst):
'''
Recursive helper function for finding nested namespaces. If this node is a
namespace node, it is appended to ``lst``. Each node also calls each of its
child ``findNestedNamespaces`` with the same list.
:Parameters:
``lst`` (list)
The list each namespace node is to be appended to.
'''
if self.kind == "namespace":
lst.append(self)
for c in self.children:
c.findNestedNamespaces(lst)
def findNestedDirectories(self, lst):
'''
Recursive helper function for finding nested directories. If this node is a
directory node, it is appended to ``lst``. Each node also calls each of its
child ``findNestedDirectories`` with the same list.
:Parameters:
``lst`` (list)
The list each directory node is to be appended to.
'''
if self.kind == "dir":
lst.append(self)
for c in self.children:
c.findNestedDirectories(lst)
def findNestedClassLike(self, lst):
'''
Recursive helper function for finding nested classes and structs. If this node
is a class or struct, it is appended to ``lst``. Each node also calls each of
its child ``findNestedClassLike`` with the same list.
:Parameters:
``lst`` (list)
The list each class or struct node is to be appended to.
'''
if self.kind == "class" or self.kind == "struct":
lst.append(self)
for c in self.children:
c.findNestedClassLike(lst)
def findNestedEnums(self, lst):
'''
Recursive helper function for finding nested enums. If this node is a class or
struct it may have had an enum added to its child list. When this occurred, the
enum was removed from ``self.enums`` in the :class:`~exhale.graph.ExhaleRoot`
class and needs to be rediscovered by calling this method on all of its
children. If this node is an enum, it is because a parent class or struct
called this method, in which case it is added to ``lst``.
**Note**: this is used slightly differently than nested directories, namespaces,
and classes will be. Refer to
:func:`~exhale.graph.ExhaleRoot.generateNodeDocuments`.
:Parameters:
``lst`` (list)
The list each enum is to be appended to.
'''
if self.kind == "enum":
lst.append(self)
for c in self.children:
c.findNestedEnums(lst)
def findNestedUnions(self, lst):
'''
Recursive helper function for finding nested unions. If this node is a class or
struct it may have had a union added to its child list. When this occurred, the
union was removed from ``self.unions`` in the :class:`~exhale.graph.ExhaleRoot`
class and needs to be rediscovered by calling this method on all of its
children. If this node is a union, it is because a parent class or struct
called this method, in which case it is added to ``lst``.
**Note**: this is used slightly differently than nested directories, namespaces,
and classes will be. Refer to
:func:`~exhale.graph.ExhaleRoot.generateNodeDocuments`.
:Parameters:
``lst`` (list)
The list each union is to be appended to.
'''
if self.kind == "union":
lst.append(self)
for c in self.children:
c.findNestedUnions(lst)
def toConsole(self, level, fmt_spec, printChildren=True):
'''
Debugging tool for printing hierarchies / ownership to the console. Recursively
calls children ``toConsole`` if this node is not a directory or a file, and
``printChildren == True``.
.. todo:: fmt_spec docs needed. keys are ``kind`` and values are color spec
:Parameters:
``level`` (int)
The indentation level to be used, should be greater than or equal to 0.
``printChildren`` (bool)
Whether or not the ``toConsole`` method for the children found in
``self.children`` should be called with ``level+1``. Default is True,
set to False for directories and files.
'''
indent = " " * level
utils.verbose_log("{indent}- [{kind}]: {name}".format(
indent=indent,
kind=utils._use_color(self.kind, fmt_spec[self.kind], sys.stderr),
name=self.name
))
# files are children of directories, the file section will print those children
if self.kind == "dir":
for c in self.children:
c.toConsole(level + 1, fmt_spec, printChildren=False)
elif printChildren:
if self.kind == "file":
next_indent = " " * (level + 1)
utils.verbose_log("{next_indent}[[[ location=\"{loc}\" ]]]".format(
next_indent=next_indent,
loc=self.location
))
for incl in self.includes:
utils.verbose_log("{next_indent}- #include <{incl}>".format(
next_indent=next_indent,
incl=incl
))
for ref, name in self.included_by:
utils.verbose_log("{next_indent}- included by: [{name}]".format(
next_indent=next_indent,
name=name
))
for n in self.namespaces_used:
n.toConsole(level + 1, fmt_spec, printChildren=False)
for c in self.children:
c.toConsole(level + 1, fmt_spec)
elif self.kind == "class" or self.kind == "struct":
relevant_children = []
for c in self.children:
if c.kind == "class" or c.kind == "struct" or \
c.kind == "enum" or c.kind == "union":
relevant_children.append(c)
for rc in sorted(relevant_children):
rc.toConsole(level + 1, fmt_spec)
elif self.kind != "union":
for c in self.children:
c.toConsole(level + 1, fmt_spec)
def typeSort(self):
'''
Sorts ``self.children`` in place, and has each child sort its own children.
Refer to :func:`~exhale.graph.ExhaleRoot.deepSortList` for more information on
when this is necessary.
'''
self.children.sort()
for c in self.children:
c.typeSort()
def inPageHierarchy(self):
'''
Whether or not this node should be included in the page view hierarchy. Helper
method for :func:`~exhale.graph.ExhaleNode.toHierarchy`. Sets the member
variable ``self.in_page_hierarchy`` to True if appropriate.
:Return (bool):
True if this node should be included in the page view --- if it is a
node of kind ``page``. Returns False otherwise.
'''
self.in_page_hierarchy = self.kind == "page"
return self.in_page_hierarchy
def inClassHierarchy(self):
'''
Whether or not this node should be included in the class view hierarchy. Helper
method for :func:`~exhale.graph.ExhaleNode.toHierarchy`. Sets the member
variable ``self.in_class_hierarchy`` to True if appropriate.
:Return (bool):
True if this node should be included in the class view --- either it is a
node of kind ``struct``, ``class``, ``enum``, ``union``, or it is a
``namespace`` that one or more if its descendants was one of the previous
four kinds. Returns False otherwise.
'''
if self.kind == "namespace":
for c in self.children:
if c.inClassHierarchy():
return True
return False
else:
# flag that this node is already in the class view so we can find the
# missing top level nodes at the end
self.in_class_hierarchy = True
# Skip children whose names were requested to be explicitly ignored.
for exclude in configs._compiled_listing_exclude:
if exclude.match(self.name):
return False
return self.kind in {"struct", "class", "enum", "union"}
def inFileHierarchy(self):
'''
Whether or not this node should be included in the file view hierarchy. Helper
method for :func:`~exhale.graph.ExhaleNode.toHierarchy`. Sets the member
variable ``self.in_file_hierarchy`` to True if appropriate.
:Return (bool):
True if this node should be included in the file view --- either it is a
node of kind ``file``, or it is a ``dir`` that one or more if its
descendants was a ``file``. Returns False otherwise.
'''
if self.kind == "file":
# flag that this file is already in the directory view so that potential
# missing files can be found later.
self.in_file_hierarchy = True
return True
elif self.kind == "dir":
for c in self.children:
if c.inFileHierarchy():
return True
return False
def inHierarchy(self, hierarchyType):
if hierarchyType == "page":
return self.inPageHierarchy()
elif hierarchyType == "class":
return self.inClassHierarchy()
elif hierarchyType == "file":
return self.inFileHierarchy()
else:
raise RuntimeError("'{}' is not a valid hierarchy type".format(hierarchyType))
def hierarchySortedDirectDescendants(self, hierarchyType):
if hierarchyType == "page":
if self.kind != "page":
raise RuntimeError(
"Page hierarchies do not apply to '{}' nodes".format(self.kind)
)
return sorted(self.children)
elif hierarchyType == "class":
# search for nested children to display as sub-items in the tree view
if self.kind == "class" or self.kind == "struct":
# first find all of the relevant children
nested_class_like = []
nested_enums = []
nested_unions = []
# important: only scan self.children, do not use recursive findNested* methods
for c in self.children:
if c.kind == "struct" or c.kind == "class":
nested_class_like.append(c)
elif c.kind == "enum":
nested_enums.append(c)
elif c.kind == "union":
nested_unions.append(c)
# sort the lists we just found
nested_class_like.sort()
nested_enums.sort()
nested_unions.sort()
# return a flattened listing with everything in the order it should be
return [
child for child in itertools.chain(nested_class_like, nested_enums, nested_unions)
]
# namespaces include nested namespaces, and any top-level class_like, enums,
# and unions. include nested namespaces first
elif self.kind == "namespace":
# pre-process and find everything that is relevant
nested_nspaces = []
nested_kids = []
for c in self.children:
if c.inHierarchy(hierarchyType):
if c.kind == "namespace":
nested_nspaces.append(c)
else:
nested_kids.append(c)
# sort the lists
nested_nspaces.sort()
nested_kids.sort()
# return a flattened listing with everything in the order it should be
return [
child for child in itertools.chain(nested_nspaces, nested_kids)
]
else:
# everything else is a terminal node
return []
elif hierarchyType == "file":
if self.kind == "dir":
# find the nested children of interest
nested_dirs = []
nested_kids = []
for c in self.children:
if c.inHierarchy(hierarchyType):
if c.kind == "dir":
nested_dirs.append(c)
elif c.kind == "file":
nested_kids.append(c)
# sort the lists
nested_dirs.sort()
nested_kids.sort()
# return a flattened listing with everything in the order it should be
return [
child for child in itertools.chain(nested_dirs, nested_kids)
]
else:
# files are terminal nodes in this hierarchy view
return []
else:
raise RuntimeError("{} is not a valid hierarchy type".format(hierarchyType))
def toHierarchy(self, hierarchyType, level, stream, lastChild=False):
'''
**Parameters**
``hierarchyType`` (str)
``"page"`` if generating the Page Hierarchy,
``"class"`` if generating the Class Hierarchy,
``"file"`` if generating the File Hierarchy.
``level`` (int)
Recursion level used to determine indentation.
``stream`` (StringIO)
The stream to write the contents to.
``lastChild`` (bool)
When :data:`~exhale.configs.createTreeView` is ``True`` and
:data:`~exhale.configs.treeViewIsBootstrap` is ``False``, the generated
HTML ``li`` elements need to add a ``class="lastChild"`` to use the
appropriate styling.
.. todo:: add thorough documentation of this
'''
# NOTE: indexpage needs to be treated specially, you need to include the
# children at the *same* level, and not actually include indexpage.
if hierarchyType == "page" and self.refid == "indexpage":
nested_children = self.hierarchySortedDirectDescendants(hierarchyType)
last_child_index = len(nested_children) - 1
child_idx = 0
for child in nested_children:
child.toHierarchy(
hierarchyType, level, stream, child_idx == last_child_index)
child_idx += 1
return
if self.inHierarchy(hierarchyType):
# For the Tree Views, we need to know if there are nested children before
# writing anything. If there are, we need to open a new list
nested_children = self.hierarchySortedDirectDescendants(hierarchyType)
############################################################################
# Write out this node. #
############################################################################
# Easy case: just write another bullet point
if not configs.createTreeView:
stream.write("{indent}- :ref:`{link}`\n".format(
indent=' ' * level,
link=self.link_name
))
# Otherwise, we're generating some raw HTML and/or JavaScript depending on
# whether we are using bootstrap or not
else:
# Declare the relevant links needed for the Tree Views
indent = " " * (level * 2)
next_indent = " {0}".format(indent)
# turn double underscores into underscores, then underscores into hyphens
html_link = self.link_name.replace("__", "_").replace("_", "-")
href = "{file}.html#{anchor}".format(
file=self.file_name.rsplit(".rst", 1)[0],
anchor=html_link
)
if self.kind != "page":
# should always have at least two parts (templates will have more)
title_as_link_parts = self.title.split(" ")
if self.template_params:
# E.g. 'Template Class Foo'
q_start = 0
q_end = 2
else:
# E.g. 'Class Foo'
q_start = 0
q_end = 1
# the qualifier will not be part of the hyperlink (for clarity of
# navigation), the link_title will be
qualifier = " ".join(title_as_link_parts[q_start:q_end])
link_title = " ".join(title_as_link_parts[q_end:])
else:
# E.g. 'Foo'
qualifier = ""
link_title = self.title
link_title = link_title.replace("&", "&").replace("<", "<").replace(">", ">")
# the actual text / link inside of the list item
li_text = '{qualifier} <a href="{href}">{link_title}</a>'.format(
qualifier=qualifier,
href=href,
link_title=link_title
)
if configs.treeViewIsBootstrap:
text = "text: \"<span class=\\\"{span_cls}\\\">{qualifier}</span> {link_title}\"".format(
span_cls=configs.treeViewBootstrapTextSpanClass,
qualifier=qualifier,
link_title=link_title
)
link = "href: \"{href}\"".format(href=href)
# write some json data, something like
# {
# text: "<span class=\\\"text-muted\\\"> some text",
# href: "link to actual item",
# selectable: false,
stream.write("{indent}{{\n{next_indent}{text},\n".format(
indent=indent,
next_indent=next_indent,
text=text
))
stream.write("{next_indent}{link},\n{next_indent}selectable: false,\n".format(
next_indent=next_indent,
link=link
))
# if requested, add the badge indicating how many children there are
# only add this if there are children
if configs.treeViewBootstrapUseBadgeTags and nested_children:
stream.write("{next_indent}tags: ['{num_children}'],\n".format(
next_indent=next_indent,
num_children=len(nested_children)
))
if nested_children:
# If there are children then `nodes: [ ... ]` will be next
stream.write("\n{next_indent}nodes: [\n".format(next_indent=next_indent))
else:
# Otherwise, this element is ending. JavaScript doesn't care
# about trailing commas :)
stream.write("{indent}}},\n".format(indent=indent))
else:
if lastChild:
opening_li = '<li class="lastChild">'
else:
opening_li = "<li>"
if nested_children:
# write this list element and begin the next list
# writes something like
# <li>
# some text with an href
# <ul>
#
# the <ul> started here gets closed below
stream.write("{indent}{li}\n{next_indent}{li_text}\n{next_indent}<ul>\n".format(
indent=indent,
li=opening_li,
next_indent=next_indent,
li_text=li_text
))
else:
# write this list element and end it now (since no children)
# writes something like
# <li>
# some text with an href
# </li>
stream.write("{indent}{li}{li_text}</li>\n".format(
indent=indent,
li=opening_li,
li_text=li_text
))
############################################################################
# Write out all of the children (if there are any). #
############################################################################
last_child_index = len(nested_children) - 1
child_idx = 0
for child in nested_children:
child.toHierarchy(hierarchyType, level + 1, stream, child_idx == last_child_index)
child_idx += 1
############################################################################
# If there were children, close the lists we started above. #
############################################################################
if configs.createTreeView and nested_children:
if configs.treeViewIsBootstrap:
# close the `nodes: [ ... ]` and final } for element
# the final comma IS necessary, and extra commas don't matter in javascript
stream.write("{next_indent}]\n{indent}}},\n".format(
next_indent=next_indent,
indent=indent
))
else:
stream.write("{next_indent}</ul>\n{indent}</li>\n".format(
next_indent=next_indent,
indent=indent
))
class ExhaleRoot(object):
'''
The full representation of the hierarchy graphs. In addition to containing specific
lists of ExhaleNodes of interest, the ExhaleRoot class is responsible for comparing
the parsed breathe hierarchy and rebuilding lost relationships using the Doxygen
xml files. Once the graph parsing has finished, the ExhaleRoot generates all of the
relevant reStructuredText documents and links them together.
The ExhaleRoot class is not designed for reuse at this time. If you want to
generate a new hierarchy with a different directory or something, changing all of
the right fields may be difficult and / or unsuccessful. Refer to the
:func:`~exhale.deploy.explode` function for intended usage.
.. danger::
Zero checks are in place to enforce this usage, and if you are modifying the
execution of this class and things are not working make sure you follow the
ordering of those methods.
.. todo::
many attributes currently stored do not need to be, refactor in future release
to just use the ``configs`` module.
**Attributes**
``root_directory`` (str)
The value of the parameter ``rootDirectory``.
``root_file_name`` (str)
The value of the parameter ``rootFileName``.
``full_root_file_path`` (str)
The full file path of the root file (``"root_directory/root_file_name"``).
``class_hierarchy_file`` (str)
The full file path the class view hierarchy will be written to. This is
incorporated into ``root_file_name`` using an ``.. include:`` directive.
``file_hierarchy_file`` (str)
The full file path the file view hierarchy will be written to. This is
incorporated into ``root_file_name`` using an ``.. include:`` directive.