Skip to content

Commit 1a51e17

Browse files
authored
Support reading Basilisp code from CLI stdin (#349)
* Support reading Basilisp code from CLI stdin * Slightly more stylish Makefile * Skip CLI tests on PyPy for now * Use this nicer Module skip * UGH * I literally hate PyPy
1 parent 6e8ddec commit 1a51e17

File tree

3 files changed

+122
-12
lines changed

3 files changed

+122
-12
lines changed

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ repl:
3333
test-pypy:
3434
@docker run \
3535
--mount src=`pwd`,target=/usr/src/app,type=bind \
36+
--workdir /usr/src/app \
3637
pypy:3.6-7.0-slim-jessie \
37-
/bin/sh -c 'cd /usr/src/app && pip install tox && tox -e pypy3'
38+
/bin/sh -c 'pip install tox && tox -e pypy3'
3839

3940

4041
.PHONY: pypy-shell
4142
pypy-shell:
4243
@docker run -it \
4344
--mount src=`pwd`,target=/usr/src/app,type=bind \
45+
--workdir /usr/src/app \
4446
pypy:3.6-7.0-slim-jessie \
45-
/bin/sh -c 'cd /usr/src/app && pip install -e . && basilisp repl'
47+
/bin/sh -c 'pip install -e . && basilisp repl'
4648

4749

4850
.PHONY: test

src/basilisp/cli.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import atexit
22
import importlib
33
import os.path
4+
import platform
45
import traceback
56
import types
67
from typing import Any
@@ -20,11 +21,13 @@
2021
)
2122
BASILISP_REPL_HISTORY_LENGTH = 1000
2223
REPL_INPUT_FILE_PATH = "<REPL Input>"
24+
STDIN_INPUT_FILE_PATH = "<stdin>"
25+
STDIN_FILE_NAME = "-"
2326

2427

2528
try:
2629
import readline
27-
except ImportError:
30+
except ImportError: # pragma: no cover
2831
pass
2932
else:
3033
readline.parse_and_bind("tab: complete")
@@ -34,23 +37,34 @@
3437
try:
3538
readline.read_history_file(BASILISP_REPL_HISTORY_FILE_PATH)
3639
readline.set_history_length(BASILISP_REPL_HISTORY_LENGTH)
37-
except FileNotFoundError:
40+
except FileNotFoundError: # pragma: no cover
3841
pass
39-
40-
atexit.register(readline.write_history_file, BASILISP_REPL_HISTORY_FILE_PATH)
42+
except Exception: # noqa # pragma: no cover
43+
# PyPy 3.6's ncurses implementation throws an error here
44+
if platform.python_implementation() != "PyPy":
45+
raise
46+
else:
47+
atexit.register(readline.write_history_file, BASILISP_REPL_HISTORY_FILE_PATH)
4148

4249

4350
@click.group()
4451
def cli():
4552
"""Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."""
46-
pass
4753

4854

4955
def eval_file(filename: str, ctx: compiler.CompilerContext, module: types.ModuleType):
5056
"""Evaluate a file with the given name into a Python module AST node."""
5157
last = None
5258
for form in reader.read_file(filename, resolver=runtime.resolve_alias):
53-
last = compiler.compile_and_exec_form(form, ctx, module, filename)
59+
last = compiler.compile_and_exec_form(form, ctx, module)
60+
return last
61+
62+
63+
def eval_stream(stream, ctx: compiler.CompilerContext, module: types.ModuleType):
64+
"""Evaluate the forms in stdin into a Python module AST node."""
65+
last = None
66+
for form in reader.read(stream, resolver=runtime.resolve_alias):
67+
last = compiler.compile_and_exec_form(form, ctx, module)
5468
return last
5569

5670

@@ -134,7 +148,7 @@ def repl(
134148
lsrc = input(f"{ns.name}=> ")
135149
except EOFError:
136150
break
137-
except KeyboardInterrupt:
151+
except KeyboardInterrupt: # pragma: no cover
138152
print("")
139153
continue
140154

@@ -143,7 +157,7 @@ def repl(
143157

144158
try:
145159
result = eval_str(lsrc, ctx, ns.module, eof)
146-
if result is eof:
160+
if result is eof: # pragma: no cover
147161
continue
148162
print(runtime.lrepr(result))
149163
repl_module.mark_repl_result(result)
@@ -209,7 +223,11 @@ def run( # pylint: disable=too-many-arguments
209223
"""Run a Basilisp script or a line of code, if it is provided."""
210224
basilisp.init()
211225
ctx = compiler.CompilerContext(
212-
filename=CLI_INPUT_FILE_PATH if code else file_or_code,
226+
filename=CLI_INPUT_FILE_PATH
227+
if code
228+
else (
229+
STDIN_INPUT_FILE_PATH if file_or_code == STDIN_FILE_NAME else file_or_code
230+
),
213231
opts={
214232
compiler.WARN_ON_SHADOWED_NAME: warn_on_shadowed_name,
215233
compiler.WARN_ON_SHADOWED_VAR: warn_on_shadowed_var,
@@ -222,16 +240,29 @@ def run( # pylint: disable=too-many-arguments
222240
with runtime.ns_bindings(in_ns) as ns:
223241
if code:
224242
print(runtime.lrepr(eval_str(file_or_code, ctx, ns.module, eof)))
243+
elif file_or_code == STDIN_FILE_NAME:
244+
print(
245+
runtime.lrepr(
246+
eval_stream(click.get_text_stream("stdin"), ctx, ns.module)
247+
)
248+
)
225249
else:
226250
print(runtime.lrepr(eval_file(file_or_code, ctx, ns.module)))
227251

228252

229253
@cli.command(short_help="run tests in a Basilisp project")
230254
@click.argument("args", nargs=-1)
231-
def test(args):
255+
def test(args): # pragma: no cover
232256
"""Run tests in a Basilisp project."""
233257
pytest.main(args=list(args))
234258

235259

260+
@cli.command(short_help="print the version of Basilisp")
261+
def version():
262+
from basilisp.__version__ import __version__
263+
264+
print(f"Basilisp {__version__}")
265+
266+
236267
if __name__ == "__main__":
237268
cli()

tests/basilisp/cli_test.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import platform
2+
import re
3+
4+
import pytest
5+
from click.testing import CliRunner
6+
7+
from basilisp.cli import cli
8+
9+
pytestmark = pytest.mark.skipif(
10+
platform.python_implementation() == "PyPy", reason="CLI tests fail on PyPy 3.6"
11+
)
12+
13+
14+
class TestREPL:
15+
def test_no_input(self):
16+
runner = CliRunner()
17+
result = runner.invoke(cli, ["repl"], input="")
18+
assert "user=> " == result.stdout
19+
20+
def test_newline(self):
21+
runner = CliRunner()
22+
result = runner.invoke(cli, ["repl"], input="\n")
23+
assert "user=> user=> " == result.stdout
24+
25+
def test_simple_expression(self):
26+
runner = CliRunner()
27+
result = runner.invoke(cli, ["repl"], input="(+ 1 2)")
28+
assert "user=> 3\nuser=> " == result.stdout
29+
30+
def test_syntax_error(self):
31+
runner = CliRunner()
32+
result = runner.invoke(cli, ["repl"], input="(+ 1 2")
33+
assert (
34+
"basilisp.lang.reader.SyntaxError: Unexpected EOF in list\nuser=> "
35+
in result.stdout
36+
)
37+
38+
def test_compiler_error(self):
39+
runner = CliRunner()
40+
result = runner.invoke(cli, ["repl"], input="(fn*)")
41+
assert (
42+
"basilisp.lang.compiler.exception.CompilerException: fn form "
43+
"must match: (fn* name? [arg*] body*) or (fn* name? method*)\nuser=> "
44+
) in result.stdout
45+
46+
def test_other_exception(self):
47+
runner = CliRunner()
48+
result = runner.invoke(
49+
cli, ["repl"], input='(throw (builtins/Exception "CLI test"))'
50+
)
51+
assert "Exception: CLI test\nuser=> " in result.stdout
52+
53+
54+
class TestRun:
55+
def test_run_code(self):
56+
runner = CliRunner()
57+
result = runner.invoke(cli, ["run", "-c", "(+ 1 2)"])
58+
assert "3\n" == result.stdout
59+
60+
def test_run_file(self):
61+
runner = CliRunner()
62+
with runner.isolated_filesystem():
63+
with open("test.lpy", mode="w") as f:
64+
f.write("(+ 1 2)")
65+
result = runner.invoke(cli, ["run", "test.lpy"])
66+
assert "3\n" == result.stdout
67+
68+
def test_run_stdin(self):
69+
runner = CliRunner()
70+
result = runner.invoke(cli, ["run", "-"], input="(+ 1 2)")
71+
assert "3\n" == result.stdout
72+
73+
74+
def test_version():
75+
runner = CliRunner()
76+
result = runner.invoke(cli, ["version"])
77+
assert re.compile(r"^Basilisp (\d+)\.(\d+)\.(\w*)(\d+)\n$").match(result.stdout)

0 commit comments

Comments
 (0)