Repository: hylang/hy

Issue:
Mangle module names passed to `hy -m`
    $ echo '(print "hello")' > hi_ho.hy
    $ hy -m hi_ho   
    hello
    $ hy -m hi-ho
    hy: module 'hi-ho' not found.


Discussion:
Kodiologist: @allison-casey Let's wrap this up. This has gone on for nearly two months. If you think Black formatting need not be adhered to unconditionally, then explain to me where you're drawing the line and why. I'm asking for pretty occasional exceptions, so maybe we can work something out.

You've said that `fmt: off` "defeats the whole purpose of formatting to a consistent style". So if you think it'd be better to remove Black and undo all the changes it made than to ever use `fmt: off`, I'm certainly game for that.

If, on the other hand, you want to keep Black, and you still don't intend to budge an inch, now or in the future, and the only reason you didn't also stop those other PRs is because you didn't notice the spacing-fix commits in them, then I'm just going to overrule these objections. Stopping every PR with the slightest deviation from a machine-enforced whitespace style, heedless of any consequences for readability, while rejecting any possibility of compromise, all to save yourself the inconvenience of typing `black` (or rather: `perl -pi -e 's/^# fmt: off\n//m' ./**/*.py; black`), is not a responsible use of your privileges as a core developer.

I want to be clear, if there's any doubt: you're a very smart, capable, and hardworking member of Hy's development team, and I greatly value everything you've done for Hy over the past year and a half. I'm not trying to punish you. It's just that, on this oddly specific matter, you've been disruptive, in a manner that is uncharacteristic for you. You're due a lot of patience and deference, but not an infinite amount.
allison-casey: If you'd like to overrule @brandonwillard and I's approval of #2209, as well as @gilch and the community members that voiced support for it, all to save yourself the inconvenience of following basic industry best practices (including those defined by Python itself) then go ahead. But know that I'm out if you do
Kodiologist: Undoing #2209 is definitely overkill so long as there's no change in people's opinions. All I'm going to do is merge PRs that you're stopping because of whitespace.

> But know that I'm out if you do

Do you mean you're going to quit the whole project over whitespace? Seriously? Well, I'm gonna miss you a lot, but I can't make you stay if you don't want to.
paultag: I've been out of pocket due to personal life stuff, but we really need to resolve this and get everyone on the same page - I don't want to put my thumb on the scale, but if we're at an actual impasse that's going to cause people to stop working on the project, I can play that role.

I'd much rather the active committers come to consensus here, though! I don't know how we want to deal with it. I would like to be helpful, but I'd like to not over-involve myself in routine matters here.

My initial thought is to have @allison-casey and @Kodiologist iron this out between the two of you in some way that's most productive, and set the standard on what  commits going forward look like. If that's not going well / stuck at an unresolvable deadlock, I can weigh in.
Kodiologist: Yes, to be clear, if there's some sort of compromise we can come to, rather than me applying brute force and Allie quitting the project, that would be a much better outcome.
lafrenierejm: I had raised this [before](https://github.com/hylang/hy/pull/2221#discussion_r810558135), but only ever saw [a response from @Kodiologist](https://github.com/hylang/hy/pull/2221#discussion_r810558496). As a possible point of compromise, maybe it's worth a larger discussion?

> What are both of your opinions on delimiting individual blocks with fmt: {off,on}? Obviously that's more verbose than either a file-wide fmt: off or giving up manual formatting entirely, but maybe would it be an acceptable compromise for now? See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html?highlight=skip#code-style for the relevant docs.
Kodiologist: Yeah, still seems better than nothing to me.
allison-casey: There's very little point to having black if it's going to be incrementally chipped away by chunks of `fmt: off/on` that i'd inevitably undo the next time i touched that code. Those chunks would have to be manually fixed regardless if flake8 or other code quality checks were ever introduced anyways as they already fail as they are. Either we use it or we don't. 
Kodiologist: So then let's not have Black. But you're also against that. So, if there's anything you would accept short of 100% fealty to the Dark One, I can't figure out what it is.
lafrenierejm: Short preface:
1. I acknowledge that I have very little investment in Hy relative to most here.
1. I _do_ find value in using Black.
My intent is not to invalidate anyone's feelings.

> There's very little point to having black if it's going to be incrementally chipped away by chunks of `fmt: off/on`

IMO this feels like a straw man. I don't think it's necessarily the case that all benefits of Black would be lost by allowing blocks of hand-formatted code. My impression is that "even" Kodi (using @Kodiologist as an example because so far they have expressed the most discomfort with Black) is not 100% opposed to Black's formatting. I agree that the benefits of using an auto-formatter would be reduced by allowing hand-formatted blocks, but IMO auto-formatting should ultimately exist to serve current and future contributors rather than as an end in itself. That said, would it at least be worth experimenting with allowing per-block formatting before anyone here decides they need to step away from the project?
Kodiologist: > My impression is that "even" Kodi … is not 100% opposed to Black's formatting.

For sure. I don't think autoformatting is a good idea, outside of desperate cases like deobfuscation, but a lot of Black's changes to this codebase were either innocuous or improvements to what was there before. The trick is the small minority of cases where its choices are not so great. And I don't think those cases arise because the Black style is a bad one overall, but because Black is a computer program and necessarily can't capture a human's understanding of code.
scauligi: > delimiting individual blocks with `fmt: {off,on}`?

My worry with this is with logistics and the inevitable gray areas of both review and later editing.

Suppose, e.g., that a poorly stylistic developer submits a PR to Hy that includes a *clearly* badly formatted block, but enclosed with `fmt: off`.
I think consensus would definitely be to request the removal of the formatting comments and reformat.

But now suppose a more stylistically aware developer submits a PR with a maybe-or-maybe-not well-formatted block, enclosed with `fmt: off`. Do we reject? Accept with a warning? Where do we draw the line? Is it just up to whichever core developers respond to the PR? What if there's disagreement among them?

Suppose there's a code block already in the codebase delimited by `fmt: off`, and I happen to edit some part of it in a PR. If I remove the `fmt: off` comments because I think black's formatting looks better, will my PR be rejected until I restore the original formatting? Should I match the existing formatting if I add to the same code block even if I disagree with its style? What if I add a different code block elsewhere in the file, but it has similar or analagous code to the custom-styled one? Should I match the custom style to keep it consistent?

If we're going to allow `fmt: off` blocks, then I feel we would need a very strict set of guidelines/rules for these kinds of cases to avoid this sort of spiraling discussion again.

---

My personal preference is black everywhere, as I find it quite reasonable. E.g., I much prefer the black-formatted version of the argparse commands in this PR, because it's a lot easier for me to see each keyword argument.

I'm very cautiously open to allowing `fmt: off` as subjectively desired for *data* (array/dict constants, test data, etc.) but I do prefer auto formatting for all code.
Kodiologist: I think we can handle it the same way we handled it in the pre-Black days. If you're reviewing a PR and you think the whitespace is unreasonable, then negotiate with the PR author. If you're writing a PR, do whatever makes sense to you and see if anybody objects. Somehow, as a community, we managed to be level-headed and proportionate about whitespace for a little under a decade. We can do it again. The trick to it isn't "a very strict set of guidelines/rules" but an interest in getting things done.

> Suppose, e.g., that a poorly stylistic developer submits a PR to Hy that includes a *clearly* badly formatted block

For what it's worth, I don't think I've ever seen this happen. Probably because the kind of programmer who submits PRs to a Lisp implementation is the not the kind of programmer who's desperately banging out a guess-the-number program for Computer Science 101.
allison-casey: django, scikit-learn, and pandas all use black. Between their _million_ lines of code between them, `fmt: off` shows up exactly _zero_ times. Hy is not special. There's nothing we've done that is different or new from what the Python community has broadly accepted black capable of doing. 

I'm with scauligi. I much prefer the black formatted versions of this PR as well as all the PRs kodi has turned black off in. 
Kodiologist: That would set a precedent that if a core developer is unhappy with something, they can refuse all attempts to reason with them and threaten to resign. What if I threatened to resign? I'm not going to pretend I want to resign, because I don't, least of all over not getting my way with respect to whitespace, but I'm trying to bend over backwards to accommodate what I thought from the beginning was a really bad idea, and I'm getting zero consideration.

I've said it before and I'll say it again: if you can't stand seeing any deviation from the Black style, then just run `black` locally. I get to see code as it was intended to be formatted by a human, you get to see the Umbral Entity's standard, and everyone's happy. If this seems like too much of an inconvenience to help me read the codebase I've been formatting with tender loving care for the last 6 years, then… I don't know what to say beyond that. I will simply be sad. And continue to be sad as Black messes up my formatting every day for the subsequent 6 years.
paultag: @Kodiologist the precedent I'm committing to here is that there was a deadlock in our normal process. I put on the BDFL hat and came in over the top with an edict in order to break the stalemate according to my own judgement. 

I don't like doing it - and it's perhaps a worse precedent, but I don't see this thread resolving itself 
Kodiologist: At least we can put this behind us. I'll add some blackening commits and merge these hoary PRs later today.

Patch:
diff --git a/NEWS.rst b/NEWS.rst
index 548f4e3d..37a1bb33 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -29,6 +29,7 @@ Bug Fixes
 * Fixed a scoping bug in comprehensions in `let` bodies.
 * Tab completion in the Hy REPL now properly unmangles symbol names.
 * `!=` with model objects is now consistent with `=`.
+* Module names supplied to `hy -m` are now mangled.
 
 New Features
 ------------------------------
@@ -40,6 +41,7 @@ New Features
   collected by `(require module *)`
 * New macro `export`
 * new function `hy.model_patterns.parse_if`
+* Added a command-line option `-u` (or `--unbuffered`) per CPython.
 
 .. _bpo-2675: https://bugs.python.org/issue2675#msg265564
 .. _bpo-46520: https://bugs.python.org/issue46520
diff --git a/hy/cmdline.py b/hy/cmdline.py
index 93c8e36f..8f3bd431 100644
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -5,6 +5,7 @@ import code
 import codeop
 import hashlib
 import importlib
+import io
 import linecache
 import os
 import py_compile
@@ -499,35 +500,42 @@ def cmdline_handler(scriptname, argv):
 
     defs = [
         dict(
-            name=["-h", "--help"], action="help", help="show this help message and exit"
+            name=["-B"],
+            action="store_true",
+            help="don't write .py[co] files on import; also PYTHONDONTWRITEBYTECODE=x",
         ),
         dict(
             name=["-c"],
             dest="command",
             terminate=True,
-            help="program passed in as a string",
-        ),
-        dict(
-            name=["-m"],
-            dest="mod",
-            terminate=True,
-            help="module to run, passed in as a string",
+            help="program passed in as string",
         ),
         dict(
             name=["-E"],
             action="store_true",
-            help="ignore PYTHON* environment variables",
+            help="ignore PYTHON* environment variables (such as PYTHONPATH)",
         ),
         dict(
-            name=["-B"],
-            action="store_true",
-            help="don't write .py[co] files on import; also PYTHONDONTWRITEBYTECODE=x",
+            name=["-h", "--help"],
+            action="help",
+            help="print this help message and exit",
         ),
         dict(
             name=["-i"],
             dest="icommand",
             terminate=True,
-            help="program passed in as a string, then stay in REPL",
+            help="program passed in as string, then stay in REPL",
+        ),
+        dict(
+            name=["-m"],
+            dest="mod",
+            terminate=True,
+            help="run library module as a script",
+        ),
+        dict(
+            name=["--repl-output-fn"],
+            dest="repl_output_fn",
+            help="function for printing REPL output (e.g., repr)",
         ),
         dict(
             name=["--spy"],
@@ -535,15 +543,14 @@ def cmdline_handler(scriptname, argv):
             help="print equivalent Python code before executing",
         ),
         dict(
-            name=["--repl-output-fn"],
-            dest="repl_output_fn",
-            help="function for printing REPL output "
-            "(e.g., hy.contrib.hy-repr.hy-repr)",
+            name=["-u", "--unbuffered"],
+            action="store_true",
+            help="force the stdout and stderr streams to be unbuffered; this option has no effect on stdin; also PYTHONUNBUFFERED=x",
         ),
         dict(
             name=["-v", "--version"],
             action="version",
-            help="show program's version number and exit",
+            help="print the Hy version number and exit",
         ),
     ]
 
@@ -613,6 +620,16 @@ def cmdline_handler(scriptname, argv):
     if "B" in options:
         sys.dont_write_bytecode = True
 
+    if "unbuffered" in options:
+        for k in "stdout", "stderr":
+            setattr(
+                sys,
+                k,
+                io.TextIOWrapper(
+                    open(getattr(sys, k).fileno(), "wb", 0), write_through=True
+                ),
+            )
+
     if "help" in options:
         print("usage:", USAGE)
         print("")
@@ -640,7 +657,7 @@ def cmdline_handler(scriptname, argv):
     if "mod" in options:
         set_path("")
         sys.argv = [program] + argv
-        runpy.run_module(options["mod"], run_name="__main__", alter_sys=True)
+        runpy.run_module(hy.mangle(options["mod"]), run_name="__main__", alter_sys=True)
         return 0
 
     if "icommand" in options:


Test patch:
diff --git a/tests/test_bin.py b/tests/test_bin.py
index 00572ca9..11ce1350 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -10,26 +10,24 @@ from importlib.util import cache_from_source
 
 import pytest
 
-hy_dir = os.environ.get("HY_DIR", "")
-
 
 def pyr(s=""):
     return "hy --repl-output-fn=repr " + s
 
 
-def run_cmd(cmd, stdin_data=None, expect=0, dontwritebytecode=False):
+def run_cmd(
+        cmd, stdin_data=None, expect=0, dontwritebytecode=False,
+        stdout=subprocess.PIPE):
     env = dict(os.environ)
     if dontwritebytecode:
         env["PYTHONDONTWRITEBYTECODE"] = "1"
     else:
         env.pop("PYTHONDONTWRITEBYTECODE", None)
 
-    cmd = shlex.split(cmd)
-    cmd[0] = os.path.join(hy_dir, cmd[0])
     p = subprocess.Popen(
-        cmd,
+        shlex.split(cmd),
         stdin=subprocess.PIPE,
-        stdout=subprocess.PIPE,
+        stdout=stdout,
         stderr=subprocess.PIPE,
         universal_newlines=True,
         shell=False,
@@ -49,11 +47,11 @@ def rm(fpath):
             pass
 
 
-def test_bin_hy():
+def test_simple():
     run_cmd("hy", "")
 
 
-def test_bin_hy_stdin():
+def test_stdin():
     output, _ = run_cmd("hy", '(.upper "hello")')
     assert "HELLO" in output
 
@@ -66,12 +64,12 @@ def test_bin_hy_stdin():
     assert "foof()" in output
 
 
-def test_bin_hy_stdin_multiline():
+def test_stdin_multiline():
     output, _ = run_cmd("hy", '(+ "a" "b"\n"c" "d")')
     assert '"abcd"' in output
 
 
-def test_bin_hy_history():
+def test_history():
     output, _ = run_cmd("hy", '''(+ "a" "b")
                                  (+ "c" "d")
                                  (+ "e" "f")
@@ -83,7 +81,7 @@ def test_bin_hy_history():
     assert '"err: TEST ERROR"' in output
 
 
-def test_bin_hy_stdin_comments():
+def test_stdin_comments():
     _, err_empty = run_cmd("hy", "")
 
     output, err = run_cmd("hy", '(+ "a" "b") ; "c"')
@@ -94,7 +92,7 @@ def test_bin_hy_stdin_comments():
     assert err == err_empty
 
 
-def test_bin_hy_stdin_assignment():
+def test_stdin_assignment():
     # If the last form is an assignment, don't print the value.
 
     output, _ = run_cmd("hy", '(setv x (+ "A" "Z"))')
@@ -109,7 +107,7 @@ def test_bin_hy_stdin_assignment():
     assert "BY" not in output
 
 
-def test_bin_hy_multi_setv():
+def test_multi_setv():
     # https://github.com/hylang/hy/issues/1255
     output, _ = run_cmd("hy", """(do
       (setv  it 0  it (+ it 1)  it (+ it 1))
@@ -117,7 +115,7 @@ def test_bin_hy_multi_setv():
     assert re.match(r"=>\s+2\s+=>", output)
 
 
-def test_bin_hy_stdin_error_underline_alignment():
+def test_stdin_error_underline_alignment():
     _, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)")
 
     msg_idx = err.rindex("    (mabcdefghi)")
@@ -134,7 +132,7 @@ def test_bin_hy_stdin_error_underline_alignment():
     )
 
 
-def test_bin_hy_stdin_except_do():
+def test_stdin_except_do():
     # https://github.com/hylang/hy/issues/533
 
     output, _ = run_cmd("hy",
@@ -154,7 +152,7 @@ def test_bin_hy_stdin_except_do():
     assert "zzz" in output
 
 
-def test_bin_hy_stdin_unlocatable_hytypeerror():
+def test_stdin_unlocatable_hytypeerror():
     # https://github.com/hylang/hy/issues/1412
     # The chief test of interest here is the returncode assertion
     # inside run_cmd.
@@ -164,7 +162,7 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
     assert "AZ" in err
 
 
-def test_bin_hy_error_parts_length():
+def test_error_parts_length():
     """Confirm that exception messages print arrows surrounding the affected
     expression."""
     prg_str = """
@@ -223,7 +221,7 @@ def test_bin_hy_error_parts_length():
     assert err_parts[2] == "    ^----^"
 
 
-def test_bin_hy_syntax_errors():
+def test_syntax_errors():
     # https://github.com/hylang/hy/issues/2004
     _, err = run_cmd("hy", "(defn foo [/])\n(defn bar [a a])")
     assert "SyntaxError: duplicate argument" in err
@@ -234,7 +232,7 @@ def test_bin_hy_syntax_errors():
     assert "PrematureEndOfInput" not in err
 
 
-def test_bin_hy_stdin_bad_repr():
+def test_stdin_bad_repr():
     # https://github.com/hylang/hy/issues/1389
     output, err = run_cmd("hy", """
          (defclass BadRepr [] (defn __repr__ [self] (/ 0)))
@@ -244,7 +242,7 @@ def test_bin_hy_stdin_bad_repr():
     assert "AZ" in output
 
 
-def test_bin_hy_stdin_py_repr():
+def test_stdin_py_repr():
     output, _ = run_cmd("hy", "(+ [1] [2])")
     assert "[1 2]" in output
 
@@ -260,7 +258,17 @@ def test_bin_hy_stdin_py_repr():
     assert "[1]+[2]" in output.replace(" ", "")
 
 
-def test_bin_hy_ignore_python_env():
+def test_mangle_m():
+    # https://github.com/hylang/hy/issues/1445
+
+    output, _ = run_cmd("hy -m tests.resources.hello_world")
+    assert "hello world" in output
+
+    output, _ = run_cmd("hy -m tests.resources.hello-world")
+    assert "hello world" in output
+
+
+def test_ignore_python_env():
     os.environ.update({"PYTHONTEST": "0"})
     output, _ = run_cmd("hy -c '(print (do (import os) (. os environ)))'")
     assert "PYTHONTEST" in output
@@ -279,7 +287,7 @@ def test_bin_hy_ignore_python_env():
     assert "PYTHONTEST" not in output
 
 
-def test_bin_hy_cmd():
+def test_cmd():
     output, _ = run_cmd("""hy -c '(print (.upper "hello"))'""")
     assert "HELLO" in output
 
@@ -299,28 +307,28 @@ def test_bin_hy_cmd():
     assert "<-c|AA|ZZ|-m>" in output
 
 
-def test_bin_hy_icmd():
+def test_icmd():
     output, _ = run_cmd("""hy -i '(.upper "hello")'""", '(.upper "bye")')
     assert "HELLO" in output
     assert "BYE" in output
 
 
-def test_bin_hy_icmd_file():
+def test_icmd_file():
     output, _ = run_cmd("hy -i tests/resources/icmd_test_file.hy", '(.upper species)')
     assert "CUTTLEFISH" in output
 
 
-def test_bin_hy_icmd_and_spy():
+def test_icmd_and_spy():
     output, _ = run_cmd('hy --spy -i "(+ [] [])"', "(+ 1 1)")
     assert "[] + []" in output
 
 
-def test_bin_hy_missing_file():
+def test_missing_file():
     _, err = run_cmd("hy foobarbaz", expect=2)
     assert "No such file" in err
 
 
-def test_bin_hy_file_with_args():
+def test_file_with_args():
     cmd = "hy tests/resources/argparse_ex.hy"
     assert "usage" in run_cmd(f"{cmd} -h")[0]
     assert "got c" in run_cmd(f"{cmd} -c bar")[0]
@@ -328,7 +336,7 @@ def test_bin_hy_file_with_args():
     assert "foo" in run_cmd(f"{cmd} -i foo -c bar")[0]
 
 
-def test_bin_hyc():
+def test_hyc():
     _, err = run_cmd("hyc", expect=0)
     assert err == ""
 
@@ -345,12 +353,12 @@ def test_bin_hyc():
     rm(cache_from_source(path))
 
 
-def test_bin_hyc_missing_file():
+def test_hyc_missing_file():
     _, err = run_cmd("hyc foobarbaz", expect=1)
     assert "[Errno 2]" in err
 
 
-def test_bin_hy_builtins():
+def test_builtins():
     # The REPL replaces `builtins.help` etc.
 
     output, _ = run_cmd("hy", 'quit')
@@ -367,7 +375,7 @@ def test_bin_hy_builtins():
     assert "help(object)" in str(builtins.help)
 
 
-def test_bin_hy_no_main():
+def test_no_main():
     output, _ = run_cmd("hy tests/resources/bin/nomain.hy")
     assert "This Should Still Work" in output
 
@@ -383,7 +391,7 @@ def test_bin_hy_no_main():
         ["hy", "-c", "'(import {modname})'"],
     ],
 )
-def test_bin_hy_byte_compile(scenario, cmd_fmt):
+def test_byte_compile(scenario, cmd_fmt):
 
     modname = "tests.resources.bin.bytecompile"
     fpath = modname.replace(".", "/") + ".hy"
@@ -421,19 +429,19 @@ def test_bin_hy_byte_compile(scenario, cmd_fmt):
     assert "The macro returned: boink" in output
 
 
-def test_bin_hy_module_main_file():
+def test_module_main_file():
     output, _ = run_cmd("hy -m tests.resources.bin")
     assert "This is a __main__.hy" in output
 
     output, _ = run_cmd("hy -m .tests.resources.bin", expect=1)
 
 
-def test_bin_hy_file_main_file():
+def test_file_main_file():
     output, _ = run_cmd("hy tests/resources/bin")
     assert "This is a __main__.hy" in output
 
 
-def test_bin_hy_file_sys_path():
+def test_file_sys_path():
     """The test resource `relative_import.hy` will perform an absolute import
     of a module in its directory: a directory that is not on the `sys.path` of
     the script executing the module (i.e. `hy`).  We want to make sure that Hy
@@ -447,8 +455,8 @@ def test_bin_hy_file_sys_path():
     assert repr(file_relative_path) in output
 
 
-def test_bin_hyc_file_sys_path():
-    # similar to test_bin_hy_file_sys_path, test hyc and hy2py to make sure
+def testc_file_sys_path():
+    # similar to test_file_sys_path, test hyc and hy2py to make sure
     # they can find the relative import at compile time
     # https://github.com/hylang/hy/issues/2021
 
@@ -464,23 +472,23 @@ def test_bin_hyc_file_sys_path():
         assert repr(file_relative_path) in output
 
 
-def test_bin_hy_module_no_main():
+def test_module_no_main():
     output, _ = run_cmd("hy -m tests.resources.bin.nomain")
     assert "This Should Still Work" in output
 
 
-def test_bin_hy_sys_executable():
+def test_sys_executable():
     output, _ = run_cmd("hy -c '(do (import sys) (print sys.executable))'")
     assert os.path.basename(output.strip()) == "hy"
 
 
-def test_bin_hy_file_no_extension():
+def test_file_no_extension():
     """Confirm that a file with no extension is processed as Hy source"""
     output, _ = run_cmd("hy tests/resources/no_extension")
     assert "This Should Still Work" in output
 
 
-def test_bin_hy_circular_macro_require():
+def test_circular_macro_require():
     """Confirm that macros can require themselves during expansion and when
     run from the command line."""
 
@@ -497,7 +505,7 @@ def test_bin_hy_circular_macro_require():
     assert output.strip() == "WOWIE"
 
 
-def test_bin_hy_macro_require():
+def test_macro_require():
     """Confirm that a `require` will load macros into the non-module namespace
     (i.e. `exec(code, locals)`) used by `runpy.run_path`.
     In other words, this confirms that the AST generated for a `require` will
@@ -516,7 +524,7 @@ def test_bin_hy_macro_require():
     assert output.strip() == "abc"
 
 
-def test_bin_hy_tracebacks():
+def test_tracebacks():
     """Make sure the printed tracebacks are correct."""
 
     # We want the filtered tracebacks.
@@ -623,3 +631,21 @@ def test_hystartup():
     assert "[1,~2]" in output
 
     del os.environ["HYSTARTUP"]
+
+
+def test_output_buffering(tmp_path):
+    tf = tmp_path / "file.txt"
+
+    pf = tmp_path / "program.hy"
+    pf.write_text(f'''
+        (print "line 1")
+        (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"):
+        with open(tf, "wb") as o:
+            _, stderr = run_cmd(f"hy {flag} {pf}", stdout=o)
+        assert stderr.strip() == expected
+        assert tf.read_text().splitlines() == ["line 1", "line 2"]