asynchronize 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/lib/asynchronize.rb +66 -60
  3. data/readme.md +66 -48
  4. data/spec/spec.rb +10 -44
  5. metadata +65 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f84e1c57b42abe8917434680f2a3889a4edc2608af231513f3b24bbe77d2e2c
4
- data.tar.gz: 8dabfe07d60a388bf73a2b7c2c3b36a8d941bf0f8502bd425b4736c2ec81fc2f
3
+ metadata.gz: 912acc8b2e852aa7ccdd7fc09193f46fa64730f74972e9a955b8ba2bf3e40f1f
4
+ data.tar.gz: c6b91b991495bb45ce6580596c9919a4a376437f3c441706c63f84a62941c77e
5
5
  SHA512:
6
- metadata.gz: 23744ccb079b4e39828a9d11a46be4a56e97869f88a9e99661685fc405fa609ed92a046ce22b3642670028ccf951c5558246269418967826741184cfbf43c206
7
- data.tar.gz: 2442aaf5bcc17538b569c00fd28b76fdfb9451d25160b47b1763e7237238d22309c49acac4c141745226e4a07f665e0feb691748d0303ea480dd75a7e06d6052
6
+ metadata.gz: 86f7e988a2b97f36bb951045fca9214fd81b53239ae6a2fe5c0330a01f6f81c51bfcdb73052e2ac07bf434ec47dd362005274fa54243f8b3be9e4ec74fe53a96
7
+ data.tar.gz: 274687d5e6b292d4f6bbf85515c95f9d4eb84bef5b8c9b5a51f6fd363ed99000ee7e36ff30672d6b714009077a637b3ecc43746a726631164b12d44d7ac1b563
@@ -1,83 +1,89 @@
1
+ ##
2
+ # Include this module to allow a declarative syntax for defining asynch methods
3
+ #
4
+ # Defines only one method on the including class: `asynchronize`
5
+ #
1
6
  module Asynchronize
2
- require 'set'
3
7
  def self.included(base)
4
8
  base.class_eval do
5
- # The methods we have already asynchronized
6
- @asynced_methods = Set.new
7
- # The methods that should be asynchronized.
8
- @methods_to_async = Set.new
9
- # Originally used a single value here, but that's not thread safe.
10
- # ...Though you probably have other problems if you have multiple
11
- # threads adding methods to your class.
12
- @methods_asyncing = Set.new
13
-
14
9
  ##
15
10
  # Call to asynchronize a method.
16
- # That method will be added to a list of methods to asynchronize.
17
- # If the method already exists, it will be redefined to an asynchronous
18
- # version. If it does not, method_missing will redefine it when it does.
19
- # If the method is redefined afterwards, method_missing will also
20
- # asynchronize that version.
11
+ #
12
+ # This does two things
13
+ # 1. Creates and prepends a module named Asynchronized.
14
+ # 2. Scopes that module to the calling class.
15
+ # 3. Defines each of the passed methods on that module.
16
+ #
17
+ # Additional notes:
18
+ # - The new methods wrap the old method within Thread.new.
19
+ # - Subsequent calls only add methods to the existing Module.
20
+ # - Will silently fail if the method has already been asynchronized
21
21
  #
22
22
  # @param methods [Symbol] The methods to be asynchronized.
23
23
  # @example To add any number of methods to be asynchronized.
24
24
  # asynchronize :method1, :method2, :methodn
25
+ #
25
26
  def self.asynchronize(*methods)
26
- @methods_to_async.merge(methods.map {|m| m.hash})
27
- methods.each do |method|
28
- # If it's not defined yet, we'll get it with method_added
29
- Asynchronize.create_new_method(method, self) if method_defined?(method)
30
- end
31
- end
32
-
33
- # Save the old method_added so we don't overwrite it.
34
- if self.methods.include?(:method_added)
35
- singleton_class.send(:alias_method, :old_method_added, :method_added)
36
- singleton_class.send(:undef_method, :method_added)
37
- end
38
-
39
- ##
40
- # Will asynchronize a method if it has not been asynchronized already, and
41
- # it is in the list of methods to asynchronize. If method missing was
42
- # already defined, it will call the previous method_missing before
43
- # anything else Ruby calls this automatically when defining a method; it
44
- # should not be called directly.
45
- def self.method_added(method)
46
- # Return if this is an inherited class that hasn't included asynchronize
47
- return if @methods_asyncing.nil?
48
- # Return if we're already processing this method
49
- return if @methods_asyncing.include?(method.hash)
50
- @methods_asyncing.add(method.hash)
51
- self.old_method_added(method) if self.methods.include?(:old_method_added)
52
- return unless @methods_to_async.include?(method.hash)
53
- # This will delete from @methods_asyncing
54
- Asynchronize.create_new_method(method, self)
27
+ return if methods.empty?
28
+ async_container = Asynchronize._get_container_for(self)
29
+ Asynchronize._define_methods_on_object(methods, async_container)
55
30
  end
56
31
  end
57
32
  end
58
33
 
34
+ private
59
35
  ##
60
- # Responsible for actually creating the new methods and removing the old.
61
- def self.create_new_method(method, klass)
62
- klass.instance_eval do
63
- old_method = instance_method(method)
64
- return if @asynced_methods.include?(old_method.hash)
65
- undef_method(method)
66
-
67
- @methods_asyncing.add(method.hash)
68
- define_method(method, Asynchronize._build_new_method(old_method))
69
- @methods_asyncing.delete(method.hash)
70
- @asynced_methods.add(instance_method(method).hash)
36
+ # Define methods on object
37
+ #
38
+ # For each method in the methods array
39
+ #
40
+ # - If method already defined, go to the next.
41
+ # - If method does not exist, create it and go to the next.
42
+ #
43
+ # @param methods [Array<Symbol>] The methods to be bound.
44
+ # @param obj [Object] The object for the methods to be defined on.
45
+ #
46
+ def self._define_methods_on_object(methods, obj)
47
+ methods.each do |method|
48
+ next if obj.methods.include?(method)
49
+ obj.send(:define_method, method, _build_method)
71
50
  end
72
51
  end
73
52
 
74
- private
75
- def self._build_new_method(old_method)
53
+ ##
54
+ # Build Method
55
+ #
56
+ # This always returns the same Proc object. In it's own method for clarity.
57
+ #
58
+ # @return [Proc] The actual asynchronous method defined.
59
+ #
60
+ def self._build_method
76
61
  return Proc.new do |*args, &block|
77
- return Thread.new(old_method, args, block) do |told_method, targs, tblock|
78
- Thread.current[:return_value] = told_method.bind(self).call(*targs)
79
- tblock.call(Thread.current[:return_value]) unless tblock.nil?
62
+ return Thread.new(args, block) do |thread_args, thread_block|
63
+ Thread.current[:return_value] = super(*thread_args)
64
+ thread_block.call(Thread.current[:return_value]) if thread_block
80
65
  end
81
66
  end
82
67
  end
68
+
69
+ ##
70
+ # Container setup
71
+ #
72
+ # Creates the container module that will hold our asynchronous wrappers.
73
+ #
74
+ # - If the container module is defined, return it.
75
+ # - If the container module is not defined, create, prepend, and return it.
76
+ #
77
+ # @param obj [Class] The Class to prepend our module to
78
+ # @return [Module] The already prepended module to define our methods on.
79
+ #
80
+ def self._get_container_for(obj)
81
+ if obj.const_defined?('Asynchronized')
82
+ return obj.const_get('Asynchronized')
83
+ else
84
+ async_container = obj.const_set('Asynchronized', Module.new)
85
+ obj.prepend async_container
86
+ return async_container
87
+ end
88
+ end
83
89
  end
data/readme.md CHANGED
@@ -2,12 +2,13 @@
2
2
  [![Maintainability](https://api.codeclimate.com/v1/badges/30d40e270a3d7a0775a9/maintainability)](https://codeclimate.com/github/kennycoc/asynchronize/maintainability)
3
3
  [![Test Coverage](https://api.codeclimate.com/v1/badges/30d40e270a3d7a0775a9/test_coverage)](https://codeclimate.com/github/kennycoc/asynchronize/test_coverage)
4
4
  # Asynchronize
5
- ### The easiest way to make multiple methods asynchronous.
5
+ ### A declarative syntax for creating multithreaded methods.
6
6
 
7
7
  Find yourself writing the same boilerplate for all your asynchronous methods?
8
8
  Get dry with asynchronize.
9
9
 
10
- Just install with `gem install asynchronize` or add to your Gemfile and `bundle`
10
+ Just add to your Gemfile and `bundle` or install globally with
11
+ `gem install asynchronize`
11
12
 
12
13
  ## Usage
13
14
  Create a class with asynchronized methods
@@ -15,7 +16,7 @@ Create a class with asynchronized methods
15
16
  require 'asynchronize'
16
17
  class Test
17
18
  include Asynchronize
18
- # This can be called anywhere.
19
+ # Can be called before or after the definitions. I prefer it at the top of classes.
19
20
  asynchronize :my_test, :my_other_test
20
21
  def my_test
21
22
  return 'test'
@@ -31,20 +32,25 @@ You can manage the thread yourself; the returned value will be in the thread
31
32
  variable `:return_value` once it returns.
32
33
  ```Ruby
33
34
  thread = Test.new.my_test
34
- thread.join
35
- puts thread[:return_value] # > test
35
+ puts thread.join[:return_value] # > test
36
36
  ```
37
37
 
38
- Or for convenience, you can just pass it a block.
39
- The return value, will still be in the thread variable `:return_value`
38
+ Or to stay asynchronous when processing the result, you can pass it a block.
39
+ It will still return the thread, and the return value will still be in the
40
+ thread variable `:return_value`
40
41
  ```Ruby
41
- Test.new.my_test do |return_value|
42
+ thread = Test.new.my_test do |return_value|
42
43
  puts return_value # > test
43
44
  end
45
+ thread.join[:return_value] # > also test
44
46
  ```
45
47
 
48
+ As you can see, it's just a regular thread. Make sure you call `Thread#join` to
49
+ ensure it completes before your process exits, and to catch any exceptions that
50
+ may have been thrown!
51
+
46
52
  ## Inspiration
47
- I got tired of writing this over and over.
53
+ While working on another project, I found myself writing this way too often:
48
54
  ```Ruby
49
55
  def method_name(args)
50
56
  Thread.new(args) do |targs|
@@ -52,63 +58,75 @@ def method_name(args)
52
58
  end
53
59
  end
54
60
  ```
55
- It's extra typing, adds an unneeded extra layer of nesting, and just feels
56
- dirty. I couldn't find an existing library that wasn't trying to solve other
57
- problems I didn't have. Now, just call asynchronize to make any method
58
- asynchronous.
61
+ It's extra typing, and adds an unneeded extra layer of nesting. I couldn't find
62
+ an existing library that wasn't trying add new layers of abstraction to
63
+ memorize; sometimes you just want a normal thread. Now, just call asynchronize
64
+ to make any method asynchronous.
59
65
 
60
66
  ## Versioning Policy
61
- Once I feel like this is ready for production code, version 1.0.0, this project
62
- will follow [Semantic Versioning](https://semver.org) until then, the patch
63
- number (0.0.x) will be updated for any changes that do not affect the public
64
- interface. Versions that increment the minor number will have at least one of
65
- the following. A new feature will be added, some feature will be deprecated, or
66
- some previously deprecated feature will be removed. Deprecated features will be
67
- removed on the very next version that increments the minor version number.
67
+
68
+ Beginning with version 1.0.0, this project will follow [Semantic Versioning]
69
+ (https://semver.org) until then, the patch number (0.0.x) will be
70
+ updated for any changes that do not affect the public interface. Versions that
71
+ increment the minor number will have at least one of the following. A new
72
+ feature will be added, some feature will be deprecated, or some previously
73
+ deprecated feature will be removed. Deprecated features will be removed on the
74
+ very next version that increments the minor version number.
68
75
 
69
76
  ## FAQ
70
77
  ### Doesn't metaprogramming hurt performance?
71
- Not at all! We're actually totally redefining the methods, so the method itself
72
- is exactly as efficient as it would have been had you wrote it that way
73
- originally.
78
+ Not at all! It actually works just like inheritance, so it won't be a problem.
74
79
 
75
80
  ### So, how does it work?
76
- When you `include Asynchronize` it does two things.
77
- 1. It defines the asynchronize method for your class
78
- 2. It defines method_added on your class.
81
+ When you `include Asynchronize` it creates an `asynchronize` method on your
82
+ class. The first time you call this method with any arguments, it creates a new
83
+ module with the methods you define. It uses `Module#prepend` to cause method
84
+ calls on the original object to be sent to it instead, and uses super to call
85
+ your original method inside it's own thread.
79
86
 
80
- When you call asynchronize, it creates a set containing all of the methods you
81
- want asynchronized. If they already exist, they are modified; otherwise,
82
- method_added checks for them with every new method you add to the class. This
83
- way, you can call asynchronize any time, and know that the methods will be
84
- asynchronized when you use them.
85
-
86
- ### So, does that mean I can't use asynchronize if I already use method_added?
87
- We check for and alias your old method_added. It will be called before
88
- anything else. Of course, if you define method_added after including
89
- Asynchronize, you have to do the same and be careful to not overwrite ours!
87
+ This implementation allows you to call asynchronize at the top of the class and
88
+ then define the methods below. Since it changes how you interact with those
89
+ method's return values, I thought it was important to allow this.
90
90
 
91
91
  ### Why do I need another gem? My code's bloated enough as it is?
92
92
  It's super tiny. Just a light wrapper around the existing language features.
93
- Seriously, it's just around fifty lines of code. Actually, according to
94
- [cloc](https://www.npmjs.com/package/cloc) there's almost four times as many
95
- lines in the tests as the source. You should read it, I'd love feedback!
93
+ Seriously, it's just around forty lines of code as of version 0.3.0. Actually,
94
+ according to [cloc](https://www.npmjs.com/package/cloc) there's almost four
95
+ times as many lines in the tests as the source. You should read it, I'd love
96
+ feedback!
96
97
 
97
98
  ### Do you accept contributions?
98
- Absolutely! If your use case isn't compatible with the project, you find a
99
- bug, or just want to donate some tests; make an issue or send a PR please.
100
- To run the test suite, just run `bundle` then `rake` from the project directory.
99
+ Absolutely!
100
+ 1. Fork it (https://github.com/kennycoc/asynchronize/fork)
101
+ 2. Create your feature branch (git checkout -b my-new-feature)
102
+ 3. Commit your changes (git commit -am 'Add some feature')
103
+ 4. Push to the branch (git push origin my-new-feature)
104
+ 5. Create a new pull request.
105
+
106
+ It's just `bundle` to install dependencies, and `rake` to run the tests.
101
107
 
102
108
  ### What's the difference between asynchronize and promises/async..await?
103
- Those projects and similar ones aim to create an entirely new abstraction to use
104
- for doing things asynchronously. This project simply aims to make the existing
105
- language features easier to use with less typing. Just define a regular method,
109
+ Those and other similar projects aim to create an entirely new abstraction to
110
+ use for interacting with threads. This project aims to be a light convenience
111
+ wrapper around the existing language features. Just define a regular method,
106
112
  then interact with it's result like a regular thread.
107
113
 
108
114
  ### What versions are supported?
109
- In theory, this should work for all versions of Ruby. So far, Travis only tests
110
- for MRI 2.2.2 and 2.5.1. I plan on verifying and adding older versions and other
111
- implementations as I am able.
115
+ Ruby 2.3 and up. Unfortunately, Ruby versions prior to 2.0 do not support
116
+ `Module#prepend` and are not supported. Ruby versions prior to 2.3 have a bug
117
+ preventing usage of `super` with `define_method`. I'm unable to find a suitable
118
+ workaround for this issue. (`method(__method__).super_method.call` causes
119
+ problems when a method inherits from the asynchronized class.)
120
+
121
+ Luckily, all major Ruby implementations support Ruby language version 2.3. So I
122
+ don't see this as a huge problem. If anyone wants support for older versions,
123
+ and knows how to workaround this issue, feel free to submit a pull request.
124
+
125
+ We explicitly test against the following versions:
126
+ - Matz Ruby 2.5.1
127
+ - Matz Ruby 2.3.4
128
+ - JRuby 9.1.13 (language version 2.3.3)
129
+ - Rubinius 3.105 (language version 2.3.1)
112
130
 
113
131
  ## License
114
132
  MIT
@@ -16,29 +16,15 @@ class BasicSpec < Minitest::Test
16
16
  end
17
17
 
18
18
  describe "when we asynchronize a method" do
19
- it "should not be the same method" do
20
- original_method = Test.instance_method :test
21
- Test.asynchronize :test
22
- new_method = Test.instance_method(:test)
23
- original_method.wont_equal(new_method, "The method was not overwritten")
24
- end
25
19
  it "should not return a thread unless we asynchronize it" do
26
20
  Test.new.test.class.wont_equal(Thread, "The method was not overwritten")
27
21
  end
28
- it "should be the same method if we call a second time" do
29
- Test.asynchronize :test
30
- original_method = Test.instance_method :test
31
- Test.asynchronize :test
32
- new_method = Test.instance_method :test
33
- original_method.must_equal(new_method,
34
- "Asynchronized Inception has occurred")
35
- end
36
22
  it "should not throw an error if the specified method does not exist" do
37
23
  Test.asynchronize :notamethod
38
24
  end
39
25
  it "should not create a method if the specified method does not exist" do
40
26
  Test.asynchronize :notamethod
41
- Test.method_defined?(:notamethod).must_equal false
27
+ Test.methods(false).wont_include(:notamethod)
42
28
  end
43
29
  it "should not affect methods on other classes when called before" do
44
30
  Test.asynchronize :test
@@ -105,35 +91,6 @@ class BasicSpec < Minitest::Test
105
91
  end
106
92
  end
107
93
 
108
- describe "when there is an existing method_added" do
109
- before do
110
- class MethodAddedTest
111
- @running = false
112
- def self.method_added(method)
113
- return if @running
114
- @running = true
115
- old_method = instance_method(method)
116
- undef_method(method)
117
- define_method(method) do
118
- return old_method.bind(self).call + 1
119
- end
120
- @running = false
121
- end
122
- include Asynchronize
123
- asynchronize :test
124
- def test
125
- return 4
126
- end
127
- end
128
- end
129
- after do
130
- BasicSpec.send(:remove_const, :MethodAddedTest)
131
- end
132
- it "should call that method_added before, and only once." do
133
- MethodAddedTest.new.test.join[:return_value].must_equal 5
134
- end
135
- end
136
-
137
94
  describe "when inheriting from another class" do
138
95
  before do
139
96
  class ChildClassTest < Test
@@ -165,5 +122,14 @@ class BasicSpec < Minitest::Test
165
122
  ChildClassTest.new.test.must_equal 6
166
123
  end
167
124
  end
125
+
126
+ describe "when asynchronize is called with no arguments" do
127
+ it "should not define an Asynchronized container" do
128
+ Test.asynchronize
129
+ Test.ancestors.find do |a|
130
+ a.name.split('::').include? 'Asynchronized'
131
+ end.must_be_nil
132
+ end
133
+ end
168
134
  end
169
135
  end
metadata CHANGED
@@ -1,17 +1,74 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asynchronize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenneth Cochran
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-04 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Take any synchronous method, and run it asynchronously, without cluttering
14
- your code with repetetive boilerplate.
11
+ date: 2018-06-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '12.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '12.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: simplecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.16'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.11'
69
+ description: Sometimes you just want a regular thread without the overhead of a whole
70
+ new layer of abstraction. Asynchronize provides a declarative syntax to wrap any
71
+ method in a Thread.
15
72
  email: kenneth.cochran101@gmail.com
16
73
  executables: []
17
74
  extensions: []
@@ -26,7 +83,7 @@ homepage: https://github.com/kennycoc/asynchronize
26
83
  licenses:
27
84
  - MIT
28
85
  metadata: {}
29
- post_install_message:
86
+ post_install_message: Making something cool with asynchronize? Let me know at https://github.com/kennycoc/asynchronize
30
87
  rdoc_options: []
31
88
  require_paths:
32
89
  - lib
@@ -34,7 +91,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
34
91
  requirements:
35
92
  - - ">="
36
93
  - !ruby/object:Gem::Version
37
- version: '0'
94
+ version: '2.3'
38
95
  required_rubygems_version: !ruby/object:Gem::Requirement
39
96
  requirements:
40
97
  - - ">="
@@ -45,7 +102,7 @@ rubyforge_project:
45
102
  rubygems_version: 2.7.6
46
103
  signing_key:
47
104
  specification_version: 4
48
- summary: Easily make multiple methods asynchronous with one line of code.
105
+ summary: A declarative syntax for creating multithreaded methods.
49
106
  test_files:
50
107
  - spec/spec.rb
51
108
  - spec/minitest_helper.rb