Skip to content

Commit 7f55b01

Browse files
authored
Static type checking using mypy (#482)
- Adds mypy for type checking, runs in CI on every commit. - Configures mypy for gitlint (excludes, ignores, tuned strictness) - Fixes several typing issues - Updates contributing docs with how to use mypy
1 parent f9ffd4f commit 7f55b01

File tree

11 files changed

+89
-21
lines changed

11 files changed

+89
-21
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"extensions": [
4141
"ms-python.python",
4242
"ms-python.vscode-pylance",
43+
"ms-python.mypy-type-checker",
4344
"charliermarsh.ruff",
4445
"tamasfe.even-better-toml"
4546
]

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ jobs:
4747
- name: Code linting (ruff)
4848
run: hatch run test:lint
4949

50+
- name: Static type checking (mypy)
51+
run: hatch run test:type-check
52+
5053
- name: Install local gitlint for integration tests
5154
run: |
5255
hatch run qa:install-local

docs/contributing.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ When contributing code, please consider all the parts that are typically require
2020
- [Integration tests](https://github.com/jorisroovers/gitlint/tree/main/qa) (also automatically
2121
[enforced by CI](https://github.com/jorisroovers/gitlint/actions)). Again, please consider writing new ones
2222
for your functionality, not only updating existing ones to make the build pass.
23+
- Code style checks: linting, formatting, type-checking
2324
- [Documentation](https://github.com/jorisroovers/gitlint/tree/main/docs).
2425

2526
Since we want to maintain a high standard of quality, all of these things will have to be done regardless before code
@@ -124,9 +125,15 @@ hatch run qa:integration-tests # Run integration tests
124125
# Formatting check (black)
125126
hatch run test:format # Run formatting checks
126127

127-
# Linting (ruff)
128+
# Linting (ruff)
128129
hatch run test:lint # Run Ruff
129130

131+
# Type Check (mypy)
132+
hatch run test:type-check # Run MyPy
133+
134+
# Run unit-tests & all checks
135+
hatch run test:all # Run unit-tests and all style checks (format, lint, type-check)
136+
130137
# Project stats
131138
hatch run test:stats
132139
```

gitlint-core/gitlint/cache.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from dataclasses import dataclass, field
2+
from typing import Any, Callable, Dict, Optional
23

34

45
@dataclass
56
class PropertyCache:
67
"""Mixin class providing a simple cache."""
78

8-
_cache: dict = field(init=False, default_factory=dict)
9+
_cache: Dict[str, Any] = field(init=False, default_factory=dict)
910

1011
def _try_cache(self, cache_key, cache_populate_func):
1112
"""Tries to get a value from the cache identified by `cache_key`.
@@ -16,7 +17,7 @@ def _try_cache(self, cache_key, cache_populate_func):
1617
return self._cache[cache_key]
1718

1819

19-
def cache(original_func=None, cachekey=None):
20+
def cache(original_func: Optional[Callable[[Any], Any]] = None, cachekey: Optional[str] = None) -> Any:
2021
"""Cache decorator. Caches function return values.
2122
Requires the parent class to extend and initialize PropertyCache.
2223
Usage:
@@ -33,7 +34,7 @@ def myfunc(args):
3334

3435
# Decorators with optional arguments are a bit convoluted in python, see some of the links below for details.
3536

36-
def cache_decorator(func):
37+
def cache_decorator(func: Callable[[Any], Any]) -> Any:
3738
# Use 'nonlocal' keyword to access parent function variable:
3839
# https://stackoverflow.com/a/14678445/381010
3940
nonlocal cachekey

gitlint-core/gitlint/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class GitLintUsageError(GitlintError):
4848
"""Exception indicating there is an issue with how gitlint is used."""
4949

5050

51-
def setup_logging():
51+
def setup_logging() -> None:
5252
"""Setup gitlint logging"""
5353

5454
# Root log, mostly used for debug

gitlint-core/gitlint/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from configparser import Error as ConfigParserError
88
from dataclasses import dataclass, field
99
from typing import ClassVar, Optional
10+
from typing import OrderedDict as OrderedDictType
1011

1112
from gitlint import (
1213
options,
@@ -427,7 +428,7 @@ class LintConfigBuilder:
427428
"""
428429

429430
RULE_QUALIFIER_SYMBOL: ClassVar[str] = ":"
430-
_config_blueprint: OrderedDict = field(init=False, default_factory=OrderedDict)
431+
_config_blueprint: OrderedDictType[str, OrderedDictType[str, str]] = field(init=False, default_factory=OrderedDict)
431432
_config_path: Optional[str] = field(init=False, default=None)
432433

433434
def set_option(self, section, option_name, option_value):

gitlint-core/gitlint/contrib/rules/authors_commit.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import re
22
from pathlib import Path
3-
from typing import Tuple
3+
from typing import Set, Tuple
44

5+
from gitlint.git import GitContext
56
from gitlint.rules import CommitRule, RuleViolation
67

78

@@ -16,9 +17,12 @@ class AllowedAuthors(CommitRule):
1617
id = "CC3"
1718

1819
@classmethod
19-
def _read_authors_from_file(cls, git_ctx) -> Tuple[str, str]:
20+
def _read_authors_from_file(cls, git_ctx: GitContext) -> Tuple[Set[str], str]:
2021
for file_name in cls.authors_file_names:
21-
path = Path(git_ctx.repository_path) / file_name
22+
if git_ctx.repository_path:
23+
path = Path(git_ctx.repository_path) / file_name
24+
else:
25+
path = Path(file_name)
2226
if path.exists():
2327
authors_file = path
2428
break

gitlint-core/gitlint/rules.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import re
44
from dataclasses import dataclass, field
5-
from typing import ClassVar, Dict, List, Optional
5+
from typing import ClassVar, Dict, List, Optional, Type
66

77
from gitlint.deprecation import Deprecation
88
from gitlint.exception import GitlintError
@@ -21,10 +21,10 @@ class Rule:
2121
"""Class representing gitlint rules."""
2222

2323
# Class attributes
24-
options_spec: ClassVar[List] = []
24+
options_spec: ClassVar[List[RuleOption]] = []
2525
id: ClassVar[str]
2626
name: ClassVar[str]
27-
target: ClassVar[Optional["LineRuleTarget"]] = None
27+
target: ClassVar[Optional[Type["LineRuleTarget"]]] = None
2828
_log: ClassVar[Optional[logging.Logger]] = None
2929

3030
# Instance attributes

gitlint-core/gitlint/utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# PLATFORM_IS_WINDOWS
1616

1717

18-
def platform_is_windows():
18+
def platform_is_windows() -> bool:
1919
return "windows" in platform.system().lower()
2020

2121

@@ -26,7 +26,7 @@ def platform_is_windows():
2626
# Encoding used for terminal encoding/decoding.
2727

2828

29-
def getpreferredencoding():
29+
def getpreferredencoding() -> str:
3030
"""Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
3131
on windows and falls back to UTF-8."""
3232
fallback_encoding = "UTF-8"
@@ -38,8 +38,8 @@ def getpreferredencoding():
3838
if PLATFORM_IS_WINDOWS:
3939
preferred_encoding = fallback_encoding
4040
for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]:
41-
encoding = os.environ.get(env_var, False)
42-
if encoding:
41+
encoding = os.environ.get(env_var, None)
42+
if encoding is not None:
4343
# Support dotted (C.UTF-8) and non-dotted (C or UTF-8) charsets:
4444
# If encoding contains a dot: split and use second part, otherwise use everything
4545
dot_index = encoding.find(".")

pyproject.toml

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ dependencies = [
9191
"ruff==0.0.252",
9292
"radon==5.1.0",
9393
"pdbr==0.8.2; sys_platform != \"win32\"",
94+
"mypy==1.1.1",
95+
"types-python-dateutil==2.8.19.12"
9496
]
9597

9698
[tool.hatch.envs.test.scripts]
@@ -101,16 +103,18 @@ u = "unit-tests"
101103
unit-tests-no-cov = "pytest -rw -s {args:gitlint-core}"
102104
format = "black --check --diff {args:.}"
103105
lint = "ruff {args:gitlint-core/gitlint qa}"
106+
type-check = "mypy {args}"
104107
autoformat = "black {args:.}"
105108
autofix = [
106109
"- ruff --fix {args:gitlint-core/gitlint qa}",
107-
"autoformat", #
110+
"autoformat", #
108111
]
109112

110113
all = [
111114
"unit-tests",
112115
"format",
113-
"lint", #
116+
"lint",
117+
"type-check", #
114118
]
115119
stats = ["./tools/stats.sh"]
116120

@@ -202,3 +206,50 @@ branch = true # measure branch coverage in addition to statement coverage
202206
[tool.coverage.report]
203207
fail_under = 97
204208
show_missing = true
209+
210+
[tool.mypy]
211+
# Selectively enable strictness options
212+
warn_unused_configs = true
213+
warn_redundant_casts = true
214+
warn_unused_ignores = true
215+
no_implicit_optional = true
216+
strict_equality = true
217+
strict_concatenate = true
218+
disallow_subclassing_any = true
219+
disallow_untyped_decorators = true
220+
disallow_any_generics = true
221+
warn_return_any = true
222+
disallow_untyped_calls = true
223+
disallow_incomplete_defs = true
224+
225+
# The following options are disabled because they're too strict for now
226+
# check_untyped_defs = true
227+
# disallow_untyped_defs = true
228+
# no_implicit_reexport = true
229+
230+
exclude = [
231+
"hatch_build.py",
232+
"tools/*",
233+
"gitlint-core/gitlint/tests/samples/user_rules/import_exception/invalid_python.py",
234+
]
235+
files = ["."]
236+
# Minimum supported python version by gitlint is 3.7, so enforce 3.7 typing semantics
237+
python_version = "3.7"
238+
239+
[[tool.mypy.overrides]]
240+
# Ignore "Dataclass attribute may only be overridden by another attribute" errors in git.py
241+
module = "gitlint.git"
242+
disable_error_code = "misc"
243+
244+
[[tool.mypy.overrides]]
245+
# Ignore in gitlint/__init__.py:
246+
# - Cannot find implementation or library stub for module named "importlib_metadata" [import]
247+
# - Call to untyped function "version" in typed context [no-untyped-call]" (Python 3.7)
248+
module = "gitlint"
249+
disable_error_code = ["import", "no-untyped-call"]
250+
251+
[[tool.mypy.overrides]]
252+
# Ignore all errors in qa/shell.py (excluding this file isn't working because mypy include/exclude semantics
253+
# are unintuitive, so we ignore all errors instead)
254+
module = "qa.shell"
255+
ignore_errors = true

qa/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# PLATFORM_IS_WINDOWS
77

88

9-
def platform_is_windows():
9+
def platform_is_windows() -> bool:
1010
return "windows" in platform.system().lower()
1111

1212

@@ -19,7 +19,7 @@ def platform_is_windows():
1919
# However, we want to be able to overwrite this behavior for testing using the GITLINT_QA_USE_SH_LIB env var.
2020

2121

22-
def use_sh_library():
22+
def use_sh_library() -> bool:
2323
gitlint_use_sh_lib_env = os.environ.get("GITLINT_QA_USE_SH_LIB", None)
2424
if gitlint_use_sh_lib_env:
2525
return gitlint_use_sh_lib_env == "1"
@@ -33,7 +33,7 @@ def use_sh_library():
3333
# Encoding for reading gitlint command output
3434

3535

36-
def getpreferredencoding():
36+
def getpreferredencoding() -> str:
3737
"""Use local.getpreferredencoding() or fallback to UTF-8."""
3838
return locale.getpreferredencoding() or "UTF-8"
3939

0 commit comments

Comments
 (0)