leto 1.0.0 → 2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 539f52438c18bb93115734b5c3c9dd5f8678886d392357c4fdca658b43d95357
4
- data.tar.gz: bbe092a276d3241d39857cb81e8fb748108b15e67cbc0e421b7390545b8a066a
3
+ metadata.gz: 0ed0d190b234b4976a219ba5b50a3e9cb4f394be7b1a27aa2f1d7bdfe93887b0
4
+ data.tar.gz: eafafd24510514cf7ba4e59cdf1a967c7464d175b8ceed1f3cafa2e78fffcc1e
5
5
  SHA512:
6
- metadata.gz: 9a0e610d258c35cf044c0118ccc10dff918edc8495df7448c058dbe6ad3201e540952fa67ac14e065229e1dd961788936e1343d30254743f47319609b5b4e169
7
- data.tar.gz: c7e0fa4e9be9c2c0da092a7dbb88c78b9b11a12d8fb4f9d34e2ed97794ab2f26282c1fe31b04db5c70039198e675d2770ab0a7ae52edcb28049e7568c6c7bc56
6
+ metadata.gz: 79b0be7884c86be4a14977e5aaae6147da85b857f632013bd82f9fe95addf28a4014ea7adeba1c75dfc63b9988ab7426c622cb79bc2c2886dca04be246ee2db5
7
+ data.tar.gz: 961a19be85b69aac0763828648c09100af4a9f84eff07a45e2b9c0c247c84e5b7ae9647a9bd4a7de791eb7cd0f0342b91f82d2cd50a85286425e4a8f915bd2fe
data/.rubocop.yml CHANGED
@@ -21,8 +21,14 @@ Lint/AmbiguousBlockAssociation:
21
21
  Lint/AmbiguousOperatorPrecedence:
22
22
  Enabled: false
23
23
 
24
+ Style/DocumentDynamicEvalDefinition:
25
+ Enabled: false
26
+
24
27
  Style/FetchEnvVar:
25
28
  Enabled: false
26
29
 
27
30
  Style/FrozenStringLiteralComment:
28
31
  Enabled: false
32
+
33
+ Style/SymbolProc:
34
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.1.0] - 2023-05-20
4
+
5
+ ### Added
6
+
7
+ - `::call`, `::trace` and `::deep_dup` support for Data feature of Ruby 3.2
8
+
9
+ ## [2.0.0] - 2023-02-27
10
+
11
+ ### Changed
12
+
13
+ - removed support for getting paths by calling `::call` with a two-arg block
14
+
15
+ ### Added
16
+
17
+ - `::deep_dup`
18
+ - `::trace`
19
+ - support for older Rubies
20
+
3
21
  ## [1.0.0] - 2023-02-24
4
22
 
5
23
  - Initial release
data/Gemfile CHANGED
@@ -6,9 +6,10 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
-
10
9
  gem "rspec", "~> 3.0"
11
10
 
12
- gem "rubocop", "~> 1.21"
13
-
14
- gem "relaxed-rubocop"
11
+ if RUBY_VERSION.to_f >= 3.0
12
+ gem "relaxed-rubocop"
13
+ gem "rubocop", "~> 1.21"
14
+ gem "simplecov-cobertura"
15
+ end
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Leto
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/leto.svg)](http://badge.fury.io/rb/leto)
4
- [![Build Status](https://github.com/jaynetics/leto/workflows/tests/badge.svg)](https://github.com/jaynetics/leto/actions)
4
+ [![Build Status](https://github.com/jaynetics/leto/actions/workflows/tests.yml/badge.svg)](https://github.com/jaynetics/leto/actions)
5
+ [![Coverage](https://codecov.io/gh/jaynetics/leto/branch/main/graph/badge.svg?token=0993K9I8VC)](https://codecov.io/gh/jaynetics/leto)
5
6
 
6
7
  A generic object traverser for Ruby (named after the Greek [childbearing goddess Leto](https://www.theoi.com/Titan/TitanisLeto.html)).
7
8
 
@@ -12,6 +13,7 @@ Takes an object and recursively yields:
12
13
  - Hash keys and values
13
14
  - Enumerable members
14
15
  - Struct members
16
+ - [Data](https://docs.ruby-lang.org/en/3.2/Data.html) members
15
17
  - Range begins and ends
16
18
 
17
19
  This makes stuff like deep-freezing fairly easy to implement:
@@ -50,8 +52,8 @@ Leto.call(object) { |el| p el }
50
52
  Leto.call(object).to_a
51
53
  # => [[{:a=>["b", ["c".."d"]]}], {:a=>["b", ["c".."d"]]}, :a, ...]
52
54
 
53
- # all (sub-)objects have a path:
54
- Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
55
+ # Leto::trace behaves like ::call, but also yields each (sub-)object's path:
56
+ Leto.trace(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
55
57
  # prints:
56
58
  #
57
59
  # [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
@@ -64,7 +66,7 @@ Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
64
66
  # "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
65
67
 
66
68
  # paths can be looked up with Leto::Path#resolve or Leto::dig
67
- path = Leto.call(object).map { |_el, path| path }.last # => #<Leto::Path...>
69
+ path = Leto.trace(object).map { |_el, path| path }.last # => #<Leto::Path...>
68
70
  path.resolve # => "d"
69
71
  Leto.dig(object, path) # => "d"
70
72
  Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
@@ -72,17 +74,19 @@ Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
72
74
 
73
75
  ### Included utility methods
74
76
 
75
- - `Leto.deep_freeze(obj)`
77
+ - [`Leto.deep_freeze(obj)`](https://github.com/search?q=deep_freeze+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
76
78
  - similar to the version above, but avoids freezing Modules and unfreezables
77
- - `Leto.deep_print(obj)`
79
+ - [`Leto.deep_print(obj)`](https://github.com/search?q=deep_print+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
78
80
  - for debugging - prints more information than `pretty_print` does by default
79
- - `Leto.deep_eql?(obj1, obj2)`
81
+ - [`Leto.deep_eql?(obj1, obj2)`](https://github.com/search?q=deep_eql+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
80
82
  - stricter version of `#eql?` that takes all ivars into consideration
81
- - `Leto.shared_mutable_state?(obj1, obj2)`
83
+ - [`Leto.deep_dup(obj)`](https://github.com/search?q=deep_dup+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
84
+ - more thorough than [`active_support`](https://www.rubydoc.info/search/gems/activesupport?q=deep_dup) or [`deep_dup`](https://github.com/ollie/deep_dup) gems, e.g. dups ivars
85
+ - [`Leto.shared_mutable_state?(obj1, obj2)`](https://github.com/search?q=shared_mutable_state+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
82
86
  - useful for debugging or verifying that a `#dup` implementation is sane
83
- - `Leto.shared_mutables(obj1, obj2)`
87
+ - [`Leto.shared_mutables(obj1, obj2)`](https://github.com/search?q=shared_mutables+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
84
88
  - useful for debugging or verifying that a `#dup` implementation is sane
85
- - `Leto.shared_objects(obj1, obj2)`
89
+ - [`Leto.shared_objects(obj1, obj2)`](https://github.com/search?q=shared_objects+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
86
90
  - returns all objects shared by `obj1` and `obj2`, whether mutable or not
87
91
 
88
92
  ## Benchmarks
data/Rakefile CHANGED
@@ -5,8 +5,10 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
8
+ if RUBY_VERSION.to_f >= 3.0
9
+ require "rubocop/rake_task"
10
+ RuboCop::RakeTask.new
11
+ task default: %i[spec rubocop]
12
+ else
13
+ task default: %i[spec]
14
+ end
data/leto.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Generic object traverser"
12
12
  spec.homepage = "https://github.com/jaynetics/leto"
13
13
  spec.license = "MIT"
14
- spec.required_ruby_version = ">= 2.6.0"
14
+ spec.required_ruby_version = ">= 2.3.0"
15
15
 
16
16
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
17
17
  spec.metadata["homepage_uri"] = spec.homepage
data/lib/leto/call.rb CHANGED
@@ -1,64 +1,92 @@
1
1
  module Leto
2
- LEAKY_PROCESS_TMS = RUBY_VERSION.to_f <= 2.7
2
+ def self.call(obj, max_depth: nil, path: nil, &block)
3
+ block_given? or return enum_for(__method__, obj, max_depth: max_depth, path: path)
3
4
 
4
- def self.call(obj, &block)
5
- return enum_for(__method__, obj) unless block_given?
6
-
7
- seen = {}.tap(&:compare_by_identity)
8
- seen[Process::Tms] = true if LEAKY_PROCESS_TMS
9
- path = block.arity == 2 ? Path.new(start: obj) : nil
10
- traverse(obj, path, seen, block)
5
+ traverse(obj, path, 0, max_depth, build_seen_hash, block)
11
6
  obj
12
7
  end
13
8
 
14
- def self.traverse(obj, path, seen, block)
15
- return if seen[obj]
9
+ def self.trace(obj, max_depth: nil, path: nil, &block)
10
+ block_given? or return enum_for(__method__, obj, max_depth: max_depth, path: path)
16
11
 
17
- seen[obj] = true
12
+ call(obj, max_depth: max_depth, path: path || Path.new(start: obj), &block)
13
+ end
18
14
 
19
- path ? block.call(obj, path) : block.call(obj)
15
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
20
16
 
21
- obj.instance_variables.each do |ivar_name|
22
- traverse(
23
- obj.instance_variable_get(ivar_name),
24
- path&.+([[:instance_variable_get, ivar_name]]),
25
- seen, block
26
- )
27
- end
17
+ private
28
18
 
29
- case obj
30
- when Hash
31
- obj.keys.each_with_index do |k, i|
32
- traverse(k, path&.+([[:keys], [:[], i]]), seen, block)
33
- traverse(obj[k], path&.+([[:[], k]]), seen, block)
34
- end
35
- when Module
36
- obj.class_variables.each do |cvar_name|
37
- traverse(
38
- obj.class_variable_get(cvar_name),
39
- path&.+([[:class_variable_get, cvar_name]]),
40
- seen, block
41
- )
42
- end
43
- obj.constants.each do |const_name|
19
+ def traverse(obj, path, depth, max_depth, seen, block)
20
+ return if seen[obj] || max_depth&.<(depth)
21
+
22
+ seen[obj] = true
23
+ depth += 1
24
+
25
+ path ? block.call(obj, path) : block.call(obj)
26
+
27
+ obj.instance_variables.each do |ivar_name|
44
28
  traverse(
45
- obj.const_get(const_name),
46
- path&.+([[:const_get, const_name]]),
47
- seen, block
29
+ obj.instance_variable_get(ivar_name),
30
+ path&.+([[:instance_variable_get, ivar_name]]),
31
+ depth, max_depth, seen, block
48
32
  )
49
33
  end
50
- when Range
51
- traverse(obj.begin, path&.+([[:begin]]), seen, block)
52
- traverse(obj.end, path&.+([[:end]]), seen, block)
53
- when Struct
54
- obj.members.each do |member|
55
- traverse(obj[member], path&.+([[:[], member]]), seen, block)
56
- end
57
- when Enumerable
58
- obj.each_with_index do |el, idx|
59
- traverse(el, path&.+([[:[], idx]]), seen, block)
34
+
35
+ case obj
36
+ when Hash
37
+ obj.keys.each_with_index do |k, i|
38
+ traverse(k, path&.+([[:keys], [:[], i]]), depth, max_depth, seen, block)
39
+ traverse(obj[k], path&.+([[:[], k]]), depth, max_depth, seen, block)
40
+ end
41
+ when Module
42
+ obj.class_variables.each do |cvar_name|
43
+ traverse(
44
+ obj.class_variable_get(cvar_name),
45
+ path&.+([[:class_variable_get, cvar_name]]),
46
+ depth, max_depth, seen, block
47
+ )
48
+ end
49
+ obj.constants.each do |const_name|
50
+ traverse(
51
+ obj.const_get(const_name),
52
+ path&.+([[:const_get, const_name]]),
53
+ depth, max_depth, seen, block
54
+ )
55
+ end
56
+ when Range
57
+ traverse(obj.begin, path&.+([[:begin]]), depth, max_depth, seen, block)
58
+ traverse(obj.end, path&.+([[:end]]), depth, max_depth, seen, block)
59
+ when Struct
60
+ obj.members.each do |member|
61
+ traverse(obj[member], path&.+([[:[], member]]), depth, max_depth, seen, block)
62
+ end
63
+ when Enumerable
64
+ obj.each_with_index do |el, idx|
65
+ traverse(el, path&.+([[:[], idx]]), depth, max_depth, seen, block)
66
+ end
67
+ #{
68
+ defined?(Data) && Data.respond_to?(:define) && <<-DATA_FEATURE_RUBY
69
+ when Data
70
+ obj.members.each do |member|
71
+ traverse(
72
+ obj.send(member),
73
+ path&.+([[:send, member]]),
74
+ depth, max_depth, seen, block
75
+ )
76
+ end
77
+ DATA_FEATURE_RUBY
78
+ }
60
79
  end
61
80
  end
62
- end
63
- private_class_method :traverse
81
+
82
+ # ignore leaky constants in old rubies
83
+ def build_seen_hash
84
+ hash = {}
85
+ hash.compare_by_identity
86
+ #{'hash[::Etc::Group] = true' if defined?(::Etc::Group)}
87
+ #{'hash[::Etc::Passwd] = true' if defined?(::Etc::Passwd)}
88
+ #{'hash[::Process::Tms] = true' if defined?(::Process::Tms)}
89
+ hash
90
+ end
91
+ RUBY
64
92
  end
data/lib/leto/path.rb CHANGED
@@ -14,6 +14,11 @@ module Leto
14
14
  steps.each(&block)
15
15
  end
16
16
 
17
+ def size
18
+ steps.size
19
+ end
20
+ alias count size
21
+
17
22
  def +(other)
18
23
  self.class.new(start: start, steps: steps + other.to_a)
19
24
  end
@@ -21,8 +26,6 @@ module Leto
21
26
  def resolve
22
27
  steps.inject(start) do |obj, (method, *args)|
23
28
  obj&.send(method, *args) or break obj
24
- rescue StandardError => e
25
- warn "#{__method__}: #{e.class} #{e.message}"
26
29
  end
27
30
  end
28
31
 
data/lib/leto/utils.rb CHANGED
@@ -8,7 +8,7 @@ module Leto
8
8
  end
9
9
 
10
10
  def self.deep_print(obj, print_method: :inspect, indent: 4, show_path: true)
11
- call(obj) do |el, path|
11
+ trace(obj) do |el, path|
12
12
  puts "#{' ' * path.count * indent}#{el.send(print_method)}" \
13
13
  "#{" @ #{path.inspect}" if show_path}" \
14
14
  [0..78]
@@ -20,8 +20,31 @@ module Leto
20
20
  call(obj1).to_a == call(obj2).to_a
21
21
  end
22
22
 
23
+ def self.deep_dup(obj, include_modules: false)
24
+ return obj if IMMUTABLE_CLASSES.include?(obj.class) || !duplicable?(obj) ||
25
+ (!include_modules && obj.is_a?(Module))
26
+
27
+ copy = obj.dup
28
+
29
+ trace(obj, max_depth: 1).each do |el, path|
30
+ method, *args = path.steps[0]
31
+ case method
32
+ when :instance_variable_get
33
+ copy.instance_variable_set(*args, deep_dup(el, include_modules: include_modules))
34
+ when :[]
35
+ copy[*args] = deep_dup(el, include_modules: include_modules)
36
+ when :send # Data
37
+ copy = copy.with(args[0] => deep_dup(el, include_modules: include_modules))
38
+ when :begin
39
+ return Range.new(deep_dup(obj.begin), deep_dup(obj.end), obj.exclude_end?)
40
+ end
41
+ end
42
+
43
+ copy
44
+ end
45
+
23
46
  def self.shared_mutable_state?(obj1, obj2)
24
- shared_mutables(obj1, obj2).any?
47
+ each_shared_object(obj1, obj2, filter: method(:mutable?)).any?
25
48
  end
26
49
 
27
50
  # returns [[shared_object, path1, path2], ...], e.g.:
@@ -33,17 +56,22 @@ module Leto
33
56
  # ["bar", [[:[], 1]], [[:[], 0]]]
34
57
  # ]
35
58
  def self.shared_mutables(obj1, obj2)
36
- shared_objects(obj1, obj2, filter: method(:mutable?))
59
+ each_shared_object(obj1, obj2, filter: method(:mutable?)).to_a
37
60
  end
38
61
 
39
62
  def self.shared_objects(obj1, obj2, filter: nil)
40
- objects_with_path1 = call(obj1).map { |el, path| [el, path] }
41
- objects_with_path2 = call(obj2).map { |el, path| [el, path] }
42
- objects_with_path1.each.with_object([]) do |(el1, path1), acc|
63
+ each_shared_object(obj1, obj2, filter: filter).to_a
64
+ end
65
+
66
+ def self.each_shared_object(obj1, obj2, filter: nil)
67
+ block_given? or return enum_for(__method__, obj1, obj2, filter: filter)
68
+
69
+ obj2_els_with_path = trace(obj2).to_a
70
+ trace(obj1).each do |el1, path1|
43
71
  next if filter && !filter.call(el1)
44
72
 
45
- objects_with_path2.reject do |el2, path2|
46
- acc << [el1, path1, path2] if el1.equal?(el2)
73
+ obj2_els_with_path.reject do |el2, path2|
74
+ yield(el1, path1, path2) if el1.equal?(el2)
47
75
  end
48
76
  end
49
77
  end
@@ -56,9 +84,26 @@ module Leto
56
84
  IMMUTABLE_CLASSES = [
57
85
  FalseClass,
58
86
  Float,
59
- Integer,
87
+ if defined?(Integer)
88
+ Integer
89
+ else
90
+ Fixnum # rubocop:disable Lint/UnifiedInteger for Ruby < 2.4
91
+ end,
60
92
  NilClass,
61
93
  Symbol,
62
94
  TrueClass,
63
95
  ].freeze
96
+
97
+ def self.duplicable?(obj)
98
+ !NON_DUPLICABLE_CLASSES.include?(obj.class) && obj.respond_to?(:dup)
99
+ end
100
+ private_class_method :duplicable?
101
+
102
+ require 'singleton'
103
+
104
+ NON_DUPLICABLE_CLASSES = [
105
+ Method,
106
+ Singleton,
107
+ UnboundMethod
108
+ ].freeze
64
109
  end
data/lib/leto/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leto
4
- VERSION = "1.0.0"
4
+ VERSION = "2.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leto
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janosch Müller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-24 00:00:00.000000000 Z
11
+ date: 2023-05-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -46,14 +46,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: 2.6.0
49
+ version: 2.3.0
50
50
  required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  requirements: []
56
- rubygems_version: 3.4.1
56
+ rubygems_version: 3.4.13
57
57
  signing_key:
58
58
  specification_version: 4
59
59
  summary: Generic object traverser