4
\$\begingroup\$

My class has a method that does some stuff, and then wants to recursively operate on some data. Specifically, I'm generating an XML report whose output includes a flat list of elements derived from a hierarchical data structure.

Example Input (a Score)

{'wrapper':True, 'sub_scores':[
  {'wrapper':True, 'sub_scores':[
    {'wrapper':False, 'name':'count-objects', 'result':'pass', 'sub_scores':[]},
    {'wrapper':False, 'name':'find-object', 'result':'pass', 'sub_scores':[
      {'wrapper':False, 'name':'measure.x', 'result':'pass', 'sub_scores':[]},
      {'wrapper':False, 'name':'measure.y', 'result':'pass', 'sub_scores':[]},
    ]},
    {'wrapper':False, 'name':'find-object', 'result':'fail', 'sub_scores':[
      {'wrapper':False, 'name':'measure.x', 'result':'skip', 'sub_scores':[]},
      {'wrapper':False, 'name':'measure.y', 'result':'skip', 'sub_scores':[]},
    ]}
  ]}   
]}

Desired Output

<report user="foo" time="1234">
  <summary tests="7" pass="4" fail="1" skip="2"/>
  <results file="foo.bin" />
  <samples>
    <sample name="count-objects" result="pass" />
    <sample name="find-object" result="pass" />
    <sample name="measure.x" result="pass" />
    <sample name="measure.y" result="pass" />
    <sample name="find-object" result="fail" />
    <sample name="measure.x" result="skip" />
    <sample name="measure.y" result="skip" />
  </samples>
</report>

In JavaScript or Lua I would create a helper function within my method that does the recursion. In Ruby I would create a lambda or proc that does the recursion. Is the 'right' way to do this in Python to create a helper static function? Or is there a better way to keep the original method self-contained, perhaps allowing closures over local variables?

class Scorer:
    def generate_report(self, score):
        r = etree.Element('report', {'user': getpass.getuser(), 'time': timestamp()})
        etree.SubElement(r, 'summary', {'tests': score.total(), 'pass': score.passed(),
                                        'skip': score.passed(), 'fail': score.failed()})
        etree.SubElement(r, 'results', {'file': self.results_path})
        samples = etree.SubElement(r, 'samples')
        Scorer._add_score_to_samples(samples, score)
        return r

    @staticmethod
    def _add_score_to_samples(samples, score):
        # Some scores are wrappers that should not be included in output
        if not score.wrapper:
            etree.SubElement(samples, 'sample', score.report_attr() )
        for s in score.sub_scores:
            Scorer._add_score_to_samples(samples, s)

I dislike 'polluting' my class with a private static method just to help out a specific method. However, if this is considered Pythonic, I'll happily continue down this path.

\$\endgroup\$
2
\$\begingroup\$

I think the code could be made better by keeping all of the XML tree-building code together. The recursive traversal of .sub_scores could be done using a generator function.

Additionally, you might want to reduce the punctuation noise by setting XML attributes using keyword arguments.

class Scorer:
    def generate_report(self, score):
        r = etree.Element('report', user=getpass.getuser(), time=timestamp())
        etree.SubElement(r, 'summary', tests=score.total(), …)
        etree.SubElement(r, 'results', file=self.results_path)
        samples = etree.SubElement(r, 'samples')
        for sample in self._samples(score):
            etree.SubElement(samples, 'sample', sample.report_attr())
        return r

    def _samples(data):
        # Some scores are wrappers that should not be included in output
        if not data.wrapper:
            yield data
        yield from _samples(data.sub_scores)

Alternatively, you could write _samples as a nested function.

class Scorer:
    def generate_report(self, score):
        def extract_samples(data):
            # Some scores are wrappers that should not be included in output
            if not data.wrapper:
                yield data
            yield from extract_samples(data.sub_scores)

        r = etree.Element('report', user=getpass.getuser(), time=timestamp())
        etree.SubElement(r, 'summary', tests=score.total(), …)
        etree.SubElement(r, 'results', file=self.results_path)
        samples = etree.SubElement(r, 'samples')
        for sample in extract_samples(score):
            etree.SubElement(samples, 'sample', sample.report_attr())
        return r
\$\endgroup\$
  • 2
    \$\begingroup\$ Note that I can't use keyword arguments in one case, because of the attribute name pass. \$\endgroup\$ – Phrogz Nov 1 '16 at 19:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.