Skip to content

Commit 209386d

Browse files
authored
Merge pull request #72 from mrkn/getattr
Introduce easy ways to get functional attributes This fixes #67
2 parents 9e6280b + 3e2b3e6 commit 209386d

File tree

6 files changed

+106
-31
lines changed

6 files changed

+106
-31
lines changed

lib/pycall.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module PyCall
44
require 'pycall/pyerror'
55
require 'pycall/pyobject_wrapper'
66
require 'pycall/pytypeobject_wrapper'
7+
require 'pycall/pymodule_wrapper'
78
require 'pycall/init'
89

910
module_function
@@ -48,6 +49,15 @@ def exec(code, globals: nil, locals: nil)
4849
end
4950
end
5051

52+
def getattr(*args)
53+
obj, *rest = args
54+
LibPython::Helpers.getattr(obj.__pyptr__, *rest)
55+
end
56+
57+
def hasattr?(obj, name)
58+
LibPython::Helpers.hasattr?(obj.__pyptr__, name)
59+
end
60+
5161
def import_module(name)
5262
LibPython::Helpers.import_module(name)
5363
end

lib/pycall/pymodule_wrapper.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
require 'pycall/pyobject_wrapper'
2+
3+
module PyCall
4+
module PyModuleWrapper
5+
include PyObjectWrapper
6+
7+
def [](*args)
8+
case args[0]
9+
when String, Symbol
10+
PyCall.getattr(self, args[0])
11+
else
12+
super
13+
end
14+
end
15+
end
16+
17+
module_function
18+
19+
class WrapperModuleCache < WrapperObjectCache
20+
def initialize
21+
super(LibPython::API::PyModule_Type)
22+
end
23+
24+
def check_wrapper_object(wrapper_object)
25+
unless wrapper_object.kind_of?(Module) && wrapper_object.kind_of?(PyObjectWrapper)
26+
raise TypeError, "unexpected type #{wrapper_object.class} (expected Module extended by PyObjectWrapper)"
27+
end
28+
end
29+
30+
def self.instance
31+
@instance ||= self.new
32+
end
33+
end
34+
35+
private_constant :WrapperModuleCache
36+
37+
def wrap_module(pymodptr)
38+
check_ismodule(pymodptr)
39+
WrapperModuleCache.instance.lookup(pymodptr) do
40+
Module.new do |mod|
41+
mod.instance_variable_set(:@__pyptr__, pymodptr)
42+
mod.extend PyModuleWrapper
43+
end
44+
end
45+
end
46+
end

lib/pycall/pyobject_wrapper.rb

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -170,32 +170,9 @@ def to_f
170170

171171
module_function
172172

173-
class WrapperModuleCache < WrapperObjectCache
174-
def initialize
175-
super(LibPython::API::PyModule_Type)
176-
end
177-
178-
def check_wrapper_object(wrapper_object)
179-
unless wrapper_object.kind_of?(Module) && wrapper_object.kind_of?(PyObjectWrapper)
180-
raise TypeError, "unexpected type #{wrapper_object.class} (expected Module extended by PyObjectWrapper)"
181-
end
182-
end
183-
184-
def self.instance
185-
@instance ||= self.new
186-
end
187-
end
188-
189-
private_constant :WrapperModuleCache
190-
191-
def wrap_module(pymodptr)
192-
check_ismodule(pymodptr)
193-
WrapperModuleCache.instance.lookup(pymodptr) do
194-
Module.new do |mod|
195-
mod.instance_variable_set(:@__pyptr__, pymodptr)
196-
mod.extend PyObjectWrapper
197-
end
198-
end
173+
def check_ismodule(pyptr)
174+
return if pyptr.kind_of? LibPython::API::PyModule_Type
175+
raise TypeError, "PyModule object is required"
199176
end
200177

201178
def check_isclass(pyptr)
@@ -204,9 +181,4 @@ def check_isclass(pyptr)
204181
return defined?(LibPython::API::PyClass_Type) && pyptr.kind_of?(LibPython::API::PyClass_Type)
205182
raise TypeError, "PyType object is required"
206183
end
207-
208-
def check_ismodule(pyptr)
209-
return if pyptr.kind_of? LibPython::API::PyModule_Type
210-
raise TypeError, "PyModule object is required"
211-
end
212184
end

spec/pycall/pymodule_wrapper_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'spec_helper'
2+
3+
::RSpec.describe PyCall::PyModuleWrapper do
4+
let(:simple_module) do
5+
PyCall.import_module('pycall.simple_module')
6+
end
7+
8+
specify do
9+
expect(simple_module).to be_an_instance_of(Module)
10+
expect(simple_module).to be_a(PyCall::PyModuleWrapper)
11+
expect(simple_module).to be_a(PyCall::PyObjectWrapper)
12+
end
13+
14+
describe '#[]' do
15+
specify do
16+
expect(simple_module[:double]).to be_a(PyCall::PyObjectWrapper)
17+
expect(simple_module[:double].call(2)).to eq(4)
18+
expect(simple_module[:answer]).to eq(42)
19+
end
20+
end
21+
end

spec/pycall_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@
4848
end
4949
end
5050

51+
describe '.getattr' do
52+
let(:pyobj) { PyCall.import_module('pycall.simple_module') }
53+
54+
specify do
55+
expect(PyCall.getattr(pyobj, :answer)).to eq(42)
56+
expect {
57+
PyCall.getattr(pyobj, :absent_name)
58+
}.to raise_error(PyCall::PyError, /AttributeError/)
59+
o = Object.new
60+
expect(PyCall.getattr(pyobj, :absent_name, o)).to equal(o)
61+
end
62+
end
63+
64+
describe '.hasattr?' do
65+
let(:pyobj) { PyCall.import_module('pycall.simple_module') }
66+
67+
specify do
68+
expect(PyCall.hasattr?(pyobj, :answer)).to eq(true)
69+
expect(PyCall.hasattr?(pyobj, :absent_name)).to eq(false)
70+
end
71+
end
72+
5173
describe '.import_module' do
5274
subject { PyCall.import_module('sys') }
5375

spec/python/pycall/simple_module.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def double(x):
2+
return 2 * x
3+
4+
answer = 42

0 commit comments

Comments
 (0)