Skip to content

Commit afd03a0

Browse files
committed
Replace lexicographic with natural sorting
Replicates graphql/graphql-js@16d2535
1 parent debce29 commit afd03a0

8 files changed

+73
-14
lines changed

src/graphql/pyutils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .is_collection import is_collection
2424
from .is_finite import is_finite
2525
from .is_integer import is_integer
26+
from .natural_compare import natural_comparison_key
2627
from .awaitable_or_value import AwaitableOrValue
2728
from .suggestion_list import suggestion_list
2829
from .frozen_error import FrozenError
@@ -48,6 +49,7 @@
4849
"is_collection",
4950
"is_finite",
5051
"is_integer",
52+
"natural_comparison_key",
5153
"AwaitableOrValue",
5254
"suggestion_list",
5355
"FrozenError",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import re
2+
from typing import Tuple
3+
4+
from itertools import cycle
5+
6+
__all__ = ["natural_comparison_key"]
7+
8+
_re_digits = re.compile(r"(\d+)")
9+
10+
11+
def natural_comparison_key(key: str) -> Tuple:
12+
"""Comparison key function for sorting strings by natural sort order.
13+
14+
See: https://en.wikipedia.org/wiki/Natural_sort_order
15+
"""
16+
return tuple(
17+
(int(part), part) if is_digit else part
18+
for part, is_digit in zip(_re_digits.split(key), cycle((False, True)))
19+
)

src/graphql/pyutils/suggestion_list.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import Collection, Optional, List
22

3+
from .natural_compare import natural_comparison_key
4+
35
__all__ = ["suggestion_list"]
46

57

@@ -21,7 +23,10 @@ def suggestion_list(input_: str, options: Collection[str]) -> List[str]:
2123
# noinspection PyShadowingNames
2224
return sorted(
2325
options_by_distance,
24-
key=lambda option: (options_by_distance.get(option, 0), option),
26+
key=lambda option: (
27+
options_by_distance.get(option, 0),
28+
natural_comparison_key(option),
29+
),
2530
)
2631

2732

src/graphql/utilities/find_breaking_changes.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from enum import Enum
2-
from operator import attrgetter
32
from typing import Any, Dict, List, NamedTuple, Union, cast
43

54
from ..language import print_ast, visit, ObjectValueNode, Visitor
6-
from ..pyutils import inspect, FrozenList, Undefined
5+
from ..pyutils import inspect, natural_comparison_key, FrozenList, Undefined
76
from ..type import (
87
GraphQLEnumType,
98
GraphQLField,
@@ -563,7 +562,10 @@ def enter_object_value(
563562
object_value_node: ObjectValueNode, *_args: Any
564563
) -> ObjectValueNode:
565564
object_value_node.fields = FrozenList(
566-
sorted(object_value_node.fields, key=attrgetter("name.value"))
565+
sorted(
566+
object_value_node.fields,
567+
key=lambda node: natural_comparison_key(node.name.value),
568+
)
567569
)
568570
return object_value_node
569571

src/graphql/utilities/lexicographic_sort_schema.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Dict, List, Optional, Tuple, Union, cast
22

33
from ..language import DirectiveLocation
4-
from ..pyutils import inspect, FrozenList
4+
from ..pyutils import inspect, natural_comparison_key, FrozenList
55
from ..type import (
66
GraphQLArgument,
77
GraphQLDirective,
@@ -174,7 +174,5 @@ def sort_named_type(type_: GraphQLNamedType) -> GraphQLNamedType:
174174

175175
def sort_by_name_key(
176176
type_: Union[GraphQLNamedType, GraphQLDirective, DirectiveLocation]
177-
) -> Tuple[bool, str]:
178-
name = type_.name
179-
# GraphQL.JS sorts '_' first using localeCompare
180-
return not name.startswith("_"), name
177+
) -> Tuple:
178+
return natural_comparison_key(type_.name)

src/graphql/validation/rules/fields_on_correct_type.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515
from ...error import GraphQLError
1616
from ...language import FieldNode
17-
from ...pyutils import did_you_mean, suggestion_list
17+
from ...pyutils import did_you_mean, natural_comparison_key, suggestion_list
1818
from . import ValidationRule
1919

2020
__all__ = ["FieldsOnCorrectTypeRule"]
@@ -109,9 +109,11 @@ def cmp(
109109
):
110110
return 1
111111

112-
if type_a.name > type_b.name:
112+
name_a = natural_comparison_key(type_a.name)
113+
name_b = natural_comparison_key(type_b.name)
114+
if name_a > name_b:
113115
return 1
114-
if type_a.name < type_b.name:
116+
if name_a < name_b:
115117
return -1
116118
return 0
117119

tests/pyutils/test_natural_compare.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from graphql.pyutils import natural_comparison_key
2+
3+
key = natural_comparison_key
4+
5+
6+
def describe_natural_compare():
7+
def handles_empty_strings():
8+
assert key("") < key("a")
9+
assert key("") < key("1")
10+
11+
def handles_strings_of_different_length():
12+
assert key("A") < key("AA")
13+
assert key("A1") < key("A1A")
14+
15+
def handles_numbers():
16+
assert key("1") < key("2")
17+
assert key("2") < key("11")
18+
19+
def handles_numbers_with_leading_zeros():
20+
assert key("0") < key("00")
21+
assert key("02") < key("11")
22+
assert key("011") < key("200")
23+
24+
def handles_numbers_embedded_into_names():
25+
assert key("a0a") < key("a9a")
26+
assert key("a00a") < key("a09a")
27+
assert key("a0a1") < key("a0a9")
28+
assert key("a10a11a") < key("a10a19a")
29+
assert key("a10a11a") < key("a10a11b")

tests/pyutils/test_suggestion_list.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ def returns_options_sorted_based_on_lexical_distance():
4646
["GraphQL", "graphics"],
4747
)
4848

49-
def returns_options_with_the_same_lexical_distance_sorted_lexicographically():
49+
def returns_options_with_the_same_lexical_distance_sorted_naturally():
5050
expect_suggestions("a", ["az", "ax", "ay"], ["ax", "ay", "az"])
5151

5252
expect_suggestions("boo", ["moo", "foo", "zoo"], ["foo", "moo", "zoo"])
5353

54-
def returns_options_sorted_first_by_lexical_distance_then_lexicographically():
54+
expect_suggestions("abc", ["a1", "a12", "a2"], ["a1", "a2", "a12"])
55+
56+
def returns_options_sorted_first_by_lexical_distance_then_naturally():
5557
expect_suggestions(
5658
"csutomer",
5759
["store", "customer", "stomer", "some", "more"],

0 commit comments

Comments
 (0)