diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..b674b52
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,337 @@
+# RandTalkBot Bot matching you with a random person on Telegram.
+# Copyright (C) 2017 quasiyoke
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+[MASTER]
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=pylint.extensions.bad_builtin,
+ pylint.extensions.check_elif,
+ pylint.extensions.docparams,
+ pylint.extensions.docstyle,
+ pylint.extensions.overlapping_exceptions,
+ pylint.extensions.redefined_variable_type,
+
+
+[MESSAGES CONTROL]
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=abstract-method,
+ bad-builtin,
+ cyclic-import,
+ duplicate-code,
+ missing-docstring,
+ missing-return-type-doc,
+ too-few-public-methods,
+ too-many-ancestors,
+ too-many-branches,
+ too-many-instance-attributes,
+ too-many-locals,
+ too-many-public-methods,
+ too-many-statements,
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=colorized
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[VARIABLES]
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+
+[BASIC]
+
+# Naming style matching correct argument names
+argument-naming-style=snake_case
+
+# Naming style matching correct attribute names
+attr-naming-style=snake_case
+
+# Naming style matching correct class names
+class-naming-style=PascalCase
+
+# Naming style matching correct constant names
+const-naming-style=UPPER_CASE
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names
+function-naming-style=snake_case
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=db,
+ err,
+ i,
+ j,
+ k,
+ Run,
+ _,
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names
+inlinevar-naming-style=any
+
+# Naming style matching correct method names
+method-naming-style=snake_case
+
+# Naming style matching correct module names
+module-naming-style=snake_case
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names
+variable-naming-style=snake_case
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes
+max-spelling-suggestions=4
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[FORMAT]
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,
+ dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=optparse,tkinter.tix
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/.travis.yml b/.travis.yml
index 15c03c8..d116577 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,7 @@ install:
- python setup.py install
script:
+ - python setup.py lint
- python setup.py test
after_success: python setup.py coverage
diff --git a/README.rst b/README.rst
index 4db294c..c00f35f 100644
--- a/README.rst
+++ b/README.rst
@@ -167,3 +167,9 @@ Launch some specific test.
::
$ python -m unittest tests.test_stranger.TestStranger
+
+If you went into trouble with codestyle (``test_codestyle`` test suite), look at the specific codestyle issues you've got:
+
+::
+
+ $ pylint randtalkbot setup tests
diff --git a/randtalkbot/__main__.py b/randtalkbot/__main__.py
index 3128950..a959bb4 100644
--- a/randtalkbot/__main__.py
+++ b/randtalkbot/__main__.py
@@ -4,7 +4,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-'''randtalkbot.__main__: executed when randtalkbot directory is called as script.'''
+"""randtalkbot.__main__: executed when randtalkbot directory is called as script."""
from .randtalkbot import main
diff --git a/randtalkbot/admin_handler.py b/randtalkbot/admin_handler.py
index 6cea9ba..17cd1d7 100644
--- a/randtalkbot/admin_handler.py
+++ b/randtalkbot/admin_handler.py
@@ -4,14 +4,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import asyncio
import logging
import re
-from .errors import StrangerError, StrangerServiceError
-from .stranger import MissingPartnerError, SEX_NAMES
+from .errors import StrangerServiceError
from .stranger_handler import StrangerHandler
from .stranger_service import StrangerService
-from telepot.exception import TelegramError
LOGGER = logging.getLogger('randtalkbot.admin_handler')
@@ -22,12 +19,19 @@ async def _handle_command_clear(self, message):
try:
telegram_id = int(telegram_id)
except (ValueError, TypeError):
- await self._sender.send_notification('Is it really telegram_id: \"{0}\"?', telegram_id)
+ await self._sender.send_notification(
+ 'Is it really telegram_id: \"{0}\"?',
+ telegram_id,
+ )
continue
try:
stranger = StrangerService.get_instance().get_stranger(telegram_id)
- except StrangerServiceError as e:
- await self._sender.send_notification('Stranger {0} wasn\'t found: {1}', telegram_id, e)
+ except StrangerServiceError as err:
+ await self._sender.send_notification(
+ 'Stranger {0} wasn\'t found: {1}',
+ telegram_id,
+ err,
+ )
continue
await stranger.end_talk()
await self._sender.send_notification('Stranger {0} was cleared', telegram_id)
@@ -54,9 +58,9 @@ async def _handle_command_pay(self, message):
return
try:
stranger = StrangerService.get_instance().get_stranger(telegram_id)
- except StrangerServiceError as e:
- await self._sender.send_notification('Stranger wasn\'t found: {0}', e)
+ except StrangerServiceError as err:
+ await self._sender.send_notification('Stranger wasn\'t found: {0}', err)
return
await stranger.pay(delta, match.group('gratitude'))
await self._sender.send_notification('Success.')
- LOGGER.debug('Pay: {} -({})-> {}'.format(self._stranger.id, delta, telegram_id))
+ LOGGER.debug('Pay: %d -(%d)-> %d', self._stranger.id, delta, telegram_id)
diff --git a/randtalkbot/bot.py b/randtalkbot/bot.py
index 74b12c9..85918a7 100644
--- a/randtalkbot/bot.py
+++ b/randtalkbot/bot.py
@@ -4,13 +4,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import asyncio
import logging
import telepot
-from .admin_handler import AdminHandler
-from .stranger_handler import StrangerHandler
from telepot.delegate import per_from_id_in, per_from_id_except
from telepot.aio.delegate import create_open, pave_event_space
+from .admin_handler import AdminHandler
+from .stranger_handler import StrangerHandler
LOGGER = logging.getLogger('randtalkbot.bot')
diff --git a/randtalkbot/configuration.py b/randtalkbot/configuration.py
index d0e2d44..68b925b 100644
--- a/randtalkbot/configuration.py
+++ b/randtalkbot/configuration.py
@@ -16,15 +16,18 @@ class ConfigurationObtainingError(Exception):
class Configuration:
def __init__(self, path):
reader = codecs.getreader('utf-8')
+
try:
- with open(path, 'rb') as f:
- configuration_json = json.load(reader(f))
- except OSError as e:
- LOGGER.error('Troubles with opening \"%s\": %s', path, e)
- raise ConfigurationObtainingError('Troubles with opening \"{0}\"'.format(path))
- except ValueError as e:
- LOGGER.error('Troubles with parsing \"%s\": %s', path, e)
- raise ConfigurationObtainingError('Troubles with parsing \"{0}\"'.format(path))
+ with open(path, 'rb') as file_descriptor:
+ configuration_json = json.load(reader(file_descriptor))
+ except OSError as err:
+ reason = f'Troubles with opening \"{path}\"'
+ LOGGER.exception(reason)
+ raise ConfigurationObtainingError(reason) from err
+ except ValueError as err:
+ reason = f'Troubles with parsing \"{path}\"'
+ LOGGER.exception(reason)
+ raise ConfigurationObtainingError(reason) from err
try:
self.database_host = configuration_json['database']['host']
@@ -33,7 +36,9 @@ def __init__(self, path):
self.database_password = configuration_json['database']['password']
self.logging = configuration_json['logging']
self.token = configuration_json['token']
- except KeyError as e:
- LOGGER.error('Troubles with obtaining parameters: %s', e)
- raise ConfigurationObtainingError('Troubles with obtaining parameters \"{0}\"'.format(e))
+ except (KeyError, TypeError) as err:
+ reason = 'Troubles with obtaining parameters'
+ LOGGER.exception(reason)
+ raise ConfigurationObtainingError(reason) from err
+
self.admins_telegram_ids = configuration_json.get('admins', [])
diff --git a/randtalkbot/db.py b/randtalkbot/db.py
index 3591552..9005c53 100644
--- a/randtalkbot/db.py
+++ b/randtalkbot/db.py
@@ -5,21 +5,20 @@
# along with this program. If not, see .
import logging
+from peewee import DatabaseError, MySQLDatabase
+from playhouse.shortcuts import RetryOperationalError
+from randtalkbot import stats, stranger, talk
from .errors import DBError
from .stats import Stats
from .stranger import Stranger
from .talk import Talk
-from peewee import *
-from playhouse.shortcuts import RetryOperationalError
-from randtalkbot import stats, stranger, talk
LOGGER = logging.getLogger('randtalkbot.db')
class RetryingDB(RetryOperationalError, MySQLDatabase):
- '''
- Automatically reconnecting database class.
+ """Automatically reconnecting database class.
@see http://docs.peewee-orm.com/en/latest/peewee/database.html#automatic-reconnect
- '''
+ """
pass
class DB:
@@ -30,18 +29,20 @@ def __init__(self, configuration):
user=configuration.database_user,
password=configuration.database_password,
)
+
# Connect to database just to check if configuration has errors.
try:
self._db.connect()
- except DatabaseError as e:
- raise DBError('DatabaseError during connecting to database. {}'.format(e))
+ except DatabaseError as err:
+ raise DBError('DatabaseError during connecting to database') from err
+
self._db.close()
- stats.database_proxy.initialize(self._db)
- stranger.database_proxy.initialize(self._db)
- talk.database_proxy.initialize(self._db)
+ stats.DATABASE_PROXY.initialize(self._db)
+ stranger.DATABASE_PROXY.initialize(self._db)
+ talk.DATABASE_PROXY.initialize(self._db)
def install(self):
try:
self._db.create_tables([Stats, Stranger, Talk])
- except DatabaseError as e:
- raise DBError('DatabaseError during creating tables. {}'.format(e))
+ except DatabaseError as err:
+ raise DBError('DatabaseError during creating tables') from err
diff --git a/randtalkbot/i18n.py b/randtalkbot/i18n.py
index 40d3f31..890f695 100644
--- a/randtalkbot/i18n.py
+++ b/randtalkbot/i18n.py
@@ -4,14 +4,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from collections import OrderedDict
import gettext
import logging
import os
import pycountry
-import re
from .utils import LOCALE_DIR
-from collections import OrderedDict
-from os import path
LOGGER = logging.getLogger('randtalkbot.i18n')
QUOTES = '\"\'“”«»'
@@ -21,69 +19,84 @@ def __init__(self, name):
super(LanguageNotFoundError, self).__init__('Language \"{0}\" wasn\'t found.'.format(name))
self.name = name
-def _get_deduplicated(a):
- '''
- Removes duplicates keeping list order.
+def _get_deduplicated(list_instance):
+ """Removes duplicates keeping list order.
>>> _get_deduplicated(['ru', 'en', 'ru', ])
['ru', 'en', ]
- '''
- return list(OrderedDict.fromkeys(a))
+ """
+ return list(OrderedDict.fromkeys(list_instance))
def _get_language_code(name):
- '''
- @throws LanguageNotFoundError
- '''
+ """Raises:
+ LanguageNotFoundError: If unable to recognize the language.
+
+ Returns:
+ str: Language code.
+ """
try:
return LANGUAGES_NAMES_TO_CODES[name.lower()]
except KeyError:
raise LanguageNotFoundError(name)
def _get_language_name(code):
- '''
- @throws LanguageNotFoundError
- '''
+ """Raises:
+ LanguageNotFoundError: If unable to recognize the language.
+
+ Returns:
+ str: Language name.
+ """
try:
return LANGUAGES_CODES_TO_NAMES[code]
except KeyError:
raise LanguageNotFoundError(code)
def get_languages_names(codes):
- '''
- @throws LanguageNotFoundError
- '''
+ """Raises:
+ LanguageNotFoundError: If unable to recognize some language.
+
+ Returns:
+ str: Comma-separated list of languages' names.
+ """
names = map(_get_language_name, codes)
return ', '.join(names)
def get_languages_codes(names):
- '''
- @throws LanguageNotFoundError
- '''
+ """Raises:
+ LanguageNotFoundError: If unable to recognize some language.
+
+ Returns:
+ list: Deduplicated list of languages' codes.
+ """
names = ''.join([c for c in names if c not in QUOTES])
+
if names.strip().lower() in SAME_LANGUAGE_NAMES:
return ['same']
+
names = [name.strip() for name in names.split(',')]
- names = filter(bool, names)
- names = map(_get_language_code, names)
- names = _get_deduplicated(names)
- return names
+ compact_names = filter(bool, names)
+ languages_codes = map(_get_language_code, compact_names)
+ unique_languages_codes = _get_deduplicated(languages_codes)
+ return unique_languages_codes
def get_translation(languages):
if not languages:
languages = ['en']
+
try:
- translation = gettext.translation(
+ translation_instance = gettext.translation(
'randtalkbot',
localedir=LOCALE_DIR,
languages=languages,
)
- except (IOError, OSError):
- translation = gettext.translation(
+ except OSError:
+ translation_instance = gettext.translation(
'randtalkbot',
localedir=LOCALE_DIR,
languages=['en'],
)
- return translation.gettext
+
+ return translation_instance.gettext
def get_translations():
for filename in os.listdir(LOCALE_DIR):
diff --git a/randtalkbot/message.py b/randtalkbot/message.py
index aa9f04b..523fff7 100644
--- a/randtalkbot/message.py
+++ b/randtalkbot/message.py
@@ -5,7 +5,6 @@
# along with this program. If not, see .
import base64
-import binascii
import logging
import json
import re
@@ -15,11 +14,11 @@
LOGGER = logging.getLogger('randtalkbot.message')
class Message:
- COMMAND_RE_PATTERN = re.compile('^/([a-z_]+)\\b\s*(.*)$')
+ COMMAND_RE_PATTERN = re.compile(r'^/([a-z_]+)\b\s*(.*)$')
def __init__(self, message_json):
try:
- content_type, chat_type, chat_id = telepot.glance(message_json)
+ content_type, unused_chat_type, unused_chat_id = telepot.glance(message_json)
except KeyError:
raise UnsupportedContentError()
if 'forward_from' in message_json:
@@ -29,25 +28,28 @@ def __init__(self, message_json):
self.type = content_type
self.command = None
self.command_args = None
+ self.sending_kwargs = {}
+
try:
init_method = getattr(self, '_init_' + content_type)
except AttributeError:
raise UnsupportedContentError()
+
init_method(message_json)
def decode_command_args(self):
try:
command_args = base64.urlsafe_b64decode(self.command_args)
- except (TypeError, ValueError, binascii.Error) as e:
- raise UnsupportedContentError('Can\'t decode base 64: {0}'.format(e))
+ except (TypeError, ValueError) as err:
+ raise UnsupportedContentError('Can\'t decode base 64') from err
try:
command_args = command_args.decode('utf-8')
- except UnicodeDecodeError as e:
- raise UnsupportedContentError('Can\'t decode UTF-8: {0}'.format(e))
+ except UnicodeDecodeError as err:
+ raise UnsupportedContentError('Can\'t decode UTF-8') from err
try:
command_args = json.loads(command_args)
- except (TypeError, ValueError) as e:
- raise UnsupportedContentError('Can\'t decode JSON: {0}'.format(e))
+ except (TypeError, ValueError) as err:
+ raise UnsupportedContentError('Can\'t decode JSON') from err
return command_args
def _init_audio(self, message_json):
@@ -97,7 +99,7 @@ def _init_sticker(self, message_json):
except (KeyError, TypeError):
raise UnsupportedContentError()
- def _init_text(self, message_json):
+ def _init_text(self, unused_message_json):
self.sending_kwargs = {
'text': self.text,
}
diff --git a/randtalkbot/randtalkbot.py b/randtalkbot/randtalkbot.py
index d428c09..06e3be7 100644
--- a/randtalkbot/randtalkbot.py
+++ b/randtalkbot/randtalkbot.py
@@ -4,21 +4,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-'''randtalkbot.randtalkbot: provides entry point main().'''
+"""randtalkbot.randtalkbot: provides entry point main()."""
import asyncio
import logging
import logging.config
import sys
+from docopt import docopt
from .bot import Bot
from .configuration import Configuration, ConfigurationObtainingError
from .db import DB
from .errors import DBError
from .stats_service import StatsService
-from .stranger_sender_service import StrangerSenderService
from .utils import __version__
-from docopt import docopt
-from randtalkbot import stranger, stranger_service
DOC = '''RandTalkBot
@@ -38,22 +36,23 @@ def main():
try:
configuration = Configuration(arguments['CONFIGURATION'])
- except ConfigurationObtainingError as e:
- sys.exit('Can\'t obtain configuration. {}'.format(e))
+ except ConfigurationObtainingError as err:
+ sys.exit(f'Can\'t obtain configuration. {err}')
logging.config.dictConfig(configuration.logging)
try:
db = DB(configuration)
- except DBError as e:
- sys.exit('Can\'t construct DB. {}'.format(e))
+ except DBError as err:
+ sys.exit(f'Can\'t construct DB. {err}')
if arguments['install']:
LOGGER.info('Installing RandTalkBot')
+
try:
db.install()
- except DBError as e:
- sys.exit('Can\'t install databases. {}'.format(e))
+ except DBError as err:
+ sys.exit(f'Can\'t install databases. {err}')
else:
LOGGER.info('Executing RandTalkBot')
loop = asyncio.get_event_loop()
diff --git a/randtalkbot/stats.py b/randtalkbot/stats.py
index da1d530..99faa38 100644
--- a/randtalkbot/stats.py
+++ b/randtalkbot/stats.py
@@ -7,48 +7,57 @@
import datetime
import json
import logging
-from peewee import *
+from peewee import DateTimeField, Model, Proxy, TextField
LOGGER = logging.getLogger('randtalkbot.stats')
-def _(s): return s
+def _(string):
+ return string
-database_proxy = Proxy()
+DATABASE_PROXY = Proxy()
+RATIO_MAX = 10
class Stats(Model):
data_json = TextField()
created = DateTimeField(default=datetime.datetime.utcnow, index=True)
class Meta:
- database = database_proxy
+ database = DATABASE_PROXY
+
+ def __init__(self, *args, **kwargs):
+ super(Stats, self).__init__(*args, **kwargs)
+ self._data_cache = None
def get_data(self):
- try:
- return self._data_cache
- except AttributeError:
+ if self._data_cache is None:
self._data_cache = json.loads(self.data_json)
- return self._data_cache
+
+ return self._data_cache
def set_data(self, data):
self._data_cache = data
self.data_json = json.dumps(data)
def get_sex_ratio(self):
- '''
- @return Ratio of males to females.
- @see https://en.wikipedia.org/wiki/Human_sex_ratio
- '''
+ """https://en.wikipedia.org/wiki/Human_sex_ratio
+
+ Returns:
+ float: Ratio of males over the females.
+
+ """
try:
sex_data = self.get_data()['sex_distribution']
except (KeyError, TypeError):
return 1
+
males_count = sex_data.get('male', 0)
females_count = sex_data.get('female', 0)
+
if males_count > 0 and females_count > 0:
return males_count / females_count
elif males_count > 0:
- return 10
+ return RATIO_MAX
elif females_count > 0:
- return 0.1
- else:
- return 1
+ return 1 / RATIO_MAX
+
+ return 1
diff --git a/randtalkbot/stats_service.py b/randtalkbot/stats_service.py
index 7e9afe8..16406f7 100644
--- a/randtalkbot/stats_service.py
+++ b/randtalkbot/stats_service.py
@@ -7,9 +7,9 @@
import asyncio
import datetime
import logging
+from peewee import DoesNotExist
from .errors import StrangerSenderServiceError
from .stats import Stats
-from peewee import *
COUNT_INTERVALS = (4, 16, 64, 256)
LOGGER = logging.getLogger('randtalkbot.stats_service')
@@ -19,11 +19,13 @@ def get_talks_stats(talks, get_value, intervals):
distribution['more'] = 0
average = 0
count = 0
+
for talk in talks:
value = get_value(talk)
increment_distribution(distribution, value, intervals)
average += value
count += 1
+
try:
average /= count
except ZeroDivisionError:
@@ -34,19 +36,26 @@ def get_talks_stats(talks, get_value, intervals):
'count': count,
}
-def increment(d, key):
+def increment(dictionary, key):
try:
- d[key] += 1
+ dictionary[key] += 1
except KeyError:
- d[key] = 1
+ dictionary[key] = 1
-def increment_distribution(d, value, intervals):
+def increment_distribution(dictionary, value, intervals):
for interval in intervals:
if value <= interval:
break
else:
interval = 'more'
- d[interval] += 1
+
+ dictionary[interval] += 1
+
+def first(iterable):
+ return iterable[0]
+
+def second(iterable):
+ return iterable[1]
class StatsService:
INTERVAL = datetime.timedelta(hours=4)
@@ -73,8 +82,10 @@ async def run(self):
while True:
next_stats_time = self._stats.created + type(self).INTERVAL
now = datetime.datetime.utcnow()
+
if next_stats_time > now:
await asyncio.sleep((next_stats_time - now).total_seconds())
+
self._update_stats()
def _update_stats(self):
@@ -89,31 +100,41 @@ def _update_stats(self):
languages_count_distribution = {}
languages_popularity = {}
total_count = 0
+
for stranger in stranger_service.get_full_strangers():
total_count += 1
increment(sex_distribution, stranger.sex)
increment(partner_sex_distribution, stranger.partner_sex)
increment(languages_count_distribution, len(stranger.get_languages()))
+
for language in stranger.get_languages():
increment(languages_popularity, language)
- languages_count_distribution_items = list(languages_count_distribution.items())
- languages_count_distribution_items.sort(key=lambda item: item[0])
+
+ langs_count_distribution_items = list(languages_count_distribution.items())
+ langs_count_distribution_items.sort(key=first)
valuable_count = total_count / 100
languages_popularity_items = [
(language, popularity)
for language, popularity in languages_popularity.items() if popularity >= valuable_count
]
- languages_popularity_items.sort(key=lambda item: item[1], reverse=True)
+ languages_popularity_items.sort(key=second, reverse=True)
+
+ languages_to_orientation = {
+ language: {}
+ for language, popularity in languages_popularity_items
+ }
- languages_to_orientation = {language: {} for language, popularity in languages_popularity_items}
for stranger in stranger_service.get_full_strangers():
orientation = '{} {}'.format(stranger.sex, stranger.partner_sex)
+
for language in stranger.get_languages():
try:
orientation_distribution = languages_to_orientation[language]
except KeyError:
continue
+
increment(orientation_distribution, orientation)
+
languages_to_orientation_items = [
(language, languages_to_orientation[language])
for language, popularity in languages_popularity_items
@@ -125,7 +146,9 @@ def _update_stats(self):
(10, 60, 60 * 5, 60 * 30, 60 * 60 * 3, ),
)
- ended_talks = Talk.get_ended_talks(after=None if self._stats is None else self._stats.created)
+ ended_talks = Talk.get_ended_talks(
+ after=None if self._stats is None else self._stats.created,
+ )
talks_duration = get_talks_stats(
ended_talks,
lambda talk: (talk.end - talk.begin).total_seconds(),
@@ -141,7 +164,7 @@ def _update_stats(self):
Talk.delete_old(before=self._stats.created)
stats_json = {
- 'languages_count_distribution': languages_count_distribution_items,
+ 'languages_count_distribution': langs_count_distribution_items,
'languages_popularity': languages_popularity_items,
'languages_to_orientation': languages_to_orientation_items,
'partner_sex_distribution': partner_sex_distribution,
@@ -156,12 +179,13 @@ def _update_stats(self):
self._stats = stats
LOGGER.info('Stats were updated')
LOGGER.debug(
- 'StrangerService cache size: %d',
+ 'StrangerService cache size: %dictionary',
StrangerService.get_instance().get_cache_size(),
)
+
try:
LOGGER.debug(
- 'StrangerSenderService cache size: %d',
+ 'StrangerSenderService cache size: %dictionary',
StrangerSenderService.get_instance().get_cache_size(),
)
except StrangerSenderServiceError:
diff --git a/randtalkbot/stranger.py b/randtalkbot/stranger.py
index 59978e4..04f0b25 100644
--- a/randtalkbot/stranger.py
+++ b/randtalkbot/stranger.py
@@ -11,12 +11,13 @@
import logging
import random
import string
-from .errors import EmptyLanguagesError, MissingPartnerError, SexError, StrangerError, StrangerSenderError
+from peewee import CharField, DateTimeField, ForeignKeyField, IntegerField, Model, Proxy
+from telepot.exception import TelegramError
+from .errors import EmptyLanguagesError, MissingPartnerError, SexError, StrangerError, \
+ StrangerSenderError
from .i18n import get_languages_names, get_translations
from .stats_service import StatsService
from .stranger_sender_service import StrangerSenderService
-from peewee import *
-from telepot.exception import TelegramError
INVITATION_CHARS = string.ascii_letters + string.digits + string.punctuation
INVITATION_LENGTH = 10
@@ -24,7 +25,22 @@
LOGGER = logging.getLogger('randtalkbot.stranger')
-def _(s): return s
+def _(string_instance):
+ return string_instance
+
+
+def get_sex_names_to_codes():
+ sex_names_to_codes = {}
+
+ for translation in get_translations():
+ for sex, name in SEX_CHOICES:
+ sex_names_to_codes[translation(name).lower()] = sex
+
+ for name, sex in ADDITIONAL_SEX_NAMES_TO_CODES.items():
+ sex_names_to_codes[translation(name).lower()] = sex
+
+ return sex_names_to_codes
+
SEX_CHOICES = (
('female', _('Female')),
@@ -42,19 +58,13 @@ def _(s): return s
'women': 'female',
}
SEX_MAX_LENGTH = 20
-SEX_NAMES_TO_CODES = {}
+SEX_NAMES_TO_CODES = get_sex_names_to_codes()
SEX_NAMES = list(zip(*SEX_CHOICES))[1]
-for translation in get_translations():
- for sex, name in SEX_CHOICES:
- SEX_NAMES_TO_CODES[translation(name).lower()] = sex
- for name, sex in ADDITIONAL_SEX_NAMES_TO_CODES.items():
- SEX_NAMES_TO_CODES[translation(name).lower()] = sex
WIZARD_CHOICES = (
('none', 'None'),
('setup', 'Setup'),
)
-
-database_proxy = Proxy()
+DATABASE_PROXY = Proxy()
class Stranger(Model):
@@ -78,18 +88,18 @@ class Stranger(Model):
UNMUTE_BONUSES_NOTIFICATIONS_DELAY = 60 * 60
class Meta:
- database = database_proxy
+ database = DATABASE_PROXY
indexes = (
(('partner_sex', 'bonus_count', 'looking_for_partner_from'), False),
(('sex', 'partner_sex', 'bonus_count', 'looking_for_partner_from'), False),
)
@classmethod
- def get_invitation(self):
+ def get_invitation(cls):
return ''.join([random.choice(INVITATION_CHARS) for i in range(INVITATION_LENGTH)])
@classmethod
- def _get_sex_code(self, sex_name):
+ def _get_sex_code(cls, sex_name):
sex = sex_name.strip().lower()
try:
return SEX_NAMES_TO_CODES[sex]
@@ -105,22 +115,31 @@ async def _add_bonuses(self, bonuses_delta):
async def _advertise(self):
await asyncio.sleep(type(self).ADVERTISING_DELAY)
+ # pylint: disable=attribute-defined-outside-init
self._deferred_advertising = None
- searching_for_partner_count = Stranger.select().where(Stranger.looking_for_partner_from != None) \
+ searching_for_partner_count = Stranger.select() \
+ .where(Stranger.looking_for_partner_from != None) \
.count()
+
if searching_for_partner_count > 1:
if StatsService.get_instance().get_stats().get_sex_ratio() >= 1:
- message = _('The search is going on. {0} users are looking for partner -- change your '
- 'preferences (languages, partner\'s sex) using /setup command to talk with them.\n'
- 'Chat *lacks females!* Send the link to your friends and earn {1} bonuses for every '
- 'invited female and {2} bonus for each male (the more bonuses you have -- the faster '
- 'partner\'s search will be):')
+ message = _(
+ 'The search is going on. {0} users are looking for partner -- change'
+ ' your preferences (languages, partner\'s sex) using /setup command to talk'
+ ' with them.\n'
+ 'Chat *lacks females!* Send the link to your friends and earn {1} bonuses'
+ ' for every invited female and {2} bonus for each male (the more bonuses'
+ ' you have -- the faster partner\'s search will be):',
+ )
else:
- message = _('The search is going on. {0} users are looking for partner -- change your '
- 'preferences (languages, partner\'s sex) using /setup command to talk with them.\n'
- 'Chat *lacks males!* Send the link to your friends and earn {1} bonuses for every '
- 'invited male and {2} bonus for each female (the more bonuses you have -- the faster '
- 'partner\'s search will be):')
+ message = _(
+ 'The search is going on. {0} users are looking for partner -- change your'
+ ' preferences (languages, partner\'s sex) using /setup command to talk'
+ ' with them.\n'
+ 'Chat *lacks males!* Send the link to your friends and earn {1} bonuses'
+ ' for every invited male and {2} bonus for each female (the more bonuses'
+ ' you have -- the faster partner\'s search will be):',
+ )
sender = self.get_sender()
try:
await sender.send_notification(
@@ -131,33 +150,39 @@ async def _advertise(self):
disable_notification=True,
)
await sender.send_notification(
- _('Do you want to talk with somebody, practice in foreign languages or you just want '
- 'to have some fun? Rand Talk will help you! It\'s a bot matching you with '
- 'a random stranger of desired sex speaking on your language. {0}'),
+ _(
+ 'Do you want to talk with somebody, practice in foreign languages or you'
+ ' just want to have some fun? Rand Talk will help you!'
+ ' It\'s a bot matching you with a random stranger of desired sex'
+ ' speaking on your language. {0}',
+ ),
self.get_invitation_link(),
disable_notification=True,
disable_web_page_preview=True,
)
- except TelegramError as e:
- LOGGER.warning('Advertise. Can\'t notify the stranger. %s', e)
+ except TelegramError as err:
+ LOGGER.warning('Advertise. Can\'t notify the stranger. %s', err)
def advertise_later(self):
+ # pylint: disable=attribute-defined-outside-init
self._deferred_advertising = asyncio.get_event_loop().create_task(self._advertise())
async def end_talk(self):
if self.looking_for_partner_from is not None:
# If stranger is looking for partner
self.looking_for_partner_from = None
+
try:
await self.get_sender().send_notification(_('Looking for partner was stopped.'))
- except TelegramError as e:
- LOGGER.warning('End chatting. Can\'t notify stranger %d: %s', self.id, e)
+ except TelegramError as err:
+ LOGGER.warning('End chatting. Can\'t notify stranger %d: %s', self.id, err)
elif self.get_partner() is not None:
# If stranger is chatting now
try:
await self._notify_talk_ended(by_self=True)
- except StrangerError as e:
- LOGGER.warning('End chatting. Can\'t notify stranger %d: %s', self.id, e)
+ except StrangerError as err:
+ LOGGER.warning('End chatting. Can\'t notify stranger %d: %s', self.id, err)
+
await self.set_partner(None)
def get_common_languages(self, partner):
@@ -182,6 +207,7 @@ def get_partner(self):
return self._partner
except AttributeError:
talk = self.get_talk()
+ # pylint: disable=attribute-defined-outside-init
self._partner = None if talk is None else talk.get_partner(self)
return self._partner
@@ -192,15 +218,16 @@ def get_start_args(self):
args = {
'i': self.invitation,
}
- args = json.dumps(args, separators=(',', ':'))
- args = base64.urlsafe_b64encode(args.encode('utf-8'))
- return args.decode('utf-8')
+ serialized_args = json.dumps(args, separators=(',', ':'))
+ serialized_args = base64.urlsafe_b64encode(serialized_args.encode('utf-8'))
+ return serialized_args.decode('utf-8')
def get_talk(self):
try:
return self._talk
except AttributeError:
from .talk import Talk
+ # pylint: disable=attribute-defined-outside-init
self._talk = Talk.get_talk(self)
return self._talk
@@ -217,19 +244,23 @@ def is_full(self):
async def kick(self):
try:
await self._notify_talk_ended(by_self=False)
- except StrangerError as e:
- LOGGER.warning('Kick. Can\'t notify stranger %d: %s', self.id, e)
+ except StrangerError as err:
+ LOGGER.warning('Kick. Can\'t notify stranger %d: %s', self.id, err)
self._pay_for_talk()
+ # pylint: disable=attribute-defined-outside-init
self._talk = None
+ # pylint: disable=attribute-defined-outside-init
self._partner = None
def mute_bonuses_notifications(self):
+ # pylint: disable=attribute-defined-outside-init
self._bonuses_notifications_muted = True
asyncio.get_event_loop().create_task(self._unmute_bonuses_notifications(self.bonus_count))
async def _unmute_bonuses_notifications(self, last_bonuses_count):
await asyncio.sleep(type(self).UNMUTE_BONUSES_NOTIFICATIONS_DELAY)
await self._notify_about_bonuses(self.bonus_count - last_bonuses_count)
+ # pylint: disable=attribute-defined-outside-init
self._bonuses_notifications_muted = False
async def _notify_about_bonuses(self, bonuses_delta):
@@ -237,7 +268,8 @@ async def _notify_about_bonuses(self, bonuses_delta):
try:
if bonuses_delta == 1:
await sender.send_notification(
- _('You\'ve received one bonus for inviting a person to the bot. '
+ _(
+ 'You\'ve received one bonus for inviting a person to the bot. '
'Bonuses will help you to find partners quickly. Total bonuses count: {0}. '
'Congratulations!\n'
'To mute this notifications, use /mute\\_bonuses.'
@@ -246,7 +278,8 @@ async def _notify_about_bonuses(self, bonuses_delta):
)
elif bonuses_delta > 1:
await sender.send_notification(
- _('You\'ve received {0} bonuses for inviting a person to the bot. '
+ _(
+ 'You\'ve received {0} bonuses for inviting a person to the bot. '
'Bonuses will help you to find partners quickly. Total bonuses count: {1}. '
'Congratulations!\n'
'To mute this notifications, use /mute\\_bonuses.'
@@ -254,13 +287,13 @@ async def _notify_about_bonuses(self, bonuses_delta):
bonuses_delta,
self.bonus_count,
)
- except TelegramError as e:
- LOGGER.info('Can\'t notify stranger %d about bonuses: %s', self.id, e)
+ except TelegramError as err:
+ LOGGER.info('Can\'t notify stranger %d about bonuses: %s', self.id, err)
async def _notify_talk_ended(self, by_self):
- '''
- @raise StrangerError If stranger we're changing has blocked the bot.
- '''
+ """Raises:
+ StrangerError: If stranger we're changing has blocked the bot.
+ """
sender = self.get_sender()
_ = sender._
sentences = []
@@ -271,7 +304,8 @@ async def _notify_talk_ended(self, by_self):
sentences.append(_('Your partner has left chat.'))
talk = self.get_talk()
- if talk is not None and talk.is_successful() and self == talk.partner1 and self.bonus_count >= 1:
+ if talk is not None and talk.is_successful() and self == talk.partner1 and \
+ self.bonus_count >= 1:
if self.bonus_count - 1:
bonuses_notification = _('You\'ve used one bonus. {0} bonus(es) left.').format(
self.bonus_count - 1,
@@ -284,13 +318,13 @@ async def _notify_talk_ended(self, by_self):
try:
await sender.send_notification(' '.join(sentences))
- except TelegramError as e:
- raise StrangerError(str(e))
+ except TelegramError as err:
+ raise StrangerError() from err
async def notify_partner_found(self, partner):
- '''
- @raise StrangerError If stranger we're changing has blocked the bot.
- '''
+ """Raises:
+ StrangerError: If stranger we're changing has blocked the bot.
+ """
self.prevent_advertising()
sender = self.get_sender()
_ = sender._
@@ -338,12 +372,12 @@ async def notify_partner_found(self, partner):
if type(self).HOUR_TIMEDELTA <= looked_for_partner_for:
long_waiting_notification = _(
'Your partner\'s been looking for you for {0} hr. Say him \"Hello\" -- '
- 'if he doesn\'t respond to you, launch search again by /begin command.',
+ 'if he doesn\'t respond to you, launch search again by /begin command.',
).format(round(looked_for_partner_for.total_seconds() / 3600))
else:
long_waiting_notification = _(
'Your partner\'s been looking for you for {0} min. Say him \"Hello\" -- '
- 'if he doesn\'t respond to you, launch search again by /begin command.',
+ 'if he doesn\'t respond to you, launch search again by /begin command.',
).format(round(looked_for_partner_for.total_seconds() / 60))
sentences.append(long_waiting_notification)
@@ -352,8 +386,8 @@ async def notify_partner_found(self, partner):
try:
await sender.send_notification(' '.join(sentences))
- except TelegramError as e:
- raise StrangerError('Can\'t notify stranger {}. {}', self.id, e)
+ except TelegramError as err:
+ raise StrangerError(f'Can\'t notify stranger {self.id}') from err
finally:
# To reset languages.
sender.update_translation()
@@ -369,12 +403,13 @@ async def pay(self, delta, gratitude):
self.bonus_count,
gratitude,
)
- except TelegramError as e:
- LOGGER.info('Pay. Can\'t notify stranger %d: %s', self.id, e)
+ except TelegramError as err:
+ LOGGER.info('Pay. Can\'t notify stranger %d: %s', self.id, err)
def _pay_for_talk(self):
talk = self.get_talk()
- if talk is not None and talk.is_successful() and self == talk.partner1 and self.bonus_count >= 1:
+ if talk is not None and talk.is_successful() and self == talk.partner1 and \
+ self.bonus_count >= 1:
self.bonus_count -= 1
self.save()
@@ -385,35 +420,42 @@ def prevent_advertising(self):
except AttributeError:
return
self._deferred_advertising.cancel()
+ # pylint: disable=attribute-defined-outside-init
self._deferred_advertising = None
async def _reward_inviter(self):
self.was_invited_as = self.sex
self.save()
sex_ratio = StatsService.get_instance().get_stats().get_sex_ratio()
- await self.invited_by._add_bonuses(
- type(self).REWARD_BIG if (self.sex == 'female' and sex_ratio >= 1) or
- (self.sex == 'male' and sex_ratio < 1) else type(self).REWARD_SMALL,
- )
+
+ if (self.sex == 'female' and sex_ratio >= 1) or (self.sex == 'male' and sex_ratio < 1):
+ reward = type(self).REWARD_BIG
+ else:
+ reward = type(self).REWARD_SMALL
+
+ # pylint: disable=no-member,protected-access
+ await self.invited_by._add_bonuses(reward)
async def send(self, message):
- '''
- @raise StrangerError if can't send message because of unknown content type.
- @raise TelegramError if stranger has blocked the bot.
- '''
+ """Raises:
+ StrangerError: If can't send message because of unknown content type.
+ TelegramError: If stranger has blocked the bot.
+ """
sender = self.get_sender()
+
try:
await sender.send(message)
- except StrangerSenderError as e:
- raise StrangerError('Can\'t send content: {0}'.format(e))
+ except StrangerSenderError as err:
+ raise StrangerError('Can\'t send content') from err
async def send_to_partner(self, message):
- '''
- @raise MissingPartnerError if there's no partner for this stranger.
- @raise StrangerError if can't send content.
- @raise TelegramError if the partner has blocked the bot.
- '''
+ """Raises:
+ MissingPartnerError: If there's no partner for this stranger.
+ StrangerError: If can't send content.
+ TelegramError: If the partner has blocked the bot.
+ """
partner = self.get_partner()
+
if partner is None:
raise MissingPartnerError()
try:
@@ -424,17 +466,21 @@ async def send_to_partner(self, message):
self.get_talk().increment_sent(self)
def set_languages(self, languages):
- '''
- @raise EmptyLanguagesError if no languages were specified.
- @raise StrangerError if too much languages were specified.
- '''
+ """Raises:
+ EmptyLanguagesError: If no languages were specified.
+ StrangerError: If too much languages were specified.
+ """
if languages == ['same']:
languages = self.get_languages()
- if not len(languages):
+
+ if not languages:
raise EmptyLanguagesError()
+
languages = json.dumps(languages)
+
if len(languages) > LANGUAGES_MAX_LENGTH:
raise StrangerError()
+
self.languages = languages
async def set_looking_for_partner(self):
@@ -442,60 +488,74 @@ async def set_looking_for_partner(self):
# to prevent lowering priority.
if self.looking_for_partner_from is None:
self.looking_for_partner_from = datetime.datetime.utcnow()
+
try:
await self.get_sender().send_notification(
_('Looking for a stranger for you.'),
)
- except TelegramError as e:
+ except TelegramError as err:
LOGGER.debug(
'Set looking for partner. Can\'t notify stranger. %s',
- e,
+ err,
)
self.looking_for_partner_from = None
+
await self.set_partner(None)
async def set_partner(self, partner):
if self.get_partner() == partner:
self.save()
return
+
if self._partner is not None:
if self._partner.get_partner() == self:
# If partner isn't talking with the stranger because of some
# error, we shouldn't kick him.
await self._partner.kick()
+
self._pay_for_talk()
+
if self._talk is not None:
self._talk.end = datetime.datetime.utcnow()
self._talk.save()
+
if partner is None:
+ # pylint: disable=attribute-defined-outside-init
self._talk = None
+ # pylint: disable=attribute-defined-outside-init
self._partner = None
else:
from .talk import Talk
+ # pylint: disable=attribute-defined-outside-init
self._talk = Talk.create(
partner1=self,
partner2=partner,
searched_since=partner.looking_for_partner_from,
)
+ # pylint: disable=attribute-defined-outside-init
self._partner = partner
+
if self.looking_for_partner_from is not None:
self.looking_for_partner_from = None
self.save()
+
+ # pylint: disable=protected-access
partner._talk = self._talk
+ # pylint: disable=protected-access
partner._partner = self
partner.looking_for_partner_from = None
partner.save()
def set_sex(self, sex_name):
- '''
- @throws SexError
- '''
+ """Raises:
+ SexError
+ """
self.sex = Stranger._get_sex_code(sex_name)
def set_partner_sex(self, partner_sex_name):
- '''
- @throws SexError
- '''
+ """Raises:
+ SexError
+ """
self.partner_sex = Stranger._get_sex_code(partner_sex_name)
def speaks_on_language(self, language):
diff --git a/randtalkbot/stranger_handler.py b/randtalkbot/stranger_handler.py
index 56662eb..c58b820 100644
--- a/randtalkbot/stranger_handler.py
+++ b/randtalkbot/stranger_handler.py
@@ -4,31 +4,25 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import asyncio
import datetime
import logging
-import re
import sys
import telepot
import telepot.aio
+from telepot.exception import TelegramError
from .errors import MissingPartnerError, PartnerObtainingError, \
- StrangerError, StrangerHandlerError, StrangerServiceError, \
- UnknownCommandError, UnsupportedContentError
-from .i18n import get_languages_codes, get_translation, \
- LanguageNotFoundError, SUPPORTED_LANGUAGES_NAMES
+ StrangerError, StrangerServiceError, UnknownCommandError, UnsupportedContentError
from .message import Message
-from .stranger import SEX_NAMES
-from .stranger_sender import StrangerSender
from .stranger_sender_service import StrangerSenderService
from .stranger_service import StrangerService
from .stranger_setup_wizard import StrangerSetupWizard
from .utils import __version__
-from telepot.exception import TelegramError
LOGGER = logging.getLogger('randtalkbot.stranger_handler')
-def _(s): return s
+def _(string_instance):
+ return string_instance
class StrangerHandler(telepot.aio.helper.UserHandler):
@@ -38,14 +32,14 @@ class StrangerHandler(telepot.aio.helper.UserHandler):
def __init__(self, seed_tuple, *args, **kwargs):
super(StrangerHandler, self).__init__(seed_tuple, *args, **kwargs)
- bot, initial_msg, seed = seed_tuple
+ bot, initial_msg, unused_seed = seed_tuple
self._from_id = initial_msg['from']['id']
try:
- self._stranger = StrangerService.get_instance(). \
- get_or_create_stranger(self._from_id)
- except StrangerServiceError as e:
- LOGGER.error('Problems with StrangerHandler construction: %s', e)
- sys.exit('Problems with StrangerHandler construction: %s' % e)
+ self._stranger = StrangerService.get_instance() \
+ .get_or_create_stranger(self._from_id)
+ except StrangerServiceError as err:
+ LOGGER.exception('Problems with StrangerHandler construction')
+ sys.exit('Problems with StrangerHandler construction: %s' % err)
self._sender = StrangerSenderService.get_instance(bot). \
get_or_create_stranger_sender(self._stranger)
self._stranger_setup_wizard = StrangerSetupWizard(self._stranger)
@@ -59,7 +53,7 @@ async def handle_command(self, message):
raise UnknownCommandError(message.command)
await handler(message)
- async def _handle_command_begin(self, message):
+ async def _handle_command_begin(self, unused_message):
self._stranger.prevent_advertising()
try:
await StrangerService.get_instance().match_partner(self._stranger)
@@ -67,10 +61,10 @@ async def _handle_command_begin(self, message):
LOGGER.debug('Looking for partner: %d', self._stranger.id)
self._stranger.advertise_later()
await self._stranger.set_looking_for_partner()
- except StrangerServiceError as e:
- LOGGER.warning('Can\'t set partner for %d. %s', self._stranger.id, e)
+ except StrangerServiceError as err:
+ LOGGER.warning('Can\'t set partner for %d. %s', self._stranger.id, err)
- async def _handle_command_end(self, message):
+ async def _handle_command_end(self, unused_message):
partner = self._stranger.get_partner()
LOGGER.debug(
'/end: %d -x-> %s',
@@ -80,36 +74,39 @@ async def _handle_command_end(self, message):
self._stranger.prevent_advertising()
await self._stranger.end_talk()
- async def _handle_command_help(self, message):
+ async def _handle_command_help(self, unused_message):
try:
await self._sender.send_notification(
- _('*Help*\n\n'
- 'Use /begin to start looking for a conversational partner, once '
- 'you\'re matched you can use /end to finish the conversation. '
- 'To choose your settings, apply /setup.\n\n'
- 'If you have any suggestions or require help, please contact @quasiyoke. '
- 'When asking questions, please provide this number: {0}.\n\n'
- 'Subscribe to [our news](https://telegram.me/RandTalk). You\'re welcome '
- 'to inspect and improve [Rand Talk v. {1} source code]'
+ _(
+ '*Help*\n\n'
+ 'Use /begin to start looking for a conversational partner, once'
+ ' you\'re matched you can use /end to finish the conversation.'
+ ' To choose your settings, apply /setup.\n\n'
+ 'If you have any suggestions or require help, please contact @quasiyoke.'
+ ' When asking questions, please provide this number: {0}.\n\n'
+ 'Subscribe to [our news](https://telegram.me/RandTalk). You\'re welcome'
+ ' to inspect and improve [Rand Talk v. {1} source code]'
'(https://github.com/quasiyoke/RandTalkBot) or to [give us 5 stars]'
- '(https://telegram.me/storebot?start=randtalkbot).'),
+ '(https://telegram.me/storebot?start=randtalkbot).',
+ ),
self._from_id,
__version__,
disable_web_page_preview=True,
)
- except TelegramError as e:
- LOGGER.warning('Handle /help command. Can\'t notify stranger. %s', e)
+ except TelegramError as err:
+ LOGGER.warning('Handle /help command. Can\'t notify stranger. %s', err)
- async def _handle_command_mute_bonuses(self, message):
+ async def _handle_command_mute_bonuses(self, unused_message):
self._stranger.mute_bonuses_notifications()
+
try:
await self._sender.send_notification(
_('Notifications about bonuses were muted for 1 hour'),
)
- except TelegramError as e:
- LOGGER.warning('Handle /mute_bonuses command. Can\'t notify stranger. %s', e)
+ except TelegramError as err:
+ LOGGER.warning('Handle /mute_bonuses command. Can\'t notify stranger. %s', err)
- async def _handle_command_setup(self, message):
+ async def _handle_command_setup(self, unused_message):
LOGGER.debug('/setup: %d', self._stranger.id)
self._stranger.prevent_advertising()
await self._stranger.end_talk()
@@ -120,54 +117,63 @@ async def _handle_command_start(self, message):
if message.command_args and not self._stranger.invited_by:
try:
command_args = message.decode_command_args()
- except UnsupportedContentError as e:
- LOGGER.info('/start error. Can\'t decode invitation %s: %s', message.command_args, e)
+ except UnsupportedContentError as err:
+ LOGGER.info(
+ '/start error. Can\'t decode invitation %s: %s',
+ message.command_args,
+ err,
+ )
else:
try:
invitation = command_args['i']
- except (KeyError, TypeError) as e:
- LOGGER.info('/start error. Can\'t obtain invitation: %s', e)
+ except (KeyError, TypeError) as err:
+ LOGGER.info('/start error. Can\'t obtain invitation: %s', err)
else:
if self._stranger.invitation == invitation:
try:
await self._sender.send_notification(
- _('Don\'t try to fool me. Forward message '
- 'with the link to your friends and '
- 'receive well-earned bonuses that will '
- 'help you to find partner quickly.'),
+ _(
+ 'Don\'t try to fool me. Forward message'
+ ' with the link to your friends and'
+ ' receive well-earned bonuses that will'
+ ' help you to find partner quickly.',
+ ),
)
- except TelegramError as e:
+ except TelegramError as err:
LOGGER.warning(
- 'Handle /start command. Can\'t notify '
- 'cheating stranger. %s',
- e,
+ 'Handle /start command. Can\'t notify cheating stranger. %s',
+ err,
)
else:
try:
- invited_by = StrangerService.get_instance().get_stranger_by_invitation(invitation)
- except StrangerServiceError as e:
- LOGGER.info('/start error. Can\'t obtain stranger who did invite: %s', e)
+ invited_by = StrangerService.get_instance() \
+ .get_stranger_by_invitation(invitation)
+ except StrangerServiceError as err:
+ LOGGER.info(
+ '/start error. Can\'t obtain stranger who did invite: %s',
+ err,
+ )
else:
self._stranger.invited_by = invited_by
self._stranger.save()
if self._stranger.wizard == 'none':
try:
await self._sender.send_notification(
- _('*Manual*\n\nUse /begin to start looking for a '
- 'conversational partner, once you\'re matched you can '
- 'use /end to end the conversation.'),
- )
- except TelegramError as e:
- LOGGER.warning(
- 'Handle /start command. Can\'t notify stranger. %s',
- e,
+ _(
+ '*Manual*\n\nUse /begin to start looking for a'
+ ' conversational partner, once you\'re matched you can'
+ ' use /end to end the conversation.',
+ ),
)
+ except TelegramError as err:
+ LOGGER.warning('Handle /start command. Can\'t notify stranger. %s', err)
+ # pylint: disable=method-hidden
async def on_close(self, error):
pass
async def on_chat_message(self, message_json):
- content_type, chat_type, chat_id = telepot.glance(message_json)
+ unused_content_type, chat_type, unused_chat_id = telepot.glance(message_json)
if chat_type != 'private':
return
@@ -179,15 +185,16 @@ async def on_chat_message(self, message_json):
return
if message.command:
- if (await self._stranger_setup_wizard.handle_command(message)):
+ if await self._stranger_setup_wizard.handle_command(message):
return
+
try:
await self.handle_command(message)
- except UnknownCommandError as e:
+ except UnknownCommandError:
await self._sender.send_notification(
_('Unknown command. Look /help for the full list of commands.'),
)
- elif not (await self._stranger_setup_wizard.handle(message)):
+ elif not await self._stranger_setup_wizard.handle(message):
try:
await self._stranger.send_to_partner(message)
except MissingPartnerError:
@@ -205,14 +212,14 @@ async def on_chat_message(self, message_json):
)
await self._stranger.end_talk()
- async def on_edited_chat_message(self, message_json):
+ async def on_edited_chat_message(self, unused_message_json):
LOGGER.info('User tried to edit their message.')
await self._sender.send_notification(
_('Messages editing isn\'t supported'),
)
async def on_inline_query(self, query):
- query_id, from_id, query_string = telepot.glance(query, flavor='inline_query')
+ query_id, unused_from_id, query_string = telepot.glance(query, flavor='inline_query')
LOGGER.debug('Inline query from %d: \"%s\"', self._stranger.id, query_string)
response = [{
'type': 'article',
@@ -221,9 +228,11 @@ async def on_inline_query(self, query):
'description': _('The more friends\'ll use your link -- the faster the search will be'),
'thumb_url': 'http://randtalk.ml/static/img/logo-500x500.png',
'message_text': (
- _('Do you want to talk with somebody, practice in foreign languages or you just want '
- 'to have some fun? Rand Talk will help you! It\'s a bot matching you with '
- 'a random stranger of desired sex speaking on your language. {0}'),
+ _(
+ 'Do you want to talk with somebody, practice in foreign languages or you'
+ ' just want to have some fun? Rand Talk will help you! It\'s a bot matching'
+ ' you with a random stranger of desired sex speaking on your language. {0}'
+ ),
self._stranger.get_invitation_link(),
),
'parse_mode': 'Markdown',
diff --git a/randtalkbot/stranger_sender.py b/randtalkbot/stranger_sender.py
index d52544a..e79425f 100644
--- a/randtalkbot/stranger_sender.py
+++ b/randtalkbot/stranger_sender.py
@@ -4,7 +4,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import asyncio
import logging
import re
import telepot
@@ -33,15 +32,15 @@ def __init__(self, bot, stranger):
self.update_translation()
@classmethod
- def _escape_markdown(cls, s):
- '''
- Escapes string to prevent injecting Markdown into notifications.
+ def _escape_markdown(cls, string_instance):
+ """Escapes string to prevent injecting Markdown into notifications.
@see https://core.telegram.org/bots/api#using-markdown
- '''
- if s is not str:
- s = str(s)
- s = cls.MARKDOWN_RE.sub(r'\\\1', s)
- return s
+ """
+ if string_instance is not str:
+ string_instance = str(string_instance)
+
+ string_instance = cls.MARKDOWN_RE.sub(r'\\\1', string_instance)
+ return string_instance
async def answer_inline_query(self, query_id, answers):
def translate(item):
@@ -55,10 +54,10 @@ def translate(item):
await self._bot.answerInlineQuery(query_id, answers, is_personal=True)
async def send(self, message):
- '''
- @raises StrangerSenderError if message's content type is not supported.
- @raises TelegramError if stranger has blocked the bot.
- '''
+ """Raises:
+ StrangerSenderError: If message's content type is not supported.
+ TelegramError: If stranger has blocked the bot.
+ """
if message.is_reply:
raise StrangerSenderError('Reply can\'t be sent.')
try:
@@ -68,13 +67,20 @@ async def send(self, message):
else:
await getattr(self, method_name)(**message.sending_kwargs)
- async def send_notification(self, message, *args, disable_notification=None, disable_web_page_preview=None,
- reply_markup=None):
- '''
- @raise TelegramError if stranger has blocked the bot.
- '''
+ async def send_notification(
+ self,
+ message,
+ *args,
+ disable_notification=None,
+ disable_web_page_preview=None,
+ reply_markup=None,
+ ):
+ """Raises:
+ TelegramError: If stranger has blocked the bot.
+ """
args = [StrangerSender._escape_markdown(arg) for arg in args]
message = self._(message).format(*args)
+
if reply_markup and 'keyboard' in reply_markup:
reply_markup = {
'keyboard': [
@@ -83,6 +89,8 @@ async def send_notification(self, message, *args, disable_notification=None, dis
],
'one_time_keyboard': True,
}
+
+ # pylint: disable=no-member
await self.sendMessage(
'*Rand Talk:* {}'.format(message),
disable_notification=disable_notification,
diff --git a/randtalkbot/stranger_sender_service.py b/randtalkbot/stranger_sender_service.py
index 5f4d80b..cbb72e8 100644
--- a/randtalkbot/stranger_sender_service.py
+++ b/randtalkbot/stranger_sender_service.py
@@ -23,11 +23,11 @@ def get_instance(cls, bot=None):
if cls._instance is None:
if bot is None:
raise StrangerSenderServiceError(
- 'Instance wasn\'t initialized. Provide arguments to '
- 'construct one.',
+ 'Instance wasn\'t initialized. Provide arguments to construct one.',
)
else:
cls._instance = cls(bot)
+
return cls._instance
def get_cache_size(self):
diff --git a/randtalkbot/stranger_service.py b/randtalkbot/stranger_service.py
index f7b6c6e..422ef91 100644
--- a/randtalkbot/stranger_service.py
+++ b/randtalkbot/stranger_service.py
@@ -5,9 +5,9 @@
# along with this program. If not, see .
import logging
+from peewee import DatabaseError, DoesNotExist
from .errors import PartnerObtainingError, StrangerError, StrangerServiceError
from .stranger import INVITATION_LENGTH, Stranger
-from peewee import *
LOGGER = logging.getLogger('randtalkbot.stranger_service')
@@ -28,6 +28,12 @@ def get_instance(cls):
cls._instance = cls()
return cls._instance
+ @classmethod
+ def get_full_strangers(cls):
+ for stranger in Stranger.select():
+ if stranger.is_full():
+ yield stranger
+
def get_cached_stranger(self, stranger):
try:
return self._strangers_cache[stranger.id]
@@ -38,11 +44,6 @@ def get_cached_stranger(self, stranger):
def get_cache_size(self):
return len(self._strangers_cache)
- def get_full_strangers(self):
- for stranger in Stranger.select():
- if stranger.is_full():
- yield stranger
-
def get_or_create_stranger(self, telegram_id):
try:
try:
@@ -52,19 +53,17 @@ def get_or_create_stranger(self, telegram_id):
invitation=Stranger.get_invitation(),
telegram_id=telegram_id,
)
- except DatabaseError as e:
- raise StrangerServiceError(
- 'Database problems during `get_or_create_stranger`: {0}'.format(e),
- )
+ except DatabaseError as err:
+ raise StrangerServiceError('Database problems during `get_or_create_stranger`') from err
+
return self.get_cached_stranger(stranger)
def get_stranger(self, telegram_id):
try:
stranger = Stranger.get(Stranger.telegram_id == telegram_id)
- except (DatabaseError, DoesNotExist) as e:
- raise StrangerServiceError(
- 'Database problems during `get_stranger`: {0}'.format(e),
- )
+ except (DatabaseError, DoesNotExist) as err:
+ raise StrangerServiceError('Database problems during `get_stranger`') from err
+
return self.get_cached_stranger(stranger)
def get_stranger_by_invitation(self, invitation):
@@ -72,38 +71,44 @@ def get_stranger_by_invitation(self, invitation):
raise StrangerServiceError(
'Invitation length is wrong: \"{0}\"'.format(invitation),
)
+
try:
stranger = Stranger.get(Stranger.invitation == invitation)
- except (DatabaseError, DoesNotExist) as e:
- raise StrangerServiceError(
- 'Database problems during `get_stranger_by_invitation`: {0}'.format(e),
- )
+ except (DatabaseError, DoesNotExist) as err:
+ raise StrangerServiceError('Database problems during `get_stranger_by_invitation`') \
+ from err
+
return self.get_cached_stranger(stranger)
def _match_partner(self, stranger):
- '''
- Tries to find a partner for obtained stranger or throws
- PartnerObtainingError if there's no proper partner.
+ """Tries to find a partner for obtained stranger.
- @throws PartnerObtainingError
- '''
+ Raises:
+ PartnerObtainingError: If there's no proper partner.
+
+ Returns:
+ Stranger
+ """
from .talk import Talk
possible_partners = Stranger.select().where(
Stranger.id != stranger.id,
Stranger.looking_for_partner_from != None,
)
+
if stranger.sex == 'not_specified':
possible_partners = possible_partners.where(Stranger.partner_sex == 'not_specified')
else:
possible_partners = possible_partners.where(
(Stranger.partner_sex == stranger.sex) | (Stranger.partner_sex == 'not_specified'),
)
+
# If stranger wants to filter partners by sex, let's do that.
if stranger.partner_sex == 'male' or stranger.partner_sex == 'female':
possible_partners = possible_partners.where(
Stranger.sex == stranger.partner_sex,
)
+
possible_partners = possible_partners.order_by(
Stranger.bonus_count.desc(),
Stranger.looking_for_partner_from,
@@ -119,7 +124,7 @@ def _match_partner(self, stranger):
continue
for priority, language in enumerate(
stranger.get_languages()[:partner_language_priority],
- ):
+ ):
if possible_partner.speaks_on_language(language):
partner = possible_partner
partner_language_priority = priority
@@ -130,37 +135,39 @@ def _match_partner(self, stranger):
break
if partner is None:
raise PartnerObtainingError()
+
self._locked_strangers_ids.add(partner.id)
return self.get_cached_stranger(partner)
async def match_partner(self, stranger):
- '''
- Finds partner for the stranger. Does handling of strangers who have
- blocked the bot.
+ """Finds partner for the stranger. Does handling of strangers who have blocked the bot.
- @raise PartnerObtainingError if there's no proper partners.
- @raise StrangerServiceError if the stranger has blocked the bot.
- '''
+ Raises:
+ PartnerObtainingError: If there's no proper partners.
+ StrangerServiceError: If the stranger has blocked the bot.
+ """
while True:
partner = self._match_partner(stranger)
+
try:
await partner.notify_partner_found(stranger)
- except StrangerError as e:
+ except StrangerError as err:
# Potential partner has blocked the bot. Let's look for next
# potential partner.
- LOGGER.info('Bad potential partner for %d. %s', stranger.id, e)
+ LOGGER.info('Bad potential partner for %d. %s', stranger.id, err)
await partner.end_talk()
self._locked_strangers_ids.discard(partner.id)
continue
+
break
+
try:
await stranger.notify_partner_found(partner)
- except StrangerError as e:
+ except StrangerError as err:
self._locked_strangers_ids.discard(partner.id)
# Stranger has blocked the bot.
- raise StrangerServiceError(
- 'Can\'t notify seeking for partner stranger: {}'.format(e),
- )
+ raise StrangerServiceError('Can\'t notify seeking for partner stranger') from err
+
await stranger.set_partner(partner)
self._locked_strangers_ids.discard(partner.id)
LOGGER.debug('Found partner: %d -> %d.', stranger.id, partner.id)
diff --git a/randtalkbot/stranger_setup_wizard.py b/randtalkbot/stranger_setup_wizard.py
index 430e8db..f14b272 100644
--- a/randtalkbot/stranger_setup_wizard.py
+++ b/randtalkbot/stranger_setup_wizard.py
@@ -4,20 +4,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import asyncio
import logging
-import re
-import sys
-import telepot
-from .errors import EmptyLanguagesError, MissingPartnerError, SexError, StrangerError
+from telepot.exception import TelegramError
+from .errors import EmptyLanguagesError, SexError, StrangerError
from .i18n import get_languages_codes, get_languages_names, LanguageNotFoundError, \
SUPPORTED_LANGUAGES_NAMES
from .stranger import SEX_NAMES
from .stranger_sender_service import StrangerSenderService
from .wizard import Wizard
-from telepot.exception import TelegramError
-def _(s): return s
+def _(string_instance):
+ return string_instance
LOGGER = logging.getLogger('randtalkbot.stranger_setup_wizard')
SEX_KEYBOARD = {
@@ -25,10 +22,9 @@ def _(s): return s
}
class StrangerSetupWizard(Wizard):
- '''
- Wizard which guides stranger through process of customizing her parameters. Activates
+ """Wizard which guides stranger through process of customizing her parameters. Activates
automatically for novices.
- '''
+ """
def __init__(self, stranger):
super(StrangerSetupWizard, self).__init__()
@@ -47,55 +43,65 @@ async def deactivate(self):
self._stranger.save()
try:
await self._sender.send_notification(
- _('Thank you. Use /begin to start looking for a conversational partner, '
- 'once you\'re matched you can use /end to end the conversation.'),
+ _(
+ 'Thank you. Use /begin to start looking for a conversational partner,'
+ ' once you\'re matched you can use /end to end the conversation.',
+ ),
reply_markup={'hide_keyboard': True},
)
- except TelegramError as e:
- LOGGER.warning('Deactivate. Can\'t notify stranger. %s', e)
+ except TelegramError as err:
+ LOGGER.warning('Deactivate. Can\'t notify stranger. %s', err)
async def handle(self, message):
- '''
- @returns `True` if message was interpreted in this method. `False` if message still needs
- interpretation.
- '''
+ """Returns:
+ bool: `True` if message was interpreted in this method. `False` if message still needs
+ interpretation.
+ """
if self._stranger.wizard == 'none': # Wizard isn't active. Check if we should activate it.
if self._stranger.is_novice():
await self.activate()
return True
- else:
- return False
+
+ return False
elif self._stranger.wizard != 'setup':
return False
+
try:
if self._stranger.wizard_step == 'languages':
try:
self._stranger.set_languages(get_languages_codes(message.text))
- except EmptyLanguagesError as e:
+ except EmptyLanguagesError as err:
await self._sender.send_notification(
_('Please specify at least one language.'),
)
- except LanguageNotFoundError as e:
+ except LanguageNotFoundError as err:
LOGGER.info('Languages weren\'t parsed: \"%s\"', message.text)
- await self._sender.send_notification(_('Language \"{0}\" wasn\'t found.'), e.name)
- except StrangerError as e:
+ await self._sender.send_notification(
+ _('Language \"{0}\" wasn\'t found.'),
+ err.name,
+ )
+ except StrangerError:
LOGGER.info('Too much languages were specified: \"%s\"', message.text)
await self._sender.send_notification(
- _('Too much languages were specified. Please shorten your list to 6 languages.'),
+ _(
+ 'Too much languages were specified. Please shorten your list'
+ ' to 6 languages.',
+ ),
)
else:
self._sender.update_translation()
self._stranger.wizard_step = 'sex'
self._stranger.save()
+
await self._prompt()
elif self._stranger.wizard_step == 'sex':
try:
self._stranger.set_sex(message.text)
- except SexError as e:
+ except SexError as err:
LOGGER.info('Stranger\'s sex wasn\'t parsed: \"%s\"', message.text)
await self._sender.send_notification(
_('Unknown sex: \"{0}\" -- is not a valid sex name.'),
- e.name,
+ err.name,
)
await self._prompt()
else:
@@ -110,11 +116,11 @@ async def handle(self, message):
elif self._stranger.wizard_step == 'partner_sex':
try:
self._stranger.set_partner_sex(message.text)
- except SexError as e:
+ except SexError as err:
LOGGER.info('Stranger partner\'s sex wasn\'t parsed: \"%s\"', message.text)
await self._sender.send_notification(
_('Unknown sex: \"{0}\" -- is not a valid sex name.'),
- e.name,
+ err.name,
)
await self._prompt()
else:
@@ -125,15 +131,16 @@ async def handle(self, message):
'Undknown wizard_step value was found: \"%s\"',
self._stranger.wizard_step,
)
- except TelegramError as e:
- LOGGER.warning('handle() Can not notify stranger. %s', e)
+ except TelegramError as err:
+ LOGGER.warning('handle() Can not notify stranger. %s', err)
+
return True
async def handle_command(self, message):
- '''
- @returns `True` if command was interpreted in this method. `False` if command still needs
- interpretation.
- '''
+ """Returns:
+ bool: `True` if command was interpreted in this method. `False` if command still needs
+ interpretation.
+ """
if self._stranger.wizard == 'none':
# Wizard isn't active. Check if we should activate it.
return (await self.handle(message)) and message.command != 'start'
@@ -146,39 +153,51 @@ async def handle_command(self, message):
await self._sender.send_notification(
_('Finish setup process please. After that you can start using bot.'),
)
- except TelegramError as e:
- LOGGER.warning('Handle command. Cant notify stranger. %s', e)
+ except TelegramError as err:
+ LOGGER.warning('Handle command. Cant notify stranger. %s', err)
await self._prompt()
return True
async def _prompt(self):
wizard_step = self._stranger.wizard_step
+
try:
if wizard_step == 'languages':
languages = self._stranger.get_languages()
# Just split languages by pairs.
- keyboard = \
- [SUPPORTED_LANGUAGES_NAMES[i: i + 2] for i in range(0, len(SUPPORTED_LANGUAGES_NAMES), 2)]
+ keyboard = [
+ SUPPORTED_LANGUAGES_NAMES[i: i + 2]
+ for i in range(0, len(SUPPORTED_LANGUAGES_NAMES), 2)
+ ]
+
try:
languages_enumeration = get_languages_names(languages)
except LanguageNotFoundError:
LOGGER.error('Language not found at setup wizard: %s', self._stranger.languages)
languages_enumeration = ''
- if len(languages) == 0 or not languages_enumeration:
- prompt = _('Enumerate the languages you speak like this: \"English, Italian\" '
- '-- in descending order of your speaking convenience or just pick one '
- 'at special keyboard.')
- else:
+
+ if languages and languages_enumeration:
if len(languages) == 1:
keyboard.append([_('Leave the language unchanged')])
- prompt = _('Your current language is {0}. Enumerate the languages '
- 'you speak like this: \"English, Italian\" -- in descending order '
- 'of your speaking convenience or just pick one at special keyboard.')
+ prompt = _(
+ 'Your current language is {0}. Enumerate the languages'
+ ' you speak like this: \"English, Italian\" -- in descending order'
+ ' of your speaking convenience or just pick one at special keyboard.',
+ )
else:
keyboard.append([_('Leave the languages unchanged')])
- prompt = _('Your current languages are: {0}. Enumerate the languages you '
- 'speak the same way -- in descending order of your speaking '
- 'convenience or just pick one at special keyboard.')
+ prompt = _(
+ 'Your current languages are: {0}. Enumerate the languages you'
+ ' speak the same way -- in descending order of your speaking'
+ ' convenience or just pick one at special keyboard.',
+ )
+ else:
+ prompt = _(
+ 'Enumerate the languages you speak like this: \"English, Italian\"'
+ ' -- in descending order of your speaking convenience or just pick one'
+ ' at special keyboard.'
+ )
+
await self._sender.send_notification(
prompt,
languages_enumeration,
@@ -188,8 +207,10 @@ async def _prompt(self):
)
elif wizard_step == 'sex':
await self._sender.send_notification(
- _('Set up your sex. If you pick \"Not Specified\" you can\'t choose '
- 'your partner\'s sex.'),
+ _(
+ 'Set up your sex. If you pick \"Not Specified\" you can\'t choose'
+ ' your partner\'s sex.',
+ ),
reply_markup=SEX_KEYBOARD,
)
elif wizard_step == 'partner_sex':
@@ -197,5 +218,5 @@ async def _prompt(self):
_('Choose your partner\'s sex'),
reply_markup=SEX_KEYBOARD,
)
- except TelegramError as e:
- LOGGER.warning('_prompt() Can not notify stranger. %s', e)
+ except TelegramError as err:
+ LOGGER.warning('_prompt() Can\'t notify stranger. %s', err)
diff --git a/randtalkbot/talk.py b/randtalkbot/talk.py
index 1b4a7fa..da9afdc 100644
--- a/randtalkbot/talk.py
+++ b/randtalkbot/talk.py
@@ -6,16 +6,18 @@
import datetime
import logging
+from peewee import DateTimeField, DoesNotExist, ForeignKeyField, IntegerField, Model, Proxy
from .errors import WrongStrangerError
from .stranger import Stranger
from .stranger_service import StrangerService
-from peewee import *
LOGGER = logging.getLogger('randtalkbot.talk')
+DATABASE_PROXY = Proxy()
-def _(s): return s
-database_proxy = Proxy()
+def _(string_instance):
+ return string_instance
+
class Talk(Model):
partner1 = ForeignKeyField(Stranger, related_name='talks_as_partner1')
@@ -27,7 +29,7 @@ class Talk(Model):
end = DateTimeField(index=True, null=True)
class Meta:
- database = database_proxy
+ database = DATABASE_PROXY
@classmethod
def delete_old(cls, before):
@@ -50,7 +52,8 @@ def get_last_partners_ids(cls, stranger):
@classmethod
def get_not_ended_talks(cls, after=None):
- talks = cls.select().where(Talk.end == None)
+ # pylint: disable=singleton-comparison
+ talks = cls.select().where(cls.end == None)
if after is not None:
talks = talks.where(Talk.begin >= after)
return talks
@@ -58,7 +61,10 @@ def get_not_ended_talks(cls, after=None):
@classmethod
def get_talk(cls, stranger):
try:
- talk = cls.get(((cls.partner1 == stranger) | (cls.partner2 == stranger)) & (cls.end == None))
+ # pylint: disable=singleton-comparison
+ talk = cls.get(
+ ((cls.partner1 == stranger) | (cls.partner2 == stranger)) & (cls.end == None),
+ )
except DoesNotExist:
return None
else:
@@ -68,9 +74,12 @@ def get_talk(cls, stranger):
return talk
def get_partner(self, stranger):
- '''
- @raise WrongStrangerError
- '''
+ """Raises:
+ WrongStrangerError
+
+ Returns:
+ Stranger
+ """
return Stranger.get(id=self.get_partner_id(stranger))
def get_partner_id(self, stranger):
diff --git a/randtalkbot/wizard.py b/randtalkbot/wizard.py
index 4b279a5..e22c058 100644
--- a/randtalkbot/wizard.py
+++ b/randtalkbot/wizard.py
@@ -4,8 +4,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import asyncio
-import logging
class Wizard:
async def activate(self):
@@ -14,9 +12,12 @@ async def activate(self):
async def deactivate(self):
raise NotImplementedError()
- async def handle(self, text):
- '''
- @returns `True` if message was interpreted in this method. `False` if message still needs
- interpretation.
- '''
+ async def handle(self, message):
+ """Raises:
+ NotImplementedError: Abstract method needs to be implemented.
+
+ Returns:
+ bool: `True` if message was interpreted in this method. `False` if message still needs
+ interpretation.
+ """
raise NotImplementedError()
diff --git a/setup.py b/setup.py
index d773de6..83249ff 100755
--- a/setup.py
+++ b/setup.py
@@ -4,19 +4,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import os
-import re
-from randtalkbot.utils import __version__
from setuptools import setup, Command
from setuptools.command.test import test as SetuptoolsTestCommand
+from randtalkbot.utils import __version__
-with open('README.rst', 'rb') as f:
- long_description = f.read().decode('utf-8')
+with open('README.rst', 'rb') as file_descriptor:
+ LONG_DESCRIPTION = file_descriptor.read() \
+ .decode('utf-8')
class CoverageCommand(Command):
user_options = []
def initialize_options(self):
+ # pylint: disable=attribute-defined-outside-init
self.coveralls_args = []
def finalize_options(self):
@@ -27,21 +27,39 @@ def run(self):
from coveralls import cli
cli.main(self.coveralls_args)
+class LintCommand(Command):
+ user_options = []
+
+ def initialize_options(self):
+ # pylint: disable=attribute-defined-outside-init
+ self.coveralls_args = []
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ self.distribution.fetch_build_eggs(self.distribution.tests_require)
+ from pylint import epylint as lint
+ lint.py_run('randtalkbot setup')
+
class TestCommand(SetuptoolsTestCommand):
def finalize_options(self):
super(TestCommand, self).finalize_options()
self.test_args = []
+ # pylint: disable=attribute-defined-outside-init
self.test_suite = True
def run_tests(self):
- import sys, coverage.cmdline
+ import coverage.cmdline
+ import sys
sys.exit(coverage.cmdline.main(argv=['run', '--source=randtalkbot', '-m', 'unittest']))
setup(
name='RandTalkBot',
version=__version__,
- description='Telegram bot matching you with a random person of desired sex speaking on your language(s).',
- long_description=long_description,
+ description='Telegram bot matching you with a random person of desired sex speaking on your'
+ ' language(s).',
+ long_description=LONG_DESCRIPTION,
keywords=['telegram', 'bot', 'anonymous', 'chat'],
license='AGPLv3+',
author='Pyotr Ermishkin',
@@ -56,6 +74,7 @@ def run_tests(self):
},
classifiers=[
'Development Status :: 5 - Production/Stable',
+ 'Framework :: Telepot',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Operating System :: OS Independent',
@@ -71,10 +90,12 @@ def run_tests(self):
],
tests_require=[
'asynctest>=0.6,<0.7',
- 'coveralls>=1.1,<2.0',
+ 'coveralls>=1.2.0,<1.3',
+ 'pylint>=1.8.1,<1.9',
],
cmdclass={
'coverage': CoverageCommand,
+ 'lint': LintCommand,
'test': TestCommand,
},
)
diff --git a/tests/test_db.py b/tests/test_db.py
index 969cc48..44e3b17 100644
--- a/tests/test_db.py
+++ b/tests/test_db.py
@@ -41,9 +41,9 @@ def test_init__ok(self):
user='foo_user',
password='foo_password',
)
- self.stats_module_mock.database_proxy.initialize.assert_called_once_with(self.database)
- self.stranger_module_mock.database_proxy.initialize.assert_called_once_with(self.database)
- self.talk_module_mock.database_proxy.initialize.assert_called_once_with(self.database)
+ self.stats_module_mock.DATABASE_PROXY.initialize.assert_called_once_with(self.database)
+ self.stranger_module_mock.DATABASE_PROXY.initialize.assert_called_once_with(self.database)
+ self.talk_module_mock.DATABASE_PROXY.initialize.assert_called_once_with(self.database)
def test_init__database_troubles(self):
self.RetryingDB.return_value.connect.side_effect = DatabaseError()
diff --git a/tests/test_stats_service.py b/tests/test_stats_service.py
index 9dbd1e1..10c42d3 100644
--- a/tests/test_stats_service.py
+++ b/tests/test_stats_service.py
@@ -355,7 +355,7 @@ def __init__(self, *args, **kwargs):
self.database = SqliteDatabase(':memory:')
def setUp(self):
- stats.database_proxy.initialize(self.database)
+ stats.DATABASE_PROXY.initialize(self.database)
self.database.create_tables([Stats])
self.update_stats = StatsService._update_stats
StatsService._update_stats = Mock()
@@ -445,9 +445,9 @@ def test_update_stats__no_stats_in_db(self):
Talk.get_not_ended_talks.assert_called_once_with(after=None)
Talk.get_ended_talks.assert_called_once_with(after=None)
Talk.delete_old.assert_not_called()
- self.assertEqual(
- json.loads(self.stats_service._stats.data_json),
- {'languages_count_distribution': [[1, 88], [2, 13]],
+ actual = json.loads(self.stats_service._stats.data_json)
+ expected = {
+ 'languages_count_distribution': [[1, 88], [2, 13]],
'languages_popularity': [['en', 67], ['it', 34], ['ru', 12]],
'languages_to_orientation': [['en',
{'female female': 6,
@@ -492,8 +492,9 @@ def test_update_stats__no_stats_in_db(self):
'60': 0,
'10': 0,
'more': 21}},
- 'total_count': 101},
- )
+ 'total_count': 101,
+ }
+ self.assertEqual(actual, expected)
@asynctest.ignore_loop
@patch('randtalkbot.stranger_service.StrangerService', Mock())
diff --git a/tests/test_stranger.py b/tests/test_stranger.py
index 4514ae8..90d4065 100644
--- a/tests/test_stranger.py
+++ b/tests/test_stranger.py
@@ -19,7 +19,7 @@
from unittest.mock import create_autospec
database = SqliteDatabase(':memory:')
-stranger.database_proxy.initialize(database)
+stranger.DATABASE_PROXY.initialize(database)
class TestStranger(asynctest.TestCase):
diff --git a/tests/test_stranger_service.py b/tests/test_stranger_service.py
index ef75c36..d4c576b 100644
--- a/tests/test_stranger_service.py
+++ b/tests/test_stranger_service.py
@@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs):
def setUp(self):
self.stranger_service = StrangerService()
- stranger.database_proxy.initialize(self.database)
+ stranger.DATABASE_PROXY.initialize(self.database)
self.database.create_tables([Stranger])
self.stranger_0 = Stranger.create(
invitation='foo',
diff --git a/tests/test_stranger_setup_wizard.py b/tests/test_stranger_setup_wizard.py
index 0f33223..3b93b73 100644
--- a/tests/test_stranger_setup_wizard.py
+++ b/tests/test_stranger_setup_wizard.py
@@ -8,6 +8,7 @@
import asynctest
from asynctest.mock import patch, Mock, CoroutineMock
from randtalkbot.errors import *
+from randtalkbot.i18n import LanguageNotFoundError
from randtalkbot.stranger_handler import *
from randtalkbot.stranger_sender_service import *
from randtalkbot.stranger_service import StrangerServiceError
@@ -118,7 +119,6 @@ async def test_handle__languages_empty_languages_error(self):
@patch('randtalkbot.stranger_setup_wizard.get_languages_codes', Mock())
async def test_handle__languages_language_not_found(self):
from randtalkbot.stranger_setup_wizard import get_languages_codes
- from randtalkbot.i18n import LanguageNotFoundError
self.stranger.wizard = 'setup'
self.stranger.wizard_step = 'languages'
get_languages_codes.side_effect = LanguageNotFoundError('foo_lang')
diff --git a/tests/test_talk.py b/tests/test_talk.py
index 2bbb600..ccea618 100644
--- a/tests/test_talk.py
+++ b/tests/test_talk.py
@@ -15,8 +15,8 @@
from unittest.mock import create_autospec, patch, Mock
database = SqliteDatabase(':memory:')
-stranger.database_proxy.initialize(database)
-talk.database_proxy.initialize(database)
+stranger.DATABASE_PROXY.initialize(database)
+talk.DATABASE_PROXY.initialize(database)
class TestTalk(unittest.TestCase):
def setUp(self):