Repository: python-telegram-bot/python-telegram-bot

Issue:
[BUG] Error on persistence of nested conversation
<!--
Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already.

The following template is a suggestion how you can report an issue you run into whilst using our library. If you just want to ask a question, feel free to delete everything; just make sure you have a describing title :)

Please mind that there is also a users' Telegram group at https://t.me/pythontelegrambotgroup for questions about the library. Questions asked there might be answered quicker than here. In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel at https://webchat.freenode.net/?channels=##python-telegram-bot to participate in the group.
-->

### Issue I am facing
When I tried to persistence a nested conversation on [nestedconversationbot.py](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.py), I got the following error message when I entered the sub-conversation, for example `Add Family Member`:
```
"'NoneType' object has no attribute 'update_conversation'"
```

I guess the sub-conversations will always get `None`, and they cause the issue while saving conversation states.

Is this a known issue? Or Is there a way to solve this problem?


### Traceback to the issue
```
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/dist-packages/telegram/ext/dispatcher.py", line 372, in process_update
    handler.handle_update(update, self, check, context)
  File "/usr/local/lib/python3.5/dist-packages/telegram/ext/conversationhandler.py", line 343, in handle_update
    new_state = handler.handle_update(update, dispatcher, check_result, context)
  File "/usr/local/lib/python3.5/dist-packages/telegram/ext/conversationhandler.py", line 357, in handle_update
    self.update_state(new_state, conversation_key)
  File "/usr/local/lib/python3.5/dist-packages/telegram/ext/conversationhandler.py", line 376, in update_state
    self.persistence.update_conversation(self.name, key, new_state)
AttributeError: 'NoneType' object has no attribute 'update_conversation'
```

### Related part of your code
I wrote my code just like this. Here, `description_conv` and `add_member_conv` are the sub-conversations of the top level `conv_handler`.

```python
# Set up third level ConversationHandler (collecting features)
description_conv = ConversationHandler(
    entry_points=[...]
    states={...},
    fallbacks=[...],
    map_to_parent={...},
    persistent=True,
    name='test',
)

# Set up second level ConversationHandler (adding a person)
add_member_conv = ConversationHandler(
    entry_points=[...]
    states={
        SELECTING_GENDER: [description_conv],
    },
    fallbacks=[...],
    map_to_parent={...},
    persistent=True,
    name='test2',
)

# Set up top level ConversationHandler (selecting action)
conv_handler = ConversationHandler(
    entry_points=[CommandHandler('start', start)],
    states={
        SELECTING_ACTION: [add_member_conv],
        DESCRIBING_SELF: [description_conv],
    },
    fallbacks=[...],
    persistent=True,
    name='test3',
)
```



Discussion:
Hi. To be honest, we didn't think of persistence issues when merging nested conversations. I'll look into it, but I fear that this won't be easy to fix.

Resolving patch:
diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py
--- a/telegram/ext/conversationhandler.py
+++ b/telegram/ext/conversationhandler.py
@@ -178,14 +178,14 @@ def __init__(self,
         if persistent and not self.name:
             raise ValueError("Conversations can't be persistent when handler is unnamed.")
         self.persistent = persistent
-        self.persistence = None
+        self._persistence = None
         """:obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
         Set by dispatcher"""
         self.map_to_parent = map_to_parent
 
         self.timeout_jobs = dict()
         self._timeout_jobs_lock = Lock()
-        self.conversations = dict()
+        self._conversations = dict()
         self._conversations_lock = Lock()
 
         self.logger = logging.getLogger(__name__)
@@ -225,6 +225,32 @@ def __init__(self,
                                   "since inline queries have no chat context.")
                     break
 
+    @property
+    def persistence(self):
+        return self._persistence
+
+    @persistence.setter
+    def persistence(self, persistence):
+        self._persistence = persistence
+        # Set persistence for nested conversations
+        for handlers in self.states.values():
+            for handler in handlers:
+                if isinstance(handler, ConversationHandler):
+                    handler.persistence = self.persistence
+
+    @property
+    def conversations(self):
+        return self._conversations
+
+    @conversations.setter
+    def conversations(self, value):
+        self._conversations = value
+        # Set conversations for nested conversations
+        for handlers in self.states.values():
+            for handler in handlers:
+                if isinstance(handler, ConversationHandler):
+                    handler.conversations = self.persistence.get_conversations(handler.name)
+
     def _get_key(self, update):
         chat = update.effective_chat
         user = update.effective_user
diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py
--- a/telegram/ext/dispatcher.py
+++ b/telegram/ext/dispatcher.py
@@ -430,8 +430,8 @@ def add_handler(self, handler, group=DEFAULT_GROUP):
                 raise ValueError(
                     "Conversationhandler {} can not be persistent if dispatcher has no "
                     "persistence".format(handler.name))
-            handler.conversations = self.persistence.get_conversations(handler.name)
             handler.persistence = self.persistence
+            handler.conversations = self.persistence.get_conversations(handler.name)
 
         if group not in self.handlers:
             self.handlers[group] = list()


Test patch:
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -54,7 +54,8 @@ def user_data():
 @pytest.fixture(scope='function')
 def conversations():
     return {'name1': {(123, 123): 3, (456, 654): 4},
-            'name2': {(123, 321): 1, (890, 890): 2}}
+            'name2': {(123, 321): 1, (890, 890): 2},
+            'name3': {(123, 321): 1, (890, 890): 2}}
 
 
 @pytest.fixture(scope="function")
@@ -648,6 +649,56 @@ def next2(update, context):
         assert ch.conversations[ch._get_key(update)] == 0
         assert ch.conversations == pickle_persistence.conversations['name2']
 
+    def test_with_nested_conversationHandler(self, dp, update, good_pickle_files,
+                                             pickle_persistence):
+        dp.persistence = pickle_persistence
+        dp.use_context = True
+        NEXT2, NEXT3 = range(1, 3)
+
+        def start(update, context):
+            return NEXT2
+
+        start = CommandHandler('start', start)
+
+        def next(update, context):
+            return NEXT2
+
+        next = MessageHandler(None, next)
+
+        def next2(update, context):
+            return ConversationHandler.END
+
+        next2 = MessageHandler(None, next2)
+
+        nested_ch = ConversationHandler(
+            [next],
+            {NEXT2: [next2]},
+            [],
+            name='name3',
+            persistent=True,
+            map_to_parent={ConversationHandler.END: ConversationHandler.END},
+        )
+
+        ch = ConversationHandler([start], {NEXT2: [nested_ch], NEXT3: []}, [], name='name2',
+                                 persistent=True)
+        dp.add_handler(ch)
+        assert ch.conversations[ch._get_key(update)] == 1
+        assert nested_ch.conversations[nested_ch._get_key(update)] == 1
+        dp.process_update(update)
+        assert ch._get_key(update) not in ch.conversations
+        assert nested_ch._get_key(update) not in nested_ch.conversations
+        update.message.text = '/start'
+        update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)]
+        dp.process_update(update)
+        assert ch.conversations[ch._get_key(update)] == 1
+        assert ch.conversations == pickle_persistence.conversations['name2']
+        assert nested_ch._get_key(update) not in nested_ch.conversations
+        dp.process_update(update)
+        assert ch.conversations[ch._get_key(update)] == 1
+        assert ch.conversations == pickle_persistence.conversations['name2']
+        assert nested_ch.conversations[nested_ch._get_key(update)] == 1
+        assert nested_ch.conversations == pickle_persistence.conversations['name3']
+
     @classmethod
     def teardown_class(cls):
         try:
@@ -672,6 +723,7 @@ def chat_data_json(chat_data):
 @pytest.fixture(scope='function')
 def conversations_json(conversations):
     return """{"name1": {"[123, 123]": 3, "[456, 654]": 4}, "name2":
+              {"[123, 321]": 1, "[890, 890]": 2}, "name3":
               {"[123, 321]": 1, "[890, 890]": 2}}"""
 
 
@@ -770,8 +822,8 @@ def test_json_changes(self, user_data, user_data_json, chat_data, chat_data_json
         assert dict_persistence.chat_data_json == json.dumps(chat_data_two)
 
         conversations_two = conversations.copy()
-        conversations_two.update({'name3': {(1, 2): 3}})
-        dict_persistence.update_conversation('name3', (1, 2), 3)
+        conversations_two.update({'name4': {(1, 2): 3}})
+        dict_persistence.update_conversation('name4', (1, 2), 3)
         assert dict_persistence.conversations == conversations_two
         assert dict_persistence.conversations_json != conversations_json
         assert dict_persistence.conversations_json == encode_conversations_to_json(
@@ -845,3 +897,53 @@ def next2(update, context):
         dp.process_update(update)
         assert ch.conversations[ch._get_key(update)] == 0
         assert ch.conversations == dict_persistence.conversations['name2']
+
+    def test_with_nested_conversationHandler(self, dp, update, conversations_json):
+        dict_persistence = DictPersistence(conversations_json=conversations_json)
+        dp.persistence = dict_persistence
+        dp.use_context = True
+        NEXT2, NEXT3 = range(1, 3)
+
+        def start(update, context):
+            return NEXT2
+
+        start = CommandHandler('start', start)
+
+        def next(update, context):
+            return NEXT2
+
+        next = MessageHandler(None, next)
+
+        def next2(update, context):
+            return ConversationHandler.END
+
+        next2 = MessageHandler(None, next2)
+
+        nested_ch = ConversationHandler(
+            [next],
+            {NEXT2: [next2]},
+            [],
+            name='name3',
+            persistent=True,
+            map_to_parent={ConversationHandler.END: ConversationHandler.END},
+        )
+
+        ch = ConversationHandler([start], {NEXT2: [nested_ch], NEXT3: []}, [], name='name2',
+                                 persistent=True)
+        dp.add_handler(ch)
+        assert ch.conversations[ch._get_key(update)] == 1
+        assert nested_ch.conversations[nested_ch._get_key(update)] == 1
+        dp.process_update(update)
+        assert ch._get_key(update) not in ch.conversations
+        assert nested_ch._get_key(update) not in nested_ch.conversations
+        update.message.text = '/start'
+        update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)]
+        dp.process_update(update)
+        assert ch.conversations[ch._get_key(update)] == 1
+        assert ch.conversations == dict_persistence.conversations['name2']
+        assert nested_ch._get_key(update) not in nested_ch.conversations
+        dp.process_update(update)
+        assert ch.conversations[ch._get_key(update)] == 1
+        assert ch.conversations == dict_persistence.conversations['name2']
+        assert nested_ch.conversations[nested_ch._get_key(update)] == 1
+        assert nested_ch.conversations == dict_persistence.conversations['name3']