Skip to content

Commit

Permalink
Adds support for :decorators in anon fns, defasync (#1189)
Browse files Browse the repository at this point in the history
Hi,

could you please review patch to support the `:decorators` key in
anonymous `fn`s. It addresses #1178.

The handling of `:decorators` has been nmoved to `fn`, which also
ensures that the meta key is now considered when passed as metadata to
the `defn` name. This enables support for supporting the meta key in
`defn` derived macros such as `defasync`, as intended.

I've updated the `defn` docstring to hint that the processing is now
done by `fn` now.

I've also added tests for `fn`, `defn` and `defasync`, along with a
section in the python interop doc about it.

Thanks

---------

Co-authored-by: ikappaki <[email protected]>
Co-authored-by: Chris Rink <[email protected]>
  • Loading branch information
3 people authored Dec 23, 2024
1 parent 1c1eef9 commit 61e86f3
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
* Added support for the `:decorators` meta key in anonymous `fn`s (#1178)

### Changed
* `import` now returns nil instead of the last module's string representation (#1174)
* The `:decorators` key now works in `defn` when passed as a metadata name key, expanding its support to `defn`-derived macros like `defasync` (#1178).

### Fixed
* Fix a bug in `defn` where the `attr-map?` and function metdata were merged into a seq instead of a map, causing `macroexpand` to fail in some cases (#1186)
Expand Down
70 changes: 69 additions & 1 deletion docs/pyinterop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,74 @@ Type hints may be applied to :lpy:form:`def` names, function arguments and retur
Return annotations are combined as by :external:py:obj:`typing.Union`, so ``typing.Union[str, str] == str``.
The annotations for individual arity arguments are preserved in their compiled form, but they are challenging to access programmatically.

.. _python_decorators:

Python Decorators
-----------------

Python decorators are functions that modify the behavior of other functions or methods.
They are applied to a function by prefixing it with the ``@decorator_name`` syntax. A decorator takes a function as input, performs some action, and returns a new function that typically extends or alters the original function's behavior.

Basilisp offers a convenience ``:decorators`` metadata key to support Python-style decorators, which allows you to pass a vector of functions that wrap the final function emitted by the :lpy:fn:`fn` anonymous function, as well as by :lpy:fn:`defn` and its derivatives, such as :lpy:fn:`defasync`.
These decorators are applied from right to left, similar to how Python decorators work, modifying the function's behavior before it is used.

.. code-block:: clojure
(import asyncio atexit)
;;; defn support
;;
;; The following will print ":goodbye!" on program exit
(defn say-goodbye {:decorators [atexit/register]}
[]
(println :goodbye!))
;;; fn support
;;
;; example decorator
(defn add-5-decorator
[f]
(fn [] (+ (f) 5)))
;; Decorators passed to fn via form metadata
(^{:decorators [add-5-decorator]} (fn [] 6))
;; => 11
;; Decorators passed to fn via function name metadata
((fn ^{:decorators [add-5-decorator]} seven [] 7))
;; => 12
;;; Decorators with arguments, and order of application
;;
;; example decorator
(defn mult-x-decorator
[x]
(fn [f]
(fn [] (* (f) x))))
((fn ^{:decorators [add-5-decorator (mult-x-decorator -1)]} seven [] 7))
;; => -2
;;; defasync support
;;
;; example async decorator
(defn add-7-async-decorator
[f]
^:async (fn [] (+ (await (f)) 7)))
(defasync ^{:decorators [add-7-async-decorator]} six
[]
(await (asyncio/sleep 0.1))
6)
(asyncio/run (six))
;; => 13
.. note::

Users wishing to apply decorators to functions are not limited to using ``:decorators`` metadata.
The ``:decorators`` feature is provided primarily to simplify porting Python code to Basilisp.
In Python, decorators are syntactic sugar for functions which return functions, but given the rich library of tools provided for composing functions and the ease of defining anonymous functions in Basilisp, the use of ``:decorators`` is not typically necessary in standard Basilisp code.
.. _arithmetic_division:

Arithmetic Division
Expand All @@ -279,4 +347,4 @@ Users still have the option to use the native :external:py:func:`operator.floord

.. seealso::

:lpy:fn:`quot`, :lpy:fn:`rem`, :lpy:fn:`mod`
:lpy:fn:`quot`, :lpy:fn:`rem`, :lpy:fn:`mod`
47 changes: 30 additions & 17 deletions src/basilisp/core.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,11 @@
After the name, an optional mapping of meta attributes may be provided.
Any metadata values given will be attached to the metadata of the
interned Var. A few special meta keys change how ``defn`` emits the
final Var and function:
interned Var, overriding any existing metadata associated with the
``name``.
A few special meta keys change how ``defn`` emits the final Var and
function. The processing is handled by :lpy:fn:`fn`:
- ``:decorators`` is an optional vector of functions which will wrap
the final function emitted by ``defn``. Like standard Python
Expand Down Expand Up @@ -411,15 +414,7 @@
{:found (first body)})))
(rest body)))

decorators (:decorators fmeta)
fn-body (if decorators
(loop [wrappers (seq (python/reversed decorators))
final `(fn ~fname ~@body)]
(if (seq wrappers)
(recur (rest wrappers)
`(~(first wrappers) ~final))
final))
`(fn ~fname ~@body))]
fn-body `(fn ~fname ~@body)]
`(def ~fname ~fn-body))))

(defn nth
Expand Down Expand Up @@ -6002,6 +5997,13 @@
(defmacro ^:no-warn-on-redef fn
"Return an anonymous (but possibly named) function.
A few special metadata keys change how ``fn`` emits the final function:
- ``:decorators`` is an optional vector of functions which will wrap
the final function emitted by ``fn``. Like standard Python
decorators and the ``comp`` function, decorators are applied to the
generated function from right to left.
Function argument vectors support sequential and associative :ref:`destructuring` .
See :lpy:form:`fn` for more details."
Expand All @@ -6017,12 +6019,23 @@
(apply list (map fn-arity-with-destructuring body))

:else
body)]
(as-> arities $
(cond->> $ name (cons name))
(cons 'fn* $)
(cond-> $
(meta &form) (with-meta (meta &form))))))
body)
fn-decl (as-> arities $
(cond->> $ name (cons name))
(cons 'fn* $)
(cond-> $
(meta &form) (with-meta (meta &form))))

fmeta (merge (meta fn-decl) (meta name))
decorators (:decorators fmeta)]
(if decorators
(loop [wrappers (seq (python/reversed decorators))
final fn-decl]
(if (seq wrappers)
(recur (rest wrappers)
`(~(first wrappers) ~final))
final))
fn-decl)))

(defn destructure
"Take a ``[binding expr]`` pair (as from a ``let`` block) and produce all of the
Expand Down
52 changes: 49 additions & 3 deletions tests/basilisp/test_core_macros.lpy
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
(ns tests.basilisp.test-core-macros
(:import contextlib inspect os socket tempfile)
(:import asyncio contextlib inspect os socket tempfile)
(:require
[basilisp.string :as str]
[basilisp.test :refer [deftest is are testing]]))

(deftest fn-decorators-test
(testing "decorators"
(testing "in form meta"
(let [add-5% #(fn [] (+ (%) 5))
f1 ^{:decorators [add-5%]} (fn [] 7)]
(is (= 12 (f1)))))

(testing "in fn name meta"
(let [add-5% #(fn [] (+ (%) 5))
f2 (fn ^{:decorators [add-5%]} f2 [] 13)]
(is (= 18 (f2)))))

(testing "with single arg"
(let [add-x% (fn [x] #(fn [] (+ (%) x)))
f3 (fn ^{:decorators [(add-x% 10)]} f3 [] 7)]
(is (= 17 (f3)))))

(testing "order"
(let [add-5% #(fn [] (+ (%) 5))
mult-x% (fn [x] #(fn [] (* (%) x)))
fvar (defn f4 {:decorators [add-5% (mult-x% -1)]} [] 9)]
(is (= -4 (f4)))))))

(deftest defn-test
(testing "single arity defn"
(testing "simple"
Expand Down Expand Up @@ -96,7 +119,13 @@
(is (= {:abc 10 :lmn 15 :xyz 11} (select-keys vmeta [:abc :lmn :xyz])))))

(testing "macroexpand with attr-map?"
(is (macroexpand '(defn fx {:abc 10} [] :kw)))))
(is (macroexpand '(defn fx {:abc 10} [] :kw))))

(testing "decorators"
(let [add-5% #(fn [] (+ (%) 5))
mult-x% (fn [x] #(fn [] (* (%) x)))
fvar (defn f12 {:decorators [add-5% (mult-x% -1)]} [] 9)]
(is (= -4 (f12))))))

(deftest defasync-test
(testing "single arity defasync"
Expand Down Expand Up @@ -189,7 +218,24 @@
(is (:async vmeta))
(is (inspect/iscoroutinefunction af8))
(is (= "0.1" (:added vmeta)))
(is (= "another multi-arity docstring" (:doc vmeta)))))))
(is (= "another multi-arity docstring" (:doc vmeta)))))

(testing "async decorators"
(testing "single"
(let [add-5% #(fn ^:async _ [] (+ (await (%)) 5))
fvar (defasync ^{:decorators [add-5%]} af9 [] 7)]
(is (= 12 (asyncio/run (af9))))))

(testing "single with arg"
(let [add-x% (fn [x] #(fn ^:async _ [] (+ (await (%)) x)))
fvar (defasync ^{:decorators [(add-x% 10)]} af10 [] 7)]
(is (= 17 (asyncio/run (af10))))))

(testing "order"
(let [add-5% #(fn ^:async _ [] (+ (await (%)) 5))
mult-x% (fn [x] #(fn ^:async _ [] (* (await (%)) x)))
fvar (defasync af11 {:decorators [add-5% (mult-x% -1)]} [] 9)]
(is (= -4 (asyncio/run (af11)))))))))

(deftest defn-private-test
(testing "single arity defn-"
Expand Down

0 comments on commit 61e86f3

Please sign in to comment.