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 *.cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
.mypy_cache/
profile_default/ profile_default/
ipython_config.py ipython_config.py

View File

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

View File

@ -14,7 +14,7 @@ import sys
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
# #
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
sys.path.insert(0, BASE_DIR / '..') sys.path.insert(0, str(BASE_DIR / '..'))
# -- Project information ----------------------------------------------------- # -- 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 ``flake8`` tool. To perform the same checks locally, run ``flake8`` on the root
directory of this repository. 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 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 .. _submit an issue: https://gitlab.com/Lucidiot/twtxt-registry-client/issues/new
.. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io .. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io
.. _GitLab repository: https://gitlab.com/Lucidiot/twtxt-registry-client .. _GitLab repository: https://gitlab.com/Lucidiot/twtxt-registry-client
.. _typing: https://docs.python.org/3/library/typing.html
.. _Sphinx: http://www.sphinx-doc.org/ .. _Sphinx: http://www.sphinx-doc.org/
.. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html .. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html

View File

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

View File

@ -3,3 +3,10 @@ exclude = .git,__pycache__,docs,*.pyc,venv
[doc8] [doc8]
ignore-path=**/*.txt,*.txt,*.egg-info,docs/_build,venv,.git 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 #!/usr/bin/env python3
from typing import List
from setuptools import setup, find_packages 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)] return [req.strip() for req in open(filename)]

View File

@ -2,6 +2,7 @@
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
from objtools.collections import Namespace from objtools.collections import Namespace
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from typing import Optional
from twtxt.config import Config from twtxt.config import Config
from twtxt_registry_client import RegistryClient, output from twtxt_registry_client import RegistryClient, output
import click import click
@ -30,7 +31,11 @@ logger = logging.getLogger(__name__)
help='Output logs to stderr for debugging purposes.', help='Output logs to stderr for debugging purposes.',
) )
@click.pass_context @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. Command-line client for the twtxt registry API.
@ -81,7 +86,9 @@ def cli(ctx, registry_url, insecure, format, verbose):
metavar='[URL]', metavar='[URL]',
) )
@click.pass_context @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. Register a user on a registry.
""" """
@ -111,7 +118,7 @@ def register(ctx, nickname, url):
help='An optional search query to filter users.', help='An optional search query to filter users.',
) )
@click.pass_context @click.pass_context
def users(ctx, query): def users(ctx: click.Context, query: Optional[str]) -> None:
""" """
List and search users on a registry. List and search users on a registry.
""" """
@ -129,7 +136,7 @@ def users(ctx, query):
help='An optional search query to filter tweets.', help='An optional search query to filter tweets.',
) )
@click.pass_context @click.pass_context
def tweets(ctx, query): def tweets(ctx: click.Context, query: Optional[str]) -> None:
""" """
List and search tweets on a registry. List and search tweets on a registry.
""" """
@ -144,7 +151,7 @@ def tweets(ctx, query):
@cli.command() @cli.command()
@click.argument('name_or_url', required=False) @click.argument('name_or_url', required=False)
@click.pass_context @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. List mentions to someone on a registry.
@ -194,7 +201,7 @@ def mentions(ctx, name_or_url):
@cli.command() @cli.command()
@click.argument('name', required=True) @click.argument('name', required=True)
@click.pass_context @click.pass_context
def tag(ctx, name): def tag(ctx: click.Context, name: str) -> None:
""" """
Search for tweets containing a tag. Search for tweets containing a tag.

View File

@ -1,5 +1,6 @@
from typing import Optional, Callable, Any
import logging import logging
import urllib import urllib.parse
import click import click
import requests import requests
@ -11,7 +12,10 @@ class RegistryClient(object):
The twtxt registry API client. 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 str registry_url: Base URL for the registry API.
:param bool insecure: Disable SSL certificate checks :param bool insecure: Disable SSL certificate checks
@ -46,8 +50,13 @@ class RegistryClient(object):
logger.debug('Using user agent {!r}'.format(user_agent)) logger.debug('Using user agent {!r}'.format(user_agent))
self.session.headers['User-Agent'] = user_agent self.session.headers['User-Agent'] = user_agent
def request(self, method, endpoint, def request(self,
*, format='plain', raise_exc=True, **params): method: Callable,
endpoint: str,
*,
format: str = 'plain',
raise_exc: bool = True,
**params: Any) -> requests.Response:
""" """
Perform an API request. Perform an API request.
@ -83,7 +92,7 @@ class RegistryClient(object):
resp.raise_for_status() resp.raise_for_status()
return resp return resp
def get(self, *args, **kwargs): def get(self, *args: Any, **kwargs: Any) -> requests.Response:
""" """
Perform a GET request. Perform a GET request.
Passes all arguments to :meth:`RegistryClient.request`. Passes all arguments to :meth:`RegistryClient.request`.
@ -95,7 +104,7 @@ class RegistryClient(object):
""" """
return self.request(self.session.get, *args, **kwargs) 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. Perform a POST request.
Passes all arguments to :meth:`RegistryClient.request`. Passes all arguments to :meth:`RegistryClient.request`.
@ -107,7 +116,10 @@ class RegistryClient(object):
""" """
return self.request(self.session.post, *args, **kwargs) 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. Register a new user.
@ -122,7 +134,9 @@ class RegistryClient(object):
""" """
return self.post('users', nickname=nickname, url=url, **kwargs) 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. List registered users.
@ -137,7 +151,9 @@ class RegistryClient(object):
""" """
return self.get('users', q=q, **kwargs) 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. List tweets from registered users.
@ -152,7 +168,7 @@ class RegistryClient(object):
""" """
return self.get('tweets', q=q, **kwargs) 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. List tweets mentioning a given user.
@ -166,7 +182,7 @@ class RegistryClient(object):
""" """
return self.get('mentions', url=url, **kwargs) 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. List tweets with a given tag.

View File

@ -7,6 +7,7 @@ import click
import json import json
import logging import logging
import humanize import humanize
import requests
import textwrap import textwrap
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,7 +22,7 @@ class FormatterRegistry(ClassRegistry):
instanciated :data:`registry` in this module instead. 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`. 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. The metaclass which allows auto-registration of each formatter.
In most cases, you should not have to use this class directly; 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 # TODO: Add link to objtools docs here once they are published
@abstractmethod @abstractmethod
def format_response(self, resp): def format_response(self, resp: requests.Response) -> str:
""" """
Generic output for an HTTP response: generally, this would include Generic output for an HTTP response: generally, this would include
the HTTP status code and the response body. This is used to output the HTTP status code and the response body. This is used to output
@ -86,7 +87,7 @@ class Formatter(metaclass=FormatterMetaclass, register=False):
""" """
@abstractmethod @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 Output tweets from a successful HTTP response. The tweets can be
obtained from ``resp.text`` and parsing of the response text is left obtained from ``resp.text`` and parsing of the response text is left
@ -100,7 +101,7 @@ class Formatter(metaclass=FormatterMetaclass, register=False):
""" """
@abstractmethod @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 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 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. 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 return resp.text
def format_tweets(self, resp): def format_tweets(self, resp: requests.Response) -> str:
return resp.text return resp.text
def format_users(self, resp): def format_users(self, resp: requests.Response) -> str:
return resp.text return resp.text
@ -138,7 +139,7 @@ class JSONFormatter(Formatter, key='json'):
Use ``-f json`` or ``--format json`` in the CLI to select it. 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 Outputs a simple JSON payload for any HTTP response, including its
HTTP status code, its URL and its body. HTTP status code, its URL and its body.
@ -162,7 +163,7 @@ class JSONFormatter(Formatter, key='json'):
'body': resp.text, '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, Outputs a list of JSON objects for an HTTP response holding tweets,
with the users' nickname and URL, the tweet's timestamp, and its 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) 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, Outputs a list of JSON objects for an HTTP response holding users,
with their nickname, URL, and last update timestamp. Sample output:: with their nickname, URL, and last update timestamp. Sample output::
@ -244,7 +245,7 @@ class PrettyFormatter(Formatter, key='pretty'):
5: 'magenta', 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, Outputs an HTTP response in a syntax similar to a true HTTP response,
with its status code, reason and body: with its status code, reason and body:
@ -273,7 +274,7 @@ class PrettyFormatter(Formatter, key='pretty'):
body=resp.text, 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 Outputs an HTTP response as a list of tweets, in a format similar to
the output of the original ``twtxt`` CLI. the output of the original ``twtxt`` CLI.
@ -333,7 +334,7 @@ class PrettyFormatter(Formatter, key='pretty'):
return '\n\n'.join(output) 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 Outputs an HTTP response as a list of users, in a format similar to
the output of the original ``twtxt`` CLI. the output of the original ``twtxt`` CLI.