From 9856de170302c517564c41ca455f8f71a09c43ac Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 23 Apr 2025 21:52:36 -0400 Subject: [PATCH 1/3] Synchronize `basilisp.main.init` to allow repeat invocations --- CHANGELOG.md | 1 + src/basilisp/main.py | 26 ++++++++++++++++++++------ tests/basilisp/cli_test.py | 27 ++++++++++++++++----------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbfa1b2..f759d369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `basilisp.core/str` now delegates to the builtin Python `str` in all cases except for customizing the string output for builtin Python types (#1237) * Optimised mainstream seq-consuming functions by coercing their inputs into `seq` upfront (#1234) * Renamed `awith` and `afor` to `with-async` and `for-async` for improved clarity (#1248) + * `basilisp.main.init` will only initialize the runtime environment on the first invocation (#1242) ### Fixed * Fix a bug where protocols with methods with leading hyphens in the could not be defined (#1230) diff --git a/src/basilisp/main.py b/src/basilisp/main.py index ea436d01..542d8560 100644 --- a/src/basilisp/main.py +++ b/src/basilisp/main.py @@ -1,6 +1,7 @@ import importlib import os import sysconfig +import threading from pathlib import Path from typing import Optional @@ -11,8 +12,11 @@ from basilisp.lang.typing import CompilerOpts from basilisp.lang.util import munge +_INIT_LOCK = threading.Lock() +_runtime_is_initialized = False -def init(opts: Optional[CompilerOpts] = None) -> None: + +def init(opts: Optional[CompilerOpts] = None, force_reload: bool = False) -> None: """ Initialize the runtime environment for Basilisp code evaluation. @@ -22,12 +26,22 @@ def init(opts: Optional[CompilerOpts] = None) -> None: If you want to execute a Basilisp file which is stored in a well-formed package or module structure, you probably want to use :py:func:`bootstrap`. + + ``init()`` may be called more than once. Only the first invocation will initialize + the runtime unless ``force_reload=True``. """ - logconfig.configure_root_logger() - runtime.init_ns_var() - runtime.bootstrap_core(opts if opts is not None else compiler_opts()) - importer.hook_imports() - importlib.import_module("basilisp.core") + global _runtime_is_initialized + + with _INIT_LOCK: + if _runtime_is_initialized and not force_reload: + return + + logconfig.configure_root_logger() + runtime.init_ns_var() + runtime.bootstrap_core(opts if opts is not None else compiler_opts()) + importer.hook_imports() + importlib.import_module("basilisp.core") + _runtime_is_initialized = True def bootstrap( diff --git a/tests/basilisp/cli_test.py b/tests/basilisp/cli_test.py index 70724554..8ff28c24 100644 --- a/tests/basilisp/cli_test.py +++ b/tests/basilisp/cli_test.py @@ -81,20 +81,25 @@ class CapturedIO: @pytest.fixture -def run_cli(monkeypatch, capsys, cap_lisp_io): +def run_cli(monkeypatch): def _run_cli(args: Sequence[str], input: Optional[str] = None): if input is not None: monkeypatch.setattr( "sys.stdin", io.TextIOWrapper(io.BytesIO(input.encode("utf-8"))) ) - invoke_cli([*args]) - python_io = capsys.readouterr() - lisp_out, lisp_err = cap_lisp_io + process = subprocess.run( + ["basilisp", *args], + stdin=io.StringIO(input) if input is not None else None, + encoding="utf-8", + capture_output=True, + check=True, + ) + return CapturedIO( - out=python_io.out, - err=python_io.err, - lisp_out=lisp_out.getvalue(), - lisp_err=lisp_err.getvalue(), + out=process.stdout, + err=process.stderr, + lisp_out=process.stdout, + lisp_err=process.stderr, ) return _run_cli @@ -204,7 +209,7 @@ def test_valid_flag(self, run_cli, val): @pytest.mark.parametrize("val", ["maybe", "not-no", "4"]) def test_invalid_flag(self, run_cli, val): - with pytest.raises(SystemExit): + with pytest.raises(subprocess.CalledProcessError): run_cli(["run", "--warn-on-var-indirection", val, "-c", "(+ 1 2)"]) @@ -355,7 +360,7 @@ def test_repl_include_extra_path( class TestRun: def test_run_ns_and_code_mutually_exclusive(self, run_cli): - with pytest.raises(SystemExit): + with pytest.raises(subprocess.CalledProcessError): run_cli(["run", "-c", "-n"]) class TestRunCode: @@ -550,7 +555,7 @@ def test_cannot_run_namespace_with_in_ns_arg( self, run_cli, namespace_name: str, namespace_file: pathlib.Path ): namespace_file.write_text("(println (+ 1 2))") - with pytest.raises(SystemExit): + with pytest.raises(subprocess.CalledProcessError): run_cli(["run", "--in-ns", "otherpackage.core", "-n", namespace_name]) def test_run_namespace( From cf067a1a45ed12c7676609387f4690c7b86501da Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 23 Apr 2025 22:00:47 -0400 Subject: [PATCH 2/3] Monkeypatch the lock --- tests/basilisp/cli_test.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/basilisp/cli_test.py b/tests/basilisp/cli_test.py index 8ff28c24..53a04bf9 100644 --- a/tests/basilisp/cli_test.py +++ b/tests/basilisp/cli_test.py @@ -81,25 +81,21 @@ class CapturedIO: @pytest.fixture -def run_cli(monkeypatch): +def run_cli(monkeypatch, capsys, cap_lisp_io): def _run_cli(args: Sequence[str], input: Optional[str] = None): + monkeypatch.setattr("basilisp.main._runtime_is_initialized", False) if input is not None: monkeypatch.setattr( "sys.stdin", io.TextIOWrapper(io.BytesIO(input.encode("utf-8"))) ) - process = subprocess.run( - ["basilisp", *args], - stdin=io.StringIO(input) if input is not None else None, - encoding="utf-8", - capture_output=True, - check=True, - ) - + invoke_cli([*args]) + python_io = capsys.readouterr() + lisp_out, lisp_err = cap_lisp_io return CapturedIO( - out=process.stdout, - err=process.stderr, - lisp_out=process.stdout, - lisp_err=process.stderr, + out=python_io.out, + err=python_io.err, + lisp_out=lisp_out.getvalue(), + lisp_err=lisp_err.getvalue(), ) return _run_cli @@ -209,7 +205,7 @@ def test_valid_flag(self, run_cli, val): @pytest.mark.parametrize("val", ["maybe", "not-no", "4"]) def test_invalid_flag(self, run_cli, val): - with pytest.raises(subprocess.CalledProcessError): + with pytest.raises(SystemExit): run_cli(["run", "--warn-on-var-indirection", val, "-c", "(+ 1 2)"]) @@ -360,7 +356,7 @@ def test_repl_include_extra_path( class TestRun: def test_run_ns_and_code_mutually_exclusive(self, run_cli): - with pytest.raises(subprocess.CalledProcessError): + with pytest.raises(SystemExit): run_cli(["run", "-c", "-n"]) class TestRunCode: @@ -555,7 +551,7 @@ def test_cannot_run_namespace_with_in_ns_arg( self, run_cli, namespace_name: str, namespace_file: pathlib.Path ): namespace_file.write_text("(println (+ 1 2))") - with pytest.raises(subprocess.CalledProcessError): + with pytest.raises(SystemExit): run_cli(["run", "--in-ns", "otherpackage.core", "-n", namespace_name]) def test_run_namespace( From 9cb415523f8850a249645c68a4715c272aab3849 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 29 Apr 2025 21:07:01 -0400 Subject: [PATCH 3/3] Documentation --- docs/gettingstarted.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst index 4adb7d41..3ee9c87e 100644 --- a/docs/gettingstarted.rst +++ b/docs/gettingstarted.rst @@ -120,8 +120,16 @@ Given a Basilisp entrypoint function ``main`` (taking no arguments) in the ``pro If you were to place this in a module such as ``myproject.main``, you could easily configure a `setuptools entry point `_ (or any analog with another build tool) to point to that script directly, effectively launching you directly to Basilisp code. For more sophisticated projects which may not have a direct or wrappable entrypoint, you can initialize the Basilisp runtime directly by calling :py:func:`basilisp.main.init` with no arguments. -This may be a better fit for a project using something like Django, where the entrypoint is dictated by Django. -In that case, you could use a hook such as Django's ``AppConfig.ready()``. +A natural placement for this function call would be in the root ``__init__.py`` for a package, where you can freely import and initialize Basilisp. + +.. code-block:: python + + import basilisp.main + + basilisp.main.init() + +You could also initialize Basilisp in a framework such as Django, where the entrypoint is dictated by the framework. +For example, you could use a hook such as Django's ``AppConfig.ready()``. .. code-block:: python