forked from metawilm/cl-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpythonic.cl
More file actions
188 lines (161 loc) · 5.68 KB
/
Copy pathpythonic.cl
File metadata and controls
188 lines (161 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
(in-package :python)
;; Pythonic: some nice macros to do Pythonic things
(defmacro py-raise (exc-type string &rest format-args)
"Raise Python exception of type EXC-TYPE, ~@
where STRING with FORMAT-ARGS is the exception argument."
`(error ,exc-type :args (format nil ,string ,@format-args)))
(defmacro py-iterate ((val object) &body body)
"Iterate over OBJECT, successively binding VAL to the new value
and executing BODY.
This works is OBJECT implements either __iter__ or __getitem__."
;; XX assumes OBJECT is a first-class object.
(let ((iterator '#:iterator))
`(block py-iterate
(tagbody
(handler-case (__iter__ ,object)
((or AttributeError %magic-method-missing%) ()
(go try-getitem))
(:no-error (,iterator) (loop (handler-case (next ,iterator)
(StopIteration () (return-from py-iterate))
(:no-error (,val) ,@body)))))
try-getitem
,(let ((index '#:index))
`(let ((,index 0))
(loop
(handler-case (__getitem__ ,object ,index)
(AttributeError () (if (= ,index 0)
(go error)
(return-from py-iterate)))
(IndexError () ;; even ok if index = 0 (empty sequence)
(return-from py-iterate))
(%magic-method-missing% ()
(go error))
(:no-error (,val)
,@body
(incf ,index))))))
error
(py-raise 'TypeError
"Iteration over non-sequence (got: ~A)" ,object)))))
(defmacro ensure-py-type (vars cl-type err-str)
"Ensure that all vars in VARS are a designator for CL-TYPE, if so SETF all vars
to the CL-TYPE value they designate. Raise TypeError if check fails; ERR-STR
may take one argument (the value of the first VAR that failed the type check)."
(let ((des-p '#:des-p)
(val '#:val)
(designator-function (cdr
(assoc cl-type
'((integer . py-int-designator-p)
(number . py-number-designator-p)
(real . py-real-designator-p)
(attribute-name . attribute-name-designator-p)
(string . py-string-designator-p)
(python-object . python-object-designator-p)))))
(vars (if (listp vars) vars (list vars))))
(assert designator-function ()
"Designator for ~A unknown [macro ENSURE-PY-TYPE]" cl-type)
`(progn ,@(loop for var in vars
collect `(multiple-value-bind (,des-p ,val)
(,designator-function ,var)
(if ,des-p
(setf ,var ,val)
(py-raise 'TypeError ,err-str ,var)))))))
;;; if, while
(defmacro py-if (test &optional then else)
`(if (py-val->lisp-bool ,test)
,then
,else))
(defmacro py-while (test &body body)
`(loop
(if (not (py-val->lisp-bool ,test))
(return))
,@body))
#+(or) ;; test
(let ((i (make-int 3)))
(py-while i
(format t "i = ~A~%" i)
(setf i (__sub__ i 1))))
;;; Standard logical operators: OR, AND, NOT
;;;
;;; XXX They expand into nested binary operations.
;;; (there's no *-ary OR/.. in Python)
(defmacro py-or (&rest clauses)
"Returns generalized Lisp boolean"
(if clauses
`(or (py-val->lisp-bool ,(car clauses))
(py-or ,@(cdr clauses)))
nil))
(defmacro py-and (&rest clauses)
"Returns generalized Lisp boolean"
(if clauses
`(and (py-val->lisp-bool ,(car clauses))
(py-and ,@(cdr clauses)))
t))
(defmacro py-not (x)
"Returns generalized Lisp boolean"
`(not (py-val->lisp-bool ,x)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Bridging the gap: making Lisp functions callable from within Python
(defmacro def-pyfun (name (&rest args) &body body)
"Define a Lisp function that is callable from within Python. ~@
It basically defines (defun NAME ...) and (defmethod __call__ #'NAME ...)."
(multiple-value-bind (pos-args key-args key-vals)
(lispargs->pyargs args)
;; For example args:
;; (a b &key (key1 . val1) (key2 . val2))
;; variables now have these values:
;; pos-args: (a b)
;; key-args: (key1 key2)
;; key-vals: ((key1 . val1) (key2 . val2))
(let ((res '#:res))
`(progn
(defun ,name (,@args)
,@body)
(let ((call-rewriter
(make-call-rewriter ',pos-args
(list ,@(mapcar (lambda (kv) `(cons ',(car kv) ,(cdr kv)))
key-vals))
nil nil)))
(defmethod __call__ ((f (eql #',name)) &optional pos-args key-args)
(let* ((,res (funcall call-rewriter pos-args key-args))
,@(mapcar (lambda (posarg)
`(,posarg (cdr (assoc ',posarg ,res))))
pos-args)
,@(mapcar (lambda (key)
`(,key (cdr (assoc ',key ,res))))
key-args))
(,name ,@pos-args ,@(loop for key in key-args
append `(,(intern key 'keyword)
,key))))))
(unless (ignore-errors (namespace-bind *scope* ',name #',name)
t)
(warn "Couldn't bind name ~A to function in current namespace (*scope*)"
',name))))))
#+(or) ;; test
(def-pyfun foo (x y &key (z 0))
(+ x y z))
(defun lispargs->pyargs (list)
;; Handles only positional and keywords (&key ..) for now.
(let ((pos ())
(keys ())
(kvs ()))
(block collect
(loop
(when (null list)
(return))
(let ((x (pop list)))
(cond ((eq x '&key) (loop for (k v) in list
collect k into keys-temp
collect `(,k . ,v) into kvs-temp
finally (setf kvs kvs-temp
keys keys-temp)
(return-from collect)))
(t (push x pos))))))
(setf pos (nreverse pos))
(values pos keys kvs)))
(assert (equal (multiple-value-list (lispargs->pyargs '(a b &key (key1 val1) (key2 val2))))
(list '(a b) '(key1 key2) '((key1 . val1) (key2 . val2)))))
#+(or) ;; test case
(progn (def-pyfun foo (a b &key (key1 *None*) (key2 *None*))
(format t "FOO got: a: ~S b: ~S key1: ~S key2: ~S~%" a b key1 key2))
(foo 1 2 :key2 3)
(__call__ #'foo '(1 2) '((key2 . 3))))