Sat, 12 Jan 2019 11:26:32 +0100
coverage: updated coverage.py to 4.5.1.
--- a/DebugClients/Python/coverage/cmdline.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/cmdline.py Sat Jan 12 11:26:32 2019 +0100 @@ -115,7 +115,10 @@ ) rcfile = optparse.make_option( '', '--rcfile', action='store', - help="Specify configuration file. Defaults to '.coveragerc'", + help=( + "Specify configuration file. " + "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried." + ), ) source = optparse.make_option( '', '--source', action='store', metavar="SRC1,SRC2,...",
--- a/DebugClients/Python/coverage/control.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/control.py Sat Jan 12 11:26:32 2019 +0100 @@ -855,8 +855,7 @@ # Find files that were never executed at all. for pkg in self.source_pkgs: if (not pkg in sys.modules or - not hasattr(sys.modules[pkg], '__file__') or - not os.path.exists(sys.modules[pkg].__file__)): + not module_has_file(sys.modules[pkg])): continue pkg_file = source_for_file(sys.modules[pkg].__file__) self._find_unexecuted_files(self._canonical_path(pkg_file)) @@ -878,15 +877,12 @@ self._warn("Module %s was never imported." % pkg, slug="module-not-imported") return - is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__') - has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__) - - if is_namespace: + if module_is_namespace(mod): # A namespace package. It's OK for this not to have been traced, # since there is no code directly in it. return - if not has_file: + if not module_has_file(mod): self._warn("Module %s has no Python source." % pkg, slug="module-not-python") return @@ -1204,6 +1200,19 @@ return info +def module_is_namespace(mod): + """Is the module object `mod` a PEP420 namespace module?""" + return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None + + +def module_has_file(mod): + """Does the module object `mod` have an existing __file__ ?""" + mod__file__ = getattr(mod, '__file__', None) + if mod__file__ is None: + return False + return os.path.exists(mod__file__) + + # FileDisposition "methods": FileDisposition is a pure value object, so it can # be implemented in either C or Python. Acting on them is done with these # functions.
--- a/DebugClients/Python/coverage/doc/AUTHORS.txt Thu Jan 10 18:01:19 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -Coverage.py was originally written by Gareth Rees, and since 2004 has been -extended and maintained by Ned Batchelder. - -Other contributions have been made by: - -Adi Roiban -Alex Gaynor -Alexander Todorov -Anthony Sottile -Arcadiy Ivanov -Ben Finney -Bill Hart -Brandon Rhodes -Brett Cannon -Buck Evan -Carl Gieringer -Catherine Proulx -Chris Adams -Chris Rose -Christian Heimes -Christine Lytwynec -Christoph Zwerschke -Conrad Ho -Danek Duvall -Danny Allen -David Christian -David Stanek -Detlev Offenbach -Devin Jeanpierre -Dmitry Shishov -Dmitry Trofimov -Eduardo Schettino -Edward Loper -Geoff Bache -George Paci -George Song -Greg Rogers -Guillaume Chazarain -Ilia Meerovich -Imri Goldberg -Ionel Cristian Mărieș -JT Olds -Jessamyn Smith -Jon Chappell -Joseph Tate -Julian Berman -Krystian Kichewko -Leonardo Pistone -Lex Berezhny -Marc Abramowitz -Marcus Cobden -Mark van der Wal -Martin Fuzzey -Matthew Desmarais -Max Linke -Mickie Betz -Noel O'Boyle -Pablo Carballo -Patrick Mezard -Peter Portante -Rodrigue Cloutier -Roger Hu -Ross Lawley -Sandra Martocchia -Sigve Tjora -Stan Hu -Stefan Behnel -Steve Leonard -Steve Peak -Ted Wexler -Titus Brown -Yury Selivanov -Zooko Wilcox-O'Hearn
--- a/DebugClients/Python/coverage/doc/CHANGES.rst Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/doc/CHANGES.rst Sat Jan 12 11:26:32 2019 +0100 @@ -16,6 +16,27 @@ .. ---------------------------- +.. _changes_452: + +Version 4.5.2 --- 2018-11-12 +---------------------------- + +- Namespace packages are supported on Python 3.7, where they used to cause + TypeErrors about path being None. Fixes `issue 700`_. + +- Python 3.8 (as of today!) passes all tests. Fixes `issue 707` and + `issue 714`_. + +- Development moved from `Bitbucket`_ to `GitHub`_. + +.. _issue 700: https://github.com/nedbat/coveragepy/issues/700 +.. _issue 707: https://github.com/nedbat/coveragepy/issues/707 +.. _issue 714: https://github.com/nedbat/coveragepy/issues/714 + +.. _Bitbucket: https://bitbucket.org/ned/coveragepy +.. _GitHub: https://github.com/nedbat/coveragepy + + .. _changes_451: Version 4.5.1 --- 2018-02-10
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/DebugClients/Python/coverage/doc/CONTRIBUTORS.txt Sat Jan 12 11:26:32 2019 +0100 @@ -0,0 +1,105 @@ +Coverage.py was originally written by Gareth Rees, and since 2004 has been +extended and maintained by Ned Batchelder. + +Other contributions, including writing code, updating docs, and submitting +useful bug reports, have been made by: + +Adi Roiban +Alex Gaynor +Alex Groce +Alex Sandro +Alexander Todorov +Andrew Hoos +Anthony Sottile +Arcadiy Ivanov +Aron Griffis +Artem Dayneko +Ben Finney +Bill Hart +Brandon Rhodes +Brett Cannon +Buck Evan +Calen Pennington +Carl Gieringer +Catherine Proulx +Chris Adams +Chris Jerdonek +Chris Rose +Chris Warrick +Christian Heimes +Christine Lytwynec +Christoph Zwerschke +Conrad Ho +Cosimo Lupo +Dan Riti +Dan Wandschneider +Danek Duvall +Daniel Hahler +Danny Allen +David Christian +David MacIver +David Stanek +Detlev Offenbach +Devin Jeanpierre +Dirk Thomas +Dmitry Shishov +Dmitry Trofimov +Eduardo Schettino +Emil Madsen +Edward Loper +Geoff Bache +George Paci +George Song +Greg Rogers +Guillaume Chazarain +Ilia Meerovich +Imri Goldberg +Ionel Cristian Mărieș +JT Olds +Jessamyn Smith +Joe Doherty +Jon Chappell +Jon Dufresne +Joseph Tate +Josh Williams +Julian Berman +Krystian Kichewko +Kyle Altendorf +Lars Hupfeldt Nielsen +Leonardo Pistone +Lex Berezhny +Loïc Dachary +Marc Abramowitz +Marcus Cobden +Mark van der Wal +Martin Fuzzey +Matthew Boehm +Matthew Desmarais +Max Linke +Mickie Betz +Nathan Land +Noel O'Boyle +Olivier Grisel +Pablo Carballo +Patrick Mezard +Peter Baughman +Peter Ebden +Peter Portante +Rodrigue Cloutier +Roger Hu +Ross Lawley +Roy Williams +Sandra Martocchia +Scott Belden +Sigve Tjora +Stan Hu +Stefan Behnel +Stephen Finucane +Steve Leonard +Steve Peak +Ted Wexler +Titus Brown +Ville Skyttä +Yury Selivanov +Zac Hatfield-Dodds +Zooko Wilcox-O'Hearn
--- a/DebugClients/Python/coverage/doc/README.rst Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/doc/README.rst Sat Jan 12 11:26:32 2019 +0100 @@ -9,7 +9,8 @@ | |license| |versions| |status| |docs| | |ci-status| |win-ci-status| |codecov| -| |kit| |format| |saythanks| +| |kit| |format| |repos| +| |tidelift| |saythanks| .. downloads badge seems to be broken... |downloads| @@ -17,9 +18,25 @@ the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. +.. |tideliftlogo| image:: doc/media/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png + :width: 75 + :alt: Tidelift + +.. list-table:: + :widths: 10 100 + + * - |tideliftlogo| + - Professional support for coverage.py is available as part of the `Tidelift + Subscription`_. Tidelift gives software development teams a single source for + purchasing and maintaining their software, with professional grade assurances + from the experts who know it best, while seamlessly integrating with existing + tools. + +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + Coverage.py runs on many versions of Python: -* CPython 2.6, 2.7 and 3.3 through 3.7. +* CPython 2.6, 2.7 and 3.3 through pre-alpha 3.8. * PyPy2 5.10 and PyPy3 5.10. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. @@ -105,6 +122,12 @@ .. |codecov| image:: http://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 :target: http://codecov.io/github/nedbat/coveragepy?branch=master :alt: Coverage! +.. |repos| image:: https://repology.org/badge/tiny-repos/python:coverage.svg + :target: https://repology.org/metapackage/python:coverage/versions + :alt: Packaging status .. |saythanks| image:: https://img.shields.io/badge/saythanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/nedbat :alt: Say thanks :) +.. |tidelift| image:: https://tidelift.com/badges/github/nedbat/coveragepy + :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + :alt: Tidelift
--- a/DebugClients/Python/coverage/env.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/env.py Sat Jan 12 11:26:32 2019 +0100 @@ -24,6 +24,24 @@ PY2 = PYVERSION < (3, 0) PY3 = PYVERSION >= (3, 0) +# Python behavior +class PYBEHAVIOR(object): + """Flags indicating this Python's behavior.""" + + # When a break/continue/return statement in a try block jumps to a finally + # block, does the finally block do the break/continue/return (pre-3.8), or + # does the finally jump back to the break/continue/return (3.8) to do the + # work? + finally_jumps_back = (PYVERSION >= (3, 8)) + + # When a function is decorated, does the trace function get called for the + # @-line and also the def-line (new behavior in 3.8)? Or just the @-line + # (old behavior)? + trace_decorated_def = (PYVERSION >= (3, 8)) + + # Are while-true loops optimized into absolute jumps with no loop setup? + nix_while_true = (PYVERSION >= (3, 8)) + # Coverage.py specifics. # Are we using the C-implemented trace function?
--- a/DebugClients/Python/coverage/execfile.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/execfile.py Sat Jan 12 11:26:32 2019 +0100 @@ -111,7 +111,15 @@ pathname = os.path.abspath(pathname) args[0] = pathname - run_python_file(pathname, args, package=packagename, modulename=modulename, path0="") + # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It + # used to be an empty string (meaning the current directory). It changed + # to be the actual path to the current directory, so that os.chdir wouldn't + # affect the outcome. + if sys.version_info >= (3, 7, 0, 'beta', 3): + path0 = os.getcwd() + else: + path0 = "" + run_python_file(pathname, args, package=packagename, modulename=modulename, path0=path0) def run_python_file(filename, args, package=None, modulename=None, path0=None):
--- a/DebugClients/Python/coverage/files.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/files.py Sat Jan 12 11:26:32 2019 +0100 @@ -260,19 +260,8 @@ class FnmatchMatcher(object): """A matcher for files by file name pattern.""" def __init__(self, pats): - self.pats = pats[:] - # fnmatch is platform-specific. On Windows, it does the Windows thing - # of treating / and \ as equivalent. But on other platforms, we need to - # take care of that ourselves. - fnpats = (fnmatch.translate(p) for p in pats) - # Python3.7 fnmatch translates "/" as "/", before that, it translates as "\/", - # so we have to deal with maybe a backslash. - fnpats = (re.sub(r"\\?/", r"[\\\\/]", p) for p in fnpats) - flags = 0 - if env.WINDOWS: - # Windows is also case-insensitive, so make the regex case-insensitive. - flags |= re.IGNORECASE - self.re = re.compile(join_regex(fnpats), flags=flags) + self.pats = list(pats) + self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS) def __repr__(self): return "<FnmatchMatcher %r>" % self.pats @@ -296,6 +285,39 @@ return the_sep +def fnmatches_to_regex(patterns, case_insensitive=False, partial=False): + """Convert fnmatch patterns to a compiled regex that matches any of them. + + Slashes are always converted to match either slash or backslash, for + Windows support, even when running elsewhere. + + If `partial` is true, then the pattern will match if the target string + starts with the pattern. Otherwise, it must match the entire string. + + Returns: a compiled regex object. Use the .match method to compare target + strings. + + """ + regexes = (fnmatch.translate(pattern) for pattern in patterns) + # Python3.7 fnmatch translates "/" as "/". Before that, it translates as "\/", + # so we have to deal with maybe a backslash. + regexes = (re.sub(r"\\?/", r"[\\\\/]", regex) for regex in regexes) + + if partial: + # fnmatch always adds a \Z to match the whole string, which we don't + # want, so we remove the \Z. While removing it, we only replace \Z if + # followed by paren (introducing flags), or at end, to keep from + # destroying a literal \Z in the pattern. + regexes = (re.sub(r'\\Z(\(\?|$)', r'\1', regex) for regex in regexes) + + flags = 0 + if case_insensitive: + flags |= re.IGNORECASE + compiled = re.compile(join_regex(regexes), flags=flags) + + return compiled + + class PathAliases(object): """A collection of aliases for paths. @@ -343,18 +365,8 @@ if not pattern.endswith(pattern_sep): pattern += pattern_sep - # Make a regex from the pattern. fnmatch always adds a \Z to - # match the whole string, which we don't want, so we remove the \Z. - # While removing it, we only replace \Z if followed by paren, or at - # end, to keep from destroying a literal \Z in the pattern. - regex_pat = fnmatch.translate(pattern) - regex_pat = re.sub(r'\\Z(\(|$)', r'\1', regex_pat) - - # We want */a/b.py to match on Windows too, so change slash to match - # either separator. - regex_pat = regex_pat.replace(r"\/", r"[\\/]") - # We want case-insensitive matching, so add that flag. - regex = re.compile(r"(?i)" + regex_pat) + # Make a regex from the pattern. + regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True) # Normalize the result: it must end with a path separator. result_sep = sep(result)
--- a/DebugClients/Python/coverage/parser.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/parser.py Sat Jan 12 11:26:32 2019 +0100 @@ -409,6 +409,8 @@ yield (byte_num, line_num) last_line_num = line_num byte_num += byte_incr + if env.PYVERSION >= (3, 6) and line_incr >= 0x80: + line_incr -= 0x100 line_num += line_incr if line_num != last_line_num: yield (byte_num, line_num) @@ -503,6 +505,10 @@ self.lineno = body[0].lineno +# TODO: some add_arcs methods here don't add arcs, they return them. Rename them. +# TODO: the cause messages have too many commas. +# TODO: Shouldn't the cause messages join with "and" instead of "or"? + class AstArcAnalyzer(object): """Analyze source text with an AST to find executable code paths.""" @@ -544,6 +550,7 @@ if code_object_handler is not None: code_object_handler(node) + @contract(start=int, end=int) def add_arc(self, start, end, smsg=None, emsg=None): """Add an arc, including message fragments to use if it is missing.""" if self.debug: # pragma: debugging @@ -572,9 +579,19 @@ else: return node.lineno + def _line_decorated(self, node): + """Compute first line number for things that can be decorated (classes and functions).""" + lineno = node.lineno + if env.PYBEHAVIOR.trace_decorated_def: + if node.decorator_list: + lineno = node.decorator_list[0].lineno + return lineno + def _line__Assign(self, node): return self.line_for_node(node.value) + _line__ClassDef = _line_decorated + def _line__Dict(self, node): # Python 3.5 changed how dict literals are made. if env.PYVERSION >= (3, 5) and node.keys: @@ -587,6 +604,8 @@ else: return node.lineno + _line__FunctionDef = _line_decorated + def _line__List(self, node): if node.elts: return self.line_for_node(node.elts[0]) @@ -690,6 +709,13 @@ node = None return node + # Missing nodes: _missing__* + # + # Entire statements can be optimized away by Python. They will appear in + # the AST, but not the bytecode. These functions are called (by + # find_non_missing_node) to find a node to use instead of the missing + # node. They can return None if the node should truly be gone. + def _missing__If(self, node): # If the if-node is missing, then one of its children might still be # here, but not both. So return the first of the two that isn't missing. @@ -717,10 +743,24 @@ return non_missing_children[0] return NodeList(non_missing_children) + def _missing__While(self, node): + body_nodes = self.find_non_missing_node(NodeList(node.body)) + if not body_nodes: + return None + # Make a synthetic While-true node. + new_while = ast.While() + new_while.lineno = body_nodes.lineno + new_while.test = ast.Name() + new_while.test.lineno = body_nodes.lineno + new_while.test.id = "True" + new_while.body = body_nodes.body + new_while.orelse = None + return new_while + def is_constant_expr(self, node): """Is this a compile-time constant?""" node_name = node.__class__.__name__ - if node_name in ["NameConstant", "Num"]: + if node_name in ["Constant", "NameConstant", "Num"]: return "Num" elif node_name == "Name": if node.id in ["True", "False", "None", "__debug__"]: @@ -805,10 +845,10 @@ # Handlers: _handle__* # # Each handler deals with a specific AST node type, dispatched from - # add_arcs. Each deals with a particular kind of node type, and returns - # the set of exits from that node. These functions mirror the Python - # semantics of each syntactic construct. See the docstring for add_arcs to - # understand the concept of exits from a node. + # add_arcs. Handlers return the set of exits from that node, and can + # also call self.add_arc to record arcs they find. These functions mirror + # the Python semantics of each syntactic construct. See the docstring + # for add_arcs to understand the concept of exits from a node. @contract(returns='ArcStarts') def _handle__Break(self, node): @@ -820,13 +860,18 @@ @contract(returns='ArcStarts') def _handle_decorated(self, node): """Add arcs for things that can be decorated (classes and functions).""" - last = self.line_for_node(node) + main_line = last = node.lineno if node.decorator_list: + if env.PYBEHAVIOR.trace_decorated_def: + last = None for dec_node in node.decorator_list: dec_start = self.line_for_node(dec_node) - if dec_start != last: + if last is not None and dec_start != last: self.add_arc(last, dec_start) - last = dec_start + last = dec_start + if env.PYBEHAVIOR.trace_decorated_def: + self.add_arc(last, main_line) + last = main_line # The definition line may have been missed, but we should have it # in `self.statements`. For some constructs, `line_for_node` is # not what we'd think of as the first line in the statement, so map @@ -968,21 +1013,45 @@ final_exits = self.add_body_arcs(node.finalbody, prev_starts=final_from) if try_block.break_from: - self.process_break_exits( - self._combine_finally_starts(try_block.break_from, final_exits) - ) + if env.PYBEHAVIOR.finally_jumps_back: + for break_line in try_block.break_from: + lineno = break_line.lineno + cause = break_line.cause.format(lineno=lineno) + for final_exit in final_exits: + self.add_arc(final_exit.lineno, lineno, cause) + breaks = try_block.break_from + else: + breaks = self._combine_finally_starts(try_block.break_from, final_exits) + self.process_break_exits(breaks) + if try_block.continue_from: - self.process_continue_exits( - self._combine_finally_starts(try_block.continue_from, final_exits) - ) + if env.PYBEHAVIOR.finally_jumps_back: + for continue_line in try_block.continue_from: + lineno = continue_line.lineno + cause = continue_line.cause.format(lineno=lineno) + for final_exit in final_exits: + self.add_arc(final_exit.lineno, lineno, cause) + continues = try_block.continue_from + else: + continues = self._combine_finally_starts(try_block.continue_from, final_exits) + self.process_continue_exits(continues) + if try_block.raise_from: self.process_raise_exits( self._combine_finally_starts(try_block.raise_from, final_exits) ) + if try_block.return_from: - self.process_return_exits( - self._combine_finally_starts(try_block.return_from, final_exits) - ) + if env.PYBEHAVIOR.finally_jumps_back: + for return_line in try_block.return_from: + lineno = return_line.lineno + cause = return_line.cause.format(lineno=lineno) + for final_exit in final_exits: + self.add_arc(final_exit.lineno, lineno, cause) + returns = try_block.return_from + else: + returns = self._combine_finally_starts(try_block.return_from, final_exits) + self.process_return_exits(returns) if exits: # The finally clause's exits are only exits for the try block
--- a/DebugClients/Python/coverage/python.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/python.py Sat Jan 12 11:26:32 2019 +0100 @@ -135,7 +135,7 @@ def __init__(self, morf, coverage=None): self.coverage = coverage - if hasattr(morf, '__file__'): + if hasattr(morf, '__file__') and morf.__file__: filename = morf.__file__ elif isinstance(morf, types.ModuleType): # A module should have had .__file__, otherwise we can't use it.
--- a/DebugClients/Python/coverage/version.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/version.py Sat Jan 12 11:26:32 2019 +0100 @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (4, 5, 1, 'final', 0) +version_info = (4, 5, 2, 'final', 0) def _make_version(major, minor, micro, releaselevel, serial):
--- a/DebugClients/Python/coverage/xmlreport.py Thu Jan 10 18:01:19 2019 +0100 +++ b/DebugClients/Python/coverage/xmlreport.py Sat Jan 12 11:26:32 2019 +0100 @@ -1,3 +1,4 @@ +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt @@ -5,6 +6,7 @@ import os import os.path +import re import sys import time import xml.dom.minidom @@ -123,11 +125,8 @@ xcoverage.setAttribute("branch-rate", "0") xcoverage.setAttribute("complexity", "0") - # Use the DOM to write the output file. - out = self.xml_out.toprettyxml() - if env.PY2: - out = out.encode("utf8") - outfile.write(out) + # Write the output file. + outfile.write(serialize_xml(self.xml_out)) # Return the total percentage. denom = lnum_tot + bnum_tot @@ -218,3 +217,23 @@ package[2] += class_lines package[3] += class_br_hits package[4] += class_branches + + +def serialize_xml(dom): + """Serialize a minidom node to XML.""" + out = dom.toprettyxml() + if env.PY2: + out = out.encode("utf8") + # In Python 3.8, minidom lost the sorting of attributes: https://bugs.python.org/issue34160 + # For the limited kinds of XML we produce, this re-sorts them. + if env.PYVERSION >= (3, 8): + rx_attr = r' [\w-]+="[^"]*"' + rx_attrs = r'(' + rx_attr + ')+' + fixed_lines = [] + for line in out.splitlines(True): + hollow_line = re.sub(rx_attrs, u"☺", line) + attrs = sorted(re.findall(rx_attr, line)) + new_line = hollow_line.replace(u"☺", "".join(attrs)) + fixed_lines.append(new_line) + out = "".join(fixed_lines) + return out