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