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 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,9 @@
1
+ $:.push('lib')
2
+ require 'monkeypatch'
3
+
4
+ Dir['task/*.rake'].each do |lib|
5
+ load lib
6
+ end
7
+
8
+ desc "Same as `rake test`"
9
+ task :default => :test
@@ -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>"
@@ -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,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.test_files = FileList['test/test*.rb']
6
+ t.verbose = true
7
+ t.ruby_opts = ["-Ilib"]
8
+ end
@@ -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