diff --git a/src/libpython_clj2/metadata.clj b/src/libpython_clj2/metadata.clj index 36b190e..07f36b5 100644 --- a/src/libpython_clj2/metadata.clj +++ b/src/libpython_clj2/metadata.clj @@ -18,6 +18,7 @@ (def builtins (import-module "builtins")) +(def py-str (get-attr builtins "str")) (def inspect (import-module "inspect")) (def argspec (get-attr inspect "getfullargspec")) (def py-source (get-attr inspect "getsource")) @@ -68,6 +69,28 @@ (catch Exception _ nil))) +(defn- py-default->jvm [x] + (let [jvm-val (->jvm x)] + (if (and (map? jvm-val) + (contains? jvm-val :type) + (contains? jvm-val :value)) + (str (py-str x)) + jvm-val))) + +(defn- py-defaults->jvm [defaults] + (when (->jvm defaults) + (->> defaults + (map py-default->jvm) + (into [])))) + +(defn- py-kwonlydefaults->jvm [kwonlydefaults] + (when (->jvm kwonlydefaults) + (->> (call-attr kwonlydefaults "items") + (map (fn [entry] + (let [[k v] (seq entry)] + [(->jvm k) (py-default->jvm v)]))) + (into {})))) + (defn py-fn-argspec [f] (if-let [spec (try (when-not (pyclass? f) (argspec f)) @@ -75,9 +98,9 @@ {:args (->jvm (get-attr spec "args")) :varargs (->jvm (get-attr spec "varargs")) :varkw (->jvm (get-attr spec "varkw")) - :defaults (->jvm (get-attr spec "defaults")) + :defaults (py-defaults->jvm (get-attr spec "defaults")) :kwonlyargs (->jvm (get-attr spec "kwonlyargs")) - :kwonlydefaults (->jvm (get-attr spec "kwonlydefaults")) + :kwonlydefaults (py-kwonlydefaults->jvm (get-attr spec "kwonlydefaults")) :annotations (->jvm (get-attr spec "annotations"))} (py-fn-argspec (get-attr f "__init__")))) @@ -132,14 +155,8 @@ (map symbol) (into [])) - ;;These sometimes have actual python symbols in them so we can't use them - ;; or-map (->> (concat - ;; (interleave kw-default-args defaults) - ;; (flatten (seq kwonlydefaults))) - ;; (partition-all 2) - ;; (map vec) - ;; (map (fn [[k v]] [(symbol k) v])) - ;; (into {})) + ;; Preserve the default values that inspect returned. These may be nil + ;; or non-keyword JVM representations of Python values. as-varkw (when (not (nil? varkw)) {:as (symbol varkw)}) default-map (->> (concat @@ -147,7 +164,7 @@ (flatten (seq kwonlydefaults))) (partition-all 2) (map vec) - (map (fn [[k v]] [(symbol k) (keyword k)])) + (map (fn [[k v]] [(symbol k) v])) (into {})) kwargs-map (merge default-map diff --git a/src/libpython_clj2/python.clj b/src/libpython_clj2/python.clj index 1ec9027..35b5950 100644 --- a/src/libpython_clj2/python.clj +++ b/src/libpython_clj2/python.clj @@ -381,6 +381,14 @@ user> (py/py. np linspace 2 3 :num 10) #'~varname)) +(defn ^:no-doc py-var-metadata [var-name var-data] + (try + (let [metadata-fn (requiring-resolve 'libpython-clj2.metadata/py-fn-metadata)] + (select-keys (metadata-fn var-name var-data {}) [:doc :arglists])) + (catch Throwable _ + {:doc (get-attr var-data "__doc__")}))) + + (defmacro from-import "Support for the from a import b,c style of importing modules and symbols in python. Documentation is included." @@ -390,7 +398,7 @@ user> (py/py. np linspace 2 3 :num 10) ~@(map (fn [varname] `(let [~'var-data (get-attr ~'mod-data ~(name varname))] (def ~varname ~'var-data) - (alter-meta! #'~varname assoc :doc (get-attr ~'var-data "__doc__")) + (alter-meta! #'~varname merge (py-var-metadata ~(name varname) ~'var-data)) #'~varname)) (concat [item] args))))) diff --git a/test/libpython_clj2/metadata_test.clj b/test/libpython_clj2/metadata_test.clj new file mode 100644 index 0000000..9b1147d --- /dev/null +++ b/test/libpython_clj2/metadata_test.clj @@ -0,0 +1,45 @@ +(ns libpython-clj2.metadata-test + (:require [clojure.test :refer :all] + [libpython-clj2.python :as py] + [libpython-clj2.metadata :as metadata])) + +(deftest pyarglists-preserves-default-values + (let [argspec {:args ["top" "topdown" "onerror"] + :varargs nil + :varkw nil + :defaults ["." true nil] + :kwonlyargs ["follow_symlinks" "dir_fd"] + :kwonlydefaults (array-map "follow_symlinks" false + "dir_fd" nil)}] + (is (= '([& [{top "." + topdown true + onerror nil + follow_symlinks false + dir_fd nil}]] + [& [{top "." + topdown true + follow_symlinks false + dir_fd nil}]] + [& [{top "." + follow_symlinks false + dir_fd nil}]] + [& [{follow_symlinks false + dir_fd nil}]]) + (metadata/pyarglists argspec))))) + +(deftest py-fn-argspec-stringifies-python-object-defaults + (let [testcode (py/import-module "testcode") + default-type-fn (py/get-attr testcode "default_type_fn")] + (is (= '([& [{dtype ""}]] + []) + (-> default-type-fn + metadata/py-fn-argspec + metadata/pyarglists))))) + +(deftest py-fn-argspec-stringifies-kwonly-python-object-defaults + (let [testcode (py/import-module "testcode") + kw-default-type-fn (py/get-attr testcode "kw_default_type_fn")] + (is (= '([& [{dtype ""}]]) + (-> kw-default-type-fn + metadata/py-fn-argspec + metadata/pyarglists))))) diff --git a/test/libpython_clj2/python_test.clj b/test/libpython_clj2/python_test.clj index 71d99c5..b4ea3ec 100644 --- a/test/libpython_clj2/python_test.clj +++ b/test/libpython_clj2/python_test.clj @@ -8,6 +8,7 @@ [tech.v3.datatype.ffi :as dt-ffi] [tech.v3.tensor :as dtt] [clojure.test :refer :all] + [clojure.repl :refer [doc]] libpython-clj2.python.bridge-as-python) (:import [java.io StringWriter] [java.util Map List] @@ -153,6 +154,31 @@ a) vec))))) +(py/from-import testcode defaults_fn) + +(deftest from-import-adds-arglists-metadata + (is (= '([& [{top "." + topdown true + onerror nil + follow_symlinks false + dir_fd nil}]] + [& [{top "." + topdown true + follow_symlinks false + dir_fd nil}]] + [& [{top "." + follow_symlinks false + dir_fd nil}]] + [& [{follow_symlinks false + dir_fd nil}]]) + (:arglists (meta #'defaults_fn))))) + +(deftest from-import-doc-renders-arglists + (let [doc-output (with-out-str (doc defaults_fn))] + (is (re-find #"defaults_fn" doc-output)) + (is (re-find #"\[\[& \[\{top \"\.\"" doc-output)) + (is (re-find #"Function with Python defaults for metadata tests\." doc-output)))) + (deftest aspy-iter (let [testcode-module (py/import-module "testcode")] (is (= [1 2 3 4 5] diff --git a/testcode/__init__.py b/testcode/__init__.py index 52727dd..0460d53 100644 --- a/testcode/__init__.py +++ b/testcode/__init__.py @@ -49,6 +49,21 @@ def complex_fn(a, b, c: str = 5, *args, d=10, **kwargs): return {"a": a, "b": b, "c": c, "args": args, "d": d, "kwargs": kwargs} +def defaults_fn(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=None): + """Function with Python defaults for metadata tests.""" + return top, topdown, onerror, follow_symlinks, dir_fd + + +def default_type_fn(dtype=int): + """Function with a Python object default for metadata tests.""" + return dtype + + +def kw_default_type_fn(*, dtype=int): + """Function with a keyword-only Python object default for metadata tests.""" + return dtype + + complex_fn_testcases = { "complex_fn(1, 2, c=10, d=10, e=10)": complex_fn(1, 2, c=10, d=10, e=10), "complex_fn(1, 2, 10, 11, 12, d=10, e=10)": complex_fn(