Skip to content

Commit fc49f8d

Browse files
committedOct 27, 2015
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.
1 parent 7842391 commit fc49f8d

18 files changed

+493
-174
lines changed
 

‎python/core/effects/qgspainteffectregistry.sip

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ class QgsPaintEffectRegistry
6969

7070
public:
7171

72+
/** Returns a reference to the singleton instance of the paint effect registry.
73+
*/
74+
static QgsPaintEffectRegistry* instance();
75+
7276
/** Returns the metadata for a specific effect.
7377
* @param name unique string name for paint effect class
7478
* @returns paint effect metadata if found, otherwise NULL

‎python/gui/qgsfieldcombobox.sip

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,7 @@ class QgsFieldComboBox : QComboBox
4444

4545
//! setField sets the currently selected field
4646
void setField( const QString& fieldName );
47+
48+
protected slots:
49+
void indexChanged( int i );
4750
};

‎src/core/effects/qgspainteffectregistry.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ class CORE_EXPORT QgsPaintEffectMetadata : public QgsPaintEffectAbstractMetadata
154154
class CORE_EXPORT QgsPaintEffectRegistry
155155
{
156156
public:
157+
158+
/** Returns a reference to the singleton instance of the paint effect registry.
159+
*/
157160
static QgsPaintEffectRegistry* instance();
158161

159162
/** Returns the metadata for a specific effect.

‎src/core/geometry/qgsgeos.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ class GEOSInit
9898

9999
static GEOSInit geosinit;
100100

101+
/**
102+
* @brief Scoped GEOS pointer
103+
* @note not available in Python bindings
104+
*/
101105
class GEOSGeomScopedPtr
102106
{
103107
public:

‎src/core/pal/costcalculator.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
#include <QList>
1919
#include "rtree.hpp"
2020

21+
/**
22+
* \class pal::CostCalculator
23+
* \note not available in Python bindings
24+
*/
25+
2126
namespace pal
2227
{
2328
class Feats;
@@ -48,9 +53,11 @@ namespace pal
4853
/**
4954
* \brief Data structure to compute polygon's candidates costs
5055
*
51-
* eight segment from center of candidat to (rpx,rpy) points (0°, 45°, 90°, ..., 315°)
56+
* Eight segments from center of candidate to (rpx,rpy) points (0°, 45°, 90°, ..., 315°)
5257
* dist store the shortest square distance from the center to an object
5358
* ok[i] is the to true whether the corresponding dist[i] is set
59+
*
60+
* \note not available in Python bindings
5461
*/
5562
class PolygonCostCalculator
5663
{

‎src/core/pal/feature.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141

4242
#include "qgslabelingenginev2.h"
4343

44+
/**
45+
* \class pal::LabelInfo
46+
* \note not available in Python bindings
47+
*/
48+
4449
namespace pal
4550
{
4651
/** Optional additional info about label (for curved labels) */
@@ -75,6 +80,8 @@ namespace pal
7580

7681
/**
7782
* \brief Main class to handle feature
83+
* \class pal::FeaturePart
84+
* \note not available in Python bindings
7885
*/
7986
class CORE_EXPORT FeaturePart : public PointSet
8087
{

‎src/core/pal/labelposition.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ namespace pal
4444

4545
/**
4646
* \brief LabelPosition is a candidate feature label position
47+
* \class pal::LabelPosition
48+
* \note not available in Python bindings
4749
*/
4850
class CORE_EXPORT LabelPosition : public PointSet
4951
{

‎src/core/pal/layer.h

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,9 @@ namespace pal
4848
class LabelInfo;
4949

5050
/**
51-
* \brief A layer of spacial entites
52-
*
53-
* a layer is a bog of feature with some data which influence the labelling process
54-
*
55-
* \author Maxence Laurent (maxence _dot_ laurent _at_ heig-vd _dot_ ch)
51+
* \brief A set of features which influence the labelling process
52+
* \class pal::Layer
53+
* \note not available in Python bindings
5654
*/
5755
class CORE_EXPORT Layer
5856
{

‎src/core/pal/pal.h

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,6 @@
4141

4242
class QgsAbstractLabelProvider;
4343

44-
/**
45-
*
46-
* \section intro_sec Introduction
47-
*
48-
* Pal is a labelling library released under the GPLv3 license
49-
*
50-
*/
51-
5244
namespace pal
5345
{
5446
/** Get GEOS context handle to be used in all GEOS library calls with reentrant API */
@@ -101,12 +93,12 @@ namespace pal
10193
};
10294

10395
/**
104-
* \brief Pal main class.
96+
* \brief Main Pal labelling class
10597
*
10698
* A pal object will contains layers and global information such as which search method
10799
* will be used.
108-
*
109-
* \author Maxence Laurent (maxence _dot_ laurent _at_ heig-vd _dot_ ch)
100+
* \class pal::Pal
101+
* \note not available in Python bindings
110102
*/
111103
class CORE_EXPORT Pal
112104
{

‎src/core/pal/palstat.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ namespace pal
3636
{
3737

3838
/**
39-
* Summury of problem
39+
* \brief Summary statistics of labelling problem.
40+
* \class pal::PalStat
41+
* \note not available in Python bindings
4042
*/
43+
4144
class PalStat
4245
{
4346

‎src/core/pal/pointset.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ namespace pal
5656
double length;
5757
} CHullBox;
5858

59-
59+
/**
60+
* \class pal::PointSet
61+
* \note not available in Python bindings
62+
*/
6063
class CORE_EXPORT PointSet
6164
{
6265
friend class FeaturePart;

‎src/core/pal/priorityqueue.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@
3939

4040
namespace pal
4141
{
42-
42+
/**
43+
* \class pal::PriorityQueue
44+
* \note not available in Python bindings
45+
*/
4346
class PriorityQueue
4447
{
4548

‎src/core/pal/problem.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ namespace pal
4141
class LabelPosition;
4242
class Label;
4343

44+
/**
45+
* \class pal::Sol
46+
* \note not available in Python bindings
47+
*/
4448
class Sol
4549
{
4650
public:
@@ -88,7 +92,9 @@ namespace pal
8892
} Chain;
8993

9094
/**
91-
* \brief Represent a problem
95+
* \brief Representation of a labeling problem
96+
* \class pal::Problem
97+
* \note not available in Python bindings
9298
*/
9399
class CORE_EXPORT Problem
94100
{

‎src/core/pal/util.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ namespace pal
4646

4747
/**
4848
* \brief For usage in problem solving algorithm
49+
* \note not available in Python bindings
4950
*/
5051
class Feats
5152
{

‎tests/src/python/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ ENDIF (ENABLE_PGTEST)
7979

8080
IF (WITH_APIDOC)
8181
ADD_PYTHON_TEST(PyQgsDocCoverage test_qgsdoccoverage.py)
82+
#SIP coverage test relies on API doc parsing to identify members which should be in bindings
83+
ADD_PYTHON_TEST(PyQgsSipCoverage test_qgssipcoverage.py)
8284
ENDIF (WITH_APIDOC)
8385

8486
IF (WITH_SERVER)

‎tests/src/python/test_qgsdoccoverage.py

Lines changed: 10 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,10 @@
1313
__revision__ = '$Format:%H$'
1414

1515
import os
16-
import glob
17-
1816
from utilities import (TestCase,
1917
unittest,
20-
printImportant)
21-
22-
try:
23-
import xml.etree.cElementTree as ET
24-
except ImportError:
25-
import xml.etree.ElementTree as ET
18+
printImportant,
19+
DoxygenParser)
2620

2721
from PyQt4.QtCore import qDebug
2822

@@ -33,146 +27,7 @@
3327
# DON'T RAISE THIS THRESHOLD!!!
3428
# (changes which lower this threshold are welcomed though!)
3529

36-
ACCEPTABLE_MISSING_DOCS = 4024
37-
38-
39-
def elemIsDocumentableClass(elem):
40-
if not elem.get('kind') == 'class':
41-
return False
42-
43-
# public or protected classes should be documented
44-
return elem.get('prot') in ('public', 'protected')
45-
46-
47-
def memberSignature(elem):
48-
a = elem.find('argsstring')
49-
try:
50-
if a is not None:
51-
return elem.find('name').text + a.text
52-
else:
53-
return elem.find('name').text
54-
except:
55-
return None
56-
57-
58-
def elemIsDocumentableMember(elem):
59-
if elem.get('kind') == 'variable':
60-
return False
61-
62-
# only public or protected members should be documented
63-
if not elem.get('prot') in ('public', 'protected'):
64-
return False
65-
66-
# ignore reimplemented methods
67-
# use two different tests, as doxygen will not detect reimplemented qt methods
68-
if elem.find('reimplements') is not None:
69-
return False
70-
args = elem.find('argsstring')
71-
if args is not None and args.text and ' override' in args.text:
72-
return False
73-
74-
# ignore destructor
75-
name = elem.find('name')
76-
try:
77-
if name.text and name.text.startswith('~'):
78-
return False
79-
except:
80-
pass
81-
82-
# ignore constructors with no arguments
83-
definition = elem.find('definition')
84-
argsstring = elem.find('argsstring')
85-
try:
86-
if definition.text == '{}::{}'.format(name.text, name.text) and argsstring.text == '()':
87-
return False
88-
except:
89-
pass
90-
91-
# ignore certain obvious operators
92-
try:
93-
if name.text in ('operator=', 'operator=='):
94-
return False
95-
except:
96-
pass
97-
98-
# ignore on_* slots
99-
try:
100-
if name.text.startswith('on_'):
101-
return False
102-
except:
103-
pass
104-
105-
# ignore deprecated members
106-
typeelem = elem.find('type')
107-
try:
108-
if typeelem.text and 'Q_DECL_DEPRECATED' in typeelem.text:
109-
return False
110-
except:
111-
pass
112-
113-
return True
114-
115-
116-
def memberIsDocumented(m):
117-
for doc_type in ('inbodydescription', 'briefdescription', 'detaileddescription'):
118-
doc = m.find(doc_type)
119-
if doc is not None and list(doc):
120-
return True
121-
return False
122-
123-
124-
def parseClassElem(e):
125-
documentable_members = 0
126-
documented_members = 0
127-
undocumented_members = []
128-
for m in e.getiterator('memberdef'):
129-
if elemIsDocumentableMember(m):
130-
documentable_members += 1
131-
if memberIsDocumented(m):
132-
documented_members += 1
133-
else:
134-
undocumented_members.append(memberSignature(m))
135-
return documentable_members, documented_members, undocumented_members
136-
137-
138-
def parseFile(f):
139-
documentable_members = 0
140-
documented_members = 0
141-
try:
142-
for event, elem in ET.iterparse(f):
143-
if event == 'end' and elem.tag == 'compounddef':
144-
if elemIsDocumentableClass(elem):
145-
members, documented, undocumented = parseClassElem(elem)
146-
documentable_members += members
147-
documented_members += documented
148-
if documented < members:
149-
print "Class {}, {}/{} members documented".format(elem.find('compoundname').text, documented, members)
150-
for u in undocumented:
151-
print ' Missing: {}'.format(u)
152-
print "\n"
153-
elem.clear()
154-
except ET.ParseError as e:
155-
# sometimes Doxygen generates malformed xml (eg for < and > operators)
156-
line_num, col = e.position
157-
with open(f, 'r') as xml_file:
158-
for i, l in enumerate(xml_file):
159-
if i == line_num - 1:
160-
line = l
161-
break
162-
caret = '{:=>{}}'.format('^', col)
163-
print 'ParseError in {}\n{}\n{}\n{}'.format(f, e, line, caret)
164-
return documentable_members, documented_members
165-
166-
167-
def parseDocs(path):
168-
documentable_members = 0
169-
documented_members = 0
170-
for f in glob.glob(os.path.join(path, '*.xml')):
171-
members, documented = parseFile(f)
172-
documentable_members += members
173-
documented_members += documented
174-
175-
return documentable_members, documented_members
30+
ACCEPTABLE_MISSING_DOCS = 4020
17631

17732

17833
class TestQgsDocCoverage(TestCase):
@@ -181,17 +36,19 @@ def testCoverage(self):
18136
print 'CTEST_FULL_OUTPUT'
18237
prefixPath = os.environ['QGIS_PREFIX_PATH']
18338
docPath = os.path.join(prefixPath, '..', 'doc', 'api', 'xml')
39+
parser = DoxygenParser(docPath)
18440

185-
documentable, documented = parseDocs(docPath)
186-
coverage = 100.0 * documented / documentable
187-
missing = documentable - documented
41+
coverage = 100.0 * parser.documented_members / parser.documentable_members
42+
missing = parser.documentable_members - parser.documented_members
18843

18944
print "---------------------------------"
190-
printImportant("{} total documentable members".format(documentable))
191-
printImportant("{} total contain valid documentation".format(documented))
45+
printImportant("{} total documentable members".format(parser.documentable_members))
46+
printImportant("{} total contain valid documentation".format(parser.documented_members))
19247
printImportant("Total documentation coverage {}%".format(coverage))
19348
printImportant("---------------------------------")
19449
printImportant("{} members missing documentation, out of {} allowed".format(missing, ACCEPTABLE_MISSING_DOCS))
50+
print "---------------------------------"
51+
print parser.undocumented_string
19552

19653
assert missing <= ACCEPTABLE_MISSING_DOCS, 'FAIL: new undocumented members have been introduced, please add documentation for these members'
19754

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for SIP binding coverage.
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Nyall Dawson'
10+
__date__ = '15/10/2015'
11+
__copyright__ = 'Copyright 2015, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import os
16+
from utilities import (TestCase,
17+
unittest,
18+
printImportant,
19+
DoxygenParser)
20+
21+
from PyQt4.QtCore import qDebug
22+
23+
#Import all the things!
24+
from qgis.analysis import *
25+
from qgis.core import *
26+
from qgis.gui import *
27+
from qgis.networkanalysis import *
28+
try:
29+
from qgis.server import *
30+
except:
31+
pass
32+
33+
# BINDING THRESHOLD
34+
#
35+
# The minimum number of unbound functions in QGIS api
36+
#
37+
# DON'T RAISE THIS THRESHOLD!!!
38+
# (changes which lower this threshold are welcomed though!)
39+
40+
ACCEPTABLE_MISSING_CLASSES = 198
41+
ACCEPTABLE_MISSING_MEMBERS = 530
42+
43+
44+
class TestQgsSipCoverage(TestCase):
45+
46+
def testCoverage(self):
47+
print 'CTEST_FULL_OUTPUT'
48+
prefixPath = os.environ['QGIS_PREFIX_PATH']
49+
docPath = os.path.join(prefixPath, '..', 'doc', 'api', 'xml')
50+
parser = DoxygenParser(docPath)
51+
52+
#first look for objects without any bindings
53+
objects = set([m[0] for m in parser.bindable_members])
54+
missing_objects = []
55+
bound_objects = {}
56+
for o in objects:
57+
try:
58+
bound_objects[o] = globals()[o]
59+
except:
60+
missing_objects.append(o)
61+
62+
missing_objects.sort()
63+
64+
missing_count = len(missing_objects)
65+
present_count = len(objects) - missing_count
66+
coverage = 100.0 * present_count / len(objects)
67+
68+
print "---------------------------------"
69+
printImportant("{} total bindable classes".format(len(objects)))
70+
printImportant("{} total have bindings".format(present_count))
71+
printImportant("Binding coverage by classes {}%".format(coverage))
72+
printImportant("---------------------------------")
73+
printImportant("{} classes missing bindings, out of {} allowed".format(missing_count, ACCEPTABLE_MISSING_CLASSES))
74+
print "---------------------------------"
75+
76+
assert missing_count <= ACCEPTABLE_MISSING_CLASSES, """\n\nFAIL: new unbound classes have been introduced, please add SIP bindings for these classes
77+
If these classes are not suitable for the Python bindings, please add the Doxygen tag
78+
"@note not available in Python bindings" to the CLASS Doxygen comments"""
79+
80+
#next check for individual members
81+
parser.bindable_members.sort()
82+
missing_members = []
83+
for m in parser.bindable_members:
84+
if m[0] in bound_objects:
85+
obj = bound_objects[m[0]]
86+
if not hasattr(obj, m[1]):
87+
missing_members.append('{}.{}'.format(m[0], m[1]))
88+
89+
missing_members.sort()
90+
missing_count = len(missing_members)
91+
present_count = len(parser.bindable_members) - missing_count
92+
coverage = 100.0 * present_count / len(parser.bindable_members)
93+
94+
print "---------------------------------"
95+
printImportant("{} total bindable members".format(len(parser.bindable_members)))
96+
printImportant("{} total have bindings".format(present_count))
97+
printImportant("Binding coverage by members {}%".format(coverage))
98+
printImportant("---------------------------------")
99+
printImportant("{} members missing bindings, out of {} allowed".format(missing_count, ACCEPTABLE_MISSING_MEMBERS))
100+
print "---------------------------------"
101+
print 'Missing classes:\n {}'.format('\n '.join(missing_objects))
102+
print "---------------------------------"
103+
print 'Missing members:\n {}'.format('\n '.join(missing_members))
104+
105+
assert missing_count <= ACCEPTABLE_MISSING_MEMBERS, """\n\nFAIL: new unbound members have been introduced, please add SIP bindings for these members
106+
If these members are not suitable for the Python bindings, please add the Doxygen tag
107+
"@note not available in Python bindings" to the MEMBER Doxygen comments"""
108+
109+
110+
if __name__ == '__main__':
111+
unittest.main()

‎tests/src/python/utilities.py

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import qgis
1515
import os
1616
import sys
17+
import glob
1718
import platform
1819
import tempfile
1920

@@ -35,6 +36,10 @@
3536
import hashlib
3637
import re
3738
from itertools import izip
39+
try:
40+
import xml.etree.cElementTree as ET
41+
except ImportError:
42+
import xml.etree.ElementTree as ET
3843

3944
import webbrowser
4045
import subprocess
@@ -401,3 +406,311 @@ def printImportant(info):
401406
print(info)
402407
with open(os.path.join(tempfile.gettempdir(), 'ctest-important.log'), 'a+') as f:
403408
f.write(u'{}\n'.format(info))
409+
410+
411+
class DoxygenParser():
412+
413+
"""
414+
Parses the XML files generated by Doxygen which describe the API docs
415+
"""
416+
417+
def __init__(self, path):
418+
"""
419+
Initializes the parser.
420+
:param path: Path to Doxygen XML output
421+
"""
422+
self.documentable_members = 0
423+
self.documented_members = 0
424+
self.undocumented_string = ''
425+
self.bindable_members = []
426+
self.parseFiles(path)
427+
428+
def parseFiles(self, path):
429+
""" Parses all the Doxygen XML files in a folder
430+
:param path: Path to Doxygen XML output
431+
"""
432+
for f in glob.glob(os.path.join(path, '*.xml')):
433+
self.parseFile(f)
434+
435+
def parseFile(self, f):
436+
""" Parses a single Doxygen XML file
437+
:param f: XML file path
438+
"""
439+
documentable_members = 0
440+
documented_members = 0
441+
442+
#Wrap everything in a try, as sometimes Doxygen XML is malformed
443+
try:
444+
for event, elem in ET.iterparse(f):
445+
if event == 'end' and elem.tag == 'compounddef':
446+
if self.elemIsPublicClass(elem):
447+
#store documentation status
448+
members, documented, undocumented, bindable = self.parseClassElem(elem)
449+
documentable_members += members
450+
documented_members += documented
451+
class_name = elem.find('compoundname').text
452+
if documented < members:
453+
self.undocumented_string += "Class {}, {}/{} members documented\n".format(class_name, documented, members)
454+
for u in undocumented:
455+
self.undocumented_string += ' Missing: {}\n'.format(u)
456+
self.undocumented_string += "\n"
457+
458+
#store bindable members
459+
if self.classElemIsBindable(elem):
460+
for m in bindable:
461+
self.bindable_members.append(m)
462+
463+
elem.clear()
464+
except ET.ParseError as e:
465+
# sometimes Doxygen generates malformed xml (eg for < and > operators)
466+
line_num, col = e.position
467+
with open(f, 'r') as xml_file:
468+
for i, l in enumerate(xml_file):
469+
if i == line_num - 1:
470+
line = l
471+
break
472+
caret = '{:=>{}}'.format('^', col)
473+
print 'ParseError in {}\n{}\n{}\n{}'.format(f, e, line, caret)
474+
475+
self.documentable_members += documentable_members
476+
self.documented_members += documented_members
477+
478+
def elemIsPublicClass(self, elem):
479+
""" Tests whether an XML element corresponds to a public (or protected) class
480+
:param elem: XML element
481+
"""
482+
483+
# only looking for classes
484+
if not elem.get('kind') == 'class':
485+
return False
486+
487+
# only looking for public or protected classes
488+
return elem.get('prot') in ('public', 'protected')
489+
490+
def classElemIsBindable(self, elem):
491+
""" Tests whether a class should have SIP bindings
492+
:param elem: XML element corresponding to a class
493+
"""
494+
try:
495+
#check for 'not available in python bindings' note in class docs
496+
detailed_sec = elem.find('detaileddescription')
497+
for p in detailed_sec.getiterator('para'):
498+
for s in p.getiterator('simplesect'):
499+
for ps in s.getiterator('para'):
500+
if 'not available in python bindings' in ps.text.lower():
501+
return False
502+
return True
503+
except:
504+
return True
505+
506+
def parseClassElem(self, e):
507+
""" Parses an XML element corresponding to a Doxygen class
508+
:param e: XML element
509+
"""
510+
documentable_members = 0
511+
documented_members = 0
512+
undocumented_members = []
513+
bindable_members = []
514+
# loop through all members
515+
for m in e.getiterator('memberdef'):
516+
if self.elemIsBindableMember(m):
517+
bindable_member = [e.find('compoundname').text, m.find('name').text]
518+
if not bindable_member in bindable_members:
519+
bindable_members.append(bindable_member)
520+
if self.elemIsDocumentableMember(m):
521+
documentable_members += 1
522+
if self.memberIsDocumented(m):
523+
documented_members += 1
524+
else:
525+
undocumented_members.append(self.memberSignature(m))
526+
return documentable_members, documented_members, undocumented_members, bindable_members
527+
528+
def memberSignature(self, elem):
529+
""" Returns the signature for a member
530+
:param elem: XML element for a class member
531+
"""
532+
a = elem.find('argsstring')
533+
try:
534+
if a is not None:
535+
return elem.find('name').text + a.text
536+
else:
537+
return elem.find('name').text
538+
except:
539+
return None
540+
541+
def elemIsBindableMember(self, elem):
542+
""" Tests whether an member should be included in SIP bindings
543+
:param elem: XML element for a class member
544+
"""
545+
546+
# only public or protected members are bindable
547+
if not self.visibility(elem) in ('public', 'protected'):
548+
return False
549+
550+
if self.isVariable(elem) and self.visibility(elem) == 'protected':
551+
#protected variables can't be bound in SIP
552+
return False
553+
554+
#check for members with special python doc notes (probably 'not available' or renamed methods, either way
555+
#they should be safe to ignore as obviously some consideration has been given to Python bindings)
556+
try:
557+
detailed_sec = elem.find('detaileddescription')
558+
for p in detailed_sec.getiterator('para'):
559+
for s in p.getiterator('simplesect'):
560+
for ps in s.getiterator('para'):
561+
if 'python' in ps.text.lower():
562+
return False
563+
except:
564+
pass
565+
566+
# ignore constructors and destructor, can't test for these
567+
if self.isDestructor(elem) or self.isConstructor(elem):
568+
return False
569+
570+
# ignore operators, also can't test
571+
if self.isOperator(elem):
572+
return False
573+
574+
return True
575+
576+
def elemIsDocumentableMember(self, elem):
577+
""" Tests whether an member should be included in Doxygen docs
578+
:param elem: XML element for a class member
579+
"""
580+
581+
# ignore variables (for now, eventually public/protected variables should be documented)
582+
if self.isVariable(elem):
583+
return False
584+
585+
# only public or protected members should be documented
586+
if not self.visibility(elem) in ('public', 'protected'):
587+
return False
588+
589+
# ignore reimplemented methods
590+
if self.isReimplementation(elem):
591+
return False
592+
593+
# ignore destructor
594+
if self.isDestructor(elem):
595+
return False
596+
597+
# ignore constructors with no arguments
598+
if self.isConstructor(elem):
599+
try:
600+
if elem.find('argsstring').text == '()':
601+
return False
602+
except:
603+
pass
604+
605+
name = elem.find('name')
606+
607+
# ignore certain obvious operators
608+
try:
609+
if name.text in ('operator=', 'operator=='):
610+
return False
611+
except:
612+
pass
613+
614+
# ignore on_* slots
615+
try:
616+
if name.text.startswith('on_'):
617+
return False
618+
except:
619+
pass
620+
621+
# ignore deprecated members
622+
typeelem = elem.find('type')
623+
try:
624+
if typeelem.text and 'Q_DECL_DEPRECATED' in typeelem.text:
625+
return False
626+
except:
627+
pass
628+
629+
return True
630+
631+
def visibility(self, elem):
632+
""" Returns the visibility of a class or member
633+
:param elem: XML element for a class or member
634+
"""
635+
try:
636+
return elem.get('prot')
637+
except:
638+
return ''
639+
640+
def isVariable(self, member_elem):
641+
""" Tests whether an member is a variable
642+
:param member_elem: XML element for a class member
643+
"""
644+
try:
645+
if member_elem.get('kind') == 'variable':
646+
return True
647+
except:
648+
pass
649+
650+
return False
651+
652+
def isDestructor(self, member_elem):
653+
""" Tests whether an member is a destructor
654+
:param member_elem: XML element for a class member
655+
"""
656+
try:
657+
name = member_elem.find('name').text
658+
if name.startswith('~'):
659+
#destructor
660+
return True
661+
except:
662+
pass
663+
return False
664+
665+
def isConstructor(self, member_elem):
666+
""" Tests whether an member is a constructor
667+
:param member_elem: XML element for a class member
668+
"""
669+
try:
670+
definition = member_elem.find('definition').text
671+
name = member_elem.find('name').text
672+
if definition == '{}::{}'.format(name, name):
673+
return True
674+
except:
675+
pass
676+
677+
return False
678+
679+
def isOperator(self, member_elem):
680+
""" Tests whether an member is an operator
681+
:param member_elem: XML element for a class member
682+
"""
683+
try:
684+
name = member_elem.find('name').text
685+
if re.match('^operator\W.*', name):
686+
return True
687+
except:
688+
pass
689+
690+
return False
691+
692+
def isReimplementation(self, member_elem):
693+
""" Tests whether an member is a reimplementation
694+
:param member_elem: XML element for a class member
695+
"""
696+
697+
# use two different tests, as Doxygen will not detect reimplemented Qt methods
698+
try:
699+
if member_elem.find('reimplements') is not None:
700+
return True
701+
if ' override' in member_elem.find('argsstring').text:
702+
return True
703+
except:
704+
pass
705+
706+
return False
707+
708+
def memberIsDocumented(self, member_elem):
709+
""" Tests whether an member has documentation
710+
:param member_elem: XML element for a class member
711+
"""
712+
for doc_type in ('inbodydescription', 'briefdescription', 'detaileddescription'):
713+
doc = member_elem.find(doc_type)
714+
if doc is not None and list(doc):
715+
return True
716+
return False

0 commit comments

Comments
 (0)
Failed to load comments.