Skip to content

Commit

Permalink
Merge pull request #2469 from Kodiologist/hy-i
Browse files Browse the repository at this point in the history
Treat `hy -i` as a flag, and allow running scripts provided through standard input
  • Loading branch information
Kodiologist authored Jul 22, 2023
2 parents 0bef56f + 1abce37 commit a39609d
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 247 deletions.
2 changes: 2 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Unreleased

Breaking Changes
------------------------------
* `hy` now only implicitly launches a REPL if standard input is a TTY.
* `hy -i` has been overhauled to work as a flag like `python3 -i`.
* `hy2py` now requires `-m` to specify modules, and uses
the same `sys.path` rules as Python when parsing a module
vs a standalone script.
Expand Down
3 changes: 2 additions & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ hy

``hy`` is a command-line interface for Hy that in general imitates the program
``python`` provided by CPython. For example, ``hy`` without arguments launches
the :ref:`REPL <repl>`, whereas ``hy foo.hy a b`` runs the Hy program
the :ref:`REPL <repl>` if standard input is a TTY and runs the standard input
as a script otherwise, whereas ``hy foo.hy a b`` runs the Hy program
``foo.hy`` with ``a`` and ``b`` as command-line arguments. See ``hy --help``
for a complete list of options and :py:ref:`Python's documentation
<using-on-cmdline>` for many details. Here are some Hy-specific details:
Expand Down
130 changes: 75 additions & 55 deletions hy/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,7 @@ def run_command(source, filename=None):
return 0


def run_icommand(source, **kwargs):
if Path(source).exists():
filename = source
set_path(source)
with open(source, "r", encoding="utf-8") as f:
source = f.read()
else:
filename = "<string>"

hr = REPL(**kwargs)
with filtered_hy_exceptions():
res = hr.runsource(source, filename=filename)

# If the command was prematurely ended, show an error (just like Python
# does).
if res:
hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)

return hr.run()


USAGE = "hy [-h | -v | -i CMD | -c CMD | -m MODULE | FILE | -] [ARG]..."
USAGE = "hy [-h | -v | -i | -c CMD | -m MODULE | FILE | -] [ARG]..."
VERSION = "hy " + hy.__version__
EPILOG = """
FILE
Expand Down Expand Up @@ -114,9 +93,8 @@ def cmdline_handler(scriptname, argv):
),
dict(
name=["-i"],
dest="icommand",
terminate=True,
help="program passed in as string, then stay in REPL",
action="store_true",
help="launch REPL after running script; forces a prompt even if stdin does not appear to be a terminal",
),
dict(
name=["-m"],
Expand Down Expand Up @@ -242,41 +220,65 @@ def proc_opt(opt, arg=None, item=None, i=None):
print(VERSION)
return 0

if "command" in options:
action, action_arg = (
# If the `command` or `mod` options were provided, we'll run
# the corresponding code.
["eval_string", options["command"]]
if "command" in options else
["run_module", options["mod"]]
if "mod" in options else
# Otherwise, we'll run any provided filename as a script (or
# standard input, if the filename is "-").
["run_script_stdin", None]
if argv and argv[0] == "-" else
["run_script_file", argv[0]]
if argv else
# With none of those arguments, we'll launch the REPL (if
# standard input is a TTY) or run a script from standard input
# (otherwise).
["just_repl", None]
if sys.stdin.isatty() else
["run_script_stdin", None])
repl = (
REPL(
spy = options.get("spy"),
output_fn = options.get("repl_output_fn"))
if "i" in options or action == "just_repl"
else None)
source = ''

if action == "eval_string":
sys.argv = ["-c"] + argv
return run_command(options["command"], filename="<string>")

if "mod" in options:
if repl:
source = action_arg
filename = '<string>'
else:
return run_command(action_arg, filename="<string>")
elif action == "run_module":
if repl: raise ValueError()
set_path("")
sys.argv = [program] + argv
runpy.run_module(hy.mangle(options["mod"]), run_name="__main__", alter_sys=True)
runpy.run_module(hy.mangle(action_arg), run_name="__main__", alter_sys=True)
return 0

if "icommand" in options:
return run_icommand(
options["icommand"],
spy=options.get("spy"),
output_fn=options.get("repl_output_fn"),
)

if argv:
if argv[0] == "-":
# Read the program from stdin
elif action == "run_script_stdin":
if repl:
source = sys.stdin
filename = 'stdin'
else:
return run_command(sys.stdin.read(), filename="<stdin>")

elif action == "run_script_file":
filename = Path(action_arg)
set_path(filename)
# Ensure __file__ is set correctly in the code we're about
# to run.
if PY3_9:
if not filename.is_absolute():
filename = Path.cwd() / filename
if platform.system() == "Windows":
filename = os.path.normpath(filename)
if repl:
source = Path(filename).read_text()
else:
# User did "hy <filename>"

filename = Path(argv[0])
set_path(filename)
# Ensure __file__ is set correctly in the code we're about
# to run.
if PY3_9:
if not filename.is_absolute():
filename = Path.cwd() / filename
if platform.system() == "Windows":
filename = os.path.normpath(filename)

try:
sys.argv = argv
with filtered_hy_exceptions():
Expand All @@ -293,8 +295,26 @@ def proc_opt(opt, arg=None, item=None, i=None):
except HyLanguageError:
hy_exc_handler(*sys.exc_info())
sys.exit(1)
else:
assert action == "just_repl"

return REPL(spy=options.get("spy"), output_fn=options.get("repl_output_fn")).run()
# If we didn't return earlier, we'll be using the REPL.
if source:
# Execute `source` in the REPL before entering interactive mode.
res = None
filename = str(filename)
with filtered_hy_exceptions():
accum = ''
for chunk in ([source] if isinstance(source, str) else source):
accum += chunk
res = repl.runsource(accum, filename=filename)
if not res:
accum = ''
# If the command was prematurely ended, show an error (just like Python
# does).
if res:
hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)
return repl.run()


# entry point for cmd line script "hy"
Expand Down
189 changes: 174 additions & 15 deletions tests/native_tests/repl.hy
Original file line number Diff line number Diff line change
@@ -1,27 +1,186 @@
; Many other tests of the REPL are in `test_bin.py`.
; Some other tests of the REPL are in `test_bin.py`.

(import
io
sys
re
pytest)

(defn test-preserve-ps1 [monkeypatch]

(defn [pytest.fixture] rt [monkeypatch capsys]
"Do a test run of the REPL."
(fn [[inp ""] [to-return 'out] [spy False] [py-repr False]]
(monkeypatch.setattr "sys.stdin" (io.StringIO inp))
(.run (hy.REPL
:spy spy
:output-fn (when py-repr repr)))
(setv result (capsys.readouterr))
(cond
(= to-return 'out) result.out
(= to-return 'err) result.err
(= to-return 'both) result)))

(defmacro has [haystack needle]
"`in` backwards."
`(in ~needle ~haystack))


(defn test-simple [rt]
(assert (has (rt #[[(.upper "hello")]]) "HELLO")))

(defn test-spy [rt]
(setv x (rt #[[(.upper "hello")]] :spy True))
(assert (has x ".upper()"))
(assert (has x "HELLO"))
; `spy` should work even when an exception is thrown
(assert (has (rt "(foof)" :spy True) "foof()")))

(defn test-multiline [rt]
(assert (has (rt "(+ 1 3\n5 9)") " 18\n=> ")))

(defn test-history [rt]
(assert (has
(rt #[[
(+ "a" "b")
(+ "c" "d")
(+ "e" "f")
(.format "*1: {}, *2: {}, *3: {}," *1 *2 *3)]])
#[["*1: ef, *2: cd, *3: ab,"]]))
(assert (has
(rt #[[
(raise (Exception "TEST ERROR"))
(+ "err: " (str *e))]])
#[["err: TEST ERROR"]])))

(defn test-comments [rt]
(setv err-empty (rt "" 'err))
(setv x (rt #[[(+ "a" "b") ; "c"]] 'both))
(assert (has x.out "ab"))
(assert (= x.err err-empty))
(assert (= (rt "; 1" 'err) err-empty)))

(defn test-assignment [rt]
"If the last form is an assignment, don't print the value."
(assert (not (has (rt #[[(setv x (+ "A" "Z"))]]) "AZ")))
(setv x (rt #[[(setv x (+ "A" "Z")) (+ "B" "Y")]]))
(assert (has x "BY"))
(assert (not (has x "AZ")))
(setv x (rt #[[(+ "B" "Y") (setv x (+ "A" "Z"))]]))
(assert (not (has x "BY")))
(assert (not (has x "AZ"))))

(defn test-multi-setv [rt]
; https://github.com/hylang/hy/issues/1255
(assert (re.match
r"=>\s+2\s+=>"
(rt (.replace
"(do
(setv it 0 it (+ it 1) it (+ it 1))
it)"
"\n" " ")))))

(defn test-error-underline-alignment [rt]
(setv err (rt "(defmacro mabcdefghi [x] x)\n(mabcdefghi)" 'err))
(setv msg-idx (.rindex err " (mabcdefghi)"))
(setv [_ e1 e2 e3 #* _] (.splitlines (cut err msg_idx None)))
(assert (.startswith e1 " ^----------^"))
(assert (.startswith e2 "expanding macro mabcdefghi"))
(assert (or
; PyPy can use a function's `__name__` instead of
; `__code__.co_name`.
(.startswith e3 " TypeError: mabcdefghi")
(.startswith e3 " TypeError: (mabcdefghi)"))))

(defn test-except-do [rt]
; https://github.com/hylang/hy/issues/533
(assert (has
(rt #[[(try (/ 1 0) (except [ZeroDivisionError] "hello"))]])
"hello"))
(setv x (rt
#[[(try (/ 1 0) (except [ZeroDivisionError] "aaa" "bbb" "ccc"))]]))
(assert (not (has x "aaa")))
(assert (not (has x "bbb")))
(assert (has x "ccc"))
(setv x (rt
#[[(when True "xxx" "yyy" "zzz")]]))
(assert (not (has x "xxx")))
(assert (not (has x "yyy")))
(assert (has x "zzz")))

(defn test-unlocatable-hytypeerror [rt]
; https://github.com/hylang/hy/issues/1412
; The chief test of interest here is that the REPL isn't itself
; throwing an error.
(assert (has
(rt :to-return 'err #[[
(import hy.errors)
(raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))]])
"AZ")))

(defn test-syntax-errors [rt]
; https://github.com/hylang/hy/issues/2004
(assert (has
(rt "(defn foo [/])\n(defn bar [a a])" 'err)
"SyntaxError: duplicate argument"))
; https://github.com/hylang/hy/issues/2014
(setv err (rt "(defn foo []\n(import re *))" 'err))
(assert (has err "SyntaxError: import * only allowed"))
(assert (not (has err "PrematureEndOfInput"))))

(defn test-bad-repr [rt]
; https://github.com/hylang/hy/issues/1389
(setv x (rt :to-return 'both #[[
(defclass BadRepr [] (defn __repr__ [self] (/ 0)))
(BadRepr)
(+ "A" "Z")]]))
(assert (has x.err "ZeroDivisionError"))
(assert (has x.out "AZ")))

(defn test-py-repr [rt]
(assert (has
(rt "(+ [1] [2])")
"[1 2]"))
(assert (has
(rt "(+ [1] [2])" :py-repr True)
"[1, 2]"))
(setv x
(rt "(+ [1] [2])" :py-repr True :spy True))
(assert (has x "[1] + [2]"))
(assert (has x "[1, 2]"))
; --spy should work even when an exception is thrown
(assert (has
(rt "(+ [1] [2] (foof))" :py-repr True :spy True)
"[1] + [2]")))

(defn test-builtins [rt]
(assert (has
(rt "quit")
"Use (quit) or Ctrl-D (i.e. EOF) to exit"))
(assert (has
(rt "exit")
"Use (exit) or Ctrl-D (i.e. EOF) to exit"))
(assert (has
(rt "help")
"Use (help) for interactive help, or (help object) for help about object."))
; The old values of these objects come back after the REPL ends.
(assert (.startswith
(str quit)
"Use quit() or")))

(defn test-preserve-ps1 [rt]
; https://github.com/hylang/hy/issues/1323#issuecomment-1310837340
(monkeypatch.setattr "sys.stdin" (io.StringIO "(+ 1 1)"))
(setv sys.ps1 "chippy")
(assert (= sys.ps1 "chippy"))
(.run (hy.REPL))
(rt "(+ 1 1)")
(assert (= sys.ps1 "chippy")))

(defn test-repl-input-1char [monkeypatch capsys]
(defn test-input-1char [rt]
; https://github.com/hylang/hy/issues/2430
(monkeypatch.setattr "sys.stdin" (io.StringIO "1\n"))
(.run (hy.REPL))
(assert (= (. capsys (readouterr) out) "=> 1\n=> " )))

(defn test-repl-no-shebangs [monkeypatch capsys]
(monkeypatch.setattr "sys.stdin" (io.StringIO "#!/usr/bin/env hy\n"))
(.run (hy.REPL))
(assert (in
"hy.reader.exceptions.LexException"
(. capsys (readouterr) err))))
(assert (=
(rt "1\n")
"=> 1\n=> ")))

(defn test-no-shebangs-allowed [rt]
(assert (has
(rt "#!/usr/bin/env hy\n" 'err)
"hy.reader.exceptions.LexException")))
Loading

0 comments on commit a39609d

Please sign in to comment.