From fdec03976a17e0708459ba2fab22f54173295f71 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Mon, 15 Feb 2021 10:13:35 -0800 Subject: [PATCH 1/2] feat: add an initial mypy test to tox.ini Add an initial mypy test to test gitlab/base.py and gitlab/__init__.py --- .github/workflows/lint.yml | 8 ++++++++ .mypy.ini | 2 ++ gitlab/cli.py | 2 +- gitlab/client.py | 2 +- test-requirements.txt | 1 + tox.ini | 9 ++++++++- 6 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .mypy.ini diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 968320daf..4b918df52 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -27,3 +27,11 @@ jobs: with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v2 + + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install --upgrade tox + - run: tox -e mypy diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 000000000..e68f0f616 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,2 @@ +[mypy] +files = gitlab/*.py diff --git a/gitlab/cli.py b/gitlab/cli.py index d858a7445..3a315a807 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -193,7 +193,7 @@ def main(): # Now we build the entire set of subcommands and do the complete parsing parser = _get_parser(gitlab.v4.cli) try: - import argcomplete + import argcomplete # type: ignore argcomplete.autocomplete(parser) except Exception: diff --git a/gitlab/client.py b/gitlab/client.py index 43cee1044..910926a67 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -25,7 +25,7 @@ import gitlab.const import gitlab.exceptions from gitlab import utils -from requests_toolbelt.multipart.encoder import MultipartEncoder +from requests_toolbelt.multipart.encoder import MultipartEncoder # type: ignore REDIRECT_MSG = ( diff --git a/test-requirements.txt b/test-requirements.txt index 8d61ad154..53456adab 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,7 @@ coverage httmock mock +mypy pytest pytest-cov responses diff --git a/tox.ini b/tox.ini index ba64a4371..826e08128 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 1.6 skipsdist = True -envlist = py39,py38,py37,py36,pep8,black,twine-check +envlist = py39,py38,py37,py36,pep8,black,twine-check,mypy [testenv] passenv = GITLAB_IMAGE GITLAB_TAG @@ -35,6 +35,13 @@ deps = -r{toxinidir}/requirements.txt commands = twine check dist/* +[testenv:mypy] +basepython = python3 +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = + mypy {posargs} + [testenv:venv] commands = {posargs} From 3727cbd21fc40b312573ca8da56e0f6cf9577d08 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Sat, 20 Feb 2021 11:25:38 -0800 Subject: [PATCH 2/2] chore: add type hints to gitlab/base.py --- gitlab/base.py | 63 +++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/gitlab/base.py b/gitlab/base.py index 6d92fdf87..f0bedc700 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -16,7 +16,9 @@ # along with this program. If not, see . import importlib +from typing import Any, Dict, Optional +from .client import Gitlab, GitlabList __all__ = [ "RESTObject", @@ -38,7 +40,7 @@ class RESTObject(object): _id_attr = "id" - def __init__(self, manager, attrs): + def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None: self.__dict__.update( { "manager": manager, @@ -50,18 +52,18 @@ def __init__(self, manager, attrs): self.__dict__["_parent_attrs"] = self.manager.parent_attrs self._create_managers() - def __getstate__(self): + def __getstate__(self) -> Dict[str, Any]: state = self.__dict__.copy() module = state.pop("_module") state["_module_name"] = module.__name__ return state - def __setstate__(self, state): + def __setstate__(self, state: Dict[str, Any]) -> None: module_name = state.pop("_module_name") self.__dict__.update(state) self.__dict__["_module"] = importlib.import_module(module_name) - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: try: return self.__dict__["_updated_attrs"][name] except KeyError: @@ -90,15 +92,15 @@ def __getattr__(self, name): except KeyError: raise AttributeError(name) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value) -> None: self.__dict__["_updated_attrs"][name] = value - def __str__(self): + def __str__(self) -> str: data = self._attrs.copy() data.update(self._updated_attrs) return "%s => %s" % (type(self), data) - def __repr__(self): + def __repr__(self) -> str: if self._id_attr: return "<%s %s:%s>" % ( self.__class__.__name__, @@ -108,12 +110,12 @@ def __repr__(self): else: return "<%s>" % self.__class__.__name__ - def __eq__(self, other): + def __eq__(self, other) -> bool: if self.get_id() and other.get_id(): return self.get_id() == other.get_id() return super(RESTObject, self) == other - def __ne__(self, other): + def __ne__(self, other) -> bool: if self.get_id() and other.get_id(): return self.get_id() != other.get_id() return super(RESTObject, self) != other @@ -121,12 +123,12 @@ def __ne__(self, other): def __dir__(self): return super(RESTObject, self).__dir__() + list(self.attributes) - def __hash__(self): + def __hash__(self) -> int: if not self.get_id(): return super(RESTObject, self).__hash__() return hash(self.get_id()) - def _create_managers(self): + def _create_managers(self) -> None: managers = getattr(self, "_managers", None) if managers is None: return @@ -136,7 +138,7 @@ def _create_managers(self): manager = cls(self.manager.gitlab, parent=self) self.__dict__[attr] = manager - def _update_attrs(self, new_attrs): + def _update_attrs(self, new_attrs) -> None: self.__dict__["_updated_attrs"] = {} self.__dict__["_attrs"] = new_attrs @@ -147,7 +149,7 @@ def get_id(self): return getattr(self, self._id_attr) @property - def attributes(self): + def attributes(self) -> Dict[str, Any]: d = self.__dict__["_updated_attrs"].copy() d.update(self.__dict__["_attrs"]) d.update(self.__dict__["_parent_attrs"]) @@ -169,7 +171,7 @@ class RESTObjectList(object): _list: A GitlabList object """ - def __init__(self, manager, obj_cls, _list): + def __init__(self, manager: "RESTManager", obj_cls, _list: GitlabList) -> None: """Creates an objects list from a GitlabList. You should not create objects of this type, but use managers list() @@ -184,10 +186,10 @@ def __init__(self, manager, obj_cls, _list): self._obj_cls = obj_cls self._list = _list - def __iter__(self): + def __iter__(self) -> "RESTObjectList": return self - def __len__(self): + def __len__(self) -> int: return len(self._list) def __next__(self): @@ -198,12 +200,12 @@ def next(self): return self._obj_cls(self.manager, data) @property - def current_page(self): + def current_page(self) -> int: """The current page number.""" return self._list.current_page @property - def prev_page(self): + def prev_page(self) -> int: """The previous page number. If None, the current page is the first. @@ -211,7 +213,7 @@ def prev_page(self): return self._list.prev_page @property - def next_page(self): + def next_page(self) -> int: """The next page number. If None, the current page is the last. @@ -219,17 +221,17 @@ def next_page(self): return self._list.next_page @property - def per_page(self): + def per_page(self) -> int: """The number of items per page.""" return self._list.per_page @property - def total_pages(self): + def total_pages(self) -> int: """The total number of pages.""" return self._list.total_pages @property - def total(self): + def total(self) -> int: """The total number of items.""" return self._list.total @@ -243,10 +245,11 @@ class RESTManager(object): ``_obj_cls``: The class of objects that will be created """ - _path = None - _obj_cls = None + _path: Optional[str] = None + _obj_cls: Optional[Any] = None + _from_parent_attrs: Dict[str, Any] = {} - def __init__(self, gl, parent=None): + def __init__(self, gl: Gitlab, parent: Optional[RESTObject] = None) -> None: """REST manager constructor. Args: @@ -259,23 +262,25 @@ def __init__(self, gl, parent=None): self._computed_path = self._compute_path() @property - def parent_attrs(self): + def parent_attrs(self) -> Optional[Dict[str, Any]]: return self._parent_attrs - def _compute_path(self, path=None): + def _compute_path(self, path: Optional[str] = None) -> Optional[str]: self._parent_attrs = {} if path is None: path = self._path + if path is None: + return None if self._parent is None or not hasattr(self, "_from_parent_attrs"): return path data = { self_attr: getattr(self._parent, parent_attr, None) - for self_attr, parent_attr in self._from_parent_attrs.items() + for self_attr, parent_attr in self._from_parent_attrs.items() # type: ignore } self._parent_attrs = data return path % data @property - def path(self): + def path(self) -> Optional[str]: return self._computed_path