From d7fafc85d724518d4fb30681db46dab6f8034eb9 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Sat, 18 Feb 2023 15:08:01 -0500 Subject: [PATCH 1/4] Minor simplification of `test_bin.py` --- tests/test_bin.py | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/tests/test_bin.py b/tests/test_bin.py index 4319bce92..d94d4b081 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -33,9 +33,9 @@ def run_cmd( else: env.pop("PYTHONDONTWRITEBYTECODE", None) - p = subprocess.Popen( - shlex.split(cmd), - stdin=subprocess.PIPE, + result = subprocess.run( + shlex.split(cmd) if isinstance(cmd, str) else cmd, + input=stdin_data, stdout=stdout, stderr=subprocess.PIPE, universal_newlines=True, @@ -43,9 +43,8 @@ def run_cmd( env=env, cwd=cwd, ) - output = p.communicate(input=stdin_data) - assert p.wait() == expect - return output + assert result.returncode == expect + return (result.stdout, result.stderr) def rm(fpath): try: @@ -351,7 +350,7 @@ def test_hyc(): assert "usage" in output path = "tests/resources/argparse_ex.hy" - _, err = run_cmd("hyc " + path) + _, err = run_cmd(["hyc", path]) assert "Compiling" in err assert os.path.exists(cache_from_source(path)) rm(cache_from_source(path)) @@ -472,7 +471,7 @@ def testc_file_sys_path(): rm(cache_from_source(test_file)) assert not os.path.exists(cache_from_source(file_relative_path)) - output, _ = run_cmd(f"{binary} {test_file}") + output, _ = run_cmd([binary, test_file]) assert repr(file_relative_path) in output @@ -500,12 +499,12 @@ def test_circular_macro_require(): test_file = "tests/resources/bin/circular_macro_require.hy" rm(cache_from_source(test_file)) assert not os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "WOWIE" # Now, with bytecode assert os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "WOWIE" @@ -519,12 +518,12 @@ def test_macro_require(): test_file = "tests/resources/bin/require_and_eval.hy" rm(cache_from_source(test_file)) assert not os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "abc" # Now, with bytecode assert os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "abc" @@ -651,11 +650,10 @@ def test_output_buffering(tmp_path): (import sys pathlib [Path]) (print :file sys.stderr (.strip (.read-text (Path #[=[{tf}]=])))) (print "line 2")''') - pf = shlex.quote(str(pf)) - for flag, expected in ("", ""), ("--unbuffered", "line 1"): + for flags, expected in ([], ""), (["--unbuffered"], "line 1"): with open(tf, "wb") as o: - _, stderr = run_cmd(f"hy {flag} {pf}", stdout=o) + _, stderr = run_cmd(["hy", *flags, pf], stdout=o) assert stderr.strip() == expected assert tf.read_text().splitlines() == ["line 1", "line 2"] @@ -671,9 +669,9 @@ def test_uufileuu(tmp_path, monkeypatch): def file_is(arg, expected_py3_9): expected = expected_py3_9 if PY3_9 and not PYPY else Path(arg) - output, _ = run_cmd("python3 " + shlex.quote(arg + "pyex.py")) + output, _ = run_cmd(["python3", arg + "pyex.py"]) assert output.rstrip() == str(expected / "pyex.py") - output, _ = run_cmd("hy " + shlex.quote(arg + "hyex.hy")) + output, _ = run_cmd(["hy", arg + "hyex.hy"]) assert output.rstrip() == str(expected / "hyex.hy") monkeypatch.chdir(tmp_path) @@ -731,15 +729,15 @@ def test_hy2py_recursive(tmp_path): (setv a 1) (setv b "hello world")""") - _, err = run_cmd(f"hy2py {(tmp_path / 'hy').as_posix()}", expect=1) + _, err = run_cmd(["hy2py", (tmp_path / 'hy')], expect=1) assert "ValueError" in err - run_cmd("hy2py " + - f"{(tmp_path / 'hy').as_posix()} " + - f"--output {(tmp_path / 'py').as_posix()}") + run_cmd(["hy2py", + (tmp_path / 'hy'), + "--output", (tmp_path / 'py')]) assert set((tmp_path / 'py').rglob('*')) == { tmp_path / 'py' / p for p in ('first.py', 'folder', 'folder/second.py')} - output, _ = run_cmd(f"python3 first.py", cwd = tmp_path / 'py') + output, _ = run_cmd("python3 first.py", cwd = tmp_path / 'py') assert output == "1\nhello world\n" From 5fb663ee5beafe0188927cf4bb23deee204bfeb4 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Sat, 18 Feb 2023 14:20:30 -0500 Subject: [PATCH 2/4] Skip shebangs in the reader, not outside it Thus the reader can properly keep track of its position in the stream. --- hy/reader/__init__.py | 9 ++------- hy/reader/hy_reader.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/hy/reader/__init__.py b/hy/reader/__init__.py index 54a856401..8744c715f 100644 --- a/hy/reader/__init__.py +++ b/hy/reader/__init__.py @@ -30,16 +30,11 @@ def read_many(stream, filename="", reader=None, skip_shebang=False): if isinstance(stream, str): stream = StringIO(stream) pos = stream.tell() - if skip_shebang: - if stream.read(2) == "#!": - stream.readline() - pos = stream.tell() - else: - stream.seek(pos) source = stream.read() stream.seek(pos) - m = hy.models.Lazy((reader or HyReader()).parse(stream, filename)) + m = hy.models.Lazy((reader or HyReader()).parse( + stream, filename, skip_shebang)) m.source = source m.filename = filename return m diff --git a/hy/reader/hy_reader.py b/hy/reader/hy_reader.py index 025adab7e..3f558b2c0 100644 --- a/hy/reader/hy_reader.py +++ b/hy/reader/hy_reader.py @@ -1,5 +1,7 @@ "Character reader for parsing Hy source." +from itertools import islice + import hy from hy.models import ( Bytes, @@ -140,7 +142,7 @@ def read_default(self, key): return self.prefixed_string('"', ident) return as_identifier(ident, reader=self) - def parse(self, stream, filename=None): + def parse(self, stream, filename=None, skip_shebang=False): """Yields all `hy.models.Object`'s in `source` Additionally exposes `self` as ``hy.&reader`` during read/compile time. @@ -151,8 +153,16 @@ def parse(self, stream, filename=None): filename (str | None): Filename to use for error messages. If `None` then previously set filename is used. + skip_shebang: + Whether to detect a skip a shebang line at the start. """ self._set_source(stream, filename) + + if skip_shebang and "".join(islice(self.peeking(), len("#!"))) == "#!": + for c in self.chars(): + if c == "\n": + break + rname = mangle("&reader") old_reader = getattr(hy, rname, None) setattr(hy, rname, self) From 5f0f808ae071fde9a2426c97c6ba7e5685716418 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Sat, 18 Feb 2023 14:36:44 -0500 Subject: [PATCH 3/4] Test shebangs --- tests/test_bin.py | 9 +++++++++ tests/test_reader.py | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_bin.py b/tests/test_bin.py index d94d4b081..04f4ac14d 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -610,6 +610,15 @@ def req_err(x): assert error_lines[-1].startswith("TypeError") +def test_traceback_shebang(tmp_path): + # https://github.com/hylang/hy/issues/2405 + (tmp_path / 'ex.hy').write_text('#!my cool shebang\n(/ 1 0)') + _, error = run_cmd(['hy', tmp_path / 'ex.hy'], expect = 1) + assert 'ZeroDivisionError' + assert 'my cool shebang' not in error + assert '(/ 1 0)' in error + + def test_hystartup(): # spy == True and custom repl-output-fn os.environ["HYSTARTUP"] = "tests/resources/hystartup.hy" diff --git a/tests/test_reader.py b/tests/test_reader.py index 0465bc00c..89a7fc7ab 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -23,8 +23,8 @@ from hy.reader.exceptions import LexException, PrematureEndOfInput -def tokenize(s): - return list(read_many(s)) +def tokenize(*args, **kwargs): + return list(read_many(*args, **kwargs)) def peoi(): @@ -675,3 +675,13 @@ def test_read_error(): assert "".join(traceback.format_exception_only(e.type, e.value)).startswith( ' File "", line 1\n (do (defn))\n ^\n' ) + + +def test_shebang(): + from hy.errors import HySyntaxError + + with pytest.raises(HySyntaxError): + # By default, `read_many` doesn't allow a shebang. + assert tokenize('#!/usr/bin/env hy\n5') + assert (tokenize('#!/usr/bin/env hy\n5', skip_shebang = True) == + [Integer(5)]) From 2173e8a8789d5e8088694a47967f6cd63d1cf724 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Sat, 18 Feb 2023 14:23:29 -0500 Subject: [PATCH 4/4] Update NEWS --- NEWS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.rst b/NEWS.rst index a7fcff32b..53ff502d2 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -11,6 +11,7 @@ Bug Fixes ------------------------------ * Fixed an installation failure in some situations when version lookup fails. +* Fixed traceback pointing in scripts with shebangs. New Features ------------------------------