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)