Skip to content

Commit eea974f

Browse files
authored
Merge pull request #242 from dbcli/krace/ty
Migrate typechecking to ty
2 parents f4c3052 + 05022e6 commit eea974f

29 files changed

+178
-165
lines changed

.github/workflows/typecheck.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ jobs:
3131
- name: Install dependencies
3232
run: uv sync --all-extras
3333

34-
- name: Run mypy
34+
- name: Run ty
3535
run: |
3636
cd litecli
37-
uv run --no-sync --frozen -- python -m ensurepip
38-
uv run --no-sync --frozen -- python -m mypy --no-pretty --install-types --non-interactive .
37+
uv run ty check -v

AGENTS.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020
- Lint: `ruff check` (add `--fix` to auto-fix)
2121
- Format: `ruff format`
2222

23-
### Mypy (type checking)
24-
- Repo-wide (recommended): `mypy --explicit-package-bases .`
25-
- Per-package: `mypy --explicit-package-bases litecli`
23+
## ty (type checking)
24+
- Repo-wide `ty check -v`
25+
- Per-package: `ty check litecli -v`
2626
- Notes:
2727
- Config is in `pyproject.toml` (target Python 3.9, stricter settings).
28-
- Use `--explicit-package-bases` to avoid module discovery issues when running outside tox.
2928

3029
## Coding Style & Naming Conventions
3130
- Formatter/linter: Ruff (configured via `.pre-commit-config.yaml` and `tox`).

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 1.18.0
2+
3+
### Internal
4+
5+
- Switch mypy to ty for type checking. [(#242)](https://github.com/dbcli/litecli/pull/242/files)
6+
17
## 1.17.0 - 2025-09-28
28

39
### Features

litecli/clistyle.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
from __future__ import annotations
22

33
import logging
4-
4+
from typing import cast
55

66
import pygments.styles
7-
from pygments.token import string_to_tokentype, Token
8-
from pygments.style import Style as PygmentsStyle
9-
from pygments.util import ClassNotFound
7+
from prompt_toolkit.styles import Style, merge_styles
108
from prompt_toolkit.styles.pygments import style_from_pygments_cls
11-
from prompt_toolkit.styles import merge_styles, Style
129
from prompt_toolkit.styles.style import _MergedStyle
10+
from pygments.style import Style as PygmentsStyle
11+
from pygments.token import Token, _TokenType, string_to_tokentype
12+
from pygments.util import ClassNotFound
1313

1414
logger = logging.getLogger(__name__)
1515

1616
# map Pygments tokens (ptk 1.0) to class names (ptk 2.0).
17-
TOKEN_TO_PROMPT_STYLE: dict[Token, str] = {
17+
TOKEN_TO_PROMPT_STYLE: dict[_TokenType, str] = {
1818
Token.Menu.Completions.Completion.Current: "completion-menu.completion.current",
1919
Token.Menu.Completions.Completion: "completion-menu.completion",
2020
Token.Menu.Completions.Meta.Current: "completion-menu.meta.completion.current",
@@ -43,10 +43,10 @@
4343
}
4444

4545
# reverse dict for cli_helpers, because they still expect Pygments tokens.
46-
PROMPT_STYLE_TO_TOKEN: dict[str, Token] = {v: k for k, v in TOKEN_TO_PROMPT_STYLE.items()}
46+
PROMPT_STYLE_TO_TOKEN: dict[str, _TokenType] = {v: k for k, v in TOKEN_TO_PROMPT_STYLE.items()}
4747

4848

49-
def parse_pygments_style(token_name: str, style_object: PygmentsStyle | dict, style_dict: dict[str, str]) -> tuple[Token, str]:
49+
def parse_pygments_style(token_name: str, style_object: PygmentsStyle | dict, style_dict: dict[str, str]) -> tuple[_TokenType, str]:
5050
"""Parse token type and style string.
5151
5252
:param token_name: str name of Pygments token. Example: "Token.String"
@@ -111,4 +111,5 @@ class OutputStyle(PygmentsStyle):
111111
default_style = ""
112112
styles = style
113113

114-
return OutputStyle
114+
# mypy does not complain but ty complains: error[invalid-return-type]: Return type does not match returned value. Hence added cast.
115+
return cast(OutputStyle, PygmentsStyle)

litecli/completion_refresher.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from __future__ import annotations
22

33
import threading
4-
from typing import Callable
5-
6-
from .packages.special.main import COMMANDS
74
from collections import OrderedDict
5+
from typing import Callable, cast
86

7+
from .packages.special.main import COMMANDS
98
from .sqlcompleter import SQLCompleter
109
from .sqlexecute import SQLExecute
1110

@@ -77,7 +76,9 @@ def _bg_refresh(
7776

7877
# If callbacks is a single function then push it into a list.
7978
if callable(callbacks):
80-
callbacks = [callbacks]
79+
callbacks_list: list[Callable] = [callbacks]
80+
else:
81+
callbacks_list = list(cast(list[Callable], callbacks))
8182

8283
while 1:
8384
for refresher in self.refreshers.values():
@@ -94,7 +95,7 @@ def _bg_refresh(
9495
# break statement.
9596
continue
9697

97-
for callback in callbacks:
98+
for callback in callbacks_list:
9899
callback(completer)
99100

100101

litecli/config.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from __future__ import annotations
22

33
import errno
4-
import shutil
54
import os
65
import platform
7-
from os.path import expanduser, exists, dirname
8-
6+
import shutil
7+
from os.path import dirname, exists, expanduser
98

109
from configobj import ConfigObj
1110

@@ -55,7 +54,7 @@ def upgrade_config(config: str, def_config: str) -> None:
5554
def get_config(liteclirc_file: str | None = None) -> ConfigObj:
5655
from litecli import __file__ as package_root
5756

58-
package_root = os.path.dirname(package_root)
57+
package_root = os.path.dirname(str(package_root))
5958

6059
liteclirc_file = liteclirc_file or f"{config_location()}config"
6160

litecli/main.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@
1313
from io import open
1414

1515
try:
16-
from sqlean import OperationalError, sqlite_version
16+
from sqlean import OperationalError, sqlite_version # type: ignore[import-untyped]
1717
except ImportError:
1818
from sqlite3 import OperationalError, sqlite_version
1919
from time import time
20-
from typing import Any, Iterable
20+
from typing import Any, Generator, Iterable, cast
2121

2222
import click
2323
import sqlparse
2424
from cli_helpers.tabular_output import TabularOutputFormatter, preprocessors
2525
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
26-
from prompt_toolkit.completion import DynamicCompleter
26+
from prompt_toolkit.completion import Completion, DynamicCompleter
2727
from prompt_toolkit.document import Document
2828
from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
2929
from prompt_toolkit.filters import HasFocus, IsDone
@@ -35,8 +35,6 @@
3535
)
3636
from prompt_toolkit.lexers import PygmentsLexer
3737
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
38-
from typing import cast
39-
from prompt_toolkit.completion import Completion
4038

4139
from .__init__ import __version__
4240
from .clibuffer import cli_is_multiline
@@ -53,8 +51,6 @@
5351
from .sqlcompleter import SQLCompleter
5452
from .sqlexecute import SQLExecute
5553

56-
click.disable_unicode_literals_warning = True
57-
5854
# Query tuples are used for maintaining history
5955
Query = namedtuple("Query", ["query", "successful", "mutating"])
6056

@@ -84,7 +80,8 @@ def __init__(
8480
self.key_bindings = c["main"]["key_bindings"]
8581
special.set_favorite_queries(self.config)
8682
self.formatter = TabularOutputFormatter(format_name=c["main"]["table_format"])
87-
self.formatter.litecli = self
83+
# self.formatter.litecli = self, ty raises unresolved-attribute, hence use dynamic assignment
84+
setattr(self.formatter, "litecli", self)
8885
self.syntax_style = c["main"]["syntax_style"]
8986
self.less_chatty = c["main"].as_bool("less_chatty")
9087
self.show_bottom_toolbar = c["main"].as_bool("show_bottom_toolbar")
@@ -181,7 +178,7 @@ def register_special_commands(self) -> None:
181178
case_sensitive=True,
182179
)
183180

184-
def change_table_format(self, arg: str, **_: Any) -> Iterable[tuple]:
181+
def change_table_format(self, arg: str, **_: Any) -> Generator[tuple[None, None, None, str], None, None]:
185182
try:
186183
self.formatter.format_name = arg
187184
yield (None, None, None, "Changed table format to {}".format(arg))
@@ -200,11 +197,14 @@ def change_db(self, arg: str | None, **_: Any) -> Iterable[tuple]:
200197
self.sqlexecute.connect(database=arg)
201198

202199
self.refresh_completions()
200+
# guard so that ty doesn't complain
201+
dbname = self.sqlexecute.dbname if self.sqlexecute is not None else ""
202+
203203
yield (
204204
None,
205205
None,
206206
None,
207-
'You are now connected to database "%s"' % (self.sqlexecute.dbname),
207+
'You are now connected to database "%s"' % (dbname),
208208
)
209209

210210
def execute_from_file(self, arg: str | None, **_: Any) -> Iterable[tuple[Any, ...]]:
@@ -303,7 +303,7 @@ def get(key: str) -> str | None:
303303

304304
return {x: get(x) for x in keys}
305305

306-
def connect(self, database: str = "") -> None:
306+
def connect(self, database: str | None = "") -> None:
307307
cnf: dict[str, str | None] = {"database": None}
308308

309309
cnf = self.read_my_cnf_files(cnf.keys())
@@ -510,7 +510,8 @@ def one_iteration(text: str | None = None) -> None:
510510
successful = False
511511
start = time()
512512
res = sqlexecute.run(text)
513-
self.formatter.query = text
513+
# Set query attribute dynamically on formatter
514+
setattr(self.formatter, "query", text)
514515
successful = True
515516
special.unset_once_if_written()
516517
# Keep track of whether or not the query is mutating. In case
@@ -522,7 +523,8 @@ def one_iteration(text: str | None = None) -> None:
522523
raise e
523524
except KeyboardInterrupt:
524525
try:
525-
sqlexecute.conn.interrupt()
526+
# since connection can be sqlite3 or sqlean, it's hard to annotate the type for interrupt. so ignore the type hint warning.
527+
sqlexecute.conn.interrupt() # type: ignore[attr-defined]
526528
except Exception as e:
527529
self.echo(
528530
"Encountered error while cancelling query: {}".format(e),
@@ -755,6 +757,7 @@ def refresh_completions(self, reset: bool = False) -> list[tuple]:
755757
if reset:
756758
with self._completer_lock:
757759
self.completer.reset_completions()
760+
assert self.sqlexecute is not None
758761
self.completion_refresher.refresh(
759762
self.sqlexecute,
760763
self._on_completions_refreshed,
@@ -815,7 +818,7 @@ def run_query(self, query: str, new_line: bool = True) -> None:
815818
results = self.sqlexecute.run(query)
816819
for result in results:
817820
title, cur, headers, status = result
818-
self.formatter.query = query
821+
setattr(self.formatter, "query", query)
819822
output = self.format_output(title, cur, headers)
820823
for line in output:
821824
click.echo(line, nl=new_line)

litecli/packages/parseutils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from typing import Generator, Iterable, Literal
55

66
import sqlparse
7-
from sqlparse.sql import IdentifierList, Identifier, Function, Token, TokenList
8-
from sqlparse.tokens import Keyword, DML, Punctuation
7+
from sqlparse.sql import Function, Identifier, IdentifierList, Token, TokenList
8+
from sqlparse.tokens import DML, Keyword, Punctuation
99

1010
cleanup_regex: dict[str, re.Pattern[str]] = {
1111
# This matches only alphanumerics and underscores.
@@ -18,10 +18,10 @@
1818
"all_punctuations": re.compile(r"([^\s]+)$"),
1919
}
2020

21+
LAST_WORD_INCLUDE_TYPE = Literal["alphanum_underscore", "many_punctuations", "most_punctuations", "all_punctuations"]
2122

22-
def last_word(
23-
text: str, include: Literal["alphanum_underscore", "many_punctuations", "most_punctuations", "all_punctuations"] = "alphanum_underscore"
24-
) -> str:
23+
24+
def last_word(text: str, include: LAST_WORD_INCLUDE_TYPE = "alphanum_underscore") -> str:
2525
R"""
2626
Find the last word in a sentence.
2727
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ruff: noqa
22

33
from __future__ import annotations
4+
from types import FunctionType
45

56
from typing import Callable, Any
67

@@ -9,11 +10,34 @@
910

1011
def export(defn: Callable[..., Any]) -> Callable[..., Any]:
1112
"""Decorator to explicitly mark functions that are exposed in a lib."""
12-
globals()[defn.__name__] = defn
13-
__all__.append(defn.__name__)
13+
# ty, requires explict check for callable of tyep | function type to access __name__
14+
if isinstance(defn, (type, FunctionType)):
15+
globals()[defn.__name__] = defn
16+
__all__.append(defn.__name__)
1417
return defn
1518

1619

1720
from . import dbcommands
1821
from . import iocommands
1922
from . import llm
23+
from . import utils
24+
from .main import CommandNotFound, register_special_command, execute
25+
from .iocommands import (
26+
set_favorite_queries,
27+
editor_command,
28+
get_filename,
29+
get_editor_query,
30+
open_external_editor,
31+
is_expanded_output,
32+
set_expanded_output,
33+
write_tee,
34+
unset_once_if_written,
35+
unset_pipe_once_if_written,
36+
disable_pager,
37+
set_pager,
38+
is_pager_enabled,
39+
write_once,
40+
write_pipe_once,
41+
close_tee,
42+
)
43+
from .llm import is_llm_command, handle_llm, FinishIteration

litecli/packages/special/favoritequeries.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import annotations
33

4+
import builtins
45
from typing import Any, cast
56

67

@@ -39,7 +40,7 @@ class FavoriteQueries(object):
3940
def __init__(self, config: Any) -> None:
4041
self.config = config
4142

42-
def list(self) -> list[str]:
43+
def list(self) -> builtins.list[str]:
4344
section = cast(dict[str, str], self.config.get(self.section_name, {}))
4445
return list(section.keys())
4546

0 commit comments

Comments
 (0)