diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ef24d62..36ae818 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -13,6 +13,7 @@ jobs: build: runs-on: ubuntu-latest + timeout-minutes: 10 strategy: matrix: python-version: [3.8] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ac1b74..0fed834 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,6 @@ jobs: cp /tmp/.coverage ./ - name: pytest run: | - pytest + pytest -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 diff --git a/IPython/core/magics/script.py b/IPython/core/magics/script.py index d4e7b08..5bf6a02 100644 --- a/IPython/core/magics/script.py +++ b/IPython/core/magics/script.py @@ -3,24 +3,23 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +import asyncio +import atexit import errno +import functools import os -import sys import signal +import sys import time -import asyncio -import atexit - +from contextlib import contextmanager from subprocess import CalledProcessError +from traitlets import Dict, List, default + from IPython.core import magic_arguments -from IPython.core.magic import ( - Magics, magics_class, line_magic, cell_magic -) +from IPython.core.magic import Magics, cell_magic, line_magic, magics_class from IPython.lib.backgroundjobs import BackgroundJobManager from IPython.utils.process import arg_split -from traitlets import List, Dict, default - #----------------------------------------------------------------------------- # Magic implementation classes @@ -67,6 +66,41 @@ def script_args(f): f = arg(f) return f + +@contextmanager +def safe_watcher(): + if sys.platform == "win32": + yield + return + + from asyncio import SafeChildWatcher + + policy = asyncio.get_event_loop_policy() + old_watcher = policy.get_child_watcher() + if isinstance(old_watcher, SafeChildWatcher): + yield + return + + loop = asyncio.get_event_loop() + try: + watcher = asyncio.SafeChildWatcher() + watcher.attach_loop(loop) + policy.set_child_watcher(watcher) + yield + finally: + watcher.close() + policy.set_child_watcher(old_watcher) + + +def dec_safe_watcher(fun): + @functools.wraps(fun) + def _inner(*args, **kwargs): + with safe_watcher(): + return fun(*args, **kwargs) + + return _inner + + @magics_class class ScriptMagics(Magics): """Magics for talking to scripts @@ -157,6 +191,7 @@ class ScriptMagics(Magics): @magic_arguments.magic_arguments() @script_args @cell_magic("script") + @dec_safe_watcher def shebang(self, line, cell): """Run a cell via a shell command @@ -204,7 +239,6 @@ class ScriptMagics(Magics): if sys.platform.startswith("win"): asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) loop = asyncio.get_event_loop() - argv = arg_split(line, posix=not sys.platform.startswith("win")) args, cmd = self.shebang.parser.parse_known_args(argv) try: diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index fd5696d..d534d11 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -4,38 +4,42 @@ Needs to be run by nose (to make ipython session available). """ +import asyncio import io import os import re +import shlex import sys import warnings -from textwrap import dedent -from unittest import TestCase -from unittest import mock from importlib import invalidate_caches from io import StringIO from pathlib import Path +from textwrap import dedent +from unittest import TestCase, mock import nose.tools as nt -import shlex +import pytest from IPython import get_ipython from IPython.core import magic from IPython.core.error import UsageError -from IPython.core.magic import (Magics, magics_class, line_magic, - cell_magic, - register_line_magic, register_cell_magic) -from IPython.core.magics import execution, script, code, logging, osm +from IPython.core.magic import ( + Magics, + cell_magic, + line_magic, + magics_class, + register_cell_magic, + register_line_magic, +) +from IPython.core.magics import code, execution, logging, osm, script from IPython.testing import decorators as dec from IPython.testing import tools as tt from IPython.utils.io import capture_output -from IPython.utils.tempdir import (TemporaryDirectory, - TemporaryWorkingDirectory) from IPython.utils.process import find_cmd -from .test_debugger import PdbTestInput +from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory -import pytest +from .test_debugger import PdbTestInput @magic.magics_class @@ -947,33 +951,64 @@ def test_script_config(): sm = script.ScriptMagics(shell=ip) nt.assert_in('whoda', sm.magics['cell']) +@dec.skip_iptest_but_not_pytest @dec.skip_win32 +@pytest.mark.skipif( + sys.platform == "win32", reason="This test does not run under Windows" +) def test_script_out(): + assert asyncio.get_event_loop().is_running() is False + ip = get_ipython() ip.run_cell_magic("script", "--out output sh", "echo 'hi'") + assert asyncio.get_event_loop().is_running() is False nt.assert_equal(ip.user_ns['output'], 'hi\n') +@dec.skip_iptest_but_not_pytest @dec.skip_win32 +@pytest.mark.skipif( + sys.platform == "win32", reason="This test does not run under Windows" +) def test_script_err(): ip = get_ipython() + assert asyncio.get_event_loop().is_running() is False ip.run_cell_magic("script", "--err error sh", "echo 'hello' >&2") + assert asyncio.get_event_loop().is_running() is False nt.assert_equal(ip.user_ns['error'], 'hello\n') + +@dec.skip_iptest_but_not_pytest @dec.skip_win32 +@pytest.mark.skipif( + sys.platform == "win32", reason="This test does not run under Windows" +) def test_script_out_err(): + ip = get_ipython() - ip.run_cell_magic("script", "--out output --err error sh", "echo 'hi'\necho 'hello' >&2") - nt.assert_equal(ip.user_ns['output'], 'hi\n') - nt.assert_equal(ip.user_ns['error'], 'hello\n') + ip.run_cell_magic( + "script", "--out output --err error sh", "echo 'hi'\necho 'hello' >&2" + ) + nt.assert_equal(ip.user_ns["output"], "hi\n") + nt.assert_equal(ip.user_ns["error"], "hello\n") + +@dec.skip_iptest_but_not_pytest @dec.skip_win32 +@pytest.mark.skipif( + sys.platform == "win32", reason="This test does not run under Windows" +) async def test_script_bg_out(): ip = get_ipython() ip.run_cell_magic("script", "--bg --out output sh", "echo 'hi'") nt.assert_equal((await ip.user_ns["output"].read()), b"hi\n") - ip.user_ns['output'].close() + ip.user_ns["output"].close() + asyncio.get_event_loop().stop() +@dec.skip_iptest_but_not_pytest @dec.skip_win32 +@pytest.mark.skipif( + sys.platform == "win32", reason="This test does not run under Windows" +) async def test_script_bg_err(): ip = get_ipython() ip.run_cell_magic("script", "--bg --err error sh", "echo 'hello' >&2") @@ -981,7 +1016,11 @@ async def test_script_bg_err(): ip.user_ns["error"].close() +@dec.skip_iptest_but_not_pytest @dec.skip_win32 +@pytest.mark.skipif( + sys.platform == "win32", reason="This test does not run under Windows" +) async def test_script_bg_out_err(): ip = get_ipython() ip.run_cell_magic(