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

Issue:
Dispatcher force updating persistence object's chat data attribute
<!--
Thanks for reporting issues of python-telegram-bot!

Use this template to notify us if you found a bug, or if you want to request a new feature.
If you're looking for help with programming your bot using our library, feel free to ask your
questions in out telegram group at: https://t.me/pythontelegrambotgroup

To make it easier for us to help you please enter detailed information below.

Please note, we only support the latest version of python-telegram-bot and
master branch. Please make sure to upgrade & recreate the issue on the latest
version prior to opening an issue.
-->
### Steps to reproduce
1. Use the echobot2 example code with a few changes list below.

2. In main function create a DictPersistence object with store_chat_data set to False.

2. Create the Updater object with persistence set to the object created in the second step.

4. Send SIGKILL to the process in which the example code is launched.
### Expected behaviour
The code should normally terminate.

### Actual behaviour
The code terminates with the following error:
File ".../echobot2.py", line 81, in main
    updater.idle()
  File ".../.venv/lib/python3.7/site-packages/telegram/ext/updater.py", line 529, in idle
    sleep(1)
  File ".../.venv/lib/python3.7/site-packages/telegram/ext/updater.py", line 504, in signal_handler
    self.dispatcher.update_persistence()
  File ".../.venv/lib/python3.7/site-packages/telegram/ext/dispatcher.py", line 437, in update_persistence
    self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
  File ".../.venv/lib/python3.7/site-packages/telegram/ext/dictpersistence.py", line 193, in update_chat_data
    if self._chat_data.get(chat_id) == data:
AttributeError: 'NoneType' object has no attribute 'get'

Tracing back the error, it seems that when receiving the SIGKILL signal, the dispatcher is force updating the persistence object's chat data attribute without checking its store_chat_data attribute, see the following code from telegram.ext.dispatcher.py:

 def update_persistence(self):
        """Update :attr:`user_data` and :attr:`chat_data` in :attr:`persistence`.
        """
        if self.persistence:
            for chat_id in self.chat_data:
                self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
            for user_id in self.user_data:
                self.persistence.update_user_data(user_id, self.user_data[user_id])

### Configuration
**Operating System:**
Manjaro 18.0.4 Illyria

**Version of Python, python-telegram-bot & dependencies:**
python-telegram-bot 12.0.0b1
certifi 2019.06.16
future 0.17.1
Python 3.7.3 (default, Jun 24 2019, 04:54:02)  [GCC 9.1.0]


Discussion:
It seems that I overlooked this scenario in #1312. I will try and build a fix :)
@Maintainers: Feel free to assign me

Resolving patch:
diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py
--- a/telegram/ext/dispatcher.py
+++ b/telegram/ext/dispatcher.py
@@ -433,10 +433,12 @@ def update_persistence(self):
         """Update :attr:`user_data` and :attr:`chat_data` in :attr:`persistence`.
         """
         if self.persistence:
-            for chat_id in self.chat_data:
-                self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
-            for user_id in self.user_data:
-                self.persistence.update_user_data(user_id, self.user_data[user_id])
+            if self.persistence.store_chat_data:
+                for chat_id in self.chat_data:
+                    self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
+            if self.persistence.store_user_data:
+                for user_id in self.user_data:
+                    self.persistence.update_user_data(user_id, self.user_data[user_id])
 
     def add_error_handler(self, callback):
         """Registers an error handler in the Dispatcher.


Test patch:
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -228,6 +228,24 @@ def pickle_persistence():
                              on_flush=False)
 
 
+@pytest.fixture(scope='function')
+def pickle_persistence_only_chat():
+    return PicklePersistence(filename='pickletest',
+                             store_user_data=False,
+                             store_chat_data=True,
+                             singe_file=False,
+                             on_flush=False)
+
+
+@pytest.fixture(scope='function')
+def pickle_persistence_only_user():
+    return PicklePersistence(filename='pickletest',
+                             store_user_data=True,
+                             store_chat_data=False,
+                             singe_file=False,
+                             on_flush=False)
+
+
 @pytest.fixture(scope='function')
 def bad_pickle_files():
     for name in ['pickletest_user_data', 'pickletest_chat_data', 'pickletest_conversations',
@@ -541,7 +559,7 @@ def second(update, context):
         dp.add_handler(h2)
         dp.process_update(update)
 
-    def test_flush_on_stop(self, bot, update, pickle_persistence, good_pickle_files):
+    def test_flush_on_stop(self, bot, update, pickle_persistence):
         u = Updater(bot=bot, persistence=pickle_persistence)
         dp = u.dispatcher
         u.running = True
@@ -559,6 +577,45 @@ def test_flush_on_stop(self, bot, update, pickle_persistence, good_pickle_files)
         assert pickle_persistence_2.get_user_data()[4242424242]['my_test'] == 'Working!'
         assert pickle_persistence_2.get_chat_data()[-4242424242]['my_test2'] == 'Working2!'
 
+    def test_flush_on_stop_only_chat(self, bot, update, pickle_persistence_only_chat):
+        os.remove('pickletest_user_data')
+        os.remove('pickletest_chat_data')
+        u = Updater(bot=bot, persistence=pickle_persistence_only_chat)
+        dp = u.dispatcher
+        u.running = True
+        dp.user_data[4242424242]['my_test'] = 'Working!'
+        dp.chat_data[-4242424242]['my_test2'] = 'Working2!'
+        u.signal_handler(signal.SIGINT, None)
+        del (dp)
+        del (u)
+        del (pickle_persistence_only_chat)
+        pickle_persistence_2 = PicklePersistence(filename='pickletest',
+                                                 store_user_data=False,
+                                                 store_chat_data=True,
+                                                 singe_file=False,
+                                                 on_flush=False)
+        assert pickle_persistence_2.get_user_data() == {}
+        assert pickle_persistence_2.get_chat_data()[-4242424242]['my_test2'] == 'Working2!'
+
+    def test_flush_on_stop_only_user(self, bot, update, pickle_persistence_only_user):
+        os.remove('pickletest_chat_data')
+        u = Updater(bot=bot, persistence=pickle_persistence_only_user)
+        dp = u.dispatcher
+        u.running = True
+        dp.user_data[4242424242]['my_test'] = 'Working!'
+        dp.chat_data[-4242424242]['my_test2'] = 'Working2!'
+        u.signal_handler(signal.SIGINT, None)
+        del (dp)
+        del (u)
+        del (pickle_persistence_only_user)
+        pickle_persistence_2 = PicklePersistence(filename='pickletest',
+                                                 store_user_data=True,
+                                                 store_chat_data=False,
+                                                 singe_file=False,
+                                                 on_flush=False)
+        assert pickle_persistence_2.get_user_data()[4242424242]['my_test'] == 'Working!'
+        assert pickle_persistence_2.get_chat_data()[-4242424242] == {}
+
     def test_with_conversationHandler(self, dp, update, good_pickle_files, pickle_persistence):
         dp.persistence = pickle_persistence
         dp.use_context = True