Add static typing
This commit is contained in:
parent
22bf2d1246
commit
42969f6e69
|
@ -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
|
||||||
|
|
|
@ -22,6 +22,11 @@ flake8:
|
||||||
script:
|
script:
|
||||||
- flake8
|
- flake8
|
||||||
|
|
||||||
|
mypy:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- mypy .
|
||||||
|
|
||||||
doc8:
|
doc8:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
|
|
|
@ -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 -----------------------------------------------------
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -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)]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Reference in New Issue