lib/pry/pry_class.rb

Summary

Maintainability
C
1 day
Test Coverage
# frozen_string_literal: true

require 'stringio'
require 'pathname'

class Pry
  LOCAL_RC_FILE = "./.pryrc".freeze

  # @return [Boolean] true if this Ruby supports safe levels and tainting,
  #  to guard against using deprecated or unsupported features
  HAS_SAFE_LEVEL = (
    RUBY_ENGINE == 'ruby' &&
    Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7')
  )

  class << self
    extend Pry::Forwardable
    attr_accessor :custom_completions
    attr_accessor :current_line
    attr_accessor :line_buffer
    attr_accessor :eval_path
    attr_accessor :cli
    attr_accessor :quiet
    attr_accessor :last_internal_error
    attr_accessor :config

    def_delegators(
      :@config, :input, :input=, :output, :output=, :commands,
      :commands=, :print, :print=, :exception_handler, :exception_handler=,
      :hooks, :hooks=, :color, :color=, :pager, :pager=, :editor, :editor=,
      :memory_size, :memory_size=, :extra_sticky_locals, :extra_sticky_locals=,
      :prompt, :prompt=, :history, :history=
    )

    #
    # @example
    #  Pry.configure do |config|
    #     config.eager_load! # optional
    #     config.input =     # ..
    #     config.foo = 2
    #  end
    #
    # @yield [config]
    #   Yields a block with {Pry.config} as its argument.
    #
    def configure
      yield config
    end
  end

  #
  # @return [main]
  #   returns the special instance of Object, "main".
  #
  def self.main
    @main ||= TOPLEVEL_BINDING.eval "self"
  end

  #
  # @return [Pry::Config]
  #  Returns a value store for an instance of Pry running on the current thread.
  #
  def self.current
    Thread.current[:__pry__] ||= {}
  end

  # Load the given file in the context of `Pry.toplevel_binding`
  # @param [String] file The unexpanded file path.
  def self.load_file_at_toplevel(file)
    toplevel_binding.eval(File.read(file), file)
  rescue RescuableException => e
    puts "Error loading #{file}: #{e}\n#{e.backtrace.first}"
  end

  # Load RC files if appropriate This method can also be used to reload the
  # files if they have changed.
  def self.load_rc_files
    rc_files_to_load.each do |file|
      critical_section do
        load_file_at_toplevel(file)
      end
    end
  end

  # Load the local RC file (./.pryrc)
  def self.rc_files_to_load
    files = []
    files << Pry.config.rc_file if Pry.config.rc_file && Pry.config.should_load_rc
    files << LOCAL_RC_FILE if Pry.config.should_load_local_rc
    files.map { |file| real_path_to(file) }.compact.uniq
  end

  # Expand a file to its canonical name (following symlinks as appropriate)
  def self.real_path_to(file)
    Pathname.new(File.expand_path(file)).realpath.to_s
  rescue Errno::ENOENT, Errno::EACCES
    nil
  end

  # Load any Ruby files specified with the -r flag on the command line.
  def self.load_requires
    Pry.config.requires.each do |file|
      require file
    end
  end

  # Trap interrupts on jruby, and make them behave like MRI so we can
  # catch them.
  def self.load_traps
    trap('INT') { raise Interrupt }
  end

  def self.load_win32console
    require 'win32console'
    # The mswin and mingw versions of pry require win32console, so this should
    # only fail on jruby (where win32console doesn't work).
    # Instead we'll recommend ansicon, which does.
  rescue LoadError
    warn <<-WARNING if Pry.config.windows_console_warning
For a better Pry experience on Windows, please use ansicon:
  https://github.com/adoxa/ansicon
If you use an alternative to ansicon and don't want to see this warning again,
you can add "Pry.config.windows_console_warning = false" to your pryrc.
    WARNING
  end

  # Do basic setup for initial session including: loading pryrc, plugins,
  # requires, and history.
  def self.initial_session_setup
    return unless initial_session?

    @initial_session = false

    # note these have to be loaded here rather than in _pry_ as
    # we only want them loaded once per entire Pry lifetime.
    load_rc_files
  end

  def self.final_session_setup
    return if @session_finalized

    @session_finalized = true
    load_requires if Pry.config.should_load_requires
    load_history if Pry.config.history_load
    load_traps if Pry.config.should_trap_interrupts
    load_win32console if Helpers::Platform.windows? && !Helpers::Platform.windows_ansi?
  end

  # Start a Pry REPL.
  # This method also loads `pryrc` as necessary the first time it is invoked.
  # @param [Object, Binding] target The receiver of the Pry session
  # @param [Hash] options
  # @option options (see Pry#initialize)
  # @example
  #   Pry.start(Object.new, :input => MyInput.new)
  def self.start(target = nil, options = {})
    return if Pry::Env['DISABLE_PRY']
    if Pry::Env['FAIL_PRY']
      raise 'You have FAIL_PRY set to true, which results in Pry calls failing'
    end

    options = options.to_hash

    if in_critical_section?
      output.puts "ERROR: Pry started inside Pry."
      output.puts "This can happen if you have a binding.pry inside a #to_s " \
                  "or #inspect function."
      return
    end

    unless mutex_available?
      output.puts "ERROR: Unable to obtain mutex lock."
      output.puts "This can happen if binding.pry is called from a signal handler"
      return
    end

    options[:target] = Pry.binding_for(target || toplevel_binding)
    initial_session_setup
    final_session_setup

    # Unless we were given a backtrace, save the current one
    if options[:backtrace].nil?
      options[:backtrace] = caller

      # If Pry was started via `binding.pry`, elide that from the backtrace
      if options[:backtrace].first =~ /pry.*core_extensions.*pry/
        options[:backtrace].shift
      end
    end

    driver = options[:driver] || Pry::REPL

    # Enter the matrix
    driver.start(options)
  rescue Pry::TooSafeException
    puts "ERROR: Pry cannot work with $SAFE > 0"
    raise
  end

  # Execute the file through the REPL loop, non-interactively.
  # @param [String] file_name File name to load through the REPL.
  def self.load_file_through_repl(file_name)
    REPLFileLoader.new(file_name).load
  end

  #
  # An inspector that clips the output to `max_length` chars.
  # In case of > `max_length` chars the `#<Object...> notation is used.
  #
  # @param [Object] obj
  #   The object to view.
  #
  # @param [Hash] options
  # @option options [Integer] :max_length (60)
  #   The maximum number of chars before clipping occurs.
  #
  # @option options [Boolean] :id (false)
  #   Boolean to indicate whether or not a hex representation of the object ID
  #   is attached to the return value when the length of inspect is greater than
  #   value of `:max_length`.
  #
  # @return [String]
  #   The string representation of `obj`.
  #
  def self.view_clip(obj, options = {})
    max = options.fetch :max_length, 60
    id = options.fetch :id, false
    if obj.is_a?(Module) && obj.name.to_s != "" && obj.name.to_s.length <= max
      obj.name.to_s
    elsif Pry.main == obj
      # Special-case to support jruby. Fixed as of:
      # https://github.com/jruby/jruby/commit/d365ebd309cf9df3dde28f5eb36ea97056e0c039
      # we can drop in the future.
      obj.to_s
      # rubocop:disable Style/CaseEquality
    elsif Pry.config.prompt_safe_contexts.any? { |v| v === obj } &&
          obj.inspect.length <= max
      # rubocop:enable Style/CaseEquality

      obj.inspect
    elsif id
      format("#<#{obj.class}:0x%<id>x>", id: obj.object_id << 1)
    else
      "#<#{obj.class}>"
    end
  rescue RescuableException
    "unknown"
  end

  # Load Readline history if required.
  def self.load_history
    Pry.history.load
  end

  # @return [Boolean] Whether this is the first time a Pry session has
  #   been started since loading the Pry class.
  def self.initial_session?
    @initial_session
  end

  # Run a Pry command from outside a session. The commands available are
  # those referenced by `Pry.config.commands` (the default command set).
  # @param [String] command_string The Pry command (including arguments,
  #   if any).
  # @param [Hash] options Optional named parameters.
  # @return [nil]
  # @option options [Object, Binding] :target The object to run the
  #   command under. Defaults to `TOPLEVEL_BINDING` (main).
  # @option options [Boolean] :show_output Whether to show command
  #   output. Defaults to true.
  # @example Run at top-level with no output.
  #   Pry.run_command "ls"
  # @example Run under Pry class, returning only public methods.
  #   Pry.run_command "ls -m", :target => Pry
  # @example Display command output.
  #   Pry.run_command "ls -av", :show_output => true
  def self.run_command(command_string, options = {})
    options = {
      target: TOPLEVEL_BINDING,
      show_output: true,
      output: Pry.config.output,
      commands: Pry.config.commands
    }.merge!(options)

    # :context for compatibility with <= 0.9.11.4
    target = options[:context] || options[:target]
    output = options[:show_output] ? options[:output] : StringIO.new

    pry = Pry.new(output: output, target: target, commands: options[:commands])
    pry.eval command_string
    nil
  end

  def self.auto_resize!
    Pry.config.input # by default, load Readline

    if !defined?(Readline) || Pry.config.input != Readline
      warn "Sorry, you must be using Readline for Pry.auto_resize! to work."
      return
    end

    if Readline::VERSION =~ /edit/i
      warn(<<-WARN)
Readline version #{Readline::VERSION} detected - will not auto_resize! correctly.
  For the fix, use GNU Readline instead:
  https://github.com/guard/guard/wiki/Add-Readline-support-to-Ruby-on-Mac-OS-X
      WARN
      return
    end

    trap :WINCH do
      begin
        Readline.set_screen_size(*output.size)
      rescue StandardError => e
        warn "\nPry.auto_resize!'s Readline.set_screen_size failed: #{e}"
      end
      begin
        Readline.refresh_line
      rescue StandardError => e
        warn "\nPry.auto_resize!'s Readline.refresh_line failed: #{e}"
      end
    end
  end

  # Set all the configurable options back to their default values
  def self.reset_defaults
    @initial_session = true
    @session_finalized = nil

    self.config = Pry::Config.new
    self.cli = false
    self.current_line = 1
    self.line_buffer = [""]
    self.eval_path = "(pry)"
  end

  # Basic initialization.
  def self.init
    reset_defaults
  end

  # Return a `Binding` object for `target` or return `target` if it is
  # already a `Binding`.
  # In the case where `target` is top-level then return `TOPLEVEL_BINDING`
  # @param [Object] target The object to get a `Binding` object for.
  # @return [Binding] The `Binding` object.
  def self.binding_for(target)
    return target if Binding === target # rubocop:disable Style/CaseEquality
    return TOPLEVEL_BINDING if Pry.main == target

    target.__binding__
  end

  def self.toplevel_binding
    unless defined?(@toplevel_binding) && @toplevel_binding
      # Grab a copy of the TOPLEVEL_BINDING without any local variables.
      # This binding has a default definee of Object, and new methods are
      # private (just as in TOPLEVEL_BINDING).
      TOPLEVEL_BINDING.eval <<-RUBY
        def self.__pry__
          binding
        end
        Pry.toplevel_binding = __pry__
        class << self; undef __pry__; end
      RUBY
    end
    @toplevel_binding.eval('private')
    @toplevel_binding
  end

  class << self
    attr_writer :toplevel_binding
  end

  def self.in_critical_section?
    Thread.current[:pry_critical_section] ||= 0
    Thread.current[:pry_critical_section] > 0
  end

  def self.critical_section
    Thread.current[:pry_critical_section] ||= 0
    Thread.current[:pry_critical_section] += 1
    yield
  ensure
    Thread.current[:pry_critical_section] -= 1
  end

  def self.mutex_available?
    Mutex.new.synchronize { true }
  rescue ThreadError
    false
  end
  private_class_method :mutex_available?
end

Pry.init