Skip to content

Commit ac3ac1f

Browse files
authored
Merge pull request #6 from Voyz/fix/asyncio-loop-and-unicode-encode
asyncio Event loop policy, UnicodeEncodeError and planners' exception handling (fixes #3)
2 parents 1fada99 + eb5eab6 commit ac3ac1f

File tree

5 files changed

+104
-7
lines changed

5 files changed

+104
-7
lines changed

databay/config.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
import os
2+
import sys
3+
import asyncio
4+
5+
# Fix for #3 - Asyncio/aiohttp causes a 'RuntimeError: Event loop is closed' error on ProactorEventLoop, which became default for Python 3.8
6+
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
7+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
8+
9+
IGNORE_WARNINGS = []
10+
111
def initialise():
212
import logging
313

414
from databay.misc.logs import ISO8601Formatter
515

6-
716
iso8601_formatter = ISO8601Formatter('%(asctime)s|%(levelname)-.1s| %(message)s (%(name)s)', millis_precision=3)# / %(threadName)s)')
817
iso8601_formatter.set_pretty(True)
918

@@ -14,4 +23,12 @@ def initialise():
1423
default_logger = logging.getLogger('databay')
1524
default_logger.addHandler(stream_handler)
1625

17-
logging.getLogger('databay').setLevel(logging.WARNING)
26+
default_logger.setLevel(logging.WARNING)
27+
28+
global IGNORE_WARNINGS
29+
IGNORE_WARNINGS = os.environ.get('DATABAY_IGNORE_WARNINGS', '').split(';')
30+
31+
if sys.platform.startswith('win') and \
32+
(sys.stdin.encoding == 'windows-1252' or sys.stdout.encoding == 'windows-1252') and \
33+
'windows-1252' not in IGNORE_WARNINGS:
34+
default_logger.warning('stdin or stdout encoder is set to \'windows-1252\'. This may cause errors with data streaming. Fix by setting following environment variables: \n\nPYTHONIOENCODING=utf-8\nPYTHONLEGACYWINDOWSSTDIO=utf-8\n\nSet DATABAY_IGNORE_WARNINGS=\'windows-1252\' to ignore this warning.')

databay/link.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __repr__(self):
3434
:returns: "{name}.{index}"
3535
"""
3636
s = ''
37-
if self.name is not '': s += f'{self.name}.'
37+
if self.name != '': s += f'{self.name}.'
3838
s += f'{self.index}'
3939
return s
4040

databay/planners/aps_planner.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ def _on_exception(self, event):
8686
raise type(event.exception)(exception_message).with_traceback(traceback)
8787
except TypeError as type_exception:
8888
# Some custom exceptions won't let you use the common constructor and will throw an error on initialisation. We catch these and just throw a generic RuntimeError.
89-
if 'required positional argument' in str(type_exception):
90-
raise Exception(exception_message).with_traceback(traceback) from None
89+
raise Exception(exception_message).with_traceback(traceback) from None
9190
except Exception as e:
9291
_LOGGER.exception(e)
9392

databay/planners/schedule_planner.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,7 @@ def _start_planner(self):
169169
raise type(ex)(exception_message).with_traceback(traceback)
170170
except TypeError as type_exception:
171171
# Some custom exceptions won't let you use the common constructor and will throw an error on initialisation. We catch these and just throw a generic RuntimeError.
172-
if 'required positional argument' in str(type_exception):
173-
raise RuntimeError(exception_message).with_traceback(traceback) from None
172+
raise RuntimeError(exception_message).with_traceback(traceback) from None
174173
except Exception as e:
175174
# if self._catch_exceptions:
176175
_LOGGER.exception(e)

test/test/unit/test_config.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import logging
2+
import os
3+
from unittest import TestCase, mock
4+
5+
from asynctest import patch, MagicMock
6+
import asyncio
7+
from importlib import reload
8+
9+
from databay import config
10+
11+
12+
class TestConfig(TestCase):
13+
14+
def setUp(self):
15+
logging.shutdown()
16+
reload(logging)
17+
18+
@patch('sys.platform', 'win32')
19+
@patch('sys.version_info')
20+
def test_event_loop_policy_3_8(self, version_info):
21+
version_info.__getitem__.side_effect = lambda x: [3, 8][x]
22+
23+
# fake a 3.8 default setup
24+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
25+
26+
reload(config)
27+
28+
self.assertIsInstance(asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy, "Asyncio event loop policy should be WindowsSelectorEventLoopPolicy.")
29+
30+
@patch('sys.platform', 'win32')
31+
@patch('sys.version_info')
32+
def test_event_loop_policy_3_7(self, version_info):
33+
version_info.__getitem__.side_effect = lambda x: [3, 7][x]
34+
35+
# fake a manual 3.8 default setup
36+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
37+
38+
reload(config)
39+
40+
self.assertIsInstance(asyncio.get_event_loop_policy(), asyncio.WindowsProactorEventLoopPolicy, "Asyncio event loop policy should be WindowsProactorEventLoopPolicy.")
41+
42+
@patch('databay.config.sys.platform', 'win32')
43+
@patch('databay.config.sys.stdout', MagicMock(encoding='windows-1252'))
44+
@patch('databay.config.sys.stdin', MagicMock(encoding='windows-1252'))
45+
@patch('logging.StreamHandler.emit', lambda x, y: None) #disable stream handler
46+
def test_windows_1252(self):
47+
with self.assertLogs(logging.getLogger('databay'), level='WARNING') as cm:
48+
config.initialise()
49+
self.assertTrue('stdin or stdout encoder is set to \'windows-1252\'' in ';'.join(cm.output))
50+
51+
@patch('databay.config.sys.platform', 'win32')
52+
@patch('databay.config.sys.stdout', MagicMock(encoding='utf-8'))
53+
@patch('databay.config.sys.stdin', MagicMock(encoding='utf-8'))
54+
def test_not_windows_1252(self):
55+
databay_logger = logging.getLogger('databay')
56+
databay_logger.warning = MagicMock()
57+
config.initialise()
58+
self.assertTrue('stdin or stdout encoder is set to \'windows-1252\'' not in databay_logger.warning.call_args_list)
59+
60+
61+
@patch('databay.config.sys.platform', 'win32')
62+
@patch('databay.config.sys.stdout', MagicMock(encoding='windows-1252'))
63+
@patch('databay.config.sys.stdin', MagicMock(encoding='windows-1252'))
64+
@patch('logging.Logger.warning')
65+
@mock.patch.dict(os.environ, {"DATABAY_IGNORE_WARNINGS": "windows-1252"})
66+
def test_windows_1252_ignored(self, warning):
67+
config.initialise()
68+
string_args = ';'.join([str(call[0][0]) for call in warning.call_args_list])
69+
self.assertTrue('stdin or stdout encoder is set to \'windows-1252\'' not in string_args, 'Should not contain windows-1252 warning')
70+
71+
72+
@patch('databay.config.sys.platform', 'win32')
73+
@patch('databay.config.sys.stdout', MagicMock(encoding='windows-1252'))
74+
@patch('databay.config.sys.stdin', MagicMock(encoding='windows-1252'))
75+
@mock.patch.dict(os.environ, {"DATABAY_IGNORE_WARNINGS": "test_ignore"})
76+
@patch('logging.StreamHandler.emit', lambda x, y: None) #disable stream handler
77+
def test_windows_1252_incorrect_ignore(self):
78+
config.initialise()
79+
with self.assertLogs(logging.getLogger('databay'), level='WARNING') as cm:
80+
config.initialise()
81+
self.assertTrue('stdin or stdout encoder is set to \'windows-1252\'' in ';'.join(cm.output))
82+

0 commit comments

Comments
 (0)