zimbatm-monkeypatch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +34 -0
- data/Rakefile +9 -0
- data/example/patch_usage.rb +24 -0
- data/lib/monkeypatch.rb +212 -0
- data/task/gem.rake +37 -0
- data/task/rcov.rake +14 -0
- data/task/rdoc.rake +17 -0
- data/task/test.rake +8 -0
- data/test/test_monkeypatch.rb +139 -0
- metadata +61 -0
data/README.rdoc
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
= MonkeyPatch
|
2
|
+
|
3
|
+
<INSERT NICE INTRO HERE>
|
4
|
+
|
5
|
+
Do you monkeys, patch ? If so, use this library, you won't regret it.
|
6
|
+
|
7
|
+
Why use a library when you could do all this by hand ? For two reasons:
|
8
|
+
|
9
|
+
1. We provide the mechanism to protect from patch collision
|
10
|
+
2. By including this gem as dependency, you declare your project is
|
11
|
+
monkeypatching.
|
12
|
+
|
13
|
+
</INSERT NICE INTRO HERE>
|
14
|
+
|
15
|
+
== Usage
|
16
|
+
|
17
|
+
Example usage:
|
18
|
+
|
19
|
+
:include:example/patch_usage.rb
|
20
|
+
|
21
|
+
== Related projects
|
22
|
+
|
23
|
+
* http://github.com/coderrr/monkey_shield/ : provides sorts of namespaces to avoid patch collision
|
24
|
+
|
25
|
+
== Ideas
|
26
|
+
|
27
|
+
* method re-definition or module/class extension could be detected, especially when using Gems. The load-path is not the same between the original definition and the new-one.
|
28
|
+
* load-path as namespace
|
29
|
+
|
30
|
+
== TODO
|
31
|
+
|
32
|
+
* Add programmable patching conditions
|
33
|
+
* Add reason string
|
34
|
+
* Add 'monkeywarn' that warns when a monkeypatch is applied
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'monkeypatch'
|
2
|
+
|
3
|
+
# Define a new extension that adds the #to_blob method
|
4
|
+
date_patch = MonkeyPatch.add_method(:to_blob) do
|
5
|
+
def to_blob; "<blob>" end
|
6
|
+
end
|
7
|
+
|
8
|
+
x = "something"
|
9
|
+
date_patch.patch_instance(x)
|
10
|
+
x.to_blob #=> "<blob>"
|
11
|
+
|
12
|
+
# Define a patch, that replaces the #to_date method
|
13
|
+
each_patch = MonkeyPatch.replace_method(:to_s) do
|
14
|
+
def to_s; "..." end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ExampleClass
|
18
|
+
def to_s; "hello" end
|
19
|
+
end
|
20
|
+
|
21
|
+
(date_patch & each_patch).patch_class(ExampleClass)
|
22
|
+
|
23
|
+
ExampleClass.new.to_s #=> "..."
|
24
|
+
ExampleClass.new.to_blob #=> "<blob>"
|
data/lib/monkeypatch.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
=begin rdoc
|
2
|
+
|
3
|
+
This is the public API to produce new patches.
|
4
|
+
|
5
|
+
Once you have a patch, look at Patch and it's children to see how to use it.
|
6
|
+
|
7
|
+
=end
|
8
|
+
module MonkeyPatch
|
9
|
+
# MonkeyPatch's version as a string
|
10
|
+
VERSION = '0.1.0'
|
11
|
+
# May be raised on check_conflicts
|
12
|
+
class ConflictError < StandardError; end
|
13
|
+
|
14
|
+
# A collection of patches. Used to collect one or more patches with the #& operator
|
15
|
+
#
|
16
|
+
# NOTE: didn't use the Set class, to not force a dependency
|
17
|
+
class PatchSet
|
18
|
+
def initialize(patches) #:nodoc:
|
19
|
+
@patches = patches.to_a.uniq
|
20
|
+
end
|
21
|
+
|
22
|
+
# Aggregates Patch (es) and PatchSet (s)
|
23
|
+
def &(other)
|
24
|
+
PatchSet.new(@patches + other.to_a)
|
25
|
+
end
|
26
|
+
def to_a; @patches.dup end
|
27
|
+
|
28
|
+
# Delegates to patches
|
29
|
+
# TODO: determine what happens if a patch fails
|
30
|
+
def patch_class(klass)
|
31
|
+
@patches.each do |patch|
|
32
|
+
patch.patch_class(klass)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Delegates to patches
|
37
|
+
def patch_instance(obj)
|
38
|
+
for patch in @patches
|
39
|
+
patch.patch_instance(obj)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Abstract definition of a patch.
|
45
|
+
#
|
46
|
+
# You cannot create Patch instance yourself, they are spawned from
|
47
|
+
# MonkeyPatch class-methods.
|
48
|
+
class Patch
|
49
|
+
class << self
|
50
|
+
# Hide new to force api usage
|
51
|
+
private :new
|
52
|
+
end
|
53
|
+
# The callstack it was defined in
|
54
|
+
attr_reader :from
|
55
|
+
|
56
|
+
def initialize(&patch_def) #:nodoc:
|
57
|
+
raise ArgumentError, "patch_def not given" unless block_given?
|
58
|
+
@patch_def = patch_def
|
59
|
+
end
|
60
|
+
|
61
|
+
# Combine patches together. Produces a PatchSet instance.
|
62
|
+
def &(other)
|
63
|
+
PatchSet.new([self]) & other
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns [self], used by #&
|
67
|
+
def to_a; [self] end
|
68
|
+
|
69
|
+
# Patches a class or module instance methods
|
70
|
+
def patch_class(klass)
|
71
|
+
raise ArgumentError, "klass is not a Class" unless klass.kind_of?(Class)
|
72
|
+
|
73
|
+
return false if !check_conditions(klass)
|
74
|
+
|
75
|
+
check_conflicts!(klass)
|
76
|
+
|
77
|
+
# Apply
|
78
|
+
apply_patch(klass)
|
79
|
+
|
80
|
+
return true
|
81
|
+
end
|
82
|
+
|
83
|
+
# Patches the instance's metaclass
|
84
|
+
def patch_instance(obj)
|
85
|
+
meta = (class << obj; self end)
|
86
|
+
patch_class(meta)
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
# Condition are checks that raise nothing
|
92
|
+
#
|
93
|
+
# If a condition isn't met, the patch is not applied
|
94
|
+
#
|
95
|
+
# Returns true if all conditions are matched
|
96
|
+
def check_conditions(klass) #:nodoc:
|
97
|
+
if klass.respond_to?(:applied_patches) && klass.applied_patches.include?(self)
|
98
|
+
log "WARN: Patch already applied"
|
99
|
+
return false
|
100
|
+
end
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
# Re-implement in childs. Make sure super is called
|
105
|
+
#
|
106
|
+
# raises a ConflictError if an error is found
|
107
|
+
def check_conflicts!(klass) #:nodoc:
|
108
|
+
end
|
109
|
+
|
110
|
+
def apply_patch(klass) #:nodoc:
|
111
|
+
klass.extend IsPatched
|
112
|
+
klass.class_eval(&@patch_def)
|
113
|
+
klass.applied_patches.push self
|
114
|
+
end
|
115
|
+
|
116
|
+
def log(msg) #:nodoc:
|
117
|
+
MonkeyPatch.logger.log(msg)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class MethodPatch < Patch
|
122
|
+
# The name of the method to be patched
|
123
|
+
attr_reader :method_name
|
124
|
+
def initialize(method_name, &patch_def) #:nodoc:
|
125
|
+
super(&patch_def)
|
126
|
+
@method_name = method_name.to_s
|
127
|
+
unless Module.new(&patch_def).instance_methods(true) == [@method_name]
|
128
|
+
raise ArgumentError, "&patch_def does not define the specified method"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
protected
|
133
|
+
def check_conflicts!(klass) #:nodoc:
|
134
|
+
super
|
135
|
+
if klass.respond_to? :applied_patches
|
136
|
+
others = klass.applied_patches.select{|p| p.respond_to?(:method_name) && p.method_name == method_name }
|
137
|
+
if others.any?
|
138
|
+
raise ConflictError, "Conflicting patches: #{([self] + others).inspect}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Spawned by MonkeyPatch.add_method
|
145
|
+
class AddMethodPatch < MethodPatch
|
146
|
+
protected
|
147
|
+
def check_conflicts!(klass) #:nodoc:
|
148
|
+
super
|
149
|
+
if klass.method_defined?(method_name)
|
150
|
+
raise ConflictError, "Add already existing method #{method_name} in #{klass}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Spawned by MonkeyPatch.replace_method
|
156
|
+
class ReplaceMethodPatch < MethodPatch
|
157
|
+
protected
|
158
|
+
def check_conflicts!(klass) #:nodoc:
|
159
|
+
super
|
160
|
+
unless klass.method_defined?(method_name)
|
161
|
+
raise ConflictError, "Replacing method #{method_name} does not exist in #{klass}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Default MonkeyPatch::logger . Use the same interface
|
167
|
+
# if you want to replace it.
|
168
|
+
class STDERRLogger
|
169
|
+
def log(msg); STDERR.puts msg end
|
170
|
+
end
|
171
|
+
|
172
|
+
# This module extends patched objects to keep track of the applied patches
|
173
|
+
module IsPatched
|
174
|
+
def applied_patches; @__applied_patches__ ||= [] end
|
175
|
+
end
|
176
|
+
|
177
|
+
@logger = STDERRLogger.new
|
178
|
+
@loaded_patches = []
|
179
|
+
class << self
|
180
|
+
# Here goes the messages, this object should respond_to? :log
|
181
|
+
# Default is STDERRLogger
|
182
|
+
attr_accessor :logger
|
183
|
+
|
184
|
+
# All defined patches are stored here
|
185
|
+
attr_reader :loaded_patches
|
186
|
+
|
187
|
+
# Creates a new patch that adds a method to a class or a module
|
188
|
+
#
|
189
|
+
# Returns a Patch (AddMethodPatch)
|
190
|
+
def add_method(method_name, &definition)
|
191
|
+
new_patch AddMethodPatch.send(:new, method_name, &definition)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Creates a new patch that replaces a method of a class or a module
|
195
|
+
#
|
196
|
+
# Returns a Patch (ReplaceMethodPatch)
|
197
|
+
def replace_method(method_name, &definition)
|
198
|
+
new_patch ReplaceMethodPatch.send(:new, method_name, &definition)
|
199
|
+
end
|
200
|
+
|
201
|
+
protected
|
202
|
+
|
203
|
+
def new_patch(patch, &definition) #:nodoc:
|
204
|
+
#patch.validate
|
205
|
+
patch.instance_variable_set(:@from, caller[1])
|
206
|
+
@loaded_patches.push(patch)
|
207
|
+
patch
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
data/task/gem.rake
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rake/gempackagetask'
|
2
|
+
require 'monkeypatch'
|
3
|
+
|
4
|
+
spec = Gem::Specification.new do |s|
|
5
|
+
s.name = 'monkeypatch'
|
6
|
+
s.version = MonkeyPatch::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.summary = "Monkey patching made safe(er)"
|
9
|
+
s.homepage = "http://github.com/zimbatm/ruby-monkeypatch"
|
10
|
+
s.description = "Provides a mechanism to avoid patch collision. It's also useful to tell if your project is using monkeypatching or not."
|
11
|
+
s.authors = ["zimbatm"]
|
12
|
+
s.email = "zimbatm@oree.ch"
|
13
|
+
s.has_rdoc = true
|
14
|
+
s.files = FileList['README.rdoc', 'Rakefile', 'lib/*', 'test/*', 'task/*', 'example/*']
|
15
|
+
s.test_files = FileList['test/test*.rb']
|
16
|
+
end
|
17
|
+
|
18
|
+
file "ruby-monkeypatch.gemspec" do |t|
|
19
|
+
File.open(t.name, 'w') do |f|
|
20
|
+
f.write(spec.to_ruby)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
25
|
+
# pkg.need_zip = true
|
26
|
+
# pkg.need_tar = true
|
27
|
+
end
|
28
|
+
|
29
|
+
namespace :gem do
|
30
|
+
desc "Updates the ruby-monkeypatch.gemspec file"
|
31
|
+
task :spec do
|
32
|
+
File.open("ruby-monkeypatch.gemspec", 'w') do |f|
|
33
|
+
f.write(spec.to_ruby)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
data/task/rcov.rake
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
begin
|
2
|
+
require 'rcov/rcovtask'
|
3
|
+
|
4
|
+
Rcov::RcovTask.new do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.test_files = FileList['test/test*.rb']
|
7
|
+
t.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
rescue LoadError
|
11
|
+
if !Rake.application.options.silent
|
12
|
+
STDERR.puts "*** Install the RCov for code coverage"
|
13
|
+
end
|
14
|
+
end
|
data/task/rdoc.rake
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
begin
|
2
|
+
require 'rdoc/task'
|
3
|
+
RDocTask = RDoc::Task
|
4
|
+
rescue LoadError
|
5
|
+
if !Rake.application.options.silent
|
6
|
+
STDERR.puts "*** Install the RDoc 2.X gem for nicer docs"
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rake/rdoctask'
|
10
|
+
RDocTask = Rake::RDocTask
|
11
|
+
end
|
12
|
+
|
13
|
+
RDocTask.new do |rd|
|
14
|
+
rd.main = "README.rdoc"
|
15
|
+
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
16
|
+
end
|
17
|
+
|
data/task/test.rake
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
require 'monkeypatch'
|
4
|
+
|
5
|
+
class TestMonkeypatch < Test::Unit::TestCase
|
6
|
+
include MonkeyPatch
|
7
|
+
|
8
|
+
class FakeLogger
|
9
|
+
attr_reader :logs
|
10
|
+
def initialize
|
11
|
+
@logs = []
|
12
|
+
end
|
13
|
+
def log(msg)
|
14
|
+
@logs.push msg
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
@logger = MonkeyPatch.logger = FakeLogger.new
|
20
|
+
@logs = @logger.logs
|
21
|
+
@c = Class.new do
|
22
|
+
def existing_method; "original" end
|
23
|
+
end
|
24
|
+
@p_add = MonkeyPatch.add_method(:new_method) do
|
25
|
+
def new_method; "exists" end
|
26
|
+
end
|
27
|
+
@p_repl = MonkeyPatch.replace_method(:existing_method) do
|
28
|
+
def existing_method; "replaced" end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_invalid_patches
|
33
|
+
assert_raise(ArgumentError) do
|
34
|
+
MonkeyPatch.add_method(:new_method)
|
35
|
+
end
|
36
|
+
assert_raise(ArgumentError) do
|
37
|
+
MonkeyPatch.replace_method(:new_method)
|
38
|
+
end
|
39
|
+
assert_raise(ArgumentError) do
|
40
|
+
MonkeyPatch.add_method(:new_method) do
|
41
|
+
def new_method_typo; end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_do_not_apply_twice
|
47
|
+
@p_add.patch_class(@c)
|
48
|
+
@p_add.patch_class(@c)
|
49
|
+
assert_equal 1, @logs.size, "Should be the notice about the twice application"
|
50
|
+
assert_equal [@p_add], @c.applied_patches
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_add_method
|
54
|
+
@p_add.patch_class(@c)
|
55
|
+
|
56
|
+
assert_equal("exists", @c.new.new_method )
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_check_conflict
|
60
|
+
p1 = MonkeyPatch.add_method(:some) do
|
61
|
+
def some; "one" end
|
62
|
+
end
|
63
|
+
p2 = MonkeyPatch.add_method(:some) do
|
64
|
+
def some; "thing" end
|
65
|
+
end
|
66
|
+
c = Class.new
|
67
|
+
assert_nothing_raised do
|
68
|
+
p1.patch_class(c)
|
69
|
+
end
|
70
|
+
assert_raise(ConflictError) do
|
71
|
+
p2.patch_class(c)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_replace_method
|
76
|
+
@p_repl.patch_class(@c)
|
77
|
+
|
78
|
+
assert @c.respond_to?(:applied_patches)
|
79
|
+
|
80
|
+
assert_equal("replaced", @c.new.existing_method )
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_patch_class_application
|
84
|
+
assert !@c.new.respond_to?(:applied_patches)
|
85
|
+
@p_add.patch_class(@c)
|
86
|
+
assert_respond_to @c, :applied_patches
|
87
|
+
|
88
|
+
assert_equal [@p_add], @c.applied_patches
|
89
|
+
|
90
|
+
@p_repl.patch_class(@c)
|
91
|
+
assert_equal [@p_add, @p_repl], @c.applied_patches
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_patch_instance_application
|
95
|
+
p = MonkeyPatch.replace_method(:to_s) do
|
96
|
+
def to_s; "right" end
|
97
|
+
end
|
98
|
+
i = "left"
|
99
|
+
p.patch_instance(i)
|
100
|
+
assert_equal("right", i.to_s)
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_replace_conflict
|
104
|
+
c = Class.new
|
105
|
+
assert_raise(ConflictError) do
|
106
|
+
@p_repl.patch_class(c)
|
107
|
+
end
|
108
|
+
assert !c.new.respond_to?(:existing_method)
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_add_method_conflict
|
112
|
+
c = Class.new do
|
113
|
+
def new_method; "not replaced" end
|
114
|
+
end
|
115
|
+
assert_raise(ConflictError) do
|
116
|
+
@p_add.patch_class(c)
|
117
|
+
end
|
118
|
+
assert_equal "not replaced", c.new.new_method
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_patch_from
|
122
|
+
assert_equal(File.basename(__FILE__), File.basename(@p_add.from).gsub(/:.*/,''))
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_patch_set
|
126
|
+
both = (@p_add & @p_repl)
|
127
|
+
assert_equal(PatchSet, both.class)
|
128
|
+
|
129
|
+
inst = @c.new
|
130
|
+
both.patch_instance(inst)
|
131
|
+
assert_equal("exists", inst.new_method )
|
132
|
+
assert_equal("replaced", inst.existing_method )
|
133
|
+
|
134
|
+
both.patch_class(@c)
|
135
|
+
assert_equal("exists", @c.new.new_method )
|
136
|
+
assert_equal("replaced", @c.new.existing_method )
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zimbatm-monkeypatch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- zimbatm
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-26 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Provides a mechanism to avoid patch collision. It's also useful to tell if your project is using monkeypatching or not.
|
17
|
+
email: zimbatm@oree.ch
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- README.rdoc
|
26
|
+
- Rakefile
|
27
|
+
- lib/monkeypatch.rb
|
28
|
+
- test/test_monkeypatch.rb
|
29
|
+
- task/gem.rake
|
30
|
+
- task/rcov.rake
|
31
|
+
- task/rdoc.rake
|
32
|
+
- task/test.rake
|
33
|
+
- example/patch_usage.rb
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://github.com/zimbatm/ruby-monkeypatch
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.2.0
|
57
|
+
signing_key:
|
58
|
+
specification_version: 2
|
59
|
+
summary: Monkey patching made safe(er)
|
60
|
+
test_files:
|
61
|
+
- test/test_monkeypatch.rb
|