Module: PyCallThread

Defined in:
lib/pycall_thread.rb

Constant Summary collapse

VALID_UNSAFE_RETURN_VALUES =
[:allow, :error, :warn]

Class Method Summary collapse

Class Method Details

.init(unsafe_return: :error, &require_pycall_block) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/pycall_thread.rb', line 6

def self.init(unsafe_return: :error, &require_pycall_block)
  # Only safe to use PyCallThread if PyCall hasn't already been loaded
  raise "PyCall::LibPython already exists: PyCall can't have been initialized already" if defined?(PyCall::LibPython)

  @initialized = true

  if VALID_UNSAFE_RETURN_VALUES.include?(unsafe_return)
    @unsafe_return = unsafe_return
  else
    raise ArgumentError, "Invalid value for unsafe_return: #{unsafe_return}. Must be one of: #{VALID_UNSAFE_RETURN_VALUES.join(', ')}"
  end
  
  # Start the thread we will use to run code invoked with PyCallThread.run
  # If we've been passed a require_pycall_block, use that to require 'pycall'
  # instead of doing it directly.
  @py_thread = Thread.new { pycall_thread_loop(&require_pycall_block) }

  at_exit do
    stop_pycall_thread
  end

  nil
end

.pycall_thread_loop(&require_pycall_block) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/pycall_thread.rb', line 65

def self.pycall_thread_loop(&require_pycall_block)
  Thread.current.name = "pycall"

  # require 'pycall' or run a user-defined block that should do the same
  if require_pycall_block
    require_pycall_block.call
  else
    require 'pycall'
  end

  loop do
    block = @queue.pop
    break if block == :stop
    block.call
  rescue => e
    puts "pycall_thread_loop(): exception in pycall_thread_loop #{e}"
    puts e.backtrace.join("\n")
  end

  # If PyCall.finalize is not present, the main proces will hang at exit
  # See: https://github.com/mrkn/pycall.rb/pull/187
  PyCall.finalize if PyCall.respond_to?(:finalize)
end

.python_object?(obj) ⇒ Boolean

Returns:

  • (Boolean)


89
90
91
92
93
94
95
96
97
98
# File 'lib/pycall_thread.rb', line 89

def self.python_object?(obj)
  [
    PyCall::IterableWrapper,
    PyCall::PyObjectWrapper,
    PyCall::PyModuleWrapper,
    PyCall::PyObjectWrapper,
    PyCall::PyTypeObjectWrapper,
    PyCall::PyPtr,
  ].any? { |kind| obj.is_a?(kind) }
end

.run(&block) ⇒ Object

Runs &block on the PyCall thread, and returns the result



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/pycall_thread.rb', line 31

def self.run(&block)
  init unless @initialized

  result_queue = Queue.new
  @queue << -> do
    begin
      result_queue << { retval: block.call }
    rescue => e
      result_queue << { exception: e }
    end
  end

  result = result_queue.pop

  if result[:exception]
    raise result[:exception]
  elsif python_object?(result[:retval])
    msg = "Trying to return a python object from a PyCallThread.run block is potentially not thread-safe. Please convert #{result.inspect} to a basic Ruby type (like string, array, number, boolean etc) before returning."
    case @unsafe_return
    when :error
      raise msg
    when :warn
      warn "Warning: #{msg}"
    end
  end

  result[:retval]
end

.stop_pycall_threadObject



60
61
62
63
# File 'lib/pycall_thread.rb', line 60

def self.stop_pycall_thread
  @queue << :stop
  @py_thread.join
end