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/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 cb88a082..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)" @@ -49,6 +51,55 @@ 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 + + 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 + + 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 daa2ac00..43223316 100644 --- a/spec/pycall/pytypeobject_wrapper_spec.rb +++ b/spec/pycall/pytypeobject_wrapper_spec.rb @@ -10,6 +10,162 @@ 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 '#<=>(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.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) @@ -83,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/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 diff --git a/spec/pycall/subclass_spec.rb b/spec/pycall/subclass_spec.rb new file mode 100644 index 00000000..e91f56e4 --- /dev/null +++ b/spec/pycall/subclass_spec.rb @@ -0,0 +1,50 @@ +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 '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]) + 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/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 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)