summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst4
-rw-r--r--coverage/cmdline.py3
-rw-r--r--coverage/collector.py4
-rw-r--r--coverage/control.py6
-rw-r--r--coverage/files.py4
-rw-r--r--coverage/html.py3
-rw-r--r--coverage/misc.py31
-rw-r--r--coverage/summary.py21
-rw-r--r--coverage/xmlreport.py8
-rw-r--r--tests/test_misc.py21
-rw-r--r--tests/test_summary.py26
11 files changed, 100 insertions, 31 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index e45c54ac..f2e2f1bc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -23,7 +23,9 @@ This list is detailed and covers changes in each pre-release version.
Unreleased
----------
-Nothing yet.
+- When sorting human-readable names, numeric components are sorted correctly:
+ file10.py will appear after file9.py. This applies to file names, module
+ names, environment variables, and test contexts.
.. _changes_602:
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 1be155b8..dfdbd1c7 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -20,6 +20,7 @@ from coverage.data import line_counts
from coverage.debug import info_formatter, info_header, short_stack
from coverage.exceptions import BaseCoverageException, ExceptionDuringRun, NoSource
from coverage.execfile import PyRunner
+from coverage.misc import human_sorted
from coverage.results import Numbers, should_fail_under
@@ -780,7 +781,7 @@ class CoverageScript:
if data:
print(f"has_arcs: {data.has_arcs()!r}")
summary = line_counts(data, fullpath=True)
- filenames = sorted(summary.keys())
+ filenames = human_sorted(summary.keys())
print(f"\n{len(filenames)} files:")
for f in filenames:
line = f"{f}: {summary[f]} lines"
diff --git a/coverage/collector.py b/coverage/collector.py
index 73babf44..733b6f32 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -10,7 +10,7 @@ from coverage import env
from coverage.debug import short_stack
from coverage.disposition import FileDisposition
from coverage.exceptions import CoverageException
-from coverage.misc import isolate_module
+from coverage.misc import human_sorted, isolate_module
from coverage.pytracer import PyTracer
os = isolate_module(os)
@@ -352,7 +352,7 @@ class Collector:
stats = tracer.get_stats()
if stats:
print("\nCoverage.py tracer stats:")
- for k in sorted(stats.keys()):
+ for k in human_sorted(stats.keys()):
print(f"{k:>20}: {stats[k]}")
if self.threading:
self.threading.settrace(None)
diff --git a/coverage/control.py b/coverage/control.py
index 8a55a317..defe9209 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -26,7 +26,7 @@ from coverage.files import PathAliases, abs_file, relative_filename, set_relativ
from coverage.html import HtmlReporter
from coverage.inorout import InOrOut
from coverage.jsonreport import JsonReporter
-from coverage.misc import bool_or_none, join_regex
+from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items
from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
from coverage.plugin import FileReporter
from coverage.plugin_support import Plugins
@@ -309,7 +309,7 @@ class Coverage:
wrote_any = False
with self._debug.without_callers():
if self._debug.should('config'):
- config_info = sorted(self.config.__dict__.items())
+ config_info = human_sorted_items(self.config.__dict__.items())
config_info = [(k, v) for k, v in config_info if not k.startswith('_')]
write_formatted_info(self._debug, "config", config_info)
wrote_any = True
@@ -1076,7 +1076,7 @@ class Coverage:
('pid', os.getpid()),
('cwd', os.getcwd()),
('path', sys.path),
- ('environment', sorted(
+ ('environment', human_sorted(
f"{k} = {v}"
for k, v in os.environ.items()
if (
diff --git a/coverage/files.py b/coverage/files.py
index 68671744..6a4f5906 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -14,7 +14,7 @@ import sys
from coverage import env
from coverage.exceptions import CoverageException
-from coverage.misc import contract, join_regex, isolate_module
+from coverage.misc import contract, human_sorted, isolate_module, join_regex
os = isolate_module(os)
@@ -199,7 +199,7 @@ class TreeMatcher:
"""
def __init__(self, paths, name):
- self.original_paths = sorted(paths)
+ self.original_paths = human_sorted(paths)
self.paths = list(map(os.path.normcase, paths))
self.name = name
diff --git a/coverage/html.py b/coverage/html.py
index b095343e..1fbac4b3 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -15,6 +15,7 @@ from coverage.data import add_data_to_hash
from coverage.exceptions import CoverageException
from coverage.files import flat_rootname
from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
+from coverage.misc import human_sorted
from coverage.report import get_analysis_to_report
from coverage.results import Numbers
from coverage.templite import Templite
@@ -123,7 +124,7 @@ class HtmlDataGeneration:
contexts = contexts_label = None
context_list = None
if category and self.config.show_contexts:
- contexts = sorted(c or self.EMPTY for c in contexts_by_lineno.get(lineno, ()))
+ contexts = human_sorted(c or self.EMPTY for c in contexts_by_lineno.get(lineno, ()))
if contexts == [self.EMPTY]:
contexts_label = self.EMPTY
else:
diff --git a/coverage/misc.py b/coverage/misc.py
index 29397537..40f00930 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -389,3 +389,34 @@ def import_local_file(modname, modfile=None):
spec.loader.exec_module(mod)
return mod
+
+
+def human_key(s):
+ """Turn a string into a list of string and number chunks.
+ "z23a" -> ["z", 23, "a"]
+ """
+ def tryint(s):
+ """If `s` is a number, return an int, else `s` unchanged."""
+ try:
+ return int(s)
+ except ValueError:
+ return s
+
+ return [tryint(c) for c in re.split(r"(\d+)", s)]
+
+def human_sorted(strings):
+ """Sort the given iterable of strings the way that humans expect.
+
+ Numeric components in the strings are sorted as numbers.
+
+ Returns the sorted list.
+
+ """
+ return sorted(strings, key=human_key)
+
+def human_sorted_items(items, reverse=False):
+ """Sort the (string, value) items the way humans expect.
+
+ Returns the sorted list of items.
+ """
+ return sorted(items, key=lambda pair: (human_key(pair[0]), pair[1]), reverse=reverse)
diff --git a/coverage/summary.py b/coverage/summary.py
index b7b172f8..0b54a05b 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -6,6 +6,7 @@
import sys
from coverage.exceptions import CoverageException
+from coverage.misc import human_sorted_items
from coverage.report import get_analysis_to_report
from coverage.results import Numbers
@@ -89,15 +90,17 @@ class SummaryReporter:
lines.append((text, args))
# Sort the lines and write them out.
- if getattr(self.config, 'sort', None):
- sort_option = self.config.sort.lower()
- reverse = False
- if sort_option[0] == '-':
- reverse = True
- sort_option = sort_option[1:]
- elif sort_option[0] == '+':
- sort_option = sort_option[1:]
-
+ sort_option = (self.config.sort or "name").lower()
+ reverse = False
+ if sort_option[0] == '-':
+ reverse = True
+ sort_option = sort_option[1:]
+ elif sort_option[0] == '+':
+ sort_option = sort_option[1:]
+
+ if sort_option == "name":
+ lines = human_sorted_items(lines, reverse=reverse)
+ else:
position = column_order.get(sort_option)
if position is None:
raise CoverageException(f"Invalid sorting option: {self.config.sort!r}")
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index 0538bfd5..6dc330f1 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -10,7 +10,7 @@ import time
import xml.dom.minidom
from coverage import __url__, __version__, files
-from coverage.misc import isolate_module
+from coverage.misc import isolate_module, human_sorted, human_sorted_items
from coverage.report import get_analysis_to_report
os = isolate_module(os)
@@ -77,7 +77,7 @@ class XmlReporter:
xcoverage.appendChild(xsources)
# Populate the XML DOM with the source info.
- for path in sorted(self.source_paths):
+ for path in human_sorted(self.source_paths):
xsource = self.xml_out.createElement("source")
xsources.appendChild(xsource)
txt = self.xml_out.createTextNode(path)
@@ -90,13 +90,13 @@ class XmlReporter:
xcoverage.appendChild(xpackages)
# Populate the XML DOM with the package info.
- for pkg_name, pkg_data in sorted(self.packages.items()):
+ for pkg_name, pkg_data in human_sorted_items(self.packages.items()):
class_elts, lhits, lnum, bhits, bnum = pkg_data
xpackage = self.xml_out.createElement("package")
xpackages.appendChild(xpackage)
xclasses = self.xml_out.createElement("classes")
xpackage.appendChild(xclasses)
- for _, class_elt in sorted(class_elts.items()):
+ for _, class_elt in human_sorted_items(class_elts.items()):
xclasses.appendChild(class_elt)
xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
xpackage.setAttribute("line-rate", rate(lhits, lnum))
diff --git a/tests/test_misc.py b/tests/test_misc.py
index 74002232..58dba6a8 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -10,6 +10,7 @@ import pytest
from coverage.exceptions import CoverageException
from coverage.misc import contract, dummy_decorator_with_args, file_be_gone
from coverage.misc import Hasher, one_of, substitute_variables, import_third_party
+from coverage.misc import human_sorted, human_sorted_items
from coverage.misc import USE_CONTRACTS
from tests.coveragetest import CoverageTest
@@ -180,3 +181,23 @@ class ImportThirdPartyTest(CoverageTest):
mod = import_third_party("xyzzy")
assert mod is None
assert "xyzzy" not in sys.modules
+
+
+HUMAN_DATA = [
+ ("z1 a2z a2a a3 a1", "a1 a2a a2z a3 z1"),
+ ("a10 a9 a100 a1", "a1 a9 a10 a100"),
+ ("4.0 3.10-win 3.10-mac 3.9-mac 3.9-win", "3.9-mac 3.9-win 3.10-mac 3.10-win 4.0"),
+]
+
+@pytest.mark.parametrize("words, ordered", HUMAN_DATA)
+def test_human_sorted(words, ordered):
+ assert " ".join(human_sorted(words.split())) == ordered
+
+@pytest.mark.parametrize("words, ordered", HUMAN_DATA)
+def test_human_sorted_items(words, ordered):
+ keys = words.split()
+ items = [(k, 1) for k in keys] + [(k, 2) for k in keys]
+ okeys = ordered.split()
+ oitems = [(k, v) for k in okeys for v in [1, 2]]
+ assert human_sorted_items(items) == oitems
+ assert human_sorted_items(items, reverse=True) == oitems[::-1]
diff --git a/tests/test_summary.py b/tests/test_summary.py
index b71921c7..4dbd3c08 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -847,8 +847,8 @@ class SummaryReporterConfigurationTest(CoverageTest):
"""
self.make_rigged_file("file1.py", 339, 155)
self.make_rigged_file("file2.py", 13, 3)
- self.make_rigged_file("file3.py", 234, 228)
- self.make_file("doit.py", "import file1, file2, file3")
+ self.make_rigged_file("file10.py", 234, 228)
+ self.make_file("doit.py", "import file1, file2, file10")
cov = Coverage(source=["."], omit=["doit.py"])
cov.start()
@@ -871,7 +871,7 @@ class SummaryReporterConfigurationTest(CoverageTest):
# ------------------------------
# file1.py 339 155 54%
# file2.py 13 3 77%
- # file3.py 234 228 3%
+ # file10.py 234 228 3%
# ------------------------------
# TOTAL 586 386 34%
@@ -906,30 +906,40 @@ class SummaryReporterConfigurationTest(CoverageTest):
msg = f"The words {words!r} don't appear in order in {text!r}"
assert indexes == sorted(indexes), msg
+ def test_default_sort_report(self):
+ # Sort the text report by the default (Name) column.
+ report = self.get_summary_text()
+ self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
+
+ def test_sort_report_by_name(self):
+ # Sort the text report explicitly by the Name column.
+ report = self.get_summary_text(('report:sort', 'Name'))
+ self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
+
def test_sort_report_by_stmts(self):
# Sort the text report by the Stmts column.
report = self.get_summary_text(('report:sort', 'Stmts'))
- self.assert_ordering(report, "file2.py", "file3.py", "file1.py")
+ self.assert_ordering(report, "file2.py", "file10.py", "file1.py")
def test_sort_report_by_missing(self):
# Sort the text report by the Missing column.
report = self.get_summary_text(('report:sort', 'Miss'))
- self.assert_ordering(report, "file2.py", "file1.py", "file3.py")
+ self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
def test_sort_report_by_cover(self):
# Sort the text report by the Cover column.
report = self.get_summary_text(('report:sort', 'Cover'))
- self.assert_ordering(report, "file3.py", "file1.py", "file2.py")
+ self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
def test_sort_report_by_cover_plus(self):
# Sort the text report by the Cover column, including the explicit + sign.
report = self.get_summary_text(('report:sort', '+Cover'))
- self.assert_ordering(report, "file3.py", "file1.py", "file2.py")
+ self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
def test_sort_report_by_cover_reversed(self):
# Sort the text report by the Cover column reversed.
report = self.get_summary_text(('report:sort', '-Cover'))
- self.assert_ordering(report, "file2.py", "file1.py", "file3.py")
+ self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
def test_sort_report_by_invalid_option(self):
# Sort the text report by a nonsense column.