diff --git a/LICENSE.txt b/LICENSE.txt index 9211ba0..ae11925 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 orlnub123 +Copyright (c) 2017-2018 orlnub123 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index e8eb853..9fb1fda 100644 --- a/README.rst +++ b/README.rst @@ -62,6 +62,13 @@ First import the package: import cleverbot +If you have the asynchronous dependencies and want to use Cleverbot +asynchronously import it as below instead: + +.. code:: py + + from cleverbot import async_ as cleverbot + Then initialize Cleverbot with your API key and optionally a cleverbot state and or timeout: @@ -72,40 +79,51 @@ and or timeout: The cleverbot state is the encoded state of the conversation that you get from talking to Cleverbot and includes the whole conversation history. -If you have the asynchronous dependencies and want to use Cleverbot -asynchronously import ``cleverbot.async_`` and initialize Cleverbot from -``cleverbot.async_.Cleverbot`` instead. The only differences are that ``say`` -is a coroutine and that you can pass an event loop to Cleverbot with a ``loop`` -keyword argument. +If you're using Cleverbot asynchronously you can also give an event loop to +Cleverbot with a ``loop`` keyword argument -------------- You can now start talking to Cleverbot. -Get the reply from the request: +Talk straight to Cleverbot: .. code:: py reply = cb.say("Hello") -Or alternatively get it later: - -.. code:: py - - cb.say("Hello") - reply = cb.output - -You can also pass in keyword arguments such as ``cs`` to change the +You can pass in keyword arguments to ``say`` such as ``cs`` to change the conversation, ``vtext`` to change the current conversation's history, or even ``cb_settings_tweak`` to change Cleverbot's mood. Read the "Parameters" section of `the official Cleverbot API docs `_ for more information. +Alternatively, start a new conversation and talk from it: + +.. code:: py + + convo = cb.conversation() + reply = convo.say("Hello") + +Conversations are like mini Cleverbots so you can pass in anything that +Cleverbot takes as keyword arguments including ``key``. The values you don't +pass in excluding the cleverbot state will be taken from the originating +Cleverbot. + +Normally conversations get saved in ``cb.conversations`` as a list but if you +want to manage them more easily you can pass in a name as the first argument to +every conversation you create which will turn ``cb.conversations`` into a +dictionary with the name as the key and the conversation as the value. Trying +to mix both named and nameless conversations will result in an error. + +``say`` is a coroutine for both Cleverbot and its conversations if you're +running asynchronously. + -------------- -If something goes wrong with the request, such as an invalid API key, an -``APIError`` will be raised containing the error message or, if you've defined -a timeout and you don't get a reply within the defined amount of seconds you'll +If something goes wrong with the request such as an invalid API key an +``APIError`` will be raised containing the error message or if you've defined +a timeout and don't get a reply within the defined amount of seconds you'll get a ``Timeout``. As an example: @@ -113,7 +131,7 @@ As an example: ``cleverbot.errors.APIError: Missing or invalid API key or POST request, please visit www.cleverbot.com/api`` -You can get the error message and additionally the HTTP status from the error +You can get the error message and the HTTP status from the error like so: .. code:: py @@ -131,26 +149,68 @@ it to catch every Cleverbot related error. -------------- -To access the data gained from the conversations you can either get them from -an attribute as shown previously or directly get them from ``cb.data``: +To access the data gained from talking straight to Cleverbot or from talking in +a conversation you can either get it from an attribute as shown previously or +directly get it from the ``data`` dictionary: .. code:: py cb.conversation_id == cb.data['conversation_id'] + convo.conversation_id == convo.data['conversation_id'] -Note that every attribute except for cs (i.e., the cleverbot state) is +Note that every attribute except for ``cs`` (i.e. the cleverbot state) is read-only and will get shadowed if you set it to something. For a list of all of the data and their descriptions go to the "JSON Reply" section in `the official Cleverbot API docs `_. -To reset the data you can simply do the following: +To reset Cleverbot's and all of its conversations' data you can simply do the +following: .. code:: py cb.reset() +To only reset a single conversation's data use ``reset`` on the conversation +instead: + +.. code:: py + + convo.reset() + +Resetting won't delete any conversations so you'll be able to reuse them. + +-------------- + +If you want to save the current state of Cleverbot and all of its conversations +you can use ``cb.save``: + +.. code:: py + + with open('cleverbot.txt', 'wb') as file: + cb.save(file) + +This saves the key and timeout you've given to Cleverbot and its conversations +and also the current cleverbot state of each. + +In order to load and recreate the previously saved state as a new Cleverbot +instance use ``cleverbot.load``: + +.. code:: py + + with open('cleverbot.txt', 'rb') as file: + cb = cleverbot.load(file) + +To only load the conversations use ``cb.load``: + +.. code:: py + + with open('cleverbot.txt', 'rb') as file: + cb.load(file) + +Loading conversations will delete the old ones. + -------------- When you're done with the current instance of Cleverbot, close Cleverbot's diff --git a/cleverbot/__init__.py b/cleverbot/__init__.py index 6d3a22a..e0518d0 100644 --- a/cleverbot/__init__.py +++ b/cleverbot/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.0.1' +__version__ = '2.1.0' -from .cleverbot import Cleverbot +from .cleverbot import Cleverbot, load from .errors import CleverbotError, APIError, DecodeError, Timeout diff --git a/cleverbot/abc.py b/cleverbot/abc.py deleted file mode 100644 index 1199515..0000000 --- a/cleverbot/abc.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import absolute_import - -import abc - - -class CleverbotBase(abc.ABCMeta('ABC', (), {})): # Python 2 compatibility - """Base class for Cleverbot.""" - - url = 'https://www.cleverbot.com/getreply' - - def __getattr__(self, attr): - """Allow access to the stored data through attributes.""" - try: - return self.data[attr] - except KeyError: - message = "'{0}' object has no attribute '{1}'" - raise AttributeError(message.format(self.__class__.__name__, attr)) - - @property - def cs(self): - return self.data.get('cs') - - @cs.setter - def cs(self, value): - self.data['cs'] = value - - @cs.deleter - def cs(self): - self.data.pop('cs', None) - - @abc.abstractmethod - def say(self): - pass - - def reset(self): - """Reset all of Cleverbot's stored data.""" - self.data = {} diff --git a/cleverbot/async_/__init__.py b/cleverbot/async_/__init__.py index c5587f0..d5a363c 100644 --- a/cleverbot/async_/__init__.py +++ b/cleverbot/async_/__init__.py @@ -1 +1,2 @@ -from .cleverbot import Cleverbot +from .cleverbot import Cleverbot, load +from .. import __version__, CleverbotError, APIError, DecodeError, Timeout diff --git a/cleverbot/async_/cleverbot.py b/cleverbot/async_/cleverbot.py index 646ee0c..ec3841f 100644 --- a/cleverbot/async_/cleverbot.py +++ b/cleverbot/async_/cleverbot.py @@ -1,34 +1,14 @@ import asyncio +import pickle import aiohttp from .. import __version__ -from ..abc import CleverbotBase +from ..base import CleverbotBase, ConversationBase from ..errors import APIError, DecodeError, Timeout -class Cleverbot(CleverbotBase): - """An asynchronous Cleverbot API wrapper.""" - - def __init__(self, key, *, cs=None, timeout=None, loop=None): - """Initialize Cleverbot with the given arguments. - - Arguments: - key: The key argument is always required. It is your API key. - cs: The cs argument stands for "cleverbot state". It is the encoded - state of the conversation so far and includes the whole - conversation history up to that point. - timeout: How many seconds to wait for the API to send data before - giving up and raising an error. - loop: The event loop used for the asynchronous requests. - """ - self.key = key - self.data = {} - if cs is not None: - self.data['cs'] = cs - self.timeout = timeout - loop = asyncio.get_event_loop() if loop is None else loop - self.session = aiohttp.ClientSession(loop=loop) +class SayMixin: @asyncio.coroutine def say(self, input=None, **kwargs): @@ -44,13 +24,6 @@ def say(self, input=None, **kwargs): Raises: APIError: A Cleverbot API error occurred. - Status codes: - 401: Unauthorised due to invalid API key. - 404: API not found. - 413: Request too large if you send a request over 64Kb. - 502 or 504: Unable to get reply from API server, please - contact us. - 503: Too many requests from a single IP address or API key. DecodeError: An error occurred while reading the reply. Timeout: The request timed out. """ @@ -86,10 +59,67 @@ def say(self, input=None, **kwargs): else: if reply.status == 200: self.data = data - return data['output'] + return data.get('output') else: - raise APIError(data['error'], data['status']) + raise APIError(data.get('error'), data.get('status')) + + +class Cleverbot(SayMixin, CleverbotBase): + """An asynchronous Cleverbot API wrapper.""" + + def __init__(self, *args, loop=None, **kwargs): + """Initialize Cleverbot with the given arguments. + + Arguments: + key: The key argument is always required. It is your API key. + cs: The cs argument stands for "cleverbot state". It is the encoded + state of the conversation so far and includes the whole + conversation history up to that point. + timeout: How many seconds to wait for the API to respond before + giving up and raising an error. + loop: The event loop used for the asynchronous requests. + """ + super().__init__(*args, **kwargs) + loop = asyncio.get_event_loop() if loop is None else loop + self.session = aiohttp.ClientSession(loop=loop) + + def conversation(self, name=None, **kwargs): + """Make a new conversation. + + Arguments: + name: The key for the dictionary the conversation will be stored as + in conversations. If None the conversation will be stored as a + list instead. Mixing both types results in an error. + **kwargs: Keyword arguments to pass into the new conversation. + These accept the same arguments as Cleverbot. + + Returns: + The new conversation. + """ + convo = Conversation(self, **kwargs) + super().conversation(name, convo) + return convo def close(self): - """Close the connection to the API.""" + """Close Cleverbot's connection to the API.""" self.session.close() + + +class Conversation(SayMixin, ConversationBase): + pass + + +def load(file): + """Load and return the previously saved Cleverbot with its conversations. + + Arguments: + file: The file object to load Cleverbot and its conversations from. + + Returns: + A new Cleverbot instance. + """ + cleverbot_kwargs, convos = pickle.load(file) + cleverbot = Cleverbot(**cleverbot_kwargs) + for convo_kwargs in convos: + cleverbot.conversation(**convo_kwargs) + return cleverbot diff --git a/cleverbot/base.py b/cleverbot/base.py new file mode 100644 index 0000000..7e18845 --- /dev/null +++ b/cleverbot/base.py @@ -0,0 +1,132 @@ +import pickle + + +class AttributeMixin(object): + + url = 'https://www.cleverbot.com/getreply' + + def __getattr__(self, attr): + """Allow access to the stored data through attributes.""" + try: + return self.data[attr] + except KeyError: + message = "'{0}' object has no attribute '{1}'" + raise AttributeError(message.format(self.__class__.__name__, attr)) + + @property + def cs(self): + return self.data.get('cs') + + @cs.setter + def cs(self, value): + self.data['cs'] = value + + @cs.deleter + def cs(self): + self.data.pop('cs', None) + + +class CleverbotBase(AttributeMixin): + """Base class for Cleverbot.""" + + def __init__(self, key, **kwargs): # Python 2 compatible keyword-only args + self.key = key + self.data = {} + if 'cs' in kwargs: + self.data['cs'] = kwargs.pop('cs') + self.timeout = kwargs.pop('timeout', None) + self.conversations = None + if kwargs: + message = "__init__() got an unexpected keyword argument '{0}'" + raise TypeError(message.format(next(iter(kwargs)))) + + def conversation(self, name, convo): + """Initialize conversations if necessary and add the conversation to + it. + """ + if self.conversations is None: + self.conversations = {} if name is not None else [] + if name is not None: + message = "Can't mix named conversations with nameless ones" + assert isinstance(self.conversations, dict), message + self.conversations[name] = convo + else: + message = "Can't mix nameless conversations with named ones" + assert isinstance(self.conversations, list), message + self.conversations.append(convo) + + def reset(self): + """Reset Cleverbot's stored data and all of its conversations.""" + self.data = {} + for convo in self.conversations: + if isinstance(self.conversations, dict): + convo = self.conversations[convo] + convo.reset() + + def save(self, file): + """Save Cleverbot and all of its conversations into the specified file + object. + + Arguments: + file: A file object that accepts bytes to save the data to. + """ + obj = ({'key': self.key, 'cs': self.cs, 'timeout': self.timeout}, []) + for convo in self.conversations: + if isinstance(self.conversations, dict): + name, convo = convo, self.conversations[convo] + convo_dict = {'name': name, 'cs': convo.data.get('cs')} + else: + convo_dict = {'cs': convo.data.get('cs')} + for item in ('key', 'timeout'): + try: + convo_dict[item] = convo.__dict__[item] + except KeyError: + pass + obj[1].append(convo_dict) + pickle.dump(obj, file, pickle.HIGHEST_PROTOCOL) + + def load(self, file): + """Load and replace Cleverbot's conversations with the previously saved + conversations from the file. + + Arguments: + file: The file object to load the saved conversations from. + """ + convos = pickle.load(file)[1] + self.conversations = None + for convo_kwargs in convos: + self.conversation(**convo_kwargs) + + +class ConversationBase(AttributeMixin): + """Base class for Conversation.""" + + def __init__(self, cleverbot, **kwargs): + self.cleverbot = cleverbot + self.data = {} + for item in ('key', 'cs', 'timeout'): + if item in kwargs: + setattr(self, item, kwargs.pop(item)) + self.session = cleverbot.session + if kwargs: + message = "__init__() got an unexpected keyword argument '{0}'" + raise TypeError(message.format(next(iter(kwargs)))) + + @property + def key(self): + return self.__dict__.get('key', self.cleverbot.key) + + @key.setter + def key(self, value): + self.__dict__['key'] = value + + @property + def timeout(self): + return self.__dict__.get('timeout', self.cleverbot.timeout) + + @timeout.setter + def timeout(self, value): + self.__dict__['timeout'] = value + + def reset(self): + self.data = {} diff --git a/cleverbot/cleverbot.py b/cleverbot/cleverbot.py index ce7c2ca..cddd206 100644 --- a/cleverbot/cleverbot.py +++ b/cleverbot/cleverbot.py @@ -1,33 +1,13 @@ +import pickle + import requests from . import __version__ -from .abc import CleverbotBase +from .base import CleverbotBase, ConversationBase from .errors import APIError, DecodeError, Timeout -class Cleverbot(CleverbotBase): - """A Cleverbot API wrapper.""" - - def __init__(self, key, **kwargs): # Python 2 compatible keyword-only args - """Initialize Cleverbot with the given arguments. - - Arguments: - key: The key argument is always required. It is your API key. - cs: The cs argument stands for "cleverbot state". It is the encoded - state of the conversation so far and includes the whole - conversation history up to that point. - timeout: How many seconds to wait for the API to send data before - giving up and raising an error. - """ - self.key = key - self.data = {} - if 'cs' in kwargs: - self.data['cs'] = kwargs.pop('cs') - self.timeout = kwargs.pop('timeout', None) - self.session = requests.Session() - if kwargs: - message = "__init__() got an unexpected keyword argument '{0}'" - raise TypeError(message.format(next(iter(kwargs)))) +class SayMixin: def say(self, input=None, **kwargs): """Talk to Cleverbot. @@ -42,13 +22,6 @@ def say(self, input=None, **kwargs): Raises: APIError: A Cleverbot API error occurred. - Status codes: - 401: Unauthorised due to invalid API key. - 404: API not found. - 413: Request too large if you send a request over 64Kb. - 502 or 504: Unable to get reply from API server, please - contact us. - 503: Too many requests from a single IP address or API key. DecodeError: An error occurred while reading the reply. Timeout: The request timed out. """ @@ -78,10 +51,65 @@ def say(self, input=None, **kwargs): else: if reply.status_code == 200: self.data = data - return data['output'] + return data.get('output') else: - raise APIError(data['error'], data['status']) + raise APIError(data.get('error'), data.get('status')) + + +class Cleverbot(SayMixin, CleverbotBase): + """A Cleverbot API wrapper.""" + + def __init__(self, *args, **kwargs): + """Initialize Cleverbot with the given arguments. + + Arguments: + key: The key argument is always required. It is your API key. + cs: The cs argument stands for "cleverbot state". It is the encoded + state of the conversation so far and includes the whole + conversation history up to that point. + timeout: How many seconds to wait for the API to respond before + giving up and raising an error. + """ + super(Cleverbot, self).__init__(*args, **kwargs) + self.session = requests.Session() + + def conversation(self, name=None, **kwargs): + """Make a new conversation. + + Arguments: + name: The key for the dictionary the conversation will be stored as + in conversations. If None the conversation will be stored as a + list instead. Mixing both types results in an error. + **kwargs: Keyword arguments to pass into the new conversation. + These accept the same arguments as Cleverbot. + + Returns: + The new conversation. + """ + convo = Conversation(self, **kwargs) + super(Cleverbot, self).conversation(name, convo) + return convo def close(self): - """Close the connection to the API.""" + """Close Cleverbot's connection to the API.""" self.session.close() + + +class Conversation(SayMixin, ConversationBase): + pass + + +def load(file): + """Load and return the previously saved Cleverbot with its conversations. + + Arguments: + file: The file object to load Cleverbot and its conversations from. + + Returns: + A new Cleverbot instance. + """ + cleverbot_kwargs, convos = pickle.load(file) + cleverbot = Cleverbot(**cleverbot_kwargs) + for convo_kwargs in convos: + cleverbot.conversation(**convo_kwargs) + return cleverbot diff --git a/cleverbot/errors.py b/cleverbot/errors.py index 0035390..4af8f82 100644 --- a/cleverbot/errors.py +++ b/cleverbot/errors.py @@ -3,14 +3,8 @@ class CleverbotError(Exception): class APIError(CleverbotError): - """Raised when a Cleverbot API error occurs. - - Errors: - 401: Unauthorised due to invalid API key. - 404: API not found. - 413: Request too large if you send a request over 16KB. - 502 or 504: Unable to get reply from API server, please contact us. - 503: Too many requests from a single IP address or API key. + """Raised when a Cleverbot API error occurs. See the official Cleverbot + documentation for an updated list of all the possible errors. """ def __init__(self, error, status): @@ -20,14 +14,13 @@ def __init__(self, error, status): class DecodeError(CleverbotError): - """Raised when a decode error occurs while reading the reply. - - Reset Cleverbot to fix it. + """Raised when a decode error occurs while reading the reply. Reset + Cleverbot or the respective conversation to fix it. """ class Timeout(CleverbotError): - """Raised when the request times out after the specified time.""" + """Raised when the request takes longer than the specified time.""" def __init__(self, timeout): super(Timeout, self).__init__( diff --git a/setup.cfg b/setup.cfg index 498ec14..910e5ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ +[aliases] +test = pytest + [metadata] license_file = LICENSE.txt diff --git a/setup.py b/setup.py index 05d9569..4f9a3e9 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,19 @@ import os import re +import sys try: from setuptools import setup except ImportError: from distutils.core import setup +if sys.version_info < (3, 4, 2): + os.environ['PYTEST_ADDOPTS'] = '-p no:asyncio' +else: + import importlib + if importlib.util.find_spec('aiohttp') is None: + os.environ['PYTEST_ADDOPTS'] = '-p no:asyncio' + os.chdir(os.path.dirname(os.path.abspath(__file__))) with open('cleverbot/__init__.py') as f: @@ -27,6 +35,8 @@ packages=['cleverbot', 'cleverbot.async_'], install_requires=['requests>=1.0.0'], extras_require={'async': ['aiohttp>=1.0.0']}, + setup_requires=['pytest-runner'], + tests_require=['pytest>=2.5.0', 'pytest-asyncio>=0.1.3'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: MIT License', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/async_tests/__init__.py b/tests/async_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/async_tests/conftest.py b/tests/async_tests/conftest.py new file mode 100644 index 0000000..e7d4856 --- /dev/null +++ b/tests/async_tests/conftest.py @@ -0,0 +1,19 @@ +import sys + +import pytest + + +def pytest_ignore_collect(path, config): + if sys.version_info < (3, 4, 2): + return True + + if hasattr(config.pluginmanager, 'get_plugin'): + plugin = config.pluginmanager.get_plugin('asyncio') + else: + plugin = config.pluginmanager.getplugin('asyncio') + if not plugin: + return True + + import importlib + if importlib.util.find_spec('aiohttp') is None: + return True diff --git a/tests/async_tests/test_cleverbot.py b/tests/async_tests/test_cleverbot.py new file mode 100644 index 0000000..073c9f3 --- /dev/null +++ b/tests/async_tests/test_cleverbot.py @@ -0,0 +1,276 @@ +import asyncio +import io + +from cleverbot import async_ as cleverbot +import pytest + + +@pytest.fixture +def cb(monkeypatch, request): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + if hasattr(request, 'param'): + @asyncio.coroutine + def mock_get(url, params, headers, timeout): + class MockResponse(object): + def __init__(self): + if request.param.get('timeout'): + raise asyncio.TimeoutError + self.status = request.param.get('status', 200) + + @asyncio.coroutine + def json(self): + return request.param.get('json', { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }) + + return MockResponse() + monkeypatch.setattr(cb.session, 'get', mock_get) + yield cb + cb.close() + + +@pytest.fixture +def cb_nameless(): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + for i, s in enumerate(map(str, range(200))): + cb.conversation(key=s, cs=s, timeout=i) + yield cb + cb.close() + + +@pytest.fixture +def cb_named(): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + for i, s in enumerate(map(str, range(200))): + cb.conversation(s, key=s, cs=s, timeout=i) + yield cb + cb.close() + + +class TestCleverbot: + + def test_init(self): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + assert cb.key == 'API_KEY' + assert cb.cs == '76nxdxIJ02AAA' + assert cb.timeout == 60 + cb.close() + + def test_cs(self): + cb = cleverbot.Cleverbot(None, cs='76nxdxIJ02AAA') + assert cb.cs == cb.data['cs'] + cb.cs = 'test' + assert cb.cs == 'test' + cb.close() + + def test_getattr(self, cb): + cb.data = {'test': 'value'} + assert cb.test == 'value' + + @pytest.mark.asyncio + @pytest.mark.parametrize( + 'cb', [ + {'status': 200, 'json': { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }} + ], indirect=True + ) + def test_say(self, cb): + assert (yield from cb.say()) == 'test' + assert cb.cs == 'cs' + assert cb.test == 'test' + + @pytest.mark.asyncio + @pytest.mark.parametrize( + 'cb', [ + {'status': 401, 'json': {'status': 401, 'error': 'text'}} + ], indirect=True + ) + def test_say_apierror(self, cb): + with pytest.raises(cleverbot.APIError, message='text'): + try: + yield from cb.say() + except cleverbot.APIError as e: + assert e.status == 401 + raise + + @pytest.mark.asyncio + @pytest.mark.parametrize('cb', [{'timeout': True}], indirect=True) + def test_say_timeout(self, cb): + with pytest.raises(cleverbot.Timeout): + yield from cb.say() + + def test_conversation_empty(self, cb): + convo = cb.conversation() + assert convo.key == cb.key + assert convo.cs is None + assert convo.timeout == cb.timeout + + def test_conversation_nameless(self, cb): + convo = cb.conversation(key='key', cs='cs', timeout=10) + assert convo.key == 'key' + assert convo.cs == 'cs' + assert convo.timeout == 10 + + def test_conversation_named(self, cb): + convo = cb.conversation('name', key='key', cs='cs', timeout=10) + assert convo.key == 'key' + assert convo.cs == 'cs' + assert convo.timeout == 10 + + def test_conversations_nameless(self, cb): + for i, s in enumerate(map(str, range(200))): + cb.conversation(key=s, cs=s*2, timeout=i) + convos = cb.conversations + for convo, (i, s) in zip(convos, enumerate(map(str, range(200)))): + for item, value in zip(('key', 'cs', 'timeout'), (s, s*2, i)): + assert getattr(convo, item) == value + + def test_conversations_named(self, cb): + convos = {s: cb.conversation(s, key=s, cs=s*2, timeout=i) + for i, s in enumerate(map(str, range(200)))} + for name, convo1 in cb.conversations.items(): + convo2 = convos[name] + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_reset_nameless(self, cb_nameless): + cb_nameless.reset() + assert not cb_nameless.data + for convo in cb_nameless.conversations: + assert not convo.data + + def test_reset_named(self, cb_named): + cb_named.reset() + assert not cb_named.data + for convo in cb_named.conversations.values(): + assert not convo.data + + +class TestConversation: + + @pytest.fixture + def convo(self, cb, monkeypatch, request): + convo = cb.conversation(key='API_KEY', cs='76nxdxIJ02AAA', timeout=60) + if hasattr(request, 'param'): + @asyncio.coroutine + def mock_get(url, params, headers, timeout): + class MockResponse(object): + def __init__(self): + if request.param.get('timeout'): + raise asyncio.TimeoutError + self.status = request.param.get('status', 200) + + @asyncio.coroutine + def json(self): + return request.param.get('json', { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }) + + return MockResponse() + monkeypatch.setattr(convo.session, 'get', mock_get) + return convo + + def test_cs(self, cb): + convo = cb.conversation(cs='76nxdxIJ02AAA') + assert convo.cs == cb.cs + convo.cs = 'test' + assert convo.cs == 'test' + + def test_getattr(self, convo): + convo.data = {'test': 'value'} + assert convo.test == 'value' + + @pytest.mark.asyncio + @pytest.mark.parametrize( + 'convo', [ + {'status': 200, 'json': { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }} + ], indirect=True + ) + def test_say(self, convo): + assert (yield from convo.say()) == 'test' + assert convo.cs == 'cs' + assert convo.test == 'test' + + @pytest.mark.asyncio + @pytest.mark.parametrize( + 'convo', [ + {'status': 401, 'json': {'status': 401, 'error': 'text'}} + ], indirect=True + ) + def test_say_apierror(self, convo): + with pytest.raises(cleverbot.APIError, message='text'): + try: + yield from convo.say() + except cleverbot.APIError as e: + assert e.status == 401 + raise + + @pytest.mark.asyncio + @pytest.mark.parametrize('convo', [{'timeout': True}], indirect=True) + def test_say_timeout(self, convo): + with pytest.raises(cleverbot.Timeout): + yield from convo.say() + + def test_reset(self, convo): + convo.reset() + assert not convo.data + + +class TestIO: + + def test_cleverbot_save_nameless(self, cb_nameless): + with io.BytesIO() as f: + cb_nameless.save(f) + assert f.getvalue() + + def test_cleverbot_save_named(self, cb_named): + with io.BytesIO() as f: + cb_named.save(f) + assert f.getvalue() + + def test_cleverbot_load_nameless(self, cb, cb_nameless): + with io.BytesIO() as f: + cb_nameless.save(f) + with io.BytesIO(f.getvalue()) as f: + cb.load(f) + for convo1, convo2 in zip(cb.conversations, cb_nameless.conversations): + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_cleverbot_load_named(self, cb, cb_named): + convos = cb_named.conversations + with io.BytesIO() as f: + cb_named.save(f) + with io.BytesIO(f.getvalue()) as f: + cb.load(f) + for name, convo1 in cb.conversations.items(): + convo2 = convos[name] + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_load_nameless(self, cb_nameless): + with io.BytesIO() as f: + cb_nameless.save(f) + with io.BytesIO(f.getvalue()) as f: + cb = cleverbot.load(f) + for item in ('key', 'cs', 'timeout'): + assert getattr(cb, item) == getattr(cb_nameless, item) + for convo1, convo2 in zip(cb.conversations, cb_nameless.conversations): + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_load_named(self, cb_named): + convos = cb_named.conversations + with io.BytesIO() as f: + cb_named.save(f) + with io.BytesIO(f.getvalue()) as f: + cb = cleverbot.load(f) + for item in ('key', 'cs', 'timeout'): + assert getattr(cb, item) == getattr(cb_named, item) + for name, convo1 in cb.conversations.items(): + convo2 = convos[name] + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) diff --git a/tests/test_cleverbot.py b/tests/test_cleverbot.py new file mode 100644 index 0000000..05ee898 --- /dev/null +++ b/tests/test_cleverbot.py @@ -0,0 +1,263 @@ +import io + +import cleverbot +import pytest +import requests + + +@pytest.fixture +def cb(monkeypatch, request): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + if hasattr(request, 'param'): + def mock_get(url, params, headers, timeout): + + class MockResponse(object): + def __init__(self): + if request.param.get('timeout'): + raise requests.Timeout + self.status_code = request.param.get('status', 200) + + def json(self): + return request.param.get('json', { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }) + + return MockResponse() + monkeypatch.setattr(cb.session, 'get', mock_get) + return cb + + +@pytest.fixture +def cb_nameless(): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + for i, s in enumerate(map(str, range(200))): + cb.conversation(key=s, cs=s, timeout=i) + return cb + + +@pytest.fixture +def cb_named(): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + for i, s in enumerate(map(str, range(200))): + cb.conversation(s, key=s, cs=s, timeout=i) + return cb + + +class TestCleverbot: + + def test_init(self): + cb = cleverbot.Cleverbot('API_KEY', cs='76nxdxIJ02AAA', timeout=60) + assert cb.key == 'API_KEY' + assert cb.cs == '76nxdxIJ02AAA' + assert cb.timeout == 60 + + def test_cs(self): + cb = cleverbot.Cleverbot(None, cs='76nxdxIJ02AAA') + assert cb.cs == cb.data['cs'] + cb.cs = 'test' + assert cb.cs == 'test' + + def test_getattr(self, cb): + cb.data = {'test': 'value'} + assert cb.test == 'value' + + @pytest.mark.parametrize( + 'cb', [ + {'status': 200, 'json': { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }} + ], indirect=True + ) + def test_say(self, cb): + assert cb.say() == 'test' + assert cb.cs == 'cs' + assert cb.test == 'test' + + @pytest.mark.parametrize( + 'cb', [ + {'status': 401, 'json': {'status': 401, 'error': 'text'}} + ], indirect=True + ) + def test_say_apierror(self, cb): + with pytest.raises(cleverbot.APIError, message='text'): + try: + cb.say() + except cleverbot.APIError as e: + assert e.status == 401 + raise + + @pytest.mark.parametrize('cb', [{'timeout': True}], indirect=True) + def test_say_timeout(self, cb): + with pytest.raises(cleverbot.Timeout): + cb.say() + + def test_conversation_empty(self, cb): + convo = cb.conversation() + assert convo.key == cb.key + assert convo.cs is None + assert convo.timeout == cb.timeout + + def test_conversation_nameless(self, cb): + convo = cb.conversation(key='key', cs='cs', timeout=10) + assert convo.key == 'key' + assert convo.cs == 'cs' + assert convo.timeout == 10 + + def test_conversation_named(self, cb): + convo = cb.conversation('name', key='key', cs='cs', timeout=10) + assert convo.key == 'key' + assert convo.cs == 'cs' + assert convo.timeout == 10 + + def test_conversations_nameless(self, cb): + for i, s in enumerate(map(str, range(200))): + cb.conversation(key=s, cs=s*2, timeout=i) + convos = cb.conversations + for convo, (i, s) in zip(convos, enumerate(map(str, range(200)))): + for item, value in zip(('key', 'cs', 'timeout'), (s, s*2, i)): + assert getattr(convo, item) == value + + def test_conversations_named(self, cb): + convos = {} + for i, s in enumerate(map(str, range(200))): + convos[s] = cb.conversation(s, key=s, cs=s*2, timeout=i) + for name, convo1 in cb.conversations.items(): + convo2 = convos[name] + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_reset_nameless(self, cb_nameless): + cb_nameless.reset() + assert not cb_nameless.data + for convo in cb_nameless.conversations: + assert not convo.data + + def test_reset_named(self, cb_named): + cb_named.reset() + assert not cb_named.data + for convo in cb_named.conversations.values(): + assert not convo.data + + +class TestConversation: + + @pytest.fixture + def convo(self, cb, monkeypatch, request): + convo = cb.conversation(key='API_KEY', cs='76nxdxIJ02AAA', timeout=60) + if hasattr(request, 'param'): + def mock_get(url, params, headers, timeout): + class MockResponse(object): + def __init__(self): + if request.param.get('timeout'): + raise requests.Timeout + self.status_code = request.param.get('status', 200) + + def json(self): + return request.param.get('json', { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }) + + return MockResponse() + monkeypatch.setattr(convo.session, 'get', mock_get) + return convo + + def test_cs(self, cb): + convo = cb.conversation(cs='76nxdxIJ02AAA') + assert convo.cs == cb.cs + convo.cs = 'test' + assert convo.cs == 'test' + + def test_getattr(self, convo): + convo.data = {'test': 'value'} + assert convo.test == 'value' + + @pytest.mark.parametrize( + 'convo', [ + {'status': 200, 'json': { + 'output': 'test', 'cs': 'cs', 'test': 'test' + }} + ], indirect=True + ) + def test_say(self, convo): + assert convo.say() == 'test' + assert convo.cs == 'cs' + assert convo.test == 'test' + + @pytest.mark.parametrize( + 'convo', [ + {'status': 401, 'json': {'status': 401, 'error': 'text'}} + ], indirect=True + ) + def test_say_apierror(self, convo): + with pytest.raises(cleverbot.APIError, message='text'): + try: + convo.say() + except cleverbot.APIError as e: + assert e.status == 401 + raise + + @pytest.mark.parametrize('convo', [{'timeout': True}], indirect=True) + def test_say_timeout(self, convo): + with pytest.raises(cleverbot.Timeout): + convo.say() + + def test_reset(self, convo): + convo.reset() + assert not convo.data + + +class TestIO: + + def test_cleverbot_save_nameless(self, cb_nameless): + with io.BytesIO() as f: + cb_nameless.save(f) + assert f.getvalue() + + def test_cleverbot_save_named(self, cb_named): + with io.BytesIO() as f: + cb_named.save(f) + assert f.getvalue() + + def test_cleverbot_load_nameless(self, cb, cb_nameless): + with io.BytesIO() as f: + cb_nameless.save(f) + with io.BytesIO(f.getvalue()) as f: + cb.load(f) + for convo1, convo2 in zip(cb.conversations, cb_nameless.conversations): + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_cleverbot_load_named(self, cb, cb_named): + convos = cb_named.conversations + with io.BytesIO() as f: + cb_named.save(f) + with io.BytesIO(f.getvalue()) as f: + cb.load(f) + for name, convo1 in cb.conversations.items(): + convo2 = convos[name] + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_load_nameless(self, cb_nameless): + with io.BytesIO() as f: + cb_nameless.save(f) + with io.BytesIO(f.getvalue()) as f: + cb = cleverbot.load(f) + for item in ('key', 'cs', 'timeout'): + assert getattr(cb, item) == getattr(cb_nameless, item) + for convo1, convo2 in zip(cb.conversations, cb_nameless.conversations): + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item) + + def test_load_named(self, cb_named): + convos = cb_named.conversations + with io.BytesIO() as f: + cb_named.save(f) + with io.BytesIO(f.getvalue()) as f: + cb = cleverbot.load(f) + for item in ('key', 'cs', 'timeout'): + assert getattr(cb, item) == getattr(cb_named, item) + for name, convo1 in cb.conversations.items(): + convo2 = convos[name] + for item in ('key', 'cs', 'timeout'): + assert getattr(convo1, item) == getattr(convo2, item)