Add static typing

This commit is contained in:
Lucidiot 2019-10-05 13:09:17 +00:00
parent 22bf2d1246
commit 42969f6e69
10 changed files with 83 additions and 33 deletions

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
.mypy_cache/
profile_default/
ipython_config.py

View File

@ -22,6 +22,11 @@ flake8:
script:
- flake8
mypy:
stage: test
script:
- mypy .
doc8:
stage: test
script:

View File

@ -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 -----------------------------------------------------

View File

@ -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

View File

@ -1,3 +1,4 @@
flake8>=3.7
Sphinx>=2.2
doc8>=0.8
mypy>=0.730

View File

@ -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

View File

@ -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)]

View File

@ -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.

View File

@ -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.

View File

@ -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.