Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Fix subclass behaviors #30

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/pycall/libpython.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions ext/pycall/pycall.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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 */
Expand Down
1 change: 1 addition & 0 deletions ext/pycall/pycall_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down
4 changes: 4 additions & 0 deletions lib/pycall/pyobject_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?('=')
Expand Down
57 changes: 54 additions & 3 deletions lib/pycall/pytypeobject_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -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
Expand Down
170 changes: 170 additions & 0 deletions spec/pycall/pytypeobject_wrapper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
62 changes: 62 additions & 0 deletions spec/pycall/pytypeptr_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading