diff --git a/src/docstub-stubs/_report.pyi b/src/docstub-stubs/_report.pyi index 1515c66..0ba8bf6 100644 --- a/src/docstub-stubs/_report.pyi +++ b/src/docstub-stubs/_report.pyi @@ -9,6 +9,7 @@ from typing import Any, ClassVar, Literal, Self, TextIO import click from ._cli_help import should_strip_ansi +from ._utils import naive_natsort_key logger: logging.Logger diff --git a/src/docstub-stubs/_utils.pyi b/src/docstub-stubs/_utils.pyi index c29d6a8..2a42f3c 100644 --- a/src/docstub-stubs/_utils.pyi +++ b/src/docstub-stubs/_utils.pyi @@ -5,6 +5,7 @@ import re from collections.abc import Callable, Hashable, Mapping, Sequence from functools import lru_cache, wraps from pathlib import Path +from typing import Any from zlib import crc32 def accumulate_qualname(qualname: str, *, start_right: bool = ...) -> None: ... @@ -18,3 +19,7 @@ def update_with_add_values( class DocstubError(Exception): pass + +_regex_digit: re.Pattern + +def naive_natsort_key(item: Any) -> tuple[str | int, ...]: ... diff --git a/src/docstub/_report.py b/src/docstub/_report.py index 86957c2..51f2514 100644 --- a/src/docstub/_report.py +++ b/src/docstub/_report.py @@ -8,6 +8,7 @@ import click from ._cli_help import should_strip_ansi +from ._utils import naive_natsort_key logger: logging.Logger = logging.getLogger(__name__) @@ -297,7 +298,7 @@ def format(self, record): msg = f"{msg}\n{indented}" # Append locations - for location in sorted(src_locations): + for location in sorted(src_locations, key=naive_natsort_key): location_styled = click.style(location, fg="magenta") msg = f"{msg}\n {location_styled}" diff --git a/src/docstub/_utils.py b/src/docstub/_utils.py index 297d881..e41f4f5 100644 --- a/src/docstub/_utils.py +++ b/src/docstub/_utils.py @@ -198,3 +198,41 @@ def update_with_add_values(*mappings, out=None): class DocstubError(Exception): """An error raised by docstub.""" + + +_regex_digit: re.Pattern = re.compile(r"(\d+)") + + +def naive_natsort_key(item): + """Transforms strings into tuples that can be sorted in natural order [1]_. + + This can be passed to the "key" argument of Python's `sorted` function. + This is a simple implementation that will likely miss many edge cases. + + Parameters + ---------- + item : Any + Item to generate the key from. `str` is called on this item before + generating the key. + + Returns + ------- + key : tuple[str | int, ...] + Key to sort by. + + Examples + -------- + >>> naive_natsort_key("exposure.py:0:10") + ('exposure.py:', 0, ':', 10, '') + >>> paths = ["exposure.py:180", "exposure.py:47"] + >>> sorted(paths, key=naive_natsort_key) + ['exposure.py:47', 'exposure.py:180'] + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Natural_sort_order + """ + parts = _regex_digit.split(str(item)) + # IDEA: Every second element should always be a digit, use that? + key = tuple(int(part) if part.isdigit() else part for part in parts) + return key diff --git a/tests/test_report.py b/tests/test_report.py index 818421a..e25e17b 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -102,7 +102,7 @@ def test_format(self, log_record): def test_format_multiple_locations(self, log_record): log_record.details = "Some details" - log_record.src_location = ["foo.py:42", "bar.py", "a/path.py:100"] + log_record.src_location = ["foo.py:42", "foo.py:9", "bar.py", "a/path.py:100"] log_record.log_id = "E321" handler = ReportHandler() @@ -110,10 +110,11 @@ def test_format_multiple_locations(self, log_record): expected = dedent( """ - E321 The actual log message (3x) + E321 The actual log message (4x) Some details a/path.py:100 bar.py + foo.py:9 foo.py:42 """ ).strip()