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.
56 import doxygenxml as xml
58 # We import DocType directly so that it is exposed from this module as well.
59 from doxygenxml import DocType
61 def _get_api_type_for_compound(grouplist):
62 """Helper function to deduce API type from Doxygen group membership."""
63 result = DocType.internal
64 for group in grouplist:
65 if isinstance(group, xml.Group):
66 if group.get_name() == 'group_publicapi':
67 result = DocType.public
68 elif group.get_name() == 'group_libraryapi':
69 result = DocType.library
70 # TODO: Check for multiple group membership
75 """Source/header file in the GROMACS tree."""
77 def __init__(self, path, directory):
78 """Initialize a file representation with basic information."""
82 self._installed = False
83 extension = os.path.splitext(path)[1]
84 self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
85 self._apitype = DocType.none
88 def set_doc_xml(self, rawdoc, sourcetree):
89 """Assiociate Doxygen documentation entity with the file."""
90 assert self._rawdoc is None
91 assert rawdoc.is_source_file() == self._sourcefile
93 if self._rawdoc.is_documented():
94 grouplist = self._rawdoc.get_groups()
95 self._apitype = _get_api_type_for_compound(grouplist)
96 for group in grouplist:
97 module = sourcetree.get_object(group)
99 self._modules.add(module)
101 def set_installed(self):
102 """Mark the file installed."""
103 self._installed = True
105 def get_reporter_location(self):
106 return reporter.Location(self._path, None)
108 def is_installed(self):
109 return self._installed
111 def is_source_file(self):
112 return self._sourcefile
114 def is_test_file(self):
115 return self._dir.is_test_directory()
117 def is_documented(self):
118 return self._rawdoc and self._rawdoc.is_documented()
120 def has_brief_description(self):
121 return self._rawdoc and self._rawdoc.has_brief_description()
126 def get_documentation_type(self):
129 return self._rawdoc.get_visibility()
131 def get_api_type(self):
134 def get_expected_module(self):
135 return self._dir.get_module()
137 def get_doc_modules(self):
140 class GeneratedFile(File):
143 class Directory(object):
145 """(Sub)directory in the GROMACS tree."""
147 def __init__(self, path, parent):
148 """Initialize a file representation with basic information."""
150 self._name = os.path.basename(path)
151 self._parent = parent
154 self._is_test_dir = False
155 if parent and parent.is_test_directory() or \
156 os.path.basename(path) in ('tests', 'legacytests'):
157 self._is_test_dir = True
158 self._subdirs = set()
160 parent._subdirs.add(self)
162 def set_doc_xml(self, rawdoc, sourcetree):
163 """Assiociate Doxygen documentation entity with the directory."""
164 assert self._rawdoc is None
165 assert self._path == rawdoc.get_path().rstrip('/')
166 self._rawdoc = rawdoc
168 def set_module(self, module):
169 assert self._module is None
170 self._module = module
175 def get_reporter_location(self):
176 return reporter.Location(self._path, None)
178 def is_test_directory(self):
179 return self._is_test_dir
181 def get_module(self):
185 return self._parent.get_module()
188 def get_subdirectories(self):
191 class Module(object):
193 """Code module in the GROMACS source tree.
195 Modules are specific subdirectories that host a more or less coherent
196 set of routines. Simplified, every subdirectory under src/gromacs/ is
197 a different module. This object provides that abstraction and also links
198 the subdirectory to the module documentation (documented as a group in
199 Doxygen) if that exists.
202 def __init__(self, name, rootdir):
205 self._rootdir = rootdir
207 def set_doc_xml(self, rawdoc, sourcetree):
208 """Assiociate Doxygen documentation entity with the module."""
209 assert self._rawdoc is None
210 self._rawdoc = rawdoc
212 def is_documented(self):
213 return self._rawdoc is not None
220 """Class/struct/union in the GROMACS source code."""
222 def __init__(self, rawdoc, files):
223 self._rawdoc = rawdoc
224 self._files = set(files)
227 return self._rawdoc.get_name()
229 def get_reporter_location(self):
230 return self._rawdoc.get_reporter_location()
235 def is_documented(self):
236 return self._rawdoc.is_documented()
238 def has_brief_description(self):
239 return self._rawdoc.has_brief_description()
241 def get_documentation_type(self):
242 if not self.is_documented():
244 if self._rawdoc.is_local():
245 return DocType.internal
246 return self._rawdoc.get_visibility()
248 def get_file_documentation_type(self):
249 return max([fileobj.get_documentation_type() for fileobj in self._files])
251 def is_in_installed_file(self):
252 return any([fileobj.is_installed() for fileobj in self._files])
254 class GromacsTree(object):
256 """Root object for navigating the GROMACS source tree.
258 On initialization, the list of files and directories is initialized by
259 walking the source tree, and modules are created for top-level
260 subdirectories. At this point, only information that is accessible from
261 file names and paths only is available.
263 set_installed_file_list() can be called to set the list of installed
266 load_xml() can be called to load information from Doxygen XML data in
267 the build tree (the Doxygen XML data must have been built separately).
270 def __init__(self, source_root, build_root, reporter):
271 """Initialize the tree object by walking the source tree."""
272 self._source_root = os.path.abspath(source_root)
273 self._build_root = os.path.abspath(build_root)
274 self._reporter = reporter
276 self._docmap = dict()
279 self._modules = dict()
280 self._classes = set()
281 self._walk_dir(os.path.join(self._source_root, 'src'))
282 rootdir = self._get_dir(os.path.join('src', 'gromacs'))
283 for subdir in rootdir.get_subdirectories():
284 self._create_module(subdir)
285 rootdir = self._get_dir(os.path.join('src', 'testutils'))
286 self._create_module(rootdir)
288 def _get_rel_path(self, path):
289 assert os.path.isabs(path)
290 if path.startswith(self._build_root):
291 return path[len(self._build_root)+1:]
292 if path.startswith(self._source_root):
293 return path[len(self._source_root)+1:]
294 raise ValueError("path not under build nor source tree: {0}".format(path))
296 def _walk_dir(self, rootpath):
297 """Construct representation of the source tree by walking the file system."""
298 assert os.path.isabs(rootpath)
299 assert rootpath not in self._dirs
300 relpath = self._get_rel_path(rootpath)
301 self._dirs[relpath] = Directory(rootpath, None)
302 for dirpath, dirnames, filenames in os.walk(rootpath):
303 if 'contrib' in dirnames:
304 dirnames.remove('contrib')
305 if 'refdata' in dirnames:
306 dirnames.remove('refdata')
307 currentdir = self._dirs[self._get_rel_path(dirpath)]
308 # Loop through a copy so that we can modify dirnames.
309 for dirname in list(dirnames):
310 fullpath = os.path.join(dirpath, dirname)
311 if fullpath == self._build_root:
312 dirnames.remove(dirname)
314 relpath = self._get_rel_path(fullpath)
315 self._dirs[relpath] = Directory(fullpath, currentdir)
316 extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu')
317 for filename in filenames:
318 basename, extension = os.path.splitext(filename)
319 if extension in extensions:
320 fullpath = os.path.join(dirpath, filename)
321 relpath = self._get_rel_path(fullpath)
322 self._files[relpath] = File(fullpath, currentdir)
323 elif extension == '.cmakein':
324 extension = os.path.splitext(basename)[1]
325 if extension in extensions:
326 fullpath = os.path.join(dirpath, basename)
327 relpath = self._get_rel_path(fullpath)
328 fullpath = os.path.join(dirpath, filename)
329 self._files[relpath] = GeneratedFile(fullpath, currentdir)
331 def _create_module(self, rootdir):
332 """Create module for a subdirectory."""
333 name = 'module_' + rootdir.get_name()
334 moduleobj = Module(name, rootdir)
335 rootdir.set_module(moduleobj)
336 self._modules[name] = moduleobj
339 """Load Doxygen XML information."""
340 xmldir = os.path.join(self._build_root, 'doxygen', 'xml')
341 self._docset = xml.DocumentationSet(xmldir, self._reporter)
342 self._docset.load_details()
343 self._docset.merge_duplicates()
349 def _load_dirs(self):
350 """Load Doxygen XML directory information."""
351 rootdirs = self._docset.get_compounds(xml.Directory,
352 lambda x: x.get_parent() is None)
353 for dirdoc in rootdirs:
354 self._load_dir(dirdoc, None)
356 def _load_dir(self, dirdoc, parent):
357 """Load Doxygen XML directory information for a single directory."""
358 path = dirdoc.get_path().rstrip('/')
359 if not os.path.isabs(path):
360 self._reporter.xml_assert(dirdoc.get_xml_path(),
361 "expected absolute path in Doxygen-produced XML file")
363 relpath = self._get_rel_path(path)
364 dirobj = self._dirs.get(relpath)
366 dirobj = Directory(path, parent)
367 self._dirs[relpath] = dirobj
368 dirobj.set_doc_xml(dirdoc, self)
369 self._docmap[dirdoc] = dirobj
370 for subdirdoc in dirdoc.get_subdirectories():
371 self._load_dir(subdirdoc, dirobj)
373 def _load_modules(self):
374 """Load Doxygen XML module (group) information."""
375 moduledocs = self._docset.get_compounds(xml.Group,
376 lambda x: x.get_name().startswith('module_'))
377 for moduledoc in moduledocs:
378 moduleobj = self._modules.get(moduledoc.get_name())
380 self._reporter.input_error(
381 "no matching directory for module: {0}".format(moduledoc))
383 moduleobj.set_doc_xml(moduledoc, self)
384 self._docmap[moduledoc] = moduleobj
386 def _load_files(self):
387 """Load Doxygen XML file information."""
388 for filedoc in self._docset.get_files():
389 path = filedoc.get_path()
390 if not os.path.isabs(path):
391 self._reporter.xml_assert(filedoc.get_xml_path(),
392 "expected absolute path in Doxygen-produced XML file")
394 extension = os.path.splitext(filedoc.get_path())[1]
395 # We don't care about Markdown files that only produce pages
396 # (and fail the directory check below).
397 if extension == '.md':
399 dirdoc = filedoc.get_directory()
401 self._reporter.xml_assert(filedoc.get_xml_path(),
402 "file is not in any directory in Doxygen")
404 relpath = self._get_rel_path(path)
405 fileobj = self._files.get(relpath)
407 fileobj = File(path, self._docmap[dirdoc])
408 self._files[relpath] = fileobj
409 fileobj.set_doc_xml(filedoc, self)
410 self._docmap[filedoc] = fileobj
412 def _load_classes(self):
413 """Load Doxygen XML class information."""
414 classdocs = self._docset.get_classes()
415 for classdoc in classdocs:
416 files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
417 classobj = Class(classdoc, files)
418 self._docmap[classdoc] = classobj
419 self._classes.add(classobj)
421 def _get_dir(self, relpath):
422 """Get directory object for a path relative to source tree root."""
423 return self._dirs.get(relpath)
425 def set_installed_file_list(self, installedfiles):
426 """Set list of installed files."""
427 for path in installedfiles:
428 if not os.path.isabs(path):
429 self._reporter.input_error(
430 "installed file not specified with absolute path: {0}"
433 relpath = self._get_rel_path(path)
434 if relpath not in self._files:
435 self._reporter.input_error(
436 "installed file not in source tree: {0}".format(path))
438 self._files[relpath].set_installed()
440 def get_object(self, docobj):
441 """Get tree object for a Doxygen XML object."""
442 return self._docmap.get(docobj)
445 """Get iterable for all files in the source tree."""
446 return self._files.itervalues()
448 def get_classes(self):
449 """Get iterable for all classes in the source tree."""
452 def get_members(self):
453 """Get iterable for all members (in Doxygen terms) in the source tree."""
454 # TODO: Add wrappers to solve some issues.
455 return self._docset.get_members()