diff --git a/.github/workflows/unit-testing.yml b/.github/workflows/unit-testing.yml index 41669b1..aa0d1ac 100644 --- a/.github/workflows/unit-testing.yml +++ b/.github/workflows/unit-testing.yml @@ -27,4 +27,4 @@ jobs: sudo -v && wget -nv -O- https://download.calibre-ebook.com/linux-installer.sh | sudo sh /dev/stdin - name: Test with calibre-debug run: | - calibre-customize -b .; calibre-debug test.py + export CALIBRE_OVERRIDE_LANG=en; calibre-customize -b .; calibre-debug test.py diff --git a/advanced.py b/advanced.py index e5d2049..0e05a8f 100644 --- a/advanced.py +++ b/advanced.py @@ -725,7 +725,6 @@ def start_batch_translation(): translator = get_translator(self.current_engine) translator.set_source_lang(self.ebook.source_lang) translator.set_target_lang(self.ebook.target_lang) - translator.disable_stream() batch_translator = ChatgptBatchTranslate(translator) batch = ChatgptBatchTranslationManager( batch_translator, self.cache, self.table, self) diff --git a/components/chatgpt.py b/components/chatgpt.py index fa47897..e7d9379 100644 --- a/components/chatgpt.py +++ b/components/chatgpt.py @@ -4,6 +4,8 @@ from ..lib.utils import traceback_error +from .alert import AlertMessage + try: from qt.core import ( @@ -17,6 +19,7 @@ QThread, QHBoxLayout, QPlainTextEdit, QEvent) load_translations() + log = Log() @@ -25,7 +28,7 @@ def request(func): def wrapper(self): try: func(self) - except Exception as e: + except Exception: self.show_information.emit( 'Oops, an error occurred!', traceback_error()) self.stack_index.emit(3) @@ -105,8 +108,11 @@ def check_details(self): def cancel_batch(self): self.process_tip.emit(_('canceling...')) self.stack_index.emit(1) - self._batch_translator.cancel(self._batch_id) - self._batch_translator.delete(self._file_id) + self._batch_info = self._batch_translator.check(self._batch_id) + if self._batch_info.get('status') not in ( + 'cancelling', 'cancelled', 'completed', 'failed'): + self._batch_translator.cancel(self._batch_id) + self._batch_translator.delete(self._file_id) self.remove_batch.emit() self.finished.emit() @@ -142,6 +148,8 @@ def __init__(self, translator, cache, table, parent=None): self.cache = cache self.table = table + self.alert = AlertMessage(self) + self.batch_worker = ChatgptBatchTranslationWorker(translator) self.batch_worker.moveToThread(self.batch_thread) self.batch_thread.finished.connect(self.batch_worker.deleteLater) @@ -267,31 +275,40 @@ def layout_buttons(self): self.batch_worker.enable_apply_button.connect(apply.setEnabled) refresh.clicked.connect(self.batch_worker.check.emit) - cancel.clicked.connect(self.batch_worker.cancel.emit) apply.clicked.connect(lambda: self.batch_worker.apply.emit()) + def cancel_batch_translation(): + action = self.alert.ask( + _('Are you sure you want to cancel the batch translation?')) + if action == 'yes': + self.batch_worker.cancel.emit() + cancel.clicked.connect(cancel_batch_translation) + return widget def layout_data(self): status = QLabel() - total = QLabel() - completed = QLabel() - failed = QLabel() + detail = QPlainTextEdit() + detail.setReadOnly(True) def set_details_data(data): - status.setText(str(data.get('status') or 'n/a')) - request_counts = data.get('request_counts') - total.setText(str(request_counts.get('total') or 'n/a')) - completed.setText(str(request_counts.get('completed') or 'n/a')) - failed.setText(str(request_counts.get('failed') or 'n/a')) + detail.clear() + batch_status = data.get('status') + status.setText(str(batch_status)) + if batch_status == 'completed': + request_counts = data.get('request_counts') + detail.appendPlainText(str(request_counts)) + else: + error_info = data.get('errors') + detail.appendPlainText(str(error_info)) self.batch_worker.trans_details.connect(set_details_data) widget = QGroupBox(_('Batch translation details')) layout = QFormLayout(widget) layout.addRow(_('Status'), status) - layout.addRow(_('Total'), total) - layout.addRow(_('Completed'), completed) - layout.addRow(_('Failed'), failed) + layout.addRow(_('Detail'), detail) + + self.set_form_layout_policy(layout) return widget @@ -318,3 +335,10 @@ def changeEvent(self, event): def done(self, reason): QDialog.done(self, reason) self.parent().raise_() + + def set_form_layout_policy(self, layout): + field_policy = getattr( + QFormLayout.FieldGrowthPolicy, 'AllNonFixedFieldsGrow', None) \ + or QFormLayout.AllNonFixedFieldsGrow + layout.setFieldGrowthPolicy(field_policy) + layout.setLabelAlignment(Qt.AlignRight) diff --git a/engines/base.py b/engines/base.py index c83022e..95b30de 100644 --- a/engines/base.py +++ b/engines/base.py @@ -91,9 +91,6 @@ def api_key_error_message(cls): return _('A correct key format "{}" is required.') \ .format(cls.api_key_hint) - def disable_stream(self): - self.stream = False - def get_api_key(self): if self.need_api_key and self.api_keys: return self.api_keys.pop(0) diff --git a/engines/openai.py b/engines/openai.py index 3ede8e1..f69be5b 100644 --- a/engines/openai.py +++ b/engines/openai.py @@ -153,12 +153,12 @@ class ChatgptBatchTranslate: def __init__(self, translator): self.translator = translator - self.translator.stream = True + self.translator.stream = False domain_name = '://'.join( urlsplit(self.translator.endpoint, 'https')[:2]) self.file_endpoint = '%s/v1/files' % domain_name - self.batch_endpont = '%s/v1/batches' % domain_name + self.batch_endpoint = '%s/v1/batches' % domain_name def _create_multipart_form_data(self, body): """https://www.rfc-editor.org/rfc/rfc2046#section-5.1""" @@ -195,12 +195,12 @@ def upload(self, paragraphs): .format(self.translator.model)) body = io.StringIO() for paragraph in paragraphs: + data = self.translator.get_body(paragraph.original) body.write(json.dumps({ "custom_id": paragraph.md5, "method": "POST", "url": "/v1/chat/completions", - "body": self.translator.get_body(paragraph.original) - })) + "body": json.loads(data)})) if paragraph != paragraphs[-1]: body.write('\n') content_type = 'multipart/form-data; boundary="%s"' % self.boundary @@ -231,8 +231,8 @@ def retrieve(self, output_file_id): headers = self.translator.get_headers() del headers['Content-Type'] response = request( - '%s/%s/content' % (self.batch_endpont, output_file_id), - headers=headers) + '%s/%s/content' % (self.file_endpoint, output_file_id), + headers=headers, as_bytes=True) translations = {} for line in io.BytesIO(response): @@ -251,14 +251,28 @@ def create(self, file_id): body = json.dumps({ 'input_file_id': file_id, 'endpoint': '/v1/chat/completions', - 'completion_window': '24h', - }) - response = request(self.batch_endpont, body, headers, 'POST') + 'completion_window': '24h'}) + response = request(self.batch_endpoint, body, headers, 'POST') return json.loads(response).get('id') def check(self, batch_id): # time.sleep(2) # return { + # 'status': 'failed', + # 'output_file_id': 'xxxx', + # 'errors': { + # 'object': 'list', + # 'data': [ + # { + # 'code': 'error-code', + # 'message': 'error-message', + # 'param': 'error-param', + # 'line': 'error-line', + # } + # ] + # }, + # } + # return { # 'status': 'completed', # 'output_file_id': 'xxxx', # 'request_counts': { @@ -269,7 +283,7 @@ def check(self, batch_id): # } response = request( - '%s/%s' % (self.batch_endpont, batch_id), + '%s/%s' % (self.batch_endpoint, batch_id), headers=self.translator.get_headers()) return json.loads(response) @@ -279,6 +293,7 @@ def cancel(self, batch_id): headers = self.translator.get_headers() response = request( - '%s/%s/cancel' % (self.batch_endpont, batch_id), + '%s/%s/cancel' % (self.batch_endpoint, batch_id), headers=headers, method='POST') - return json.loads(response).get('status') == 'cancelling' + return json.loads(response).get('status') in ( + 'cancelling', 'cancelled') diff --git a/lib/utils.py b/lib/utils.py index c567066..45511b9 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -145,7 +145,7 @@ def traceback_error(): def request( url, data=None, headers={}, method='GET', timeout=30, proxy_uri=None, - stream=False): + as_bytes=False, stream=False): br = Browser() br.set_handle_robots(False) # Do not verify SSL certificates @@ -171,4 +171,6 @@ def request( response = br.response() if stream: return response + if as_bytes: + return response.read() return response.read().decode('utf-8').strip() diff --git a/tests/test_convertion.py b/tests/test_convertion.py index b113c85..7f86241 100644 --- a/tests/test_convertion.py +++ b/tests/test_convertion.py @@ -6,8 +6,6 @@ from ..lib.ebook import Ebooks -load_translations() - module_name = 'calibre_plugins.ebook_translator.lib.conversion' @@ -37,7 +35,7 @@ def test_translate_done_job_failed_not_debug(self): with patch(module_name + '.DEBUG', False): self.worker.translate_done(self.job) self.gui.job_exception.assert_called_once_with( - self.job, dialog_title=_('Translation job failed')) + self.job, dialog_title='Translation job failed') @patch(module_name + '.os') @patch(module_name + '.open') @@ -95,17 +93,16 @@ def test_translate_done_ebook_to_library( self.worker.api.format_abspath.assert_called_once_with(89, 'epub') self.worker.gui.status_bar.show_message.assert_called_once_with( - 'test description ' + _('completed'), 5000) + 'test description completed', 5000) arguments = self.worker.gui.proceed_question.mock_calls[0].args self.assertIsInstance(arguments[0], Callable) self.assertIs(self.worker.gui.job_manager.launch_gui_app, arguments[1]) self.assertEqual('/path/to/log', arguments[2]) - self.assertEqual(_('Ebook Translation Log'), arguments[3]) - self.assertEqual(_('Translation Completed'), arguments[4]) - self.assertEqual(_( - 'The translation of "{}" was completed. ' - 'Do you want to open the book?') - .format('test custom title [German]'), + self.assertEqual('Ebook Translation Log', arguments[3]) + self.assertEqual('Translation Completed', arguments[4]) + self.assertEqual( + 'The translation of "test custom title [German]" was completed. ' + 'Do you want to open the book?', arguments[5]) mock_payload = Mock() @@ -161,17 +158,16 @@ def test_translate_done_ebook_to_path( '/path/to/test.epub', '/path/to/test_ custom title_ [German].epub') self.worker.gui.status_bar.show_message.assert_called_once_with( - 'test description ' + _('completed'), 5000) + 'test description ' + 'completed', 5000) arguments = self.worker.gui.proceed_question.mock_calls[0].args self.assertIsInstance(arguments[0], Callable) self.assertIs(self.worker.gui.job_manager.launch_gui_app, arguments[1]) self.assertEqual('/path/to/log', arguments[2]) - self.assertEqual(_('Ebook Translation Log'), arguments[3]) - self.assertEqual(_('Translation Completed'), arguments[4]) - self.assertEqual(_( - 'The translation of "{}" was completed. ' - 'Do you want to open the book?') - .format('test: custom title* [German]'), + self.assertEqual('Ebook Translation Log', arguments[3]) + self.assertEqual('Translation Completed', arguments[4]) + self.assertEqual( + 'The translation of "test: custom title* [German]" was completed. ' + 'Do you want to open the book?', arguments[5]) mock_payload = Mock() @@ -225,19 +221,18 @@ def test_translate_done_other_to_library( .assert_called_once_with(1) self.worker.api.format_abspath.assert_called_once_with(90, 'srt') self.worker.gui.status_bar.show_message.assert_called_once_with( - 'test description ' + _('completed'), 5000) + 'test description ' + 'completed', 5000) self.assertEqual('test custom title [German]', metadata.title) arguments = self.worker.gui.proceed_question.mock_calls[0].args self.assertIsInstance(arguments[0], Callable) self.assertIs(self.worker.gui.job_manager.launch_gui_app, arguments[1]) self.assertEqual('C:\\path\\to\\log', arguments[2]) - self.assertEqual(_('Ebook Translation Log'), arguments[3]) - self.assertEqual(_('Translation Completed'), arguments[4]) - self.assertEqual(_( - 'The translation of "{}" was completed. ' - 'Do you want to open the book?') - .format('test custom title [German]'), + self.assertEqual('Ebook Translation Log', arguments[3]) + self.assertEqual('Translation Completed', arguments[4]) + self.assertEqual( + 'The translation of "test custom title [German]" was completed. ' + 'Do you want to open the book?', arguments[5]) mock_payload = Mock() @@ -281,17 +276,16 @@ def test_translate_done_other_to_path( '/path/to/test.srt', '/path/to/test_ custom title_ [German].srt') self.worker.gui.status_bar.show_message.assert_called_once_with( - 'test description ' + _('completed'), 5000) + 'test description ' + 'completed', 5000) arguments = self.worker.gui.proceed_question.mock_calls[0].args self.assertIsInstance(arguments[0], Callable) self.assertIs(self.worker.gui.job_manager.launch_gui_app, arguments[1]) self.assertEqual('/path/to/log', arguments[2]) - self.assertEqual(_('Ebook Translation Log'), arguments[3]) - self.assertEqual(_('Translation Completed'), arguments[4]) - self.assertEqual(_( - 'The translation of "{}" was completed. ' - 'Do you want to open the book?') - .format('test: custom title* [German]'), + self.assertEqual('Ebook Translation Log', arguments[3]) + self.assertEqual('Translation Completed', arguments[4]) + self.assertEqual( + 'The translation of "test: custom title* [German]" was completed. ' + 'Do you want to open the book?', arguments[5]) mock_payload = Mock() diff --git a/tests/test_engine.py b/tests/test_engine.py index 923f695..6896d33 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -19,8 +19,6 @@ create_engine_template, load_engine_data, CustomTranslate) -load_translations() - module_name = 'calibre_plugins.ebook_translator.engines' @@ -50,7 +48,7 @@ def test_class(self): self.assertEqual('POST', Base.method) self.assertFalse(Base.stream) self.assertTrue(Base.need_api_key) - self.assertEqual(_('API Keys'), Base.api_key_hint) + self.assertEqual('API Keys', Base.api_key_hint) self.assertEqual(r'^[^\s]+$', Base.api_key_pattern) self.assertEqual(['401'], Base.api_key_errors) self.assertEqual('\n\n', Base.separator) @@ -352,7 +350,7 @@ def test_get_usage(self, mock_request): '{"character_count": 30, "character_limit": 100}' self.assertEqual( - _('{} total, {} used, {} left').format(100, 30, 70), + '100 total, 30 used, 70 left', self.translator.get_usage(),) mock_request.return_value = '' @@ -367,8 +365,9 @@ def test_translate(self, mock_request): mock_request.return_value = '' error = re.compile( - _('Can not parse returned response. Raw data: {}') - .format('\n\nTraceback.*\n\n'), re.S) + 'Can not parse returned response. Raw data: ' + '\n\nTraceback.*\n\n', + re.S) with self.assertRaisesRegex(Exception, error): self.translator.translate('Hello World!') @@ -403,7 +402,7 @@ def test_get_body(self): 'temperature': 1.0})) def test_get_body_without_stream(self): - self.translator.disable_stream() + self.translator.stream = False self.assertEqual( self.translator.get_body('test content'), json.dumps({ @@ -470,15 +469,14 @@ def test_translate_normal(self, mock_request): class TestChatgptBatchTranslate(unittest.TestCase): def setUp(self): - self.translator = Mock(ChatgptTranslate) - self.translator.endpoint = 'https://api.openai.com/test' + self.mock_translator = Mock(ChatgptTranslate) + self.mock_translator.endpoint = 'https://api.openai.com/test' self.mock_headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer abc', - 'User-Agent': 'Ebook-Translator/v1.0.0' - } - self.translator.get_headers.return_value = self.mock_headers - self.batch_translator = ChatgptBatchTranslate(self.translator) + 'User-Agent': 'Ebook-Translator/v1.0.0'} + self.mock_translator.get_headers.return_value = self.mock_headers + self.batch_translator = ChatgptBatchTranslate(self.mock_translator) def test_class_object(self): self.assertEqual(ChatgptBatchTranslate.supported_models, [ @@ -505,16 +503,18 @@ def test_class_object(self): def test_created_translator(self): self.assertIsInstance(self.batch_translator, ChatgptBatchTranslate) + self.assertIs(self.mock_translator, self.batch_translator.translator) + self.assertFalse(self.mock_translator.stream) self.assertEqual( self.batch_translator.file_endpoint, 'https://api.openai.com/v1/files') self.assertEqual( - self.batch_translator.batch_endpont, + self.batch_translator.batch_endpoint, 'https://api.openai.com/v1/batches') def test_upload_with_unsupported_model(self): - self.translator.model = 'fake-model' - self.translator.stream = True + self.mock_translator.model = 'fake-model' + self.mock_translator.stream = True with self.assertRaises(UnsupportedModel) as cm: self.batch_translator.upload([Mock(Paragraph)]) self.assertEqual( @@ -540,19 +540,17 @@ def test_upload(self, mock_request): mock_paragraph_2 = Mock(Paragraph) mock_paragraph_2.md5 = 'def' mock_paragraph_2.original = 'test content 2' - self.translator.model = 'gpt-4o' - self.translator.api_key = 'abc' + self.mock_translator.model = 'gpt-4o' + self.mock_translator.api_key = 'abc' def mock_get_body(text): - return { + return json.dumps({ 'model': 'gpt-3.5-turbo', 'messages': [ {'role': 'system', 'content': 'some prompt...'}, - {'role': 'user', 'content': text} - ], - 'temperature': 1.0 - } - self.translator.get_body.side_effect = mock_get_body + {'role': 'user', 'content': text}], + 'temperature': 1.0}) + self.mock_translator.get_body.side_effect = mock_get_body file_id = self.batch_translator.upload( [mock_paragraph_1, mock_paragraph_2]) @@ -580,7 +578,8 @@ def mock_get_body(text): '"content": "test content 2"}], "temperature": 1.0}}\r\n' '--xxxxxxxxxx--').encode() mock_request.assert_called_once_with( - 'https://api.openai.com/v1/files', mock_body, self.mock_headers, 'POST') + 'https://api.openai.com/v1/files', mock_body, self.mock_headers, + 'POST') @patch(module_name + '.openai.request') def test_delete(self, mock_request): @@ -607,7 +606,7 @@ def test_retrieve(self, mock_request): b'{"custom_id":"def","response":{"status_code":200,"body":{' b'"choices": [{"message": {"content": "B"}}]}}}') mock_request.return_value = line_1 + b'\n' + line_2 - self.translator.get_headers.return_value = { + self.mock_translator.get_headers.return_value = { 'Content-Type': 'application/json', 'Authorization': 'Bearer abc', 'User-Agent': 'Ebook-Translator/v1.0.0'} @@ -620,8 +619,8 @@ def test_retrieve(self, mock_request): 'Authorization': 'Bearer abc', 'User-Agent': 'Ebook-Translator/v1.0.0'} mock_request.assert_called_once_with( - 'https://api.openai.com/v1/batches/test-batch-id/content', - headers=headers) + 'https://api.openai.com/v1/files/test-batch-id/content', + headers=headers, as_bytes=True) @patch(module_name + '.openai.request') def test_create(self, mock_request): @@ -952,56 +951,51 @@ def test_create_engine_template(self): def test_load_engine_data(self): self.assertEqual( - (False, _('Engine data must be in valid JSON format.')), + (False, 'Engine data must be in valid JSON format.'), load_engine_data('')) self.assertEqual( - (False, _('Invalid engine data.')), - load_engine_data('""')) + (False, 'Invalid engine data.'), load_engine_data('""')) self.assertEqual( - (False, _('Engine name is required.')), - load_engine_data('{}')) + (False, 'Engine name is required.'), load_engine_data('{}')) self.assertEqual( - (False, _( - 'Engine name must be different from builtin engine name.')), + (False, 'Engine name must be different from builtin engine name.'), load_engine_data('{"name":"Google(Free)"}')) self.assertEqual( - (False, _('Language codes are required.')), + (False, 'Language codes are required.'), load_engine_data('{"name":"Test"}')) self.assertEqual( - (False, _('Language codes are required.')), + (False, 'Language codes are required.'), load_engine_data('{"name":"Test","langiages":{}}')) self.assertEqual( - (False, _('Source and target must be added in pair.')), + (False, 'Source and target must be added in pair.'), load_engine_data('{"name":"Test","languages":{"source":{}}}')) self.assertEqual( - (False, _('Source and target must be added in pair.')), + (False, 'Source and target must be added in pair.'), load_engine_data('{"name":"Test","languages":{"target":{}}}')) self.assertEqual( - (False, _('Request information is required.')), + (False, 'Request information is required.'), load_engine_data('{"name":"Test","languages":{"English":"EN"}}')) self.assertEqual( - (False, _('API URL is required.')), - load_engine_data( + (False, 'API URL is required.'), load_engine_data( '{"name":"Test","languages":{"English":"EN"},' '"request":{"test":null}}')) self.assertEqual( - (False, _('Placeholder is required.')), - load_engine_data( + (False, 'Placeholder is required.'), load_engine_data( '{"name":"Test","languages":{"English":"EN"},' '"request":{"url":"https://test.api","data":{}}}')) self.assertEqual( - (False, _('Request headers must be an JSON object.')), + (False, 'Request headers must be an JSON object.'), load_engine_data( '{"name":"Test","languages":{"English":"EN"},' '"request":{"url":"https://test.api","data":"",' '"headers":"abc"}}')) self.assertEqual( - (False, _('A appropriate Content-Type in headers is required.')), + (False, 'A appropriate Content-Type in headers is required.'), load_engine_data( '{"name":"Test","languages":{"English":"EN"},' '"request":{"url":"https://test.api","data":""}}')) self.assertEqual( - (False, _('Expression to parse response is required.')), + (False, 'Expression to parse response is required.'), load_engine_data( '{"name":"Test","languages":{"English":"EN"},' '"request":{"url":"https://test.api","data":"",' diff --git a/tests/test_translation.py b/tests/test_translation.py index ed9bd0d..f62dc05 100644 --- a/tests/test_translation.py +++ b/tests/test_translation.py @@ -8,9 +8,6 @@ from ..engines.deepl import DeeplTranslate -load_translations() - - class TestGlossary(unittest.TestCase): @patch('calibre_plugins.ebook_translator.lib.translation.open') def test_load_from_file(self, mock_open): @@ -230,7 +227,7 @@ def test_translate_paragraph_fresh(self): self.translator.get_target_lang.return_value = 'zh' self.translation.translate_paragraph(self.paragraph) - self.streaming.assert_has_calls([call(''), call(_('Translating...'))]) + self.streaming.assert_has_calls([call(''), call('Translating...')]) self.glossary.restore.assert_called_with('你好世界') self.assertEqual('你好呀世界', self.paragraph.translation) @@ -250,7 +247,7 @@ def test_translate_paragraph_streaming(self, mock_time): self.translation.translate_paragraph(self.paragraph) self.streaming.assert_has_calls([ - call(''), call(_('Translating...')), call(''), call('你'), + call(''), call('Translating...'), call(''), call('你'), call('好'), call('世'), call('界')]) mock_time.sleep.assert_called_with(0.05) @@ -263,7 +260,7 @@ def test_translate_paragraph_streaming(self, mock_time): self.translation.total = 2 self.translation.translate_paragraph(self.paragraph) - self.streaming.assert_has_calls([call(''), call(_('Translating...'))]) + self.streaming.assert_has_calls([call(''), call('Translating...')]) mock_time.sleep.assert_not_called() self.assertEqual('你好呀世界', self.paragraph.translation) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3d731c7..696b074 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -79,7 +79,8 @@ def test_open_file_with_open_error(self, mock_open, mock_codecs_open): @patch(module_name + '.ssl') @patch(module_name + '.Request') @patch(module_name + '.Browser') - def test_request(self, mock_browser, mock_request, mock_ssl): + def test_request_output_as_string( + self, mock_browser, mock_request, mock_ssl): browser = mock_browser() self.assertIs( @@ -98,6 +99,29 @@ def test_request(self, mock_browser, mock_request, mock_ssl): method='GET') browser.open.assert_called_once_with(mock_request()) + @patch(module_name + '.ssl') + @patch(module_name + '.Request') + @patch(module_name + '.Browser') + def test_request_output_as_bytes( + self, mock_browser, mock_request, mock_ssl): + browser = mock_browser() + + self.assertIs( + request('https://example.com/api', 'test data', as_bytes=True), + browser.response().read()) + + browser.set_handle_robots.assert_called_once_with(False) + mock_ssl._create_unverified_context.assert_called_once_with( + cert_reqs=mock_ssl.CERT_NONE) + browser.set_ca_data.assert_called_once_with( + context=mock_ssl._create_unverified_context()) + browser.set_proxies.assert_not_called() + + mock_request.assert_called_once_with( + 'https://example.com/api', 'test data', headers={}, timeout=30, + method='GET') + browser.open.assert_called_once_with(mock_request()) + @patch(module_name + '.ssl') @patch(module_name + '.Request') @patch(module_name + '.Browser')