mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-17 00:04:02 -04:00
Add test for coverage of SIP bindings
Not perfect, but good for a quick warning if a new class or member has been added to the public API without Python bindings. The test only considers the name of members, since it seems to be impossible to test for the signature of a Python member. (So adding a new overloaded method without bindings will still unfortunately pass). You can avoid the test where bindings are not applicable: - for a whole class by placing "@note not available in Python bindings" in the class' Doxygen comments - or by placing the @note inside a member's Doxygen comments for a specific member Additionally, classes which aren't included in the API docs will not be tested.
This commit is contained in:
parent
7842391eba
commit
fc49f8dfa9
@ -69,6 +69,10 @@ class QgsPaintEffectRegistry
|
||||
|
||||
public:
|
||||
|
||||
/** Returns a reference to the singleton instance of the paint effect registry.
|
||||
*/
|
||||
static QgsPaintEffectRegistry* instance();
|
||||
|
||||
/** Returns the metadata for a specific effect.
|
||||
* @param name unique string name for paint effect class
|
||||
* @returns paint effect metadata if found, otherwise NULL
|
||||
|
@ -44,4 +44,7 @@ class QgsFieldComboBox : QComboBox
|
||||
|
||||
//! setField sets the currently selected field
|
||||
void setField( const QString& fieldName );
|
||||
|
||||
protected slots:
|
||||
void indexChanged( int i );
|
||||
};
|
||||
|
@ -154,6 +154,9 @@ class CORE_EXPORT QgsPaintEffectMetadata : public QgsPaintEffectAbstractMetadata
|
||||
class CORE_EXPORT QgsPaintEffectRegistry
|
||||
{
|
||||
public:
|
||||
|
||||
/** Returns a reference to the singleton instance of the paint effect registry.
|
||||
*/
|
||||
static QgsPaintEffectRegistry* instance();
|
||||
|
||||
/** Returns the metadata for a specific effect.
|
||||
|
@ -98,6 +98,10 @@ class GEOSInit
|
||||
|
||||
static GEOSInit geosinit;
|
||||
|
||||
/**
|
||||
* @brief Scoped GEOS pointer
|
||||
* @note not available in Python bindings
|
||||
*/
|
||||
class GEOSGeomScopedPtr
|
||||
{
|
||||
public:
|
||||
|
@ -18,6 +18,11 @@
|
||||
#include <QList>
|
||||
#include "rtree.hpp"
|
||||
|
||||
/**
|
||||
* \class pal::CostCalculator
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
|
||||
namespace pal
|
||||
{
|
||||
class Feats;
|
||||
@ -48,9 +53,11 @@ namespace pal
|
||||
/**
|
||||
* \brief Data structure to compute polygon's candidates costs
|
||||
*
|
||||
* eight segment from center of candidat to (rpx,rpy) points (0°, 45°, 90°, ..., 315°)
|
||||
* Eight segments from center of candidate to (rpx,rpy) points (0°, 45°, 90°, ..., 315°)
|
||||
* dist store the shortest square distance from the center to an object
|
||||
* ok[i] is the to true whether the corresponding dist[i] is set
|
||||
*
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class PolygonCostCalculator
|
||||
{
|
||||
|
@ -41,6 +41,11 @@
|
||||
|
||||
#include "qgslabelingenginev2.h"
|
||||
|
||||
/**
|
||||
* \class pal::LabelInfo
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
|
||||
namespace pal
|
||||
{
|
||||
/** Optional additional info about label (for curved labels) */
|
||||
@ -75,6 +80,8 @@ namespace pal
|
||||
|
||||
/**
|
||||
* \brief Main class to handle feature
|
||||
* \class pal::FeaturePart
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class CORE_EXPORT FeaturePart : public PointSet
|
||||
{
|
||||
|
@ -44,6 +44,8 @@ namespace pal
|
||||
|
||||
/**
|
||||
* \brief LabelPosition is a candidate feature label position
|
||||
* \class pal::LabelPosition
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class CORE_EXPORT LabelPosition : public PointSet
|
||||
{
|
||||
|
@ -48,11 +48,9 @@ namespace pal
|
||||
class LabelInfo;
|
||||
|
||||
/**
|
||||
* \brief A layer of spacial entites
|
||||
*
|
||||
* a layer is a bog of feature with some data which influence the labelling process
|
||||
*
|
||||
* \author Maxence Laurent (maxence _dot_ laurent _at_ heig-vd _dot_ ch)
|
||||
* \brief A set of features which influence the labelling process
|
||||
* \class pal::Layer
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class CORE_EXPORT Layer
|
||||
{
|
||||
|
@ -41,14 +41,6 @@
|
||||
|
||||
class QgsAbstractLabelProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* \section intro_sec Introduction
|
||||
*
|
||||
* Pal is a labelling library released under the GPLv3 license
|
||||
*
|
||||
*/
|
||||
|
||||
namespace pal
|
||||
{
|
||||
/** Get GEOS context handle to be used in all GEOS library calls with reentrant API */
|
||||
@ -101,12 +93,12 @@ namespace pal
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Pal main class.
|
||||
* \brief Main Pal labelling class
|
||||
*
|
||||
* A pal object will contains layers and global information such as which search method
|
||||
* will be used.
|
||||
*
|
||||
* \author Maxence Laurent (maxence _dot_ laurent _at_ heig-vd _dot_ ch)
|
||||
* \class pal::Pal
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class CORE_EXPORT Pal
|
||||
{
|
||||
|
@ -36,8 +36,11 @@ namespace pal
|
||||
{
|
||||
|
||||
/**
|
||||
* Summury of problem
|
||||
* \brief Summary statistics of labelling problem.
|
||||
* \class pal::PalStat
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
|
||||
class PalStat
|
||||
{
|
||||
|
||||
|
@ -56,7 +56,10 @@ namespace pal
|
||||
double length;
|
||||
} CHullBox;
|
||||
|
||||
|
||||
/**
|
||||
* \class pal::PointSet
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class CORE_EXPORT PointSet
|
||||
{
|
||||
friend class FeaturePart;
|
||||
|
@ -39,7 +39,10 @@
|
||||
|
||||
namespace pal
|
||||
{
|
||||
|
||||
/**
|
||||
* \class pal::PriorityQueue
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class PriorityQueue
|
||||
{
|
||||
|
||||
|
@ -41,6 +41,10 @@ namespace pal
|
||||
class LabelPosition;
|
||||
class Label;
|
||||
|
||||
/**
|
||||
* \class pal::Sol
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class Sol
|
||||
{
|
||||
public:
|
||||
@ -88,7 +92,9 @@ namespace pal
|
||||
} Chain;
|
||||
|
||||
/**
|
||||
* \brief Represent a problem
|
||||
* \brief Representation of a labeling problem
|
||||
* \class pal::Problem
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class CORE_EXPORT Problem
|
||||
{
|
||||
|
@ -46,6 +46,7 @@ namespace pal
|
||||
|
||||
/**
|
||||
* \brief For usage in problem solving algorithm
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
class Feats
|
||||
{
|
||||
|
@ -79,6 +79,8 @@ ENDIF (ENABLE_PGTEST)
|
||||
|
||||
IF (WITH_APIDOC)
|
||||
ADD_PYTHON_TEST(PyQgsDocCoverage test_qgsdoccoverage.py)
|
||||
#SIP coverage test relies on API doc parsing to identify members which should be in bindings
|
||||
ADD_PYTHON_TEST(PyQgsSipCoverage test_qgssipcoverage.py)
|
||||
ENDIF (WITH_APIDOC)
|
||||
|
||||
IF (WITH_SERVER)
|
||||
|
@ -13,16 +13,10 @@ __copyright__ = 'Copyright 2015, The QGIS Project'
|
||||
__revision__ = '$Format:%H$'
|
||||
|
||||
import os
|
||||
import glob
|
||||
|
||||
from utilities import (TestCase,
|
||||
unittest,
|
||||
printImportant)
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
printImportant,
|
||||
DoxygenParser)
|
||||
|
||||
from PyQt4.QtCore import qDebug
|
||||
|
||||
@ -33,146 +27,7 @@ from PyQt4.QtCore import qDebug
|
||||
# DON'T RAISE THIS THRESHOLD!!!
|
||||
# (changes which lower this threshold are welcomed though!)
|
||||
|
||||
ACCEPTABLE_MISSING_DOCS = 4024
|
||||
|
||||
|
||||
def elemIsDocumentableClass(elem):
|
||||
if not elem.get('kind') == 'class':
|
||||
return False
|
||||
|
||||
# public or protected classes should be documented
|
||||
return elem.get('prot') in ('public', 'protected')
|
||||
|
||||
|
||||
def memberSignature(elem):
|
||||
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 elemIsDocumentableMember(elem):
|
||||
if elem.get('kind') == 'variable':
|
||||
return False
|
||||
|
||||
# only public or protected members should be documented
|
||||
if not elem.get('prot') in ('public', 'protected'):
|
||||
return False
|
||||
|
||||
# ignore reimplemented methods
|
||||
# use two different tests, as doxygen will not detect reimplemented qt methods
|
||||
if elem.find('reimplements') is not None:
|
||||
return False
|
||||
args = elem.find('argsstring')
|
||||
if args is not None and args.text and ' override' in args.text:
|
||||
return False
|
||||
|
||||
# ignore destructor
|
||||
name = elem.find('name')
|
||||
try:
|
||||
if name.text and name.text.startswith('~'):
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
# ignore constructors with no arguments
|
||||
definition = elem.find('definition')
|
||||
argsstring = elem.find('argsstring')
|
||||
try:
|
||||
if definition.text == '{}::{}'.format(name.text, name.text) and argsstring.text == '()':
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
# ignore certain obvious operators
|
||||
try:
|
||||
if name.text in ('operator=', 'operator=='):
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
# ignore on_* slots
|
||||
try:
|
||||
if name.text.startswith('on_'):
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
# ignore deprecated members
|
||||
typeelem = elem.find('type')
|
||||
try:
|
||||
if typeelem.text and 'Q_DECL_DEPRECATED' in typeelem.text:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def memberIsDocumented(m):
|
||||
for doc_type in ('inbodydescription', 'briefdescription', 'detaileddescription'):
|
||||
doc = m.find(doc_type)
|
||||
if doc is not None and list(doc):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parseClassElem(e):
|
||||
documentable_members = 0
|
||||
documented_members = 0
|
||||
undocumented_members = []
|
||||
for m in e.getiterator('memberdef'):
|
||||
if elemIsDocumentableMember(m):
|
||||
documentable_members += 1
|
||||
if memberIsDocumented(m):
|
||||
documented_members += 1
|
||||
else:
|
||||
undocumented_members.append(memberSignature(m))
|
||||
return documentable_members, documented_members, undocumented_members
|
||||
|
||||
|
||||
def parseFile(f):
|
||||
documentable_members = 0
|
||||
documented_members = 0
|
||||
try:
|
||||
for event, elem in ET.iterparse(f):
|
||||
if event == 'end' and elem.tag == 'compounddef':
|
||||
if elemIsDocumentableClass(elem):
|
||||
members, documented, undocumented = parseClassElem(elem)
|
||||
documentable_members += members
|
||||
documented_members += documented
|
||||
if documented < members:
|
||||
print "Class {}, {}/{} members documented".format(elem.find('compoundname').text, documented, members)
|
||||
for u in undocumented:
|
||||
print ' Missing: {}'.format(u)
|
||||
print "\n"
|
||||
elem.clear()
|
||||
except ET.ParseError as e:
|
||||
# sometimes Doxygen generates malformed xml (eg 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)
|
||||
return documentable_members, documented_members
|
||||
|
||||
|
||||
def parseDocs(path):
|
||||
documentable_members = 0
|
||||
documented_members = 0
|
||||
for f in glob.glob(os.path.join(path, '*.xml')):
|
||||
members, documented = parseFile(f)
|
||||
documentable_members += members
|
||||
documented_members += documented
|
||||
|
||||
return documentable_members, documented_members
|
||||
ACCEPTABLE_MISSING_DOCS = 4020
|
||||
|
||||
|
||||
class TestQgsDocCoverage(TestCase):
|
||||
@ -181,17 +36,19 @@ class TestQgsDocCoverage(TestCase):
|
||||
print 'CTEST_FULL_OUTPUT'
|
||||
prefixPath = os.environ['QGIS_PREFIX_PATH']
|
||||
docPath = os.path.join(prefixPath, '..', 'doc', 'api', 'xml')
|
||||
parser = DoxygenParser(docPath)
|
||||
|
||||
documentable, documented = parseDocs(docPath)
|
||||
coverage = 100.0 * documented / documentable
|
||||
missing = documentable - documented
|
||||
coverage = 100.0 * parser.documented_members / parser.documentable_members
|
||||
missing = parser.documentable_members - parser.documented_members
|
||||
|
||||
print "---------------------------------"
|
||||
printImportant("{} total documentable members".format(documentable))
|
||||
printImportant("{} total contain valid documentation".format(documented))
|
||||
printImportant("{} total documentable members".format(parser.documentable_members))
|
||||
printImportant("{} total contain valid documentation".format(parser.documented_members))
|
||||
printImportant("Total documentation coverage {}%".format(coverage))
|
||||
printImportant("---------------------------------")
|
||||
printImportant("{} members missing documentation, out of {} allowed".format(missing, ACCEPTABLE_MISSING_DOCS))
|
||||
print "---------------------------------"
|
||||
print parser.undocumented_string
|
||||
|
||||
assert missing <= ACCEPTABLE_MISSING_DOCS, 'FAIL: new undocumented members have been introduced, please add documentation for these members'
|
||||
|
||||
|
111
tests/src/python/test_qgssipcoverage.py
Normal file
111
tests/src/python/test_qgssipcoverage.py
Normal file
@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""QGIS Unit tests for SIP binding coverage.
|
||||
|
||||
.. note:: 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__ = 'Nyall Dawson'
|
||||
__date__ = '15/10/2015'
|
||||
__copyright__ = 'Copyright 2015, The QGIS Project'
|
||||
# This will get replaced with a git SHA1 when you do a git archive
|
||||
__revision__ = '$Format:%H$'
|
||||
|
||||
import os
|
||||
from utilities import (TestCase,
|
||||
unittest,
|
||||
printImportant,
|
||||
DoxygenParser)
|
||||
|
||||
from PyQt4.QtCore import qDebug
|
||||
|
||||
#Import all the things!
|
||||
from qgis.analysis import *
|
||||
from qgis.core import *
|
||||
from qgis.gui import *
|
||||
from qgis.networkanalysis import *
|
||||
try:
|
||||
from qgis.server import *
|
||||
except:
|
||||
pass
|
||||
|
||||
# BINDING THRESHOLD
|
||||
#
|
||||
# The minimum number of unbound functions in QGIS api
|
||||
#
|
||||
# DON'T RAISE THIS THRESHOLD!!!
|
||||
# (changes which lower this threshold are welcomed though!)
|
||||
|
||||
ACCEPTABLE_MISSING_CLASSES = 198
|
||||
ACCEPTABLE_MISSING_MEMBERS = 530
|
||||
|
||||
|
||||
class TestQgsSipCoverage(TestCase):
|
||||
|
||||
def testCoverage(self):
|
||||
print 'CTEST_FULL_OUTPUT'
|
||||
prefixPath = os.environ['QGIS_PREFIX_PATH']
|
||||
docPath = os.path.join(prefixPath, '..', 'doc', 'api', 'xml')
|
||||
parser = DoxygenParser(docPath)
|
||||
|
||||
#first look for objects without any bindings
|
||||
objects = set([m[0] for m in parser.bindable_members])
|
||||
missing_objects = []
|
||||
bound_objects = {}
|
||||
for o in objects:
|
||||
try:
|
||||
bound_objects[o] = globals()[o]
|
||||
except:
|
||||
missing_objects.append(o)
|
||||
|
||||
missing_objects.sort()
|
||||
|
||||
missing_count = len(missing_objects)
|
||||
present_count = len(objects) - missing_count
|
||||
coverage = 100.0 * present_count / len(objects)
|
||||
|
||||
print "---------------------------------"
|
||||
printImportant("{} total bindable classes".format(len(objects)))
|
||||
printImportant("{} total have bindings".format(present_count))
|
||||
printImportant("Binding coverage by classes {}%".format(coverage))
|
||||
printImportant("---------------------------------")
|
||||
printImportant("{} classes missing bindings, out of {} allowed".format(missing_count, ACCEPTABLE_MISSING_CLASSES))
|
||||
print "---------------------------------"
|
||||
|
||||
assert missing_count <= ACCEPTABLE_MISSING_CLASSES, """\n\nFAIL: new unbound classes have been introduced, please add SIP bindings for these classes
|
||||
If these classes are not suitable for the Python bindings, please add the Doxygen tag
|
||||
"@note not available in Python bindings" to the CLASS Doxygen comments"""
|
||||
|
||||
#next check for individual members
|
||||
parser.bindable_members.sort()
|
||||
missing_members = []
|
||||
for m in parser.bindable_members:
|
||||
if m[0] in bound_objects:
|
||||
obj = bound_objects[m[0]]
|
||||
if not hasattr(obj, m[1]):
|
||||
missing_members.append('{}.{}'.format(m[0], m[1]))
|
||||
|
||||
missing_members.sort()
|
||||
missing_count = len(missing_members)
|
||||
present_count = len(parser.bindable_members) - missing_count
|
||||
coverage = 100.0 * present_count / len(parser.bindable_members)
|
||||
|
||||
print "---------------------------------"
|
||||
printImportant("{} total bindable members".format(len(parser.bindable_members)))
|
||||
printImportant("{} total have bindings".format(present_count))
|
||||
printImportant("Binding coverage by members {}%".format(coverage))
|
||||
printImportant("---------------------------------")
|
||||
printImportant("{} members missing bindings, out of {} allowed".format(missing_count, ACCEPTABLE_MISSING_MEMBERS))
|
||||
print "---------------------------------"
|
||||
print 'Missing classes:\n {}'.format('\n '.join(missing_objects))
|
||||
print "---------------------------------"
|
||||
print 'Missing members:\n {}'.format('\n '.join(missing_members))
|
||||
|
||||
assert missing_count <= ACCEPTABLE_MISSING_MEMBERS, """\n\nFAIL: new unbound members have been introduced, please add SIP bindings for these members
|
||||
If these members are not suitable for the Python bindings, please add the Doxygen tag
|
||||
"@note not available in Python bindings" to the MEMBER Doxygen comments"""
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -14,6 +14,7 @@ __revision__ = '$Format:%H$'
|
||||
import qgis
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import platform
|
||||
import tempfile
|
||||
|
||||
@ -35,6 +36,10 @@ from qgis_interface import QgisInterface
|
||||
import hashlib
|
||||
import re
|
||||
from itertools import izip
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import webbrowser
|
||||
import subprocess
|
||||
@ -401,3 +406,311 @@ def printImportant(info):
|
||||
print(info)
|
||||
with open(os.path.join(tempfile.gettempdir(), 'ctest-important.log'), 'a+') as f:
|
||||
f.write(u'{}\n'.format(info))
|
||||
|
||||
|
||||
class DoxygenParser():
|
||||
|
||||
"""
|
||||
Parses the XML files generated by Doxygen which describe the API docs
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
Initializes the parser.
|
||||
:param path: Path to Doxygen XML output
|
||||
"""
|
||||
self.documentable_members = 0
|
||||
self.documented_members = 0
|
||||
self.undocumented_string = ''
|
||||
self.bindable_members = []
|
||||
self.parseFiles(path)
|
||||
|
||||
def parseFiles(self, path):
|
||||
""" Parses all the Doxygen XML files in a folder
|
||||
:param path: Path to Doxygen XML output
|
||||
"""
|
||||
for f in glob.glob(os.path.join(path, '*.xml')):
|
||||
self.parseFile(f)
|
||||
|
||||
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 = self.parseClassElem(elem)
|
||||
documentable_members += members
|
||||
documented_members += documented
|
||||
class_name = elem.find('compoundname').text
|
||||
if documented < members:
|
||||
self.undocumented_string += "Class {}, {}/{} members documented\n".format(class_name, documented, members)
|
||||
for u in undocumented:
|
||||
self.undocumented_string += ' Missing: {}\n'.format(u)
|
||||
self.undocumented_string += "\n"
|
||||
|
||||
#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 (eg 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 'not available in python bindings' note in class docs
|
||||
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 'not available in python bindings' 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 = []
|
||||
bindable_members = []
|
||||
# loop through all members
|
||||
for m in e.getiterator('memberdef'):
|
||||
if self.elemIsBindableMember(m):
|
||||
bindable_member = [e.find('compoundname').text, m.find('name').text]
|
||||
if not bindable_member 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.append(self.memberSignature(m))
|
||||
return documentable_members, documented_members, undocumented_members, bindable_members
|
||||
|
||||
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
|
||||
|
||||
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 '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
|
||||
|
||||
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 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=='):
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
# ignore on_* slots
|
||||
try:
|
||||
if name.text.startswith('on_'):
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
# ignore deprecated members
|
||||
typeelem = elem.find('type')
|
||||
try:
|
||||
if typeelem.text and 'Q_DECL_DEPRECATED' in typeelem.text:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
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 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 definition == '{}::{}'.format(name, name):
|
||||
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 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 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user