From e45f3557b1ac4104d167992d352b498861ea5dcf Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 29 Mar 2019 20:01:11 -0700 Subject: [PATCH 1/6] Do not load PyCall in pytest_sessionstart This makes non-test related pytest invocation (e.g. pytest --fixtures) fast (enough). --- src/julia/pytestplugin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/julia/pytestplugin.py b/src/julia/pytestplugin.py index 884c6b81..f946c185 100644 --- a/src/julia/pytestplugin.py +++ b/src/julia/pytestplugin.py @@ -29,10 +29,12 @@ def pytest_sessionstart(session): if not session.config.getoption("julia"): return - from .core import Julia + from .core import LibJulia, enable_debug + + enable_debug() + api = LibJulia.load(julia=session.config.getoption("julia_runtime")) + api.init_julia() - global JULIA - JULIA = Julia(runtime=session.config.getoption("julia_runtime"), debug=True) # Initialize Julia runtime as soon as possible (or more precisely # before importing any additional Python modules) to avoid, e.g., @@ -48,4 +50,6 @@ def julia(request): if not request.config.getoption("julia"): pytest.skip("--no-julia is given.") - return JULIA + from .core import Julia + + return Julia() From d298cfef6e94c4459a5d2ad95ef51312d3b6110f Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 29 Mar 2019 20:46:43 -0700 Subject: [PATCH 2/6] Do not enable pytest plugin by default --- docs/source/pytest.rst | 26 +++++++++++++++++++++----- setup.py | 3 --- src/julia/pytestplugin.py | 2 +- tox.ini | 1 + 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/source/pytest.rst b/docs/source/pytest.rst index 68058352..5b0cc77c 100644 --- a/docs/source/pytest.rst +++ b/docs/source/pytest.rst @@ -17,11 +17,27 @@ tricky aspects of PyJulia initialization: * The tests requiring Julia can be skipped with :option:`--no-julia`. -Use |pytest -p no:julia|_ to disable PyJulia plugin. - -.. |pytest -p no:julia| replace:: ``pytest -p no:julia`` -.. _pytest -p no:julia: - https://docs.pytest.org/en/latest/plugins.html#deactivating-unregistering-a-plugin-by-name +* It enables debug-level logging. This is highly recommended + especially in CI setting as miss-configuration of PyJulia may result + in segmentation fault in which Python cannot provide useful + traceback. + +To activate PyJulia's pytest plugin [#]_ add ``-p julia.pytestplugin`` +to the command line option. There are several ways to do this by +default in your project. One option is to include this using +``addopts`` setup of ``pytest.ini`` or ``tox.ini`` file. See `How to +change command line options defaults +`_: + +.. code-block:: ini + + [pytest] + addopts = + -p julia.pytestplugin + +.. [#] This plugin is not activated by default (as in normal + ``pytest-*`` plugin packages) to avoid accidentally breaking user's + ``pytest`` setup when PyJulia is included as a non-test dependency. Options diff --git a/setup.py b/setup.py index 3dbd7a48..b27a0543 100644 --- a/setup.py +++ b/setup.py @@ -71,9 +71,6 @@ def pyload(path): "console_scripts": [ "python-jl = julia.python_jl:main", ], - "pytest11": [ - "julia = julia.pytestplugin", - ], }, # We bundle Julia scripts etc. inside `julia` directory. Thus, # this directory must exist in the file system (not in a zip diff --git a/src/julia/pytestplugin.py b/src/julia/pytestplugin.py index f946c185..5c38e0bb 100644 --- a/src/julia/pytestplugin.py +++ b/src/julia/pytestplugin.py @@ -7,7 +7,7 @@ def pytest_addoption(parser): import os # Note: the help strings have to be synchronized manually with - # ../docs/source/pytest.rst + # ../../docs/source/pytest.rst parser.addoption( "--no-julia", diff --git a/tox.ini b/tox.ini index 6f63a184..7b4eb50e 100644 --- a/tox.ini +++ b/tox.ini @@ -43,6 +43,7 @@ passenv = [pytest] addopts = + -p julia.pytestplugin --doctest-modules --ignore=test/_star_import.py From 5f50cd3368d5cc240633ce2de540e6a6c1e173a9 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 29 Mar 2019 20:56:11 -0700 Subject: [PATCH 3/6] Support all Julia options in pytest plugin --- docs/source/pytest.rst | 9 +++++++++ src/julia/options.py | 27 +++++++++++++++++++++++---- src/julia/pytestplugin.py | 15 ++++++++++++++- test/test_options.py | 11 ++--------- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/docs/source/pytest.rst b/docs/source/pytest.rst index 5b0cc77c..c6e693ad 100644 --- a/docs/source/pytest.rst +++ b/docs/source/pytest.rst @@ -54,6 +54,15 @@ Following options can be passed to :program:`pytest` Julia executable to be used. Defaults to environment variable `PYJULIA_TEST_RUNTIME`. +.. option:: --julia- + + Some ```` that can be passed to ``julia`` executable + (e.g., ``--compiled-modules=no``) can be passed to ``pytest`` + plugin by ``--julia-`` (e.g., + ``--julia-compiled-modules=no``). See ``pytest -p + julia.pytestplugin --help`` for the actual list of options. + + Fixture ======= diff --git a/src/julia/options.py b/src/julia/options.py index 203090ac..7d5b9696 100644 --- a/src/julia/options.py +++ b/src/julia/options.py @@ -13,6 +13,14 @@ def __get__(self, instance, owner): return self return getattr(instance, self.dataname, self.default) + def cli_argument_name(self): + name = {"bindir": "home"}.get(self.name, self.name) + return "--" + name.replace("_", "-") + + def cli_argument_spec(self): + return dict(help="Julia's ``{}`` option.".format(self.cli_argument_name())) + # TODO: parse help from `options_docs`. + class String(OptionDescriptor): def __init__(self, name, default=None): @@ -54,6 +62,12 @@ def __set__(self, instance, value): def _domain(self): # used in test return set(self.choicemap) + def cli_argument_spec(self): + return dict( + super(Choices, self).cli_argument_spec(), + choices=list(self.choicemap.values()), + ) + def yes_no_etc(*etc): choicemap = {True: "yes", False: "no", "yes": "yes", "no": "no"} @@ -149,16 +163,21 @@ def is_specified(self, name): def specified(self): for name in dir(self.__class__): if self.is_specified(name): - yield name, getattr(self, name) + yield getattr(self.__class__, name), getattr(self, name) def as_args(self): args = [] - for (name, value) in self.specified(): - name = {"bindir": "home"}.get(name, name) - args.append("--" + name.replace("_", "-")) + for (desc, value) in self.specified(): + args.append(desc.cli_argument_name()) args.append(value) return args + @classmethod + def supported_options(cls): + for name in dir(cls): + if cls.is_supported(name): + yield getattr(cls, name) + def parse_jl_options(options): """ diff --git a/src/julia/pytestplugin.py b/src/julia/pytestplugin.py index 5c38e0bb..c6088055 100644 --- a/src/julia/pytestplugin.py +++ b/src/julia/pytestplugin.py @@ -2,6 +2,8 @@ import pytest +from .options import JuliaOptions + def pytest_addoption(parser): import os @@ -24,6 +26,12 @@ def pytest_addoption(parser): default=os.getenv("PYJULIA_TEST_RUNTIME", "julia"), ) + for desc in JuliaOptions.supported_options(): + parser.addoption( + "--julia-{}".format(desc.cli_argument_name().lstrip("-")), + **desc.cli_argument_spec() + ) + def pytest_sessionstart(session): if not session.config.getoption("julia"): @@ -31,9 +39,14 @@ def pytest_sessionstart(session): from .core import LibJulia, enable_debug + options = JuliaOptions() + for desc in JuliaOptions.supported_options(): + cli_option = "--julia-{}".format(desc.cli_argument_name().lstrip("-")) + desc.__set__(options, session.config.getoption(cli_option)) + enable_debug() api = LibJulia.load(julia=session.config.getoption("julia_runtime")) - api.init_julia() + api.init_julia(options) # Initialize Julia runtime as soon as possible (or more precisely diff --git a/test/test_options.py b/test/test_options.py index 503ebfad..c11e4bd3 100644 --- a/test/test_options.py +++ b/test/test_options.py @@ -13,20 +13,13 @@ def parse_options_docs(docs): return optdefs -def supported_names(cls): - for name in dir(cls): - if cls.is_supported(name): - yield name - - def test_options_docs(): """ Ensure that `JuliaOptions` and `JuliaOptions.__doc__` agree. """ optdefs = parse_options_docs(options_docs) - for name in supported_names(JuliaOptions): - odef = optdefs.pop(name) - desc = getattr(JuliaOptions, name) + for desc in JuliaOptions.supported_options(): + odef = optdefs.pop(desc.name) assert odef["domain"] == desc._domain() assert not optdefs From 3816e5ef557d49f01e70878c6729874eac58bb3a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 30 Mar 2019 11:25:35 -0700 Subject: [PATCH 4/6] Make `tox -e py2 -- --julia-compiled-modules=no` work for case PyCall is configured with Python 3 and Julia >= 1 is used. --- src/julia/pytestplugin.py | 17 ++++++++++++++++- test/test_core.py | 10 +--------- test/test_utils.py | 1 + 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/julia/pytestplugin.py b/src/julia/pytestplugin.py index c6088055..5f25af63 100644 --- a/src/julia/pytestplugin.py +++ b/src/julia/pytestplugin.py @@ -4,6 +4,8 @@ from .options import JuliaOptions +_USING_DEFAULT_SETUP = True + def pytest_addoption(parser): import os @@ -44,8 +46,13 @@ def pytest_sessionstart(session): cli_option = "--julia-{}".format(desc.cli_argument_name().lstrip("-")) desc.__set__(options, session.config.getoption(cli_option)) + julia_runtime = session.config.getoption("julia_runtime") + + global _USING_DEFAULT_SETUP + _USING_DEFAULT_SETUP = not (julia_runtime or options.as_args()) + enable_debug() - api = LibJulia.load(julia=session.config.getoption("julia_runtime")) + api = LibJulia.load(julia=julia_runtime) api.init_julia(options) @@ -66,3 +73,11 @@ def julia(request): from .core import Julia return Julia() + + +def pytest_runtest_setup(item): + if not (item.config.getoption("julia") and _USING_DEFAULT_SETUP): + for mark in item.iter_markers("pyjulia__using_default_setup"): + pytest.skip( + "using non-default setup (e.g., --julia- is given)" + ) diff --git a/test/test_core.py b/test/test_core.py index b10da36a..a9dfad65 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -147,15 +147,7 @@ def test_module_dir(julia): assert "resize_b" in dir(Base) -@pytest.mark.skipif( - "PYJULIA_TEST_RUNTIME" in orig_env, - reason=( - "cannot be tested with custom Julia executable;" - " PYJULIA_TEST_RUNTIME is set to {}".format( - orig_env.get("PYJULIA_TEST_RUNTIME") - ) - ), -) +@pytest.mark.pyjulia__using_default_setup def test_import_without_setup(): command = [sys.executable, "-c", "from julia import Base"] print("Executing:", *command) diff --git a/test/test_utils.py b/test/test_utils.py index 2e70003b..55138296 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -44,6 +44,7 @@ def test_raise_separate_cache_error_dynamically_linked(): assert "have to match exactly" in str(excinfo.value) +@pytest.mark.pyjulia__using_default_setup def test_atexit(): proc = runcode( sys.executable, From c162fd4a094032f126145cf3ce097b42eee9284a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 30 Mar 2019 11:50:49 -0700 Subject: [PATCH 5/6] Add `julia` pytest marker --- docs/source/pytest.rst | 20 +++++++++++++++++++- src/julia/pytestplugin.py | 4 ++++ test/test_libjulia.py | 3 +++ test/test_python_jl.py | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/source/pytest.rst b/docs/source/pytest.rst index c6e693ad..3291ff46 100644 --- a/docs/source/pytest.rst +++ b/docs/source/pytest.rst @@ -13,7 +13,7 @@ tricky aspects of PyJulia initialization: bundled with ``julia`` are newer than the ones otherwise loaded). * It provides a way to succinctly mark certain tests require Julia - runtime (see `Fixture`_). + runtime (see `Fixture`_ and `Marker`_). * The tests requiring Julia can be skipped with :option:`--no-julia`. @@ -77,3 +77,21 @@ initialized. Example usage:: This fixture also "marks" that this test requires a Julia runtime. Thus, the tests using ``julia`` fixture are not run when :option:`--no-julia` is passed. + + +Marker +====== + +PyJulia's pytest plugin also includes a `pytest marker +`_ ``julia`` +which can be used to mark that the test requires PyJulia setup. It is +similar to ``julia`` fixture but it does not instantiate the actual +:class:`.Julia` object. + +Example usage:: + + import pytest + + @pytest.mark.julia + def test_import(): + from julia import MyModule diff --git a/src/julia/pytestplugin.py b/src/julia/pytestplugin.py index 5f25af63..f29e1e60 100644 --- a/src/julia/pytestplugin.py +++ b/src/julia/pytestplugin.py @@ -76,6 +76,10 @@ def julia(request): def pytest_runtest_setup(item): + if not item.config.getoption("julia"): + for mark in item.iter_markers("julia"): + pytest.skip("--no-julia is given.") + if not (item.config.getoption("julia") and _USING_DEFAULT_SETUP): for mark in item.iter_markers("pyjulia__using_default_setup"): pytest.skip( diff --git a/test/test_libjulia.py b/test/test_libjulia.py index 8098a3f2..f85d7d8d 100644 --- a/test/test_libjulia.py +++ b/test/test_libjulia.py @@ -9,6 +9,7 @@ @pytest.mark.skipif("juliainfo.version_info < (0, 7)") +@pytest.mark.julia def test_compiled_modules_no(): runcode( sys.executable, @@ -28,6 +29,7 @@ def test_compiled_modules_no(): @pytest.mark.skipif("not juliainfo.is_compatible_python()") +@pytest.mark.julia def test_custom_sysimage(tmpdir): sysimage = str(tmpdir.join("sys.so")) runcode( @@ -58,6 +60,7 @@ def test_custom_sysimage(tmpdir): ) +@pytest.mark.julia def test_non_existing_sysimage(tmpdir): proc = runcode( sys.executable, diff --git a/test/test_python_jl.py b/test/test_python_jl.py index 359f854d..14cf6e4e 100644 --- a/test/test_python_jl.py +++ b/test/test_python_jl.py @@ -60,6 +60,7 @@ def test_cli_quick_pass_no_julia(args): @pytest.mark.skipif( not PYJULIA_TEST_REBUILD, reason="PYJULIA_TEST_REBUILD=yes is not set") +@pytest.mark.julia def test_cli_import(): args = ["-c", dedent(""" from julia import Base From fd509f15d170c836f8bbd70705bc3fedc7cf68fe Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 31 Mar 2019 19:03:24 -0700 Subject: [PATCH 6/6] Workaround Julia 0.6 bug with quick initialization --- src/julia/pytestplugin.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/julia/pytestplugin.py b/src/julia/pytestplugin.py index f29e1e60..760fa3a3 100644 --- a/src/julia/pytestplugin.py +++ b/src/julia/pytestplugin.py @@ -39,7 +39,7 @@ def pytest_sessionstart(session): if not session.config.getoption("julia"): return - from .core import LibJulia, enable_debug + from .core import LibJulia, JuliaInfo, Julia, enable_debug options = JuliaOptions() for desc in JuliaOptions.supported_options(): @@ -52,8 +52,14 @@ def pytest_sessionstart(session): _USING_DEFAULT_SETUP = not (julia_runtime or options.as_args()) enable_debug() - api = LibJulia.load(julia=julia_runtime) - api.init_julia(options) + info = JuliaInfo.load(julia=julia_runtime) + if (info.version_major, info.version_minor) < (0, 7): + # In Julia 0.6, we have to load PyCall.jl here to do the + # fake-julia setup. + Julia(runtime=julia_runtime) + else: + api = LibJulia.from_juliainfo(info) + api.init_julia(options) # Initialize Julia runtime as soon as possible (or more precisely