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 +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile +4 -2
- data/README.md +11 -9
- data/Rakefile +7 -5
- data/leto.gemspec +1 -1
- data/lib/leto/call.rb +36 -19
- data/lib/leto/path.rb +5 -2
- data/lib/leto/utils.rb +39 -6
- data/lib/leto/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2926017bd4fc2ec078b89e6d39157ed7e79e48693e82628665c3435c5c39b4f4
|
4
|
+
data.tar.gz: f8736c1e1e0a5e7a7b6c93d8c48c7393b6d554906245c5fcc71d05aa3d200789
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
-
#
|
54
|
-
Leto.
|
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.
|
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.
|
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
|
-
|
9
|
-
|
10
|
-
RuboCop::RakeTask.new
|
11
|
-
|
12
|
-
|
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.
|
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
|
-
|
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
|
-
|
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.
|
15
|
-
return
|
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
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
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
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:
|
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-
|
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.
|
49
|
+
version: 2.3.0
|
50
50
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|