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 ^' )