asynchronize 0.2.1 → 0.3.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.
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