Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace with-decorator with decorator lists in defn and defclass #2270

Merged
merged 12 commits into from
May 9, 2022
5 changes: 4 additions & 1 deletion NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Removals
`(defreader foo (setv arg (.parse-one-form &reader)) …)`.
* `hy.read-str` has been removed. Use `hy.read`, which now accepts
strings, instead.
* `with-decorator` and `#@` have been removed in favor of decorator
lists (see below).

Breaking Changes
------------------------------
Expand All @@ -21,7 +23,6 @@ Breaking Changes
(cond [a b] [x y z]) ; Old
(cond a b x (do y z)) ; New

* `@#` is now a reader macro and has been moved to Hyrule.
* `defmacro` once again requires the macro name as a symbol, not
a string literal.
* The parser has been completely rewritten. It is mostly
Expand Down Expand Up @@ -66,6 +67,8 @@ New Features
keyword arguments. Hy includes a workaround for a CPython bug that
prevents the generation of legal Python code for these cases
(`bpo-46520`_).
* `defn` and `defclass` now allow a decorator list as their first
argument.
* You can now set the variable `_hy_export_macros` to control what macros are
collected by `(require module *)`
* New macro `export`
Expand Down
68 changes: 16 additions & 52 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
If there at least two body forms, and the first of them is a string literal,
this string becomes the :term:`py:docstring` of the function.

``defn`` accepts two additional, optional arguments: a bracketed list of
:term:`decorators <py:decorator>` and an annotation (see :hy:data:`^`) for
the return value. These are placed before the function name (in that order,
if both are present)::

(defn [decorator1 decorator2] ^annotation name [params] …)

Parameters may be prefixed with the following special symbols. If you use more
than one, they can only appear in the given order (so all positional only arguments
must precede ``/``, all positional or keyword arguments must precede a ``#*`` rest
Expand Down Expand Up @@ -898,19 +905,20 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
positionally if its ``__match_args__`` attribute is defined, see
`pep 636 <https://www.python.org/dev/peps/pep-0636/#appendix-a-quick-intro>`_)::

=> (with-decorator
... dataclass
... (defclass Point []
... (^int x)
... (^int y)))
=> (import dataclasses [dataclass])
=> (defclass [dataclass] Point []
... (^int x)
... (^int y))
=> (match (Point 1 2)
... (Point 1 x) :if (= (% x 2) 0) x
... (Point 1 x) :if (= (% x 2) 0) x)
2

.. hy:function:: (defclass [class-name super-classes #* body])

New classes are declared with ``defclass``. It can take optional parameters in the following order:
a list defining (a) possible super class(es) and a string (:term:`py:docstring`).
New classes are declared with ``defclass``. It can take optional parameters
in the following order: a list defining (a) possible super class(es) and a
string (:term:`py:docstring`). The class name may also be preceded by a list
of :term:`decorators <py:decorator>`, as in :hy:func:`defn`.

:strong:`Examples`

Expand Down Expand Up @@ -1504,50 +1512,6 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
(because the context manager's ``__aexit__`` method returned true), in which
case it returns ``None``.

.. hy:function:: (with-decorator [#* args])

``with-decorator`` is used to wrap a function with another. The function
performing the decoration should accept a single value: the function being
decorated, and return a new function. ``with-decorator`` takes a minimum
of two parameters: the function performing decoration and the function
being decorated. More than one decorator function can be applied; they
will be applied in order from outermost to innermost, ie. the first
decorator will be the outermost one, and so on. Decorators with arguments
are called just like a function call.

::

(with-decorator decorator-fun
(defn some-function [] ...)

(with-decorator decorator1 decorator2 ...
(defn some-function [] ...)

(with-decorator (decorator arg) ..
(defn some-function [] ...)


In the following example, ``inc-decorator`` is used to decorate the function
``addition`` with a function that takes two parameters and calls the
decorated function with values that are incremented by 1. When
the decorated ``addition`` is called with values 1 and 1, the end result
will be 4 (``1+1 + 1+1``).

::

=> (defn inc-decorator [func]
... (fn [value-1 value-2] (func (+ value-1 1) (+ value-2 1))))
=> (defn inc2-decorator [func]
... (fn [value-1 value-2] (func (+ value-1 2) (+ value-2 2))))

=> (with-decorator inc-decorator (defn addition [a b] (+ a b)))
=> (addition 1 1)
4
=> (with-decorator inc2-decorator inc-decorator
... (defn addition [a b] (+ a b)))
=> (addition 1 1)
8

.. hy:function:: (yield [object])

``yield`` is used to create a generator object that returns one or more values.
Expand Down
1 change: 0 additions & 1 deletion docs/cheatsheet.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@
"while",
"with",
"with/a",
"with-decorator",
"yield",
"yield-from"
]
Expand Down
53 changes: 28 additions & 25 deletions hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -1398,25 +1398,32 @@ def compile_function_lambda(compiler, expr, root, returns, params, body):
# Otherwise create a standard function
node = asty.AsyncFunctionDef if root == "fn/a" else asty.FunctionDef
name = compiler.get_anon_var()
ret += compile_function_node(compiler, expr, node, name, args, returns, body)
ret += compile_function_node(compiler, expr, node, [], name, args, returns, body)

# return its name as the final expr
return ret + Result(expr=ret.temp_variables[0])


@pattern_macro(["defn", "defn/a"], [OPTIONAL_ANNOTATION, SYM, lambda_list, many(FORM)])
def compile_function_def(compiler, expr, root, returns, name, params, body):
@pattern_macro(
["defn", "defn/a"],
[maybe(brackets(many(FORM))), OPTIONAL_ANNOTATION, SYM, lambda_list, many(FORM)],
)
def compile_function_def(compiler, expr, root, decorators, returns, name, params, body):
node = asty.FunctionDef if root == "defn" else asty.AsyncFunctionDef
args, ret = compile_lambda_list(compiler, params)
decorators, ret, _ = compiler._compile_collect(decorators[0] if decorators else [])
args, ret2 = compile_lambda_list(compiler, params)
ret += ret2
name = mangle(compiler._nonconst(name))
compiler.scope.define(name)
with compiler.scope.create(ScopeFn, args):
body = compiler._compile_branch(body)

return ret + compile_function_node(compiler, expr, node, name, args, returns, body)
return ret + compile_function_node(
compiler, expr, node, decorators, name, args, returns, body
)


def compile_function_node(compiler, expr, node, name, args, returns, body):
def compile_function_node(compiler, expr, node, decorators, name, args, returns, body):
ret = Result()

if body.expr:
Expand All @@ -1427,7 +1434,7 @@ def compile_function_node(compiler, expr, node, name, args, returns, body):
name=name,
args=args,
body=body.stmts or [asty.Pass(expr)],
decorator_list=[],
decorator_list=decorators,
returns=compiler.compile(returns).force_expr if returns is not None else None,
)

Expand Down Expand Up @@ -1573,19 +1580,6 @@ def compile_arguments_set(compiler, decls, ret, is_kwonly=False):
return args_ast, args_defaults, ret


_decoratables = (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)


@pattern_macro("with-decorator", [oneplus(FORM)])
def compile_decorate_expression(compiler, expr, name, args):
decs, fn = args[:-1], compiler.compile(args[-1])
if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables):
raise compiler._syntax_error(args[-1], "Decorated a non-function")
decs, ret, _ = compiler._compile_collect(decs)
fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list
return ret + fn


@pattern_macro("return", [maybe(FORM)])
def compile_return(compiler, expr, root, arg):
ret = Result()
Expand Down Expand Up @@ -1615,13 +1609,22 @@ def compile_yield_from_or_await_expression(compiler, expr, root, arg):
# ------------------------------------------------


@pattern_macro("defclass", [SYM, maybe(brackets(many(FORM)) + maybe(STR) + many(FORM))])
def compile_class_expression(compiler, expr, root, name, rest):
@pattern_macro(
"defclass",
[
maybe(brackets(many(FORM))),
SYM,
maybe(brackets(many(FORM)) + maybe(STR) + many(FORM)),
],
)
def compile_class_expression(compiler, expr, root, decorators, name, rest):
base_list, docstring, body = rest or ([[]], None, [])

bases_expr, bases, keywords = compiler._compile_collect(
decorators, ret, _ = compiler._compile_collect(decorators[0] if decorators else [])
bases_expr, ret2, keywords = compiler._compile_collect(
base_list[0], with_kwargs=True
)
ret += ret2

bodyr = Result()

Expand All @@ -1635,9 +1638,9 @@ def compile_class_expression(compiler, expr, root, name, rest):
e = compiler._compile_branch(body)
bodyr += e + e.expr_as_stmt()

return bases + asty.ClassDef(
return ret + asty.ClassDef(
expr,
decorator_list=[],
decorator_list=decorators,
name=name,
keywords=keywords,
starargs=None,
Expand Down
5 changes: 0 additions & 5 deletions tests/compilers/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,6 @@ def test_ast_anon_fns_basics():
cant_compile("(fn)")


def test_ast_non_decoratable():
"""Ensure decorating garbage breaks"""
cant_compile("(with-decorator (foo) (* x x))")


def test_ast_lambda_lists():
"""Ensure the compiler chokes on invalid lambda-lists"""
cant_compile("(fn [[a b c]] a)")
Expand Down
9 changes: 5 additions & 4 deletions tests/native_tests/comprehensions.hy
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
(assert (is (type (gfor x "abc" :do (setv y 1) x)) types.GeneratorType)))


(with-decorator (pytest.mark.parametrize "specialop"
["for" "lfor" "sfor" "gfor" "dfor"])
(defn test-fors [specialop]
(defn
[(pytest.mark.parametrize "specialop"
["for" "lfor" "sfor" "gfor" "dfor"])]
test-fors [specialop]

(setv cases [
['(f x [] x)
Expand Down Expand Up @@ -97,7 +98,7 @@
(setv result (hy.eval expr))
(when (= specialop "dfor")
(setv result (.keys result)))
(assert (= (sorted result) answer) (str expr)))))
(assert (= (sorted result) answer) (str expr))))


(defn test-fors-no-loopers []
Expand Down
48 changes: 48 additions & 0 deletions tests/native_tests/decorators.hy
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
(defn test-decorated-1line-function []
(defn foodec [func]
(fn [] (+ (func) 1)))
(defn [foodec] tfunction []
(* 2 2))
(assert (= (tfunction) 5)))


(defn test-decorated-multiline-function []
(defn bazdec [func]
(fn [] (+ (func) "x")))
(defn [bazdec] f []
(setv intermediate "i")
(+ intermediate "b"))
(assert (= (f) "ibx")))


(defn test-decorated-class []
(defn bardec [cls]
(setv cls.attr2 456)
cls)
(defclass [bardec] cls []
(setv attr1 123))
(assert (= cls.attr1 123))
(assert (= cls.attr2 456)))


(defn test-stacked-decorators []
(defn dec1 [f] (fn [] (+ (f) "a")))
(defn dec2 [f] (fn [] (+ (f) "b")))
(defn [dec1 dec2] f [] "c")
(assert (= (f) "cba")))


(defn test-evaluation-order []
(setv l [])
(defn foo [f]
(.append l "foo")
(fn []
(.append l "foo fn")
(f)))
(defn
[(do (.append l "dec") foo)] ; Decorator list
bar ; Function name
[[arg (do (.append l "arg") 1)]] ; Lambda list
(.append l "bar body") arg) ; Body
(.append l (bar))
(assert (= l ["dec" "arg" "foo" "foo fn" "bar body" 1])))
8 changes: 3 additions & 5 deletions tests/native_tests/language.hy
Original file line number Diff line number Diff line change
Expand Up @@ -1704,11 +1704,9 @@ cee"} dee" "ey bee\ncee dee"))
(defn test-decorated-defn/a []
(defn decorator [func] (fn/a [] (/ (await (func)) 2)))

(with-decorator
decorator
(defn/a coro-test []
(await (asyncio.sleep 0))
42))
(defn/a [decorator] coro-test []
(await (asyncio.sleep 0))
42)
(assert (= (asyncio.run (coro-test)) 21)))


Expand Down
Loading