mocktail 0.0.2 → 0.0.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5a4729eb601d9f3ed3ece213a460dd0881105a505a812c4d035c14cec252223
4
- data.tar.gz: f4b3a3352f63de7de812ce8db616b570de740f1ce7550f8677eb47ecb99305fb
3
+ metadata.gz: e719e2144c3da8a2569e2ef5e7cf2c78d054625011fcf0bdf8f98f300bdcd805
4
+ data.tar.gz: 3635b7210ac6d5a7c05c4000f100ac1e07f4c8d5de7de81586e41f3b81e11ce5
5
5
  SHA512:
6
- metadata.gz: 469b39914d0d887b7cd8d25006f5dc6f3ca8a1a3b5594f39bfae5e3c21f36f2983a30ec2849c916129e216dd9fb731f476abbdc5a41ad0f80c740d7106793a7d
7
- data.tar.gz: d7c24487a15c7ddc9b540d6c9d5c2b3f07da3599eba61eee128a79f5faa3fd5f6c4e54b7ea4318119a86737ead855f30d814a7f39e30c1e20a6f3880f0c21ce2
6
+ metadata.gz: d84b7964c5fbe14e3ef2a8a881c1c328f0f4c37cbde2d1423965016fe26f153c6801032c5aa4a7018db6d98924aecbb031e3e473992178bb9d5fe46e53ac2ea5
7
+ data.tar.gz: 384e42b818b92ace165e975dafd1482e00a46fc3be4c4abf0bb54e2ff648f29da1b338a8845be5c64e1878abf738e4061345083c0e952206380119ecb82729cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.0.3
2
+
3
+ * Implement method_missing on all mocked instance methods to print out useful
4
+ information, like the target type, the attempted call, an example method
5
+ definition that would match the call (for paint-by-numbers-like TDD), and
6
+ did_you_mean gem integration of similar method names in case it was just a
7
+ miss
8
+ * Cleans artificially-generated argument errors of gem-internal backtraces
9
+
1
10
  # 0.0.2
2
11
 
3
12
  * Drop Ruby 2.7 support. Unbeknownst to me (since I developed mocktail using
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (0.0.2)
4
+ mocktail (0.0.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,5 +1,3 @@
1
- require_relative "../share/simulates_argument_error"
2
-
3
1
  module Mocktail
4
2
  class ValidatesArguments
5
3
  def self.disable!
@@ -30,28 +28,9 @@ module Mocktail
30
28
  def validate(dry_call)
31
29
  return if self.class.disabled?
32
30
 
33
- arg_params, kwarg_params = dry_call.original_method.parameters.reject { |type, _|
34
- type == :block
35
- }.partition { |type, _|
36
- [:req, :opt, :rest].include?(type)
37
- }
38
-
39
- unless args_match?(arg_params, dry_call.args) &&
40
- kwargs_match?(kwarg_params, dry_call.kwargs)
41
- raise @simulates_argument_error.simulate(arg_params, dry_call.args, kwarg_params, dry_call.kwargs)
31
+ if (error = @simulates_argument_error.simulate(dry_call))
32
+ raise error
42
33
  end
43
34
  end
44
-
45
- private
46
-
47
- def args_match?(arg_params, args)
48
- args.size >= arg_params.count { |type, _| type == :req } &&
49
- (arg_params.any? { |type, _| type == :rest } || args.size <= arg_params.size)
50
- end
51
-
52
- def kwargs_match?(kwarg_params, kwargs)
53
- kwarg_params.select { |type, _| type == :keyreq }.all? { |_, name| kwargs.key?(name) } &&
54
- (kwarg_params.any? { |type, _| type == :keyrest } || kwargs.keys.all? { |name| kwarg_params.any? { |_, key| name == key } })
55
- end
56
35
  end
57
36
  end
@@ -2,13 +2,13 @@ module Mocktail
2
2
  class DeclaresDryClass
3
3
  def initialize
4
4
  @handles_dry_call = HandlesDryCall.new
5
+ @raises_neato_no_method_error = RaisesNeatoNoMethodError.new
5
6
  end
6
7
 
7
8
  def declare(type)
8
- type_type = type_of(type)
9
9
  instance_methods = instance_methods_on(type)
10
10
  dry_class = Class.new(Object) {
11
- include type if type_type == :module
11
+ include type if type.instance_of?(Module)
12
12
 
13
13
  def initialize(*args, **kwargs, &blk)
14
14
  end
@@ -18,15 +18,19 @@ module Mocktail
18
18
  }
19
19
  alias_method :kind_of?, :is_a?
20
20
 
21
- if type_type == :class
21
+ if type.instance_of?(Class)
22
22
  define_method :instance_of?, ->(thing) {
23
23
  type == thing
24
24
  }
25
25
  end
26
26
  }
27
27
 
28
- add_stringify_methods!(dry_class, :to_s, type, type_type, instance_methods)
29
- add_stringify_methods!(dry_class, :inspect, type, type_type, instance_methods)
28
+ # These have special implementations, but if the user defines
29
+ # any of them on the object itself, then they'll be replaced with normal
30
+ # mocked methods. YMMV
31
+ add_stringify_methods!(dry_class, :to_s, type, instance_methods)
32
+ add_stringify_methods!(dry_class, :inspect, type, instance_methods)
33
+ define_method_missing_errors!(dry_class, type, instance_methods)
30
34
 
31
35
  define_double_methods!(dry_class, type, instance_methods)
32
36
 
@@ -43,7 +47,7 @@ module Mocktail
43
47
  singleton: false,
44
48
  double: self,
45
49
  original_type: type,
46
- dry_type: self.class,
50
+ dry_type: dry_class,
47
51
  method: method,
48
52
  original_method: type.instance_method(method),
49
53
  args: args,
@@ -54,7 +58,7 @@ module Mocktail
54
58
  end
55
59
  end
56
60
 
57
- def add_stringify_methods!(dry_class, method_name, type, type_type, instance_methods)
61
+ def add_stringify_methods!(dry_class, method_name, type, instance_methods)
58
62
  dry_class.define_singleton_method method_name, -> {
59
63
  if (id_matches = super().match(/:([0-9a-fx]+)>$/))
60
64
  "#<Class #{"including module " if type.instance_of?(Module)}for mocktail of #{type.name}:#{id_matches[1]}>"
@@ -74,20 +78,41 @@ module Mocktail
74
78
  end
75
79
  end
76
80
 
77
- def type_of(type)
78
- if type.is_a?(Class)
79
- :class
80
- elsif type.is_a?(Module)
81
- :module
82
- end
81
+ def define_method_missing_errors!(dry_class, type, instance_methods)
82
+ return if instance_methods.include?(:method_missing)
83
+
84
+ raises_neato_no_method_error = @raises_neato_no_method_error
85
+ dry_class.define_method :method_missing, ->(name, *args, **kwargs, &block) {
86
+ raises_neato_no_method_error.call(
87
+ Call.new(
88
+ singleton: false,
89
+ double: self,
90
+ original_type: type,
91
+ dry_type: self.class,
92
+ method: name,
93
+ original_method: nil,
94
+ args: args,
95
+ kwargs: kwargs,
96
+ block: block
97
+ )
98
+ )
99
+ }
83
100
  end
84
101
 
85
102
  def instance_methods_on(type)
86
- type.instance_methods.reject { |m|
87
- ignored_ancestors.include?(type.instance_method(m).owner)
103
+ methods = type.instance_methods + [
104
+ (:respond_to_missing? if type.private_method_defined?(:respond_to_missing?))
105
+ ].compact
106
+
107
+ methods.reject { |m|
108
+ ignore?(type, m)
88
109
  }
89
110
  end
90
111
 
112
+ def ignore?(type, method_name)
113
+ ignored_ancestors.include?(type.instance_method(method_name).owner)
114
+ end
115
+
91
116
  def ignored_ancestors
92
117
  Object.ancestors
93
118
  end
@@ -0,0 +1,79 @@
1
+ require_relative "share/stringifies_call"
2
+ require_relative "share/creates_identifier"
3
+
4
+ module Mocktail
5
+ class RaisesNeatoNoMethodError
6
+ def initialize
7
+ @stringifies_call = StringifiesCall.new
8
+ @creates_identifier = CreatesIdentifier.new
9
+ end
10
+
11
+ def call(call)
12
+ raise NoMethodError.new <<~MSG
13
+ No method `#{call.original_type.name}##{call.method}' exists for call:
14
+
15
+ #{@stringifies_call.stringify(call, anonymous_blocks: true, always_parens: true)}
16
+
17
+ Need to define the method? Here's a sample definition:
18
+
19
+ def #{call.method}#{params(call)}
20
+ end
21
+ #{corrections(call)}
22
+ MSG
23
+ end
24
+
25
+ private
26
+
27
+ def params(call)
28
+ return if (params_lists = [
29
+ params_list(call.args),
30
+ kwparams_list(call.kwargs),
31
+ block_param(call.block)
32
+ ].compact).empty?
33
+
34
+ "(#{params_lists.join(", ")})"
35
+ end
36
+
37
+ def params_list(args)
38
+ return if args.empty?
39
+
40
+ count_repeats(args.map { |arg|
41
+ @creates_identifier.create(arg, default: "arg")
42
+ }).join(", ")
43
+ end
44
+
45
+ def kwparams_list(kwargs)
46
+ return if kwargs.empty?
47
+
48
+ kwargs.keys.map { |key| "#{key}:" }.join(", ")
49
+ end
50
+
51
+ def block_param(block)
52
+ return if block.nil?
53
+
54
+ "&blk"
55
+ end
56
+
57
+ def count_repeats(identifiers)
58
+ identifiers.map.with_index { |id, i|
59
+ if (preceding_matches = identifiers[0...i].count { |other_id| id == other_id }) > 0
60
+ "#{id}#{preceding_matches + 1}"
61
+ else
62
+ id
63
+ end
64
+ }
65
+ end
66
+
67
+ def corrections(call)
68
+ return if (corrections = DidYouMean::SpellChecker.new(dictionary: call.original_type.instance_methods).correct(call.method)).empty?
69
+
70
+ <<~MSG
71
+
72
+ There #{corrections.size == 1 ? "is" : "are"} also #{corrections.size} similar method#{"s" if corrections.size != 1} on #{call.original_type.name}.
73
+
74
+ Did you mean?
75
+ #{corrections.map { |c| " #{c}" }.join("\n")}
76
+ MSG
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ module Mocktail
2
+ class CreatesIdentifier
3
+ def create(s, default: "identifier", max_length: 24)
4
+ id = s.to_s.downcase.gsub(/[^\w\s]/, "").gsub(/^\d+/, "")[0...max_length].strip.gsub(/\s+/, "_")
5
+
6
+ if id.empty?
7
+ default
8
+ else
9
+ id
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,18 +1,22 @@
1
1
  module Mocktail
2
2
  class StringifiesCall
3
- def stringify(call)
4
- "#{call.method}#{args_to_s(call)}#{blockify(call.block)}"
3
+ def stringify(call, anonymous_blocks: false, always_parens: false)
4
+ "#{call.method}#{args_to_s(call, parens: always_parens)}#{blockify(call.block, anonymous: anonymous_blocks)}"
5
5
  end
6
6
 
7
7
  private
8
8
 
9
- def args_to_s(call)
10
- unless (args_lists = [
9
+ def args_to_s(call, parens: true)
10
+ args_lists = [
11
11
  argify(call.args),
12
12
  kwargify(call.kwargs),
13
13
  lambdafy(call.block)
14
- ].compact).empty?
14
+ ].compact
15
+
16
+ if !args_lists.empty?
15
17
  "(#{args_lists.join(", ")})"
18
+ elsif parens
19
+ "()"
16
20
  end
17
21
  end
18
22
 
@@ -31,9 +35,14 @@ module Mocktail
31
35
  "&lambda[#{source_locationify(block)}]"
32
36
  end
33
37
 
34
- def blockify(block)
38
+ def blockify(block, anonymous:)
35
39
  return unless block && !block.lambda?
36
- " { Proc at #{source_locationify(block)} }"
40
+
41
+ if anonymous
42
+ " {…}"
43
+ else
44
+ " { Proc at #{source_locationify(block)} }"
45
+ end
37
46
  end
38
47
 
39
48
  def source_locationify(block)
@@ -0,0 +1,15 @@
1
+ module Mocktail
2
+ class CleansBacktrace
3
+ BASE_PATH = (Pathname.new(__FILE__) + "../../..").to_s
4
+
5
+ def clean(error)
6
+ raise error
7
+ rescue => e
8
+ e.tap do |e|
9
+ e.set_backtrace(e.backtrace.drop_while { |frame|
10
+ frame.start_with?(BASE_PATH)
11
+ })
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Mocktail
2
+ class ReconcilesArgsWithParams
3
+ def reconcile(signature)
4
+ args_match?(signature.positional_params, signature.positional_args) &&
5
+ kwargs_match?(signature.keyword_params, signature.keyword_args)
6
+ end
7
+
8
+ private
9
+
10
+ def args_match?(arg_params, args)
11
+ args.size >= arg_params.required.size &&
12
+ (arg_params.rest? || args.size <= arg_params.allowed.size)
13
+ end
14
+
15
+ def kwargs_match?(kwarg_params, kwargs)
16
+ kwarg_params.required.all? { |name| kwargs.key?(name) } &&
17
+ (kwarg_params.rest? || kwargs.keys.all? { |name| kwarg_params.allowed.include?(name) })
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module Mocktail
2
+ class RecreatesMessage
3
+ def recreate(signature)
4
+ req_args = signature.positional_params.required.size
5
+ allowed_args = signature.positional_params.allowed.size
6
+ rest_args = signature.positional_params.rest?
7
+ req_kwargs = signature.keyword_params.required
8
+
9
+ if signature.positional_args.size < req_args || (!rest_args && signature.positional_args.size > allowed_args)
10
+ expected_desc = if rest_args
11
+ "#{req_args}+"
12
+ elsif allowed_args != req_args
13
+ "#{req_args}..#{allowed_args}"
14
+ else
15
+ req_args.to_s
16
+ end
17
+
18
+ "wrong number of arguments (given #{signature.positional_args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.join(", ")}" unless req_kwargs.empty?})"
19
+
20
+ elsif !(missing_kwargs = req_kwargs.reject { |name| signature.keyword_args.key?(name) }).empty?
21
+ "missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |name| name.inspect }.join(", ")}"
22
+ elsif !(unknown_kwargs = signature.keyword_args.keys.reject { |name| signature.keyword_params.all.include?(name) }).empty?
23
+ "unknown keyword#{"s" if unknown_kwargs.size > 1}: #{unknown_kwargs.map { |name| name.inspect }.join(", ")}"
24
+ else
25
+ "unknown cause (this is probably a bug in Mocktail)"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Mocktail
2
+ class TransformsParams
3
+ def transform(dry_call)
4
+ params = dry_call.original_method.parameters
5
+
6
+ Signature.new(
7
+ positional_params: Params.new(
8
+ all: params.select { |type, _|
9
+ [:req, :opt, :rest].include?(type)
10
+ }.map { |_, name| name },
11
+ required: params.select { |t, _| t == :req }.map { |_, n| n },
12
+ optional: params.select { |t, _| t == :opt }.map { |_, n| n },
13
+ rest: params.find { |type, _| type == :rest } & [1]
14
+ ),
15
+ positional_args: dry_call.args,
16
+
17
+ keyword_params: Params.new(
18
+ all: params.select { |type, _|
19
+ [:keyreq, :key, :keyrest].include?(type)
20
+ }.map { |_, name| name },
21
+ required: params.select { |t, _| t == :keyreq }.map { |_, n| n },
22
+ optional: params.select { |t, _| t == :key }.map { |_, n| n },
23
+ rest: params.find { |type, _| type == :keyrest } & [1]
24
+ ),
25
+ keyword_args: dry_call.kwargs,
26
+
27
+ block_param: params.find { |type, _| type == :block } & [1],
28
+ block_arg: dry_call.block
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "simulates_argument_error/transforms_params"
2
+ require_relative "simulates_argument_error/reconciles_args_with_params"
3
+ require_relative "simulates_argument_error/recreates_message"
4
+ require_relative "simulates_argument_error/cleans_backtrace"
5
+ require_relative "share/stringifies_call"
6
+
7
+ module Mocktail
8
+ class SimulatesArgumentError
9
+ def initialize
10
+ @transforms_params = TransformsParams.new
11
+ @reconciles_args_with_params = ReconcilesArgsWithParams.new
12
+ @recreates_message = RecreatesMessage.new
13
+ @cleans_backtrace = CleansBacktrace.new
14
+ @stringifies_call = StringifiesCall.new
15
+ end
16
+
17
+ def simulate(dry_call)
18
+ signature = @transforms_params.transform(dry_call)
19
+
20
+ unless @reconciles_args_with_params.reconcile(signature)
21
+ @cleans_backtrace.clean(
22
+ ArgumentError.new([
23
+ @recreates_message.recreate(signature),
24
+ "[Mocktail call: `#{@stringifies_call.stringify(dry_call)}']"
25
+ ].join(" "))
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ module Mocktail
2
+ class Signature < Struct.new(
3
+ :positional_params,
4
+ :positional_args,
5
+ :keyword_params,
6
+ :keyword_args,
7
+ :block_param,
8
+ :block_arg,
9
+ keyword_init: true
10
+ )
11
+ end
12
+
13
+ class Params < Struct.new(
14
+ :all,
15
+ :required,
16
+ :optional,
17
+ :rest,
18
+ keyword_init: true
19
+ )
20
+
21
+ def initialize(**params)
22
+ super
23
+ self.all ||= []
24
+ self.required ||= []
25
+ self.optional ||= []
26
+ end
27
+
28
+ def allowed
29
+ required + optional
30
+ end
31
+
32
+ def rest?
33
+ !!rest
34
+ end
35
+ end
36
+ end
@@ -3,6 +3,7 @@ require_relative "value/call"
3
3
  require_relative "value/demo_config"
4
4
  require_relative "value/double"
5
5
  require_relative "value/matcher_registry"
6
+ require_relative "value/signature"
6
7
  require_relative "value/stubbing"
7
8
  require_relative "value/top_shelf"
8
9
  require_relative "value/type_replacement"
@@ -1,5 +1,5 @@
1
1
  require_relative "raises_verification_error/gathers_calls_of_method"
2
- require_relative "raises_verification_error/stringifies_call"
2
+ require_relative "../share/stringifies_call"
3
3
 
4
4
  module Mocktail
5
5
  class RaisesVerificationError
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
data/lib/mocktail.rb CHANGED
@@ -6,11 +6,13 @@ require_relative "mocktail/imitates_type"
6
6
  require_relative "mocktail/initializes_mocktail"
7
7
  require_relative "mocktail/matcher_presentation"
8
8
  require_relative "mocktail/matchers"
9
+ require_relative "mocktail/raises_neato_no_method_error"
9
10
  require_relative "mocktail/registers_matcher"
10
11
  require_relative "mocktail/registers_stubbing"
11
12
  require_relative "mocktail/replaces_next"
12
13
  require_relative "mocktail/replaces_type"
13
14
  require_relative "mocktail/resets_state"
15
+ require_relative "mocktail/simulates_argument_error"
14
16
  require_relative "mocktail/value"
15
17
  require_relative "mocktail/verifies_call"
16
18
  require_relative "mocktail/version"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mocktail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-02 00:00:00.000000000 Z
11
+ date: 2021-10-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -53,6 +53,7 @@ files:
53
53
  - lib/mocktail/matchers/not.rb
54
54
  - lib/mocktail/matchers/numeric.rb
55
55
  - lib/mocktail/matchers/that.rb
56
+ - lib/mocktail/raises_neato_no_method_error.rb
56
57
  - lib/mocktail/records_demonstration.rb
57
58
  - lib/mocktail/registers_matcher.rb
58
59
  - lib/mocktail/registers_stubbing.rb
@@ -61,14 +62,21 @@ files:
61
62
  - lib/mocktail/replaces_type/redefines_new.rb
62
63
  - lib/mocktail/replaces_type/redefines_singleton_methods.rb
63
64
  - lib/mocktail/resets_state.rb
65
+ - lib/mocktail/share/creates_identifier.rb
64
66
  - lib/mocktail/share/determines_matching_calls.rb
65
- - lib/mocktail/share/simulates_argument_error.rb
67
+ - lib/mocktail/share/stringifies_call.rb
68
+ - lib/mocktail/simulates_argument_error.rb
69
+ - lib/mocktail/simulates_argument_error/cleans_backtrace.rb
70
+ - lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb
71
+ - lib/mocktail/simulates_argument_error/recreates_message.rb
72
+ - lib/mocktail/simulates_argument_error/transforms_params.rb
66
73
  - lib/mocktail/value.rb
67
74
  - lib/mocktail/value/cabinet.rb
68
75
  - lib/mocktail/value/call.rb
69
76
  - lib/mocktail/value/demo_config.rb
70
77
  - lib/mocktail/value/double.rb
71
78
  - lib/mocktail/value/matcher_registry.rb
79
+ - lib/mocktail/value/signature.rb
72
80
  - lib/mocktail/value/stubbing.rb
73
81
  - lib/mocktail/value/top_shelf.rb
74
82
  - lib/mocktail/value/type_replacement.rb
@@ -76,7 +84,6 @@ files:
76
84
  - lib/mocktail/verifies_call/finds_verifiable_calls.rb
77
85
  - lib/mocktail/verifies_call/raises_verification_error.rb
78
86
  - lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb
79
- - lib/mocktail/verifies_call/raises_verification_error/stringifies_call.rb
80
87
  - lib/mocktail/version.rb
81
88
  - mocktail.gemspec
82
89
  homepage: https://github.com/testdouble/mocktail
@@ -1,28 +0,0 @@
1
- module Mocktail
2
- class SimulatesArgumentError
3
- def simulate(arg_params, args, kwarg_params, kwargs)
4
- req_args = arg_params.count { |type, _| type == :req }
5
- opt_args = arg_params.count { |type, _| type == :opt }
6
- rest_args = arg_params.any? { |type, _| type == :rest }
7
- req_kwargs = kwarg_params.select { |type, _| type == :keyreq }
8
-
9
- allowed_args = req_args + opt_args
10
- msg = if args.size < req_args || (!rest_args && args.size > allowed_args)
11
- expected_desc = if rest_args
12
- "#{req_args}+"
13
- elsif allowed_args != req_args
14
- "#{req_args}..#{allowed_args}"
15
- else
16
- req_args.to_s
17
- end
18
-
19
- "wrong number of arguments (given #{args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.map { |_, name| name }.join(", ")}" unless req_kwargs.empty?})"
20
-
21
- elsif !(missing_kwargs = req_kwargs.reject { |_, name| kwargs.key?(name) }).empty?
22
- "missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |_, name| name.inspect }.join(", ")}"
23
- end
24
-
25
- ArgumentError.new(msg)
26
- end
27
- end
28
- end