Skip to content

Commit a9eca48

Browse files
committed
add django-stubs as a dependency, fix pyright and mypy type checking issues
1 parent 804688b commit a9eca48

File tree

8 files changed

+52
-54
lines changed

8 files changed

+52
-54
lines changed

.github/workflows/lint.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ jobs:
2525
uses: actions/setup-python@v5
2626
with:
2727
python-version: ${{ matrix.python-version }}
28-
28+
- name: Run pyright
29+
uses: jakebailey/pyright-action@v2
2930
- name: Install Poetry
3031
uses: snok/install-poetry@v1
3132
with:
@@ -43,6 +44,7 @@ jobs:
4344
poetry run black django_typer --check
4445
poetry run pylint django_typer
4546
poetry run mypy django_typer
47+
pyright django_typer
4648
poetry check
4749
poetry run pip check
4850
poetry run python -m readme_renderer ./README.rst -o /tmp/README.html

CONTRIBUTING.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
.. _readthedocs: https://readthedocs.org/
99
.. _me: https://github.com/bckohan
1010
.. _black: https://black.readthedocs.io/en/stable/
11+
.. _pyright: https://github.com/microsoft/pyright
1112

1213
Contributing
1314
############
@@ -51,7 +52,7 @@ then run Sphinx_:
5152
Static Analysis
5253
---------------
5354

54-
`django-typer` uses Pylint_ for python linting and mypy_ for static type
55+
`django-typer` uses Pylint_ for python linting and mypy_ and pyright_ for static type
5556
checking. Header imports are also standardized using isort_ and formatting is
5657
done with black_. Before any PR is accepted the following must be run, and
5758
static analysis tools should not produce any errors or warnings. Disabling
@@ -63,6 +64,7 @@ certain errors or warnings where justified is acceptable:
6364
poetry run black django_typer
6465
poetry run pylint django_typer
6566
poetry run mypy django_typer
67+
pyright django_typer
6668
poetry check
6769
poetry run pip check
6870
poetry run python -m readme_renderer ./README.rst

django_typer/__init__.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
from django.core.management import get_commands
7777
from django.core.management.base import BaseCommand, CommandError
7878
from django.core.management.base import OutputWrapper as BaseOutputWrapper
79+
from django.core.management.color import Style as ColorStyle
7980
from django.db.models import Model
8081
from django.utils.translation import gettext as _
8182

@@ -94,9 +95,16 @@
9495

9596
from .completers import ModelObjectCompleter
9697
from .parsers import ModelObjectParser
97-
from .types import ForceColor, NoColor, PythonPath, Settings, SkipChecks
98-
from .types import Style as ColorStyle
99-
from .types import Traceback, Verbosity, Version
98+
from .types import (
99+
ForceColor,
100+
NoColor,
101+
PythonPath,
102+
Settings,
103+
SkipChecks,
104+
Traceback,
105+
Verbosity,
106+
Version,
107+
)
100108
from .utils import _command_context, traceback_config, with_typehint
101109

102110
VERSION = (1, 0, 1)
@@ -1671,19 +1679,20 @@ def command2(self, option: t.Optional[str] = None):
16711679
style: ColorStyle
16721680
stdout: BaseOutputWrapper
16731681
stderr: BaseOutputWrapper
1674-
requires_system_checks: t.Union[t.Sequence[str], str]
1682+
# requires_system_checks: t.Union[t.List[str], t.Tuple[str, ...], t.Literal['__all__']]
16751683

16761684
# we do not use verbosity because the base command does not do anything with it
16771685
# if users want to use a verbosity flag like the base django command adds
16781686
# they can use the type from django_typer.types.Verbosity
1679-
suppressed_base_arguments: t.Optional[t.Iterable[str]] = {"verbosity"}
1687+
suppressed_base_arguments = {"verbosity"}
16801688

16811689
typer_app: Typer
16821690
no_color: bool = False
16831691
force_color: bool = False
16841692
_num_commands: int = 0
16851693
_has_callback: bool = False
16861694
_root_groups: int = 0
1695+
_handle: t.Callable[..., t.Any]
16871696

16881697
command_tree: CommandNode
16891698

@@ -1701,8 +1710,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):
17011710

17021711
def __init__(
17031712
self,
1704-
stdout: t.Optional[t.IO[str]] = None,
1705-
stderr: t.Optional[t.IO[str]] = None,
1713+
stdout: t.Optional[t.TextIO] = None,
1714+
stderr: t.Optional[t.TextIO] = None,
17061715
no_color: bool = no_color,
17071716
force_color: bool = force_color,
17081717
**kwargs,
@@ -1795,7 +1804,9 @@ def __init_subclass__(cls, **_):
17951804
"""Avoid passing typer arguments up the subclass init chain"""
17961805
return super().__init_subclass__()
17971806

1798-
def create_parser(self, prog_name: str, subcommand: str, **_):
1807+
def create_parser( # pyright: ignore[reportIncompatibleMethodOverride]
1808+
self, prog_name: str, subcommand: str, **_
1809+
):
17991810
"""
18001811
Create a parser for this command. This also sets the command
18011812
context, so any functions below this call on the stack may
@@ -1862,7 +1873,7 @@ def handle(self, option1: bool, option2: bool):
18621873
:param kwargs: the options to directly pass to handle()
18631874
"""
18641875
with self:
1865-
if getattr(self, "_handle", None):
1876+
if getattr(self, "_handle", None) and callable(self._handle):
18661877
return self._handle(*args, **kwargs)
18671878
raise NotImplementedError(
18681879
_(

django_typer/apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def check_traceback_config(app_configs, **kwargs) -> t.List[CheckMessage]:
6262
A system check that validates that the traceback config is valid and
6363
contains only the expected parameters.
6464
"""
65-
warnings = []
65+
warnings: t.List[CheckMessage] = []
6666
tb_cfg = traceback_config()
6767
if isinstance(tb_cfg, dict):
6868
if rich:

django_typer/completers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,9 @@ def __init__(
255255
distinct: bool = distinct,
256256
):
257257
self.model_cls = model_cls
258-
self.lookup_field = lookup_field or model_cls._meta.pk.name
258+
self.lookup_field = str(
259+
lookup_field or getattr(self.model_cls._meta.pk, "name", "id")
260+
)
259261
self.help_field = help_field
260262
self.limit = limit
261263
self.case_insensitive = case_insensitive
@@ -322,9 +324,9 @@ def __call__(
322324
],
323325
help=getattr(obj, self.help_field, None) if self.help_field else "",
324326
)
325-
for obj in self.model_cls.objects.filter(completion_qry).distinct()[
326-
0 : self.limit
327-
]
327+
for obj in getattr(self.model_cls, "objects")
328+
.filter(completion_qry)
329+
.distinct()[0 : self.limit]
328330
if (
329331
getattr(obj, self.lookup_field) is not None
330332
and str(getattr(obj, self.lookup_field))

django_typer/parsers.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727

2828
from click import Context, Parameter, ParamType
2929
from django.apps import AppConfig, apps
30-
from django.core.exceptions import ObjectDoesNotExist
30+
from django.contrib.contenttypes.fields import GenericForeignKey
3131
from django.core.management import CommandError
32-
from django.db.models import Field, Model, UUIDField
32+
from django.db.models import Field, ForeignObjectRel, Model, UUIDField
3333
from django.utils.translation import gettext as _
3434

3535
from django_typer.completers import ModelObjectCompleter
@@ -71,7 +71,7 @@ def handle(
7171
If not provided, a CommandError will be raised.
7272
"""
7373

74-
error_handler = t.Callable[[t.Type[Model], str, ObjectDoesNotExist], None]
74+
error_handler = t.Callable[[t.Type[Model], str, Exception], None]
7575

7676
model_cls: t.Type[Model]
7777
lookup_field: str
@@ -84,11 +84,6 @@ def handle(
8484

8585
__name__ = "ModelObjectParser" # typer internals expect this
8686

87-
@property
88-
def name(self):
89-
"""Descriptive name of the model object."""
90-
return self.model_cls._meta.verbose_name
91-
9287
def __init__(
9388
self,
9489
model_cls: t.Type[Model],
@@ -97,12 +92,23 @@ def __init__(
9792
on_error: t.Optional[error_handler] = on_error,
9893
):
9994
self.model_cls = model_cls
100-
self.lookup_field = lookup_field or model_cls._meta.pk.name
95+
self.lookup_field = str(
96+
lookup_field or getattr(self.model_cls._meta.pk, "name", "id")
97+
)
10198
self.on_error = on_error
10299
self.case_insensitive = case_insensitive
103-
self._field = self.model_cls._meta.get_field(self.lookup_field)
100+
field = self.model_cls._meta.get_field(self.lookup_field)
101+
assert not isinstance(field, (ForeignObjectRel, GenericForeignKey)), _(
102+
"{cls} is not a supported lookup field."
103+
).format(cls=self._field.__class__.__name__)
104+
self._field = field
104105
if self.case_insensitive and "iexact" in self._field.get_lookups():
105106
self._lookup = "__iexact"
107+
self.name = (
108+
str(self.model_cls._meta.verbose_name)
109+
if self.model_cls._meta.verbose_name
110+
else self.model_cls.__name__
111+
)
106112

107113
def convert(
108114
self, value: t.Any, param: t.Optional[Parameter], ctx: t.Optional[Context]
@@ -134,7 +140,7 @@ def convert(
134140
)
135141
except (self.model_cls.DoesNotExist, ValueError) as err:
136142
if self.on_error:
137-
return self.on_error(self.model_cls, value, err)
143+
return self.on_error(self.model_cls, str(value), err)
138144
raise CommandError(
139145
_('{model} "{value}" does not exist!').format(
140146
model=self.model_cls.__name__, value=value

django_typer/types.py

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66

77
import sys
88
from pathlib import Path
9-
from typing import Any, Callable, Optional, cast
9+
from typing import Optional, cast
1010

1111
if sys.version_info < (3, 9):
1212
from typing_extensions import Annotated
1313
else:
1414
from typing import Annotated
1515

1616
from django.core.management import CommandError
17-
from django.core.management.color import Style as ColorStyle
1817
from django.utils.translation import gettext_lazy as _
1918
from typer import Option
2019

@@ -216,28 +215,3 @@ def handle(self, verbosity: Verbosity = 1):
216215
The --skip-checks option is included by default and behaves the same as on BaseCommand_ use it to
217216
skip system checks.
218217
"""
219-
220-
221-
class Style(ColorStyle):
222-
"""
223-
For type hinting.
224-
"""
225-
226-
ERROR: Callable[[Any], str]
227-
ERROR_OUTPUT: Callable[[Any], str]
228-
HTTP_BAD_REQUEST: Callable[[Any], str]
229-
HTTP_INFO: Callable[[Any], str]
230-
HTTP_NOT_FOUND: Callable[[Any], str]
231-
HTTP_NOT_MODIFIED: Callable[[Any], str]
232-
HTTP_REDIRECT: Callable[[Any], str]
233-
HTTP_SERVER_ERROR: Callable[[Any], str]
234-
HTTP_SUCCESS: Callable[[Any], str]
235-
MIGRATE_HEADING: Callable[[Any], str]
236-
MIGRATE_LABEL: Callable[[Any], str]
237-
NOTICE: Callable[[Any], str]
238-
SQL_COLTYPE: Callable[[Any], str]
239-
SQL_FIELD: Callable[[Any], str]
240-
SQL_KEYWORD: Callable[[Any], str]
241-
SQL_TABLE: Callable[[Any], str]
242-
SUCCESS: Callable[[Any], str]
243-
WARNING: Callable[[Any], str]

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ scipy = [
8686
{ version = ">=1.11", markers = "python_version > '3.8'" },
8787
{ version = "<=1.10", markers = "python_version <= '3.8'" },
8888
]
89+
django-stubs = "^4.2.7"
8990

9091
[tool.poetry.extras]
9192
rich = ["rich"]

0 commit comments

Comments
 (0)