From 42969f6e696ae62fc0def2d6b42af4cfd90423c5 Mon Sep 17 00:00:00 2001 From: Lucidiot Date: Sat, 5 Oct 2019 13:09:17 +0000 Subject: [PATCH] Add static typing --- .gitignore | 1 + .gitlab-ci.yml | 5 ++++ docs/conf.py | 2 +- docs/contributing.rst | 11 +++++++++ requirements-dev.txt | 1 + setup.cfg | 7 ++++++ setup.py | 3 ++- twtxt_registry_client/__main__.py | 19 +++++++++++----- twtxt_registry_client/client.py | 38 ++++++++++++++++++++++--------- twtxt_registry_client/output.py | 29 +++++++++++------------ 10 files changed, 83 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 5b74a01..dafe072 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ coverage.xml *.cover .hypothesis/ .pytest_cache/ +.mypy_cache/ profile_default/ ipython_config.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2e34651..d872c3d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,6 +22,11 @@ flake8: script: - flake8 +mypy: + stage: test + script: + - mypy . + doc8: stage: test script: diff --git a/docs/conf.py b/docs/conf.py index 4e4cd40..d7f00cb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ import sys # documentation root, use os.path.abspath to make it absolute, like shown here. # BASE_DIR = Path(__file__).resolve().parent -sys.path.insert(0, BASE_DIR / '..') +sys.path.insert(0, str(BASE_DIR / '..')) # -- Project information ----------------------------------------------------- diff --git a/docs/contributing.rst b/docs/contributing.rst index 174d4ca..5f4c7f4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -39,6 +39,16 @@ The source code follows the PEP 8 code style and performs CI checks using the ``flake8`` tool. To perform the same checks locally, run ``flake8`` on the root directory of this repository. +Static typing +^^^^^^^^^^^^^ + +This package makes use of the standard `typing`_ module to include PEP 484 +type annotations. Type checking is done using the ``mypy`` tool and everything +in this package should be typed; this allows other packages to use *objtools* +and use static typing themselves or benefit from the enhanced documentations +or IDE warnings. To run type checking locally, run ``mypy`` on the root +directory of the repository. + Documentation ------------- @@ -51,5 +61,6 @@ They are also subject to linting using the ``doc8`` tool. .. _submit an issue: https://gitlab.com/Lucidiot/twtxt-registry-client/issues/new .. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io .. _GitLab repository: https://gitlab.com/Lucidiot/twtxt-registry-client +.. _typing: https://docs.python.org/3/library/typing.html .. _Sphinx: http://www.sphinx-doc.org/ .. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html diff --git a/requirements-dev.txt b/requirements-dev.txt index 1d3dfb8..31a7714 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ flake8>=3.7 Sphinx>=2.2 doc8>=0.8 +mypy>=0.730 diff --git a/setup.cfg b/setup.cfg index b963157..7189cc2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,10 @@ exclude = .git,__pycache__,docs,*.pyc,venv [doc8] ignore-path=**/*.txt,*.txt,*.egg-info,docs/_build,venv,.git + +[mypy] +ignore_missing_imports=True +disallow_incomplete_defs=True +disallow_untyped_defs=True +check_untyped_defs=True +no_implicit_optional=True diff --git a/setup.py b/setup.py index 3b4b0d3..941ebcd 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 +from typing import List from setuptools import setup, find_packages -def read_requirements(filename): +def read_requirements(filename: str) -> List[str]: return [req.strip() for req in open(filename)] diff --git a/twtxt_registry_client/__main__.py b/twtxt_registry_client/__main__.py index e74d6a4..cead844 100755 --- a/twtxt_registry_client/__main__.py +++ b/twtxt_registry_client/__main__.py @@ -2,6 +2,7 @@ from urllib.parse import urlsplit, urlunsplit from objtools.collections import Namespace from requests.exceptions import HTTPError +from typing import Optional from twtxt.config import Config from twtxt_registry_client import RegistryClient, output import click @@ -30,7 +31,11 @@ logger = logging.getLogger(__name__) help='Output logs to stderr for debugging purposes.', ) @click.pass_context -def cli(ctx, registry_url, insecure, format, verbose): +def cli(ctx: click.Context, + registry_url: str, + insecure: bool, + format: str, + verbose: bool) -> None: """ Command-line client for the twtxt registry API. @@ -81,7 +86,9 @@ def cli(ctx, registry_url, insecure, format, verbose): metavar='[URL]', ) @click.pass_context -def register(ctx, nickname, url): +def register(ctx: click.Context, + nickname: Optional[str], + url: Optional[str]) -> None: """ Register a user on a registry. """ @@ -111,7 +118,7 @@ def register(ctx, nickname, url): help='An optional search query to filter users.', ) @click.pass_context -def users(ctx, query): +def users(ctx: click.Context, query: Optional[str]) -> None: """ List and search users on a registry. """ @@ -129,7 +136,7 @@ def users(ctx, query): help='An optional search query to filter tweets.', ) @click.pass_context -def tweets(ctx, query): +def tweets(ctx: click.Context, query: Optional[str]) -> None: """ List and search tweets on a registry. """ @@ -144,7 +151,7 @@ def tweets(ctx, query): @cli.command() @click.argument('name_or_url', required=False) @click.pass_context -def mentions(ctx, name_or_url): +def mentions(ctx: click.Context, name_or_url: Optional[str]) -> None: """ List mentions to someone on a registry. @@ -194,7 +201,7 @@ def mentions(ctx, name_or_url): @cli.command() @click.argument('name', required=True) @click.pass_context -def tag(ctx, name): +def tag(ctx: click.Context, name: str) -> None: """ Search for tweets containing a tag. diff --git a/twtxt_registry_client/client.py b/twtxt_registry_client/client.py index 3e0a86e..cd92155 100644 --- a/twtxt_registry_client/client.py +++ b/twtxt_registry_client/client.py @@ -1,5 +1,6 @@ +from typing import Optional, Callable, Any import logging -import urllib +import urllib.parse import click import requests @@ -11,7 +12,10 @@ class RegistryClient(object): The twtxt registry API client. """ - def __init__(self, registry_url, insecure=False, disclose_identity=None): + def __init__(self, + registry_url: str, + insecure: bool = False, + disclose_identity: Optional[bool] = None): """ :param str registry_url: Base URL for the registry API. :param bool insecure: Disable SSL certificate checks @@ -46,8 +50,13 @@ class RegistryClient(object): logger.debug('Using user agent {!r}'.format(user_agent)) self.session.headers['User-Agent'] = user_agent - def request(self, method, endpoint, - *, format='plain', raise_exc=True, **params): + def request(self, + method: Callable, + endpoint: str, + *, + format: str = 'plain', + raise_exc: bool = True, + **params: Any) -> requests.Response: """ Perform an API request. @@ -83,7 +92,7 @@ class RegistryClient(object): resp.raise_for_status() return resp - def get(self, *args, **kwargs): + def get(self, *args: Any, **kwargs: Any) -> requests.Response: """ Perform a GET request. Passes all arguments to :meth:`RegistryClient.request`. @@ -95,7 +104,7 @@ class RegistryClient(object): """ return self.request(self.session.get, *args, **kwargs) - def post(self, *args, **kwargs): + def post(self, *args: Any, **kwargs: Any) -> requests.Response: """ Perform a POST request. Passes all arguments to :meth:`RegistryClient.request`. @@ -107,7 +116,10 @@ class RegistryClient(object): """ return self.request(self.session.post, *args, **kwargs) - def register(self, nickname, url, **kwargs): + def register(self, + nickname: str, + url: str, + **kwargs: Any) -> requests.Response: """ Register a new user. @@ -122,7 +134,9 @@ class RegistryClient(object): """ return self.post('users', nickname=nickname, url=url, **kwargs) - def list_users(self, *, q=None, **kwargs): + def list_users(self, *, + q: Optional[str] = None, + **kwargs: Any) -> requests.Response: """ List registered users. @@ -137,7 +151,9 @@ class RegistryClient(object): """ return self.get('users', q=q, **kwargs) - def list_tweets(self, *, q=None, **kwargs): + def list_tweets(self, *, + q: Optional[str] = None, + **kwargs: Any) -> requests.Response: """ List tweets from registered users. @@ -152,7 +168,7 @@ class RegistryClient(object): """ return self.get('tweets', q=q, **kwargs) - def list_mentions(self, url, **kwargs): + def list_mentions(self, url: str, **kwargs: Any) -> requests.Response: """ List tweets mentioning a given user. @@ -166,7 +182,7 @@ class RegistryClient(object): """ return self.get('mentions', url=url, **kwargs) - def list_tag_tweets(self, name, **kwargs): + def list_tag_tweets(self, name: str, **kwargs: Any) -> requests.Response: """ List tweets with a given tag. diff --git a/twtxt_registry_client/output.py b/twtxt_registry_client/output.py index c2a8dee..e24d237 100644 --- a/twtxt_registry_client/output.py +++ b/twtxt_registry_client/output.py @@ -7,6 +7,7 @@ import click import json import logging import humanize +import requests import textwrap logger = logging.getLogger(__name__) @@ -21,7 +22,7 @@ class FormatterRegistry(ClassRegistry): instanciated :data:`registry` in this module instead. """ - def check_value(self, value): + def check_value(self, value: type) -> None: """ Ensure that a new formatter class subclasses :class:`Formatter`. @@ -40,7 +41,7 @@ classes. """ -class FormatterMetaclass(registry.metaclass, ABCMeta): +class FormatterMetaclass(registry.metaclass, ABCMeta): # type: ignore """ The metaclass which allows auto-registration of each formatter. In most cases, you should not have to use this class directly; @@ -71,7 +72,7 @@ class Formatter(metaclass=FormatterMetaclass, register=False): # TODO: Add link to objtools docs here once they are published @abstractmethod - def format_response(self, resp): + def format_response(self, resp: requests.Response) -> str: """ Generic output for an HTTP response: generally, this would include the HTTP status code and the response body. This is used to output @@ -86,7 +87,7 @@ class Formatter(metaclass=FormatterMetaclass, register=False): """ @abstractmethod - def format_tweets(self, resp): + def format_tweets(self, resp: requests.Response) -> str: """ Output tweets from a successful HTTP response. The tweets can be obtained from ``resp.text`` and parsing of the response text is left @@ -100,7 +101,7 @@ class Formatter(metaclass=FormatterMetaclass, register=False): """ @abstractmethod - def format_users(self, resp): + def format_users(self, resp: requests.Response) -> str: """ Output users from a successful HTTP response. The users can be obtained from ``resp.text`` and parsing of the response text is left to the @@ -121,13 +122,13 @@ class RawFormatter(Formatter, key='raw'): Use ``-f raw`` or ``--format raw`` in the CLI to select it. """ - def format_response(self, resp): + def format_response(self, resp: requests.Response) -> str: return resp.text - def format_tweets(self, resp): + def format_tweets(self, resp: requests.Response) -> str: return resp.text - def format_users(self, resp): + def format_users(self, resp: requests.Response) -> str: return resp.text @@ -138,7 +139,7 @@ class JSONFormatter(Formatter, key='json'): Use ``-f json`` or ``--format json`` in the CLI to select it. """ - def format_response(self, resp): + def format_response(self, resp: requests.Response) -> str: """ Outputs a simple JSON payload for any HTTP response, including its HTTP status code, its URL and its body. @@ -162,7 +163,7 @@ class JSONFormatter(Formatter, key='json'): 'body': resp.text, }) - def format_tweets(self, resp): + def format_tweets(self, resp: requests.Response) -> str: """ Outputs a list of JSON objects for an HTTP response holding tweets, with the users' nickname and URL, the tweet's timestamp, and its @@ -196,7 +197,7 @@ class JSONFormatter(Formatter, key='json'): }) return json.dumps(output) - def format_users(self, resp): + def format_users(self, resp: requests.Response) -> str: """ Outputs a list of JSON objects for an HTTP response holding users, with their nickname, URL, and last update timestamp. Sample output:: @@ -244,7 +245,7 @@ class PrettyFormatter(Formatter, key='pretty'): 5: 'magenta', } - def format_response(self, resp): + def format_response(self, resp: requests.Response) -> str: """ Outputs an HTTP response in a syntax similar to a true HTTP response, with its status code, reason and body: @@ -273,7 +274,7 @@ class PrettyFormatter(Formatter, key='pretty'): body=resp.text, ) - def format_tweets(self, resp): + def format_tweets(self, resp: requests.Response) -> str: """ Outputs an HTTP response as a list of tweets, in a format similar to the output of the original ``twtxt`` CLI. @@ -333,7 +334,7 @@ class PrettyFormatter(Formatter, key='pretty'): return '\n\n'.join(output) - def format_users(self, resp): + def format_users(self, resp: requests.Response) -> str: """ Outputs an HTTP response as a list of users, in a format similar to the output of the original ``twtxt`` CLI.