pycall 1.2.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +150 -0
- data/.github/workflows/windows.yml +127 -0
- data/.gitignore +2 -0
- data/CHANGES.md +39 -0
- data/README.md +153 -7
- data/Rakefile +81 -19
- data/ci/travis_install.sh +30 -9
- data/examples/classifier_comparison.rb +1 -1
- data/examples/hist.rb +1 -1
- data/examples/notebooks/classifier_comparison.ipynb +1 -1
- data/examples/notebooks/leaflet.ipynb +77 -0
- data/ext/pycall/gc.c +84 -5
- data/ext/pycall/libpython.c +7 -1
- data/ext/pycall/pycall.c +60 -6
- data/ext/pycall/pycall_internal.h +27 -1
- data/ext/pycall/ruby_wrapper.c +11 -2
- data/ext/pycall/thread.c +36 -0
- data/lib/pycall.rb +16 -0
- data/lib/pycall/dict.rb +2 -2
- data/lib/pycall/init.rb +3 -8
- data/lib/pycall/iterable_wrapper.rb +32 -0
- data/lib/pycall/libpython/finder.rb +131 -84
- data/lib/pycall/list.rb +2 -2
- data/lib/pycall/pyobject_wrapper.rb +1 -1
- data/lib/pycall/python/investigator.py +82 -9
- data/lib/pycall/pytypeobject_wrapper.rb +10 -0
- data/lib/pycall/version.rb +7 -1
- data/lib/pycall/wrapper_object_cache.rb +56 -25
- data/pycall.gemspec +9 -2
- metadata +24 -7
- data/.travis.yml +0 -56
- data/appveyor.yml +0 -104
@@ -11,10 +11,19 @@ extern "C" {
|
|
11
11
|
#include <ruby.h>
|
12
12
|
#include <ruby/encoding.h>
|
13
13
|
#include <ruby/thread.h>
|
14
|
+
|
14
15
|
#include <assert.h>
|
15
16
|
#include <inttypes.h>
|
16
17
|
#include <limits.h>
|
17
18
|
|
19
|
+
#if defined(_WIN32)
|
20
|
+
# define PYCALL_THREAD_WIN32
|
21
|
+
# include <ruby/win32.h>
|
22
|
+
#elif defined(HAVE_PTHREAD_H)
|
23
|
+
# define PYCALL_THREAD_PTHREAD
|
24
|
+
# include <pthread.h>
|
25
|
+
#endif
|
26
|
+
|
18
27
|
#if SIZEOF_LONG == SIZEOF_VOIDP
|
19
28
|
# define PTR2NUM(x) (LONG2NUM((long)(x)))
|
20
29
|
# define NUM2PTR(x) ((void*)(NUM2ULONG(x)))
|
@@ -492,6 +501,23 @@ extern PyTypeObject PyRuby_Type;
|
|
492
501
|
|
493
502
|
PyObject * PyRuby_New(VALUE ruby_object);
|
494
503
|
|
504
|
+
/* ==== thread support ==== */
|
505
|
+
|
506
|
+
#if defined(PYCALL_THREAD_WIN32)
|
507
|
+
typedef DWORD pycall_tls_key;
|
508
|
+
#elif defined(PYCALL_THREAD_PTHREAD)
|
509
|
+
typedef pthread_key_t pycall_tls_key;
|
510
|
+
#else
|
511
|
+
# error "unsupported thread type"
|
512
|
+
#endif
|
513
|
+
|
514
|
+
int pycall_tls_create(pycall_tls_key* tls_key);
|
515
|
+
void *pycall_tls_get(pycall_tls_key tls_key);
|
516
|
+
int pycall_tls_set(pycall_tls_key tls_key, void *ptr);
|
517
|
+
|
518
|
+
int pycall_without_gvl_p(void);
|
519
|
+
VALUE pycall_without_gvl(VALUE (* func)(VALUE), VALUE arg);
|
520
|
+
|
495
521
|
/* ==== pycall ==== */
|
496
522
|
|
497
523
|
typedef struct {
|
@@ -637,7 +663,7 @@ Py_ssize_t pycall_python_hexversion(void);
|
|
637
663
|
|
638
664
|
void pycall_Py_DecRef(PyObject *);
|
639
665
|
|
640
|
-
|
666
|
+
extern const rb_data_type_t pycall_pyptr_data_type;
|
641
667
|
size_t pycall_pyptr_memsize(void const *);
|
642
668
|
void pycall_pyptr_free(void *);
|
643
669
|
|
data/ext/pycall/ruby_wrapper.c
CHANGED
@@ -356,11 +356,16 @@ PyRuby_getattro_with_gvl(PyRubyObject *pyro, PyObject *pyobj_name)
|
|
356
356
|
|
357
357
|
VALUE cPyRubyPtr;
|
358
358
|
|
359
|
-
|
359
|
+
static rb_data_type_t pycall_pyrubyptr_data_type = {
|
360
360
|
"PyCall::PyRubyPtr",
|
361
361
|
{ 0, pycall_pyptr_free, pycall_pyptr_memsize, },
|
362
362
|
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
|
363
|
-
|
363
|
+
# if defined _WIN32 && !defined __CYGWIN__
|
364
|
+
0,
|
365
|
+
# else
|
366
|
+
&pycall_pyptr_data_type,
|
367
|
+
# endif
|
368
|
+
0, RUBY_TYPED_FREE_IMMEDIATELY
|
364
369
|
#endif
|
365
370
|
};
|
366
371
|
|
@@ -462,6 +467,10 @@ pycall_init_ruby_wrapper(void)
|
|
462
467
|
|
463
468
|
/* PyCall::PyRubyPtr */
|
464
469
|
|
470
|
+
#if defined _WIN32 && !defined __CYGWIN__
|
471
|
+
pycall_pyrubyptr_data_type.parent = &pycall_pyptr_data_type;
|
472
|
+
#endif
|
473
|
+
|
465
474
|
cPyRubyPtr = rb_define_class_under(mPyCall, "PyRubyPtr", cPyPtr);
|
466
475
|
rb_define_alloc_func(cPyRubyPtr, pycall_pyruby_allocate);
|
467
476
|
rb_define_method(cPyRubyPtr, "__ruby_object_id__", pycall_pyruby_get_ruby_object_id, 0);
|
data/ext/pycall/thread.c
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#include "pycall_internal.h"
|
2
|
+
|
3
|
+
#if defined(PYCALL_THREAD_WIN32)
|
4
|
+
int pycall_tls_create(pycall_tls_key *tls_key)
|
5
|
+
{
|
6
|
+
*tls_key = TlsAlloc();
|
7
|
+
return *tls_key == TLS_OUT_OF_INDEXES;
|
8
|
+
}
|
9
|
+
|
10
|
+
void *pycall_tls_get(pycall_tls_key tls_key)
|
11
|
+
{
|
12
|
+
return TlsGetValue(tls_key);
|
13
|
+
}
|
14
|
+
|
15
|
+
int pycall_tls_set(pycall_tls_key tls_key, void *ptr)
|
16
|
+
{
|
17
|
+
return 0 == TlsSetValue(tls_key, ptr);
|
18
|
+
}
|
19
|
+
#endif
|
20
|
+
|
21
|
+
#if defined(PYCALL_THREAD_PTHREAD)
|
22
|
+
int pycall_tls_create(pycall_tls_key *tls_key)
|
23
|
+
{
|
24
|
+
return pthread_key_create(tls_key, NULL);
|
25
|
+
}
|
26
|
+
|
27
|
+
void *pycall_tls_get(pycall_tls_key tls_key)
|
28
|
+
{
|
29
|
+
return pthread_getspecific(tls_key);
|
30
|
+
}
|
31
|
+
|
32
|
+
int pycall_tls_set(pycall_tls_key tls_key, void *ptr)
|
33
|
+
{
|
34
|
+
return pthread_setspecific(tls_key, ptr);
|
35
|
+
}
|
36
|
+
#endif
|
data/lib/pycall.rb
CHANGED
@@ -5,6 +5,7 @@ module PyCall
|
|
5
5
|
require 'pycall/pyobject_wrapper'
|
6
6
|
require 'pycall/pytypeobject_wrapper'
|
7
7
|
require 'pycall/pymodule_wrapper'
|
8
|
+
require 'pycall/iterable_wrapper'
|
8
9
|
require 'pycall/init'
|
9
10
|
|
10
11
|
module_function
|
@@ -58,10 +59,25 @@ module PyCall
|
|
58
59
|
LibPython::Helpers.hasattr?(obj.__pyptr__, name)
|
59
60
|
end
|
60
61
|
|
62
|
+
def same?(left, right)
|
63
|
+
case left
|
64
|
+
when PyObjectWrapper
|
65
|
+
case right
|
66
|
+
when PyObjectWrapper
|
67
|
+
return left.__pyptr__ == right.__pyptr__
|
68
|
+
end
|
69
|
+
end
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
61
73
|
def import_module(name)
|
62
74
|
LibPython::Helpers.import_module(name)
|
63
75
|
end
|
64
76
|
|
77
|
+
def iterable(obj)
|
78
|
+
IterableWrapper.new(obj)
|
79
|
+
end
|
80
|
+
|
65
81
|
def len(obj)
|
66
82
|
case obj
|
67
83
|
when PyObjectWrapper
|
data/lib/pycall/dict.rb
CHANGED
data/lib/pycall/init.rb
CHANGED
@@ -30,20 +30,15 @@ module PyCall
|
|
30
30
|
remove_method :const_missing
|
31
31
|
end
|
32
32
|
|
33
|
-
ENV['PYTHONPATH'] = [ File.expand_path('../python', __FILE__), ENV['PYTHONPATH'] ].compact.join(File::PATH_SEPARATOR)
|
34
|
-
|
35
33
|
LibPython.instance_variable_set(:@handle, LibPython::Finder.find_libpython(python))
|
36
34
|
class << LibPython
|
37
35
|
undef_method :handle
|
38
36
|
attr_reader :handle
|
39
37
|
end
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
rescue LoadError
|
45
|
-
require 'pycall.so'
|
46
|
-
end
|
39
|
+
require 'pycall.so'
|
40
|
+
|
41
|
+
PyCall.sys.path.append(File.expand_path('../python', __FILE__))
|
47
42
|
|
48
43
|
require 'pycall/dict'
|
49
44
|
require 'pycall/list'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PyCall
|
2
|
+
class IterableWrapper
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(obj)
|
6
|
+
@obj = check_iterable(obj)
|
7
|
+
end
|
8
|
+
|
9
|
+
private def check_iterable(obj)
|
10
|
+
unless PyCall.hasattr?(obj, :__iter__)
|
11
|
+
raise ArgumentError, "%p object is not iterable" % obj
|
12
|
+
end
|
13
|
+
obj
|
14
|
+
end
|
15
|
+
|
16
|
+
def each
|
17
|
+
return enum_for(__method__) unless block_given?
|
18
|
+
iter = @obj.__iter__()
|
19
|
+
while true
|
20
|
+
begin
|
21
|
+
yield iter.__next__()
|
22
|
+
rescue PyCall::PyError => err
|
23
|
+
if err.type == PyCall.builtins.StopIteration
|
24
|
+
break
|
25
|
+
else
|
26
|
+
raise err
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'pycall/error'
|
2
2
|
require 'fiddle'
|
3
|
+
require 'pathname'
|
3
4
|
|
4
5
|
module PyCall
|
5
6
|
module LibPython
|
@@ -27,73 +28,114 @@ module PyCall
|
|
27
28
|
def find_python_config(python = nil)
|
28
29
|
python ||= DEFAULT_PYTHON
|
29
30
|
Array(python).each do |python_cmd|
|
30
|
-
|
31
|
-
|
31
|
+
begin
|
32
|
+
python_config = investigate_python_config(python_cmd)
|
33
|
+
return [python_cmd, python_config] unless python_config.empty?
|
34
|
+
rescue
|
35
|
+
end
|
32
36
|
end
|
33
|
-
rescue
|
34
|
-
raise ::PyCall::PythonNotFound
|
35
|
-
else
|
36
37
|
raise ::PyCall::PythonNotFound
|
37
38
|
end
|
38
39
|
|
39
40
|
def find_libpython(python = nil)
|
40
41
|
debug_report("find_libpython(#{python.inspect})")
|
41
42
|
python, python_config = find_python_config(python)
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
43
|
+
suffix = python_config[:SHLIB_SUFFIX]
|
44
|
+
|
45
|
+
use_conda = (ENV.fetch("CONDA_PREFIX", nil) == File.dirname(python_config[:executable]))
|
46
|
+
python_home = if !ENV.key?("PYTHONHOME") || use_conda
|
47
|
+
python_config[:PYTHONHOME]
|
48
|
+
else
|
49
|
+
ENV["PYTHONHOME"]
|
50
|
+
end
|
51
|
+
ENV["PYTHONHOME"] = python_home
|
52
|
+
|
53
|
+
candidate_paths(python_config) do |path|
|
54
|
+
debug_report("Candidate: #{path}")
|
55
|
+
normalized = normalize_path(path, suffix)
|
56
|
+
if normalized
|
57
|
+
debug_report("Trying to dlopen: #{normalized}")
|
50
58
|
begin
|
51
|
-
return dlopen(
|
59
|
+
return dlopen(normalized)
|
52
60
|
rescue Fiddle::DLError
|
53
|
-
debug_report "#{$!.class}: #{$!.message}"
|
54
|
-
else
|
55
|
-
debug_report "Success to dlopen #{libpython.inspect} from ENV['LIBPYTHON']"
|
61
|
+
debug_report "dlopen(#{normalized.inspect}) => #{$!.class}: #{$!.message}"
|
56
62
|
end
|
63
|
+
else
|
64
|
+
debug_report("Not found.")
|
57
65
|
end
|
58
|
-
warn "WARNING(#{self}.#{__method__}) Ignore the wrong libpython location specified in ENV['LIBPYTHON']."
|
59
66
|
end
|
67
|
+
end
|
60
68
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
69
|
+
def candidate_names(python_config)
|
70
|
+
names = []
|
71
|
+
names << python_config[:LDLIBRARY] if python_config[:LDLIBRARY]
|
72
|
+
suffix = python_config[:SHLIB_SUFFIX]
|
73
|
+
if python_config[:LIBRARY]
|
74
|
+
ext = File.extname(python_config[:LIBRARY])
|
75
|
+
names << python_config[:LIBRARY].delete_suffix(ext) + suffix
|
76
|
+
end
|
77
|
+
dlprefix = if windows? then "" else "lib" end
|
78
|
+
sysdata = {
|
79
|
+
v_major: python_config[:version_major],
|
80
|
+
VERSION: python_config[:VERSION],
|
81
|
+
ABIFLAGS: python_config[:ABIFLAGS],
|
82
|
+
}
|
83
|
+
[
|
84
|
+
"python%{VERSION}%{ABIFLAGS}" % sysdata,
|
85
|
+
"python%{VERSION}" % sysdata,
|
86
|
+
"python%{v_major}" % sysdata,
|
87
|
+
"python"
|
88
|
+
].each do |stem|
|
89
|
+
names << "#{dlprefix}#{stem}#{suffix}"
|
83
90
|
end
|
84
91
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
92
|
+
names.compact!
|
93
|
+
names.uniq!
|
94
|
+
|
95
|
+
debug_report("candidate_names: #{names}")
|
96
|
+
return names
|
97
|
+
end
|
98
|
+
|
99
|
+
def candidate_paths(python_config)
|
100
|
+
# The candidate library that linked by executable
|
101
|
+
yield python_config[:linked_libpython]
|
102
|
+
|
103
|
+
lib_dirs = make_libpaths(python_config)
|
104
|
+
lib_basenames = candidate_names(python_config)
|
105
|
+
|
106
|
+
# candidates by absolute paths
|
107
|
+
lib_dirs.each do |dir|
|
108
|
+
lib_basenames.each do |name|
|
109
|
+
yield File.join(dir, name)
|
93
110
|
end
|
94
111
|
end
|
95
112
|
|
96
|
-
|
113
|
+
# library names for searching in system library paths
|
114
|
+
lib_basenames.each do |name|
|
115
|
+
yield name
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def normalize_path(path, suffix, apple_p=apple?)
|
120
|
+
return nil if path.nil?
|
121
|
+
case
|
122
|
+
when path.nil?,
|
123
|
+
Pathname.new(path).relative?
|
124
|
+
nil
|
125
|
+
when File.exist?(path)
|
126
|
+
File.realpath(path)
|
127
|
+
when File.exist?(path + suffix)
|
128
|
+
File.realpath(path + suffix)
|
129
|
+
when apple_p
|
130
|
+
normalize_path(remove_suffix_apple(path), ".so", false)
|
131
|
+
else
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Strip off .so or .dylib
|
137
|
+
def remove_suffix_apple(path)
|
138
|
+
path.sub(/\.(?:dylib|so)\z/, '')
|
97
139
|
end
|
98
140
|
|
99
141
|
def investigate_python_config(python)
|
@@ -120,47 +162,25 @@ module PyCall
|
|
120
162
|
File.expand_path('../../python/investigator.py', __FILE__)
|
121
163
|
end
|
122
164
|
|
123
|
-
def
|
124
|
-
|
125
|
-
case RUBY_PLATFORM
|
126
|
-
when /mingw32/, /cygwin/, /mswin/
|
127
|
-
ENV['PYTHONHOME'] = python_config[:exec_prefix]
|
128
|
-
else
|
129
|
-
ENV['PYTHONHOME'] = python_config.values_at(:prefix, :exec_prefix).join(':')
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
165
|
+
def make_libpaths(python_config)
|
166
|
+
libpaths = python_config.values_at(:LIBPL, :srcdir, :LIBDIR)
|
133
167
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
lib
|
138
|
-
libs << lib << File.basename(lib) if lib
|
139
|
-
end
|
140
|
-
if (lib = python_config[:LIBRARY])
|
141
|
-
libs << File.basename(lib, File.extname(lib))
|
168
|
+
if windows?
|
169
|
+
libpaths << File.dirname(python_config[:executable])
|
170
|
+
else
|
171
|
+
libpaths << File.expand_path('../../lib', python_config[:executable])
|
142
172
|
end
|
143
173
|
|
144
|
-
|
145
|
-
|
146
|
-
libs.uniq!
|
147
|
-
|
148
|
-
debug_report "libs: #{libs.inspect}"
|
149
|
-
return libs
|
150
|
-
end
|
151
|
-
|
152
|
-
def make_libpaths(python_config)
|
153
|
-
executable = python_config[:executable]
|
154
|
-
libpaths = [ python_config[:LIBDIR] ]
|
155
|
-
if Fiddle::WINDOWS
|
156
|
-
libpaths << File.dirname(executable)
|
157
|
-
else
|
158
|
-
libpaths << File.expand_path('../../lib', executable)
|
174
|
+
if apple?
|
175
|
+
libpaths << python_config[:PYTHONFRAMEWORKPREFIX]
|
159
176
|
end
|
160
|
-
|
177
|
+
|
161
178
|
exec_prefix = python_config[:exec_prefix]
|
162
|
-
libpaths << exec_prefix
|
179
|
+
libpaths << exec_prefix
|
180
|
+
libpaths << File.join(exec_prefix, 'lib')
|
181
|
+
|
163
182
|
libpaths.compact!
|
183
|
+
libpaths.uniq!
|
164
184
|
|
165
185
|
debug_report "libpaths: #{libpaths.inspect}"
|
166
186
|
return libpaths
|
@@ -168,6 +188,14 @@ module PyCall
|
|
168
188
|
|
169
189
|
private
|
170
190
|
|
191
|
+
def windows?
|
192
|
+
Fiddle::WINDOWS
|
193
|
+
end
|
194
|
+
|
195
|
+
def apple?
|
196
|
+
RUBY_PLATFORM.include?("darwin")
|
197
|
+
end
|
198
|
+
|
171
199
|
def dlopen(libname)
|
172
200
|
Fiddle.dlopen(libname).tap do |handle|
|
173
201
|
debug_report("dlopen(#{libname.inspect}) = #{handle.inspect}") if handle
|
@@ -186,3 +214,22 @@ module PyCall
|
|
186
214
|
end
|
187
215
|
end
|
188
216
|
end
|
217
|
+
|
218
|
+
if __FILE__ == $0
|
219
|
+
require "pp"
|
220
|
+
python, python_config = PyCall::LibPython::Finder.find_python_config
|
221
|
+
|
222
|
+
puts "python_config:"
|
223
|
+
pp python_config
|
224
|
+
|
225
|
+
puts "\ncandidate_names:"
|
226
|
+
p PyCall::LibPython::Finder.candidate_names(python_config)
|
227
|
+
|
228
|
+
puts "\nlib_dirs:"
|
229
|
+
p PyCall::LibPython::Finder.make_libpaths(python_config)
|
230
|
+
|
231
|
+
puts "\ncandidate_paths:"
|
232
|
+
PyCall::LibPython::Finder.candidate_paths(python_config) do |path|
|
233
|
+
puts "- #{path}"
|
234
|
+
end
|
235
|
+
end
|