From 3ee87e9b395a19cf21fe1f96ed9b0531fc88fc69 Mon Sep 17 00:00:00 2001 From: ikappaki Date: Thu, 19 Dec 2024 20:11:08 +0000 Subject: [PATCH 1/4] Adds support for :decorators in anon fns, defasync --- CHANGELOG.md | 4 ++ docs/pyinterop.rst | 63 ++++++++++++++++++++++++++++- src/basilisp/core.lpy | 47 +++++++++++++-------- tests/basilisp/test_core_macros.lpy | 52 ++++++++++++++++++++++-- 4 files changed, 145 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3d3d2e..be3f642a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/pyinterop.rst b/docs/pyinterop.rst index 280e5399..c487c051 100644 --- a/docs/pyinterop.rst +++ b/docs/pyinterop.rst @@ -262,6 +262,67 @@ 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 + .. _arithmetic_division: Arithmetic Division @@ -279,4 +340,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` \ No newline at end of file + :lpy:fn:`quot`, :lpy:fn:`rem`, :lpy:fn:`mod` diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 1fd88cb5..9a636840 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -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 @@ -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 @@ -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." @@ -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 diff --git a/tests/basilisp/test_core_macros.lpy b/tests/basilisp/test_core_macros.lpy index b1f19688..53648b24 100644 --- a/tests/basilisp/test_core_macros.lpy +++ b/tests/basilisp/test_core_macros.lpy @@ -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%]} a-fn [] 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" @@ -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" @@ -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-" From 5e470629fdbe6d06fa0084ad4caa842954344562 Mon Sep 17 00:00:00 2001 From: ikappaki <34983288+ikappaki@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:59:53 +0000 Subject: [PATCH 2/4] Update docs/pyinterop.rst Co-authored-by: Chris Rink --- docs/pyinterop.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/pyinterop.rst b/docs/pyinterop.rst index c487c051..418af1ff 100644 --- a/docs/pyinterop.rst +++ b/docs/pyinterop.rst @@ -267,9 +267,11 @@ Type hints may be applied to :lpy:form:`def` names, function arguments and retur 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. +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. +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 From 821213a6a1290951885c4f1e04b0dc0f830fd8f7 Mon Sep 17 00:00:00 2001 From: ikappaki <34983288+ikappaki@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:00:04 +0000 Subject: [PATCH 3/4] Update docs/pyinterop.rst Co-authored-by: Chris Rink --- docs/pyinterop.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/pyinterop.rst b/docs/pyinterop.rst index 418af1ff..b511a67a 100644 --- a/docs/pyinterop.rst +++ b/docs/pyinterop.rst @@ -325,6 +325,11 @@ These decorators are applied from right to left, similar to how Python decorator (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 From 55c02ce0307811afa8926dd938c2d4401957ddc6 Mon Sep 17 00:00:00 2001 From: ikappaki <34983288+ikappaki@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:00:58 +0000 Subject: [PATCH 4/4] Update tests/basilisp/test_core_macros.lpy Co-authored-by: Chris Rink --- tests/basilisp/test_core_macros.lpy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/basilisp/test_core_macros.lpy b/tests/basilisp/test_core_macros.lpy index 53648b24..884c75eb 100644 --- a/tests/basilisp/test_core_macros.lpy +++ b/tests/basilisp/test_core_macros.lpy @@ -13,12 +13,12 @@ (testing "in fn name meta" (let [add-5% #(fn [] (+ (%) 5)) - f2 (fn ^{:decorators [add-5%]} a-fn [] 13)] + 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)] + f3 (fn ^{:decorators [(add-x% 10)]} f3 [] 7)] (is (= 17 (f3))))) (testing "order"