Repository: hylang/hy

Issue:
On Pythons too old to support a construct, create a degenerate special operator
As I implemented it, the `special` decorator in `compiler.py` can check the current version of Python, and if it fails, no special operator is created. For example, the use of `@special((PY3_8, "setx"), …)` means that on Python 3.7, `setx` is an ordinary name. This leads to unhelpful error messages, and it's confusing that the set of special operator names changes depending on the version of Python. Instead, when the version check fails, `@special` should create a degenerate special operator that unconditionally fails at compile-time with an error message saying "setx isn't supported on this version of Python" or something.


Discussion:

Resolving patch:
diff --git a/conftest.py b/conftest.py
--- a/conftest.py
+++ b/conftest.py
@@ -1,5 +1,8 @@
+import sys
 import os
 import importlib
+from operator import or_
+from functools import reduce
 
 import py
 import pytest
@@ -15,7 +18,15 @@
 
 
 def pytest_ignore_collect(path, config):
-    return (("py3_8_only" in path.basename and not PY3_8) or None)
+    versions = [
+        (sys.version_info < (3, 8), "sub_py3_7_only"),
+        (PY3_8, "py3_8_only"),
+    ]
+
+    return reduce(
+        or_,
+        (name in path.basename and not condition for condition, name in versions),
+    )
 
 
 def pyimport_patch_mismatch(self, **kwargs):
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -102,6 +102,15 @@ def calling_module(n=1):
 _bad_roots = tuple(mangle(x) for x in (
     "unquote", "unquote-splice", "unpack-mapping", "except"))
 
+def unsupported_special_form_fn(min_version):
+    major, minor = min_version
+    def fn(compiler, expr, root, *args):
+        raise compiler._syntax_error(
+            expr,
+            f"{root} requires Python {major}.{minor} or later",
+        )
+
+    return fn
 
 def special(names, pattern):
     """Declare special operators. The decorated method and the given pattern
@@ -110,8 +119,12 @@ def special(names, pattern):
     def dec(fn):
         for name in names if isinstance(names, list) else [names]:
             if isinstance(name, tuple):
-                condition, name = name
-                if not condition:
+                min_version, name = name
+                if sys.version_info < min_version:
+                    _special_form_compilers[mangle(name)] = (
+                        unsupported_special_form_fn(min_version),
+                        many(FORM),
+                    )
                     continue
             _special_form_compilers[mangle(name)] = (fn, pattern)
         return fn
@@ -1325,7 +1338,7 @@ def compile_augassign_expression(self, expr, root, target, values):
             expr, target=target, value=ret.force_expr, op=op())
 
     @special("setv", [many(OPTIONAL_ANNOTATION + FORM + FORM)])
-    @special((PY3_8, "setx"), [times(1, 1, SYM + FORM)])
+    @special(((3, 8), "setx"), [times(1, 1, SYM + FORM)])
     def compile_def_expression(self, expr, root, decls):
         if not decls:
             return asty.Constant(expr, value=None)


Test patch:
diff --git a/tests/native_tests/sub_py3_7_only.hy b/tests/native_tests/sub_py3_7_only.hy
new file mode 100644
--- /dev/null
+++ b/tests/native_tests/sub_py3_7_only.hy
@@ -0,0 +1,13 @@
+;; Copyright 2021 the authors.
+;; This file is part of Hy, which is free software licensed under the Expat
+;; license. See the LICENSE.
+
+;; Tests where the emitted code relies on Python ≥3.8.
+;; conftest.py skips this file when running on Python <3.8.
+
+(import pytest)
+
+(defn test-setx []
+  (with [e (pytest.raises hy.errors.HySyntaxError)]
+    (hy.eval '(setx x 1)))
+  (assert (= "setx requires Python 3.8 or later")))