Skip to content

Instantly share code, notes, and snippets.

@hopsoft
Created November 29, 2011 19:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hopsoft/1405973 to your computer and use it in GitHub Desktop.
Save hopsoft/1405973 to your computer and use it in GitHub Desktop.
An attempt to define a standard for applying monkey patches
# An attempt to define a standard interface for monkey patching existing method definitions
# on existing Object instances, Classes, and Modules.
#
# This effort warrants a new monkey patching nomenclature.
# * Monkey Patch - a re-definition of an existing method that was patched via MonkeyPatcher
# * Patch - a re-definition of an existing method
#
# Lets get started with some usage examples.
# First lets add some helper methods to all objects.
#
# MonkeyPatcher.deflower_object
#
# Now lets kick the tires.
#
# class Foo
# def bar
# :bar
# end
# end
#
# Foo.monkey_patched?(:bar) # => false
# Foo.patched?(:bar) # => false
# Foo.new.bar # => :bar
# Foo.method_detail(:bar)
# # => {["(pry)", 4]=>[{:context=>"Foo", :current=>true}]}
#
# Foo.monkey_patch(:bar) { :patch1 }
# Foo.monkey_patched?(:bar) # => true
# Foo.patched? # => true
# Foo.new.bar # => :patch1
# Foo.method_detail(:bar)
# # => { ["(pry)", 14] => [{:context => "Foo", :monkey_patch => true, :current => true}],
# # ["(pry)", 4] => [{:context => "Foo"}] }
#
# f = Foo.new
# f.monkey_patch(:bar) { :patch2 }
# f.monkey_patched?(:bar) # => true
# f.patched?(:bar) # => true
# f.bar # => :patch2
# f.method_detail(:bar)
# # => { ["(pry)", 23] => [{:context => "Foo", :monkey_patch => true, :current => true}],
# # ["(pry)", 14] => [{:context => "Foo"}, {:context => "Foo", :monkey_patch => true}],
# # ["(pry)", 4] => [{:context => "Foo"}] }
#
# Foo.method_detail(:bar)
# # => { ["(pry)", 14] => [{:context => "Foo", :monkey_patch => true, :current => true}],
# # ["(pry)", 4] => [{:context => "Foo"}] }
#
# MonkeyPatcher.all_methods_with_detail(Foo)
# # => { :bar=> { ["(pry)", 14] => [{:context=>"Foo", :monkey_patch=>true, :current=>true}],
# # ["(pry)", 4] => [{:context=>"Foo"}] },
# # :to_yaml => {
# # ["/Users/nhopkins/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/1.9.1/psych/core_ext.rb", 13] => [
# # {:context=>"Foo", :current=>true},
# # {:context=>"Object"}] },
# # ...
#
# Enjoy! And, please update this gist with any comments or requests.
class MonkeyPatcher
class << self
# Adds some monkey patching helper methods to Object.
# Namely:
# * monkey_patch
# * monkey_patched?
# * patched?
# * method_detail
def deflower_object
Object.send(:include, MonkeyPatcher::ObjectInstanceMethods)
end
# Indicates if a context is either a Class or Module.
def definition?(context)
context.is_a?(Class) || context.is_a?(Module)
end
# Returns the eigenclass for a context.
def eigenclass(context)
class << context
self
end
end
# Indicates whether or not a context is an eigenclass.
def eigenclass?(context)
definition?(context) && context.name.nil?
end
# Returns a patch-context for a context.
def patch_context(context)
return context if definition?(context)
eigenclass(context)
end
# Returns a patch-context name for a context.
def patch_context_name(context)
return context.superclass.name if eigenclass?(context)
return context.name if definition?(context)
"#{context.class.name}_#{context.object_id}"
end
# Returns all monkey patches that have been applied to a context.
def monkey_patches(context)
patch_context(context).instance_eval { @monkey_patches ||= {} }
end
# Returns a list of monkey patches for a specific method that have been applied to a context.
def patch_list(context, method_name)
monkey_patches(context).inject([]) do |value, patches|
value.concat(patches.last[method_name] || [])
end
end
# Monkey patches a method for a context.
def monkey_patch(context, method_name, &block)
key = patch_context_name(context)
monkey_patches(context)[key] ||= {}
patch_context(context).instance_eval do
orig_method = instance_method(method_name)
# apply the monkey patch in this wonky way in order to save the
# correct method object so we can compare method object_ids later
define_method(:tmp_monkey_patch, &block)
new_method = instance_method(:tmp_monkey_patch)
remove_method(:tmp_monkey_patch)
define_method(method_name, new_method)
#define_method(method_name, &block)
# save some information about the monkey patch
patch_data = Proc.new do |a, b|
{ :orig_method => a,
:orig_method_source => (a.source_location rescue nil),
:method => b,
:method_source => b.source_location }
end
# initialize and add the original method definition to the patch list
if MonkeyPatcher.monkey_patches(context)[key][method_name].nil?
MonkeyPatcher.monkey_patches(context)[key][method_name] ||= []
MonkeyPatcher.monkey_patches(context)[key][method_name] << patch_data.call(nil, orig_method)
end
# add monkey patches to the list
MonkeyPatcher.monkey_patches(context)[key][method_name] << patch_data.call(orig_method, new_method)
end
end
# Returns a Hash of detailed information about a context's methods.
#
# Example:
#
# # engine.rb -------------------------
# module Engine
# def start
# "start engine"
# end
# end
#
# # motor_vehicle.rb ------------------
# class MotorVehicle
# include Engine
#
# def start
# "motor vehicle #{super}"
# end
# end
#
# # car.rb ----------------------------
# class Car < MotorVehicle
# def start
# "car start override"
# end
# end
#
# # project.rb ------------------------
# Car.monkey_patch(:start) { "patched car start" }
#
# # reopen_car.rb ----------------------------
# class Car < MotorVehicle
# def start
# "re-opened car start override after a monkey patch"
# end
# end
#
# #####################################
#
# Car.method_info
#
# :start => {
# ["/path/to/monkey_patch.rb", 1] => [{:context => "Car", :monkey_patch => true}],
# ["/path/to/car.rb", 2] => [{:context => "Car"}],
# ["/path/to/reopen_car.rb", 2] => [{:context => "Car", :current => true}],
# ["/path/to/motor_vehicle.rb", 4] => [{:context => "MotorVehicle"}],
# ["/path/to/engine.rb", 2] => [{:context => "Engine"}]},
# :other_method => {["path/to/file", LINE_NUMBER] => [{...}]},
# :other_method => {["path/to/file", LINE_NUMBER] => [{...}]},
# ...}
#
def all_methods_with_detail(context)
info = {}
patch_context(context).instance_eval do
ancestors.each do |ancestor|
ancestor.instance_methods.each do |method_name|
current_method = instance_method(method_name)
info[method_name] ||= {}
patch_finder = lambda do |patch|
key = patch[:method_source]
value = { :context => ancestor.name }
value[:monkey_patch] = true if patch[:orig_method]
value[:current] = true if current_method == patch[:method]
info[method_name][key] ||= []
info[method_name][key] << value
end
# find method data for object instances
unless MonkeyPatcher.definition?(context)
MonkeyPatcher.patch_list(context, method_name).reverse.each(&patch_finder)
end
# find method data for the ancestor chain
MonkeyPatcher.patch_list(ancestor, method_name).reverse.each(&patch_finder)
# capture method data that not patched via MonkeyPatcher
method = ancestor.instance_method(method_name)
key = method.source_location # rescue "-"
value = { :context => ancestor.name }
value[:current] = true if current_method == method
info[method_name][key] ||= []
values = info[method_name][key]
patched = values.inject(false) { |p, v| p ||= v[:monkey_patch] }
info[method_name][key] << value unless patched
end
end
end
info
end
# Returns a Hash of detailed information about a method.
def method_detail(context, method_name)
all_methods_with_detail(context)[method_name]
end
# Indicates if a method on a given context has been monkey patched with MonkeyPatcher.
def monkey_patched?(context, method_name)
details = method_detail(context, method_name)
details.values.flatten.inject(false) { |p, v| p ||= v[:monkey_patch] }
end
# Indicates if a method has been patched.
def patched?(context, method_name)
method_detail(context, method_name).length > 1
end
end
module ObjectInstanceMethods
def monkey_patch(method_name, &block)
MonkeyPatcher.monkey_patch(self, method_name, &block)
end
def method_detail(method_name)
MonkeyPatcher.method_detail(self, method_name)
end
def monkey_patched?(method_name)
MonkeyPatcher.monkey_patched?(self, method_name)
end
def patched?(method_name)
MonkeyPatcher.patched?(self, method_name)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment