Skip to content

Commit 2166764

Browse files
authored
Merge pull request #6 from MightyHelper/copilot/add-type-hints-and-ci-checks
Add comprehensive modern type hints and mypy strict mode integration
2 parents ca60982 + 05fece3 commit 2166764

6 files changed

Lines changed: 122 additions & 95 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Test with tox
3030
run: tox
3131

32-
lint-and-format:
32+
lint-format-and-type-check:
3333
runs-on: ubuntu-latest
3434

3535
steps:
@@ -50,3 +50,6 @@ jobs:
5050

5151
- name: Check formatting with ruff
5252
run: tox -e format-check
53+
54+
- name: Type check with mypy
55+
run: tox -e mypy

pyproject.toml

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,14 @@ authors = [
1111
description = "Interactive sorting utility for text lines with various sorting options"
1212
readme = "README.md"
1313
license = {text = "MIT"}
14-
requires-python = ">=3.8"
14+
requires-python = ">=3.10"
1515
classifiers = [
1616
"Development Status :: 4 - Beta",
1717
"Intended Audience :: Developers",
1818
"Intended Audience :: End Users/Desktop",
1919
"License :: OSI Approved :: MIT License",
2020
"Operating System :: OS Independent",
2121
"Programming Language :: Python :: 3",
22-
"Programming Language :: Python :: 3.8",
23-
"Programming Language :: Python :: 3.9",
2422
"Programming Language :: Python :: 3.10",
2523
"Programming Language :: Python :: 3.11",
2624
"Programming Language :: Python :: 3.12",
@@ -46,7 +44,7 @@ where = ["src"]
4644
# Ruff configuration
4745
[tool.ruff]
4846
line-length = 120
49-
target-version = "py38"
47+
target-version = "py310"
5048

5149
[tool.ruff.lint]
5250
select = [
@@ -68,4 +66,21 @@ ignore = [
6866
quote-style = "double"
6967
indent-style = "space"
7068
skip-magic-trailing-comma = false
71-
line-ending = "auto"
69+
line-ending = "auto"
70+
71+
# mypy configuration
72+
[tool.mypy]
73+
python_version = "3.10"
74+
strict = true
75+
warn_return_any = true
76+
warn_unused_configs = true
77+
disallow_untyped_defs = true
78+
disallow_incomplete_defs = true
79+
check_untyped_defs = true
80+
disallow_untyped_decorators = true
81+
no_implicit_optional = true
82+
warn_redundant_casts = true
83+
warn_unused_ignores = true
84+
warn_no_return = true
85+
warn_unreachable = true
86+
strict_equality = true

src/interactive_sort/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Interactive Sort - A tool for interactively sorting lines of text."""
22

3-
__version__ = "1.0.0"
4-
__author__ = "MightyHelper"
5-
__description__ = "Interactive sorting utility for text lines with various sorting options"
3+
__version__: str = "1.0.0"
4+
__author__: str = "MightyHelper"
5+
__description__: str = "Interactive sorting utility for text lines with various sorting options"
66

77
from .main import InteractiveSort, main
88

9-
__all__ = ["InteractiveSort", "main"]
9+
__all__: list[str] = ["InteractiveSort", "main"]

src/interactive_sort/main.py

Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
import os
55
import re
66
import sys
7+
from collections.abc import Callable
78
from functools import cmp_to_key
9+
from typing import Any
810

911

1012
class InteractiveSort:
11-
def __init__(self, args):
12-
self.data = []
13-
self.args = args
14-
self.month_map = {
13+
def __init__(self, args: argparse.Namespace) -> None:
14+
self.data: list[str] = []
15+
self.args: argparse.Namespace = args
16+
self.month_map: dict[str, int] = {
1517
"JAN": 1,
1618
"FEB": 2,
1719
"MAR": 3,
@@ -26,11 +28,11 @@ def __init__(self, args):
2628
"DEC": 12,
2729
}
2830

29-
def get_sort_key(self, item):
31+
def get_sort_key(self, item: str) -> str:
3032
"""Generates a key for sorting based on the provided arguments."""
31-
key = item
33+
key: str = item
3234
if self.args.field_separator:
33-
fields = key.split(self.args.field_separator)
35+
fields: list[str] = key.split(self.args.field_separator)
3436
if fields:
3537
key = fields[0]
3638
else:
@@ -42,35 +44,39 @@ def get_sort_key(self, item):
4244
key = "".join(filter(str.isprintable, key))
4345
return key
4446

45-
def month_sort_key(self, item):
47+
def month_sort_key(self, item: str) -> int:
4648
"""Extracts and converts the month from the beginning of the string (or first field)."""
47-
text_to_check = item
49+
text_to_check: str = item
4850
if self.args.field_separator:
49-
fields = item.split(self.args.field_separator)
51+
fields: list[str] = item.split(self.args.field_separator)
5052
if fields:
5153
text_to_check = fields[0]
5254
else:
5355
return 0
5456

55-
match = re.match(r"^(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\s*", text_to_check, re.IGNORECASE)
57+
match: re.Match[str] | None = re.match(
58+
r"^(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\s*", text_to_check, re.IGNORECASE
59+
)
5660
if match:
5761
return self.month_map.get(match.group(1).upper(), 0) # 0 for unknown
5862
return 0 # Treat lines without a month at the beginning as "unknown"
5963

60-
def human_numeric_sort_key(self, item):
64+
def human_numeric_sort_key(self, item: str) -> float | str:
6165
"""Converts a human-readable number string to a comparable tuple (or from the first field)."""
62-
text_to_check = item
66+
text_to_check: str = item
6367
if self.args.field_separator:
64-
fields = item.split(self.args.field_separator)
68+
fields: list[str] = item.split(self.args.field_separator)
6569
if fields:
6670
text_to_check = fields[0]
6771
else:
6872
return item # Fallback
6973

70-
parts = re.findall(r"(\d+\.?\d*)([KMGTPEZY]?)", text_to_check, re.IGNORECASE)
74+
parts: list[tuple[str, str]] = re.findall(r"(\d+\.?\d*)([KMGTPEZY]?)", text_to_check, re.IGNORECASE)
7175
if parts:
72-
value, unit = parts[0]
73-
value = float(value)
76+
value_str: str
77+
unit: str
78+
value_str, unit = parts[0]
79+
value: float = float(value_str)
7480
unit = unit.upper()
7581
if unit == "K":
7682
value *= 1024
@@ -94,11 +100,11 @@ def human_numeric_sort_key(self, item):
94100
except ValueError:
95101
return text_to_check # Keep original string if not convertible to number
96102

97-
def numeric_sort_key(self, item):
103+
def numeric_sort_key(self, item: str) -> float | str:
98104
"""Tries to convert the item (or first field) to a float for numeric sorting."""
99-
text_to_check = item
105+
text_to_check: str = item
100106
if self.args.field_separator:
101-
fields = item.split(self.args.field_separator)
107+
fields: list[str] = item.split(self.args.field_separator)
102108
if fields:
103109
text_to_check = fields[0]
104110
else:
@@ -109,90 +115,87 @@ def numeric_sort_key(self, item):
109115
except ValueError:
110116
return text_to_check # Keep original string if not a valid number
111117

112-
def compare_items(self, item1, item2):
118+
def compare_items(self, item1: str, item2: str) -> int:
113119
"""Compares two items based on the active sorting options."""
114120
if self.args.month_sort:
115-
key1 = self.month_sort_key(item1)
116-
key2 = self.month_sort_key(item2)
121+
key1: int = self.month_sort_key(item1)
122+
key2: int = self.month_sort_key(item2)
117123
if key1 != key2:
118124
return key1 - key2
119125

120126
if self.args.human_numeric_sort:
121-
key1 = self.human_numeric_sort_key(item1)
122-
key2 = self.human_numeric_sort_key(item2)
123-
if isinstance(key1, (int, float)) and isinstance(key2, (int, float)):
124-
if key1 != key2:
125-
return key1 - key2
127+
key1_human: float | str = self.human_numeric_sort_key(item1)
128+
key2_human: float | str = self.human_numeric_sort_key(item2)
129+
if isinstance(key1_human, int | float) and isinstance(key2_human, int | float):
130+
if key1_human != key2_human:
131+
return int(key1_human - key2_human)
126132

127133
if self.args.numeric_sort:
128-
key1 = self.numeric_sort_key(item1)
129-
key2 = self.numeric_sort_key(item2)
130-
if isinstance(key1, (int, float)) and isinstance(key2, (int, float)):
131-
if key1 != key2:
132-
return key1 - key2
134+
key1_numeric: float | str = self.numeric_sort_key(item1)
135+
key2_numeric: float | str = self.numeric_sort_key(item2)
136+
if isinstance(key1_numeric, int | float) and isinstance(key2_numeric, int | float):
137+
if key1_numeric != key2_numeric:
138+
return int(key1_numeric - key2_numeric)
133139

134140
# Default string comparison (after applying ignore-case and ignore-nonprinting)
135-
key1 = self.get_sort_key(item1)
136-
key2 = self.get_sort_key(item2)
137-
if key1 < key2:
141+
key1_str: str = self.get_sort_key(item1)
142+
key2_str: str = self.get_sort_key(item2)
143+
if key1_str < key2_str:
138144
return -1
139-
elif key1 > key2:
145+
elif key1_str > key2_str:
140146
return 1
141147
else:
142148
return 0
143149

144-
def add_item(self, item):
150+
def add_item(self, item: str) -> None:
145151
"""Adds an item to the sorted data using a custom comparison."""
146152
if not self.args.month_sort and not self.args.human_numeric_sort and not self.args.numeric_sort:
147-
148-
def key_func(x):
149-
return self.get_sort_key(x)
150-
153+
key_func: Callable[[str], Any] = self.get_sort_key
151154
else:
152155
key_func = cmp_to_key(self.compare_items)
153156

154157
bisect.insort_left(self.data, item, key=key_func)
155158

156-
def display_sorted(self):
159+
def display_sorted(self) -> None:
157160
"""Displays the current sorted data."""
158161
output_stream = sys.stderr if self.args.stderr else sys.stdout
159162

160163
if not self.args.stderr:
161164
os.system("cls" if os.name == "nt" else "clear")
162165

163166
print("--- Interactive Sorted Data ---", file=output_stream)
164-
items_to_display = self.data
167+
items_to_display: list[str] = self.data
165168
if self.args.unique:
166169
items_to_display = sorted(
167170
set(items_to_display), key=lambda x: self.data.index(x)
168171
) # Maintain original order of first occurrence
169172
if self.args.reverse:
170-
items_to_display = reversed(items_to_display)
173+
items_to_display = list(reversed(items_to_display))
171174

172175
for item in items_to_display:
173176
print(item, file=output_stream)
174177
print("-----------------------------", file=output_stream)
175178

176-
def output_final_sorted(self):
179+
def output_final_sorted(self) -> None:
177180
"""Outputs the final sorted data to stdout (used when stderr flag is set)."""
178-
items_to_display = self.data
181+
items_to_display: list[str] = self.data
179182
if self.args.unique:
180183
items_to_display = sorted(
181184
set(items_to_display), key=lambda x: self.data.index(x)
182185
) # Maintain original order of first occurrence
183186
if self.args.reverse:
184-
items_to_display = reversed(items_to_display)
187+
items_to_display = list(reversed(items_to_display))
185188

186189
for item in items_to_display:
187190
print(item, file=sys.stdout)
188191

189-
def run(self):
192+
def run(self) -> None:
190193
"""Runs the interactive sort application."""
191194
output_stream = sys.stderr if self.args.stderr else sys.stdout
192195
print("Enter lines of text. Press Ctrl+D (or Ctrl+Z then Enter on Windows) to finish.", file=output_stream)
193196
try:
194197
while True:
195-
line = input()
198+
line: str = input()
196199
self.add_item(line)
197200
self.display_sorted()
198201
except EOFError:
@@ -205,9 +208,9 @@ def run(self):
205208
print("\nExiting.", file=output_stream)
206209

207210

208-
def main():
211+
def main() -> None:
209212
"""Main entry point for the interactive-sort command."""
210-
parser = argparse.ArgumentParser(description="Interactively sort input lines.")
213+
parser: argparse.ArgumentParser = argparse.ArgumentParser(description="Interactively sort input lines.")
211214
parser.add_argument("-f", "--ignore-case", action="store_true", help="fold lower case to upper case characters")
212215
parser.add_argument("-i", "--ignore-nonprinting", action="store_true", help="consider only printable characters")
213216
parser.add_argument("-M", "--month-sort", action="store_true", help="compare (unknown) < 'JAN' < ... < 'DEC'")
@@ -224,10 +227,11 @@ def main():
224227
default=None,
225228
help="use FIELD as the field separator when finding an ordering key",
226229
)
230+
parser.add_argument("--stderr", action="store_true", help="output to stderr for debugging")
227231

228-
args = parser.parse_args()
232+
args: argparse.Namespace = parser.parse_args()
229233

230-
sorter = InteractiveSort(args)
234+
sorter: InteractiveSort = InteractiveSort(args)
231235
sorter.run()
232236

233237

0 commit comments

Comments
 (0)