mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-22 00:06:12 -05:00
run Doxygen and doc coverage test on static Travis config
This commit is contained in:
parent
05d9f92d99
commit
281daee0ba
@ -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)
|
||||
|
@ -13,4 +13,4 @@
|
||||
# #
|
||||
###########################################################################
|
||||
|
||||
pip install autopep8
|
||||
pip install autopep8 nose2 mock termcolor
|
||||
|
@ -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}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,6 +21,7 @@
|
||||
.pydevproject
|
||||
/CMakeLists.txt.user
|
||||
/CMakeLists.txt.user.*
|
||||
.ci/travis/code_layout/scripts/qgisstyle*
|
||||
api_doc
|
||||
build*
|
||||
debian/*.debhelper
|
||||
|
13
.travis.yml
13
.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
|
||||
|
@ -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
|
||||
|
47
cmake/CreateQgsVersion.cmake
Normal file
47
cmake/CreateQgsVersion.cmake
Normal file
@ -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)
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
523
tests/src/python/doxygen_parser.py
Normal file
523
tests/src/python/doxygen_parser.py
Normal file
@ -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
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user