From 03bff134bfab28b628ff16719e0c0578122e62f0 Mon Sep 17 00:00:00 2001 From: Colin Marquardt Date: Tue, 19 Sep 2023 09:00:04 +0200 Subject: [PATCH] Improve docstrings, add API section to Sphinx (#112) --- .gitignore | 1 + README.rst | 12 +-- docs/Makefile | 4 +- docs/api/inheritance_diagram.rst | 5 ++ docs/conf.py | 15 +++- docs/index.rst | 14 +--- junitparser/cli.py | 6 +- junitparser/junitparser.py | 128 +++++++++++++++---------------- junitparser/xunit2.py | 12 +-- 9 files changed, 102 insertions(+), 95 deletions(-) create mode 100644 docs/api/inheritance_diagram.rst diff --git a/.gitignore b/.gitignore index a14cab2..4b56807 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ coverage.xml # Sphinx documentation docs/_build/ +docs/api # PyBuilder target/ diff --git a/README.rst b/README.rst index f4c56df..69ee720 100644 --- a/README.rst +++ b/README.rst @@ -19,14 +19,14 @@ Features * Merge test result xml files. * Specify xml parser. For example you can use lxml to speed things up. * Invoke from command line, or `python -m junitparser` -* Python 2 and 3 support (As of Nov 2020, 1/4 of the users are still on Python +* Python 2 and 3 support (As of Nov 2020, 1/4 of the users are still on Python 2, so there is no plan to drop Python 2 support) Note on version 2 ----------------- -Version 2 improved support for pytest result xml files by fixing a few issues, -notably that there could be multiple or entries. There is a +Version 2 improved support for pytest result xml files by fixing a few issues, +notably that there could be multiple or entries. There is a breaking change that ``TestCase.result`` is now a list instead of a single item. If you are using this attribute, please update your code accordingly. @@ -232,7 +232,7 @@ read them out: Command Line ------------ -.. code-block:: shell +.. code-block:: console $ junitparser --help usage: junitparser [-h] [-v] {merge} ... @@ -249,7 +249,7 @@ Command Line -v, --version show program's version number and exit -.. code-block:: shell +.. code-block:: console $ junitparser merge --help usage: junitparser merge [-h] [--glob] paths [paths ...] output @@ -264,7 +264,7 @@ Command Line --suite-name SUITE_NAME Name added to . -.. code-block:: shell +.. code-block:: console $ junitparser verify --help usage: junitparser verify [-h] [--glob] paths [paths ...] diff --git a/docs/Makefile b/docs/Makefile index 298ea9e..b61b876 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,6 +6,7 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build +SPHINXAPIDOC = sphinx-apidoc # Put it first so that "make" without argument is like "make help". help: @@ -16,4 +17,5 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXAPIDOC) -f -o api/ -H "API" -e ../junitparser + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api/inheritance_diagram.rst b/docs/api/inheritance_diagram.rst new file mode 100644 index 0000000..1fcd88c --- /dev/null +++ b/docs/api/inheritance_diagram.rst @@ -0,0 +1,5 @@ +Class Inheritance +================= + +.. inheritance-diagram:: junitparser.junitparser + :parts: 5 diff --git a/docs/conf.py b/docs/conf.py index 12d9e0a..37df7c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,12 +16,13 @@ import sys sys.path.insert(0, os.path.abspath("../junitparser")) +sys.path.insert(0, os.path.abspath("../")) # -- Project information ----------------------------------------------------- project = "junitparser" -copyright = "2019, Joel Wang" +copyright = "2019-2023, Joel Wang" author = "Joel Wang" # The short X.Y version @@ -41,8 +42,16 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", ] +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -60,7 +69,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -87,7 +96,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +# html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. diff --git a/docs/index.rst b/docs/index.rst index 5cb125e..b79a102 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,17 +1,11 @@ -.. junitparser documentation master file, created by - sphinx-quickstart on Fri Mar 1 10:40:19 2019. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. include:: ../README.rst .. toctree:: :maxdepth: 2 - :caption: Contents: - -.. include:: ../README.rst - -.. .. automodule:: junitparser -.. :members: + :hidden: + api/inheritance_diagram + api/modules Indices and tables ================== diff --git a/junitparser/cli.py b/junitparser/cli.py index eeb83ae..8687a8b 100644 --- a/junitparser/cli.py +++ b/junitparser/cli.py @@ -6,7 +6,7 @@ def merge(paths, output, suite_name): - """Merge xml report.""" + """Merge XML report.""" result = JUnitXml() for path in paths: result += JUnitXml.fromfile(path) @@ -45,7 +45,7 @@ def _parser(prog_name=None): # pragma: no cover # command: merge merge_parser = command_parser.add_parser( - "merge", help="Merge Junit XML format reports with junitparser." + "merge", help="Merge JUnit XML format reports with junitparser." ) merge_parser.add_argument( "--glob", @@ -56,7 +56,7 @@ def _parser(prog_name=None): # pragma: no cover ) merge_parser.add_argument("paths", nargs="+", help="Original XML path(s).") merge_parser.add_argument( - "output", help='Merged XML Path, setting to "-" will output console' + "output", help='Merged XML Path, setting to "-" will output to the console' ) merge_parser.add_argument( "--suite-name", diff --git a/junitparser/junitparser.py b/junitparser/junitparser.py index fc1f00a..b921b99 100644 --- a/junitparser/junitparser.py +++ b/junitparser/junitparser.py @@ -3,9 +3,10 @@ existing Result XML files, or create new JUnit/xUnit result XMLs from scratch. Reference schema: https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd -This according to the document is Apache Ant's JUnit output. -See documentation for other supported schemas. +This, according to the document, is Apache Ant's JUnit output. + +See the documentation for other supported schemas. """ import itertools @@ -56,14 +57,14 @@ class Attr(object): By default they are all string values. To support different value types, inherit this class and define your own methods. - Also see: :class:`InitAttr`, :class:`FloatAttr`. + Also see: :class:`IntAttr`, :class:`FloatAttr`. """ def __init__(self, name: str = None): self.name = name def __get__(self, instance, cls): - """Gets value from attribute, return None if attribute doesn't exist.""" + """Get value from attribute, return ``None`` if attribute doesn't exist.""" return instance._elem.attrib.get(self.name) def __set__(self, instance, value: str): @@ -75,7 +76,7 @@ def __set__(self, instance, value: str): class IntAttr(Attr): """An integer attribute for an XML element. - This class is used internally for counting test cases, but you could use + This class is used internally for counting testcases, but you could use it for any specific purpose. """ @@ -121,7 +122,7 @@ def attributed(cls): class junitxml(type): - """Metaclass to decorate the xml class""" + """Metaclass to decorate the XML class.""" def __new__(meta, name, bases, methods): cls = super(junitxml, meta).__new__(meta, name, bases, methods) @@ -130,7 +131,7 @@ def __new__(meta, name, bases, methods): class Element(metaclass=junitxml): - """Base class for all Junit XML elements.""" + """Base class for all JUnit XML elements.""" def __init__(self, name: str = None): if not name: @@ -152,27 +153,27 @@ def __repr__(self): return """""" % tag def append(self, sub_elem): - """Adds the element subelement to the end of this elements internal + """Add the element subelement to the end of this elements internal list of subelements. """ self._elem.append(sub_elem._elem) def extend(self, sub_elems): - """Adds elements subelement to the end of this elements internal + """Add elements subelement to the end of this elements internal list of subelements. """ self._elem.extend((sub_elem._elem for sub_elem in sub_elems)) @classmethod def fromstring(cls, text: str): - """Construct Junit objects from a XML string.""" + """Construct JUnit object *cls* from XML string *test*.""" instance = cls() instance._elem = etree.fromstring(text) # nosec return instance @classmethod def fromelem(cls, elem): - """Constructs Junit objects from an elementTree element.""" + """Construct JUnit objects from an ElementTree element *elem*.""" if elem is None: return instance = cls() @@ -183,25 +184,25 @@ def fromelem(cls, elem): return instance def iterchildren(self, Child): - """Iterate through specified Child type elements.""" + """Iterate through specified *Child* type elements.""" elems = self._elem.iterfind(Child._tag) for elem in elems: yield Child.fromelem(elem) def child(self, Child): - """Find a single child of specified Child type.""" + """Find a single child of specified *Child* type.""" elem = self._elem.find(Child._tag) return Child.fromelem(elem) def remove(self, sub_elem): - """Remove a sub element.""" + """Remove subelement *sub_elem*.""" for elem in self._elem.iterfind(sub_elem._tag): child = sub_elem.__class__.fromelem(elem) if child == sub_elem: self._elem.remove(child._elem) def tostring(self): - """Converts element to XML string.""" + """Convert element to XML string.""" return etree.tostring(self._elem, encoding="utf-8") @@ -209,8 +210,8 @@ class Result(Element): """Base class for test result. Attributes: - message: result as message string - type: message type + message: Result as message string. + type: Message type. """ _tag = None @@ -271,10 +272,10 @@ def __eq__(self, other): class System(Element): - """Parent class for SystemOut and SystemErr. + """Parent class for :class:`SystemOut` and :class:`SystemErr`. Attributes: - text: the output message + text: The output message. """ _tag = "" @@ -304,14 +305,9 @@ class TestCase(Element): """Object to store a testcase and its result. Attributes: - name: case name - classname: the parent class of the case - time: how much time is consumed by the test - - Properties: - result: Failure, Skipped, or Error - system_out: stdout - system_err: stderr + name: Name of the testcase. + classname: The parent class of the testcase. + time: The time consumed by the testcase. """ _tag = "testcase" @@ -357,7 +353,7 @@ def is_skipped(self): @property def result(self): - """A list of Failure, Skipped, or Error objects.""" + """A list of :class:`Failure`, :class:`Skipped`, or :class:`Error` objects.""" results = [] for entry in self: if isinstance(entry, tuple(POSSIBLE_RESULTS)): @@ -411,13 +407,13 @@ def system_err(self, value: str): class Property(Element): - """A key/value pare that's stored in the test suite. + """A key/value pare that's stored in the testsuite. Use it to store anything you find interesting or useful. Attributes: - name: the property name - value: the property value + name: The property name. + value: The property value. """ _tag = "property" @@ -441,7 +437,7 @@ def __lt__(self, other): class Properties(Element): - """A list of properties inside a test suite. + """A list of properties inside a testsuite. See :class:`Property` """ @@ -474,14 +470,14 @@ class TestSuite(Element): """The object. Attributes: - name: test suite name - hostname: name of the test machine - time: time concumed by the test suite - timestamp: when the test was run - tests: total number of tests - failures: number of failed tests - errors: number of cases with errors - skipped: number of skipped cases + name: The name of the testsuite. + hostname: Name of the test machine. + time: Time consumed by the testsuite. + timestamp: When the test was run. + tests: Total number of tests. + failures: Number of failed tests. + errors: Number of cases with errors. + skipped: Number of skipped cases. """ _tag = "testsuite" @@ -527,7 +523,7 @@ def props_eq(props1, props2): def __add__(self, other): if self == other: - # Merge the two suites + # Merge the two testsuites result = deepcopy(self) for case in other: result._add_testcase_no_update_stats(case) @@ -535,7 +531,7 @@ def __add__(self, other): result.add_testsuite(suite) result.update_statistics() else: - # Create a new test result containing two suites + # Create a new test result containing two testsuites result = JUnitXml() result.add_testsuite(self) result.add_testsuite(other) @@ -556,15 +552,15 @@ def __iadd__(self, other): result.add_testsuite(other) return result - def remove_testcase(self, testcase: TestCase): - """Removes a test case from the suite.""" + def remove_testcase(self, testcase): + """Remove testcase *testcase* from the testsuite.""" for case in self: if case == testcase: super().remove(case) self.update_statistics() def update_statistics(self): - """Updates test count and test time.""" + """Update test count and test time.""" tests = errors = failures = skipped = 0 time = 0 for case in self: @@ -585,9 +581,9 @@ def update_statistics(self): self.time = round(time, 3) def add_property(self, name, value): - """Adds a property to the testsuite. + """Add a property *name* = *value* to the testsuite. - See :class:`Property` and :class:`Properties` + See :class:`Property` and :class:`Properties`. """ props = self.child(Properties) @@ -598,28 +594,28 @@ def add_property(self, name, value): props.add_property(prop) def add_testcase(self, testcase): - """Adds a testcase to the suite.""" + """Add a testcase *testcase* to the testsuite.""" self.append(testcase) self.update_statistics() def add_testcases(self, testcases): - """Adds test cases to the suite.""" + """Add testcases *testcases* to the testsuite.""" self.extend(testcases) self.update_statistics() def _add_testcase_no_update_stats(self, testcase): - """ - Adds a testcase to the suite (without updating stats). + """Add *testcase* to the testsuite (without updating statistics). + For internal use only to avoid quadratic behaviour in merge. """ self.append(testcase) def add_testsuite(self, suite): - """Adds a testsuite inside current testsuite.""" + """Add a testsuite *suite* to the testsuite.""" self.append(suite) def properties(self): - """Iterates through all properties.""" + """Iterate through all :class:`Property` elements in the testsuite.""" props = self.child(Properties) if props is None: return @@ -627,7 +623,7 @@ def properties(self): yield prop def remove_property(self, property_: Property): - """Removes a property.""" + """Remove property *property_* from the testsuite.""" props = self.child(Properties) if props is None: return @@ -636,7 +632,7 @@ def remove_property(self, property_: Property): props.remove(property_) def testsuites(self): - """Iterates through all testsuites.""" + """Iterate through all testsuites.""" for suite in self.iterchildren(TestSuite): yield suite @@ -647,15 +643,15 @@ def write(self, filepath: str = None, pretty=False): class JUnitXml(Element): """The JUnitXml root object. - It may contains a :class:`TestSuites` or a :class:`TestSuite`. + It may contain ```` or a ````. Attributes: - name: test suite name if it only contains one test suite - time: time consumed by the test suites - tests: total number of tests - failures: number of failed cases - errors: number of cases with errors - skipped: number of skipped cases + name: Name of the testsuite if it only contains one testsuite. + time: Time consumed by the testsuites. + tests: Total number of tests. + failures: Number of failed cases. + errors: Number of cases with errors. + skipped: Number of skipped cases. """ _tag = "testsuites" @@ -699,7 +695,7 @@ def __iadd__(self, other): return self def add_testsuite(self, suite: TestSuite): - """Add a test suite.""" + """Add a testsuite.""" for existing_suite in self: if existing_suite == suite: for case in suite: @@ -726,7 +722,7 @@ def update_statistics(self): @classmethod def fromroot(cls, root_elem: Element): - """Constructs Junit objects from an elementTree root element.""" + """Construct JUnit objects from an elementTree root element.""" if root_elem.tag == "testsuites": instance = cls() elif root_elem.tag == "testsuite": @@ -738,7 +734,7 @@ def fromroot(cls, root_elem: Element): @classmethod def fromstring(cls, text: str): - """Construct Junit objects from a XML string.""" + """Construct JUnit objects from an XML string.""" root_elem = etree.fromstring(text) # nosec return cls.fromroot(root_elem) @@ -755,7 +751,7 @@ def fromfile(cls, filepath: str, parse_func=None): return instance def write(self, filepath: str = None, pretty=False, to_console=False): - """Write the object into a junit xml file. + """Write the object into a JUnit XML file. If `file_path` is not specified, it will write to the original file. If `pretty` is True, the result file will be more human friendly. diff --git a/junitparser/xunit2.py b/junitparser/xunit2.py index 19e8904..802bc50 100644 --- a/junitparser/xunit2.py +++ b/junitparser/xunit2.py @@ -2,9 +2,9 @@ The flavor based on Jenkins xunit plugin: https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd -According to the internet, The schema is compatible with: +According to the internet, the schema is compatible with: -- pytest (as default, though it also supports a "legacy" xunit1 flavor) +- Pytest (as default, though it also supports a "legacy" xunit1 flavor) - Erlang/OTP - Maven Surefire - CppTest @@ -20,7 +20,7 @@ class JUnitXml(junitparser.JUnitXml): - # Pytest and xunit schema doesn't have skipped in testsuites + # Pytest and xunit schema doesn't have "skipped" in testsuites skipped = None def update_statistics(self): @@ -40,7 +40,7 @@ def update_statistics(self): class TestSuite(junitparser.TestSuite): - """TestSuit for Pytest, with some different attributes.""" + """TestSuite for Pytest, with some different attributes.""" group = junitparser.Attr() id = junitparser.Attr() @@ -194,9 +194,9 @@ def flaky_failures(self): return self._rerun_results(FlakyFailure) def flaky_errors(self): - """""" + """""" return self._rerun_results(FlakyError) def add_rerun_result(self, result: RerunType): - """Append a rerun result to the test case. A case can have multiple rerun results""" + """Append a rerun result to the testcase. A testcase can have multiple rerun results.""" self.append(result)