diff --git a/src/libpython_clj2/metadata.clj b/src/libpython_clj2/metadata.clj index 36b190e..fbb7e1b 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,39 @@ (catch Exception _ nil))) +(def ^:private default-repr-max-len 200) + +(defn- safe-py-str [x] + (let [s (try (str (py-str x)) + (catch Throwable _ ""))] + (if (> (count s) default-repr-max-len) + (str (subs s 0 default-repr-max-len) "...") + s))) + +(defn- opaque? [v] + (and (map? v) (contains? v :type) (contains? v :value))) + +(defn- py-default->jvm [x] + (let [jvm-val (->jvm x)] + ;; nested opaques have no JVM form and leak pointers - stringify instead + (if (some opaque? (tree-seq coll? seq jvm-val)) + (safe-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 +109,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 +166,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 +175,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..5035057 --- /dev/null +++ b/test/libpython_clj2/metadata_test.clj @@ -0,0 +1,92 @@ +(ns libpython-clj2.metadata-test + (:require [clojure.test :refer :all] + [clojure.string :as str] + [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))))) + +(defn- tc [n] (py/get-attr (py/import-module "testcode") n)) + +(defn- default-of [n sym] + (->> (-> (tc n) metadata/py-fn-argspec metadata/pyarglists first) + (tree-seq coll? seq) (filter map?) first sym)) + +(deftest py-default-class-object + (is (= "" (default-of "f_class" 'x)))) + +(deftest py-default-bad-repr-preserves-var + (is (= '([& [{x ""}]] []) + (-> (tc "f_badstr") metadata/py-fn-argspec metadata/pyarglists)))) + +(deftest py-default-custom-repr + (is (= (apply str (repeat 40 "x")) (default-of "f_weird" 'x)))) + +(deftest py-default-partial + (is (= "functools.partial(, 0)" (default-of "f_partial" 'x)))) + +(deftest py-default-nested-opaque-no-pointer-leak + (is (= "(, )" (default-of "f_nested_opaque" 'x)))) + +(deftest py-default-lambda + (is (str/starts-with? (default-of "f_lambda" 'x) " at 0x"))) + +(deftest py-default-sentinel + (is (str/starts-with? (default-of "f_sentinel" 'x) "> (-> (tc "f_mixed") metadata/py-fn-argspec metadata/pyarglists first) + (tree-seq coll? seq) (filter map?) first)] + (is (= 1 (m 'b))) + (is (= "" (m 'c))) + (is (str/starts-with? (m 'd) "