Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type checking with mypy #726

Merged
merged 25 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fc06a4c
Add mypy dev dependency
carmenbianca Apr 9, 2023
08f73ba
Add mypy to pre-commit
carmenbianca Apr 9, 2023
2927f70
Mypy: ignore missing imports for certain modules
carmenbianca Apr 9, 2023
93f86c3
Mypy: disallow untyped and incomplete function defs
carmenbianca Apr 9, 2023
f908109
Exclude small scripts from mypy type checking
carmenbianca Apr 10, 2023
6404d6a
Set default files to type check
carmenbianca Apr 10, 2023
b5d05eb
Add mypy instruction to CONTRIBUTING
carmenbianca Apr 9, 2023
c918059
Add py.typed marker
carmenbianca Apr 9, 2023
394663a
Add GitHub job for mypy
carmenbianca Apr 9, 2023
a3f092b
Make _util mypy-compliant
carmenbianca Apr 9, 2023
a21c34f
Make comment mypy-compliant
carmenbianca Apr 9, 2023
21906d7
Make vcs mypy-compliant
carmenbianca Apr 9, 2023
3a92cdd
Make project mypy-compliant
carmenbianca Apr 9, 2023
f9108d6
Make header mypy-compliant
carmenbianca Apr 9, 2023
3890dcf
Make _main mypy-compliant
carmenbianca Apr 9, 2023
b0a7540
Make init mypy-compliant
carmenbianca Apr 9, 2023
8ba2fe3
Make conftest mypy-compliant
carmenbianca Apr 9, 2023
8b726a5
Make other tests mypy-compliant
carmenbianca Apr 9, 2023
7e1b376
Make download mypy-compliant
carmenbianca Apr 9, 2023
de584f4
Make spdx mypy-compliant
carmenbianca Apr 9, 2023
30ea22a
Make _format mypy-compliant
carmenbianca Apr 10, 2023
ad05304
Make _licenses mypy-compliant
carmenbianca Apr 10, 2023
40d1786
Make supported_licenses mypy-compliant
carmenbianca Apr 10, 2023
ecf467e
Make report mypy-compliant
carmenbianca Jun 10, 2023
bf40ad4
Make lint mypy-compliant
carmenbianca Jun 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ jobs:
poetry run isort --check src/ tests/
poetry run black .

mypy:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install dependencies
run: |
pip install poetry
poetry install --no-interaction
- name: Test typing with mypy
run: |
poetry run mypy

prettier:
runs-on: ubuntu-20.04
container: node:latest
Expand Down
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ repos:
- id: isort
name: isort (pyi)
types: [pyi]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.2.0
hooks:
- id: mypy
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Next, you'll find the following commands handy:
- `poetry run reuse`
- `poetry run pytest`
- `poetry run pylint src`
- `poetry run mypy`
- `make docs`

## Development conventions
Expand Down
49 changes: 48 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pre-commit = "^2.9.0"
pylint = "^2.12.2"
pytest = ">=6.0.0"
pytest-cov = ">=2.10.0"
mypy = "^1.0"

[tool.poetry.scripts]
reuse = 'reuse._main:main'
Expand All @@ -94,3 +95,27 @@ line_length = 80

[tool.pytest.ini_options]
addopts = "--doctest-modules"

[tool.mypy]
files = [
"src/reuse/**.py",
"tests/**.py",
]
exclude = [
'^_build\.py$',
'^conf\.py$',
]

[[tool.mypy.overrides]]
module = "reuse.*"
disallow_untyped_defs = true
disallow_incomplete_defs = true

[[tool.mypy.overrides]]
module = [
"binaryornot.check",
"boolean.boolean",
"license_expression",
"pkg_resources",
]
ignore_missing_imports = true
16 changes: 8 additions & 8 deletions src/reuse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from dataclasses import dataclass, field
from enum import Enum, auto
from importlib.metadata import PackageNotFoundError, version
from typing import NamedTuple, Optional, Set, Type
from typing import Any, Dict, NamedTuple, Optional, Set, Type

from boolean.boolean import Expression

Expand Down Expand Up @@ -109,25 +109,25 @@ class ReuseInfo:
source_path: Optional[str] = None
source_type: Optional[SourceType] = None

def _check_nonexistent(self, **kwargs) -> None:
def _check_nonexistent(self, **kwargs: Any) -> None:
nonexistent_attributes = set(kwargs) - set(self.__dict__)
if nonexistent_attributes:
raise KeyError(
f"The following attributes do not exist in"
f" {self.__class__}: {', '.join(nonexistent_attributes)}"
)

def copy(self, **kwargs) -> Type["ReuseInfo"]:
def copy(self, **kwargs: Any) -> "ReuseInfo":
"""Return a copy of ReuseInfo, replacing the values of attributes with
the values from *kwargs*.
"""
self._check_nonexistent(**kwargs)
new_kwargs = {}
for key, value in self.__dict__.items():
new_kwargs[key] = kwargs.get(key, value)
return self.__class__(**new_kwargs)
return self.__class__(**new_kwargs) # type: ignore

def union(self, value) -> Type["ReuseInfo"]:
def union(self, value: "ReuseInfo") -> "ReuseInfo":
"""Return a new instance of ReuseInfo where all Set attributes are equal
to the union of the set in *self* and the set in *value*.

Expand All @@ -147,16 +147,16 @@ def union(self, value) -> Type["ReuseInfo"]:
new_kwargs[key] = attr_val.union(other_val)
else:
new_kwargs[key] = attr_val
return self.__class__(**new_kwargs)
return self.__class__(**new_kwargs) # type: ignore

def contains_copyright_or_licensing(self) -> bool:
"""Either *spdx_expressions* or *copyright_lines* is non-empty."""
return bool(self.spdx_expressions or self.copyright_lines)

def __bool__(self):
def __bool__(self) -> bool:
return any(self.__dict__.values())

def __or__(self, value) -> Type["ReuseInfo"]:
def __or__(self, value: "ReuseInfo") -> "ReuseInfo":
return self.union(value)


Expand Down
7 changes: 4 additions & 3 deletions src/reuse/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,28 @@
"""Formatting functions primarily for the CLI."""

from textwrap import fill, indent
from typing import Iterator

WIDTH = 78
INDENT = 2


def fill_paragraph(text, width=WIDTH, indent_width=0):
def fill_paragraph(text: str, width: int = WIDTH, indent_width: int = 0) -> str:
"""Wrap a single paragraph."""
return indent(
fill(text.strip(), width=width - indent_width), indent_width * " "
)


def fill_all(text, width=WIDTH, indent_width=0):
def fill_all(text: str, width: int = WIDTH, indent_width: int = 0) -> str:
"""Wrap all paragraphs."""
return "\n\n".join(
fill_paragraph(paragraph, width=width, indent_width=indent_width)
for paragraph in split_into_paragraphs(text)
)


def split_into_paragraphs(text):
def split_into_paragraphs(text: str) -> Iterator[str]:
"""Yield all paragraphs in a text. A paragraph is a piece of text
surrounded by empty lines.
"""
Expand Down
5 changes: 3 additions & 2 deletions src/reuse/_licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@

import json
import os
from typing import Dict, List, Tuple

_BASE_DIR = os.path.dirname(__file__)
_RESOURCES_DIR = os.path.join(_BASE_DIR, "resources")
_LICENSES = os.path.join(_RESOURCES_DIR, "licenses.json")
_EXCEPTIONS = os.path.join(_RESOURCES_DIR, "exceptions.json")


def _load_license_list(file_name):
def _load_license_list(file_name: str) -> Tuple[List[int], Dict[str, Dict]]:
"""Return the licenses list version tuple and a mapping of licenses
id->name loaded from a JSON file
from https://github.com/spdx/license-list-data
Expand All @@ -33,7 +34,7 @@ def _load_license_list(file_name):
return version, licenses_map


def _load_exception_list(file_name):
def _load_exception_list(file_name: str) -> Tuple[List[int], Dict[str, Dict]]:
"""Return the exceptions list version tuple and a mapping of
exceptions id->name loaded from a JSON file
from https://github.com/spdx/license-list-data
Expand Down
20 changes: 10 additions & 10 deletions src/reuse/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import logging
import sys
from gettext import gettext as _
from typing import List
from typing import IO, Callable, List, Optional, Type, cast

from . import (
__REUSE_version__,
Expand Down Expand Up @@ -238,14 +238,14 @@ def parser() -> argparse.ArgumentParser:


def add_command( # pylint: disable=too-many-arguments,redefined-builtin
subparsers,
subparsers: argparse._SubParsersAction,
name: str,
add_arguments_func,
run_func,
formatter_class=None,
description: str = None,
help: str = None,
aliases: list = None,
add_arguments_func: Callable[[argparse.ArgumentParser], None],
run_func: Callable[[argparse.Namespace, Project, IO[str]], int],
formatter_class: Optional[Type[argparse.HelpFormatter]] = None,
description: Optional[str] = None,
help: Optional[str] = None,
aliases: Optional[List[str]] = None,
) -> None:
"""Add a subparser for a command."""
if formatter_class is None:
Expand All @@ -262,10 +262,10 @@ def add_command( # pylint: disable=too-many-arguments,redefined-builtin
subparser.set_defaults(parser=subparser)


def main(args: List[str] = None, out=sys.stdout) -> int:
def main(args: Optional[List[str]] = None, out: IO[str] = sys.stdout) -> int:
"""Main entry function."""
if args is None:
args = sys.argv[1:]
args = cast(List[str], sys.argv[1:])

main_parser = parser()
parsed_args = main_parser.parse_args(args)
Expand Down
Loading