Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basilisp.reflect namespace for Python runtime reflection #838

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added `basilisp.pprint/print-table` function (#983)
* Added `basilisp.core/read-all` function (#986)
* Added various compiler arguments to CLI commands (#989)
* Added `basilisp.reflect` namespace for Python VM runtime reflection (#837)

### Changed
* Improved on the nREPL server exception messages by matching that of the REPL user-friendly format (#968)
Expand Down
10 changes: 10 additions & 0 deletions docs/api/reflect.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
basilisp.reflect
================

.. toctree::
:maxdepth: 2
:caption: Contents:

.. autonamespace:: basilisp.reflect
:members:
:undoc-members:
202 changes: 202 additions & 0 deletions src/basilisp/reflect.lpy
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
(ns basilisp.reflect
"Runtime reflection of Python objects."
(:import inspect
types))

(defn ^:private qualname->sym
"Canonicalize a Python ``__qualname__`` as a symbol."
[qualname]
(let [[namespace-or-name maybe-name] (.rsplit qualname "." 1)]
(if (nil? maybe-name)
(symbol namespace-or-name)
(symbol namespace-or-name maybe-name))))

(defprotocol Reflectable
(reflect* [this]
"Reflect on ``this`` and return a map describing the object."))

(defn members->map
"Given a seq of 2-tuples of ``member-name, member``, return a mapping of the
member name converted to a symbol and the member."
[m]
(into {}
(map (fn [[member-name member]]
[(symbol member-name) (py->lisp member)]))
m))

(defn ^:private ^:inline py-property?
"Return ``true`` if this object is an instance of a Python ``property``."
[o]
(instance? python/property o))

(def ^:private method-like?
"Predicate for determining if a class member can be treated similar to a method.

Note that Python supports many different class member types beyond simple methods.
It may be useful or even necessary to use :lpy:fn:`reflect` to assess the specific
type of a method-like class member."
(some-fn inspect/ismethod inspect/isfunction inspect/ismethoddescriptor inspect/isbuiltin))

(extend-protocol Reflectable
types/ModuleType
(reflect* [this]
(let [is-basilisp-module? (instance? basilisp.lang.runtime/BasilispModule this)
members-by-group (group-by (fn [[_ member]]
(cond
(inspect/ismodule member) :modules
(inspect/isclass member) :classes
(inspect/isfunction member) :functions
:else :attributes))
(inspect/getmembers this))]
{:name (symbol (python/getattr this "__name__"))
:file (python/getattr this "__file__" nil)
:package (python/getattr this "__package__" nil)
:is-basilisp-module? is-basilisp-module?
:basilisp-ns (when is-basilisp-module?
(python/getattr this "__basilisp_namespace__"))
:modules (members->map (:modules members-by-group))
:classes (members->map (:classes members-by-group))
:functions (members->map (:functions members-by-group))
:attributes (members->map (:attributes members-by-group))}))
python/type
(reflect* [this]
(let [members-by-group (group-by (fn [[_ member]]
(cond
(method-like? member) :methods
(py-property? member) :properties
:else :attributes))
(inspect/getmembers this))]
{:qualified-name (qualname->sym (python/getattr this "__qualname__"))
:name (symbol (python/getattr this "__name__"))
:bases (set (bases this))
:supers (supers this)
:subclasses (subclasses this)
:attributes (members->map (:attributes members-by-group))
:methods (members->map (:methods members-by-group))
:properties (members->map (:properties members-by-group))}))
python/object
(reflect* [this]
(reflect* (python/type this)))
nil
(reflect* [this]
nil))

;;;;;;;;;;;;;;;
;; Callables ;;
;;;;;;;;;;;;;;;

(def ^:private inspect-sig-kind-mapping
{inspect.Parameter/POSITIONAL_ONLY :positional-only
inspect.Parameter/POSITIONAL_OR_KEYWORD :positional-or-keyword
inspect.Parameter/VAR_POSITIONAL :var-positional
inspect.Parameter/KEYWORD_ONLY :keyword-only
inspect.Parameter/VAR_KEYWORD :var-keyword})

(defn ^:private signature->map
"Convert a Python ``inspect.Signature`` object into a map.

Signature maps include the following keys:

:keyword ``:parameters``: an vector of maps describing parameters to the callable
in the strict order they were defined; parameter map keys are defined below
:keyword ``:return-annotation``: the return annotation of the callable object or
``::empty`` if no return annotation is defined

Parameter maps include the following keys:

:keyword ``:name``: the name of the parameter coerced to a symbol; the symbol
will not be demunged
:keyword ``:default``: the default value of this parameter if one is defined or
``::empty`` otherwise
:keyword ``:annotation``: the annotation of this parameter if one is defined or
``::empty`` otherwise
:keyword ``:kind``: the kind of Python parameter this is coerced to a keyword

In cases where a field may contain a reference to the ``inspect.Signature.empty``
or ``inspect.Parameter.empty`` singletons, the corresponding Basilisp value is the
namespaced keyword ``::empty``.
"
[^inspect/Signature sig]
(let [return-anno (.-return-annotation sig)]
{:parameters (mapv (fn [[param-name ^inspect/Parameter param]]
(let [default (.-default param)
anno (.-annotation param)
kind (.-kind param)]
{:name (symbol param-name)
:default (if (operator/is default inspect.Parameter/empty)
::empty
default)
:annotation (if (operator/is anno inspect.Parameter/empty)
::empty
anno)
:kind (get inspect-sig-kind-mapping kind)}))
(.items (.-parameters sig)))
:return-annotation (if (operator/is return-anno inspect.Signature/empty)
::empty
return-anno)}))

(defn ^:private signature
"Return the signature of a potentially callable object as a map if the signature
can be determined, ``nil`` otherwise.

Signature maps contain the keys as described in :lpy:fn:`signature->map`."
[f]
(try
(-> (inspect/signature f)
(signature->map))
(catch python/TypeError _ nil)
(catch python/ValueError _ nil)))

(defn ^:private reflect-callable
[f]
{:qualified-name (qualname->sym (python/getattr f "__qualname__"))
:name (symbol (python/getattr f "__name__"))
:signature (signature f)
:module (inspect/getmodule f)
:doc (inspect/getdoc f)
:file (try
(inspect/getfile f)
(catch python/TypeError _ nil))
:is-basilisp-fn? (python/getattr f "_basilisp_fn" false)
:is-class? (inspect/isclass f)
:is-method? (inspect/ismethod f)
:is-function? (inspect/isfunction f)
:is-generator-fn? (inspect/isgeneratorfunction f)
:is-generator? (inspect/isgenerator f)
:is-coroutine? (inspect/iscoroutine f)
:is-awaitable? (inspect/isawaitable f)
:is-async-gen-fn? (inspect/isasyncgenfunction f)
:is-builtin? (inspect/isbuiltin f)
:is-method-wrapper? (inspect/ismethodwrapper f)
:is-routine? (inspect/isroutine f)
:is-method-descriptor? (inspect/ismethoddescriptor f)})

(extend types/FunctionType Reflectable {:reflect* reflect-callable})
(extend types/LambdaType Reflectable {:reflect* reflect-callable})
(extend types/CoroutineType Reflectable {:reflect* reflect-callable})
(extend types/MethodType Reflectable {:reflect* reflect-callable})
(extend types/BuiltinFunctionType Reflectable {:reflect* reflect-callable})
(extend types/BuiltinMethodType Reflectable {:reflect* reflect-callable})
(extend types/WrapperDescriptorType Reflectable {:reflect* reflect-callable})
(extend types/MethodWrapperType Reflectable {:reflect* reflect-callable})
(extend types/MethodDescriptorType Reflectable {:reflect* reflect-callable})
(extend types/ClassMethodDescriptorType Reflectable {:reflect* reflect-callable})

;;;;;;;;;;;;;;;;;;;;;;
;; Public Interface ;;
;;;;;;;;;;;;;;;;;;;;;;

(defn reflect
"Reflect the object ``o`` and return details about its type as a map.

If ``o`` is a Python class (that is, it is an instance of ``type``), then [...]

If ``o`` is a callable (function, coroutine, method, builtin, etc.), then [...]

If ``o`` is a Python module, then [...]

If ``o`` is an object, then return the results of ``(reflect (type o))``.

If ``o`` is ``nil``, return ``nil``."
[o]
(reflect* o))