diff --git a/.ci/travis/code_layout/CMakeLists.txt b/.ci/travis/code_layout/CMakeLists.txt index 9d7c8279084..1f6a1f3413f 100644 --- a/.ci/travis/code_layout/CMakeLists.txt +++ b/.ci/travis/code_layout/CMakeLists.txt @@ -4,6 +4,8 @@ CMAKE_MINIMUM_REQUIRED(VERSION 2.8.6) SET(CMAKE_CXX_STANDARD 11) +SET(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/../../../cmake ${CMAKE_MODULE_PATH}) + ADD_SUBDIRECTORY(../../../src/astyle qgisstyle) ENABLE_TESTING() @@ -12,3 +14,15 @@ ADD_TEST(qgis_indentation ${CMAKE_SOURCE_DIR}/../../../scripts/verify-indentatio ADD_TEST(qgis_spelling ${CMAKE_SOURCE_DIR}/../../../scripts/spell_check/spell_test.sh) ADD_TEST(qgis_sipify ${CMAKE_SOURCE_DIR}/../../../tests/scripts/test_sipify.sh) ADD_TEST(qgis_sip_uptodate ${CMAKE_SOURCE_DIR}/../../../tests/scripts/test_sipfiles_uptodate.sh) + +IF (WITH_APIDOC) + ADD_SUBDIRECTORY(../../../doc doc) + + INCLUDE(CreateQgsVersion) + CREATE_QGSVERSION() + + set(QGIS_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/doc") + + INCLUDE(UsePythonTest) + ADD_PYTHON_TEST(PyQgsDocCoverage ${CMAKE_SOURCE_DIR}/../../../tests/src/python/test_qgsdoccoverage.py) +ENDIF (WITH_APIDOC) diff --git a/.ci/travis/code_layout/before_install.sh b/.ci/travis/code_layout/before_install.sh index 94b87121960..c58c1226949 100755 --- a/.ci/travis/code_layout/before_install.sh +++ b/.ci/travis/code_layout/before_install.sh @@ -13,4 +13,4 @@ # # ########################################################################### -pip install autopep8 +pip install autopep8 nose2 mock termcolor diff --git a/.ci/travis/code_layout/install.sh b/.ci/travis/code_layout/install.sh index 954d577944c..ad77d0dc40f 100755 --- a/.ci/travis/code_layout/install.sh +++ b/.ci/travis/code_layout/install.sh @@ -18,5 +18,5 @@ export CORES=2 mkdir build cd build -cmake ../.ci/travis/code_layout +cmake -DWITH_APIDOC=ON ../.ci/travis/code_layout make -j${CORES} diff --git a/.gitignore b/.gitignore index 86ee2ead03c..48df43d6a11 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ .pydevproject /CMakeLists.txt.user /CMakeLists.txt.user.* +.ci/travis/code_layout/scripts/qgisstyle* api_doc build* debian/*.debhelper diff --git a/.travis.yml b/.travis.yml index 754b985f36c..974410ad568 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,26 +22,27 @@ matrix: apt: sources: - llvm-toolchain-precise-3.8 - - george-edison55-precise-backports # doxygen 1.8.3 + - george-edison55-precise-backports # doxygen 1.8.3 # TODO: remove - ubuntu-toolchain-r-test packages: - - doxygen + - doxygen # TODO: remove - bison - flex - - graphviz + - graphviz # TODO: remove - libpq-dev - libfcgi-dev - libfftw3-3 - libgsl0-dev - pkg-config - poppler-utils - - txt2tags + - txt2tags # TODO: remove - xvfb - clang-3.8 - os: linux language: python + python: "3.5" env: - TRAVIS_CONFIG=code_layout dist: trusty @@ -51,8 +52,12 @@ matrix: addons: apt: sources: + - george-edison55-precise-backports # doxygen 1.8.3 - sourceline: 'ppa:jonathonf/backports' # silversearcher-ag backport packages: + - doxygen + - graphviz + - txt2tags - pkg-config - xvfb - flip diff --git a/CMakeLists.txt b/CMakeLists.txt index 939386865cf..b440d6a94c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -691,51 +691,8 @@ ENDIF(NOT CLANG_TIDY_EXE) ############################################################# # create qgsversion.h -IF (EXISTS ${CMAKE_SOURCE_DIR}/.git/index) - FIND_PROGRAM(GITCOMMAND git PATHS c:/cygwin/bin) - IF(GITCOMMAND) - IF(WIN32) - IF(USING_NINJA) - SET(ARG %a) - ELSE(USING_NINJA) - SET(ARG %%a) - ENDIF(USING_NINJA) - ADD_CUSTOM_COMMAND( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc - COMMAND for /f \"usebackq tokens=1\" ${ARG} in "(`\"${GITCOMMAND}\" log -n1 --oneline`)" do echo \#define QGSVERSION \"${ARG}\" >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp - COMMAND for /f \"usebackq tokens=1\" ${ARG} in "(`\"${GITCOMMAND}\" log -n1 --oneline`)" do echo PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} \(${ARG}\)\" >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc - COMMAND ${CMAKE_COMMAND} -DSRC=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp -DDST=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h -P ${CMAKE_SOURCE_DIR}/cmake/CopyIfChanged.cmake - MAIN_DEPENDENCY ${CMAKE_SOURCE_DIR}/.git/index - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - ) - ELSE(WIN32) - ADD_CUSTOM_COMMAND( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc - COMMAND ${GITCOMMAND} log -n1 --pretty=\#define\\ QGSVERSION\\ \\"%h\\" >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp - COMMAND ${GITCOMMAND} log -n1 --pretty='PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} \(%h\)\"' >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc - COMMAND ${GITCOMMAND} config remote.$$\(${GITCOMMAND} config branch.$$\(${GITCOMMAND} name-rev --name-only HEAD\).remote\).url | sed -e 's/^/\#define QGS_GIT_REMOTE_URL \"/' -e 's/$$/\"/' >>${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp - COMMAND ${CMAKE_COMMAND} -DSRC=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp -DDST=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h -P ${CMAKE_SOURCE_DIR}/cmake/CopyIfChanged.cmake - MAIN_DEPENDENCY ${CMAKE_SOURCE_DIR}/.git/index - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - ) - ENDIF(WIN32) - ELSE(GITCOMMAND) - MESSAGE(STATUS "git marker, but no git found - version will be unknown") - IF(NOT SHA) - SET(SHA "unknown") - ENDIF(NOT SHA) - FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h "#define QGSVERSION \"${SHA}\"\n") - FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc "PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} (${SHA})\"\n") - ENDIF(GITCOMMAND) -ELSE (EXISTS ${CMAKE_SOURCE_DIR}/.git/index) - IF(NOT SHA) - SET(SHA "exported") - ENDIF(NOT SHA) - FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h "#define QGSVERSION \"${SHA}\"\n") - FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc "PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} (${SHA})\"\n") -ENDIF (EXISTS ${CMAKE_SOURCE_DIR}/.git/index) - -ADD_CUSTOM_TARGET(version ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h) +INCLUDE(CreateQgsVersion) +CREATE_QGSVERSION() ############################################################# # process subdirs diff --git a/cmake/CreateQgsVersion.cmake b/cmake/CreateQgsVersion.cmake new file mode 100644 index 00000000000..05d2c5ff7dd --- /dev/null +++ b/cmake/CreateQgsVersion.cmake @@ -0,0 +1,47 @@ +MACRO(CREATE_QGSVERSION) + IF (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/../.git/index) + FIND_PROGRAM(GITCOMMAND git PATHS c:/cygwin/bin) + IF(GITCOMMAND) + IF(WIN32) + IF(USING_NINJA) + SET(ARG %a) + ELSE(USING_NINJA) + SET(ARG %%a) + ENDIF(USING_NINJA) + ADD_CUSTOM_COMMAND( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc + COMMAND for /f \"usebackq tokens=1\" ${ARG} in "(`\"${GITCOMMAND}\" log -n1 --oneline`)" do echo \#define QGSVERSION \"${ARG}\" >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp + COMMAND for /f \"usebackq tokens=1\" ${ARG} in "(`\"${GITCOMMAND}\" log -n1 --oneline`)" do echo PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} \(${ARG}\)\" >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc + COMMAND ${CMAKE_COMMAND} -DSRC=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp -DDST=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h -P ${CMAKE_SOURCE_DIR}/cmake/CopyIfChanged.cmake + MAIN_DEPENDENCY ${CMAKE_SOURCE_DIR}/.git/index + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + ELSE(WIN32) + ADD_CUSTOM_COMMAND( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc + COMMAND ${GITCOMMAND} log -n1 --pretty=\#define\\ QGSVERSION\\ \\"%h\\" >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp + COMMAND ${GITCOMMAND} log -n1 --pretty='PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} \(%h\)\"' >${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc + COMMAND ${GITCOMMAND} config remote.$$\(${GITCOMMAND} config branch.$$\(${GITCOMMAND} name-rev --name-only HEAD\).remote\).url | sed -e 's/^/\#define QGS_GIT_REMOTE_URL \"/' -e 's/$$/\"/' >>${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp + COMMAND ${CMAKE_COMMAND} -DSRC=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h.temp -DDST=${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h -P ${CMAKE_SOURCE_DIR}/cmake/CopyIfChanged.cmake + MAIN_DEPENDENCY ${CMAKE_SOURCE_DIR}/.git/index + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + ENDIF(WIN32) + ELSE(GITCOMMAND) + MESSAGE(STATUS "git marker, but no git found - version will be unknown") + IF(NOT SHA) + SET(SHA "unknown") + ENDIF(NOT SHA) + FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h "#define QGSVERSION \"${SHA}\"\n") + FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc "PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} (${SHA})\"\n") + ENDIF(GITCOMMAND) + ELSE (EXISTS ${CMAKE_SOURCE_DIR}/.git/index) + IF(NOT SHA) + SET(SHA "exported") + ENDIF(NOT SHA) + FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h "#define QGSVERSION \"${SHA}\"\n") + FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.inc "PROJECT_NUMBER = \"${COMPLETE_VERSION}-${RELEASE_NAME} (${SHA})\"\n") + ENDIF (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/../.git/index) + + ADD_CUSTOM_TARGET(version ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/qgsversion.h) +ENDMACRO(CREATE_QGSVERSION) diff --git a/cmake_templates/Doxyfile.in b/cmake_templates/Doxyfile.in index ab9fc85d39b..c1699c11064 100644 --- a/cmake_templates/Doxyfile.in +++ b/cmake_templates/Doxyfile.in @@ -1133,7 +1133,7 @@ HTML_STYLESHEET = # list). For an example see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_EXTRA_STYLESHEET = @CMAKE_SOURCE_DIR@/doc/api_custom.css +HTML_EXTRA_STYLESHEET = @CMAKE_CURRENT_SOURCE_DIR@/api_custom.css # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 28313245982..cafdfc7773c 100755 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -18,7 +18,7 @@ IF(TXT2TAGS_EXECUTABLE) ADD_CUSTOM_TARGET (t2tdoc ALL DEPENDS ${QGIS_DOC_FILES}) ELSE(TXT2TAGS_EXECUTABLE) SET(QGIS_DOC_FILES - ${CMAKE_SOURCE_DIR}/INSTALL + ${CMAKE_CURRENT_SOURCE_DIR}/../INSTALL ) ENDIF(TXT2TAGS_EXECUTABLE) @@ -46,65 +46,65 @@ IF(WITH_APIDOC) ENDIF(GENERATE_QHP) SET(DOXYGEN_INCLUDE_PATH - ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core ) STRING(REPLACE ";" " " DOXYGEN_INCLUDE_PATH "${DOXYGEN_INCLUDE_PATH}") SET(DOXYGEN_INPUT - ${CMAKE_SOURCE_DIR}/doc - ${CMAKE_SOURCE_DIR}/src/core - ${CMAKE_SOURCE_DIR}/src/core/annotations - ${CMAKE_SOURCE_DIR}/src/core/auth - ${CMAKE_SOURCE_DIR}/src/core/composer - ${CMAKE_SOURCE_DIR}/src/core/diagram - ${CMAKE_SOURCE_DIR}/src/core/dxf - ${CMAKE_SOURCE_DIR}/src/core/effects - ${CMAKE_SOURCE_DIR}/src/core/fieldformatter - ${CMAKE_SOURCE_DIR}/src/core/geometry - ${CMAKE_SOURCE_DIR}/src/core/gps - ${CMAKE_SOURCE_DIR}/src/core/layertree - ${CMAKE_SOURCE_DIR}/src/core/metadata - ${CMAKE_SOURCE_DIR}/src/core/pal - ${CMAKE_SOURCE_DIR}/src/core/processing - ${CMAKE_SOURCE_DIR}/src/core/providers - ${CMAKE_SOURCE_DIR}/src/core/providers/memory - ${CMAKE_SOURCE_DIR}/src/core/raster - ${CMAKE_SOURCE_DIR}/src/core/scalebar - ${CMAKE_SOURCE_DIR}/src/core/symbology-ng - ${CMAKE_SOURCE_DIR}/src/gui - ${CMAKE_SOURCE_DIR}/src/gui/auth - ${CMAKE_SOURCE_DIR}/src/gui/attributetable - ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets - ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets/core - ${CMAKE_SOURCE_DIR}/src/gui/effects - ${CMAKE_SOURCE_DIR}/src/gui/layertree - ${CMAKE_SOURCE_DIR}/src/gui/locator - ${CMAKE_SOURCE_DIR}/src/gui/raster - ${CMAKE_SOURCE_DIR}/src/gui/symbology-ng - ${CMAKE_SOURCE_DIR}/src/analysis - ${CMAKE_SOURCE_DIR}/src/analysis/interpolation - ${CMAKE_SOURCE_DIR}/src/analysis/network - ${CMAKE_SOURCE_DIR}/src/analysis/openstreetmap - ${CMAKE_SOURCE_DIR}/src/analysis/raster - ${CMAKE_SOURCE_DIR}/src/analysis/vector - ${CMAKE_SOURCE_DIR}/src/plugins + ${CMAKE_CURRENT_SOURCE_DIR}/../doc + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/annotations + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/auth + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/composer + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/diagram + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/dxf + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/effects + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/fieldformatter + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/geometry + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/gps + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/layertree + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/metadata + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/pal + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/processing + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/providers + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/providers/memory + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/raster + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/scalebar + ${CMAKE_CURRENT_SOURCE_DIR}/../src/core/symbology-ng + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/auth + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/attributetable + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/editorwidgets + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/editorwidgets/core + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/effects + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/layertree + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/locator + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/raster + ${CMAKE_CURRENT_SOURCE_DIR}/../src/gui/symbology-ng + ${CMAKE_CURRENT_SOURCE_DIR}/../src/analysis + ${CMAKE_CURRENT_SOURCE_DIR}/../src/analysis/interpolation + ${CMAKE_CURRENT_SOURCE_DIR}/../src/analysis/network + ${CMAKE_CURRENT_SOURCE_DIR}/../src/analysis/openstreetmap + ${CMAKE_CURRENT_SOURCE_DIR}/../src/analysis/raster + ${CMAKE_CURRENT_SOURCE_DIR}/../src/analysis/vector + ${CMAKE_CURRENT_SOURCE_DIR}/../src/plugins ) IF(WITH_SERVER_PLUGINS) SET(DOXYGEN_INPUT ${DOXYGEN_INPUT} - ${CMAKE_SOURCE_DIR}/src/server/qgsserver.h - ${CMAKE_SOURCE_DIR}/src/server/qgscapabilitiescache.h - ${CMAKE_SOURCE_DIR}/src/server/qgsserverexception.h - ${CMAKE_SOURCE_DIR}/src/server/qgsrequesthandler.h - ${CMAKE_SOURCE_DIR}/src/server/qgsserverfilter.h - ${CMAKE_SOURCE_DIR}/src/server/qgsaccesscontrolfilter.h - ${CMAKE_SOURCE_DIR}/src/server/qgsserverinterface.h - ${CMAKE_SOURCE_DIR}/src/server/qgsserverrequest.h - ${CMAKE_SOURCE_DIR}/src/server/qgsserverresponse.h - ${CMAKE_SOURCE_DIR}/src/server/qgsserverrequest.h - ${CMAKE_SOURCE_DIR}/src/server/qgsservice.h - ${CMAKE_SOURCE_DIR}/src/server/qgsserviceregistry.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserver.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgscapabilitiescache.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserverexception.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsrequesthandler.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserverfilter.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsaccesscontrolfilter.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserverinterface.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserverrequest.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserverresponse.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserverrequest.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsservice.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/server/qgsserviceregistry.h ) ENDIF(WITH_SERVER_PLUGINS) @@ -120,7 +120,7 @@ IF(WITH_APIDOC) STRING(REPLACE ";" " " DOXYGEN_INPUT "${DOXYGEN_INPUT}") - CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/cmake_templates/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY) + CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/../cmake_templates/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY) SET (DOXYGEN_ON_DEMAND FALSE CACHE BOOL "Determines whether the QGIS API doxygen documentation should be build on demand only") diff --git a/tests/src/python/doxygen_parser.py b/tests/src/python/doxygen_parser.py new file mode 100644 index 00000000000..29f5f0e2dc0 --- /dev/null +++ b/tests/src/python/doxygen_parser.py @@ -0,0 +1,523 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + mocked + --------------------- + Date : May 2017 + Copyright : (C) 2017 by Denis Rouzaud + Email : denis.rouzaud@gmail.com +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +__author__ = 'Denis Rouzaud' +__date__ = 'May 2017' +__copyright__ = '(C) 2017, Denis Rouzaud' + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = ':%H$' + +import re +import glob +import os + +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + + +class DoxygenParser(): + + """ + Parses the XML files generated by Doxygen which describe the API docs + """ + + def __init__(self, path, acceptable_missing={}, acceptable_missing_added_note=[], acceptable_missing_brief=[]): + """ + Initializes the parser. + :param path: Path to Doxygen XML output + """ + self.acceptable_missing = acceptable_missing + self.acceptable_missing_added_note = acceptable_missing_added_note + self.acceptable_missing_brief = acceptable_missing_brief + self.documentable_members = 0 + self.documented_members = 0 + self.undocumented_members = {} + self.bindable_members = [] + self.groups = {} + self.classes_missing_group = [] + self.classes_missing_brief = [] + self.classes_missing_version_added = [] + # for some reason the Doxygen generation on Travis refuses to assign these classes to groups + self.acceptable_missing_group = ['QgsOgcUtils::LayerProperties', + 'QgsSQLStatement::Node', + 'QgsSQLStatement::NodeBinaryOperator', + 'QgsSQLStatement::NodeColumnRef', + 'QgsSQLStatement::NodeFunction', + 'QgsSQLStatement::NodeInOperator', + 'QgsSQLStatement::NodeList', + 'QgsSQLStatement::NodeLiteral', + 'QgsSQLStatement::NodeUnaryOperator', + 'QgsRuleBasedLabeling::Rule', + 'QgsSQLStatement::Visitor'] + self.version_regex = re.compile(r'QGIS [\d\.]+.*') + self.parseFiles(path) + + def parseFiles(self, path): + """ Parses all the Doxygen XML files in a folder + :param path: Path to Doxygen XML output + """ + + # find groups + for f in glob.glob(os.path.join(path, 'group__*.xml')): + group, members = self.parseGroup(f) + self.groups[group] = members + + # parse docs + for f in glob.glob(os.path.join(path, '*.xml')): + self.parseFile(f) + + def parseGroup(self, f): + """ Parses a single Doxygen Group XML file + :param f: XML file path + """ + name = None + members = [] + + # Wrap everything in a try, as sometimes Doxygen XML is malformed + try: + for event, elem in ET.iterparse(f): + if event == 'end' and elem.tag == 'compoundname': + name = elem.text + if event == 'end' and elem.tag == 'innerclass': + members.append(elem.text) + except: + pass + + return name, members + + def hasGroup(self, class_name): + """ Returns true if a class has been assigned to a group + :param class_name class name to test + """ + for g in self.groups: + if class_name in self.groups[g]: + return True + return False + + def parseFile(self, f): + """ Parses a single Doxygen XML file + :param f: XML file path + """ + documentable_members = 0 + documented_members = 0 + + # Wrap everything in a try, as sometimes Doxygen XML is malformed + try: + for event, elem in ET.iterparse(f): + if event == 'end' and elem.tag == 'compounddef': + if self.elemIsPublicClass(elem): + # store documentation status + members, documented, undocumented, bindable, has_brief_description, found_version_added = self.parseClassElem(elem) + documentable_members += members + documented_members += documented + class_name = elem.find('compoundname').text + acceptable_missing = self.acceptable_missing.get(class_name, []) + + if not self.hasGroup(class_name) and class_name not in self.acceptable_missing_group: + self.classes_missing_group.append(class_name) + if class_name not in self.acceptable_missing_brief and not has_brief_description: + self.classes_missing_brief.append(class_name) + if class_name not in self.acceptable_missing_added_note and not found_version_added: + self.classes_missing_version_added.append(class_name) + + # GEN LIST + # if len(undocumented) > 0: + # print('"%s": [%s],' % (class_name, ", ".join(['"%s"' % e.replace('"', '\\"') for e in undocumented]))) + + unacceptable_undocumented = undocumented - set(acceptable_missing) + + # do a case insensitive check too + unacceptable_undocumented_insensitive = set([u.lower() for u in undocumented]) - set([u.lower() for u in acceptable_missing]) + + if len(unacceptable_undocumented_insensitive) > 0: + self.undocumented_members[class_name] = {} + self.undocumented_members[class_name]['documented'] = documented + self.undocumented_members[class_name]['members'] = members + self.undocumented_members[class_name]['missing_members'] = unacceptable_undocumented + + # store bindable members + if self.classElemIsBindable(elem): + for m in bindable: + self.bindable_members.append(m) + + elem.clear() + except ET.ParseError as e: + # sometimes Doxygen generates malformed xml (e.g., for < and > operators) + line_num, col = e.position + with open(f, 'r') as xml_file: + for i, l in enumerate(xml_file): + if i == line_num - 1: + line = l + break + caret = '{:=>{}}'.format('^', col) + print(('ParseError in {}\n{}\n{}\n{}'.format(f, e, line, caret))) + + self.documentable_members += documentable_members + self.documented_members += documented_members + + def elemIsPublicClass(self, elem): + """ Tests whether an XML element corresponds to a public (or protected) class + :param elem: XML element + """ + + # only looking for classes + if not elem.get('kind') == 'class': + return False + + # only looking for public or protected classes + return elem.get('prot') in ('public', 'protected') + + def classElemIsBindable(self, elem): + """ Tests whether a class should have SIP bindings + :param elem: XML element corresponding to a class + """ + try: + # check for classes with special python doc notes (probably 'not available' or renamed classes, either way + # they should be safe to ignore as obviously some consideration has been given to Python bindings) + detailed_sec = elem.find('detaileddescription') + for p in detailed_sec.getiterator('para'): + for s in p.getiterator('simplesect'): + for ps in s.getiterator('para'): + if ps.text and 'python' in ps.text.lower(): + return False + return True + except: + return True + + def parseClassElem(self, e): + """ Parses an XML element corresponding to a Doxygen class + :param e: XML element + """ + documentable_members = 0 + documented_members = 0 + undocumented_members = set() + bindable_members = [] + # loop through all members + for m in e.getiterator('memberdef'): + signature = self.memberSignature(m) + if signature is None: + continue + if self.elemIsBindableMember(m): + bindable_member = [e.find('compoundname').text, m.find('name').text] + if bindable_member not in bindable_members: + bindable_members.append(bindable_member) + if self.elemIsDocumentableMember(m): + documentable_members += 1 + if self.memberIsDocumented(m): + documented_members += 1 + else: + undocumented_members.add(signature) + # test for brief description + d = e.find('briefdescription') + has_brief_description = False + if d: + has_brief_description = True + + # test for "added in QGIS xxx" string + d = e.find('detaileddescription') + found_version_added = False + for para in d.getiterator('para'): + for s in para.getiterator('simplesect'): + if s.get('kind') == 'since': + for p in s.getiterator('para'): + if self.version_regex.match(p.text): + found_version_added = True + break + if found_version_added: + break + + return documentable_members, documented_members, undocumented_members, bindable_members, has_brief_description, found_version_added + + def memberSignature(self, elem): + """ Returns the signature for a member + :param elem: XML element for a class member + """ + a = elem.find('argsstring') + try: + if a is not None: + return elem.find('name').text + a.text + else: + return elem.find('name').text + except: + return None + + def elemIsBindableMember(self, elem): + """ Tests whether an member should be included in SIP bindings + :param elem: XML element for a class member + """ + + # only public or protected members are bindable + if not self.visibility(elem) in ('public', 'protected'): + return False + + # property themselves are not bound, only getters and setters + if self.isProperty(elem): + return False + + # ignore friend classes + if self.isFriendClass(elem): + return False + + # ignore typedefs (can't test for them) + if self.isTypeDef(elem): + return False + + if self.isVariable(elem) and self.visibility(elem) == 'protected': + # protected variables can't be bound in SIP + return False + + # check for members with special python doc notes (probably 'not available' or renamed methods, either way + # they should be safe to ignore as obviously some consideration has been given to Python bindings) + try: + detailed_sec = elem.find('detaileddescription') + for p in detailed_sec.getiterator('para'): + for s in p.getiterator('simplesect'): + for ps in s.getiterator('para'): + if ps.text and 'python' in ps.text.lower(): + return False + except: + pass + + # ignore constructors and destructor, can't test for these + if self.isDestructor(elem) or self.isConstructor(elem): + return False + + # ignore operators, also can't test + if self.isOperator(elem): + return False + + # ignore deprecated members + if self.isDeprecated(elem): + return False + + return True + + def elemIsDocumentableMember(self, elem): + """ Tests whether an member should be included in Doxygen docs + :param elem: XML element for a class member + """ + + # ignore variables (for now, eventually public/protected variables should be documented) + if self.isVariable(elem): + return False + + # only public or protected members should be documented + if not self.visibility(elem) in ('public', 'protected'): + return False + + # ignore reimplemented methods + if self.isReimplementation(elem): + return False + + # ignore friend classes + if self.isFriendClass(elem): + return False + + # ignore destructor + if self.isDestructor(elem): + return False + + # ignore constructors with no arguments + if self.isConstructor(elem): + try: + if elem.find('argsstring').text == '()': + return False + except: + pass + + name = elem.find('name') + + # ignore certain obvious operators + try: + if name.text in ('operator=', 'operator==', 'operator!='): + return False + except: + pass + + # ignore on_* slots + try: + if name.text.startswith('on_'): + return False + except: + pass + + # ignore deprecated members + if self.isDeprecated(elem): + return False + + return True + + def visibility(self, elem): + """ Returns the visibility of a class or member + :param elem: XML element for a class or member + """ + try: + return elem.get('prot') + except: + return '' + + def isVariable(self, member_elem): + """ Tests whether an member is a variable + :param member_elem: XML element for a class member + """ + try: + if member_elem.get('kind') == 'variable': + return True + except: + pass + + return False + + def isProperty(self, member_elem): + """ Tests whether an member is a property + :param member_elem: XML element for a class member + """ + try: + if member_elem.get('kind') == 'property': + return True + except: + pass + + return False + + def isDestructor(self, member_elem): + """ Tests whether an member is a destructor + :param member_elem: XML element for a class member + """ + try: + name = member_elem.find('name').text + if name.startswith('~'): + # destructor + return True + except: + pass + return False + + def isConstructor(self, member_elem): + """ Tests whether an member is a constructor + :param member_elem: XML element for a class member + """ + try: + definition = member_elem.find('definition').text + name = member_elem.find('name').text + if '{}::{}'.format(name, name) in definition: + return True + except: + pass + + return False + + def isOperator(self, member_elem): + """ Tests whether an member is an operator + :param member_elem: XML element for a class member + """ + try: + name = member_elem.find('name').text + if re.match('^operator\W.*', name): + return True + except: + pass + + return False + + def isFriendClass(self, member_elem): + """ Tests whether an member is a friend class + :param member_elem: XML element for a class member + """ + try: + if member_elem.get('kind') == 'friend': + return True + except: + pass + return False + + def isTypeDef(self, member_elem): + """ Tests whether an member is a type def + :param member_elem: XML element for a class member + """ + try: + if member_elem.get('kind') == 'typedef': + return True + except: + pass + return False + + def isReimplementation(self, member_elem): + """ Tests whether an member is a reimplementation + :param member_elem: XML element for a class member + """ + + # use two different tests, as Doxygen will not detect reimplemented Qt methods + try: + if member_elem.find('reimplements') is not None: + return True + if ' override' in member_elem.find('argsstring').text: + return True + except: + pass + + return False + + def isDeprecated(self, member_elem): + """ Tests whether an member is deprecated + :param member_elem: XML element for a class member + """ + + # look for both Q_DECL_DEPRECATED and Doxygen deprecated tag + decl_deprecated = False + type_elem = member_elem.find('type') + try: + if 'Q_DECL_DEPRECATED' in type_elem.text: + decl_deprecated = True + except: + pass + + doxy_deprecated = False + try: + for p in member_elem.find('detaileddescription').getiterator('para'): + for s in p.getiterator('xrefsect'): + if s.find('xreftitle') is not None and 'Deprecated' in s.find('xreftitle').text: + doxy_deprecated = True + break + except: + assert 0, member_elem.find('definition').text + + if not decl_deprecated and not doxy_deprecated: + return False + + # only functions for now, but in future this should also apply for enums and variables + if member_elem.get('kind') in ('function', 'variable'): + assert decl_deprecated, 'Error: Missing Q_DECL_DEPRECATED for {}'.format(member_elem.find('definition').text) + assert doxy_deprecated, 'Error: Missing Doxygen deprecated tag for {}'.format(member_elem.find('definition').text) + + return True + + def memberIsDocumented(self, member_elem): + """ Tests whether an member has documentation + :param member_elem: XML element for a class member + """ + for doc_type in ('inbodydescription', 'briefdescription', 'detaileddescription'): + doc = member_elem.find(doc_type) + if doc is not None and list(doc): + return True + return False diff --git a/tests/src/python/test_qgsdoccoverage.py b/tests/src/python/test_qgsdoccoverage.py index c3c8b1b0ead..3d9d3c905b1 100644 --- a/tests/src/python/test_qgsdoccoverage.py +++ b/tests/src/python/test_qgsdoccoverage.py @@ -14,10 +14,14 @@ __revision__ = '$Format:%H$' import os import sys -from qgis.testing import unittest +try: + from qgis.static_testing import unittest +except ImportError: + from nose2.compat import unittest + from termcolor import colored -from utilities import DoxygenParser +from doxygen_parser import DoxygenParser from acceptable_missing_doc import ACCEPTABLE_MISSING_DOCS, ACCEPTABLE_MISSING_ADDED_NOTE, ACCEPTABLE_MISSING_BRIEF # TO regenerate the list: diff --git a/tests/src/python/utilities.py b/tests/src/python/utilities.py index bfbb4c97e0e..a8360c5859f 100644 --- a/tests/src/python/utilities.py +++ b/tests/src/python/utilities.py @@ -15,7 +15,6 @@ import qgis # NOQA import os import sys -import glob import platform import tempfile import re @@ -37,11 +36,7 @@ from qgis.core import ( ) from qgis.testing import start_app import hashlib -import re -try: - import xml.etree.cElementTree as ET -except ImportError: - import xml.etree.ElementTree as ET + import webbrowser import subprocess @@ -325,494 +320,6 @@ def printImportant(info): f.write('{}\n'.format(info)) -class DoxygenParser(): - - """ - Parses the XML files generated by Doxygen which describe the API docs - """ - - def __init__(self, path, acceptable_missing={}, acceptable_missing_added_note=[], acceptable_missing_brief=[]): - """ - Initializes the parser. - :param path: Path to Doxygen XML output - """ - self.acceptable_missing = acceptable_missing - self.acceptable_missing_added_note = acceptable_missing_added_note - self.acceptable_missing_brief = acceptable_missing_brief - self.documentable_members = 0 - self.documented_members = 0 - self.undocumented_members = {} - self.bindable_members = [] - self.groups = {} - self.classes_missing_group = [] - self.classes_missing_brief = [] - self.classes_missing_version_added = [] - # for some reason the Doxygen generation on Travis refuses to assign these classes to groups - self.acceptable_missing_group = ['QgsOgcUtils::LayerProperties', - 'QgsSQLStatement::Node', - 'QgsSQLStatement::NodeBinaryOperator', - 'QgsSQLStatement::NodeColumnRef', - 'QgsSQLStatement::NodeFunction', - 'QgsSQLStatement::NodeInOperator', - 'QgsSQLStatement::NodeList', - 'QgsSQLStatement::NodeLiteral', - 'QgsSQLStatement::NodeUnaryOperator', - 'QgsRuleBasedLabeling::Rule', - 'QgsSQLStatement::Visitor'] - self.version_regex = re.compile(r'QGIS [\d\.]+.*') - self.parseFiles(path) - - def parseFiles(self, path): - """ Parses all the Doxygen XML files in a folder - :param path: Path to Doxygen XML output - """ - - # find groups - for f in glob.glob(os.path.join(path, 'group__*.xml')): - group, members = self.parseGroup(f) - self.groups[group] = members - - # parse docs - for f in glob.glob(os.path.join(path, '*.xml')): - self.parseFile(f) - - def parseGroup(self, f): - """ Parses a single Doxygen Group XML file - :param f: XML file path - """ - name = None - members = [] - - # Wrap everything in a try, as sometimes Doxygen XML is malformed - try: - for event, elem in ET.iterparse(f): - if event == 'end' and elem.tag == 'compoundname': - name = elem.text - if event == 'end' and elem.tag == 'innerclass': - members.append(elem.text) - except: - pass - - return name, members - - def hasGroup(self, class_name): - """ Returns true if a class has been assigned to a group - :param class_name class name to test - """ - for g in self.groups: - if class_name in self.groups[g]: - return True - return False - - def parseFile(self, f): - """ Parses a single Doxygen XML file - :param f: XML file path - """ - documentable_members = 0 - documented_members = 0 - - # Wrap everything in a try, as sometimes Doxygen XML is malformed - try: - for event, elem in ET.iterparse(f): - if event == 'end' and elem.tag == 'compounddef': - if self.elemIsPublicClass(elem): - # store documentation status - members, documented, undocumented, bindable, has_brief_description, found_version_added = self.parseClassElem(elem) - documentable_members += members - documented_members += documented - class_name = elem.find('compoundname').text - acceptable_missing = self.acceptable_missing.get(class_name, []) - - if not self.hasGroup(class_name) and class_name not in self.acceptable_missing_group: - self.classes_missing_group.append(class_name) - if class_name not in self.acceptable_missing_brief and not has_brief_description: - self.classes_missing_brief.append(class_name) - if class_name not in self.acceptable_missing_added_note and not found_version_added: - self.classes_missing_version_added.append(class_name) - - # GEN LIST - # if len(undocumented) > 0: - # print('"%s": [%s],' % (class_name, ", ".join(['"%s"' % e.replace('"', '\\"') for e in undocumented]))) - - unacceptable_undocumented = undocumented - set(acceptable_missing) - - # do a case insensitive check too - unacceptable_undocumented_insensitive = set([u.lower() for u in undocumented]) - set([u.lower() for u in acceptable_missing]) - - if len(unacceptable_undocumented_insensitive) > 0: - self.undocumented_members[class_name] = {} - self.undocumented_members[class_name]['documented'] = documented - self.undocumented_members[class_name]['members'] = members - self.undocumented_members[class_name]['missing_members'] = unacceptable_undocumented - - # store bindable members - if self.classElemIsBindable(elem): - for m in bindable: - self.bindable_members.append(m) - - elem.clear() - except ET.ParseError as e: - # sometimes Doxygen generates malformed xml (e.g., for < and > operators) - line_num, col = e.position - with open(f, 'r') as xml_file: - for i, l in enumerate(xml_file): - if i == line_num - 1: - line = l - break - caret = '{:=>{}}'.format('^', col) - print(('ParseError in {}\n{}\n{}\n{}'.format(f, e, line, caret))) - - self.documentable_members += documentable_members - self.documented_members += documented_members - - def elemIsPublicClass(self, elem): - """ Tests whether an XML element corresponds to a public (or protected) class - :param elem: XML element - """ - - # only looking for classes - if not elem.get('kind') == 'class': - return False - - # only looking for public or protected classes - return elem.get('prot') in ('public', 'protected') - - def classElemIsBindable(self, elem): - """ Tests whether a class should have SIP bindings - :param elem: XML element corresponding to a class - """ - try: - # check for classes with special python doc notes (probably 'not available' or renamed classes, either way - # they should be safe to ignore as obviously some consideration has been given to Python bindings) - detailed_sec = elem.find('detaileddescription') - for p in detailed_sec.getiterator('para'): - for s in p.getiterator('simplesect'): - for ps in s.getiterator('para'): - if ps.text and 'python' in ps.text.lower(): - return False - return True - except: - return True - - def parseClassElem(self, e): - """ Parses an XML element corresponding to a Doxygen class - :param e: XML element - """ - documentable_members = 0 - documented_members = 0 - undocumented_members = set() - bindable_members = [] - # loop through all members - for m in e.getiterator('memberdef'): - signature = self.memberSignature(m) - if signature is None: - continue - if self.elemIsBindableMember(m): - bindable_member = [e.find('compoundname').text, m.find('name').text] - if bindable_member not in bindable_members: - bindable_members.append(bindable_member) - if self.elemIsDocumentableMember(m): - documentable_members += 1 - if self.memberIsDocumented(m): - documented_members += 1 - else: - undocumented_members.add(signature) - # test for brief description - d = e.find('briefdescription') - has_brief_description = False - if d: - has_brief_description = True - - # test for "added in QGIS xxx" string - d = e.find('detaileddescription') - found_version_added = False - for para in d.getiterator('para'): - for s in para.getiterator('simplesect'): - if s.get('kind') == 'since': - for p in s.getiterator('para'): - if self.version_regex.match(p.text): - found_version_added = True - break - if found_version_added: - break - - return documentable_members, documented_members, undocumented_members, bindable_members, has_brief_description, found_version_added - - def memberSignature(self, elem): - """ Returns the signature for a member - :param elem: XML element for a class member - """ - a = elem.find('argsstring') - try: - if a is not None: - return elem.find('name').text + a.text - else: - return elem.find('name').text - except: - return None - - def elemIsBindableMember(self, elem): - """ Tests whether an member should be included in SIP bindings - :param elem: XML element for a class member - """ - - # only public or protected members are bindable - if not self.visibility(elem) in ('public', 'protected'): - return False - - # property themselves are not bound, only getters and setters - if self.isProperty(elem): - return False - - # ignore friend classes - if self.isFriendClass(elem): - return False - - # ignore typedefs (can't test for them) - if self.isTypeDef(elem): - return False - - if self.isVariable(elem) and self.visibility(elem) == 'protected': - # protected variables can't be bound in SIP - return False - - # check for members with special python doc notes (probably 'not available' or renamed methods, either way - # they should be safe to ignore as obviously some consideration has been given to Python bindings) - try: - detailed_sec = elem.find('detaileddescription') - for p in detailed_sec.getiterator('para'): - for s in p.getiterator('simplesect'): - for ps in s.getiterator('para'): - if ps.text and 'python' in ps.text.lower(): - return False - except: - pass - - # ignore constructors and destructor, can't test for these - if self.isDestructor(elem) or self.isConstructor(elem): - return False - - # ignore operators, also can't test - if self.isOperator(elem): - return False - - # ignore deprecated members - if self.isDeprecated(elem): - return False - - return True - - def elemIsDocumentableMember(self, elem): - """ Tests whether an member should be included in Doxygen docs - :param elem: XML element for a class member - """ - - # ignore variables (for now, eventually public/protected variables should be documented) - if self.isVariable(elem): - return False - - # only public or protected members should be documented - if not self.visibility(elem) in ('public', 'protected'): - return False - - # ignore reimplemented methods - if self.isReimplementation(elem): - return False - - # ignore friend classes - if self.isFriendClass(elem): - return False - - # ignore destructor - if self.isDestructor(elem): - return False - - # ignore constructors with no arguments - if self.isConstructor(elem): - try: - if elem.find('argsstring').text == '()': - return False - except: - pass - - name = elem.find('name') - - # ignore certain obvious operators - try: - if name.text in ('operator=', 'operator==', 'operator!='): - return False - except: - pass - - # ignore on_* slots - try: - if name.text.startswith('on_'): - return False - except: - pass - - # ignore deprecated members - if self.isDeprecated(elem): - return False - - return True - - def visibility(self, elem): - """ Returns the visibility of a class or member - :param elem: XML element for a class or member - """ - try: - return elem.get('prot') - except: - return '' - - def isVariable(self, member_elem): - """ Tests whether an member is a variable - :param member_elem: XML element for a class member - """ - try: - if member_elem.get('kind') == 'variable': - return True - except: - pass - - return False - - def isProperty(self, member_elem): - """ Tests whether an member is a property - :param member_elem: XML element for a class member - """ - try: - if member_elem.get('kind') == 'property': - return True - except: - pass - - return False - - def isDestructor(self, member_elem): - """ Tests whether an member is a destructor - :param member_elem: XML element for a class member - """ - try: - name = member_elem.find('name').text - if name.startswith('~'): - # destructor - return True - except: - pass - return False - - def isConstructor(self, member_elem): - """ Tests whether an member is a constructor - :param member_elem: XML element for a class member - """ - try: - definition = member_elem.find('definition').text - name = member_elem.find('name').text - if '{}::{}'.format(name, name) in definition: - return True - except: - pass - - return False - - def isOperator(self, member_elem): - """ Tests whether an member is an operator - :param member_elem: XML element for a class member - """ - try: - name = member_elem.find('name').text - if re.match('^operator\W.*', name): - return True - except: - pass - - return False - - def isFriendClass(self, member_elem): - """ Tests whether an member is a friend class - :param member_elem: XML element for a class member - """ - try: - if member_elem.get('kind') == 'friend': - return True - except: - pass - return False - - def isTypeDef(self, member_elem): - """ Tests whether an member is a type def - :param member_elem: XML element for a class member - """ - try: - if member_elem.get('kind') == 'typedef': - return True - except: - pass - return False - - def isReimplementation(self, member_elem): - """ Tests whether an member is a reimplementation - :param member_elem: XML element for a class member - """ - - # use two different tests, as Doxygen will not detect reimplemented Qt methods - try: - if member_elem.find('reimplements') is not None: - return True - if ' override' in member_elem.find('argsstring').text: - return True - except: - pass - - return False - - def isDeprecated(self, member_elem): - """ Tests whether an member is deprecated - :param member_elem: XML element for a class member - """ - - # look for both Q_DECL_DEPRECATED and Doxygen deprecated tag - decl_deprecated = False - type_elem = member_elem.find('type') - try: - if 'Q_DECL_DEPRECATED' in type_elem.text: - decl_deprecated = True - except: - pass - - doxy_deprecated = False - try: - for p in member_elem.find('detaileddescription').getiterator('para'): - for s in p.getiterator('xrefsect'): - if s.find('xreftitle') is not None and 'Deprecated' in s.find('xreftitle').text: - doxy_deprecated = True - break - except: - assert 0, member_elem.find('definition').text - - if not decl_deprecated and not doxy_deprecated: - return False - - # only functions for now, but in future this should also apply for enums and variables - if member_elem.get('kind') in ('function', 'variable'): - assert decl_deprecated, 'Error: Missing Q_DECL_DEPRECATED for {}'.format(member_elem.find('definition').text) - assert doxy_deprecated, 'Error: Missing Doxygen deprecated tag for {}'.format(member_elem.find('definition').text) - - return True - - def memberIsDocumented(self, member_elem): - """ Tests whether an member has documentation - :param member_elem: XML element for a class member - """ - for doc_type in ('inbodydescription', 'briefdescription', 'detaileddescription'): - doc = member_elem.find(doc_type) - if doc is not None and list(doc): - return True - return False - - def waitServer(url, timeout=10): """ Wait for a server to be online and to respond HTTP errors are ignored