Skip to content

Commit

Permalink
Merge branch 'pry-method-patcher'
Browse files Browse the repository at this point in the history
  • Loading branch information
ConradIrwin committed Mar 30, 2013
2 parents 00fce1b + 9c1f814 commit 290eb40
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 53 deletions.
2 changes: 2 additions & 0 deletions lib/pry/code.rb
Expand Up @@ -61,6 +61,8 @@ class << self
def from_file(filename, code_type = type_from_filename(filename))
code = if filename == Pry.eval_path
Pry.line_buffer.drop(1)
elsif Pry::Method::Patcher.code_for(filename)
Pry::Method::Patcher.code_for(filename)
else
File.read(abs_path(filename))
end
Expand Down
3 changes: 1 addition & 2 deletions lib/pry/commands/edit.rb
@@ -1,6 +1,5 @@
class Pry
class Command::Edit < Pry::ClassCommand
require 'pry/commands/edit/method_patcher'
require 'pry/commands/edit/exception_patcher'
require 'pry/commands/edit/file_and_line_locator'

Expand Down Expand Up @@ -83,7 +82,7 @@ def apply_runtime_patch
ExceptionPatcher.new(_pry_, state, file_and_line_for_current_exception).perform_patch
else
if code_object.is_a?(Pry::Method)
MethodPatcher.new(_pry_, code_object).perform_patch
code_object.redefine Pry::Editor.edit_tempfile_with_content(code_object.source)
else
raise NotImplementedError, "Cannot yet patch #{code_object} objects!"
end
Expand Down
47 changes: 31 additions & 16 deletions lib/pry/method.rb
Expand Up @@ -19,6 +19,7 @@ def Method(obj)
class Method
require 'pry/method/weird_method_locator'
require 'pry/method/disowned'
require 'pry/method/patcher'

extend Helpers::BaseHelpers
include Helpers::BaseHelpers
Expand Down Expand Up @@ -263,26 +264,18 @@ def name_with_owner
def source
@source ||= case source_type
when :c
info = pry_doc_info
if info and info.source
code = strip_comments_from_c_code(info.source)
end
c_source
when :ruby
# clone of MethodSource.source_helper that knows to use our
# hacked version of source_location for rbx core methods, and
# our input buffer for methods defined in (pry)
file, line = *source_location
raise SourceNotFoundError, "Could not locate source for #{name_with_owner}!" unless file

begin
code = Pry::Code.from_file(file).expression_at(line)
rescue SyntaxError => e
raise MethodSource::SourceNotFoundError.new(e.message)
end
strip_leading_whitespace(code)
ruby_source
end
end

# Update the live copy of the method's source.
def redefine(source)
Patcher.new(self).patch_in_ram source
Pry::Method(owner.instance_method(name))
end

# Can we get the source code for this method?
# @return [Boolean]
def source?
Expand Down Expand Up @@ -547,5 +540,27 @@ def method_name_from_first_line(first_ln)

nil
end

def c_source
info = pry_doc_info
if info and info.source
strip_comments_from_c_code(info.source)
end
end

def ruby_source
# clone of MethodSource.source_helper that knows to use our
# hacked version of source_location for rbx core methods, and
# our input buffer for methods defined in (pry)
file, line = *source_location
raise SourceNotFoundError, "Could not locate source for #{name_with_owner}!" unless file

begin
code = Pry::Code.from_file(file).expression_at(line)
rescue SyntaxError => e
raise MethodSource::SourceNotFoundError.new(e.message)
end
strip_leading_whitespace(code)
end
end
end
@@ -1,37 +1,38 @@
class Pry
class Command::Edit
class MethodPatcher
attr_accessor :_pry_
attr_accessor :code_object
class Method
class Patcher
attr_accessor :method

def initialize(_pry_, code_object)
@_pry_ = _pry_
@code_object = code_object
@@source_cache = {}

def initialize(method)
@method = method
end

def self.code_for(filename)
@@source_cache[filename]
end

# perform the patch
def perform_patch
if code_object.alias?
def patch_in_ram(source)
if method.alias?
with_method_transaction do
_pry_.evaluate_ruby patched_code
redefine source
end
else
_pry_.evaluate_ruby patched_code
redefine source
end
end

private

def patched_code
@patched_code ||= wrap(Pry::Editor.edit_tempfile_with_content(adjusted_lines))
def redefine(source)
@@source_cache[cache_key] = source
TOPLEVEL_BINDING.eval wrap(source), cache_key
end

# The method code adjusted so that the first line is rewritten
# so that def self.foo --> def foo
def adjusted_lines
lines = code_object.source.lines.to_a
lines[0] = definition_line_for_owner(lines.first)
lines
def cache_key
"pry-redefined(0x#{method.owner.object_id.to_s(16)}##{method.name})"
end

# Run some code ensuring that at the end target#meth_name will not have changed.
Expand All @@ -45,17 +46,17 @@ def adjusted_lines
# @param [Module] target The owner of the method

def with_method_transaction
temp_name = "__pry_#{code_object.original_name}__"
co = code_object
code_object.owner.class_eval do
alias_method temp_name, co.original_name
temp_name = "__pry_#{method.original_name}__"
method = self.method
method.owner.class_eval do
alias_method temp_name, method.original_name
yield
alias_method co.name, co.original_name
alias_method co.original_name, temp_name
alias_method method.name, method.original_name
alias_method method.original_name, temp_name
end

ensure
co.send(:remove_method, temp_name) rescue nil
method.send(:remove_method, temp_name) rescue nil
end

# Update the definition line so that it can be eval'd directly on the Method's
Expand All @@ -70,11 +71,11 @@ def with_method_transaction
#
# @param String The original definition line. e.g. def self.foo(bar, baz=1)
# @return String The new definition line. e.g. def foo(bar, baz=1)
def definition_line_for_owner(line)
if line =~ /^def (?:.*?\.)?#{Regexp.escape(code_object.original_name)}(?=[\(\s;]|$)/
"def #{code_object.original_name}#{$'}"
def definition_for_owner(line)
if line =~ /\Adef (?:.*?\.)?#{Regexp.escape(method.original_name)}(?=[\(\s;]|$)/
"def #{method.original_name}#{$'}"
else
raise CommandError, "Could not find original `def #{code_object.original_name}` line to patch."
raise CommandError, "Could not find original `def #{method.original_name}` line to patch."
end
end

Expand All @@ -87,15 +88,16 @@ def wrap(source)

# Update the source code so that when it has the right owner when eval'd.
#
# This (combined with definition_line_for_owner) is backup for the case that
# This (combined with definition_for_owner) is backup for the case that
# wrap_for_nesting fails, to ensure that the method will stil be defined in
# the correct place.
#
# @param [String] source The source to wrap
# @return [String]
def wrap_for_owner(source)
Pry.current[:pry_owner] = code_object.owner
"Pry.current[:pry_owner].class_eval do\n#{source}\nend"
Pry.current[:pry_owner] = method.owner
owner_source = definition_for_owner(source)
"Pry.current[:pry_owner].class_eval do; #{owner_source}\nend"
end

# Update the new source code to have the correct Module.nesting.
Expand All @@ -111,9 +113,9 @@ def wrap_for_owner(source)
# @param [String] source The source to wrap.
# @return [String]
def wrap_for_nesting(source)
nesting = Pry::Code.from_file(code_object.source_file).nesting_at(code_object.source_line)
nesting = Pry::Code.from_file(method.source_file).nesting_at(method.source_line)

(nesting + [source] + nesting.map{ "end" } + [""]).join("\n")
(nesting + [source] + nesting.map{ "end" } + [""]).join(";")
rescue Pry::Indent::UnparseableNestingError
source
end
Expand Down
27 changes: 27 additions & 0 deletions spec/method/patcher_spec.rb
@@ -0,0 +1,27 @@
require 'helper'

describe Pry::Method::Patcher do

before do
@x = Object.new
def @x.test; :before; end
@method = Pry::Method(@x.method(:test))
end

it "should change the behaviour of the method" do
@x.test.should == :before
@method.redefine "def @x.test; :after; end\n"
@x.test.should == :after
end

it "should return a new method with new source" do
@method.source.strip.should == "def @x.test; :before; end"
@method.redefine("def @x.test; :after; end\n").
source.strip.should == "def @x.test; :after; end"
end

it "should change the source of new Pry::Method objects" do
@method.redefine "def @x.test; :after; end\n"
Pry::Method(@x.method(:test)).source.strip.should == "def @x.test; :after; end"
end
end

0 comments on commit 290eb40

Please sign in to comment.