Created
November 29, 2011 19:02
-
-
Save hopsoft/1405973 to your computer and use it in GitHub Desktop.
An attempt to define a standard for applying monkey patches
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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