leto 1.0.0 → 2.0.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: 2926017bd4fc2ec078b89e6d39157ed7e79e48693e82628665c3435c5c39b4f4
4
+ data.tar.gz: f8736c1e1e0a5e7a7b6c93d8c48c7393b6d554906245c5fcc71d05aa3d200789
5
5
  SHA512:
6
- metadata.gz: 9a0e610d258c35cf044c0118ccc10dff918edc8495df7448c058dbe6ad3201e540952fa67ac14e065229e1dd961788936e1343d30254743f47319609b5b4e169
7
- data.tar.gz: c7e0fa4e9be9c2c0da092a7dbb88c78b9b11a12d8fb4f9d34e2ed97794ab2f26282c1fe31b04db5c70039198e675d2770ab0a7ae52edcb28049e7568c6c7bc56
6
+ metadata.gz: 1b13efbac9e6e200a43067a57b219a5128d69eea06a4a41a8fe2bd88d49932d0a10a60b38807443133300b86aaa9d3d75045e0bc340057d6d16564647b68c8ba
7
+ data.tar.gz: 476318c70ed5d9a1df6ab4abc67a9d3dad35a003585d34ec4bdc743f6990823db6dc5c3430a244ed12bc3c454cbb5a8e1937dc2ada58568c8a38ce1e018cbbb9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.0.0] - 2023-02-27
4
+
5
+ ### Changed
6
+
7
+ - removed support for gettings paths by calling `::call` with a two-arg block
8
+
9
+ ### Added
10
+
11
+ - `::deep_dup`
12
+ - `::trace`
13
+ - support for older Rubies
14
+
3
15
  ## [1.0.0] - 2023-02-24
4
16
 
5
17
  - Initial release
data/Gemfile CHANGED
@@ -9,6 +9,8 @@ gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
11
 
12
- gem "rubocop", "~> 1.21"
12
+ if RUBY_VERSION.to_f >= 3.0
13
+ gem "rubocop", "~> 1.21"
13
14
 
14
- gem "relaxed-rubocop"
15
+ gem "relaxed-rubocop"
16
+ end
data/README.md CHANGED
@@ -50,8 +50,8 @@ Leto.call(object) { |el| p el }
50
50
  Leto.call(object).to_a
51
51
  # => [[{:a=>["b", ["c".."d"]]}], {:a=>["b", ["c".."d"]]}, :a, ...]
52
52
 
53
- # all (sub-)objects have a path:
54
- Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
53
+ # Leto::trace behaves like ::call, but also yields each (sub-)object's path:
54
+ Leto.trace(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
55
55
  # prints:
56
56
  #
57
57
  # [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
@@ -64,7 +64,7 @@ Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
64
64
  # "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
65
65
 
66
66
  # paths can be looked up with Leto::Path#resolve or Leto::dig
67
- path = Leto.call(object).map { |_el, path| path }.last # => #<Leto::Path...>
67
+ path = Leto.trace(object).map { |_el, path| path }.last # => #<Leto::Path...>
68
68
  path.resolve # => "d"
69
69
  Leto.dig(object, path) # => "d"
70
70
  Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
@@ -72,17 +72,19 @@ Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
72
72
 
73
73
  ### Included utility methods
74
74
 
75
- - `Leto.deep_freeze(obj)`
75
+ - [`Leto.deep_freeze(obj)`](https://github.com/search?q=deep_freeze+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
76
76
  - similar to the version above, but avoids freezing Modules and unfreezables
77
- - `Leto.deep_print(obj)`
77
+ - [`Leto.deep_print(obj)`](https://github.com/search?q=deep_print+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
78
78
  - for debugging - prints more information than `pretty_print` does by default
79
- - `Leto.deep_eql?(obj1, obj2)`
79
+ - [`Leto.deep_eql?(obj1, obj2)`](https://github.com/search?q=deep_eql+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
80
80
  - stricter version of `#eql?` that takes all ivars into consideration
81
- - `Leto.shared_mutable_state?(obj1, obj2)`
81
+ - [`Leto.deep_dup(obj)`](https://github.com/search?q=deep_dup+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
82
+ - more thorough than `active_support` or `deep_dup` gems, e.g. dups ivars
83
+ - [`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
84
  - useful for debugging or verifying that a `#dup` implementation is sane
83
- - `Leto.shared_mutables(obj1, obj2)`
85
+ - [`Leto.shared_mutables(obj1, obj2)`](https://github.com/search?q=shared_mutables+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
84
86
  - useful for debugging or verifying that a `#dup` implementation is sane
85
- - `Leto.shared_objects(obj1, obj2)`
87
+ - [`Leto.shared_objects(obj1, obj2)`](https://github.com/search?q=shared_objects+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
86
88
  - returns all objects shared by `obj1` and `obj2`, whether mutable or not
87
89
 
88
90
  ## 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,20 +1,22 @@
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)
11
+
12
+ call(obj, max_depth: max_depth, path: path || Path.new(start: obj), &block)
13
+ end
14
+
15
+ def self.traverse(obj, path, depth, max_depth, seen, block)
16
+ return if seen[obj] || max_depth&.<(depth)
16
17
 
17
18
  seen[obj] = true
19
+ depth += 1
18
20
 
19
21
  path ? block.call(obj, path) : block.call(obj)
20
22
 
@@ -22,43 +24,58 @@ module Leto
22
24
  traverse(
23
25
  obj.instance_variable_get(ivar_name),
24
26
  path&.+([[:instance_variable_get, ivar_name]]),
25
- seen, block
27
+ depth, max_depth, seen, block
26
28
  )
27
29
  end
28
30
 
29
31
  case obj
30
32
  when Hash
31
33
  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
+ traverse(k, path&.+([[:keys], [:[], i]]), depth, max_depth, seen, block)
35
+ traverse(obj[k], path&.+([[:[], k]]), depth, max_depth, seen, block)
34
36
  end
35
37
  when Module
36
38
  obj.class_variables.each do |cvar_name|
37
39
  traverse(
38
40
  obj.class_variable_get(cvar_name),
39
41
  path&.+([[:class_variable_get, cvar_name]]),
40
- seen, block
42
+ depth, max_depth, seen, block
41
43
  )
42
44
  end
43
45
  obj.constants.each do |const_name|
44
46
  traverse(
45
47
  obj.const_get(const_name),
46
48
  path&.+([[:const_get, const_name]]),
47
- seen, block
49
+ depth, max_depth, seen, block
48
50
  )
49
51
  end
50
52
  when Range
51
- traverse(obj.begin, path&.+([[:begin]]), seen, block)
52
- traverse(obj.end, path&.+([[:end]]), seen, block)
53
+ traverse(obj.begin, path&.+([[:begin]]), depth, max_depth, seen, block)
54
+ traverse(obj.end, path&.+([[:end]]), depth, max_depth, seen, block)
53
55
  when Struct
54
56
  obj.members.each do |member|
55
- traverse(obj[member], path&.+([[:[], member]]), seen, block)
57
+ traverse(obj[member], path&.+([[:[], member]]), depth, max_depth, seen, block)
56
58
  end
57
59
  when Enumerable
58
60
  obj.each_with_index do |el, idx|
59
- traverse(el, path&.+([[:[], idx]]), seen, block)
61
+ traverse(el, path&.+([[:[], idx]]), depth, max_depth, seen, block)
60
62
  end
61
63
  end
62
64
  end
63
65
  private_class_method :traverse
66
+
67
+ if RUBY_VERSION.to_f > 2.7
68
+ def self.build_seen_hash
69
+ {}.tap(&:compare_by_identity)
70
+ end
71
+ else
72
+ # ignore leaky constants in old rubies
73
+ def self.build_seen_hash
74
+ hash = {}.tap(&:compare_by_identity)
75
+ hash[::Etc::Group] = true if defined?(::Etc::Group)
76
+ hash[::Etc::Passwd] = true if defined?(::Etc::Passwd)
77
+ hash[::Process::Tms] = true if defined?(::Process::Tms)
78
+ hash
79
+ end
80
+ end
64
81
  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,6 +20,23 @@ 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
+ trace(obj, max_depth: 1).each_with_object(obj.dup) do |(el, path), copy|
28
+ method, *args = path.steps[0]
29
+ case method
30
+ when :instance_variable_get
31
+ copy.instance_variable_set(*args, deep_dup(el, include_modules: include_modules))
32
+ when :[]
33
+ copy[*args] = deep_dup(el, include_modules: include_modules)
34
+ when :begin
35
+ return Range.new(deep_dup(obj.begin), deep_dup(obj.end), obj.exclude_end?)
36
+ end
37
+ end
38
+ end
39
+
23
40
  def self.shared_mutable_state?(obj1, obj2)
24
41
  shared_mutables(obj1, obj2).any?
25
42
  end
@@ -37,12 +54,11 @@ module Leto
37
54
  end
38
55
 
39
56
  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|
57
+ obj2_els_with_path = trace(obj2).to_a
58
+ trace(obj1).each_with_object([]) do |(el1, path1), acc|
43
59
  next if filter && !filter.call(el1)
44
60
 
45
- objects_with_path2.reject do |el2, path2|
61
+ obj2_els_with_path.reject do |el2, path2|
46
62
  acc << [el1, path1, path2] if el1.equal?(el2)
47
63
  end
48
64
  end
@@ -56,9 +72,26 @@ module Leto
56
72
  IMMUTABLE_CLASSES = [
57
73
  FalseClass,
58
74
  Float,
59
- Integer,
75
+ if defined?(Integer)
76
+ Integer
77
+ else
78
+ Fixnum # rubocop:disable Lint/UnifiedInteger for Ruby < 2.4
79
+ end,
60
80
  NilClass,
61
81
  Symbol,
62
82
  TrueClass,
63
83
  ].freeze
84
+
85
+ def self.duplicable?(obj)
86
+ !NON_DUPLICABLE_CLASSES.include?(obj.class) && obj.respond_to?(:dup)
87
+ end
88
+ private_class_method :duplicable?
89
+
90
+ require 'singleton'
91
+
92
+ NON_DUPLICABLE_CLASSES = [
93
+ Method,
94
+ Singleton,
95
+ UnboundMethod
96
+ ].freeze
64
97
  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.0.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.0.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-02-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -46,7 +46,7 @@ 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
  - - ">="