Skip to content

Commit

Permalink
Merge pull request #2602 from Kodiologist/py3-13
Browse files Browse the repository at this point in the history
Support Python 3.13
  • Loading branch information
Kodiologist authored Sep 12, 2024
2 parents 03ca15a + a379838 commit 9cbd040
Show file tree
Hide file tree
Showing 14 changed files with 77 additions and 142 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Unreleased
=============================

New Features
------------------------------
* Python 3.13 is now supported.

Bug Fixes
------------------------------
* Fixed a crash on Python 3.12.6.
Expand Down
6 changes: 6 additions & 0 deletions hy/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
9 changes: 7 additions & 2 deletions hy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
57 changes: 26 additions & 31 deletions hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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=[],
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
10 changes: 6 additions & 4 deletions hy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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)
)
Expand Down
11 changes: 6 additions & 5 deletions hy/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,17 @@ 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="<input>", symbol="single"):
def __call__(self, source, filename="<input>", 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)

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
Expand All @@ -148,7 +149,7 @@ def __call__(self, source, filename="<input>", 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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions tests/compilers/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,6 @@ def test_ast_expression_basics():
),
args=[ast.Name(id="bar", ctx=ast.Load())],
keywords=[],
starargs=None,
kwargs=None,
)
)

Expand Down
32 changes: 10 additions & 22 deletions tests/native_tests/defclass.hy
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/macros.hy
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@

(setv expected [" File \"<string>\", line 1"
" (defmacro blah [x] `(print ~@z)) (blah y)"
" ^------^"
(hy.compat.reu " ^------^")
"expanding macro blah"
" NameError: global name 'z' is not defined"])

Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/repl.hy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 3 additions & 59 deletions tests/test_bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<string>", 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

Expand Down Expand Up @@ -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 "<string>", line 1, in <module>'
# PyPy will add "global" to this error message, so we work around that.
assert error_lines[-1].strip().replace(" global", "") == (
Expand Down
Loading

0 comments on commit 9cbd040

Please sign in to comment.