diff --git a/.mypy.ini b/.mypy.ini
index ce4c89be1..c6d006826 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -4,3 +4,6 @@ files = gitlab/*.py
# disallow_incomplete_defs: This flag reports an error whenever it encounters a
# partly annotated function definition.
disallow_incomplete_defs = True
+# disallow_untyped_defs: This flag reports an error whenever it encounters a
+# function without type annotations or with incomplete type annotations.
+disallow_untyped_defs = True
diff --git a/gitlab/__main__.py b/gitlab/__main__.py
index 14a1fa2e2..b25cb4932 100644
--- a/gitlab/__main__.py
+++ b/gitlab/__main__.py
@@ -1,4 +1,5 @@
import gitlab.cli
-__name__ == "__main__" and gitlab.cli.main()
+if __name__ == "__main__":
+ gitlab.cli.main()
diff --git a/gitlab/base.py b/gitlab/base.py
index 7121cb0bb..c81c6d981 100644
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -17,7 +17,7 @@
import importlib
from types import ModuleType
-from typing import Any, Dict, NamedTuple, Optional, Tuple, Type
+from typing import Any, Dict, Iterable, NamedTuple, Optional, Tuple, Type
from .client import Gitlab, GitlabList
from gitlab import types as g_types
@@ -133,8 +133,8 @@ def __ne__(self, other: object) -> bool:
return self.get_id() != other.get_id()
return super(RESTObject, self) != other
- def __dir__(self):
- return super(RESTObject, self).__dir__() | self.attributes.keys()
+ def __dir__(self) -> Iterable[str]:
+ return set(self.attributes).union(super(RESTObject, self).__dir__())
def __hash__(self) -> int:
if not self.get_id():
@@ -155,7 +155,7 @@ def _update_attrs(self, new_attrs: Dict[str, Any]) -> None:
self.__dict__["_updated_attrs"] = {}
self.__dict__["_attrs"] = new_attrs
- def get_id(self):
+ def get_id(self) -> Any:
"""Returns the id of the resource."""
if self._id_attr is None or not hasattr(self, self._id_attr):
return None
@@ -207,10 +207,10 @@ def __iter__(self) -> "RESTObjectList":
def __len__(self) -> int:
return len(self._list)
- def __next__(self):
+ def __next__(self) -> RESTObject:
return self.next()
- def next(self):
+ def next(self) -> RESTObject:
data = self._list.next()
return self._obj_cls(self.manager, data)
diff --git a/gitlab/cli.py b/gitlab/cli.py
index 0a97ed7cf..a0efeec6c 100644
--- a/gitlab/cli.py
+++ b/gitlab/cli.py
@@ -21,7 +21,7 @@
import functools
import re
import sys
-from typing import Any, Callable, Dict, Optional, Tuple, Union
+from typing import Any, Callable, cast, Dict, Optional, Tuple, TypeVar, Union
import gitlab.config # noqa: F401
@@ -35,14 +35,21 @@
custom_actions: Dict[str, Dict[str, Tuple[Tuple[str, ...], Tuple[str, ...], bool]]] = {}
+# For an explanation of how these type-hints work see:
+# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
+#
+# The goal here is that functions which get decorated will retain their types.
+__F = TypeVar("__F", bound=Callable[..., Any])
+
+
def register_custom_action(
cls_names: Union[str, Tuple[str, ...]],
mandatory: Tuple[str, ...] = tuple(),
optional: Tuple[str, ...] = tuple(),
-) -> Callable:
- def wrap(f: Callable) -> Callable:
+) -> Callable[[__F], __F]:
+ def wrap(f: __F) -> __F:
@functools.wraps(f)
- def wrapped_f(*args, **kwargs):
+ def wrapped_f(*args: Any, **kwargs: Any) -> Any:
return f(*args, **kwargs)
# in_obj defines whether the method belongs to the obj or the manager
@@ -63,7 +70,7 @@ def wrapped_f(*args, **kwargs):
action = f.__name__.replace("_", "-")
custom_actions[final_name][action] = (mandatory, optional, in_obj)
- return wrapped_f
+ return cast(__F, wrapped_f)
return wrap
@@ -135,12 +142,16 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
return parser
-def _get_parser(cli_module):
+def _get_parser() -> argparse.ArgumentParser:
+ # NOTE: We must delay import of gitlab.v4.cli until now or
+ # otherwise it will cause circular import errors
+ import gitlab.v4.cli
+
parser = _get_base_parser()
- return cli_module.extend_parser(parser)
+ return gitlab.v4.cli.extend_parser(parser)
-def _parse_value(v):
+def _parse_value(v: Any) -> Any:
if isinstance(v, str) and v.startswith("@"):
# If the user-provided value starts with @, we try to read the file
# path provided after @ as the real value. Exit on any error.
@@ -162,18 +173,10 @@ def docs() -> argparse.ArgumentParser:
if "sphinx" not in sys.modules:
sys.exit("Docs parser is only intended for build_sphinx")
- # NOTE: We must delay import of gitlab.v4.cli until now or
- # otherwise it will cause circular import errors
- import gitlab.v4.cli
-
- return _get_parser(gitlab.v4.cli)
-
+ return _get_parser()
-def main():
- # NOTE: We must delay import of gitlab.v4.cli until now or
- # otherwise it will cause circular import errors
- import gitlab.v4.cli
+def main() -> None:
if "--version" in sys.argv:
print(gitlab.__version__)
sys.exit(0)
@@ -183,7 +186,7 @@ def main():
# This first parsing step is used to find the gitlab config to use, and
# load the propermodule (v3 or v4) accordingly. At that point we don't have
# any subparser setup
- (options, args) = parser.parse_known_args(sys.argv)
+ (options, _) = parser.parse_known_args(sys.argv)
try:
config = gitlab.config.GitlabConfigParser(options.gitlab, options.config_file)
except gitlab.config.ConfigError as e:
@@ -196,14 +199,14 @@ def main():
raise ModuleNotFoundError(name="gitlab.v%s.cli" % config.api_version)
# Now we build the entire set of subcommands and do the complete parsing
- parser = _get_parser(gitlab.v4.cli)
+ parser = _get_parser()
try:
import argcomplete # type: ignore
argcomplete.autocomplete(parser)
except Exception:
pass
- args = parser.parse_args(sys.argv[1:])
+ args = parser.parse_args()
config_files = args.config_file
gitlab_id = args.gitlab
@@ -216,7 +219,7 @@ def main():
action = args.whaction
what = args.what
- args = args.__dict__
+ args_dict = vars(args)
# Remove CLI behavior-related args
for item in (
"gitlab",
@@ -228,8 +231,8 @@ def main():
"version",
"output",
):
- args.pop(item)
- args = {k: _parse_value(v) for k, v in args.items() if v is not None}
+ args_dict.pop(item)
+ args_dict = {k: _parse_value(v) for k, v in args_dict.items() if v is not None}
try:
gl = gitlab.Gitlab.from_config(gitlab_id, config_files)
@@ -241,6 +244,4 @@ def main():
if debug:
gl.enable_debug()
- gitlab.v4.cli.run(gl, what, action, args, verbose, output, fields)
-
- sys.exit(0)
+ gitlab.v4.cli.run(gl, what, action, args_dict, verbose, output, fields)
diff --git a/gitlab/config.py b/gitlab/config.py
index c663bf841..d2a05df9b 100644
--- a/gitlab/config.py
+++ b/gitlab/config.py
@@ -206,7 +206,7 @@ def __init__(
except Exception:
pass
- def _get_values_from_helper(self):
+ def _get_values_from_helper(self) -> None:
"""Update attributes that may get values from an external helper program"""
for attr in HELPER_ATTRIBUTES:
value = getattr(self, attr)
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
index f5b3600e1..77a74927b 100644
--- a/gitlab/exceptions.py
+++ b/gitlab/exceptions.py
@@ -16,10 +16,16 @@
# along with this program. If not, see .
import functools
+from typing import Any, Callable, cast, Optional, Type, TypeVar, TYPE_CHECKING, Union
class GitlabError(Exception):
- def __init__(self, error_message="", response_code=None, response_body=None):
+ def __init__(
+ self,
+ error_message: Union[str, bytes] = "",
+ response_code: Optional[int] = None,
+ response_body: Optional[bytes] = None,
+ ) -> None:
Exception.__init__(self, error_message)
# Http status code
@@ -30,11 +36,15 @@ def __init__(self, error_message="", response_code=None, response_body=None):
try:
# if we receive str/bytes we try to convert to unicode/str to have
# consistent message types (see #616)
+ if TYPE_CHECKING:
+ assert isinstance(error_message, bytes)
self.error_message = error_message.decode()
except Exception:
+ if TYPE_CHECKING:
+ assert isinstance(error_message, str)
self.error_message = error_message
- def __str__(self):
+ def __str__(self) -> str:
if self.response_code is not None:
return "{0}: {1}".format(self.response_code, self.error_message)
else:
@@ -269,7 +279,14 @@ class GitlabUnfollowError(GitlabOperationError):
pass
-def on_http_error(error):
+# For an explanation of how these type-hints work see:
+# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
+#
+# The goal here is that functions which get decorated will retain their types.
+__F = TypeVar("__F", bound=Callable[..., Any])
+
+
+def on_http_error(error: Type[Exception]) -> Callable[[__F], __F]:
"""Manage GitlabHttpError exceptions.
This decorator function can be used to catch GitlabHttpError exceptions
@@ -280,14 +297,14 @@ def on_http_error(error):
GitlabError
"""
- def wrap(f):
+ def wrap(f: __F) -> __F:
@functools.wraps(f)
- def wrapped_f(*args, **kwargs):
+ def wrapped_f(*args: Any, **kwargs: Any) -> Any:
try:
return f(*args, **kwargs)
except GitlabHttpError as e:
raise error(e.error_message, e.response_code, e.response_body) from e
- return wrapped_f
+ return cast(__F, wrapped_f)
return wrap
diff --git a/gitlab/tests/test_cli.py b/gitlab/tests/test_cli.py
index 2246369e5..aed9fc41a 100644
--- a/gitlab/tests/test_cli.py
+++ b/gitlab/tests/test_cli.py
@@ -26,7 +26,6 @@
import pytest
from gitlab import cli
-import gitlab.v4.cli
def test_what_to_cls():
@@ -94,14 +93,14 @@ def test_base_parser():
def test_v4_parse_args():
- parser = cli._get_parser(gitlab.v4.cli)
+ parser = cli._get_parser()
args = parser.parse_args(["project", "list"])
assert args.what == "project"
assert args.whaction == "list"
def test_v4_parser():
- parser = cli._get_parser(gitlab.v4.cli)
+ parser = cli._get_parser()
subparsers = next(
action
for action in parser._actions
diff --git a/gitlab/types.py b/gitlab/types.py
index 0495c972c..22d51e718 100644
--- a/gitlab/types.py
+++ b/gitlab/types.py
@@ -15,46 +15,50 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
+from typing import Any, Optional, TYPE_CHECKING
+
class GitlabAttribute(object):
- def __init__(self, value=None):
+ def __init__(self, value: Any = None) -> None:
self._value = value
- def get(self):
+ def get(self) -> Any:
return self._value
- def set_from_cli(self, cli_value):
+ def set_from_cli(self, cli_value: Any) -> None:
self._value = cli_value
- def get_for_api(self):
+ def get_for_api(self) -> Any:
return self._value
class ListAttribute(GitlabAttribute):
- def set_from_cli(self, cli_value):
+ def set_from_cli(self, cli_value: str) -> None:
if not cli_value.strip():
self._value = []
else:
self._value = [item.strip() for item in cli_value.split(",")]
- def get_for_api(self):
+ def get_for_api(self) -> str:
# Do not comma-split single value passed as string
if isinstance(self._value, str):
return self._value
+ if TYPE_CHECKING:
+ assert isinstance(self._value, list)
return ",".join([str(x) for x in self._value])
class LowercaseStringAttribute(GitlabAttribute):
- def get_for_api(self):
+ def get_for_api(self) -> str:
return str(self._value).lower()
class FileAttribute(GitlabAttribute):
- def get_file_name(self, attr_name=None):
+ def get_file_name(self, attr_name: Optional[str] = None) -> Optional[str]:
return attr_name
class ImageAttribute(FileAttribute):
- def get_file_name(self, attr_name=None):
+ def get_file_name(self, attr_name: Optional[str] = None) -> str:
return "%s.png" % attr_name if attr_name else "image.png"