From 0b8b678063a0c1bd6a90a94d97b02a6861606e54 Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 11 Sep 2017 06:01:01 +0900 Subject: [PATCH 1/6] Add examples --- spec/pycall/subclass_spec.rb | 46 +++++++++++++++++++++++++++++ spec/python/pycall/subclass_test.py | 9 ++++++ 2 files changed, 55 insertions(+) create mode 100644 spec/pycall/subclass_spec.rb create mode 100644 spec/python/pycall/subclass_test.py diff --git a/spec/pycall/subclass_spec.rb b/spec/pycall/subclass_spec.rb new file mode 100644 index 00000000..e9973853 --- /dev/null +++ b/spec/pycall/subclass_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +::RSpec.describe 'A subclass of the wrapper class of a Python class' do + let(:subclass_test) do + PyCall.import_module('pycall.subclass_test') + end + + let(:superclass_wrapper) do + subclass_test.SuperClass + end + + let(:subclass) do + Class.new(superclass_wrapper) + end + + it 'calls __init__ of superclass' do + a = subclass.new(1, 2, 3) + expect(a.init_args.to_a).to eq([1, 2, 3]) + end + + it 'calls initialize' do + subclass.class_eval do + def initialize(*args, &b) + super() + b.call + end + end + expect {|b| a = subclass.new(&b) }.to yield_control + end + + it 'calls __init__ of superclass via super' do + subclass.class_eval do + def initialize + super(10, 20, 30) + end + end + a = subclass.new + expect(a.init_args.to_a).to eq([10, 20, 30]) + end + + it 'calls an instance methods of the superclass' do + a = subclass.new + expect(a.dbl(21)).to eq(42) + expect(subclass_test.call_dbl(a, 30)).to eq(60) + end +end diff --git a/spec/python/pycall/subclass_test.py b/spec/python/pycall/subclass_test.py new file mode 100644 index 00000000..b6b002ec --- /dev/null +++ b/spec/python/pycall/subclass_test.py @@ -0,0 +1,9 @@ +class SuperClass(object): + def __init__(self, *args): + self.init_args = args + + def dbl(self, x): + return 2 * x + +def call_dbl(obj, x): + return obj.dbl(x) From 982275103a0cc8b8629d7a8ca3c2ba2ac636619c Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 11 Sep 2017 09:27:19 +0900 Subject: [PATCH 2/6] Add PyTypePtr#subclass? --- ext/pycall/libpython.c | 1 + ext/pycall/pycall.c | 18 ++++++++++ ext/pycall/pycall_internal.h | 1 + spec/pycall/pytypeptr_spec.rb | 62 +++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 spec/pycall/pytypeptr_spec.rb diff --git a/ext/pycall/libpython.c b/ext/pycall/libpython.c index dceb7481..2d7abaa1 100644 --- a/ext/pycall/libpython.c +++ b/ext/pycall/libpython.c @@ -94,6 +94,7 @@ pycall_init_libpython_api_table(VALUE libpython_handle) INIT_API_TABLE_ENTRY(_PyObject_New, required); INIT_API_TABLE_ENTRY(PyCallable_Check, required); INIT_API_TABLE_ENTRY(PyObject_IsInstance, required); + INIT_API_TABLE_ENTRY(PyObject_IsSubclass, required); INIT_API_TABLE_ENTRY2(PyObject_Hash._hash_t, PyObject_Hash, required); INIT_API_TABLE_ENTRY(PyObject_RichCompare, required); INIT_API_TABLE_ENTRY(PyObject_Call, required); diff --git a/ext/pycall/pycall.c b/ext/pycall/pycall.c index 73b86750..367ba086 100644 --- a/ext/pycall/pycall.c +++ b/ext/pycall/pycall.c @@ -526,6 +526,23 @@ pycall_pytypeptr_get_tp_flags(VALUE obj) return Qnil; } +static VALUE +pycall_pytypeptr_subclass_p(VALUE obj, VALUE other) +{ + PyObject *pyobj, *pyobj_other; + int res; + + pyobj = get_pytypeobj_ptr(obj); + pyobj_other = check_get_pytypeobj_ptr(other); + + res = Py_API(PyObject_IsSubclass)(pyobj, pyobj_other); + if (res < 0) { + pycall_pyerror_fetch_and_raise("PyObject_IsSubclass in pycall_pytypeptr_subclass_p"); + } + + return res ? Qtrue : Qfalse; +} + static VALUE pycall_pytypeptr_eqq(VALUE obj, VALUE other) { @@ -2108,6 +2125,7 @@ Init_pycall(void) rb_define_method(cPyTypePtr, "__tp_name__", pycall_pytypeptr_get_tp_name, 0); rb_define_method(cPyTypePtr, "__tp_basicsize__", pycall_pytypeptr_get_tp_basicsize, 0); rb_define_method(cPyTypePtr, "__tp_flags__", pycall_pytypeptr_get_tp_flags, 0); + rb_define_method(cPyTypePtr, "subclass?", pycall_pytypeptr_subclass_p, 1); rb_define_method(cPyTypePtr, "===", pycall_pytypeptr_eqq, 1); /* PyCall::LibPython::API */ diff --git a/ext/pycall/pycall_internal.h b/ext/pycall/pycall_internal.h index 2ebf6f4c..cc797c70 100644 --- a/ext/pycall/pycall_internal.h +++ b/ext/pycall/pycall_internal.h @@ -525,6 +525,7 @@ typedef struct { PyObject * (* _PyObject_New)(PyTypeObject *); int (* PyCallable_Check)(PyObject *); int (* PyObject_IsInstance)(PyObject *, PyObject *); + int (* PyObject_IsSubclass)(PyObject *, PyObject *); union { long (* _long)(PyObject *); Py_hash_t (* _hash_t)(PyObject *); diff --git a/spec/pycall/pytypeptr_spec.rb b/spec/pycall/pytypeptr_spec.rb new file mode 100644 index 00000000..08271965 --- /dev/null +++ b/spec/pycall/pytypeptr_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +module PyCall + ::RSpec.describe PyTypePtr do + describe '#subclass?(other)' do + subject { PyCall::List.__pyptr__ } + + context 'when the value of `other` is a PyTypePtr' do + specify do + expect(subject.subclass?(PyCall.builtins.object.__pyptr__)).to eq(true) + expect(subject.subclass?(PyCall.builtins.dict.__pyptr__)).to eq(false) + end + end + + context 'when the value of `other` is a PyPtr' do + specify do + expect { subject.subclass?(Conversion.from_ruby(42)) }.to raise_error(TypeError) + end + end + + context 'when the value of `other` is a PyTypeObjectWrapper' do + specify do + expect { subject.subclass?(PyCall.builtins.object) }.to raise_error(TypeError) + expect { subject.subclass?(PyCall.builtins.dict) }.to raise_error(TypeError) + end + end + + context 'when the value of `other` is a Class' do + specify do + expect { subject.subclass?(Array) }.to raise_error(TypeError) + expect { subject.subclass?(Hash) }.to raise_error(TypeError) + end + end + + context 'when the value of `other` is an instance of other class' do + specify do + expect { subject.subclass?(12) }.to raise_error(TypeError) + end + end + end + + describe '#<=>' do + pending + end + + describe '#<' do + pending + end + + describe '#>' do + pending + end + + describe '#<=' do + pending + end + + describe '#>=' do + pending + end + end +end From 29a04fc409e2e2a7c037fff2498be09aa58c716b Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 11 Sep 2017 09:34:18 +0900 Subject: [PATCH 3/6] Add PyTypeObjectWrapper#subclass? --- lib/pycall/pytypeobject_wrapper.rb | 11 +++++++++ spec/pycall/pytypeobject_wrapper_spec.rb | 31 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/lib/pycall/pytypeobject_wrapper.rb b/lib/pycall/pytypeobject_wrapper.rb index cb88a082..e65e92fc 100644 --- a/lib/pycall/pytypeobject_wrapper.rb +++ b/lib/pycall/pytypeobject_wrapper.rb @@ -49,6 +49,17 @@ def ===(other) end end + def subclass?(other) + case other + when PyTypeObjectWrapper + __pyptr__.subclass?(other.__pyptr__) + when Class, Module + other >= self || false + else + __pyptr__.subclass?(other) + end + end + private def register_python_type_mapping diff --git a/spec/pycall/pytypeobject_wrapper_spec.rb b/spec/pycall/pytypeobject_wrapper_spec.rb index daa2ac00..eaf00e53 100644 --- a/spec/pycall/pytypeobject_wrapper_spec.rb +++ b/spec/pycall/pytypeobject_wrapper_spec.rb @@ -10,6 +10,37 @@ module PyCall PyCall.wrap_class(simple_class) end + describe '#subclass?(other)' do + subject { PyCall.builtins.list } + + context 'when the value of other is a PyTypeObjectWrapper' do + specify do + expect(subject.subclass?(PyCall.builtins.object)).to eq(true) + expect(subject.subclass?(PyCall.builtins.list)).to eq(true) + expect(subject.subclass?(PyCall.builtins.dict)).to eq(false) + end + end + + context 'when the value of other is a Class' do + specify do + expect(subject.subclass?(Object)).to eq(true) + expect(subject.subclass?(PyObjectWrapper)).to eq(true) + expect(subject.subclass?(PyTypeObjectWrapper)).to eq(false) + expect(subject.subclass?(Array)).to eq(false) + end + end + + context 'when the other cases' do + it 'behaves as well as PyTypePtr#subclass?' do + expect(subject.subclass?(PyCall.builtins.object.__pyptr__)).to eq(true) + expect(subject.subclass?(PyCall.builtins.list.__pyptr__)).to eq(true) + expect(subject.subclass?(PyCall.builtins.dict.__pyptr__)).to eq(false) + expect { subject.subclass?(Conversion.from_ruby(12)) }.to raise_error(TypeError) + expect { subject.subclass?(12) }.to raise_error(TypeError) + end + end + end + describe '#===' do specify do expect(PyCall.builtins.tuple === PyCall.tuple()).to eq(true) From e37b72a1b9ed8abc64a9c33f53bc894f93b0b69d Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 11 Sep 2017 10:28:24 +0900 Subject: [PATCH 4/6] Add PyTypeObjectWrapper#<=> --- lib/pycall/pytypeobject_wrapper.rb | 18 ++++++ spec/pycall/pytypeobject_wrapper_spec.rb | 73 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/lib/pycall/pytypeobject_wrapper.rb b/lib/pycall/pytypeobject_wrapper.rb index e65e92fc..5f7958c1 100644 --- a/lib/pycall/pytypeobject_wrapper.rb +++ b/lib/pycall/pytypeobject_wrapper.rb @@ -60,6 +60,24 @@ def subclass?(other) end end + def <=>(other) + return 0 if equal?(other) + case other + when PyTypeObjectWrapper + return super if __pyptr__ == other.__pyptr__ + other = other.__pyptr__ + when Class, Module + return -1 if subclass?(other) + return 1 if other > self + end + + return nil unless other.is_a?(PyTypePtr) + return 0 if __pyptr__ == other + return -1 if __pyptr__.subclass?(other) + return 1 if other.subclass?(__pyptr__) + nil + end + private def register_python_type_mapping diff --git a/spec/pycall/pytypeobject_wrapper_spec.rb b/spec/pycall/pytypeobject_wrapper_spec.rb index eaf00e53..9c5e35c1 100644 --- a/spec/pycall/pytypeobject_wrapper_spec.rb +++ b/spec/pycall/pytypeobject_wrapper_spec.rb @@ -41,6 +41,79 @@ module PyCall end end + describe '#<=>(other)' do + context 'when the value of other is a PyTypeObjectWrapper' do + context 'when the given class is a superclass in Python of the receiver' do + it 'returns -1' do + expect(PyCall.builtins.list <=> PyCall.builtins.object).to eq(-1) + end + end + + context 'when the given class is a subclass in Python of the receiver' do + it 'returns 1' do + expect(PyCall.builtins.object <=> PyCall.builtins.list).to eq(1) + end + end + + context 'when the given class is the receiver' do + it 'returns 0' do + expect(PyCall.builtins.list <=> PyCall.builtins.list).to eq(0) + end + end + end + + context 'when the value of other is a PyTypePtr' do + context 'when the given class is a superclass in Python of the receiver' do + it 'returns -1' do + expect(PyCall.builtins.list <=> PyCall.builtins.object.__pyptr__).to eq(-1) + end + end + + context 'when the given class is a subclass in Python of the receiver' do + it 'returns 1' do + expect(PyCall.builtins.object <=> PyCall.builtins.list.__pyptr__).to eq(1) + end + end + + context 'when the given class is the receiver' do + it 'returns 0' do + expect(PyCall.builtins.list <=> PyCall.builtins.list.__pyptr__).to eq(0) + end + end + end + + context 'when the value of other is a Class' do + context 'when the given class is a superclass of the receiver' do + it 'returns -1' do + expect(PyCall.builtins.list <=> Object).to eq(-1) + expect(PyCall.builtins.list <=> PyObjectWrapper).to eq(-1) + end + end + + context 'when the given class is a subclass of the receiver' do + let(:subclass) { Class.new(PyCall.builtins.list) } + + it 'returns 1' do + expect(PyCall.builtins.list <=> subclass).to eq(1) + end + end + + context 'when the given class is neither a superclass or a subclass of the receiver' do + it 'returns nil' do + expect(PyCall.builtins.list <=> PyTypeObjectWrapper).to eq(nil) + expect(PyCall.builtins.list <=> Array).to eq(nil) + end + end + end + + context 'when the other cases' do + it 'returns nil' do + expect(PyCall.builtins.list <=> Conversion.from_ruby(42)).to eq(nil) + expect(PyCall.builtins.list <=> 42).to eq(nil) + end + end + end + describe '#===' do specify do expect(PyCall.builtins.tuple === PyCall.tuple()).to eq(true) From a515c51bc5c5846d88b67c7c55c4faf21c88cd94 Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 11 Sep 2017 10:34:46 +0900 Subject: [PATCH 5/6] Add PyTypeObjectWrapper#<, #>, #<=, #>= --- lib/pycall/pytypeobject_wrapper.rb | 20 +++++++++ spec/pycall/pytypeobject_wrapper_spec.rb | 52 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/lib/pycall/pytypeobject_wrapper.rb b/lib/pycall/pytypeobject_wrapper.rb index 5f7958c1..46dc25bd 100644 --- a/lib/pycall/pytypeobject_wrapper.rb +++ b/lib/pycall/pytypeobject_wrapper.rb @@ -78,6 +78,26 @@ def <=>(other) nil end + def <(other) + cmp = self <=> other + cmp && cmp < 0 + end + + def >(other) + cmp = self <=> other + cmp && cmp > 0 + end + + def <=(other) + cmp = self <=> other + cmp && cmp <= 0 + end + + def >=(other) + cmp = self <=> other + cmp && cmp >= 0 + end + private def register_python_type_mapping diff --git a/spec/pycall/pytypeobject_wrapper_spec.rb b/spec/pycall/pytypeobject_wrapper_spec.rb index 9c5e35c1..5ea80aeb 100644 --- a/spec/pycall/pytypeobject_wrapper_spec.rb +++ b/spec/pycall/pytypeobject_wrapper_spec.rb @@ -114,6 +114,58 @@ module PyCall end end + describe '#<' do + specify do + expect(PyCall.builtins.list < PyCall.builtins.list).to eq(false) + expect(PyCall.builtins.list < PyCall.builtins.object).to eq(true) + expect(PyCall.builtins.object < PyCall.builtins.list).to eq(false) + expect(PyCall.builtins.list < PyCall.builtins.dict).to eq(nil) + expect(PyCall.builtins.list < Object).to eq(true) + expect(PyCall.builtins.list < Array).to eq(nil) + expect(PyCall.builtins.list < Conversion.from_ruby(42)).to eq(nil) + expect(PyCall.builtins.list < 42).to eq(nil) + end + end + + describe '#>' do + specify do + expect(PyCall.builtins.list > PyCall.builtins.list).to eq(false) + expect(PyCall.builtins.list > PyCall.builtins.object).to eq(false) + expect(PyCall.builtins.object > PyCall.builtins.list).to eq(true) + expect(PyCall.builtins.list > PyCall.builtins.dict).to eq(nil) + expect(PyCall.builtins.list > Object).to eq(false) + expect(PyCall.builtins.list > Array).to eq(nil) + expect(PyCall.builtins.list > Conversion.from_ruby(42)).to eq(nil) + expect(PyCall.builtins.list > 42).to eq(nil) + end + end + + describe '#<=' do + specify do + expect(PyCall.builtins.list <= PyCall.builtins.list).to eq(true) + expect(PyCall.builtins.list <= PyCall.builtins.object).to eq(true) + expect(PyCall.builtins.object <= PyCall.builtins.list).to eq(false) + expect(PyCall.builtins.list <= PyCall.builtins.dict).to eq(nil) + expect(PyCall.builtins.list <= Object).to eq(true) + expect(PyCall.builtins.list <= Array).to eq(nil) + expect(PyCall.builtins.list <= Conversion.from_ruby(42)).to eq(nil) + expect(PyCall.builtins.list <= 42).to eq(nil) + end + end + + describe '#>=' do + specify do + expect(PyCall.builtins.list >= PyCall.builtins.list).to eq(true) + expect(PyCall.builtins.list >= PyCall.builtins.object).to eq(false) + expect(PyCall.builtins.object >= PyCall.builtins.list).to eq(true) + expect(PyCall.builtins.list >= PyCall.builtins.dict).to eq(nil) + expect(PyCall.builtins.list >= Object).to eq(false) + expect(PyCall.builtins.list >= Array).to eq(nil) + expect(PyCall.builtins.list >= Conversion.from_ruby(42)).to eq(nil) + expect(PyCall.builtins.list >= 42).to eq(nil) + end + end + describe '#===' do specify do expect(PyCall.builtins.tuple === PyCall.tuple()).to eq(true) From 662d7cbf2316f14ff51420f0e20e691c1a09dbe1 Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 11 Sep 2017 11:06:25 +0900 Subject: [PATCH 6/6] Call __init__ in initialize method for subclasses --- lib/pycall/pyobject_wrapper.rb | 4 ++++ lib/pycall/pytypeobject_wrapper.rb | 8 +++++--- spec/pycall/pytypeobject_wrapper_spec.rb | 14 ++++++++++++++ spec/pycall/subclass_spec.rb | 4 ++++ spec/python/pycall/initialize_test.py | 11 +++++++++++ 5 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 spec/python/pycall/initialize_test.py diff --git a/lib/pycall/pyobject_wrapper.rb b/lib/pycall/pyobject_wrapper.rb index 8abb7781..594c3704 100644 --- a/lib/pycall/pyobject_wrapper.rb +++ b/lib/pycall/pyobject_wrapper.rb @@ -26,6 +26,10 @@ def self.extend_object(obj) :| => :__or__ }.freeze + def initialize(*args) + __init__(*args) + end + def method_missing(name, *args) name_str = name.to_s if name.kind_of?(Symbol) name_str.chop! if name_str.end_with?('=') diff --git a/lib/pycall/pytypeobject_wrapper.rb b/lib/pycall/pytypeobject_wrapper.rb index 46dc25bd..a5244f5a 100644 --- a/lib/pycall/pytypeobject_wrapper.rb +++ b/lib/pycall/pytypeobject_wrapper.rb @@ -20,12 +20,14 @@ def inherited(subclass) subclass.instance_variable_set(:@__pyptr__, __pyptr__) end - def new(*args) - wrap_pyptr(LibPython::Helpers.call_object(__pyptr__, *args)) + def new(*args, &b) + wrap_pyptr(__new__(__pyptr__, *args)).tap do |obj| + obj.instance_eval { initialize(*args, &b) } + end end def wrap_pyptr(pyptr) - return pyptr if pyptr.kind_of? self + return pyptr if pyptr.class <= self pyptr = pyptr.__pyptr__ if pyptr.kind_of? PyObjectWrapper unless pyptr.kind_of? PyPtr raise TypeError, "unexpected argument type #{pyptr.class} (expected PyCall::PyPtr)" diff --git a/spec/pycall/pytypeobject_wrapper_spec.rb b/spec/pycall/pytypeobject_wrapper_spec.rb index 5ea80aeb..43223316 100644 --- a/spec/pycall/pytypeobject_wrapper_spec.rb +++ b/spec/pycall/pytypeobject_wrapper_spec.rb @@ -239,6 +239,20 @@ module PyCall obj = extended_class.new expect(obj.__pyptr__.__ob_type__).to eq(simple_class_wrapper) end + + it 'calls __init__ only once' do + test_class = PyCall.import_module('pycall.initialize_test').InitializeTest + obj = test_class.new(42) + expect(obj.values.to_a).to eq([42]) + end + + context 'when __new__ is redefined' do + it 'calls __init__ only once' do + test_class = PyCall.import_module('pycall.initialize_test').NewOverrideTest + obj = test_class.new(42) + expect(obj.values.to_a).to eq([42, 42]) + end + end end end end diff --git a/spec/pycall/subclass_spec.rb b/spec/pycall/subclass_spec.rb index e9973853..e91f56e4 100644 --- a/spec/pycall/subclass_spec.rb +++ b/spec/pycall/subclass_spec.rb @@ -13,6 +13,10 @@ Class.new(superclass_wrapper) end + it 'is an instance of subclass' do + expect(subclass.new.class).to eq(subclass) + end + it 'calls __init__ of superclass' do a = subclass.new(1, 2, 3) expect(a.init_args.to_a).to eq([1, 2, 3]) diff --git a/spec/python/pycall/initialize_test.py b/spec/python/pycall/initialize_test.py new file mode 100644 index 00000000..276ea93f --- /dev/null +++ b/spec/python/pycall/initialize_test.py @@ -0,0 +1,11 @@ +class InitializeTest(object): + def __init__(self, x): + if not hasattr(self, 'values'): + self.values = [] + self.values.append(x) + +class NewOverrideTest(InitializeTest): + def __new__(cls, x): + obj = super().__new__(cls) + obj.__init__(x) + return obj