diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4e5694cdc..36c0080bb 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,7 +12,7 @@ jobs:
matrix:
name-prefix: ['']
os: [ubuntu-latest]
- python: [3.8, 3.9, '3.10', 3.11, 3.12, pypy-3.10, pyodide]
+ python: [3.8, 3.9, '3.10', 3.11, 3.12, 3.13-dev, pypy-3.10, pyodide]
include:
# To keep the overall number of runs low, we test Windows and MacOS
# only on the latest CPython.
diff --git a/NEWS.rst b/NEWS.rst
index 2f425a42b..4d6eacd08 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -3,6 +3,10 @@
Unreleased
=============================
+New Features
+------------------------------
+* Python 3.13 is now supported.
+
Bug Fixes
------------------------------
* Fixed a crash on Python 3.12.6.
diff --git a/hy/compat.py b/hy/compat.py
index a77f4834d..481cbad97 100644
--- a/hy/compat.py
+++ b/hy/compat.py
@@ -7,6 +7,7 @@
PY3_11 = sys.version_info >= (3, 11)
PY3_12 = sys.version_info >= (3, 12)
PY3_12_6 = sys.version_info >= (3, 12, 6)
+PY3_13 = sys.version_info >= (3, 13)
PYPY = platform.python_implementation() == "PyPy"
PYODIDE = platform.system() == "Emscripten"
@@ -69,3 +70,8 @@ def getdoc(object):
result = inspect.getcomments(object)
return result and re.sub('^ *\n', '', result.rstrip()) or ''
pydoc.getdoc = getdoc
+
+
+def reu(x):
+ '(R)eplace an (e)rror (u)nderline. This is only used for testing Hy.'
+ return x.replace('-', '^') if PY3_13 else x
diff --git a/hy/compiler.py b/hy/compiler.py
index 0800f087a..94d7997dd 100755
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -698,7 +698,10 @@ def compile_fstring(self, fstring):
def compile_list(self, expression):
elts, ret, _ = self._compile_collect(expression)
node = {List: asty.List, Set: asty.Set}[type(expression)]
- return ret + node(expression, elts=elts, ctx=ast.Load())
+ return ret + node(
+ expression,
+ elts = elts,
+ **({} if node is asty.Set else dict(ctx = ast.Load())))
@builds_model(Dict)
def compile_dict(self, m):
@@ -911,7 +914,9 @@ def hy_compile(
body.append(ast.fix_missing_locations(ast.Import([ast.alias("hy", None)])))
body += result.stmts
- ret = root(body=body, type_ignores=[])
+ ret = root(
+ body = body,
+ **({} if root is ast.Interactive else dict(type_ignores = [])))
if get_expr:
expr = ast.Expression(body=expr)
diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py
index dc57a8f22..573e8167f 100644
--- a/hy/core/result_macros.py
+++ b/hy/core/result_macros.py
@@ -528,6 +528,7 @@ def compile_assign(
ann_result = compiler.compile(ann)
result = ann_result + result
+ target = dict(target = st_name)
if is_assignment_expr:
node = asty.NamedExpr
elif ann is not None:
@@ -539,12 +540,12 @@ def compile_assign(
)
else:
node = asty.Assign
+ target = dict(targets = [st_name])
result += node(
name if hasattr(name, "start_line") else result,
value=result.force_expr if not annotate_only else None,
- target=st_name,
- targets=[st_name],
+ **target
)
return result
@@ -1137,7 +1138,7 @@ def compile_with_expression(compiler, expr, root, args, body):
else compiler._storeize(variable, compiler.compile(variable))
)
items.append(
- asty.withitem(expr, context_expr=ctx.force_expr, optional_vars=variable)
+ ast.withitem(context_expr=ctx.force_expr, optional_vars=variable)
)
if not cbody:
@@ -1211,7 +1212,6 @@ def compile_match_expression(compiler, expr, root, subject, clauses):
name=fname,
args=ast.arguments(
args=[],
- varargs=None,
kwarg=None,
posonlyargs=[],
kwonlyargs=[],
@@ -1356,9 +1356,7 @@ def compile_raise_expression(compiler, expr, root, exc, cause):
ret += cause
cause = cause.force_expr
- return ret + asty.Raise(
- expr, type=ret.expr, exc=exc, inst=None, tback=None, cause=cause
- )
+ return ret + asty.Raise(expr, exc=exc, cause=cause)
@pattern_macro(
@@ -1780,8 +1778,6 @@ def compile_class_expression(compiler, expr, root, decorators, tp, name, rest):
decorator_list=decorators,
name=name,
keywords=keywords,
- starargs=None,
- kwargs=None,
bases=bases_expr,
body=bodyr.stmts or [asty.Pass(expr)],
**digest_type_params(compiler, tp)
@@ -1965,35 +1961,34 @@ def compile_import(compiler, expr, root, entries):
elif assignments == "EXPORTS":
compiler.scope.define(prefix)
node = asty.Import
- names = [
- asty.alias(
- module,
- name=module_name,
- asname=prefix if prefix != module_name else None,
- )
- ]
+ names = [asty.alias(
+ module,
+ name = module_name,
+ asname = prefix if prefix != module_name else None)]
else:
node = asty.ImportFrom
names = []
for k, v in assignments:
compiler.scope.define(mangle(v))
- names.append(
- asty.alias(
- module, name=mangle(k), asname=None if v == k else mangle(v)
- )
- )
+ names.append(asty.alias(
+ module,
+ name = mangle(k),
+ asname = None if v == k else mangle(v)))
ret += node(
expr,
- module=module_name if module_name and module_name.strip(".") else None,
- names=names,
- level=(
- len(module[0])
- if isinstance(module, Expression) and module[1][0] == Symbol("None")
- else len(module)
- if isinstance(module, Symbol) and not module.strip(".")
- else 0
- ),
- )
+ names = names,
+ **({} if node is asty.Import else dict(
+ module = module_name
+ if module_name and module_name.strip(".")
+ else None,
+ level =
+ len(module[0])
+ if isinstance(module, Expression)
+ and module[1][0] == Symbol("None")
+ else len(module)
+ if isinstance(module, Symbol)
+ and not module.strip(".")
+ else 0)))
return ret
diff --git a/hy/errors.py b/hy/errors.py
index d5d7ae106..732a9133e 100644
--- a/hy/errors.py
+++ b/hy/errors.py
@@ -6,7 +6,7 @@
from contextlib import contextmanager
from hy import _initialize_env_var
-from hy.compat import PYPY
+from hy.compat import PYPY, PY3_13
_hy_show_internal_errors = _initialize_env_var("HY_SHOW_INTERNAL_ERRORS", False)
@@ -113,7 +113,7 @@ def __str__(self):
)
arrow_idx, _ = next(
- ((i, x) for i, x in enumerate(output) if x.strip() == "^"), (None, None)
+ ((i, x) for i, x in enumerate(output) if set(x.strip()) == {"^"}), (None, None)
)
if arrow_idx:
msg_idx = arrow_idx + 1
@@ -125,8 +125,10 @@ def __str__(self):
# Get rid of erroneous error-type label.
output[msg_idx] = re.sub("^SyntaxError: ", "", output[msg_idx])
- # Extend the text arrow, when given enough source info.
- if arrow_idx and self.arrow_offset:
+ # Extend the text arrow, when given enough source info. We
+ # don't do this on newer Pythons because they make their own
+ # underlines.
+ if arrow_idx and self.arrow_offset and not PY3_13:
output[arrow_idx] = "{}{}^\n".format(
output[arrow_idx].rstrip("\n"), "-" * (self.arrow_offset - 1)
)
diff --git a/hy/repl.py b/hy/repl.py
index c11c66642..ebced9022 100644
--- a/hy/repl.py
+++ b/hy/repl.py
@@ -126,7 +126,10 @@ def _update_exc_info(self):
sys.last_traceback = getattr(sys.last_traceback, "tb_next", sys.last_traceback)
self.locals["_hy_last_traceback"] = sys.last_traceback
- def __call__(self, source, filename="", symbol="single"):
+ def __call__(self, source, filename="", symbol=None):
+ symbol = "exec"
+ # This parameter is required by `codeop.Compile`, but we
+ # ignore it in favor of always using "exec".
hash_digest = hashlib.sha1(source.encode("utf-8").strip()).hexdigest()
name = "{}-{}".format(filename.strip("<>"), hash_digest)
@@ -134,8 +137,6 @@ def __call__(self, source, filename="", symbol="single"):
self._cache(source, name)
try:
- root_ast = ast.Interactive if symbol == "single" else ast.Module
-
# Our compiler doesn't correspond to a real, fixed source file, so
# we need to [re]set these.
self.hy_compiler.filename = name
@@ -148,7 +149,7 @@ def __call__(self, source, filename="", symbol="single"):
exec_ast, eval_ast = hy_compile(
hy_ast,
self.module,
- root=root_ast,
+ root=ast.Module,
get_expr=True,
compiler=self.hy_compiler,
filename=name,
@@ -342,7 +343,7 @@ def _error_wrap(self, exc_info_override=False, *args, **kwargs):
self.locals[mangle("*e")] = sys.last_value
- def showsyntaxerror(self, filename=None):
+ def showsyntaxerror(self, filename=None, source=None):
if filename is None:
filename = self.filename
self.print_last_value = False
diff --git a/setup.py b/setup.py
index 444712c9d..93360e758 100755
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,7 @@ def run(self):
version='0.0.0',
setup_requires=["wheel"] + requires,
install_requires=requires,
- python_requires=">= 3.8, < 3.13",
+ python_requires=">= 3.8, < 3.14",
entry_points={
"console_scripts": [
"hy = hy.cmdline:hy_main",
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
index 351467a8d..6e266f7d4 100644
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -316,8 +316,6 @@ def test_ast_expression_basics():
),
args=[ast.Name(id="bar", ctx=ast.Load())],
keywords=[],
- starargs=None,
- kwargs=None,
)
)
diff --git a/tests/native_tests/defclass.hy b/tests/native_tests/defclass.hy
index 3d9b28658..41981f689 100644
--- a/tests/native_tests/defclass.hy
+++ b/tests/native_tests/defclass.hy
@@ -115,30 +115,18 @@
(defn test-pep-3115 []
- (defclass member-table [dict]
- (defn __init__ [self]
- (setv self.member-names []))
+ "Test setting a metaclass with `:metaclass`, and using `__prepare__`."
+ (defclass MyDict [dict]
(defn __setitem__ [self key value]
- (when (not-in key self)
- (.append self.member-names key))
- (dict.__setitem__ self key value)))
-
- (defclass OrderedClass [type]
- (setv __prepare__ (classmethod (fn [metacls name bases]
- (member-table))))
-
- (defn __new__ [cls name bases classdict]
- (setv result (type.__new__ cls name bases (dict classdict)))
- (setv result.member-names classdict.member-names)
- result))
-
- (defclass MyClass [:metaclass OrderedClass]
- (defn method1 [self] (pass))
- (defn method2 [self] (pass)))
-
- (assert (= (. (MyClass) member-names)
- ["__module__" "__qualname__" "method1" "method2"])))
+ (dict.__setitem__ self (+ "prefixed_" key) value)))
+ (defclass MyMetaclass [type]
+ (defn [classmethod] __prepare__ [metacls name bases]
+ (MyDict)))
+ (defclass MyClass [:metaclass MyMetaclass]
+ (defn [classmethod] method [self] 1))
+
+ (assert (= (MyClass.prefixed-method) 1)))
(defn test-pep-487 []
diff --git a/tests/native_tests/macros.hy b/tests/native_tests/macros.hy
index 6490cb707..457224168 100644
--- a/tests/native_tests/macros.hy
+++ b/tests/native_tests/macros.hy
@@ -149,7 +149,7 @@
(setv expected [" File \"\", line 1"
" (defmacro blah [x] `(print ~@z)) (blah y)"
- " ^------^"
+ (hy.compat.reu " ^------^")
"expanding macro blah"
" NameError: global name 'z' is not defined"])
diff --git a/tests/native_tests/repl.hy b/tests/native_tests/repl.hy
index 7169720b3..eccded584 100644
--- a/tests/native_tests/repl.hy
+++ b/tests/native_tests/repl.hy
@@ -83,7 +83,7 @@
(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 e1 (hy.compat.reu " ^----------^")))
(assert (.startswith e2 "expanding macro mabcdefghi"))
(assert (or
; PyPy can use a function's `__name__` instead of
diff --git a/tests/test_bin.py b/tests/test_bin.py
index bfe06949b..b2d436b76 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -92,64 +92,6 @@ def test_i_flag_repl_env():
assert "Nopers" in out
-def test_error_parts_length():
- """Confirm that exception messages print arrows surrounding the affected
- expression."""
- prg_str = """
- (import hy.errors
- hy.importer [read-many])
-
- (setv test-expr (read-many "(+ 1\n\n'a 2 3\n\n 1)"))
- (setv test-expr.start-line {})
- (setv test-expr.end-line {})
- (setv test-expr.start-column {})
- (setv test-expr.end-column {})
-
- (raise (hy.errors.HyLanguageError
- "this\nis\na\nmessage"
- test-expr
- None
- None))
- """
-
- # Up-arrows right next to each other.
- _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 2))
-
- msg_idx = err.rindex("HyLanguageError:")
- assert msg_idx
- err_parts = err[msg_idx:].splitlines()[1:]
-
- expected = [
- ' File "", line 3',
- " 'a 2 3",
- " ^^",
- "this",
- "is",
- "a",
- "message",
- ]
-
- for obs, exp in zip(err_parts, expected):
- assert obs.startswith(exp)
-
- # Make sure only one up-arrow is printed
- _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 1))
-
- msg_idx = err.rindex("HyLanguageError:")
- assert msg_idx
- err_parts = err[msg_idx:].splitlines()[1:]
- assert err_parts[2] == " ^"
-
- # Make sure lines are printed in between arrows separated by more than one
- # character.
- _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 6))
-
- msg_idx = err.rindex("HyLanguageError:")
- assert msg_idx
- err_parts = err[msg_idx:].splitlines()[1:]
- assert err_parts[2] == " ^----^"
-
-
def test_mangle_m():
# https://github.com/hylang/hy/issues/1445
@@ -480,7 +422,9 @@ def req_err(x):
# NameError: name 'a' is not defined
output, error = run_cmd('hy -c "(print a)"', expect=1)
# Filter out the underline added by Python 3.11.
- error_lines = [x for x in error.splitlines() if set(x) != {" ", "^"}]
+ error_lines = [x
+ for x in error.splitlines()
+ if not (set(x) <= {" ", "^", "~"})]
assert error_lines[3] == ' File "", line 1, in '
# PyPy will add "global" to this error message, so we work around that.
assert error_lines[-1].strip().replace(" global", "") == (
diff --git a/tests/test_reader.py b/tests/test_reader.py
index f7b7171ea..f0ad6b536 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -49,7 +49,7 @@ def check_trace_output(capsys, execinfo, expected):
hy_exc_handler(execinfo.type, execinfo.value, execinfo.tb)
captured_w_filtering = capsys.readouterr()[-1].strip("\n")
- output = [x.rstrip() for x in captured_w_filtering.split("\n") if "^^^" not in x]
+ output = [x.rstrip() for x in captured_w_filtering.split("\n") if not (set(x) <= {" ", "^", "~"})]
# Make sure the filtered frames aren't the same as the unfiltered ones.
assert output != captured_wo_filtering.split("\n")
@@ -81,15 +81,8 @@ def test_lex_single_quote_err():
# https://github.com/hylang/hy/issues/1252
with lexe() as execinfo:
tokenize("' ")
- check_ex(
- execinfo,
- [
- ' File "", line 1',
- " '",
- " ^",
- "hy.PrematureEndOfInput: Premature end of input while attempting to parse one form",
- ],
- )
+ assert type(execinfo.value) is PrematureEndOfInput
+ assert execinfo.value.msg == "Premature end of input while attempting to parse one form"
def test_lex_expression_symbols():
@@ -660,7 +653,6 @@ def test_lex_exception_filtering(capsys):
[
' File "", line 2',
" (foo",
- " ^",
"hy.PrematureEndOfInput: Premature end of input while attempting to parse one form",
],
)
@@ -674,7 +666,6 @@ def test_lex_exception_filtering(capsys):
[
' File "", line 3',
" 1.foo",
- " ^",
"hy.reader.exceptions.LexException: The parts of a dotted identifier must be symbols",
],
)
@@ -690,8 +681,9 @@ def test_read_error():
with pytest.raises(HySyntaxError) as e:
hy.eval(hy.read("(do (defn))"))
+
assert "".join(traceback.format_exception_only(e.type, e.value)).startswith(
- ' File "", line 1\n (do (defn))\n ^\n'
+ ' File "", line 1\n (do (defn))\n ^'
)