leto 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="