Module: PyCallThread

Defined in:
lib/pycall_thread.rb

Overview

Provides a way to run PyCall code from multiple threads, see PyCallThread.init and PyCallThread.run for more information.

Constant Summary collapse

VALID_UNSAFE_RETURN_VALUES =
%i[allow error warn].freeze

Class Method Summary collapse

Class Method Details

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

Raises:

  • (ArgumentError)


8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/pycall_thread.rb', line 8

def self.init(unsafe_return: :error, &require_pycall_block)
  raise ArgumentError, "Invalid value for unsafe_return: #{unsafe_return}. Must be one of: #{VALID_UNSAFE_RETURN_VALUES.join(", ")}" unless VALID_UNSAFE_RETURN_VALUES.include?(unsafe_return)

  @unsafe_return = unsafe_return

  # Start the thread we will use to run code invoked with PyCallThread.run
  @py_thread = Thread.new { pycall_thread_loop }
  @initialized = true

  # If we've been passed a require_pycall_block, use that to require 'pycall'
  # instead of doing it directly.
  require_pycall(&require_pycall_block)

  at_exit { stop_pycall_thread }
end

.pycall_thread_loopObject



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/pycall_thread.rb', line 76

def self.pycall_thread_loop
  Thread.current.name = "pycall"

  loop do
    block = @queue.pop
    break if block == :stop

    block.call
  rescue StandardError => 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)


94
95
96
97
98
99
100
101
102
103
# File 'lib/pycall_thread.rb', line 94

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

.require_pycall(&require_pycall_block) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/pycall_thread.rb', line 24

def self.require_pycall(&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)

  run do
    # 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
    nil
  end
end

.run(&block) ⇒ Object

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



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/pycall_thread.rb', line 40

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

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

  run_result(result_queue.pop)
end

.run_result(result) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/pycall_thread.rb', line 55

def self.run_result(result)
  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



71
72
73
74
# File 'lib/pycall_thread.rb', line 71

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