-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
v1.0.0: unit testing and continuous integration
- Loading branch information
Showing
10 changed files
with
364 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# This workflows will upload a Python Package using Twine when a release is created | ||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries | ||
|
||
name: Upload Python Package | ||
|
||
on: | ||
release: | ||
types: [created] | ||
|
||
jobs: | ||
deploy: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Set up Python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.x' | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install setuptools wheel twine | ||
- name: Build and publish | ||
env: | ||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} | ||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} | ||
run: | | ||
python setup.py sdist bdist_wheel | ||
twine upload dist/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
os: linux | ||
dist: xenial | ||
language: python | ||
python: | ||
- "3.8" | ||
- "3.9" | ||
script: | ||
- python -m unittest discover -v |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/asyncode)](https://pypi.org/project/asyncode) | ||
[![PyPI - Wheel](https://img.shields.io/pypi/wheel/asyncode)](https://pypi.org/project/asyncode) | ||
[![Read the Docs](https://img.shields.io/readthedocs/asyncode)](https://asyncode.readthedocs.io) | ||
[![Travis CI](https://img.shields.io/travis/loic-simon/asyncode)](https://travis-ci.org/github/loic-simon/asyncode) | ||
|
||
Python package for emulating Python's interactive interpreter in asynchronous contexts. | ||
|
||
|
@@ -16,15 +17,15 @@ pip install asyncode | |
|
||
### Dependencies | ||
|
||
* Python 3.7+ | ||
* Python **≥ 3.5** *(no CI for Python < 3.8)* | ||
|
||
|
||
|
||
## Usage | ||
|
||
This package's external API consists in a two classes, **`AsyncInteractiveInterpreter`** and **AsyncInteractiveConsole**, which subclass respectively [`code.InteractiveInterpreter`](https://docs.python.org/3/library/code.html#interactive-interpreter-objects) and [`code.InteractiveInterpreter`](https://docs.python.org/3/library/code.html#interactive-console-objects). | ||
This package's external API consists in two classes, **`AsyncInteractiveInterpreter`** and **`AsyncInteractiveConsole`**, which subclass respectively [`code.InteractiveInterpreter`](https://docs.python.org/3/library/code.html#interactive-interpreter-objects) and [`code.InteractiveConsole`](https://docs.python.org/3/library/code.html#interactive-console-objects). | ||
|
||
These classes are meant to be used in running asynchronous environments. Minimal working code will need to subclass provided classes to implement appropriate functions: | ||
These classes are meant to be used in **already running asynchronous contexts**. Minimal useful code will need to subclass provided classes to implement specific functions: | ||
|
||
```py | ||
import asyncode | ||
|
@@ -33,11 +34,11 @@ class MyAsyncConsole(asyncode.AsyncInteractiveConsole): | |
"""AsyncInteractiveConsole adapted to running environment""" | ||
|
||
async def write(self, data): | ||
"""Use appropriate method""" | ||
"""Use specific function""" | ||
await some_output_coroutine(data) | ||
|
||
async def raw_input(self, prompt=""): | ||
"""Use appropriate method""" | ||
"""Use specific functions""" | ||
if prompt: | ||
await some_output_coroutine(prompt) | ||
|
||
|
@@ -51,7 +52,7 @@ async def run_interpreter(): | |
try: | ||
await console.interact() | ||
except SystemExit: | ||
# Do not exit the whole program! | ||
# Do not exit the whole program when sending "exit()" or "quit()" | ||
await some_output_coroutine("Bye!") | ||
``` | ||
|
||
|
@@ -66,6 +67,7 @@ Pull requests are welcome. Do not hesitate to get in touch with me (see below) f | |
|
||
|
||
## License | ||
|
||
This work is shared under [the MIT license](LICENSE). | ||
|
||
© 2020 Loïc Simon ([[email protected]](mailto:[email protected])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import sys | ||
import unittest | ||
from textwrap import dedent | ||
from contextlib import ExitStack | ||
from unittest import mock | ||
|
||
sys.path.append("..") | ||
import asyncode | ||
|
||
|
||
class TestAsyncodeLikeCode(unittest.IsolatedAsyncioTestCase): | ||
"""Do same tests as code standard module (but async)""" | ||
|
||
def setUp(self): | ||
self.console = asyncode.AsyncInteractiveConsole() | ||
self.mock_sys() | ||
|
||
def mock_sys(self): | ||
"Mock system environment for AsyncInteractiveConsole" | ||
# use exit stack to match patch context managers to addCleanup | ||
stack = ExitStack() | ||
self.addCleanup(stack.close) | ||
self.infunc = stack.enter_context(mock.patch('asyncode.input', | ||
create=True)) | ||
self.stdout = stack.enter_context(mock.patch('asyncode.sys.stdout')) | ||
self.stderr = stack.enter_context(mock.patch('asyncode.sys.stderr')) | ||
self.sysmod = stack.enter_context(mock.patch('asyncode.sys', | ||
wraps=asyncode.sys, | ||
spec=asyncode.sys)) | ||
if sys.excepthook is sys.__excepthook__: | ||
self.sysmod.excepthook = self.sysmod.__excepthook__ | ||
del self.sysmod.ps1 | ||
del self.sysmod.ps2 | ||
|
||
async def test_ps1(self): | ||
"""Check default / custom sys.ps1 are correctly set""" | ||
# default | ||
self.infunc.side_effect = EOFError('Finished') | ||
await self.console.interact() | ||
self.assertEqual(self.sysmod.ps1, '>>> ') | ||
# custom | ||
self.sysmod.ps1 = 'custom1> ' | ||
await self.console.interact() | ||
self.assertEqual(self.sysmod.ps1, 'custom1> ') | ||
|
||
async def test_ps2(self): | ||
"""Check default / custom sys.ps2 are correctly set""" | ||
# default | ||
self.infunc.side_effect = EOFError('Finished') | ||
await self.console.interact() | ||
self.assertEqual(self.sysmod.ps2, '... ') | ||
# custom | ||
self.sysmod.ps1 = 'custom2> ' | ||
await self.console.interact() | ||
self.assertEqual(self.sysmod.ps1, 'custom2> ') | ||
|
||
async def test_console_stderr(self): | ||
"""Check stdout is correctly printed""" | ||
self.infunc.side_effect = ["'antioch'", "", EOFError('Finished')] | ||
await self.console.interact() | ||
for call in list(self.stdout.method_calls): | ||
if 'antioch' in ''.join(call[1]): | ||
break | ||
else: | ||
raise AssertionError("no console stdout") | ||
|
||
async def test_syntax_error(self): | ||
"""Check compiler exceptions are retrieved""" | ||
self.infunc.side_effect = ["undefined", EOFError('Finished')] | ||
await self.console.interact() | ||
for call in self.stderr.method_calls: | ||
if 'NameError' in ''.join(call[1]): | ||
break | ||
else: | ||
raise AssertionError("No syntax error from console") | ||
|
||
async def test_sysexcepthook(self): | ||
"""Check custom excepthooks are called""" | ||
self.infunc.side_effect = ["raise ValueError('')", | ||
EOFError('Finished')] | ||
hook = mock.Mock() | ||
self.sysmod.excepthook = hook | ||
await self.console.interact() | ||
self.assertTrue(hook.called) | ||
|
||
async def test_banner(self): | ||
"""Check default/custom banner is correctly printed""" | ||
# with banner ==> stderr called 3 times, custom banner in 1st call | ||
self.infunc.side_effect = EOFError('Finished') | ||
await self.console.interact(banner='Foo') | ||
self.assertEqual(len(self.stderr.method_calls), 3) | ||
banner_call = self.stderr.method_calls[0] | ||
self.assertEqual(banner_call, ['write', ('Foo\n',), {}]) | ||
|
||
# no banner ==> stderr called 2 times | ||
self.stderr.reset_mock() | ||
self.infunc.side_effect = EOFError('Finished') | ||
await self.console.interact(banner='') | ||
self.assertEqual(len(self.stderr.method_calls), 2) | ||
|
||
async def test_exit_msg(self): | ||
"""Check default/custom exit message is correctly printed""" | ||
# default exit message ==> stderr called 2 times and good msg | ||
self.infunc.side_effect = EOFError('Finished') | ||
await self.console.interact(banner='') | ||
self.assertEqual(len(self.stderr.method_calls), 2) | ||
err_msg = self.stderr.method_calls[1] | ||
expected = 'now exiting AsyncInteractiveConsole...\n' | ||
self.assertEqual(err_msg, ['write', (expected,), {}]) | ||
|
||
# no exit message ==> stderr called 1 time | ||
self.stderr.reset_mock() | ||
self.infunc.side_effect = EOFError('Finished') | ||
await self.console.interact(banner='', exitmsg='') | ||
self.assertEqual(len(self.stderr.method_calls), 1) | ||
|
||
# custom exit message ==> stderr called 2 times and custom msg | ||
self.stderr.reset_mock() | ||
message = ( | ||
'bye! \N{GREEK SMALL LETTER ZETA}\N{CYRILLIC SMALL LETTER ZHE}' | ||
) | ||
self.infunc.side_effect = EOFError('Finished') | ||
await self.console.interact(banner='', exitmsg=message) | ||
self.assertEqual(len(self.stderr.method_calls), 2) | ||
err_msg = self.stderr.method_calls[1] | ||
expected = message + '\n' | ||
self.assertEqual(err_msg, ['write', (expected,), {}]) | ||
|
||
async def test_cause_tb(self): | ||
"""Check exceptions chaining is correctly printed""" | ||
self.infunc.side_effect = ["raise ValueError('') from AttributeError", | ||
EOFError('Finished')] | ||
await self.console.interact() | ||
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) | ||
expected = dedent(""" | ||
AttributeError | ||
The above exception was the direct cause of the following exception: | ||
Traceback (most recent call last): | ||
File "<console>", line 1, in <module> | ||
ValueError | ||
""") | ||
self.assertIn(expected, output) | ||
|
||
async def test_context_tb(self): | ||
"""Check exceptions inception is correctly printed""" | ||
self.infunc.side_effect = ["try: ham\nexcept: eggs\n", | ||
EOFError('Finished')] | ||
await self.console.interact() | ||
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) | ||
expected = dedent(""" | ||
Traceback (most recent call last): | ||
File "<console>", line 1, in <module> | ||
NameError: name 'ham' is not defined | ||
During handling of the above exception, another exception occurred: | ||
Traceback (most recent call last): | ||
File "<console>", line 2, in <module> | ||
NameError: name 'eggs' is not defined | ||
""") | ||
self.assertIn(expected, output) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import sys | ||
import unittest | ||
from contextlib import ExitStack | ||
from unittest import mock | ||
|
||
sys.path.append("..") | ||
import asyncode | ||
|
||
|
||
class TestAsyncodeAsync(unittest.IsolatedAsyncioTestCase): | ||
"""Do same tests as code standard module (but async)""" | ||
|
||
def setUp(self): | ||
self.console = asyncode.AsyncInteractiveConsole() | ||
self.mock_sys() | ||
|
||
def mock_sys(self): | ||
"Mock system environment for AsyncInteractiveConsole" | ||
# use exit stack to match patch context managers to addCleanup | ||
stack = ExitStack() | ||
self.addCleanup(stack.close) | ||
self.infunc = stack.enter_context(mock.patch('asyncode.input', | ||
create=True)) | ||
self.stdout = stack.enter_context(mock.patch('asyncode.sys.stdout')) | ||
self.stderr = stack.enter_context(mock.patch('asyncode.sys.stderr')) | ||
self.sysmod = stack.enter_context(mock.patch('asyncode.sys', | ||
wraps=asyncode.sys, | ||
spec=asyncode.sys)) | ||
|
||
async def test_toplevel_await(self): | ||
"""Check top-level await statements are not rejected""" | ||
self.infunc.side_effect = ["await lindbz", | ||
EOFError('Finished')] | ||
await self.console.interact() | ||
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) | ||
self.assertNotIn('SyntaxError', output) | ||
|
||
async def test_coroutine(self): | ||
"""Check coroutines are awaited""" | ||
self.infunc.side_effect = ["import asyncio", | ||
"await asyncio.sleep(0, 'grizzz')", | ||
EOFError('Finished')] | ||
await self.console.interact() | ||
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls) | ||
self.assertIn('grizzz', output) | ||
|
||
async def test_exc_in_coroutine(self): | ||
"""Check exceptions occuring in coroutines are retrieved""" | ||
self.infunc.side_effect = ["import asyncio", | ||
"await asyncio.sleep(None)", | ||
EOFError('Finished')] | ||
await self.console.interact() | ||
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) | ||
self.assertIn('TypeError', output) |
Oops, something went wrong.