From fbdf03f4348daa556db77c86c8b7b5be000b0f96 Mon Sep 17 00:00:00 2001 From: Savyasachi Date: Mon, 8 Jun 2026 16:24:32 -0700 Subject: [PATCH 1/3] fix(metadata): preserve pyarglists defaults --- src/libpython_clj2/metadata.clj | 12 +++--------- test/libpython_clj2/metadata_test.clj | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 test/libpython_clj2/metadata_test.clj diff --git a/src/libpython_clj2/metadata.clj b/src/libpython_clj2/metadata.clj index 36b190e..25e7258 100644 --- a/src/libpython_clj2/metadata.clj +++ b/src/libpython_clj2/metadata.clj @@ -132,14 +132,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 +141,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/test/libpython_clj2/metadata_test.clj b/test/libpython_clj2/metadata_test.clj new file mode 100644 index 0000000..caf418b --- /dev/null +++ b/test/libpython_clj2/metadata_test.clj @@ -0,0 +1,27 @@ +(ns libpython-clj2.metadata-test + (:require [clojure.test :refer :all] + [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))))) From 5ef017182d123535ff323002c41ee56752024d80 Mon Sep 17 00:00:00 2001 From: Savyasachi Date: Mon, 8 Jun 2026 16:39:16 -0700 Subject: [PATCH 2/3] fix(metadata): add python import arglists --- src/libpython_clj2/metadata.clj | 27 +++++++++++++++++++++++++-- src/libpython_clj2/python.clj | 10 +++++++++- test/libpython_clj2/metadata_test.clj | 10 ++++++++++ test/libpython_clj2/python_test.clj | 19 +++++++++++++++++++ testcode/__init__.py | 10 ++++++++++ 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/libpython_clj2/metadata.clj b/src/libpython_clj2/metadata.clj index 25e7258..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__")))) 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 index caf418b..f7c6ef5 100644 --- a/test/libpython_clj2/metadata_test.clj +++ b/test/libpython_clj2/metadata_test.clj @@ -1,5 +1,6 @@ (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 @@ -25,3 +26,12 @@ [& [{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))))) diff --git a/test/libpython_clj2/python_test.clj b/test/libpython_clj2/python_test.clj index 71d99c5..064ecc5 100644 --- a/test/libpython_clj2/python_test.clj +++ b/test/libpython_clj2/python_test.clj @@ -153,6 +153,25 @@ 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 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..fe0db4a 100644 --- a/testcode/__init__.py +++ b/testcode/__init__.py @@ -49,6 +49,16 @@ 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 + + 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( From 3e66f7e3e7ef7ed05af851a7488c5bbadcaa2a6f Mon Sep 17 00:00:00 2001 From: Savyasachi Date: Mon, 8 Jun 2026 16:43:44 -0700 Subject: [PATCH 3/3] test(metadata): cover python arglist docs --- test/libpython_clj2/metadata_test.clj | 8 ++++++++ test/libpython_clj2/python_test.clj | 7 +++++++ testcode/__init__.py | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/test/libpython_clj2/metadata_test.clj b/test/libpython_clj2/metadata_test.clj index f7c6ef5..9b1147d 100644 --- a/test/libpython_clj2/metadata_test.clj +++ b/test/libpython_clj2/metadata_test.clj @@ -35,3 +35,11 @@ (-> 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 064ecc5..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] @@ -172,6 +173,12 @@ 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 fe0db4a..0460d53 100644 --- a/testcode/__init__.py +++ b/testcode/__init__.py @@ -59,6 +59,11 @@ def default_type_fn(dtype=int): 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(