Repository: hylang/hy

Issue:
Python 3.10: required field "lineno" missing from alias
Python 3.10 has introduced a change that breaks Hy (feeling like a case of "This Is The Way" with Python pre-releases :trollface:).

It seems to be related to https://bugs.python.org/issue43798 (some potentially useful discussion in https://github.com/pytest-dev/pytest/issues/8539).

Here's the full backtrace (on latest master; specifically e1150cf08106a727a1a3024044710576803a8292):

```console
$ python --version
Python 3.10.0b1
$ hy
Traceback (most recent call last):
  File "/opt/hylang/hy/hy/compiler.py", line 1648, in compile_eval_and_compile
    hy_eval(new_expr + body,
  File "/opt/hylang/hy/hy/compiler.py", line 1977, in hy_eval
    eval(ast_compile(_ast, filename, "exec"),
  File "/opt/hylang/hy/hy/compiler.py", line 62, in ast_compile
    return compile(a, filename, mode, hy_ast_compile_flags)
TypeError: required field "lineno" missing from alias

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/bin/hy", line 8, in <module>
    from hy.cmdline import hy_main
  File "/opt/hylang/hy/hy/__init__.py", line 14, in <module>
    hy.importer._inject_builtins()
  File "/opt/hylang/hy/hy/importer.py", line 178, in _inject_builtins
    core = importlib.import_module('hy.core')
  File "/usr/local/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/opt/hylang/hy/hy/core/__init__.py", line 1, in <module>
    from .shadow import *
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 879, in exec_module
  File "<frozen importlib._bootstrap_external>", line 1017, in get_code
  File "/opt/hylang/hy/hy/importer.py", line 130, in _hy_source_to_code
    data = hy_compile(hy_tree, module)
  File "/opt/hylang/hy/hy/compiler.py", line 2066, in hy_compile
    stdlib_ast = compiler.compile(mkexpr("eval-and-compile", mkexpr("import", "hy")))
  File "/opt/hylang/hy/hy/compiler.py", line 379, in compile
    raise e
  File "/opt/hylang/hy/hy/compiler.py", line 370, in compile
    ret = self.compile_atom(tree)
  File "/opt/hylang/hy/hy/compiler.py", line 364, in compile_atom
    return Result() + _model_compilers[type(atom)](self, atom)
  File "/opt/hylang/hy/hy/compiler.py", line 1730, in compile_expression
    return Result() + build_method(
  File "/opt/hylang/hy/hy/compiler.py", line 1665, in compile_eval_and_compile
    raise HyEvalError(str(e), self.filename, body, self.source)
hy.errors.HyEvalError: 
  File "[hy.models.Expression([
  hy.models.Symbol('import'),
  hy.models.Symbol('hy')])]", line 1
    ;; Copyright 2021 the authors.
    ^----------------------------^
required field "lineno" missing from alias

```

(I'm testing with the `Dockerfile` here in the repo by changing `python:3` to `python:3.10-rc`, but any way of getting a recent Python 3.10 pre-release should reproduce similarly.)


Discussion:
Unfortunately, Travis is still using 3.10.0a5 for 3.10, so it will be a while before we'll be able to test this regularly. Thanks for the heads-up.
Yeah, I figured that was the case, but wanted to make sure it was filed and on the radar (with a central place to track it). :+1: :heart:

(Any appetite for switching to GitHub Actions? :sweat_smile:)
If you mean that GitHub Actions can be used for testing, in place of Travis, then I'm not aware of that.

Resolving patch:
diff --git a/hy/_compat.py b/hy/_compat.py
--- a/hy/_compat.py
+++ b/hy/_compat.py
@@ -10,33 +10,15 @@
 PY3_10 = sys.version_info >= (3, 10)
 
 
-if PY3_9:
-
-    import ast
-    class Unparser(ast._Unparser):
-      # Work around some limitations of Python's `ast.unparse`.
-      # This will no longer be necessary with
-      # https://github.com/python/cpython/pull/24897 .
-
-        def visit_Constant(self, node):
-            if isinstance(node.value, (float, complex)):
-                self.write(repr(node.value)
-                    .replace("inf", ast._INFSTR)
-                    .replace('nan', '({}-{})'.format(ast._INFSTR, ast._INFSTR)))
-            else:
-                super().visit_Constant(node)
-
-        def visit_Set(self, node):
-            if node.elts:
-                super().visit_Set(node)
-            else:
-                self.write('{*()}')
-
-    def ast_unparse(x):
-        unparser = Unparser()
-        return unparser.visit(x)
-
-else:
-
-    import astor.code_gen
-    ast_unparse = astor.code_gen.to_source
+if not PY3_9:
+    # Shim `ast.unparse`.
+    import ast, astor.code_gen
+    ast.unparse = astor.code_gen.to_source
+
+
+if not PY3_7:
+    # Shim `asyncio.run`.
+    import asyncio
+    def f(coro):
+        return asyncio.get_event_loop().run_until_complete(coro)
+    asyncio.run = f
diff --git a/hy/cmdline.py b/hy/cmdline.py
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -37,7 +37,6 @@
 from hy.completer import completion, Completer
 from hy.macros import macro, require
 from hy.models import Expression, String, Symbol
-from hy._compat import ast_unparse
 
 
 sys.last_type = None
@@ -320,7 +319,7 @@ def ast_callback(self, exec_ast, eval_ast):
                 new_ast = ast.Module(
                     exec_ast.body + ([] if eval_ast is None else [ast.Expr(eval_ast.body)]),
                     type_ignores=[])
-                print(ast_unparse(new_ast))
+                print(ast.unparse(new_ast))
             except Exception:
                 msg = 'Exception in AST callback:\n{}\n'.format(
                     traceback.format_exc())
@@ -784,7 +783,7 @@ def hy2py_main():
         print()
 
     if not options.without_python:
-        _print_for_windows(ast_unparse(_ast))
+        _print_for_windows(ast.unparse(_ast))
 
     parser.exit(0)
 
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1125,17 +1125,19 @@ def compile_import_or_require(self, expr, root, entries):
                 level = len(ast_module) - len(module)
                 if assignments == "ALL" and prefix == "":
                     node = asty.ImportFrom
-                    names = [ast.alias(name="*", asname=None)]
+                    names = [asty.alias(entry, name="*", asname=None)]
                 elif assignments == "ALL":
                     node = asty.Import
                     prefix = mangle(prefix)
-                    names = [ast.alias(
+                    names = [asty.alias(
+                        entry,
                         name=ast_module,
                         asname=prefix if prefix != module else None)]
                 else:
                     node = asty.ImportFrom
                     names = [
-                        ast.alias(
+                        asty.alias(
+                            entry,
                             name=mangle(k),
                             asname=None if v == k else mangle(v))
                         for k, v in assignments]


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,27 @@
+name: Run tests
+
+on: [pull_request, push]
+
+jobs:
+  test:
+
+    strategy:
+      fail-fast: false
+      matrix:
+        python: [3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy-3.7]
+
+    name: ${{ matrix.python }}
+    runs-on: ubuntu-latest
+    env:
+      TERM: xterm-256color
+        # This is needed to avoid a terminfo-related crash when
+        # testing PyPy.
+
+    steps:
+    - uses: actions/checkout@v2
+    - uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python }}
+    - run: pip install -e .
+    - run: pip install pytest
+    - run: pytest
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -11,11 +11,12 @@
         [fractions [Fraction]]
         pickle
         [typing [get-type-hints List Dict]]
+        asyncio
         [hy.errors [HyLanguageError HySyntaxError]]
         pytest)
 (import sys)
 
-(import [hy._compat [PY3_8 PY3_10]])
+(import [hy._compat [PY3_8]])
 
 (defn test-sys-argv []
   "NATIVE: test sys.argv"
@@ -892,9 +893,7 @@
   (for [[k v] (.items (dict
       :p1 int  :p3 str  :o1 str  :o2 int
       :k1 str  :k2 int  :kwargs bool))]
-    (assert (=
-      (. f __annotations__ [k])
-      (if PY3_10 v.__name__ v)))))
+    (assert (is (. f __annotations__ [k]) v))))
 
 
 (defn test-return []
@@ -1836,9 +1835,6 @@ cee\"} dee" "ey bee\ncee dee"))
              ["__module__" "__qualname__" "method1" "method2"])))
 
 
-(import [asyncio [get-event-loop sleep]])
-
-
 (defn test-unpacking-pep448-1star []
   (setv l [1 2 3])
   (setv p [4 5])
@@ -1860,21 +1856,16 @@ cee\"} dee" "ey bee\ncee dee"))
   (assert (= (fun #**d1 :e "eee" #**d2) [1 2 3 4 "eee" None])))
 
 
-(defn run-coroutine [coro]
-  "Run a coroutine until its done in the default event loop."""
-  (.run_until_complete (get-event-loop) (coro)))
-
-
 (defn test-fn/a []
-  (assert (= (run-coroutine (fn/a [] (await (sleep 0)) [1 2 3]))
+  (assert (= (asyncio.run ((fn/a [] (await (asyncio.sleep 0)) [1 2 3])))
              [1 2 3])))
 
 
 (defn test-defn/a []
   (defn/a coro-test []
-    (await (sleep 0))
+    (await (asyncio.sleep 0))
     [1 2 3])
-  (assert (= (run-coroutine coro-test) [1 2 3])))
+  (assert (= (asyncio.run (coro-test)) [1 2 3])))
 
 
 (defn test-decorated-defn/a []
@@ -1882,70 +1873,70 @@ cee\"} dee" "ey bee\ncee dee"))
 
   #@(decorator
       (defn/a coro-test []
-        (await (sleep 0))
+        (await (asyncio.sleep 0))
         42))
-  (assert (= (run-coroutine coro-test) 21)))
+  (assert (= (asyncio.run (coro-test)) 21)))
 
 
 (defn test-single-with/a []
-  (run-coroutine
-    (fn/a []
+  (asyncio.run
+    ((fn/a []
       (with/a [t (AsyncWithTest 1)]
-        (assert (= t 1))))))
+        (assert (= t 1)))))))
 
 (defn test-two-with/a []
-  (run-coroutine
-    (fn/a []
+  (asyncio.run
+    ((fn/a []
       (with/a [t1 (AsyncWithTest 1)
                t2 (AsyncWithTest 2)]
         (assert (= t1 1))
-        (assert (= t2 2))))))
+        (assert (= t2 2)))))))
 
 (defn test-thrice-with/a []
-  (run-coroutine
-    (fn/a []
+  (asyncio.run
+    ((fn/a []
       (with/a [t1 (AsyncWithTest 1)
                t2 (AsyncWithTest 2)
                t3 (AsyncWithTest 3)]
         (assert (= t1 1))
         (assert (= t2 2))
-        (assert (= t3 3))))))
+        (assert (= t3 3)))))))
 
 (defn test-quince-with/a []
-  (run-coroutine
-    (fn/a []
+  (asyncio.run
+    ((fn/a []
       (with/a [t1 (AsyncWithTest 1)
                t2 (AsyncWithTest 2)
                t3 (AsyncWithTest 3)
                _ (AsyncWithTest 4)]
         (assert (= t1 1))
         (assert (= t2 2))
-        (assert (= t3 3))))))
+        (assert (= t3 3)))))))
 
 (defn test-for-async []
   (defn/a numbers []
     (for [i [1 2]]
       (yield i)))
 
-  (run-coroutine
-    (fn/a []
+  (asyncio.run
+    ((fn/a []
       (setv x 0)
       (for [:async a (numbers)]
         (setv x (+ x a)))
-      (assert (= x 3)))))
+      (assert (= x 3))))))
 
 (defn test-for-async-else []
   (defn/a numbers []
     (for [i [1 2]]
       (yield i)))
 
-  (run-coroutine
-    (fn/a []
+  (asyncio.run
+    ((fn/a []
       (setv x 0)
       (for [:async a (numbers)]
         (setv x (+ x a))
         (else (setv x (+ x 50))))
-      (assert (= x 53)))))
+      (assert (= x 53))))))
 
 (defn test-variable-annotations []
   (defclass AnnotationContainer []
diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy
--- a/tests/native_tests/native_macros.hy
+++ b/tests/native_tests/native_macros.hy
@@ -154,7 +154,6 @@
   (import ast)
   (import [hy.compiler [hy-compile]])
   (import [hy.lex [hy-parse]])
-  (import [hy._compat [ast-unparse]])
   (setv macro1 "(defmacro nif [expr pos zero neg]
       (setv g (gensym))
       `(do
@@ -169,8 +168,8 @@
   ;; gensym each time
   (setv _ast1 (hy-compile (hy-parse macro1) __name__))
   (setv _ast2 (hy-compile (hy-parse macro1) __name__))
-  (setv s1 (ast-unparse _ast1))
-  (setv s2 (ast-unparse _ast2))
+  (setv s1 (ast.unparse _ast1))
+  (setv s2 (ast.unparse _ast2))
   ;; and make sure there is something new that starts with _G\uffff
   (assert (in (mangle "_G\uffff") s1))
   (assert (in (mangle "_G\uffff") s2))
@@ -181,7 +180,6 @@
   (import ast)
   (import [hy.compiler [hy-compile]])
   (import [hy.lex [hy-parse]])
-  (import [hy._compat [ast-unparse]])
   (setv macro1 "(defmacro nif [expr pos zero neg]
       (with-gensyms [a]
         `(do
@@ -196,8 +194,8 @@
   ;; gensym each time
   (setv _ast1 (hy-compile (hy-parse macro1) __name__))
   (setv _ast2 (hy-compile (hy-parse macro1) __name__))
-  (setv s1 (ast-unparse _ast1))
-  (setv s2 (ast-unparse _ast2))
+  (setv s1 (ast.unparse _ast1))
+  (setv s2 (ast.unparse _ast2))
   (assert (in (mangle "_a\uffff") s1))
   (assert (in (mangle "_a\uffff") s2))
   (assert (not (= s1 s2))))
@@ -206,7 +204,6 @@
   (import ast)
   (import [hy.compiler [hy-compile]])
   (import [hy.lex [hy-parse]])
-  (import [hy._compat [ast-unparse]])
   (setv macro1 "(defmacro/g! nif [expr pos zero neg]
         `(do
            (setv ~g!res ~expr)
@@ -220,8 +217,8 @@
   ;; gensym each time
   (setv _ast1 (hy-compile (hy-parse macro1) __name__))
   (setv _ast2 (hy-compile (hy-parse macro1) __name__))
-  (setv s1 (ast-unparse _ast1))
-  (setv s2 (ast-unparse _ast2))
+  (setv s1 (ast.unparse _ast1))
+  (setv s2 (ast.unparse _ast2))
   (assert (in (mangle "_res\uffff") s1))
   (assert (in (mangle "_res\uffff") s2))
   (assert (not (= s1 s2)))
@@ -236,7 +233,6 @@
   (import ast)
   (import [hy.compiler [hy-compile]])
   (import [hy.lex [hy-parse]])
-  (import [hy._compat [ast-unparse]])
   (setv macro1 "(defmacro! nif [expr pos zero neg]
         `(do
            (setv ~g!res ~expr)
@@ -250,8 +246,8 @@
   ;; gensym each time
   (setv _ast1 (hy-compile (hy-parse macro1) __name__))
   (setv _ast2 (hy-compile (hy-parse macro1) __name__))
-  (setv s1 (ast-unparse _ast1))
-  (setv s2 (ast-unparse _ast2))
+  (setv s1 (ast.unparse _ast1))
+  (setv s2 (ast.unparse _ast2))
   (assert (in (mangle "_res\uffff") s1))
   (assert (in (mangle "_res\uffff") s2))
   (assert (not (= s1 s2)))
diff --git a/tests/test_hy2py.py b/tests/test_hy2py.py
--- a/tests/test_hy2py.py
+++ b/tests/test_hy2py.py
@@ -126,5 +126,4 @@ class C: pass
     assert m.pys_accum == [0, 1, 2, 3, 4]
     assert m.py_accum == "01234"
 
-    assert (asyncio.get_event_loop().run_until_complete(m.coro())
-        == list("abcdef"))
+    assert asyncio.run(m.coro()) == list("abcdef")