zulip_bots: Use python3 type annotations.

This commit is contained in:
dkvasov 2018-05-17 16:30:48 +03:00
parent bb39ce981d
commit 85c6b5a1c7

View file

@ -1,5 +1,4 @@
from __future__ import print_function import configparser
import json import json
import logging import logging
import os import os
@ -7,38 +6,40 @@ import signal
import sys import sys
import time import time
import configparser
if False: from mypy_extensions import NoReturn
from mypy_extensions import NoReturn
from typing import Any, Optional, List, Dict, IO, Text from typing import Any, Optional, List, Dict, IO, Text
from zulip import Client, ZulipError from zulip import Client, ZulipError
from zulip_bots.custom_exceptions import ConfigValidationError from zulip_bots.custom_exceptions import ConfigValidationError
class NoBotConfigException(Exception): class NoBotConfigException(Exception):
pass pass
def exit_gracefully(signum, frame):
# type: (int, Optional[Any]) -> None class StateHandlerError(Exception):
pass
def exit_gracefully(signum: int, frame: Optional[Any]) -> None:
sys.exit(0) sys.exit(0)
def get_bots_directory_path():
# type: () -> str def get_bots_directory_path() -> str:
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(current_dir, 'bots') return os.path.join(current_dir, 'bots')
class RateLimit(object): class RateLimit(object):
def __init__(self, message_limit, interval_limit): def __init__(self, message_limit: int, interval_limit: int) -> None:
# type: (int, int) -> None
self.message_limit = message_limit self.message_limit = message_limit
self.interval_limit = interval_limit self.interval_limit = interval_limit
self.message_list = [] # type: List[float] self.message_list = [] # type: List[float]
self.error_message = '-----> !*!*!*MESSAGE RATE LIMIT REACHED, EXITING*!*!*! <-----\n' self.error_message = '-----> !*!*!*MESSAGE RATE LIMIT REACHED, EXITING*!*!*! <-----\n'
'Is your bot trapped in an infinite loop by reacting to its own messages?' 'Is your bot trapped in an infinite loop by reacting to its own messages?'
def is_legal(self): def is_legal(self) -> bool:
# type: () -> bool
self.message_list.append(time.time()) self.message_list.append(time.time())
if len(self.message_list) > self.message_limit: if len(self.message_list) > self.message_limit:
self.message_list.pop(0) self.message_list.pop(0)
@ -47,31 +48,25 @@ class RateLimit(object):
else: else:
return True return True
def show_error_and_exit(self): def show_error_and_exit(self) -> NoReturn:
# type: () -> NoReturn
logging.error(self.error_message) logging.error(self.error_message)
sys.exit(1) sys.exit(1)
class StateHandlerError(Exception):
pass
class StateHandler(object): class StateHandler(object):
def __init__(self, client): def __init__(self, client: Client) -> None:
# type: (Client) -> None
self._client = client self._client = client
self.marshal = lambda obj: json.dumps(obj) self.marshal = lambda obj: json.dumps(obj)
self.demarshal = lambda obj: json.loads(obj) self.demarshal = lambda obj: json.loads(obj)
self.state_ = dict() # type: Dict[Text, Any] self.state_ = dict() # type: Dict[Text, Any]
def put(self, key, value): def put(self, key: Text, value: Any) -> None:
# type: (Text, Any) -> None
self.state_[key] = self.marshal(value) self.state_[key] = self.marshal(value)
response = self._client.update_storage({'storage': {key: self.state_[key]}}) response = self._client.update_storage({'storage': {key: self.state_[key]}})
if response['result'] != 'success': if response['result'] != 'success':
raise StateHandlerError("Error updating state: {}".format(str(response))) raise StateHandlerError("Error updating state: {}".format(str(response)))
def get(self, key): def get(self, key: Text) -> Any:
# type: (Text) -> Any
if key in self.state_: if key in self.state_:
return self.demarshal(self.state_[key]) return self.demarshal(self.state_[key])
@ -83,10 +78,10 @@ class StateHandler(object):
self.state_[key] = marshalled_value self.state_[key] = marshalled_value
return self.demarshal(marshalled_value) return self.demarshal(marshalled_value)
def contains(self, key): def contains(self, key: Text) -> bool:
# type: (Text) -> bool
return key in self.state_ return key in self.state_
class ExternalBotHandler(object): class ExternalBotHandler(object):
def __init__( def __init__(
self, self,
@ -156,8 +151,7 @@ class ExternalBotHandler(object):
content=response, content=response,
)) ))
def update_message(self, message): def update_message(self, message: Dict[str, Any]) -> Dict[str, Any]:
# type: (Dict[str, Any]) -> Dict[str, Any]
if self._rate_limit.is_legal(): if self._rate_limit.is_legal():
return self._client.update_message(message) return self._client.update_message(message)
else: else:
@ -219,8 +213,8 @@ class ExternalBotHandler(object):
def quit(self, message: str="") -> None: def quit(self, message: str="") -> None:
sys.exit(message) sys.exit(message)
def extract_query_without_mention(message, client):
# type: (Dict[str, Any], ExternalBotHandler) -> Optional[str] def extract_query_without_mention(message: Dict[str, Any], client: ExternalBotHandler) -> Optional[str]:
""" """
If the bot is the first @mention in the message, then this function returns If the bot is the first @mention in the message, then this function returns
the stripped message with the bot's @mention removed. Otherwise, it returns None. the stripped message with the bot's @mention removed. Otherwise, it returns None.
@ -230,8 +224,8 @@ def extract_query_without_mention(message, client):
return None return None
return message['content'][len(mention):].lstrip() return message['content'][len(mention):].lstrip()
def is_private_message_from_another_user(message_dict, current_user_id):
# type: (Dict[str, Any], int) -> bool def is_private_message_from_another_user(message_dict: Dict[str, Any], current_user_id: int) -> bool:
""" """
Checks whether a message dict represents a PM from another user. Checks whether a message dict represents a PM from another user.
@ -243,21 +237,28 @@ def is_private_message_from_another_user(message_dict, current_user_id):
return current_user_id != message_dict['sender_id'] return current_user_id != message_dict['sender_id']
return False return False
def display_config_file_errors(error_msg, config_file):
# type: (str, str) -> None def display_config_file_errors(error_msg: str, config_file: str) -> None:
file_contents = open(config_file).read() file_contents = open(config_file).read()
print('\nERROR: {} seems to be broken:\n\n{}'.format(config_file, file_contents)) print('\nERROR: {} seems to be broken:\n\n{}'.format(config_file, file_contents))
print('\nMore details here:\n\n{}\n'.format(error_msg)) print('\nMore details here:\n\n{}\n'.format(error_msg))
def run_message_handler_for_bot(lib_module, quiet, config_file, bot_config_file, bot_name):
# type: (Any, bool, str, str, str) -> Any def run_message_handler_for_bot(
# lib_module: Any,
# lib_module is of type Any, since it can contain any bot's quiet: bool,
# handler class. Eventually, we want bot's handler classes to config_file: str,
# inherit from a common prototype specifying the handle_message bot_config_file: str,
# function. bot_name: str,
# ) -> Any:
# Set default bot_details, then override from class, if provided """
lib_module is of type Any, since it can contain any bot's
handler class. Eventually, we want bot's handler classes to
inherit from a common prototype specifying the handle_message
function.
Set default bot_details, then override from class, if provided
"""
bot_details = { bot_details = {
'name': bot_name.capitalize(), 'name': bot_name.capitalize(),
'description': "", 'description': "",
@ -294,8 +295,7 @@ def run_message_handler_for_bot(lib_module, quiet, config_file, bot_config_file,
print("\n\t{}".format(bot_details['description'])) print("\n\t{}".format(bot_details['description']))
print(message_handler.usage()) print(message_handler.usage())
def handle_message(message, flags): def handle_message(message: Dict[str, Any], flags: List[str]) -> None:
# type: (Dict[str, Any], List[str]) -> None
logging.info('waiting for next message') logging.info('waiting for next message')
# `mentioned` will be in `flags` if the bot is mentioned at ANY position # `mentioned` will be in `flags` if the bot is mentioned at ANY position
# (not necessarily the first @mention in the message). # (not necessarily the first @mention in the message).
@ -322,8 +322,7 @@ def run_message_handler_for_bot(lib_module, quiet, config_file, bot_config_file,
logging.info('starting message handling...') logging.info('starting message handling...')
def event_callback(event): def event_callback(event: Dict[str, Any]) -> None:
# type: (Dict[str, Any]) -> None
if event['type'] == 'message': if event['type'] == 'message':
handle_message(event['message'], event['flags']) handle_message(event['message'], event['flags'])