Skip to content

Commit 48fd08f

Browse files
authored
Proxies (#1147)
Fixes #425 Fixes #1241
1 parent 74b2846 commit 48fd08f

File tree

9 files changed

+446
-5
lines changed

9 files changed

+446
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added
99
* Added support for referring imported Python names as by `from ... import ...` (#1154)
1010
* Added the `basilisp.url` namespace for structured URL manipulation (#1239)
11+
* Added support for proxies (#425)
12+
* Added a `:slots` meta flag for `deftype` to disable creation of `__slots__` on created types (#1241)
1113

1214
### Changed
1315
* Removed implicit support for single-use iterables in sequences, and introduced `iterator-seq` to expliciltly handle them (#1192)

docs/pyinterop.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,30 @@ Users still have the option to use the native :external:py:func:`operator.floord
405405
.. seealso::
406406

407407
:lpy:fn:`quot`, :lpy:fn:`rem`, :lpy:fn:`mod`
408+
409+
.. _proxies:
410+
411+
Proxies
412+
-------
413+
414+
Basilisp supports creating instances of anonymous classes deriving from one or more concrete types with the :lpy:fn:`proxy` macro.
415+
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.
416+
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.
417+
418+
.. code-block::
419+
420+
(def p
421+
(proxy [io/StringIO] []
422+
(write [s]
423+
(println "length" (count s))
424+
(proxy-super write s))))
425+
426+
(.write p "blah") ;; => 4
427+
;; prints "length 4"
428+
(.getvalue p) ;; => "blah"
429+
430+
.. seealso::
431+
432+
:lpy:fn:`proxy`, :lpy:fn:`proxy-mappings`, :lpy:fn:`proxy-super`,
433+
:lpy:fn:`construct-proxy`, :lpy:fn:`init-proxy`, :lpy:fn:`update-proxy`,
434+
:lpy:fn:`get-proxy-class`

src/basilisp/core.lpy

Lines changed: 260 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
decimal
88
fractions
99
importlib.util
10+
inspect
1011
math
1112
multiprocessing
1213
os
@@ -6447,7 +6448,7 @@
64476448
(let [name-str (name interface-name)
64486449
method-sigs (->> methods
64496450
(map (fn [[method-name args docstring]]
6450-
[method-name (conj args 'self) docstring]))
6451+
[method-name (vec (concat ['self] args)) docstring]))
64516452
(map #(list 'quote %)))]
64526453
`(def ~interface-name
64536454
(gen-interface :name ~name-str
@@ -6883,7 +6884,16 @@
68836884

68846885
Methods must supply a ``this`` or ``self`` parameter. ``recur`` special forms used in
68856886
the body of a method should not include that parameter, as it will be supplied
6886-
automatically."
6887+
automatically.
6888+
6889+
.. note::
6890+
6891+
``deftype`` creates new types with ``__slots__`` by default. To disable usage
6892+
of ``__slots__``, provide the ``^{:slots false}`` meta key on the type name.
6893+
6894+
.. code-block::
6895+
6896+
(deftype ^{:slots false} Point [x y z])"
68876897
[type-name fields & method-impls]
68886898
(let [ctor-name (with-meta
68896899
(symbol (str "->" (name type-name)))
@@ -6951,6 +6961,254 @@
69516961
~@methods)
69526962
(meta &form))))
69536963

6964+
;;;;;;;;;;;;;
6965+
;; Proxies ;;
6966+
;;;;;;;;;;;;;
6967+
6968+
(def ^:private excluded-proxy-methods
6969+
#{"__getattribute__"
6970+
"__init__"
6971+
"__new__"
6972+
"__subclasshook__"})
6973+
6974+
(def ^:private proxy-cache (atom {}))
6975+
6976+
;; One consequence of adhering so closely to the Clojure proxy model is that this
6977+
;; style of dispatch method doesn't align well with the Basilisp style of defining
6978+
;; multi-arity methods (which involves creating the "main" entrypoint method which
6979+
;; dispatches to private implementations for all of the defined arities).
6980+
;;
6981+
;; Fortunately, since the public interface of even multi-arity methods is a single
6982+
;; public method, when callers provide a multi-arity override for such methods,
6983+
;; only the public entrypoint method is overridden in the proxy mappings. This
6984+
;; should be a sufficient compromise, but it does mean that the superclass arity
6985+
;; implementations are never overridden.
6986+
(defn ^:private proxy-base-methods
6987+
[base]
6988+
(->> (inspect/getmembers base inspect/isroutine)
6989+
(remove (comp excluded-proxy-methods first))
6990+
(mapcat (fn [[method-name method]]
6991+
(let [meth-sym (symbol method-name)
6992+
meth `(fn ~meth-sym [~'self & args#]
6993+
(if-let [override (get (.- ~'self ~'-proxy-mappings) ~method-name)]
6994+
(apply override ~'self args#)
6995+
(-> (.- ~'self __class__)
6996+
(python/super ~'self)
6997+
(.- ~meth-sym)
6998+
(apply args#))))]
6999+
[method-name (eval meth *ns*)])))))
7000+
7001+
(defn ^:private proxy-type
7002+
"Generate a proxy class with the given bases."
7003+
[bases]
7004+
(let [methods (apply hash-map (mapcat proxy-base-methods bases))
7005+
method-names (set (map munge (keys methods)))
7006+
base-methods {"__init__" (fn __init__ [self proxy-mappings & args]
7007+
(apply (.- (python/super (.- self __class__) self) __init__) args)
7008+
(. self (_set_proxy_mappings proxy-mappings))
7009+
nil)
7010+
"_get_proxy_mappings" (fn _get_proxy_mappings [self]
7011+
(.- self -proxy-mappings))
7012+
"_set_proxy_mappings" (fn _set_proxy_mappings [self proxy-mappings]
7013+
(let [provided-methods (set (keys proxy-mappings))]
7014+
(when-not (.issubset provided-methods method-names)
7015+
(throw
7016+
(ex-info "Proxy override methods must correspond to methods on the declared supertypes"
7017+
{:expected method-names
7018+
:given provided-methods
7019+
:diff (.difference provided-methods method-names)}))))
7020+
(set! (.- self -proxy-mappings) proxy-mappings)
7021+
nil)
7022+
"_update_proxy_mappings" (fn _update_proxy_mappings [self proxy-mappings]
7023+
(let [updated-mappings (->> proxy-mappings
7024+
(reduce* (fn [m [k v]]
7025+
(if v
7026+
(assoc! m k v)
7027+
(dissoc! m k)))
7028+
(transient (.- self -proxy-mappings)))
7029+
(persistent!))]
7030+
(. self (_set_proxy_mappings updated-mappings))
7031+
nil))
7032+
"__setattr__" (fn __setattr__ [self attr val]
7033+
"Override __setattr__ specifically for _proxy_mappings."
7034+
(if (= attr "_proxy_mappings")
7035+
(. python/object __setattr__ self attr val)
7036+
((.- (python/super (.- self __class__) self) __setattr__) attr val)))
7037+
"_proxy_mappings" nil}
7038+
7039+
;; Remove Python ``object`` from the bases if it is present to avoid errors
7040+
;; about creating a consistent MRO for the given bases
7041+
proxy-bases (concat (remove #{python/object} bases) [basilisp.lang.interfaces/IProxy])]
7042+
(python/type (basilisp.lang.util/genname "Proxy")
7043+
(python/tuple proxy-bases)
7044+
(python/dict (merge methods base-methods)))))
7045+
7046+
(defn get-proxy-class
7047+
"Given zero or more base classes, return a proxy class for the given classes.
7048+
7049+
If no classes, Python's ``object`` will be used as the superclass.
7050+
7051+
Generated classes are cached, such that the same set of base classes will always
7052+
return the same resulting proxy class."
7053+
[& bases]
7054+
(let [base-set (if (seq bases)
7055+
(set bases)
7056+
#{python/object})]
7057+
(-> (swap! proxy-cache (fn [cache]
7058+
(if (get cache base-set)
7059+
cache
7060+
(assoc cache base-set (proxy-type base-set)))))
7061+
(get base-set))))
7062+
7063+
(defn proxy-mappings
7064+
"Return the current method map for the given proxy.
7065+
7066+
Throws an exception if ``proxy`` is not a proxy."
7067+
[proxy]
7068+
(if-not (instance? basilisp.lang.interfaces/IProxy proxy)
7069+
(throw
7070+
(ex-info "Cannot get proxy mappings from object which does not implement IProxy"
7071+
{:obj proxy}))
7072+
(. proxy (_get-proxy-mappings))))
7073+
7074+
(defn construct-proxy
7075+
"Construct an instance of the proxy class ``c`` with the given constructor arguments.
7076+
7077+
Throws an exception if ``c`` is not a subclass of
7078+
:py:class:`basilisp.lang.interfaces.IProxy`.
7079+
7080+
.. note::
7081+
7082+
In JVM Clojure, this function is useful for selecting a specific constructor based
7083+
on argument count, but Python does not support multi-arity methods (including
7084+
constructors), so this is likely of limited use."
7085+
[c & ctor-args]
7086+
(if-not (python/issubclass c basilisp.lang.interfaces/IProxy)
7087+
(throw
7088+
(ex-info "Cannot construct instance of class which is not a subclass of IProxy"
7089+
{:class c :args ctor-args}))
7090+
(apply c {} ctor-args)))
7091+
7092+
(defn init-proxy
7093+
"Set the current proxy method map for the given proxy.
7094+
7095+
Method maps are maps of string method names to their implementations as Basilisp
7096+
functions.
7097+
7098+
Throws an exception if ``proxy`` is not a proxy."
7099+
[proxy mappings]
7100+
(if-not (instance? basilisp.lang.interfaces/IProxy proxy)
7101+
(throw
7102+
(ex-info "Cannot set proxy mappings for an object which does not implement IProxy"
7103+
{:obj proxy}))
7104+
(do
7105+
(. proxy (_set-proxy-mappings mappings))
7106+
proxy)))
7107+
7108+
(defn update-proxy
7109+
"Update the current proxy method map for the given proxy.
7110+
7111+
Method maps are maps of string method names to their implementations as Basilisp
7112+
functions. If ``nil`` is passed in place of a function for a method, that method will
7113+
revert to its default behavior.
7114+
7115+
Throws an exception if ``proxy`` is not a proxy."
7116+
[proxy mappings]
7117+
(if-not (instance? basilisp.lang.interfaces/IProxy proxy)
7118+
(throw
7119+
(ex-info "Cannot update proxy mappings for object which does not implement IProxy"
7120+
{:obj proxy}))
7121+
(do
7122+
(. proxy (_update-proxy-mappings mappings))
7123+
proxy)))
7124+
7125+
(defmacro proxy
7126+
"Create a new proxy class instance.
7127+
7128+
The proxy class may implement 0 or more interface (or subclass 0 or more classes),
7129+
which are given as the vector ``class-and-interfaces``. If 0 such supertypes are
7130+
provided, Python's ``object`` type will be used.
7131+
7132+
If the supertype constructors take arguments, those arguments are given in the
7133+
potentially empty vector ``args``.
7134+
7135+
The remaining forms (if any) should be method overrides for any methods of the
7136+
declared classes and interfaces. Not every method needs to be overridden. Override
7137+
declarations may be multi-arity to simulate multi-arity methods. Overrides need
7138+
not include ``this``, as it will be automatically added and is available within
7139+
all proxy methods. Proxy methods may access the proxy superclass using the
7140+
:lpy:fn:`proxy-super` macro.
7141+
7142+
Overrides take the following form::
7143+
7144+
(single-arity []
7145+
...)
7146+
7147+
(multi-arity
7148+
([] ...)
7149+
([arg1] ...)
7150+
([arg1 & others] ...))
7151+
7152+
.. note::
7153+
7154+
Proxy override methods can be defined with Python keyword argument support since
7155+
they are just standard Basilisp functions. See :ref:`basilisp_functions_with_kwargs`
7156+
for more information.
7157+
7158+
.. warning::
7159+
7160+
The ``proxy`` macro does not verify that the provided override implementations
7161+
arities match those of the method being overridden.
7162+
7163+
.. warning::
7164+
7165+
Attempting to create a proxy with multiple superclasses defined with ``__slots__``
7166+
may fail with a ``TypeError``. If you control any of the designated superclasses,
7167+
removing conflicting ``__slots__`` should enable creation of the proxy type."
7168+
[class-and-interfaces args & fs]
7169+
(let [formatted-single (fn [method-name [arg-vec & body]]
7170+
(apply list
7171+
'fn
7172+
method-name
7173+
(with-meta (vec (concat ['this] arg-vec)) (meta arg-vec))
7174+
body))
7175+
formatted-multi (fn [method-name & arities]
7176+
(apply list
7177+
'fn
7178+
method-name
7179+
(map (fn [[arg-vec & body]]
7180+
(apply list
7181+
(with-meta (vec (concat ['this] arg-vec)) (meta arg-vec))
7182+
body))
7183+
arities)))
7184+
methods (map (fn [[method-name & body :as form]]
7185+
[(munge method-name)
7186+
(with-meta
7187+
(if (vector? (first body))
7188+
(formatted-single method-name body)
7189+
(apply formatted-multi method-name body))
7190+
(meta form))])
7191+
fs)
7192+
method-map (reduce* (fn [m [method-name method]]
7193+
(if-let [existing-method (get m method-name)]
7194+
(throw
7195+
(ex-info "Cannot define proxy class with duplicate method"
7196+
{:method-name method-name
7197+
:impls [existing-method method]}))
7198+
(assoc m method-name method)))
7199+
{}
7200+
methods)]
7201+
`((get-proxy-class ~@class-and-interfaces) ~method-map ~@args)))
7202+
7203+
(defmacro proxy-super
7204+
"Macro which expands to a call to the method named ``meth`` on the superclass
7205+
with the provided ``args``.
7206+
7207+
Note this macro explicitly captures the implicit ``this`` parameter added to proxy
7208+
methods."
7209+
[meth & args]
7210+
`(. (~'python/super (.- ~'this ~'__class__) ~'this) (~meth ~@args)))
7211+
69547212
;;;;;;;;;;;;;
69557213
;; Records ;;
69567214
;;;;;;;;;;;;;

src/basilisp/lang/compiler/analyzer.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
SYM_PRIVATE_META_KEY,
7070
SYM_PROPERTY_META_KEY,
7171
SYM_REDEF_META_KEY,
72+
SYM_SLOTS_META_KEY,
7273
SYM_STATICMETHOD_META_KEY,
7374
SYM_TAG_META_KEY,
7475
SYM_USE_VAR_INDIRECTION_KEY,
@@ -659,13 +660,13 @@ def AnalyzerException(
659660
MetaGetter = Callable[[Union[IMeta, Var]], Any]
660661

661662

662-
def _bool_meta_getter(meta_kw: kw.Keyword) -> BoolMetaGetter:
663+
def _bool_meta_getter(meta_kw: kw.Keyword, default: bool = False) -> BoolMetaGetter:
663664
"""Return a function which checks an object with metadata for a boolean
664665
value by meta_kw."""
665666

666667
def has_meta_prop(o: Union[IMeta, Var]) -> bool:
667668
return bool(
668-
Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).or_else_get(False)
669+
Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).or_else_get(default)
669670
)
670671

671672
return has_meta_prop
@@ -688,6 +689,7 @@ def get_meta_prop(o: Union[IMeta, Var]) -> Any:
688689
_is_py_classmethod = _bool_meta_getter(SYM_CLASSMETHOD_META_KEY)
689690
_is_py_property = _bool_meta_getter(SYM_PROPERTY_META_KEY)
690691
_is_py_staticmethod = _bool_meta_getter(SYM_STATICMETHOD_META_KEY)
692+
_is_slotted_type = _bool_meta_getter(SYM_SLOTS_META_KEY, True)
691693
_is_macro = _bool_meta_getter(SYM_MACRO_META_KEY)
692694
_is_no_inline = _bool_meta_getter(SYM_NO_INLINE_META_KEY)
693695
_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
20222024
verified_abstract=type_abstractness.is_statically_verified_as_abstract,
20232025
artificially_abstract=type_abstractness.artificially_abstract_supertypes,
20242026
is_frozen=is_frozen,
2027+
use_slots=_is_slotted_type(name),
20252028
use_weakref_slot=not type_abstractness.supertype_already_weakref,
20262029
env=ctx.get_node_env(pos=ctx.syntax_position),
20272030
)

src/basilisp/lang/compiler/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class SpecialForm:
4343
SYM_PRIVATE_META_KEY = kw.keyword("private")
4444
SYM_CLASSMETHOD_META_KEY = kw.keyword("classmethod")
4545
SYM_DEFAULT_META_KEY = kw.keyword("default")
46+
SYM_SLOTS_META_KEY = kw.keyword("slots")
4647
SYM_DYNAMIC_META_KEY = kw.keyword("dynamic")
4748
SYM_PROPERTY_META_KEY = kw.keyword("property")
4849
SYM_MACRO_META_KEY = kw.keyword("macro")

src/basilisp/lang/compiler/generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1548,7 +1548,7 @@ def _deftype_to_py_ast( # pylint: disable=too-many-locals
15481548
verified_abstract=node.verified_abstract,
15491549
artificially_abstract_bases=artificially_abstract_bases,
15501550
is_frozen=node.is_frozen,
1551-
use_slots=True,
1551+
use_slots=node.use_slots,
15521552
use_weakref_slot=node.use_weakref_slot,
15531553
),
15541554
ast.Call(

src/basilisp/lang/compiler/nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ class DefType(Node[SpecialForm]):
424424
verified_abstract: bool = False
425425
artificially_abstract: IPersistentSet[DefTypeBase] = lset.EMPTY
426426
is_frozen: bool = True
427+
use_slots: bool = True
427428
use_weakref_slot: bool = True
428429
meta: NodeMeta = None
429430
children: Sequence[kw.Keyword] = vec.v(FIELDS, MEMBERS)

0 commit comments

Comments
 (0)