Skip to content

Commit ca792ed

Browse files
galenlynchstevengj
authored andcommitted
Allow @PyDEF to define classes with class variables (#605)
* Allow @PyDEF to define classes with class variables Before this commit, `@pydef` and `@pydef_object` did not support the creation of python classes with [class variables][1]. I have expanded `@pydef` to support class variables with the following syntax: ```julia @PyDEF mutable struct ObjectCounter obj_count = 0 # Class variable function __init__(::PyObject) ObjectCounter[:obj_count] += 1 end end ``` closes #513 [1]: https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables * Be more careful about checking expression in parse_pydef
1 parent b9cca81 commit ca792ed

File tree

3 files changed

+59
-6
lines changed

3 files changed

+59
-6
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,16 @@ Here's another example using [Tkinter](https://wiki.python.org/moin/TkInter):
461461
app = SampleApp()
462462
app[:mainloop]()
463463

464+
Class variables are also supported:
465+
466+
using PyCall
467+
@pydef mutable struct ObjectCounter
468+
obj_count = 0 # Class variable
469+
function __init__(::PyObject)
470+
ObjectCounter[:obj_count] += 1
471+
end
472+
end
473+
464474
### GUI Event Loops
465475

466476
For Python packages that have a graphical user interface (GUI),

src/pyclass.jl

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Arguments
2323
Return value: the created class (`::PyTypeObject`)
2424
"""
2525
function def_py_class(type_name::AbstractString, methods::Vector;
26-
base_classes=[], getsets::Vector=[])
26+
base_classes=[], getsets::Vector=[], class_vars=[])
2727
# Only create new-style classes
2828
base_classes = union(base_classes, [pybuiltin("object")])
2929
methods = Dict(py_name => jlfun2pyfun(jl_fun::Function)
@@ -32,7 +32,7 @@ function def_py_class(type_name::AbstractString, methods::Vector;
3232
jlfun2pyfun(setter))
3333
for (py_name::Symbol, getter, setter) in getsets)
3434
return pybuiltin("type")(type_name, tuple(base_classes...),
35-
merge(methods, getter_setters))
35+
merge(methods, getter_setters, Dict(class_vars)))
3636
end
3737

3838
######################################################################
@@ -58,6 +58,11 @@ function parse_pydef_toplevel(expr)
5858
return class_name::Symbol, base_classes, lines
5959
end
6060

61+
# From MacroTools
62+
function isfunction(expr)
63+
@capture(MacroTools.longdef1(expr), function (fcall_ | fcall_) body_ end)
64+
end
65+
6166
function parse_pydef(expr)
6267
class_name, base_classes, lines = parse_pydef_toplevel(expr)
6368
# Now we parse every method definition / getter / setter
@@ -66,8 +71,12 @@ function parse_pydef(expr)
6671
getter_dict = Dict{Any, Symbol}() # python_var => jl_getter_name
6772
setter_dict = Dict{Any, Symbol}()
6873
method_syms = Dict{Any, Symbol}() # see below
74+
class_vars = Dict{Symbol, Any}()
6975
for line in lines
70-
if !isa(line, LineNumberNode) && line.head != :line # need to skip those
76+
line isa LineNumberNode && continue
77+
line isa Expr || error("Malformed line: $line")
78+
line.head == :line && continue
79+
if isfunction(line)
7180
def_dict = splitdef(line)
7281
py_f = def_dict[:name]
7382
# The dictionary of the new Julia-side definition.
@@ -101,11 +110,23 @@ function parse_pydef(expr)
101110
error("Malformed line: $line")
102111
end
103112
push!(function_defs, combinedef(jl_def_dict))
113+
elseif line.head == :(=) # Non function assignment
114+
class_vars[line.args[1]] = line.args[2]
115+
else
116+
error("Malformed line: $line")
104117
end
105118
end
106119
@assert(isempty(setdiff(keys(setter_dict), keys(getter_dict))),
107120
"All .set attributes must have a .get")
108-
class_name, base_classes, methods, getter_dict, setter_dict, function_defs
121+
return (
122+
class_name,
123+
base_classes,
124+
methods,
125+
getter_dict,
126+
setter_dict,
127+
function_defs,
128+
class_vars
129+
)
109130
end
110131

111132
"""
@@ -159,20 +180,30 @@ metaclass as a `PyObject` instead of binding it to the class name.
159180
It's side-effect-free, except for the definition of the class methods.
160181
"""
161182
macro pydef_object(class_expr)
162-
class_name, base_classes, methods_, getter_dict, setter_dict, function_defs=
183+
class_name,
184+
base_classes,
185+
methods_,
186+
getter_dict,
187+
setter_dict,
188+
function_defs,
189+
class_vars =
163190
parse_pydef(class_expr)
164191
methods = [:($(Expr(:quote, py_name::Symbol)), $(esc(jl_fun::Symbol)))
165192
for (py_name, jl_fun) in methods_]
166193
getsets = [:($(Expr(:quote, attribute)),
167194
$(esc(getter)),
168195
$(esc(get(setter_dict, attribute, nothing))))
169196
for (attribute, getter) in getter_dict]
197+
class_var_pairs = [
198+
:($(Expr(:quote, py_name)), $(esc(val_expr)))
199+
for (py_name, val_expr) in class_vars
200+
]
170201
:(begin
171202
$(map(esc, function_defs)...)
172203
# This line doesn't have any side-effect, it just returns the Python
173204
# (meta-)class, as a PyObject
174205
def_py_class($(string(class_name)), [$(methods...)];
175206
base_classes=[$(map(esc, base_classes)...)],
176-
getsets=[$(getsets...)])
207+
getsets=[$(getsets...)], class_vars = [$(class_var_pairs...)])
177208
end)
178209
end

test/runtests.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,14 @@ end
573573
end
574574
end
575575

576+
# @pydef with class variable
577+
@pydef mutable struct ObjectCounter
578+
obj_count = 1 - 1
579+
function __init__(::PyObject)
580+
ObjectCounter[:obj_count] += 1
581+
end
582+
end
583+
576584
@testset "pydef" begin
577585
d = Doubler(5)
578586
@test d[:x] == 5
@@ -583,6 +591,10 @@ end
583591

584592
@test_throws ErrorException @pywith IgnoreError(false) error()
585593
@test (@pywith IgnoreError(true) error(); true)
594+
595+
@test ObjectCounter[:obj_count] == 0
596+
a = ObjectCounter()
597+
@test ObjectCounter[:obj_count] == 1
586598
end
587599

588600
@testset "callback" begin

0 commit comments

Comments
 (0)