Skip to content

Commit 792263b

Browse files
authored
Merge pull request #233 from dbcli/amjith/type-checking
Type checking
2 parents c1f7911 + 31dea66 commit 792263b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+722
-576
lines changed

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ jobs:
5252
- name: Set up Python
5353
uses: actions/setup-python@v5
5454
with:
55-
python-version: {{ matrix.python-version }}
55+
python-version: ${{ matrix.python-version }}
5656

5757
- name: Install dependencies
58-
run: uv sync --all-extras -p {{ matrix.python-version }}
58+
run: uv sync --all-extras -p ${{ matrix.python-version }}
5959

6060
- name: Build
6161
run: uv build

.github/workflows/typecheck.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Typecheck
2+
3+
on:
4+
pull_request:
5+
paths-ignore:
6+
- '**/*.md'
7+
- 'AUTHORS'
8+
9+
jobs:
10+
typecheck:
11+
name: Typecheck
12+
runs-on: ubuntu-latest
13+
14+
strategy:
15+
matrix:
16+
python-version: ["3.13"]
17+
18+
steps:
19+
- name: Check out Git repository
20+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
27+
- uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0
28+
with:
29+
version: 'latest'
30+
31+
- name: Install dependencies
32+
run: uv sync --all-extras
33+
34+
- name: Run mypy
35+
run: |
36+
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 .

AGENTS.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,30 @@
1010
- Create env: `python -m venv .venv && source .venv/bin/activate`.
1111
- Install dev deps: `pip install -e .[dev]`.
1212
- Run all tests + coverage: `tox`.
13-
- Style/lint (ruff): `tox -e style` (runs `ruff check --fix` and `ruff format`).
1413
- Extra tests with SQLean: `tox -e sqlean` (installs `[sqlean]` extras).
15-
- Run tests directly: `pytest -v` or focused: `pytest -k keyword`.
14+
- Run tests directly: `pytest -q` or focused: `pytest -k keyword`.
1615
- Launch CLI locally: `litecli path/to.db`.
1716

17+
### Ruff (lint/format)
18+
- Full style pass: `tox -e style` (runs `ruff check --fix` and `ruff format`).
19+
- Direct commands:
20+
- Lint: `ruff check` (add `--fix` to auto-fix)
21+
- Format: `ruff format`
22+
23+
### Mypy (type checking)
24+
- Repo-wide (recommended): `mypy --explicit-package-bases .`
25+
- Per-package: `mypy --explicit-package-bases litecli`
26+
- Notes:
27+
- 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.
29+
1830
## Coding Style & Naming Conventions
1931
- Formatter/linter: Ruff (configured via `.pre-commit-config.yaml` and `tox`).
2032
- Indentation: 4 spaces. Line length: 140 (see `pyproject.toml`).
2133
- Naming: modules/functions/variables `snake_case`; classes `CamelCase`; tests `test_*.py`.
2234
- Keep imports sorted and unused code removed (ruff enforces).
35+
- Use lowercase type hints for dict, list, tuples etc.
36+
- Use | for Unions and | None for Optional.
2337

2438
## Testing Guidelines
2539
- Framework: Pytest with coverage (`coverage run -m pytest` via tox).
@@ -33,6 +47,11 @@
3347
- PRs: include clear description, steps to reproduce/verify, and screenshots or snippets for CLI output when helpful. Use the PR template.
3448
- Ensure CI passes (tests + ruff). Re-run `tox -e style` before requesting review.
3549

50+
## Changelog Discipline
51+
- Always add an "Unreleased" section at the top of `CHANGELOG.md` when making changes.
52+
- Keep entries succinct; avoid overly detailed technical notes.
53+
- Group under "Features", "Bug Fixes", and "Internal" when applicable.
54+
3655
## Security & Configuration Tips
3756
- Do not commit local databases or secrets. Use files under `tests/data/` for fixtures.
3857
- User settings live outside the repo; document defaults by editing `litecli/liteclirc`.

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
* Avoid Click 8.1.* to prevent messing up the pager when the PAGER env var has a string with spaces.
1010

11+
### Internal
12+
13+
- Add type checking using mypy.
1114

1215
## 1.16.0 - 2025-08-16
1316

litecli/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# type: ignore
2+
from __future__ import annotations
3+
14
import importlib.metadata
25

36
__version__ = importlib.metadata.version("litecli")

litecli/clibuffer.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
from __future__ import unicode_literals
1+
from __future__ import annotations
2+
3+
from typing import Any
24

35
from prompt_toolkit.enums import DEFAULT_BUFFER
4-
from prompt_toolkit.filters import Condition
6+
from prompt_toolkit.filters import Condition, Filter
57
from prompt_toolkit.application import get_app
68

79

8-
def cli_is_multiline(cli):
10+
def cli_is_multiline(cli: Any) -> Filter:
911
@Condition
10-
def cond():
11-
doc = get_app().layout.get_buffer_by_name(DEFAULT_BUFFER).document
12+
def cond() -> bool:
13+
buf = get_app().layout.get_buffer_by_name(DEFAULT_BUFFER)
14+
assert buf is not None
15+
doc = buf.document
1216

1317
if not cli.multi_line:
1418
return False
@@ -18,7 +22,7 @@ def cond():
1822
return cond
1923

2024

21-
def _multiline_exception(text):
25+
def _multiline_exception(text: str) -> bool:
2226
orig = text
2327
text = text.strip()
2428

litecli/clistyle.py

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

33
import logging
44

5+
56
import pygments.styles
67
from pygments.token import string_to_tokentype, Token
78
from pygments.style import Style as PygmentsStyle
89
from pygments.util import ClassNotFound
910
from prompt_toolkit.styles.pygments import style_from_pygments_cls
1011
from prompt_toolkit.styles import merge_styles, Style
12+
from prompt_toolkit.styles.style import _MergedStyle
1113

1214
logger = logging.getLogger(__name__)
1315

1416
# map Pygments tokens (ptk 1.0) to class names (ptk 2.0).
15-
TOKEN_TO_PROMPT_STYLE = {
17+
TOKEN_TO_PROMPT_STYLE: dict[Token, str] = {
1618
Token.Menu.Completions.Completion.Current: "completion-menu.completion.current",
1719
Token.Menu.Completions.Completion: "completion-menu.completion",
1820
Token.Menu.Completions.Meta.Current: "completion-menu.meta.completion.current",
@@ -41,10 +43,10 @@
4143
}
4244

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

4648

47-
def parse_pygments_style(token_name, style_object, style_dict):
49+
def parse_pygments_style(token_name: str, style_object: PygmentsStyle | dict, style_dict: dict[str, str]) -> tuple[Token, str]:
4850
"""Parse token type and style string.
4951
5052
:param token_name: str name of Pygments token. Example: "Token.String"
@@ -53,20 +55,20 @@ def parse_pygments_style(token_name, style_object, style_dict):
5355
5456
"""
5557
token_type = string_to_tokentype(token_name)
56-
try:
58+
if isinstance(style_object, PygmentsStyle):
5759
other_token_type = string_to_tokentype(style_dict[token_name])
5860
return token_type, style_object.styles[other_token_type]
59-
except AttributeError:
61+
else:
6062
return token_type, style_dict[token_name]
6163

6264

63-
def style_factory(name, cli_style):
65+
def style_factory(name: str, cli_style: dict[str, str]) -> _MergedStyle:
6466
try:
6567
style = pygments.styles.get_style_by_name(name)
6668
except ClassNotFound:
6769
style = pygments.styles.get_style_by_name("native")
6870

69-
prompt_styles = []
71+
prompt_styles: list[tuple[str, str]] = []
7072
# prompt-toolkit used pygments tokens for styling before, switched to style
7173
# names in 2.0. Convert old token types to new style names, for backwards compatibility.
7274
for token in cli_style:
@@ -84,11 +86,11 @@ def style_factory(name, cli_style):
8486
# https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/styles/defaults.py
8587
prompt_styles.append((token, cli_style[token]))
8688

87-
override_style = Style([("bottom-toolbar", "noreverse")])
89+
override_style: Style = Style([("bottom-toolbar", "noreverse")])
8890
return merge_styles([style_from_pygments_cls(style), override_style, Style(prompt_styles)])
8991

9092

91-
def style_factory_output(name, cli_style):
93+
def style_factory_output(name: str, cli_style: dict[str, str]) -> PygmentsStyle:
9294
try:
9395
style = pygments.styles.get_style_by_name(name).styles
9496
except ClassNotFound:

litecli/clitoolbar.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
from __future__ import unicode_literals
1+
from __future__ import annotations
2+
3+
from typing import Callable, Any
24

35
from prompt_toolkit.key_binding.vi_state import InputMode
46
from prompt_toolkit.enums import EditingMode
57
from prompt_toolkit.application import get_app
68

79

8-
def create_toolbar_tokens_func(cli, show_fish_help):
9-
"""
10-
Return a function that generates the toolbar tokens.
11-
"""
10+
def create_toolbar_tokens_func(cli: Any, show_fish_help: Callable[[], bool]) -> Callable[[], list[tuple[str, str]]]:
11+
"""Return a function that generates the toolbar tokens."""
1212

13-
def get_toolbar_tokens():
14-
result = []
13+
def get_toolbar_tokens() -> list[tuple[str, str]]:
14+
result: list[tuple[str, str]] = []
1515
result.append(("class:bottom-toolbar", " "))
1616

1717
if cli.multi_line:
@@ -35,7 +35,7 @@ def get_toolbar_tokens():
3535
return get_toolbar_tokens
3636

3737

38-
def _get_vi_mode():
38+
def _get_vi_mode() -> str:
3939
"""Get the current vi mode for display."""
4040
return {
4141
InputMode.INSERT: "I",

litecli/completion_refresher.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
from __future__ import annotations
2+
13
import threading
4+
from typing import Callable
5+
26
from .packages.special.main import COMMANDS
37
from collections import OrderedDict
48

@@ -7,13 +11,18 @@
711

812

913
class CompletionRefresher(object):
10-
refreshers = OrderedDict()
14+
refreshers: dict[str, Callable] = OrderedDict()
1115

12-
def __init__(self):
13-
self._completer_thread = None
16+
def __init__(self) -> None:
17+
self._completer_thread: threading.Thread | None = None
1418
self._restart_refresh = threading.Event()
1519

16-
def refresh(self, executor, callbacks, completer_options=None):
20+
def refresh(
21+
self,
22+
executor: SQLExecute,
23+
callbacks: Callable | list[Callable],
24+
completer_options: dict | None = None,
25+
) -> list[tuple]:
1726
"""Creates a SQLCompleter object and populates it with the relevant
1827
completion suggestions in a background thread.
1928
@@ -36,6 +45,7 @@ def refresh(self, executor, callbacks, completer_options=None):
3645
# if DB is memory, needed to use same connection
3746
# So can't use same connection with different thread
3847
self._bg_refresh(executor, callbacks, completer_options)
48+
return [(None, None, None, "Auto-completion refresh started in the background.")]
3949
else:
4050
self._completer_thread = threading.Thread(
4151
target=self._bg_refresh,
@@ -44,19 +54,17 @@ def refresh(self, executor, callbacks, completer_options=None):
4454
)
4555
self._completer_thread.daemon = True
4656
self._completer_thread.start()
47-
return [
48-
(
49-
None,
50-
None,
51-
None,
52-
"Auto-completion refresh started in the background.",
53-
)
54-
]
55-
56-
def is_refreshing(self):
57-
return self._completer_thread and self._completer_thread.is_alive()
58-
59-
def _bg_refresh(self, sqlexecute, callbacks, completer_options):
57+
return [(None, None, None, "Auto-completion refresh started in the background.")]
58+
59+
def is_refreshing(self) -> bool:
60+
return bool(self._completer_thread and self._completer_thread.is_alive())
61+
62+
def _bg_refresh(
63+
self,
64+
sqlexecute: SQLExecute,
65+
callbacks: Callable | list[Callable],
66+
completer_options: dict,
67+
) -> None:
6068
completer = SQLCompleter(**completer_options)
6169

6270
e = sqlexecute
@@ -90,41 +98,42 @@ def _bg_refresh(self, sqlexecute, callbacks, completer_options):
9098
callback(completer)
9199

92100

93-
def refresher(name, refreshers=CompletionRefresher.refreshers):
101+
def refresher(name: str, refreshers: dict[str, Callable] = CompletionRefresher.refreshers) -> Callable:
94102
"""Decorator to add the decorated function to the dictionary of
95103
refreshers. Any function decorated with a @refresher will be executed as
96104
part of the completion refresh routine."""
97105

98-
def wrapper(wrapped):
106+
def wrapper(wrapped: Callable) -> Callable:
99107
refreshers[name] = wrapped
100108
return wrapped
101109

102110
return wrapper
103111

104112

105113
@refresher("databases")
106-
def refresh_databases(completer, executor):
114+
def refresh_databases(completer: SQLCompleter, executor: SQLExecute) -> None:
107115
completer.extend_database_names(executor.databases())
108116

109117

110118
@refresher("schemata")
111-
def refresh_schemata(completer, executor):
119+
def refresh_schemata(completer: SQLCompleter, executor: SQLExecute) -> None:
112120
# name of the current database.
113121
completer.extend_schemata(executor.dbname)
114122
completer.set_dbname(executor.dbname)
115123

116124

117125
@refresher("tables")
118-
def refresh_tables(completer, executor):
119-
completer.extend_relations(executor.tables(), kind="tables")
120-
completer.extend_columns(executor.table_columns(), kind="tables")
126+
def refresh_tables(completer: SQLCompleter, executor: SQLExecute) -> None:
127+
table_cols = list(executor.table_columns())
128+
completer.extend_relations(table_cols, kind="tables")
129+
completer.extend_columns(table_cols, kind="tables")
121130

122131

123132
@refresher("functions")
124-
def refresh_functions(completer, executor):
133+
def refresh_functions(completer: SQLCompleter, executor: SQLExecute) -> None:
125134
completer.extend_functions(executor.functions())
126135

127136

128137
@refresher("special_commands")
129-
def refresh_special(completer, executor):
130-
completer.extend_special_commands(COMMANDS.keys())
138+
def refresh_special(completer: SQLCompleter, executor: SQLExecute) -> None:
139+
completer.extend_special_commands(list(COMMANDS.keys()))

0 commit comments

Comments
 (0)