Skip to content

Commit 6f24f4e

Browse files
committed
Add basilisp.reflect namespace for Python runtime reflection
1 parent b03c5c2 commit 6f24f4e

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed

src/basilisp/reflect.lpy

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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

Comments
 (0)