summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoris Roovers <joris.roovers@gmail.com>2023-04-11 09:53:04 +0200
committerGitHub <noreply@github.com>2023-04-11 09:53:04 +0200
commit7f55b0155c69a3b3e56fd4779fb062058291b9b5 (patch)
treed13902e6ac94541ef5c17b7ed51c2647d81d9a8c
parentf9ffd4fe06166dcd213b7ea4a04b0c21a621df2d (diff)
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
-rw-r--r--.devcontainer/devcontainer.json1
-rw-r--r--.github/workflows/ci.yml3
-rw-r--r--docs/contributing.md9
-rw-r--r--gitlint-core/gitlint/cache.py7
-rw-r--r--gitlint-core/gitlint/cli.py2
-rw-r--r--gitlint-core/gitlint/config.py3
-rw-r--r--gitlint-core/gitlint/contrib/rules/authors_commit.py10
-rw-r--r--gitlint-core/gitlint/rules.py6
-rw-r--r--gitlint-core/gitlint/utils.py8
-rw-r--r--pyproject.toml55
-rw-r--r--qa/utils.py6
11 files changed, 89 insertions, 21 deletions
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 65fcc1c..e389544 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -40,6 +40,7 @@
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
+ "ms-python.mypy-type-checker",
"charliermarsh.ruff",
"tamasfe.even-better-toml"
]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e7a6447..2f4ac80 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,6 +47,9 @@ jobs:
- name: Code linting (ruff)
run: hatch run test:lint
+ - name: Static type checking (mypy)
+ run: hatch run test:type-check
+
- name: Install local gitlint for integration tests
run: |
hatch run qa:install-local
diff --git a/docs/contributing.md b/docs/contributing.md
index cf4755a..bdc9246 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -20,6 +20,7 @@ When contributing code, please consider all the parts that are typically require
- [Integration tests](https://github.com/jorisroovers/gitlint/tree/main/qa) (also automatically
[enforced by CI](https://github.com/jorisroovers/gitlint/actions)). Again, please consider writing new ones
for your functionality, not only updating existing ones to make the build pass.
+- Code style checks: linting, formatting, type-checking
- [Documentation](https://github.com/jorisroovers/gitlint/tree/main/docs).
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
# Formatting check (black)
hatch run test:format # Run formatting checks
-# Linting (ruff)
+# Linting (ruff)
hatch run test:lint # Run Ruff
+# Type Check (mypy)
+hatch run test:type-check # Run MyPy
+
+# Run unit-tests & all checks
+hatch run test:all # Run unit-tests and all style checks (format, lint, type-check)
+
# Project stats
hatch run test:stats
```
diff --git a/gitlint-core/gitlint/cache.py b/gitlint-core/gitlint/cache.py
index 5c20896..ec5e273 100644
--- a/gitlint-core/gitlint/cache.py
+++ b/gitlint-core/gitlint/cache.py
@@ -1,11 +1,12 @@
from dataclasses import dataclass, field
+from typing import Any, Callable, Dict, Optional
@dataclass
class PropertyCache:
"""Mixin class providing a simple cache."""
- _cache: dict = field(init=False, default_factory=dict)
+ _cache: Dict[str, Any] = field(init=False, default_factory=dict)
def _try_cache(self, cache_key, cache_populate_func):
"""Tries to get a value from the cache identified by `cache_key`.
@@ -16,7 +17,7 @@ class PropertyCache:
return self._cache[cache_key]
-def cache(original_func=None, cachekey=None):
+def cache(original_func: Optional[Callable[[Any], Any]] = None, cachekey: Optional[str] = None) -> Any:
"""Cache decorator. Caches function return values.
Requires the parent class to extend and initialize PropertyCache.
Usage:
@@ -33,7 +34,7 @@ def cache(original_func=None, cachekey=None):
# Decorators with optional arguments are a bit convoluted in python, see some of the links below for details.
- def cache_decorator(func):
+ def cache_decorator(func: Callable[[Any], Any]) -> Any:
# Use 'nonlocal' keyword to access parent function variable:
# https://stackoverflow.com/a/14678445/381010
nonlocal cachekey
diff --git a/gitlint-core/gitlint/cli.py b/gitlint-core/gitlint/cli.py
index 82f35ce..d3de5ac 100644
--- a/gitlint-core/gitlint/cli.py
+++ b/gitlint-core/gitlint/cli.py
@@ -48,7 +48,7 @@ class GitLintUsageError(GitlintError):
"""Exception indicating there is an issue with how gitlint is used."""
-def setup_logging():
+def setup_logging() -> None:
"""Setup gitlint logging"""
# Root log, mostly used for debug
diff --git a/gitlint-core/gitlint/config.py b/gitlint-core/gitlint/config.py
index 4637af2..7d1ba3b 100644
--- a/gitlint-core/gitlint/config.py
+++ b/gitlint-core/gitlint/config.py
@@ -7,6 +7,7 @@ from configparser import ConfigParser
from configparser import Error as ConfigParserError
from dataclasses import dataclass, field
from typing import ClassVar, Optional
+from typing import OrderedDict as OrderedDictType
from gitlint import (
options,
@@ -427,7 +428,7 @@ class LintConfigBuilder:
"""
RULE_QUALIFIER_SYMBOL: ClassVar[str] = ":"
- _config_blueprint: OrderedDict = field(init=False, default_factory=OrderedDict)
+ _config_blueprint: OrderedDictType[str, OrderedDictType[str, str]] = field(init=False, default_factory=OrderedDict)
_config_path: Optional[str] = field(init=False, default=None)
def set_option(self, section, option_name, option_value):
diff --git a/gitlint-core/gitlint/contrib/rules/authors_commit.py b/gitlint-core/gitlint/contrib/rules/authors_commit.py
index 5c4a150..d1e8cfa 100644
--- a/gitlint-core/gitlint/contrib/rules/authors_commit.py
+++ b/gitlint-core/gitlint/contrib/rules/authors_commit.py
@@ -1,7 +1,8 @@
import re
from pathlib import Path
-from typing import Tuple
+from typing import Set, Tuple
+from gitlint.git import GitContext
from gitlint.rules import CommitRule, RuleViolation
@@ -16,9 +17,12 @@ class AllowedAuthors(CommitRule):
id = "CC3"
@classmethod
- def _read_authors_from_file(cls, git_ctx) -> Tuple[str, str]:
+ def _read_authors_from_file(cls, git_ctx: GitContext) -> Tuple[Set[str], str]:
for file_name in cls.authors_file_names:
- path = Path(git_ctx.repository_path) / file_name
+ if git_ctx.repository_path:
+ path = Path(git_ctx.repository_path) / file_name
+ else:
+ path = Path(file_name)
if path.exists():
authors_file = path
break
diff --git a/gitlint-core/gitlint/rules.py b/gitlint-core/gitlint/rules.py
index 58b334f..9ac9a79 100644
--- a/gitlint-core/gitlint/rules.py
+++ b/gitlint-core/gitlint/rules.py
@@ -2,7 +2,7 @@ import copy
import logging
import re
from dataclasses import dataclass, field
-from typing import ClassVar, Dict, List, Optional
+from typing import ClassVar, Dict, List, Optional, Type
from gitlint.deprecation import Deprecation
from gitlint.exception import GitlintError
@@ -21,10 +21,10 @@ class Rule:
"""Class representing gitlint rules."""
# Class attributes
- options_spec: ClassVar[List] = []
+ options_spec: ClassVar[List[RuleOption]] = []
id: ClassVar[str]
name: ClassVar[str]
- target: ClassVar[Optional["LineRuleTarget"]] = None
+ target: ClassVar[Optional[Type["LineRuleTarget"]]] = None
_log: ClassVar[Optional[logging.Logger]] = None
# Instance attributes
diff --git a/gitlint-core/gitlint/utils.py b/gitlint-core/gitlint/utils.py
index ba4f956..e55983a 100644
--- a/gitlint-core/gitlint/utils.py
+++ b/gitlint-core/gitlint/utils.py
@@ -15,7 +15,7 @@ LOG_FORMAT = "%(levelname)s: %(name)s %(message)s"
# PLATFORM_IS_WINDOWS
-def platform_is_windows():
+def platform_is_windows() -> bool:
return "windows" in platform.system().lower()
@@ -26,7 +26,7 @@ PLATFORM_IS_WINDOWS = platform_is_windows()
# Encoding used for terminal encoding/decoding.
-def getpreferredencoding():
+def getpreferredencoding() -> str:
"""Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
on windows and falls back to UTF-8."""
fallback_encoding = "UTF-8"
@@ -38,8 +38,8 @@ def getpreferredencoding():
if PLATFORM_IS_WINDOWS:
preferred_encoding = fallback_encoding
for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]:
- encoding = os.environ.get(env_var, False)
- if encoding:
+ encoding = os.environ.get(env_var, None)
+ if encoding is not None:
# Support dotted (C.UTF-8) and non-dotted (C or UTF-8) charsets:
# If encoding contains a dot: split and use second part, otherwise use everything
dot_index = encoding.find(".")
diff --git a/pyproject.toml b/pyproject.toml
index 10a7e81..391d2e7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -91,6 +91,8 @@ dependencies = [
"ruff==0.0.252",
"radon==5.1.0",
"pdbr==0.8.2; sys_platform != \"win32\"",
+ "mypy==1.1.1",
+ "types-python-dateutil==2.8.19.12"
]
[tool.hatch.envs.test.scripts]
@@ -101,16 +103,18 @@ u = "unit-tests"
unit-tests-no-cov = "pytest -rw -s {args:gitlint-core}"
format = "black --check --diff {args:.}"
lint = "ruff {args:gitlint-core/gitlint qa}"
+type-check = "mypy {args}"
autoformat = "black {args:.}"
autofix = [
"- ruff --fix {args:gitlint-core/gitlint qa}",
- "autoformat", #
+ "autoformat", #
]
all = [
"unit-tests",
"format",
- "lint", #
+ "lint",
+ "type-check", #
]
stats = ["./tools/stats.sh"]
@@ -202,3 +206,50 @@ branch = true # measure branch coverage in addition to statement coverage
[tool.coverage.report]
fail_under = 97
show_missing = true
+
+[tool.mypy]
+# Selectively enable strictness options
+warn_unused_configs = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+no_implicit_optional = true
+strict_equality = true
+strict_concatenate = true
+disallow_subclassing_any = true
+disallow_untyped_decorators = true
+disallow_any_generics = true
+warn_return_any = true
+disallow_untyped_calls = true
+disallow_incomplete_defs = true
+
+# The following options are disabled because they're too strict for now
+# check_untyped_defs = true
+# disallow_untyped_defs = true
+# no_implicit_reexport = true
+
+exclude = [
+ "hatch_build.py",
+ "tools/*",
+ "gitlint-core/gitlint/tests/samples/user_rules/import_exception/invalid_python.py",
+]
+files = ["."]
+# Minimum supported python version by gitlint is 3.7, so enforce 3.7 typing semantics
+python_version = "3.7"
+
+[[tool.mypy.overrides]]
+# Ignore "Dataclass attribute may only be overridden by another attribute" errors in git.py
+module = "gitlint.git"
+disable_error_code = "misc"
+
+[[tool.mypy.overrides]]
+# Ignore in gitlint/__init__.py:
+# - Cannot find implementation or library stub for module named "importlib_metadata" [import]
+# - Call to untyped function "version" in typed context [no-untyped-call]" (Python 3.7)
+module = "gitlint"
+disable_error_code = ["import", "no-untyped-call"]
+
+[[tool.mypy.overrides]]
+# Ignore all errors in qa/shell.py (excluding this file isn't working because mypy include/exclude semantics
+# are unintuitive, so we ignore all errors instead)
+module = "qa.shell"
+ignore_errors = true
diff --git a/qa/utils.py b/qa/utils.py
index d560d86..752e1bb 100644
--- a/qa/utils.py
+++ b/qa/utils.py
@@ -6,7 +6,7 @@ import platform
# PLATFORM_IS_WINDOWS
-def platform_is_windows():
+def platform_is_windows() -> bool:
return "windows" in platform.system().lower()
@@ -19,7 +19,7 @@ PLATFORM_IS_WINDOWS = platform_is_windows()
# However, we want to be able to overwrite this behavior for testing using the GITLINT_QA_USE_SH_LIB env var.
-def use_sh_library():
+def use_sh_library() -> bool:
gitlint_use_sh_lib_env = os.environ.get("GITLINT_QA_USE_SH_LIB", None)
if gitlint_use_sh_lib_env:
return gitlint_use_sh_lib_env == "1"
@@ -33,7 +33,7 @@ USE_SH_LIB = use_sh_library()
# Encoding for reading gitlint command output
-def getpreferredencoding():
+def getpreferredencoding() -> str:
"""Use local.getpreferredencoding() or fallback to UTF-8."""
return locale.getpreferredencoding() or "UTF-8"