Merge branch 'typing' into 'master'
Add static typing Closes #9 See merge request Lucidiot/twtxt-registry-client!1
This commit is contained in:
commit
1d7648e6ea
|
@ -32,6 +32,7 @@ coverage.xml
|
|||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
|
|
@ -22,6 +22,11 @@ flake8:
|
|||
script:
|
||||
- flake8
|
||||
|
||||
mypy:
|
||||
stage: test
|
||||
script:
|
||||
- mypy .
|
||||
|
||||
doc8:
|
||||
stage: test
|
||||
script:
|
||||
|
|
|
@ -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 -----------------------------------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
flake8>=3.7
|
||||
Sphinx>=2.2
|
||||
doc8>=0.8
|
||||
mypy>=0.730
|
||||
|
|
|
@ -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
|
||||
|
|
3
setup.py
3
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)]
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Reference in New Issue