|
| 1 | +(ns basilisp.reflect |
| 2 | + "Runtime reflection of Python objects." |
| 3 | + (:import inspect |
| 4 | + types)) |
| 5 | + |
| 6 | +(defn ^:private qualname->sym |
| 7 | + "Canonicalize a Python ``__qualname__`` as a symbol." |
| 8 | + [qualname] |
| 9 | + (let [[namespace-or-name maybe-name] (.rsplit qualname "." 1)] |
| 10 | + (if (nil? maybe-name) |
| 11 | + (symbol namespace-or-name) |
| 12 | + (symbol namespace-or-name maybe-name)))) |
| 13 | + |
| 14 | +(defprotocol Reflectable |
| 15 | + (reflect* [this] |
| 16 | + "Reflect on ``this`` and return a map describing the object.")) |
| 17 | + |
| 18 | +(defn members->map |
| 19 | + "Given a seq of 2-tuples of ``member-name, member``, return a mapping of the |
| 20 | + member name converted to a symbol and the member." |
| 21 | + [m] |
| 22 | + (into {} |
| 23 | + (map (fn [[member-name member]] |
| 24 | + [(symbol member-name) (py->lisp member)])) |
| 25 | + m)) |
| 26 | + |
| 27 | +(defn ^:private ^:inline py-property? |
| 28 | + "Return ``true`` if this object is an instance of a Python ``property``." |
| 29 | + [o] |
| 30 | + (instance? python/property o)) |
| 31 | + |
| 32 | +(def ^:private method-like? |
| 33 | + "Predicate for determining if a class member can be treated similar to a method. |
| 34 | + |
| 35 | + Note that Python supports many different class member types beyond simple methods. |
| 36 | + It may be useful or even necessary to use :lpy:fn:`reflect` to assess the specific |
| 37 | + type of a method-like class member." |
| 38 | + (some-fn inspect/ismethod inspect/isfunction inspect/ismethoddescriptor inspect/isbuiltin)) |
| 39 | + |
| 40 | +(extend-protocol Reflectable |
| 41 | + types/ModuleType |
| 42 | + (reflect* [this] |
| 43 | + (let [is-basilisp-module? (instance? basilisp.lang.runtime/BasilispModule this) |
| 44 | + members-by-group (group-by (fn [[_ member]] |
| 45 | + (cond |
| 46 | + (inspect/ismodule member) :modules |
| 47 | + (inspect/isclass member) :classes |
| 48 | + (inspect/isfunction member) :functions |
| 49 | + :else :attributes)) |
| 50 | + (inspect/getmembers this))] |
| 51 | + {:name (symbol (python/getattr this "__name__")) |
| 52 | + :file (python/getattr this "__file__" nil) |
| 53 | + :package (python/getattr this "__package__" nil) |
| 54 | + :is-basilisp-module? is-basilisp-module? |
| 55 | + :basilisp-ns (when is-basilisp-module? |
| 56 | + (python/getattr this "__basilisp_namespace__")) |
| 57 | + :modules (members->map (:modules members-by-group)) |
| 58 | + :classes (members->map (:classes members-by-group)) |
| 59 | + :functions (members->map (:functions members-by-group)) |
| 60 | + :attributes (members->map (:attributes members-by-group))})) |
| 61 | + python/type |
| 62 | + (reflect* [this] |
| 63 | + (let [members-by-group (group-by (fn [[_ member]] |
| 64 | + (cond |
| 65 | + (method-like? member) :methods |
| 66 | + (py-property? member) :properties |
| 67 | + :else :attributes)) |
| 68 | + (inspect/getmembers this))] |
| 69 | + {:qualified-name (qualname->sym (python/getattr this "__qualname__")) |
| 70 | + :name (symbol (python/getattr this "__name__")) |
| 71 | + :bases (set (bases this)) |
| 72 | + :supers (supers this) |
| 73 | + :subclasses (subclasses this) |
| 74 | + :attributes (members->map (:attributes members-by-group)) |
| 75 | + :methods (members->map (:methods members-by-group)) |
| 76 | + :properties (members->map (:properties members-by-group))})) |
| 77 | + python/object |
| 78 | + (reflect* [this] |
| 79 | + (reflect* (python/type this))) |
| 80 | + nil |
| 81 | + (reflect* [this] |
| 82 | + nil)) |
| 83 | + |
| 84 | +;;;;;;;;;;;;;;; |
| 85 | +;; Callables ;; |
| 86 | +;;;;;;;;;;;;;;; |
| 87 | + |
| 88 | +(def ^:private inspect-sig-kind-mapping |
| 89 | + {inspect.Parameter/POSITIONAL_ONLY :positional-only |
| 90 | + inspect.Parameter/POSITIONAL_OR_KEYWORD :positional-or-keyword |
| 91 | + inspect.Parameter/VAR_POSITIONAL :var-positional |
| 92 | + inspect.Parameter/KEYWORD_ONLY :keyword-only |
| 93 | + inspect.Parameter/VAR_KEYWORD :var-keyword}) |
| 94 | + |
| 95 | +(defn ^:private signature->map |
| 96 | + "Convert a Python ``inspect.Signature`` object into a map. |
| 97 | + |
| 98 | + Signature maps include the following keys: |
| 99 | + |
| 100 | + :keyword ``:parameters``: an vector of maps describing parameters to the callable |
| 101 | + in the strict order they were defined; parameter map keys are defined below |
| 102 | + :keyword ``:return-annotation``: the return annotation of the callable object or |
| 103 | + ``::empty`` if no return annotation is defined |
| 104 | + |
| 105 | + Parameter maps include the following keys: |
| 106 | + |
| 107 | + :keyword ``:name``: the name of the parameter coerced to a symbol; the symbol |
| 108 | + will not be demunged |
| 109 | + :keyword ``:default``: the default value of this parameter if one is defined or |
| 110 | + ``::empty`` otherwise |
| 111 | + :keyword ``:annotation``: the annotation of this parameter if one is defined or |
| 112 | + ``::empty`` otherwise |
| 113 | + :keyword ``:kind``: the kind of Python parameter this is coerced to a keyword |
| 114 | + |
| 115 | + In cases where a field may contain a reference to the ``inspect.Signature.empty`` |
| 116 | + or ``inspect.Parameter.empty`` singletons, the corresponding Basilisp value is the |
| 117 | + namespaced keyword ``::empty``. |
| 118 | + " |
| 119 | + [^inspect/Signature sig] |
| 120 | + (let [return-anno (.-return-annotation sig)] |
| 121 | + {:parameters (mapv (fn [[param-name ^inspect/Parameter param]] |
| 122 | + (let [default (.-default param) |
| 123 | + anno (.-annotation param) |
| 124 | + kind (.-kind param)] |
| 125 | + {:name (symbol param-name) |
| 126 | + :default (if (operator/is default inspect.Parameter/empty) |
| 127 | + ::empty |
| 128 | + default) |
| 129 | + :annotation (if (operator/is anno inspect.Parameter/empty) |
| 130 | + ::empty |
| 131 | + anno) |
| 132 | + :kind (get inspect-sig-kind-mapping kind)})) |
| 133 | + (.items (.-parameters sig))) |
| 134 | + :return-annotation (if (operator/is return-anno inspect.Signature/empty) |
| 135 | + ::empty |
| 136 | + return-anno)})) |
| 137 | + |
| 138 | +(defn ^:private signature |
| 139 | + "Return the signature of a potentially callable object as a map if the signature |
| 140 | + can be determined, ``nil`` otherwise. |
| 141 | + |
| 142 | + Signature maps contain the keys as described in :lpy:fn:`signature->map`." |
| 143 | + [f] |
| 144 | + (try |
| 145 | + (-> (inspect/signature f) |
| 146 | + (signature->map)) |
| 147 | + (catch python/TypeError _ nil) |
| 148 | + (catch python/ValueError _ nil))) |
| 149 | + |
| 150 | +(defn ^:private reflect-callable |
| 151 | + [f] |
| 152 | + {:qualified-name (qualname->sym (python/getattr f "__qualname__")) |
| 153 | + :name (symbol (python/getattr f "__name__")) |
| 154 | + :signature (signature f) |
| 155 | + :module (inspect/getmodule f) |
| 156 | + :doc (inspect/getdoc f) |
| 157 | + :file (try |
| 158 | + (inspect/getfile f) |
| 159 | + (catch python/TypeError _ nil)) |
| 160 | + :is-basilisp-fn? (python/getattr f "_basilisp_fn" false) |
| 161 | + :is-class? (inspect/isclass f) |
| 162 | + :is-method? (inspect/ismethod f) |
| 163 | + :is-function? (inspect/isfunction f) |
| 164 | + :is-generator-fn? (inspect/isgeneratorfunction f) |
| 165 | + :is-generator? (inspect/isgenerator f) |
| 166 | + :is-coroutine? (inspect/iscoroutine f) |
| 167 | + :is-awaitable? (inspect/isawaitable f) |
| 168 | + :is-async-gen-fn? (inspect/isasyncgenfunction f) |
| 169 | + :is-builtin? (inspect/isbuiltin f) |
| 170 | + :is-method-wrapper? (inspect/ismethodwrapper f) |
| 171 | + :is-routine? (inspect/isroutine f) |
| 172 | + :is-method-descriptor? (inspect/ismethoddescriptor f)}) |
| 173 | + |
| 174 | +(extend types/FunctionType Reflectable {:reflect* reflect-callable}) |
| 175 | +(extend types/LambdaType Reflectable {:reflect* reflect-callable}) |
| 176 | +(extend types/CoroutineType Reflectable {:reflect* reflect-callable}) |
| 177 | +(extend types/MethodType Reflectable {:reflect* reflect-callable}) |
| 178 | +(extend types/BuiltinFunctionType Reflectable {:reflect* reflect-callable}) |
| 179 | +(extend types/BuiltinMethodType Reflectable {:reflect* reflect-callable}) |
| 180 | +(extend types/WrapperDescriptorType Reflectable {:reflect* reflect-callable}) |
| 181 | +(extend types/MethodWrapperType Reflectable {:reflect* reflect-callable}) |
| 182 | +(extend types/MethodDescriptorType Reflectable {:reflect* reflect-callable}) |
| 183 | +(extend types/ClassMethodDescriptorType Reflectable {:reflect* reflect-callable}) |
| 184 | + |
| 185 | +;;;;;;;;;;;;;;;;;;;;;; |
| 186 | +;; Public Interface ;; |
| 187 | +;;;;;;;;;;;;;;;;;;;;;; |
| 188 | + |
| 189 | +(defn reflect |
| 190 | + "Reflect the object ``o`` and return details about its type as a map. |
| 191 | + |
| 192 | + If ``o`` is a Python class (that is, it is an instance of ``type``), then [...] |
| 193 | + |
| 194 | + If ``o`` is a callable (function, coroutine, method, builtin, etc.), then [...] |
| 195 | + |
| 196 | + If ``o`` is a Python module, then [...] |
| 197 | + |
| 198 | + If ``o`` is an object, then return the results of ``(reflect (type o))``. |
| 199 | + |
| 200 | + If ``o`` is ``nil``, return ``nil``." |
| 201 | + [o] |
| 202 | + (reflect* o)) |
0 commit comments