From 3a0f9f3e554c896d0c4843dbdc7071e45f3bc6bb Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 26 Nov 2024 13:37:27 -0500 Subject: [PATCH 01/24] Proxies --- src/basilisp/core.lpy | 136 ++++++++++++++++++++++++++++++++ src/basilisp/lang/interfaces.py | 27 +++++++ 2 files changed, 163 insertions(+) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 05e8a1e63..0ca6ac288 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7,6 +7,7 @@ decimal fractions importlib.util + inspect math multiprocessing os @@ -6951,6 +6952,141 @@ ~@methods) (meta &form)))) +;;;;;;;;;;;;; +;; Proxies ;; +;;;;;;;;;;;;; + +(def ^:private excluded-proxy-methods + #{"__getattribute__" + "__init__" + "__new__"}) + +(def ^:private proxy-cache (atom {})) + +(defn ^:private proxy-base-methods + [base] + (->> (inspect/getmembers base inspect/isroutine) + (remove (comp excluded-proxy-methods first)) + (mapcat (fn [[method-name method]] + (let [meth-sym (symbol method-name) + meth `(fn ~meth-sym [~'self & args#] + (if-let [override (get (.- ~'self ~'-proxy-mappings) ~method-name)] + (apply override ~'self args#) + (-> (.- ~'self __class__) + (python/super ~'self) + (.- ~meth-sym) + (apply args#))))] + [method-name (eval meth *ns*)]))))) + +(defn ^:private proxy-type + "Generate a proxy class with the given bases." + [bases] + (let [methods (apply hash-map (mapcat proxy-base-methods bases)) + base-methods {"__init__" (fn __init__ [self proxy-mappings & args] + (apply (.- (python/super (.- self __class__) self) __init__) args) + (set! (.- self -proxy-mappings) proxy-mappings) + nil) + "_proxy_mappings" nil}] + (python/type (basilisp.lang.util/genname "Proxy") + (python/tuple (concat bases [basilisp.lang.interfaces/IProxy])) + (python/dict (merge methods base-methods))))) + +;; TODO: support object as super +(defn get-proxy-class + "Given one or more base classes, return a proxy class for the given classes. + + Generated classes are cached, such that the same set of base classes will always + return the same resulting proxy class." + [& bases] + (let [base-set (set bases)] + (-> (swap! proxy-cache (fn [cache] + (if (get cache base-set) + cache + (assoc cache base-set (proxy-type bases))))) + (get base-set)))) + +(defn proxy-mappings + "Return the current method map for the given proxy. + + Throws an exception if ``proxy`` is not a proxy." + [proxy] + (if-not (instance? basilisp.lang.interfaces/IProxy proxy) + (throw + (ex-info "Cannot get proxy mappings from object which does not implement IProxy" + {:obj proxy})) + (. proxy (_get-proxy-mappings)))) + +(defn construct-proxy + "Construct an instance of the proxy class ``c`` with the given constructor arguments. + + Throws an exception if ``c`` is not a subclass of + :py:class:`basilisp.lang.interfaces.IProxy`. + + .. note:: + + In JVM Clojure, this function is useful for selecting a specific constructor based + on argument count, but Python does not support multi-arity methods (including + constructors), so this is likely of limited use." + [c & ctor-args] + (if-not (python/issubclass c basilisp.lang.interfaces/IProxy) + (throw + (ex-info "Cannot construct instance of class which is not a subclass of IProxy" + {:class c :args ctor-args})) + (apply c {} ctor-args))) + +(defn init-proxy + "Set the current proxy method map for the given proxy. + + Method maps are maps of string method names to their implementations as Basilisp + functions. + + Throws an exception if ``proxy`` is not a proxy." + [proxy mappings] + (if-not (instance? basilisp.lang.interfaces/IProxy proxy) + (throw + (ex-info "Cannot get proxy mappings from object which does not implement IProxy" + {:obj proxy})) + (do + (. proxy (_set-proxy-mappings mappings)) + proxy))) + +;; TODO: handle nil methods +(defn update-proxy + "Update the current proxy method map for the given proxy. + + Throws an exception if ``proxy`` is not a proxy." + [proxy mappings] + (if-not (instance? basilisp.lang.interfaces/IProxy proxy) + (throw + (ex-info "Cannot update proxy mappings for object which does not implement IProxy" + {:obj proxy})) + (do + (. proxy (_update-proxy-mappings mappings)) + proxy))) + +;; TODO: how to handle multi-arity fns +;; TODO: kwargs on supertypes +(defmacro proxy + [class-and-interfaces args & fs] + (let [formatted-arity (fn [method-name [arg-vec & body]] + [method-name + (apply list method-name (vec (concat ['this] arg-vec)) body)]) + methods (mapcat (fn [[method-name & body]] + (if (vector? (first body)) + [(formatted-arity method-name body)] + (map (partial formatted-arity method-name) body))) + fs)] + `((get-proxy-class ~@class-and-interfaces) {} ~@args))) + +;; TODO: handle explicit class and self +(defmacro proxy-super + "Macro which expands to a call to the method named ``meth`` on the superclass + with the provided ``args``. + + Note that the default" + [meth & args] + `(. (~'python/super) (~meth ~@args))) + ;;;;;;;;;;;;; ;; Records ;; ;;;;;;;;;;;;; diff --git a/src/basilisp/lang/interfaces.py b/src/basilisp/lang/interfaces.py index 9102a8933..0a0abf76b 100644 --- a/src/basilisp/lang/interfaces.py +++ b/src/basilisp/lang/interfaces.py @@ -372,6 +372,33 @@ def pop(self: Self) -> Self: raise NotImplementedError() +class IProxy(ABC): + """``IProxy`` is a marker interface for proxy types. + + All types created by ``proxy`` are automatically marked with ``IProxy``. + + .. seealso:: + + :ref:`proxies`""" + + __slots__ = () + + _proxy_mappings: "IPersistentMap[str, Callable]" + + def _get_proxy_mappings(self) -> "IPersistentMap[str, Callable]": + return self._proxy_mappings + + def _set_proxy_mappings( + self, proxy_mappings: "IPersistentMap[str, Callable]" + ) -> None: + self._proxy_mappings = proxy_mappings + + def _update_proxy_mappings( + self, proxy_mappings: "IPersistentMap[str, Callable]" + ) -> None: + self._proxy_mappings = proxy_mappings + + T_key = TypeVar("T_key") V_contra = TypeVar("V_contra", contravariant=True) From 9b5c5933de88a267240ae954a504866c19c7c657 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 26 Nov 2024 13:54:15 -0500 Subject: [PATCH 02/24] More --- src/basilisp/core.lpy | 30 ++++++++++++++++++++++++------ src/basilisp/lang/interfaces.py | 6 +++--- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 0ca6ac288..71c2bfab8 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -6982,11 +6982,26 @@ "Generate a proxy class with the given bases." [bases] (let [methods (apply hash-map (mapcat proxy-base-methods bases)) - base-methods {"__init__" (fn __init__ [self proxy-mappings & args] - (apply (.- (python/super (.- self __class__) self) __init__) args) - (set! (.- self -proxy-mappings) proxy-mappings) - nil) - "_proxy_mappings" nil}] + base-methods {"__init__" (fn __init__ [self proxy-mappings & args] + (apply (.- (python/super (.- self __class__) self) __init__) args) + (set! (.- self -proxy-mappings) proxy-mappings) + nil) + "_get_proxy_mappings" (fn _get_proxy_mappings [self] + (.- self -proxy-mappings)) + "_set_proxy_mappings" (fn _set_proxy_mappings [self proxy-mappings] + (set! (.- self -proxy-mappings) proxy-mappings) + nil) + "_update_proxy_mappings" (fn _update_proxy_mappings [self proxy-mappings] + (let [updated-mappings (->> proxy-mappings + (reduce* (fn [m [k v]] + (if v + (assoc m k v) + (dissoc m k))) + (.- self -proxy-mappings)) + (persistent!))] + (set! (.- self -proxy-mappings) updated-mappings) + nil)) + "_proxy_mappings" nil}] (python/type (basilisp.lang.util/genname "Proxy") (python/tuple (concat bases [basilisp.lang.interfaces/IProxy])) (python/dict (merge methods base-methods))))) @@ -7050,10 +7065,13 @@ (. proxy (_set-proxy-mappings mappings)) proxy))) -;; TODO: handle nil methods (defn update-proxy "Update the current proxy method map for the given proxy. + Method maps are maps of string method names to their implementations as Basilisp + functions. If ``nil`` is passed in place of a function for a method, that method will + revert to its default behavior. + Throws an exception if ``proxy`` is not a proxy." [proxy mappings] (if-not (instance? basilisp.lang.interfaces/IProxy proxy) diff --git a/src/basilisp/lang/interfaces.py b/src/basilisp/lang/interfaces.py index 0a0abf76b..5a7cd041e 100644 --- a/src/basilisp/lang/interfaces.py +++ b/src/basilisp/lang/interfaces.py @@ -386,17 +386,17 @@ class IProxy(ABC): _proxy_mappings: "IPersistentMap[str, Callable]" def _get_proxy_mappings(self) -> "IPersistentMap[str, Callable]": - return self._proxy_mappings + raise NotImplementedError() def _set_proxy_mappings( self, proxy_mappings: "IPersistentMap[str, Callable]" ) -> None: - self._proxy_mappings = proxy_mappings + raise NotImplementedError() def _update_proxy_mappings( self, proxy_mappings: "IPersistentMap[str, Callable]" ) -> None: - self._proxy_mappings = proxy_mappings + raise NotImplementedError() T_key = TypeVar("T_key") From 1f18af480481eb4fb76a82b31bc98aea818ac729 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Fri, 27 Dec 2024 13:29:22 -0500 Subject: [PATCH 03/24] Documentation --- docs/pyinterop.rst | 7 +++++++ src/basilisp/lang/interfaces.py | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/pyinterop.rst b/docs/pyinterop.rst index 72826421d..7925ffcbf 100644 --- a/docs/pyinterop.rst +++ b/docs/pyinterop.rst @@ -405,3 +405,10 @@ Users still have the option to use the native :external:py:func:`operator.floord .. seealso:: :lpy:fn:`quot`, :lpy:fn:`rem`, :lpy:fn:`mod` + +.. _proxies: + +Proxies +------- + +TBD diff --git a/src/basilisp/lang/interfaces.py b/src/basilisp/lang/interfaces.py index 5a7cd041e..79a7d5e1a 100644 --- a/src/basilisp/lang/interfaces.py +++ b/src/basilisp/lang/interfaces.py @@ -379,7 +379,9 @@ class IProxy(ABC): .. seealso:: - :ref:`proxies`""" + :ref:`proxies`, :lpy:fn:`proxy`, :lpy:fn:`proxy-mappings`, :lpy:fn:`proxy-super`, + :lpy:fn:`construct-proxy`, :lpy:fn:`init-proxy`, :lpy:fn:`update-proxy`, + :lpy:fn:`get-proxy-class`""" __slots__ = () From 16dd1f35fddd198e15c31467741e7e4c585dd59c Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 26 Nov 2024 14:03:47 -0500 Subject: [PATCH 04/24] proxy-super --- src/basilisp/core.lpy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 71c2bfab8..422b2c535 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7096,14 +7096,14 @@ fs)] `((get-proxy-class ~@class-and-interfaces) {} ~@args))) -;; TODO: handle explicit class and self (defmacro proxy-super "Macro which expands to a call to the method named ``meth`` on the superclass with the provided ``args``. - Note that the default" + Note this macro explicitly captures the implicit ``this`` parameter added to proxy + methods." [meth & args] - `(. (~'python/super) (~meth ~@args))) + `(. (~'python/super (.- ~'this ~'__class__) ~'this) (~meth ~@args))) ;;;;;;;;;;;;; ;; Records ;; From 85901876312dd38972871e8ee79e8fc2ae971207 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 26 Nov 2024 14:05:07 -0500 Subject: [PATCH 05/24] Transient correctly --- src/basilisp/core.lpy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 422b2c535..906c15471 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -6995,9 +6995,9 @@ (let [updated-mappings (->> proxy-mappings (reduce* (fn [m [k v]] (if v - (assoc m k v) - (dissoc m k))) - (.- self -proxy-mappings)) + (assoc! m k v) + (dissoc! m k))) + (transient (.- self -proxy-mappings))) (persistent!))] (set! (.- self -proxy-mappings) updated-mappings) nil)) From abda4830b713bdc21a6324e4787c24b2bfc0e1b6 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Mon, 9 Dec 2024 11:05:10 -0500 Subject: [PATCH 06/24] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa4783ae..bdacf8e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v0.3.6] ### Added * Added support for the `:decorators` meta key in anonymous `fn`s (#1178) + * Added support for proxies (#425) ### Changed * `import` now returns nil instead of the last module's string representation (#1174) From ad05aef55b7dfc13cffe5d5e03c7c7485d85b45a Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 26 Nov 2024 14:10:29 -0500 Subject: [PATCH 07/24] Update error message --- src/basilisp/core.lpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 906c15471..899644991 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7059,7 +7059,7 @@ [proxy mappings] (if-not (instance? basilisp.lang.interfaces/IProxy proxy) (throw - (ex-info "Cannot get proxy mappings from object which does not implement IProxy" + (ex-info "Cannot set proxy mappings for an object which does not implement IProxy" {:obj proxy})) (do (. proxy (_set-proxy-mappings mappings)) From a123b2a680336d55ce924a71a253de2d1f72dd25 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 26 Nov 2024 14:20:26 -0500 Subject: [PATCH 08/24] Object base --- src/basilisp/core.lpy | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 899644991..cbffbbfe6 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7006,14 +7006,17 @@ (python/tuple (concat bases [basilisp.lang.interfaces/IProxy])) (python/dict (merge methods base-methods))))) -;; TODO: support object as super (defn get-proxy-class - "Given one or more base classes, return a proxy class for the given classes. + "Given zero or more base classes, return a proxy class for the given classes. + + If no classes, Python's ``object`` will be used as the superclass. Generated classes are cached, such that the same set of base classes will always return the same resulting proxy class." [& bases] - (let [base-set (set bases)] + (let [base-set (if (seq bases) + (set bases) + #{python/object})] (-> (swap! proxy-cache (fn [cache] (if (get cache base-set) cache From d3bb662cb4f72d26500bf0396d9558d9457cdde4 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Tue, 26 Nov 2024 14:38:19 -0500 Subject: [PATCH 09/24] Fix it up --- src/basilisp/core.lpy | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index cbffbbfe6..6a6967560 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7001,9 +7001,15 @@ (persistent!))] (set! (.- self -proxy-mappings) updated-mappings) nil)) - "_proxy_mappings" nil}] + "_proxy_mappings" nil} + + ;; Remove Python ``object`` from the bases if it is present to avoid errors + ;; about creating a consistent MRO for the given bases + proxy-bases (concat (remove #{python/object} bases) [basilisp.lang.interfaces/IProxy])] + #_(doseq [[method-name method] methods] + (println method-name method)) (python/type (basilisp.lang.util/genname "Proxy") - (python/tuple (concat bases [basilisp.lang.interfaces/IProxy])) + (python/tuple proxy-bases) (python/dict (merge methods base-methods))))) (defn get-proxy-class @@ -7020,7 +7026,7 @@ (-> (swap! proxy-cache (fn [cache] (if (get cache base-set) cache - (assoc cache base-set (proxy-type bases))))) + (assoc cache base-set (proxy-type base-set))))) (get base-set)))) (defn proxy-mappings @@ -7090,14 +7096,15 @@ (defmacro proxy [class-and-interfaces args & fs] (let [formatted-arity (fn [method-name [arg-vec & body]] - [method-name - (apply list method-name (vec (concat ['this] arg-vec)) body)]) + [(name method-name) + (apply list 'fn method-name (vec (concat ['this] arg-vec)) body)]) methods (mapcat (fn [[method-name & body]] (if (vector? (first body)) - [(formatted-arity method-name body)] + (formatted-arity method-name body) (map (partial formatted-arity method-name) body))) fs)] - `((get-proxy-class ~@class-and-interfaces) {} ~@args))) + #_(println methods) + `((get-proxy-class ~@class-and-interfaces) ~(apply hash-map methods) ~@args))) (defmacro proxy-super "Macro which expands to a call to the method named ``meth`` on the superclass From 80f5c15a819b7df130d1ff3d1a697f2613ee87ea Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 10:41:50 -0500 Subject: [PATCH 10/24] That's an oopsie --- src/basilisp/core.lpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 6a6967560..dd2165d58 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -6448,7 +6448,7 @@ (let [name-str (name interface-name) method-sigs (->> methods (map (fn [[method-name args docstring]] - [method-name (conj args 'self) docstring])) + [method-name (vec (concat ['self] args)) docstring])) (map #(list 'quote %)))] `(def ~interface-name (gen-interface :name ~name-str From 1ecb162390b6ab8cdf088d5ddc702de4506fbe0e Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 11:45:48 -0500 Subject: [PATCH 11/24] More stuff --- src/basilisp/core.lpy | 32 ++++++++++++++++++++--- tests/basilisp/test_proxies.lpy | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 tests/basilisp/test_proxies.lpy diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index dd2165d58..095bdfbbc 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7091,17 +7091,43 @@ (. proxy (_update-proxy-mappings mappings)) proxy))) -;; TODO: how to handle multi-arity fns ;; TODO: kwargs on supertypes +;; TODO: check interface/superclass method membership +;; TODO: check if duplicate methods (defmacro proxy + "Create a new proxy class instance. + + The proxy class may implement 0 or more interface (or subclass 0 or more classes), + which are given as the vector ``class-and-interfaces``. If 0 such supertypes are + provided, Python's ``object`` type will be used. + + If the supertype constructors take arguments, those arguments are given in the + potentially empty vector ``args``. + + The remaining forms (if any) should be method overrides for any methods of the + declared classes and interfaces. Not every method needs to be overridden. Override + declarations may be multi-arity to simulate multi-arity methods. Overrides need + not include ``this``, as it will be automatically added and is available within + all proxy methods. Proxy methods may access the proxy superclass using the + :lpy:fn:`proxy-super` macro. + + Overrides take the following form:: + + (single-arity [] + ...) + + (multi-arity + ([] ...) + ([arg1] ...) + ([arg1 & others] ...))" [class-and-interfaces args & fs] (let [formatted-arity (fn [method-name [arg-vec & body]] - [(name method-name) + [(munge method-name) (apply list 'fn method-name (vec (concat ['this] arg-vec)) body)]) methods (mapcat (fn [[method-name & body]] (if (vector? (first body)) (formatted-arity method-name body) - (map (partial formatted-arity method-name) body))) + (mapcat (partial formatted-arity method-name) body))) fs)] #_(println methods) `((get-proxy-class ~@class-and-interfaces) ~(apply hash-map methods) ~@args))) diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy new file mode 100644 index 000000000..376208214 --- /dev/null +++ b/tests/basilisp/test_proxies.lpy @@ -0,0 +1,45 @@ +(ns tests.basilisp.test-proxies + (:require + [basilisp.test :as test :refer [deftest is are testing]])) + +(def no-op-proxy + (proxy [] [])) + +(deftest get-proxy-class-test + (is (identical? (get-proxy-class) (get-proxy-class))) + (is (python/issubclass (get-proxy-class) basilisp.lang.interfaces/IProxy))) + +(deftest proxy-mappings-test + (is (= {} (proxy-mappings no-op-proxy))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (proxy-mappings (python/object))))) + +(deftest construct-proxy-test + (let [obj-proxy-cls (get-proxy-class)] + (is (instance? obj-proxy-cls (construct-proxy obj-proxy-cls))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (construct-proxy python/object))))) + +(definterface ToString + (to-string []) + (to-string [arg1]) + (to-string [arg1 & rest])) + +;; TODO: (?) needs to be mutable or setting _proxy_mappings on the instance fails +(deftype ConcreteToString [^:mutable arg] + ToString + (to-string [this] "0") + (to-string [this arg1] (str "1" arg1)) + (to-string [this arg1 & rest] (str "rest" arg1 rest))) + +(deftest proxy-test + (testing "multi-arity interface methods" + (let [p (proxy [ConcreteToString] [1] + (to-string + ([] "hi i am 0") + ([arg1] (str "i am 1 " arg1)) + ([arg1 & args] (str "i am rest " arg1 " " args))))] + (is (= "hi i am 0" (.to-string p))) + (is (= "i am 1 yes" (.to-string p "yes"))) + (is (= "i am rest first " (.to-string p "first"))) + (is (= "i am rest first (:yes)" (.to-string p "first" :yes)))))) From 605679bf3836b698addad283ff1e26d6facef110 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 11:55:29 -0500 Subject: [PATCH 12/24] Check it --- src/basilisp/core.lpy | 3 ++- src/basilisp/lang/interfaces.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 095bdfbbc..360838878 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -6959,7 +6959,8 @@ (def ^:private excluded-proxy-methods #{"__getattribute__" "__init__" - "__new__"}) + "__new__" + "__subclasshook__"}) (def ^:private proxy-cache (atom {})) diff --git a/src/basilisp/lang/interfaces.py b/src/basilisp/lang/interfaces.py index 79a7d5e1a..91beb1fd4 100644 --- a/src/basilisp/lang/interfaces.py +++ b/src/basilisp/lang/interfaces.py @@ -385,16 +385,17 @@ class IProxy(ABC): __slots__ = () - _proxy_mappings: "IPersistentMap[str, Callable]" - + @abstractmethod def _get_proxy_mappings(self) -> "IPersistentMap[str, Callable]": raise NotImplementedError() + @abstractmethod def _set_proxy_mappings( self, proxy_mappings: "IPersistentMap[str, Callable]" ) -> None: raise NotImplementedError() + @abstractmethod def _update_proxy_mappings( self, proxy_mappings: "IPersistentMap[str, Callable]" ) -> None: From 75ebc77ff4498503000fd8731b8fedd16a7b279b Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 12:07:15 -0500 Subject: [PATCH 13/24] Fix multi-arity --- src/basilisp/core.lpy | 22 +++++++++++++++------- tests/basilisp/test_proxies.lpy | 1 - 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 360838878..c49dadd48 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7122,14 +7122,22 @@ ([arg1] ...) ([arg1 & others] ...))" [class-and-interfaces args & fs] - (let [formatted-arity (fn [method-name [arg-vec & body]] + (let [formatted-single (fn [method-name [arg-vec & body]] + [(munge method-name) + (apply list 'fn method-name (vec (concat ['this] arg-vec)) body)]) + formatted-multi (fn [method-name & arities] [(munge method-name) - (apply list 'fn method-name (vec (concat ['this] arg-vec)) body)]) - methods (mapcat (fn [[method-name & body]] - (if (vector? (first body)) - (formatted-arity method-name body) - (mapcat (partial formatted-arity method-name) body))) - fs)] + (apply list + 'fn + method-name + (map (fn [[arg-vec & body]] + (apply list (vec (concat ['this] arg-vec)) body)) + arities))]) + methods (mapcat (fn [[method-name & body]] + (if (vector? (first body)) + (formatted-single method-name body) + (apply formatted-multi method-name body))) + fs)] #_(println methods) `((get-proxy-class ~@class-and-interfaces) ~(apply hash-map methods) ~@args))) diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy index 376208214..20761234e 100644 --- a/tests/basilisp/test_proxies.lpy +++ b/tests/basilisp/test_proxies.lpy @@ -41,5 +41,4 @@ ([arg1 & args] (str "i am rest " arg1 " " args))))] (is (= "hi i am 0" (.to-string p))) (is (= "i am 1 yes" (.to-string p "yes"))) - (is (= "i am rest first " (.to-string p "first"))) (is (= "i am rest first (:yes)" (.to-string p "first" :yes)))))) From 368536f0f757a5025944525923657dd6972b07b2 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 12:15:18 -0500 Subject: [PATCH 14/24] Documentation --- src/basilisp/core.lpy | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index c49dadd48..214e9048c 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -6964,6 +6964,16 @@ (def ^:private proxy-cache (atom {})) +;; One consequence of adhering so closely to the Clojure proxy model is that this +;; style of dispatch method doesn't align well with the Basilisp style of defining +;; multi-arity methods (which involves creating the "main" entrypoint method which +;; dispatches to private implementations for all of the defined arities). +;; +;; Fortunately, since the public interface of even multi-arity methods is a single +;; public method, when callers provide a multi-arity override for such methods, +;; only the public entrypoint method is overridden in the proxy mappings. This +;; should be a sufficient compromise, but it does mean that the superclass arity +;; implementations are never overridden. (defn ^:private proxy-base-methods [base] (->> (inspect/getmembers base inspect/isroutine) @@ -7120,7 +7130,12 @@ (multi-arity ([] ...) ([arg1] ...) - ([arg1 & others] ...))" + ([arg1 & others] ...)) + + .. warning:: + + The ``proxy`` macro does not verify that the provided override implementations + arities match those of the method being overridden." [class-and-interfaces args & fs] (let [formatted-single (fn [method-name [arg-vec & body]] [(munge method-name) From be2448a5e15293c04f483bac95358fb0d91c7aec Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 13:17:35 -0500 Subject: [PATCH 15/24] Disallow duplicate method defs in `proxy` macro --- src/basilisp/core.lpy | 38 ++++++++++++++++++++------------- tests/basilisp/test_proxies.lpy | 8 +++++++ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 214e9048c..736e7aebe 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7104,7 +7104,6 @@ ;; TODO: kwargs on supertypes ;; TODO: check interface/superclass method membership -;; TODO: check if duplicate methods (defmacro proxy "Create a new proxy class instance. @@ -7140,21 +7139,30 @@ (let [formatted-single (fn [method-name [arg-vec & body]] [(munge method-name) (apply list 'fn method-name (vec (concat ['this] arg-vec)) body)]) - formatted-multi (fn [method-name & arities] - [(munge method-name) - (apply list - 'fn - method-name - (map (fn [[arg-vec & body]] - (apply list (vec (concat ['this] arg-vec)) body)) - arities))]) - methods (mapcat (fn [[method-name & body]] - (if (vector? (first body)) - (formatted-single method-name body) - (apply formatted-multi method-name body))) - fs)] + formatted-multi (fn [method-name & arities] + [(munge method-name) + (apply list + 'fn + method-name + (map (fn [[arg-vec & body]] + (apply list (vec (concat ['this] arg-vec)) body)) + arities))]) + methods (map (fn [[method-name & body]] + (if (vector? (first body)) + (formatted-single method-name body) + (apply formatted-multi method-name body))) + fs) + method-map (reduce* (fn [m [method-name method]] + (if-let [existing-method (get m method-name)] + (throw + (ex-info "Cannot define proxy class with duplicate method" + {:method-name method-name + :impls [existing-method method]})) + (assoc m method-name method))) + {} + methods)] #_(println methods) - `((get-proxy-class ~@class-and-interfaces) ~(apply hash-map methods) ~@args))) + `((get-proxy-class ~@class-and-interfaces) ~method-map ~@args))) (defmacro proxy-super "Macro which expands to a call to the method named ``meth`` on the superclass diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy index 20761234e..e10683e51 100644 --- a/tests/basilisp/test_proxies.lpy +++ b/tests/basilisp/test_proxies.lpy @@ -20,6 +20,9 @@ (is (thrown? basilisp.lang.exception/ExceptionInfo (construct-proxy python/object))))) +(definterface Describable + (describe-me [])) + (definterface ToString (to-string []) (to-string [arg1]) @@ -33,6 +36,11 @@ (to-string [this arg1 & rest] (str "rest" arg1 rest))) (deftest proxy-test + (testing "disallows duplicate method overrides" + (proxy [Describable] [] + (describe-me [] "I'm a proxy") + (describe-me [] "Proxy"))) + (testing "multi-arity interface methods" (let [p (proxy [ConcreteToString] [1] (to-string From a0fff5a3d7176f6587711c8a4b7f17b4fda5368f Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 13:48:58 -0500 Subject: [PATCH 16/24] Sure --- src/basilisp/core.lpy | 42 +++++++++++++++++++++------------ tests/basilisp/test_proxies.lpy | 7 +++--- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 736e7aebe..15c9e420c 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7010,7 +7010,7 @@ (dissoc! m k))) (transient (.- self -proxy-mappings))) (persistent!))] - (set! (.- self -proxy-mappings) updated-mappings) + (. self (_set_proxy_mappings updated-mappings)) nil)) "_proxy_mappings" nil} @@ -7102,7 +7102,6 @@ (. proxy (_update-proxy-mappings mappings)) proxy))) -;; TODO: kwargs on supertypes ;; TODO: check interface/superclass method membership (defmacro proxy "Create a new proxy class instance. @@ -7131,26 +7130,39 @@ ([arg1] ...) ([arg1 & others] ...)) + .. note:: + + Proxy override methods can be defined with Python keyword argument support since + they are just standard Basilisp functions. See :ref:`basilisp_functions_with_kwargs` + for more information. + .. warning:: The ``proxy`` macro does not verify that the provided override implementations arities match those of the method being overridden." [class-and-interfaces args & fs] (let [formatted-single (fn [method-name [arg-vec & body]] - [(munge method-name) - (apply list 'fn method-name (vec (concat ['this] arg-vec)) body)]) + (apply list + 'fn + method-name + (with-meta (vec (concat ['this] arg-vec)) (meta arg-vec)) + body)) formatted-multi (fn [method-name & arities] - [(munge method-name) - (apply list - 'fn - method-name - (map (fn [[arg-vec & body]] - (apply list (vec (concat ['this] arg-vec)) body)) - arities))]) - methods (map (fn [[method-name & body]] - (if (vector? (first body)) - (formatted-single method-name body) - (apply formatted-multi method-name body))) + (apply list + 'fn + method-name + (map (fn [[arg-vec & body]] + (apply list + (with-meta (vec (concat ['this] arg-vec)) (meta arg-vec)) + body)) + arities))) + methods (map (fn [[method-name & body :as form]] + [(munge method-name) + (with-meta + (if (vector? (first body)) + (formatted-single method-name body) + (apply formatted-multi method-name body)) + (meta form))]) fs) method-map (reduce* (fn [m [method-name method]] (if-let [existing-method (get m method-name)] diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy index e10683e51..e9d5edf26 100644 --- a/tests/basilisp/test_proxies.lpy +++ b/tests/basilisp/test_proxies.lpy @@ -37,9 +37,10 @@ (deftest proxy-test (testing "disallows duplicate method overrides" - (proxy [Describable] [] - (describe-me [] "I'm a proxy") - (describe-me [] "Proxy"))) + (is (thrown? basilisp.lang.compiler/CompilerException + (eval '(proxy [Describable] [] + (describe-me [] "I'm a proxy") + (describe-me [] "Proxy")))))) (testing "multi-arity interface methods" (let [p (proxy [ConcreteToString] [1] From 6152cd93678a88e18902e23bfb0c1c1c75e1694f Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Wed, 27 Nov 2024 14:05:21 -0500 Subject: [PATCH 17/24] Check membership --- src/basilisp/core.lpy | 16 ++++++++++------ tests/basilisp/test_proxies.lpy | 5 +++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 15c9e420c..6f9609da5 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -6993,13 +6993,21 @@ "Generate a proxy class with the given bases." [bases] (let [methods (apply hash-map (mapcat proxy-base-methods bases)) + method-names (set (map munge (keys methods))) base-methods {"__init__" (fn __init__ [self proxy-mappings & args] (apply (.- (python/super (.- self __class__) self) __init__) args) - (set! (.- self -proxy-mappings) proxy-mappings) + (. self (_set_proxy_mappings proxy-mappings)) nil) "_get_proxy_mappings" (fn _get_proxy_mappings [self] (.- self -proxy-mappings)) "_set_proxy_mappings" (fn _set_proxy_mappings [self proxy-mappings] + (let [provided-methods (set (keys proxy-mappings))] + (when-not (.issubset provided-methods method-names) + (throw + (ex-info "Proxy override methods must correspond to methods on the declared supertypes" + {:expected method-names + :given provided-methods + :diff (.difference provided-methods method-names)})))) (set! (.- self -proxy-mappings) proxy-mappings) nil) "_update_proxy_mappings" (fn _update_proxy_mappings [self proxy-mappings] @@ -7016,9 +7024,7 @@ ;; Remove Python ``object`` from the bases if it is present to avoid errors ;; about creating a consistent MRO for the given bases - proxy-bases (concat (remove #{python/object} bases) [basilisp.lang.interfaces/IProxy])] - #_(doseq [[method-name method] methods] - (println method-name method)) + proxy-bases (concat (remove #{python/object} bases) [basilisp.lang.interfaces/IProxy])] (python/type (basilisp.lang.util/genname "Proxy") (python/tuple proxy-bases) (python/dict (merge methods base-methods))))) @@ -7102,7 +7108,6 @@ (. proxy (_update-proxy-mappings mappings)) proxy))) -;; TODO: check interface/superclass method membership (defmacro proxy "Create a new proxy class instance. @@ -7173,7 +7178,6 @@ (assoc m method-name method))) {} methods)] - #_(println methods) `((get-proxy-class ~@class-and-interfaces) ~method-map ~@args))) (defmacro proxy-super diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy index e9d5edf26..2dde6af60 100644 --- a/tests/basilisp/test_proxies.lpy +++ b/tests/basilisp/test_proxies.lpy @@ -42,6 +42,11 @@ (describe-me [] "I'm a proxy") (describe-me [] "Proxy")))))) + (testing "disallows overriding non-superclass methods" + (is (thrown? basilisp.lang.exception/ExceptionInfo + (proxy [Describable] [] + (other-method [] "Proxy"))))) + (testing "multi-arity interface methods" (let [p (proxy [ConcreteToString] [1] (to-string From df4a918cc5fa0f6b49e96033a923ee258b57f127 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Sat, 19 Apr 2025 21:19:49 -0400 Subject: [PATCH 18/24] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdacf8e4d..e86e66b2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Added support for referring imported Python names as by `from ... import ...` (#1154) * Added the `basilisp.url` namespace for structured URL manipulation (#1239) + * Added support for proxies (#425) ### Changed * Removed implicit support for single-use iterables in sequences, and introduced `iterator-seq` to expliciltly handle them (#1192) @@ -37,7 +38,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v0.3.6] ### Added * Added support for the `:decorators` meta key in anonymous `fn`s (#1178) - * Added support for proxies (#425) ### Changed * `import` now returns nil instead of the last module's string representation (#1174) From edd68a4a8e0935d4dedc8c9a1e0c2b4c25f6012c Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Sat, 19 Apr 2025 21:33:22 -0400 Subject: [PATCH 19/24] Override attrs __setattr__ for _proxy_mappings attr specifically --- src/basilisp/core.lpy | 5 +++++ tests/basilisp/test_proxies.lpy | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 6f9609da5..c20473811 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7020,6 +7020,11 @@ (persistent!))] (. self (_set_proxy_mappings updated-mappings)) nil)) + "__setattr__" (fn __setattr__ [self attr val] + "Override __setattr__ specifically for _proxy_mappings." + (if (= attr "_proxy_mappings") + (. python/object __setattr__ self attr val) + ((.- (python/super (.- self __class__) self) __setattr__) attr val))) "_proxy_mappings" nil} ;; Remove Python ``object`` from the bases if it is present to avoid errors diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy index 2dde6af60..1a5974714 100644 --- a/tests/basilisp/test_proxies.lpy +++ b/tests/basilisp/test_proxies.lpy @@ -20,6 +20,18 @@ (is (thrown? basilisp.lang.exception/ExceptionInfo (construct-proxy python/object))))) +(deftest init-proxy-test + (let [obj-proxy (construct-proxy (get-proxy-class))] + (is (identical? obj-proxy (init-proxy obj-proxy {}))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (init-proxy (python/object) {}))))) + +(deftest update-proxy-test + (let [obj-proxy (construct-proxy (get-proxy-class))] + (is (identical? obj-proxy (update-proxy obj-proxy {}))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (update-proxy (python/object) {}))))) + (definterface Describable (describe-me [])) @@ -28,8 +40,7 @@ (to-string [arg1]) (to-string [arg1 & rest])) -;; TODO: (?) needs to be mutable or setting _proxy_mappings on the instance fails -(deftype ConcreteToString [^:mutable arg] +(deftype ConcreteToString [arg] ToString (to-string [this] "0") (to-string [this arg1] (str "1" arg1)) From 1cad7f739e5ffd957a48a946abc6e8d6b9da6740 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Sun, 20 Apr 2025 11:40:12 -0400 Subject: [PATCH 20/24] Allow disabling slots --- CHANGELOG.md | 1 + src/basilisp/core.lpy | 8 ++- src/basilisp/lang/compiler/analyzer.py | 7 ++- src/basilisp/lang/compiler/constants.py | 1 + src/basilisp/lang/compiler/generator.py | 2 +- src/basilisp/lang/compiler/nodes.py | 1 + tests/basilisp/test_proxies.lpy | 70 ++++++++++++++++--------- 7 files changed, 62 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e86e66b2d..5ce7046e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added support for referring imported Python names as by `from ... import ...` (#1154) * Added the `basilisp.url` namespace for structured URL manipulation (#1239) * Added support for proxies (#425) + * Added a `:slots` meta flag for `deftype` to disable creation of `__slots__` on created types (#1241) ### Changed * Removed implicit support for single-use iterables in sequences, and introduced `iterator-seq` to expliciltly handle them (#1192) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index c20473811..145b6dc87 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -7149,7 +7149,13 @@ .. warning:: The ``proxy`` macro does not verify that the provided override implementations - arities match those of the method being overridden." + arities match those of the method being overridden. + + .. warning:: + + Attempting to create a proxy with multiple superclasses defined with ``__slots__`` + may fail with a ``TypeError``. If you control any of the designated superclasses, + removing conflicting ``__slots__`` should enable creation of the proxy type." [class-and-interfaces args & fs] (let [formatted-single (fn [method-name [arg-vec & body]] (apply list diff --git a/src/basilisp/lang/compiler/analyzer.py b/src/basilisp/lang/compiler/analyzer.py index e52a51802..d8bd33529 100644 --- a/src/basilisp/lang/compiler/analyzer.py +++ b/src/basilisp/lang/compiler/analyzer.py @@ -69,6 +69,7 @@ SYM_PRIVATE_META_KEY, SYM_PROPERTY_META_KEY, SYM_REDEF_META_KEY, + SYM_SLOTS_META_KEY, SYM_STATICMETHOD_META_KEY, SYM_TAG_META_KEY, SYM_USE_VAR_INDIRECTION_KEY, @@ -659,13 +660,13 @@ def AnalyzerException( MetaGetter = Callable[[Union[IMeta, Var]], Any] -def _bool_meta_getter(meta_kw: kw.Keyword) -> BoolMetaGetter: +def _bool_meta_getter(meta_kw: kw.Keyword, default: bool = False) -> BoolMetaGetter: """Return a function which checks an object with metadata for a boolean value by meta_kw.""" def has_meta_prop(o: Union[IMeta, Var]) -> bool: return bool( - Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).or_else_get(False) + Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).or_else_get(default) ) return has_meta_prop @@ -688,6 +689,7 @@ def get_meta_prop(o: Union[IMeta, Var]) -> Any: _is_py_classmethod = _bool_meta_getter(SYM_CLASSMETHOD_META_KEY) _is_py_property = _bool_meta_getter(SYM_PROPERTY_META_KEY) _is_py_staticmethod = _bool_meta_getter(SYM_STATICMETHOD_META_KEY) +_is_slotted_type = _bool_meta_getter(SYM_SLOTS_META_KEY, True) _is_macro = _bool_meta_getter(SYM_MACRO_META_KEY) _is_no_inline = _bool_meta_getter(SYM_NO_INLINE_META_KEY) _is_allow_var_indirection = _bool_meta_getter(SYM_NO_WARN_ON_VAR_INDIRECTION_META_KEY) @@ -2022,6 +2024,7 @@ def _deftype_ast( # pylint: disable=too-many-locals verified_abstract=type_abstractness.is_statically_verified_as_abstract, artificially_abstract=type_abstractness.artificially_abstract_supertypes, is_frozen=is_frozen, + use_slots=_is_slotted_type(name), use_weakref_slot=not type_abstractness.supertype_already_weakref, env=ctx.get_node_env(pos=ctx.syntax_position), ) diff --git a/src/basilisp/lang/compiler/constants.py b/src/basilisp/lang/compiler/constants.py index d8a104494..3ca67020b 100644 --- a/src/basilisp/lang/compiler/constants.py +++ b/src/basilisp/lang/compiler/constants.py @@ -43,6 +43,7 @@ class SpecialForm: SYM_PRIVATE_META_KEY = kw.keyword("private") SYM_CLASSMETHOD_META_KEY = kw.keyword("classmethod") SYM_DEFAULT_META_KEY = kw.keyword("default") +SYM_SLOTS_META_KEY = kw.keyword("slots") SYM_DYNAMIC_META_KEY = kw.keyword("dynamic") SYM_PROPERTY_META_KEY = kw.keyword("property") SYM_MACRO_META_KEY = kw.keyword("macro") diff --git a/src/basilisp/lang/compiler/generator.py b/src/basilisp/lang/compiler/generator.py index 6581041e1..5c9ce203d 100644 --- a/src/basilisp/lang/compiler/generator.py +++ b/src/basilisp/lang/compiler/generator.py @@ -1548,7 +1548,7 @@ def _deftype_to_py_ast( # pylint: disable=too-many-locals verified_abstract=node.verified_abstract, artificially_abstract_bases=artificially_abstract_bases, is_frozen=node.is_frozen, - use_slots=True, + use_slots=node.use_slots, use_weakref_slot=node.use_weakref_slot, ), ast.Call( diff --git a/src/basilisp/lang/compiler/nodes.py b/src/basilisp/lang/compiler/nodes.py index 8d14ab186..36067f247 100644 --- a/src/basilisp/lang/compiler/nodes.py +++ b/src/basilisp/lang/compiler/nodes.py @@ -424,6 +424,7 @@ class DefType(Node[SpecialForm]): verified_abstract: bool = False artificially_abstract: IPersistentSet[DefTypeBase] = lset.EMPTY is_frozen: bool = True + use_slots: bool = True use_weakref_slot: bool = True meta: NodeMeta = None children: Sequence[kw.Keyword] = vec.v(FIELDS, MEMBERS) diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy index 1a5974714..0f3424232 100644 --- a/tests/basilisp/test_proxies.lpy +++ b/tests/basilisp/test_proxies.lpy @@ -5,14 +5,52 @@ (def no-op-proxy (proxy [] [])) +(definterface Describable + (describe-me [])) + +(deftype ^{:slots false} DescribableType [arg] + Describable + (describe-me [this] (str "I'm a type with " arg))) + +(def single-arity-proxy + (proxy [DescribableType] [:orig] + (describe-me [] + (str "Proxy with: " (proxy-super describe-me))))) + +(definterface ToString + (to-string []) + (to-string [arg1]) + (to-string [arg1 & rest])) + +(deftype ^{:slots false} ConcreteToString [arg] + ToString + (to-string [this] "0") + (to-string [this arg1] (str "1" arg1)) + (to-string [this arg1 & rest] (str "rest" arg1 rest))) + +(def multi-arity-proxy + (proxy [ConcreteToString] [1] + (to-string + ([] "hi i am 0") + ([arg1] (str "i am 1 " arg1)) + ([arg1 & args] (str "i am rest " arg1 " " args))))) + (deftest get-proxy-class-test (is (identical? (get-proxy-class) (get-proxy-class))) - (is (python/issubclass (get-proxy-class) basilisp.lang.interfaces/IProxy))) + (is (python/issubclass (get-proxy-class) basilisp.lang.interfaces/IProxy)) + (is (identical? (get-proxy-class DescribableType) (get-proxy-class DescribableType))) + (is (python/issubclass (get-proxy-class DescribableType) basilisp.lang.interfaces/IProxy)) + (is (identical? (get-proxy-class DescribableType ConcreteToString) + (get-proxy-class DescribableType ConcreteToString))) + (is (python/issubclass (get-proxy-class DescribableType ConcreteToString) + basilisp.lang.interfaces/IProxy))) (deftest proxy-mappings-test (is (= {} (proxy-mappings no-op-proxy))) (is (thrown? basilisp.lang.exception/ExceptionInfo - (proxy-mappings (python/object))))) + (proxy-mappings (python/object)))) + (is (= #{"describe_me"} (set (keys (proxy-mappings single-arity-proxy))))) + (is (= #{"to_string"} (set (keys (proxy-mappings multi-arity-proxy)))))) (deftest construct-proxy-test (let [obj-proxy-cls (get-proxy-class)] @@ -32,20 +70,6 @@ (is (thrown? basilisp.lang.exception/ExceptionInfo (update-proxy (python/object) {}))))) -(definterface Describable - (describe-me [])) - -(definterface ToString - (to-string []) - (to-string [arg1]) - (to-string [arg1 & rest])) - -(deftype ConcreteToString [arg] - ToString - (to-string [this] "0") - (to-string [this arg1] (str "1" arg1)) - (to-string [this arg1 & rest] (str "rest" arg1 rest))) - (deftest proxy-test (testing "disallows duplicate method overrides" (is (thrown? basilisp.lang.compiler/CompilerException @@ -58,12 +82,10 @@ (proxy [Describable] [] (other-method [] "Proxy"))))) + (testing "single-arity interface method" + (is (= "Proxy with: I'm a type with :orig" (.describe-me single-arity-proxy)))) + (testing "multi-arity interface methods" - (let [p (proxy [ConcreteToString] [1] - (to-string - ([] "hi i am 0") - ([arg1] (str "i am 1 " arg1)) - ([arg1 & args] (str "i am rest " arg1 " " args))))] - (is (= "hi i am 0" (.to-string p))) - (is (= "i am 1 yes" (.to-string p "yes"))) - (is (= "i am rest first (:yes)" (.to-string p "first" :yes)))))) + (is (= "hi i am 0" (.to-string multi-arity-proxy))) + (is (= "i am 1 yes" (.to-string multi-arity-proxy "yes"))) + (is (= "i am rest first (:yes)" (.to-string multi-arity-proxy "first" :yes))))) From 72e8b25ccff1e1f1568f8319a613868210ad51ab Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Sun, 20 Apr 2025 11:43:51 -0400 Subject: [PATCH 21/24] Documentation --- src/basilisp/core.lpy | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 145b6dc87..4c3259b27 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -6884,7 +6884,16 @@ Methods must supply a ``this`` or ``self`` parameter. ``recur`` special forms used in the body of a method should not include that parameter, as it will be supplied - automatically." + automatically. + + .. note:: + + ``deftype`` creates new types with ``__slots__`` by default. To disable usage + of ``__slots__``, provide the ``^{:slots false}`` meta key on the type name. + + .. code-block:: + + (deftype ^{:slots false} Point [x y z])" [type-name fields & method-impls] (let [ctor-name (with-meta (symbol (str "->" (name type-name))) From 1031c55be1a9770dc95ff4bd24c3e1508067bf05 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Sun, 20 Apr 2025 12:17:58 -0400 Subject: [PATCH 22/24] More tests --- tests/basilisp/test_proxies.lpy | 66 +++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/tests/basilisp/test_proxies.lpy b/tests/basilisp/test_proxies.lpy index 0f3424232..4b8d742f2 100644 --- a/tests/basilisp/test_proxies.lpy +++ b/tests/basilisp/test_proxies.lpy @@ -24,15 +24,15 @@ (deftype ^{:slots false} ConcreteToString [arg] ToString - (to-string [this] "0") - (to-string [this arg1] (str "1" arg1)) - (to-string [this arg1 & rest] (str "rest" arg1 rest))) + (to-string [this] (str "0 " arg)) + (to-string [this arg1] (str "1 " arg " " arg1)) + (to-string [this arg1 & rest] (str "rest" arg arg1 rest))) (def multi-arity-proxy (proxy [ConcreteToString] [1] (to-string - ([] "hi i am 0") - ([arg1] (str "i am 1 " arg1)) + ([] (str "hi i am 0 arg " (proxy-super to-string))) + ([arg1] (str "i am 1 arg " (proxy-super to-string arg1))) ([arg1 & args] (str "i am rest " arg1 " " args))))) (deftest get-proxy-class-test @@ -53,22 +53,50 @@ (is (= #{"to_string"} (set (keys (proxy-mappings multi-arity-proxy)))))) (deftest construct-proxy-test - (let [obj-proxy-cls (get-proxy-class)] - (is (instance? obj-proxy-cls (construct-proxy obj-proxy-cls))) - (is (thrown? basilisp.lang.exception/ExceptionInfo - (construct-proxy python/object))))) + (testing "no args" + (let [obj-proxy-cls (get-proxy-class)] + (is (instance? obj-proxy-cls (construct-proxy obj-proxy-cls))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (construct-proxy python/object))))) + + (testing "superclass args" + (let [obj-proxy-cls (get-proxy-class DescribableType)] + (is (instance? obj-proxy-cls (construct-proxy obj-proxy-cls 1))) + (is (thrown? python/TypeError (construct-proxy obj-proxy-cls)))))) (deftest init-proxy-test - (let [obj-proxy (construct-proxy (get-proxy-class))] - (is (identical? obj-proxy (init-proxy obj-proxy {}))) - (is (thrown? basilisp.lang.exception/ExceptionInfo - (init-proxy (python/object) {}))))) + (testing "no proxy methods" + (let [obj-proxy (construct-proxy (get-proxy-class))] + (is (identical? obj-proxy (init-proxy obj-proxy {}))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (init-proxy (python/object) {}))))) + + (testing "has proxy methods" + (let [obj-proxy (proxy [DescribableType] [1] + (describe-me [] "I'm a proxy"))] + (is (= "I'm a proxy" (.describe-me obj-proxy))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (init-proxy obj-proxy {"some-other-method" identity}))) + (init-proxy obj-proxy {"describe_me" (fn [this] "I'm not a proxy")}) + (is (= "I'm not a proxy" (.describe-me obj-proxy)))))) (deftest update-proxy-test - (let [obj-proxy (construct-proxy (get-proxy-class))] - (is (identical? obj-proxy (update-proxy obj-proxy {}))) - (is (thrown? basilisp.lang.exception/ExceptionInfo - (update-proxy (python/object) {}))))) + (testing "no proxy methods" + (let [obj-proxy (construct-proxy (get-proxy-class))] + (is (identical? obj-proxy (update-proxy obj-proxy {}))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (update-proxy (python/object) {}))))) + + (testing "has proxy methods" + (let [obj-proxy (proxy [DescribableType] [1] + (describe-me [] "I'm a proxy"))] + (is (= "I'm a proxy" (.describe-me obj-proxy))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (update-proxy obj-proxy {"some-other-method" identity}))) + (update-proxy obj-proxy {"describe_me" nil}) + (is (= "I'm a type with 1" (.describe-me obj-proxy))) + (update-proxy obj-proxy {"describe_me" (fn [this] "I'm a proxy again")}) + (is (= "I'm a proxy again" (.describe-me obj-proxy)))))) (deftest proxy-test (testing "disallows duplicate method overrides" @@ -86,6 +114,6 @@ (is (= "Proxy with: I'm a type with :orig" (.describe-me single-arity-proxy)))) (testing "multi-arity interface methods" - (is (= "hi i am 0" (.to-string multi-arity-proxy))) - (is (= "i am 1 yes" (.to-string multi-arity-proxy "yes"))) + (is (= "hi i am 0 arg 0 1" (.to-string multi-arity-proxy))) + (is (= "i am 1 arg 1 1 yes" (.to-string multi-arity-proxy "yes"))) (is (= "i am rest first (:yes)" (.to-string multi-arity-proxy "first" :yes))))) From 9892f89fd4538a640b8b8ca8bc903b92c354d3f4 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Sun, 20 Apr 2025 12:30:15 -0400 Subject: [PATCH 23/24] Documentation --- docs/pyinterop.rst | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/pyinterop.rst b/docs/pyinterop.rst index 7925ffcbf..97504ee67 100644 --- a/docs/pyinterop.rst +++ b/docs/pyinterop.rst @@ -411,4 +411,24 @@ Users still have the option to use the native :external:py:func:`operator.floord Proxies ------- -TBD +Basilisp supports creating instances of anonymous classes deriving from one or more concrete types with the :lpy:fn:`proxy` macro. +It may be necessary to use ``proxy`` in preference to :lpy:fn:`reify` for cases when the superclass type is concrete, where ``reify`` would otherwise fail. +Proxies can also be useful in cases where it is necessary to wrap superclass methods with additional functionality or access internal state of class instances. + +.. code-block:: + + (def p + (proxy [io/StringIO] [] + (write [s] + (println "length" (count s)) + (proxy-super write s)))) + + (.write p "blah") ;; => 4 + ;; prints "length 4" + (.getvalue p) ;; => "blah" + + .. seealso:: + + lpy:fn:`proxy`, :lpy:fn:`proxy-mappings`, :lpy:fn:`proxy-super`, + :lpy:fn:`construct-proxy`, :lpy:fn:`init-proxy`, :lpy:fn:`update-proxy`, + :lpy:fn:`get-proxy-class` From b78a81a0bbbea29d3342268fb746bc37439b58f0 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Sun, 20 Apr 2025 12:31:49 -0400 Subject: [PATCH 24/24] Correct docs --- docs/pyinterop.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pyinterop.rst b/docs/pyinterop.rst index 97504ee67..a811619c3 100644 --- a/docs/pyinterop.rst +++ b/docs/pyinterop.rst @@ -427,8 +427,8 @@ Proxies can also be useful in cases where it is necessary to wrap superclass met ;; prints "length 4" (.getvalue p) ;; => "blah" - .. seealso:: +.. seealso:: - lpy:fn:`proxy`, :lpy:fn:`proxy-mappings`, :lpy:fn:`proxy-super`, - :lpy:fn:`construct-proxy`, :lpy:fn:`init-proxy`, :lpy:fn:`update-proxy`, - :lpy:fn:`get-proxy-class` + :lpy:fn:`proxy`, :lpy:fn:`proxy-mappings`, :lpy:fn:`proxy-super`, + :lpy:fn:`construct-proxy`, :lpy:fn:`init-proxy`, :lpy:fn:`update-proxy`, + :lpy:fn:`get-proxy-class`