3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2014, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 # Lesser General Public License for more details.
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
36 """GROMACS-specific representation for source tree and documentation.
38 This module provides classes that construct a GROMACS-specific representation
39 of the source tree and associate the Doxygen XML output with it. It constructs
40 an initial representation by walking the source tree in the file system, and
41 then associates information from the Doxygen XML output into this.
42 It also adds some additional knowledge from how the GROMACS source tree is
43 organized to construct a representation that is easy to process and check as
44 the top-level scripts expect.
46 The object model is rooted at a GromacsTree object. Currently, it constructs a
47 representation of the source tree from the file system, but is otherwise mostly
48 a thin wrapper around the Doxygen XML tree. It already adds some relations and
49 rules that come from GROMACS-specific knowledge. In the future, more such
50 customizations will be added.
59 import doxygenxml as xml
61 # We import DocType directly so that it is exposed from this module as well.
62 from doxygenxml import DocType
64 def _get_api_type_for_compound(grouplist):
65 """Helper function to deduce API type from Doxygen group membership."""
66 result = DocType.internal
67 for group in grouplist:
68 if isinstance(group, xml.Group):
69 if group.get_name() == 'group_publicapi':
70 result = DocType.public
71 elif group.get_name() == 'group_libraryapi':
72 result = DocType.library
73 # TODO: Check for multiple group membership
76 class IncludedFile(object):
78 """Information about an #include directive in a file."""
80 def __init__(self, including_file, lineno, included_file, included_path, is_relative, is_system, line):
81 self._including_file = including_file
82 self._line_number = lineno
83 self._included_file = included_file
84 self._included_path = included_path
85 #self._used_include_path = used_include_path
86 self._is_relative = is_relative
87 self._is_system = is_system
92 return '<{0}>'.format(self._included_path)
94 return '"{0}"'.format(self._included_path)
97 return self._is_system
99 def is_relative(self):
100 return self._is_relative
102 def get_included_path(self):
103 return self._included_path
105 def get_including_file(self):
106 return self._including_file
109 return self._included_file
111 def get_line_number(self):
112 return self._line_number
114 def get_full_line(self):
115 """Return the full source line on which this include appears.
117 Trailing newline is included."""
120 def get_reporter_location(self):
121 return reporter.Location(self._including_file.get_abspath(), self._line_number)
123 class IncludeBlock(object):
125 """Block of consequent #include directives in a file."""
127 def __init__(self, first_included_file):
128 self._first_line = first_included_file.get_line_number()
129 self._last_line = self._first_line
131 self.add_file(first_included_file)
133 def add_file(self, included_file):
134 self._files.append(included_file)
135 self._last_line = included_file.get_line_number()
137 def get_includes(self):
140 def get_first_line(self):
141 return self._first_line
143 def get_last_line(self):
144 return self._last_line
148 """Source/header file in the GROMACS tree."""
150 def __init__(self, abspath, relpath, directory):
151 """Initialize a file representation with basic information."""
152 self._abspath = abspath
153 self._relpath = relpath
154 self._dir = directory
156 self._installed = False
157 extension = os.path.splitext(abspath)[1]
158 self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
159 self._apitype = DocType.none
160 self._modules = set()
162 self._include_blocks = []
163 self._main_header = None
166 directory.add_file(self)
168 def set_doc_xml(self, rawdoc, sourcetree):
169 """Assiociate Doxygen documentation entity with the file."""
170 assert self._rawdoc is None
171 assert rawdoc.is_source_file() == self._sourcefile
172 self._rawdoc = rawdoc
173 if self._rawdoc.is_documented():
174 grouplist = self._rawdoc.get_groups()
175 self._apitype = _get_api_type_for_compound(grouplist)
176 for group in grouplist:
177 module = sourcetree.get_object(group)
179 self._modules.add(module)
181 def set_installed(self):
182 """Mark the file installed."""
183 self._installed = True
185 def set_git_filter_attribute(self, filtername):
186 """Set the git filter attribute associated with the file."""
187 self._filter = filtername
189 def set_main_header(self, included_file):
190 """Set the main header file for a source file."""
191 assert self.is_source_file()
192 self._main_header = included_file
194 def _process_include(self, lineno, is_system, includedpath, line, sourcetree):
195 """Process #include directive during scan()."""
198 fileobj = sourcetree.find_include_file(includedpath)
200 fullpath = os.path.join(self._dir.get_abspath(), includedpath)
201 fullpath = os.path.abspath(fullpath)
202 if os.path.exists(fullpath):
204 fileobj = sourcetree.get_file(fullpath)
206 fileobj = sourcetree.find_include_file(includedpath)
207 included_file = IncludedFile(self, lineno, fileobj, includedpath,
208 is_relative, is_system, line)
209 self._includes.append(included_file)
212 def scan_contents(self, sourcetree, keep_contents):
213 """Scan the file contents and initialize information based on it."""
214 # TODO: Consider a more robust regex.
215 include_re = r'^\s*#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
217 # TODO: Consider reading directly into this list, and iterate that.
219 with open(self._abspath, 'r') as scanfile:
220 for lineno, line in enumerate(scanfile, 1):
222 match = re.match(include_re, line)
224 is_system = (match.group('quote') == '<')
225 includedpath = match.group('path')
226 included_file = self._process_include(lineno, is_system,
227 includedpath, line, sourcetree)
228 if current_block is None:
229 current_block = IncludeBlock(included_file)
230 self._include_blocks.append(current_block)
232 current_block.add_file(included_file)
233 elif line and not line.isspace():
238 def get_reporter_location(self):
239 return reporter.Location(self._abspath, None)
241 def is_installed(self):
242 return self._installed
244 def is_external(self):
245 return self._dir.is_external()
247 def is_source_file(self):
248 return self._sourcefile
250 def is_test_file(self):
251 return self._dir.is_test_directory()
253 def should_includes_be_sorted(self):
254 """Return whether the include directives in the file should be sorted."""
255 return self._filter in ('includesort', 'uncrustify')
257 def is_documented(self):
258 return self._rawdoc and self._rawdoc.is_documented()
260 def has_brief_description(self):
261 return self._rawdoc and self._rawdoc.has_brief_description()
263 def get_abspath(self):
266 def get_relpath(self):
270 return os.path.basename(self._abspath)
272 def get_directory(self):
275 def get_doc_type(self):
278 return self._rawdoc.get_visibility()
280 def get_api_type(self):
283 def api_type_is_reliable(self):
284 if self._apitype in (DocType.internal, DocType.library):
286 module = self.get_module()
287 return module and module.is_documented()
290 if self.api_type_is_reliable():
291 return self.get_api_type() == DocType.public
292 return self.get_api_type() == DocType.public or self.is_installed()
294 def is_module_internal(self):
295 if self.is_source_file():
297 return not self.is_installed() and self.get_api_type() <= DocType.internal
299 def get_expected_module(self):
300 return self._dir.get_module()
302 def get_doc_modules(self):
305 def get_module(self):
306 module = self.get_expected_module()
307 if not module and len(self._modules) == 1:
308 module = list(self._modules)[0]
311 def get_includes(self):
312 return self._includes
314 def get_include_blocks(self):
315 return self._include_blocks
317 def get_main_header(self):
318 return self._main_header
320 def get_contents(self):
323 class GeneratedFile(File):
324 def __init__(self, abspath, relpath, directory):
325 File.__init__(self, abspath, relpath, directory)
326 self._generator_source_file = None
328 def set_generator_source(self, sourcefile):
329 self._generator_source_file = sourcefile
331 def get_reporter_location(self):
332 if self._generator_source_file:
333 return self._generator_source_file.get_reporter_location()
334 return File.get_reporter_location(self)
336 class GeneratorSourceFile(File):
339 class Directory(object):
341 """(Sub)directory in the GROMACS tree."""
343 def __init__(self, abspath, relpath, parent):
344 """Initialize a file representation with basic information."""
345 self._abspath = abspath
346 self._relpath = relpath
347 self._name = os.path.basename(abspath)
348 self._parent = parent
351 self._is_test_dir = False
352 if parent and parent.is_test_directory() or \
353 self._name in ('tests', 'legacytests'):
354 self._is_test_dir = True
355 self._is_external = False
356 if parent and parent.is_external() or self._name == 'external':
357 self._is_external = True
358 self._subdirs = set()
360 parent._subdirs.add(self)
362 self._has_installed_files = None
364 def set_doc_xml(self, rawdoc, sourcetree):
365 """Assiociate Doxygen documentation entity with the directory."""
366 assert self._rawdoc is None
367 assert self._abspath == rawdoc.get_path().rstrip('/')
368 self._rawdoc = rawdoc
370 def set_module(self, module):
371 assert self._module is None
372 self._module = module
374 def add_file(self, fileobj):
375 self._files.add(fileobj)
380 def get_reporter_location(self):
381 return reporter.Location(self._abspath, None)
383 def get_abspath(self):
386 def get_relpath(self):
389 def is_test_directory(self):
390 return self._is_test_dir
392 def is_external(self):
393 return self._is_external
395 def has_installed_files(self):
396 if self._has_installed_files is None:
397 self._has_installed_files = False
398 for subdir in self._subdirs:
399 if subdir.has_installed_files():
400 self._has_installed_files = True
402 for fileobj in self._files:
403 if fileobj.is_installed():
404 self._has_installed_files = True
406 return self._has_installed_files
408 def get_module(self):
412 return self._parent.get_module()
415 def get_subdirectories(self):
419 for subdir in self._subdirs:
420 for fileobj in subdir.get_files():
422 for fileobj in self._files:
425 def contains(self, fileobj):
426 """Check whether file is within the directory or its subdirectories."""
427 dirobj = fileobj.get_directory()
431 dirobj = dirobj._parent
434 class ModuleDependency(object):
436 """Dependency between modules."""
438 def __init__(self, othermodule):
439 """Initialize empty dependency object with given module as dependency."""
440 self._othermodule = othermodule
441 self._includedfiles = []
442 self._cyclesuppression = None
444 def add_included_file(self, includedfile):
445 """Add IncludedFile that is part of this dependency."""
446 assert includedfile.get_file().get_module() == self._othermodule
447 self._includedfiles.append(includedfile)
449 def set_cycle_suppression(self):
450 """Set suppression on cycles containing this dependency."""
451 self._cyclesuppression = True
453 def is_cycle_suppressed(self):
454 """Return whether cycles containing this dependency are suppressed."""
455 return self._cyclesuppression is not None
457 def get_other_module(self):
458 """Get module that this dependency is to."""
459 return self._othermodule
461 def get_included_files(self):
462 """Get IncludedFile objects for the individual include dependencies."""
463 return self._includedfiles
465 class Module(object):
467 """Code module in the GROMACS source tree.
469 Modules are specific subdirectories that host a more or less coherent
470 set of routines. Simplified, every subdirectory under src/gromacs/ is
471 a different module. This object provides that abstraction and also links
472 the subdirectory to the module documentation (documented as a group in
473 Doxygen) if that exists.
476 def __init__(self, name, rootdir):
479 self._rootdir = rootdir
481 self._dependencies = dict()
483 def set_doc_xml(self, rawdoc, sourcetree):
484 """Assiociate Doxygen documentation entity with the module."""
485 assert self._rawdoc is None
486 self._rawdoc = rawdoc
487 if self._rawdoc.is_documented():
488 groups = list(self._rawdoc.get_groups())
490 groupname = groups[0].get_name()
491 if groupname.startswith('group_'):
492 self._group = groupname[6:]
494 def add_dependency(self, othermodule, includedfile):
495 """Add #include dependency from a file in this module."""
496 assert includedfile.get_file().get_module() == othermodule
497 if othermodule not in self._dependencies:
498 self._dependencies[othermodule] = ModuleDependency(othermodule)
499 self._dependencies[othermodule].add_included_file(includedfile)
501 def is_documented(self):
502 return self._rawdoc is not None
507 def get_root_dir(self):
511 # TODO: Include public API convenience headers?
512 return self._rootdir.get_files()
517 def get_dependencies(self):
518 return self._dependencies.itervalues()
520 class Namespace(object):
522 """Namespace in the GROMACS source code."""
524 def __init__(self, rawdoc):
525 self._rawdoc = rawdoc
527 def is_anonymous(self):
528 return self._rawdoc.is_anonymous()
532 """Class/struct/union in the GROMACS source code."""
534 def __init__(self, rawdoc, files):
535 self._rawdoc = rawdoc
536 self._files = set(files)
539 return self._rawdoc.get_name()
541 def get_reporter_location(self):
542 return self._rawdoc.get_reporter_location()
547 def is_documented(self):
548 return self._rawdoc.is_documented()
550 def has_brief_description(self):
551 return self._rawdoc.has_brief_description()
553 def get_doc_type(self):
554 """Return documentation type (visibility) for the class.
556 In addition to the actual code, this encodes GROMACS-specific logic
557 of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
558 Local classes never appear outside the full documentation, no matter
559 what is their visibility.
561 if not self.is_documented():
563 if self._rawdoc.is_local():
564 return DocType.internal
565 return self._rawdoc.get_visibility()
567 def get_file_doc_type(self):
568 return max([fileobj.get_doc_type() for fileobj in self._files])
570 def is_in_installed_file(self):
571 return any([fileobj.is_installed() for fileobj in self._files])
573 class Member(object):
575 """Member (in Doxygen terminology) in the GROMACS source tree.
577 Currently, modeling is limited to the minimal set of properties that the
581 def __init__(self, rawdoc, namespace):
582 self._rawdoc = rawdoc
583 self._namespace = namespace
586 return self._rawdoc.get_name()
588 def get_reporter_location(self):
589 return self._rawdoc.get_reporter_location()
591 def is_documented(self):
592 return self._rawdoc.is_documented()
594 def has_brief_description(self):
595 return self._rawdoc.has_brief_description()
597 def has_inbody_description(self):
598 return self._rawdoc.has_inbody_description()
600 def is_visible(self):
601 """Return whether the member is visible in Doxygen documentation.
603 Doxygen ignores members whose parent compounds are not documented.
604 However, when EXTRACT_ANON_NPACES=ON (which is set for our full
605 documentation), members of anonymous namespaces are extracted even if
606 the namespace is the only parent and is not documented.
608 if self._namespace and self._namespace.is_anonymous():
610 return self._rawdoc.get_inherited_visibility() != DocType.none
613 class GromacsTree(object):
615 """Root object for navigating the GROMACS source tree.
617 On initialization, the list of files and directories is initialized by
618 walking the source tree, and modules are created for top-level
619 subdirectories. At this point, only information that is accessible from
620 file names and paths only is available.
622 load_git_attributes() can be called to load attribute information from
623 .gitattributes for all the files.
625 load_installed_file_list() can be called to load the list of installed
626 files from the build tree (generated by the find-installed-headers target).
628 scan_files() can be called to read all the files and initialize #include
629 dependencies between the files based on the information. This is done like
630 this instead of relying on Doxygen-extracted include files to make the
631 dependency graph independent from preprocessor macro definitions
632 (Doxygen only sees those #includes that the preprocessor sees, which
633 depends on what #defines it has seen).
635 load_xml() can be called to load information from Doxygen XML data in
636 the build tree (the Doxygen XML data must have been built separately).
639 def __init__(self, source_root, build_root, reporter):
640 """Initialize the tree object by walking the source tree."""
641 self._source_root = os.path.abspath(source_root)
642 self._build_root = os.path.abspath(build_root)
643 self._reporter = reporter
645 self._docmap = dict()
648 self._modules = dict()
649 self._classes = set()
650 self._namespaces = set()
651 self._members = set()
652 self._walk_dir(os.path.join(self._source_root, 'src'))
653 for fileobj in self.get_files():
654 if fileobj and fileobj.is_source_file() and not fileobj.is_external():
655 (basedir, name) = os.path.split(fileobj.get_abspath())
656 (basename, ext) = os.path.splitext(name)
657 header = self.get_file(os.path.join(basedir, basename + '.h'))
658 if not header and ext == '.cu':
659 header = self.get_file(os.path.join(basedir, basename + '.cuh'))
660 if not header and fileobj.is_test_file():
661 basedir = os.path.dirname(basedir)
662 header = self.get_file(os.path.join(basedir, basename + '.h'))
664 # Somewhat of a hack; currently, the tests for
665 # analysisdata/modules/ and trajectoryanalysis/modules/
666 # is at the top-level tests directory.
667 # TODO: It could be clearer to split the tests so that
668 # there would be a separate modules/tests/.
669 header = self.get_file(os.path.join(basedir, 'modules', basename + '.h'))
670 if not header and basename.endswith('_tests'):
671 header = self.get_file(os.path.join(basedir, basename[:-6] + '.h'))
672 if not header and fileobj.get_relpath().startswith('src/gromacs'):
673 header = self._files.get(os.path.join('src/gromacs/legacyheaders', basename + '.h'))
675 fileobj.set_main_header(header)
676 rootdir = self._get_dir(os.path.join('src', 'gromacs'))
677 for subdir in rootdir.get_subdirectories():
678 self._create_module(subdir)
679 rootdir = self._get_dir(os.path.join('src', 'testutils'))
680 self._create_module(rootdir)
682 def _get_rel_path(self, path):
683 assert os.path.isabs(path)
684 if path.startswith(self._build_root):
685 return os.path.relpath(path, self._build_root)
686 if path.startswith(self._source_root):
687 return os.path.relpath(path, self._source_root)
688 raise ValueError("path not under build nor source tree: {0}".format(path))
690 def _walk_dir(self, rootpath):
691 """Construct representation of the source tree by walking the file system."""
692 assert os.path.isabs(rootpath)
693 assert rootpath not in self._dirs
694 relpath = self._get_rel_path(rootpath)
695 self._dirs[relpath] = Directory(rootpath, relpath, None)
696 for dirpath, dirnames, filenames in os.walk(rootpath):
697 if 'contrib' in dirnames:
698 dirnames.remove('contrib')
699 if 'refdata' in dirnames:
700 dirnames.remove('refdata')
701 currentdir = self._dirs[self._get_rel_path(dirpath)]
702 # Loop through a copy so that we can modify dirnames.
703 for dirname in list(dirnames):
704 fullpath = os.path.join(dirpath, dirname)
705 if fullpath == self._build_root:
706 dirnames.remove(dirname)
708 relpath = self._get_rel_path(fullpath)
709 self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
710 extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
711 for filename in filenames:
712 basename, extension = os.path.splitext(filename)
713 if extension in extensions:
714 fullpath = os.path.join(dirpath, filename)
715 relpath = self._get_rel_path(fullpath)
716 self._files[relpath] = File(fullpath, relpath, currentdir)
717 elif extension == '.cmakein':
718 extension = os.path.splitext(basename)[1]
719 if extension in extensions:
720 fullpath = os.path.join(dirpath, filename)
721 relpath = self._get_rel_path(fullpath)
722 sourcefile = GeneratorSourceFile(fullpath, relpath, currentdir)
723 self._files[relpath] = sourcefile
724 fullpath = os.path.join(dirpath, basename)
725 relpath = self._get_rel_path(fullpath)
726 fullpath = os.path.join(self._build_root, relpath)
727 generatedfile = GeneratedFile(fullpath, relpath, currentdir)
728 self._files[relpath] = generatedfile
729 generatedfile.set_generator_source(sourcefile)
730 elif extension in ('.l', '.y', '.pre'):
731 fullpath = os.path.join(dirpath, filename)
732 relpath = self._get_rel_path(fullpath)
733 self._files[relpath] = GeneratorSourceFile(fullpath, relpath, currentdir)
735 def _create_module(self, rootdir):
736 """Create module for a subdirectory."""
737 name = 'module_' + rootdir.get_name()
738 moduleobj = Module(name, rootdir)
739 rootdir.set_module(moduleobj)
740 self._modules[name] = moduleobj
742 def scan_files(self, only_files=None, keep_contents=False):
743 """Read source files to initialize #include dependencies."""
745 filelist = only_files
747 filelist = self._files.itervalues()
748 for fileobj in filelist:
749 if not fileobj.is_external():
750 fileobj.scan_contents(self, keep_contents)
751 module = fileobj.get_module()
753 for includedfile in fileobj.get_includes():
754 otherfile = includedfile.get_file()
756 othermodule = otherfile.get_module()
757 if othermodule and othermodule != module:
758 module.add_dependency(othermodule, includedfile)
760 def load_xml(self, only_files=None):
761 """Load Doxygen XML information.
763 If only_files is True, XML data is not loaded for code constructs, but
764 only for files, directories, and their potential parents.
766 xmldir = os.path.join(self._build_root, 'docs', 'html', 'doxygen', 'xml')
767 self._docset = xml.DocumentationSet(xmldir, self._reporter)
769 if isinstance(only_files, collections.Iterable):
770 filelist = [x.get_abspath() for x in only_files]
771 self._docset.load_file_details(filelist)
773 self._docset.load_file_details()
775 self._docset.load_details()
776 self._docset.merge_duplicates()
781 self._load_namespaces()
785 def _load_dirs(self):
786 """Load Doxygen XML directory information."""
787 rootdirs = self._docset.get_compounds(xml.Directory,
788 lambda x: x.get_parent() is None)
789 for dirdoc in rootdirs:
790 self._load_dir(dirdoc, None)
792 def _load_dir(self, dirdoc, parent):
793 """Load Doxygen XML directory information for a single directory."""
794 path = dirdoc.get_path().rstrip('/')
795 if not os.path.isabs(path):
796 self._reporter.xml_assert(dirdoc.get_xml_path(),
797 "expected absolute path in Doxygen-produced XML file")
799 relpath = self._get_rel_path(path)
800 dirobj = self._dirs.get(relpath)
802 dirobj = Directory(path, relpath, parent)
803 self._dirs[relpath] = dirobj
804 dirobj.set_doc_xml(dirdoc, self)
805 self._docmap[dirdoc] = dirobj
806 for subdirdoc in dirdoc.get_subdirectories():
807 self._load_dir(subdirdoc, dirobj)
809 def _load_modules(self):
810 """Load Doxygen XML module (group) information."""
811 moduledocs = self._docset.get_compounds(xml.Group,
812 lambda x: x.get_name().startswith('module_'))
813 for moduledoc in moduledocs:
814 moduleobj = self._modules.get(moduledoc.get_name())
816 self._reporter.input_error(
817 "no matching directory for module: {0}".format(moduledoc))
819 moduleobj.set_doc_xml(moduledoc, self)
820 self._docmap[moduledoc] = moduleobj
822 def _load_files(self):
823 """Load Doxygen XML file information."""
824 for filedoc in self._docset.get_files():
825 path = filedoc.get_path()
827 # In case of only partially loaded file information,
828 # the path information is not set for unloaded files.
830 if not os.path.isabs(path):
831 self._reporter.xml_assert(filedoc.get_xml_path(),
832 "expected absolute path in Doxygen-produced XML file")
834 extension = os.path.splitext(path)[1]
835 # We don't care about Markdown files that only produce pages
836 # (and fail the directory check below).
837 if extension == '.md':
839 dirdoc = filedoc.get_directory()
841 self._reporter.xml_assert(filedoc.get_xml_path(),
842 "file is not in any directory in Doxygen")
844 relpath = self._get_rel_path(path)
845 fileobj = self._files.get(relpath)
847 fileobj = File(path, relpath, self._docmap[dirdoc])
848 self._files[relpath] = fileobj
849 fileobj.set_doc_xml(filedoc, self)
850 self._docmap[filedoc] = fileobj
852 def _load_namespaces(self):
853 """Load Doxygen XML namespace information."""
854 nsdocs = self._docset.get_namespaces()
856 nsobj = Namespace(nsdoc)
857 self._docmap[nsdoc] = nsobj
858 self._namespaces.add(nsobj)
860 def _load_classes(self):
861 """Load Doxygen XML class information."""
862 classdocs = self._docset.get_classes()
863 for classdoc in classdocs:
864 files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
865 classobj = Class(classdoc, files)
866 self._docmap[classdoc] = classobj
867 self._classes.add(classobj)
869 def _load_members(self):
870 """Load Doxygen XML member information."""
871 memberdocs = self._docset.get_members()
872 for memberdoc in memberdocs:
873 nsdoc = memberdoc.get_namespace()
874 nsobj = self.get_object(nsdoc)
875 memberobj = Member(memberdoc, nsobj)
876 self._docmap[memberdoc] = memberobj
877 self._members.add(memberobj)
879 def _get_dir(self, relpath):
880 """Get directory object for a path relative to source tree root."""
881 return self._dirs.get(relpath)
883 def get_file(self, path):
884 """Get file object for a path relative to source tree root."""
885 return self._files.get(self._get_rel_path(path))
887 def find_include_file(self, includedpath):
888 """Find a file object corresponding to an include path."""
889 for testdir in ('src', 'src/external/thread_mpi/include',
890 'src/external/tng_io/include'):
891 testpath = os.path.join(testdir, includedpath)
892 if testpath in self._files:
893 return self._files[testpath]
895 def load_git_attributes(self):
896 """Load git attribute information for files."""
897 args = ['git', 'check-attr', '--stdin', 'filter']
898 git_check_attr = subprocess.Popen(args, stdin=subprocess.PIPE,
899 stdout=subprocess.PIPE, cwd=self._source_root)
900 filelist = '\n'.join(map(File.get_relpath, self._files.itervalues()))
901 filters = git_check_attr.communicate(filelist)[0]
902 for fileinfo in filters.splitlines():
903 path, dummy, value = fileinfo.split(': ')
904 fileobj = self._files.get(path)
905 assert fileobj is not None
906 fileobj.set_git_filter_attribute(value)
908 def load_installed_file_list(self):
909 """Load list of installed files from the build tree."""
910 listpath = os.path.join(self._build_root, 'docs', 'doxygen', 'installed-headers.txt')
911 with open(listpath, 'r') as installedfp:
912 for line in installedfp:
914 if not os.path.isabs(path):
915 self._reporter.input_error(
916 "installed file not specified with absolute path: {0}"
919 relpath = self._get_rel_path(path)
920 if relpath not in self._files:
921 self._reporter.input_error(
922 "installed file not in source tree: {0}".format(path))
924 self._files[relpath].set_installed()
926 def load_cycle_suppression_list(self, filename):
927 """Load a list of edges to suppress in cycles.
929 These edges between modules, if present, will be marked in the
930 corresponding ModuleDependency objects.
932 with open(filename, 'r') as fp:
935 if not line or line.startswith('#'):
937 modulenames = ['module_' + x.strip() for x in line.split('->')]
938 if len(modulenames) != 2:
939 self._reporter.input_error(
940 "invalid cycle suppression line: {0}".format(line))
942 firstmodule = self._modules.get(modulenames[0])
943 secondmodule = self._modules.get(modulenames[1])
944 if not firstmodule or not secondmodule:
945 self._reporter.input_error(
946 "unknown modules mentioned on cycle suppression line: {0}".format(line))
948 for dep in firstmodule.get_dependencies():
949 if dep.get_other_module() == secondmodule:
950 # TODO: Check that each suppression is actually part of
952 dep.set_cycle_suppression()
954 def get_object(self, docobj):
955 """Get tree object for a Doxygen XML object."""
958 return self._docmap.get(docobj)
961 """Get iterable for all files in the source tree."""
962 return self._files.itervalues()
964 def get_modules(self):
965 """Get iterable for all modules in the source tree."""
966 return self._modules.itervalues()
968 def get_classes(self):
969 """Get iterable for all classes in the source tree."""
972 def get_members(self):
973 """Get iterable for all members (in Doxygen terms) in the source tree."""