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):