tangle 0.8.2 → 0.11.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.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tangle
4
+ #
5
+ # Protected methods of BaseGraph
6
+ #
7
+ module BaseGraphProtected
8
+ protected
9
+
10
+ def copy_vertices_and_edges(from)
11
+ @vertices = from.instance_variable_get(:@vertices).dup
12
+ @vertices_by_name = from.instance_variable_get(:@vertices_by_name).dup
13
+ @edges = from.instance_variable_get(:@edges).dup
14
+ end
15
+
16
+ def select_vertices!(selected = nil)
17
+ vertices.each do |vertex|
18
+ delete_vertex(vertex) if block_given? && !yield(vertex)
19
+ next if selected.nil?
20
+
21
+ delete_vertex(vertex) unless selected.any? { |vtx| vtx.eql?(vertex) }
22
+ end
23
+ end
24
+
25
+ def insert_vertex(vertex, name = nil)
26
+ @vertices[vertex] = Set[]
27
+ @vertices_by_name[name] = vertex unless name.nil?
28
+ end
29
+
30
+ def delete_vertex(vertex)
31
+ @vertices[vertex].each do |edge|
32
+ delete_edge(edge) if edge.include?(vertex)
33
+ end
34
+ @vertices.delete(vertex)
35
+ @vertices_by_name.delete_if { |_, vtx| vtx.eql?(vertex) }
36
+ end
37
+
38
+ # Insert a prepared edge into the graph
39
+ #
40
+ def insert_edge(edge)
41
+ @edges << edge
42
+ edge.each_vertex { |vertex| @vertices.fetch(vertex) << edge }
43
+ end
44
+
45
+ def delete_edge(edge)
46
+ edge.each_vertex { |vertex| @vertices.fetch(vertex).delete(edge) }
47
+ @edges.delete(edge)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tangle
4
+ # Raised for zero arity of a currified method, subclass of ArgumentError
5
+ class CurrifyError < ArgumentError
6
+ def initialize(msg = 'method accepts no arguments', *)
7
+ super
8
+ end
9
+ end
10
+
11
+ # Currification of instance methods, for adding callbacks to other objects
12
+ module Currify
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ # Class method extensions for currification of instance methods
18
+ module ClassMethods
19
+ # Return a list of currified methods for a given tag.
20
+ #
21
+ # :call-seq:
22
+ # self.class.currified_methods(tag) => Array of Symbol
23
+ def currified_methods(tag)
24
+ mine = @currified_methods&.[](tag) || []
25
+ return mine unless superclass.respond_to?(:currified_methods)
26
+
27
+ superclass.currified_methods(tag) + mine
28
+ end
29
+
30
+ private
31
+
32
+ # Add a symbol to the list of currified methods for a tag.
33
+ #
34
+ # :call-seq:
35
+ # class X
36
+ # currify :tag, :method
37
+ def currify(tag, method)
38
+ raise CurrifyError if instance_method(method).arity.zero?
39
+
40
+ @currified_methods ||= {}
41
+ @currified_methods[tag] ||= []
42
+ @currified_methods[tag] << method
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def define_currified_methods(obj, tag)
49
+ self.class.currified_methods(tag)&.each do |name|
50
+ obj.instance_exec(name, method(name).curry) do |method_name, method|
51
+ define_singleton_method(method_name) do |*args|
52
+ method.call(self, *args)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tangle/directed/graph'
2
4
  require 'tangle/directed/acyclic/partial_order'
3
5
 
@@ -17,6 +19,7 @@ module Tangle
17
19
  def insert_edge(edge)
18
20
  raise CyclicError if successor?(edge.head, edge.tail) ||
19
21
  predecessor?(edge.tail, edge.head)
22
+
20
23
  super
21
24
  end
22
25
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  module Directed
3
5
  module Acyclic
@@ -29,6 +31,7 @@ module Tangle
29
31
  raise GraphError unless graph == other.graph
30
32
  return 0 if vertex == other.vertex
31
33
  return -1 if graph.successor?(vertex, other.vertex)
34
+
32
35
  1
33
36
  end
34
37
  end
@@ -1,4 +1,6 @@
1
- require 'tangle/edge'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../edge'
2
4
 
3
5
  module Tangle
4
6
  module Directed
@@ -31,7 +33,7 @@ module Tangle
31
33
  super
32
34
  @tail = tail
33
35
  @head = head
34
- @vertices = { tail => head }
36
+ @vertices = { tail => head }.freeze
35
37
  end
36
38
  end
37
39
  end
@@ -1,103 +1,125 @@
1
- require 'tangle/graph'
2
- require 'tangle/directed/edge'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_graph'
4
+ require_relative 'edge'
3
5
 
4
6
  module Tangle
5
7
  module Directed
6
8
  #
7
9
  # A directed graph
8
- class Graph < Tangle::Graph
9
- Edge = Tangle::Directed::Edge
10
- DEFAULT_MIXINS = [].freeze
11
-
10
+ class Graph < Tangle::BaseGraph
12
11
  # Return the incoming edges for +vertex+
13
12
  def in_edges(vertex)
14
13
  edges(vertex).select { |edge| edge.head?(vertex) }
15
14
  end
15
+ currify :vertex, :in_edges
16
16
 
17
17
  # Return the direct predecessors of +vertex+
18
18
  def direct_predecessors(vertex)
19
19
  in_edges(vertex).map(&:tail).to_set
20
20
  end
21
+ currify :vertex, :direct_predecessors
21
22
 
22
23
  # Is +other+ a direct predecessor of +vertex+?
23
24
  def direct_predecessor?(vertex, other)
24
25
  direct_predecessors(vertex).include?(other)
25
26
  end
27
+ currify :vertex, :direct_predecessor?
26
28
 
27
29
  # Return a breadth first enumerator for all predecessors
28
30
  def predecessors(vertex)
29
31
  vertex_enumerator(vertex, :direct_predecessors)
30
32
  end
33
+ currify :vertex, :predecessors
31
34
 
32
35
  # Is +other+ a predecessor of +vertex+?
33
36
  def predecessor?(vertex, other)
34
37
  predecessors(vertex).any? { |vtx| other.eql?(vtx) }
35
38
  end
39
+ currify :vertex, :predecessor?
36
40
 
37
41
  # Return a subgraph with all predecessors of a +vertex+
38
42
  def predecessor_subgraph(vertex, &selector)
39
43
  subgraph(predecessors(vertex), &selector)
40
44
  end
45
+ currify :vertex, :predecessor_subgraph
41
46
 
42
47
  # Return the outgoing edges for +vertex+
43
48
  def out_edges(vertex)
44
49
  edges(vertex).select { |edge| edge.tail?(vertex) }
45
50
  end
51
+ currify :vertex, :out_edges
46
52
 
47
53
  # Return the direct successors of +vertex+
48
54
  def direct_successors(vertex)
49
55
  out_edges(vertex).map(&:head).to_set
50
56
  end
57
+ currify :vertex, :direct_successors
51
58
 
52
59
  # Is +other+ a direct successor of +vertex+?
53
60
  def direct_successor?(vertex, other)
54
61
  direct_successors(vertex).include?(other)
55
62
  end
63
+ currify :vertex, :direct_successor?
56
64
 
57
65
  # Return a breadth first enumerator for all successors
58
66
  def successors(vertex)
59
67
  vertex_enumerator(vertex, :direct_successors)
60
68
  end
69
+ currify :vertex, :successors
61
70
 
62
71
  # Is +other+ a successor of +vertex+?
63
72
  def successor?(vertex, other)
64
73
  successors(vertex).any? { |vtx| other.eql?(vtx) }
65
74
  end
75
+ currify :vertex, :successor?
66
76
 
67
77
  # Return a subgraph with all successors of a +vertex+
68
78
  def successor_subgraph(vertex, &selector)
69
79
  subgraph(successors(vertex), &selector)
70
80
  end
81
+ currify :vertex, :successor_subgraph
71
82
 
72
83
  # Return the in degree for +vertex+
73
84
  def in_degree(vertex)
74
85
  in_edges(vertex).count
75
86
  end
87
+ currify :vertex, :in_degree
76
88
 
77
89
  # Return the out degree for +vertex+
78
90
  def out_degree(vertex)
79
91
  out_edges(vertex).count
80
92
  end
93
+ currify :vertex, :out_degree
81
94
 
82
95
  # Is +vertex+ a sink in the graph?
83
96
  def sink?(vertex)
84
97
  out_degree(vertex).zero?
85
98
  end
99
+ currify :vertex, :sink?
86
100
 
87
101
  # Is +vertex+ a source in the graph?
88
102
  def source?(vertex)
89
103
  in_degree(vertex).zero?
90
104
  end
105
+ currify :vertex, :source?
91
106
 
92
107
  # Is +vertex+ internal in the graph?
93
108
  def internal?(vertex)
94
109
  !(sink?(vertex) || source?(vertex))
95
110
  end
111
+ currify :vertex, :internal?
96
112
 
97
113
  # Is the graph balanced?
98
114
  def balanced?
99
115
  vertices.all? { |vertex| in_degree(vertex) == out_degree(vertex) }
100
116
  end
117
+
118
+ private
119
+
120
+ def new_edge(*args, **kwargs)
121
+ Edge.new(*args, **kwargs)
122
+ end
101
123
  end
102
124
  end
103
125
  end
data/lib/tangle/edge.rb CHANGED
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
- require 'tangle/errors'
4
+ require_relative 'mixin'
3
5
 
4
6
  module Tangle
5
7
  #
6
- # An edge in a graph, connecting two vertices
8
+ # An edge in an undirected graph, connecting two vertices
7
9
  #
8
10
  class Edge
9
11
  include Tangle::Mixin::Initialize
@@ -31,16 +33,6 @@ module Tangle
31
33
  @vertices.fetch(from_vertex)
32
34
  end
33
35
 
34
- def to_s
35
- vertex1, vertex2 = @vertices.keys
36
- "{#{vertex1}<->#{vertex2}}"
37
- end
38
- alias inspect to_s
39
-
40
- def each_vertex(&block)
41
- @vertices.each_key(&block)
42
- end
43
-
44
36
  def include?(vertex)
45
37
  each_vertex.include?(vertex)
46
38
  end
@@ -51,9 +43,8 @@ module Tangle
51
43
 
52
44
  private
53
45
 
54
- def initialize_vertices(vertex1, vertex2 = vertex1)
46
+ def initialize_vertices(vertex1, vertex2)
55
47
  @loop = vertex1 == vertex2
56
- @vertices = { vertex1 => vertex2, vertex2 => vertex1 }.freeze
57
48
  end
58
49
  end
59
50
  end
data/lib/tangle/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  #
3
5
  # LoopError is raised when a looped edge is disallowed.
data/lib/tangle/mixin.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  module Mixin
3
5
  #
@@ -10,7 +12,6 @@ module Tangle
10
12
 
11
13
  def initialize_mixins(mixins: nil, **kwargs)
12
14
  @mixins = mixins
13
-
14
15
  extend_with_mixins unless @mixins.nil?
15
16
  initialize_kwargs(**kwargs) unless kwargs.empty?
16
17
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  module Mixin
3
5
  # Tangle mixin for loading a directory structure as a graph
@@ -7,17 +9,25 @@ module Tangle
7
9
  # options are:
8
10
  # root: root directory for the structure (mandatory)
9
11
  # loaders: list of object loader lambdas (mandatory)
12
+ # ->(graph, **) { ... } => finished?
10
13
  # follow_links: bool for following symlinks to directories
11
- # (default false)
14
+ # exclude_root: bool for excluding the root directory
15
+ #
16
+ # All bool options default to false.
17
+ #
18
+ # A loader lambda is called with the graph as only positional
19
+ # argument, and a number of keyword arguments:
12
20
  #
13
- # A loader lambda will be called like
14
- # ->(graph, path, parent_path) { ... }
21
+ # path: Path of current filesystem object
22
+ # parent: Path of filesystem parent object
23
+ # lstat: File.lstat for path
24
+ # stat: File.stat for path, if lstat.symlink?
15
25
  #
16
- # The lambdas are called in order until one returns true
26
+ # The lambdas are called in order until one returns true.
17
27
  #
18
28
  # Example:
19
- # loader = lambda do |g, path, parent|
20
- # vertex = File::Stat.new(path)
29
+ # loader = lambda do |g, path:, parent:, lstat:, **|
30
+ # vertex = kwargs[:lstat]
21
31
  # g.add_vertex(vertex, name: path)
22
32
  # g.add_edge(g[parent], vertex) unless parent.nil?
23
33
  # end
@@ -30,25 +40,50 @@ module Tangle
30
40
 
31
41
  private
32
42
 
33
- def initialize_kwarg_directory(**options)
43
+ def initialize_kwarg_directory(options)
34
44
  @root_directory = options.fetch(:root)
35
45
  @directory_loaders = options.fetch(:loaders)
36
46
  @follow_directory_links = options[:follow_links]
47
+ @exclude_root = options[:exclude_root]
37
48
  load_directory_graph(@root_directory)
38
49
  end
39
50
 
40
51
  def load_directory_graph(path, parent = nil)
41
- @directory_loaders.any? do |loader|
42
- loader.to_proc.call(self, path, parent)
43
- end
44
-
45
- return if File.symlink?(path) && !@follow_directory_links
46
- return unless File.directory?(path)
52
+ return unless load_directory_object(path, parent)
47
53
 
48
54
  Dir.each_child(path) do |file|
49
55
  load_directory_graph(File.join(path, file), path)
50
56
  end
51
57
  end
58
+
59
+ # Load a filesystem object into the graph, returning
60
+ # +true+ if the object was a directory (or link to one,
61
+ # and we're following links).
62
+ def load_directory_object(path, parent = nil)
63
+ if @exclude_root
64
+ return true if path == @root_directory
65
+
66
+ parent = nil if parent == @root_directory
67
+ end
68
+
69
+ try_directory_loaders(path, parent)
70
+ end
71
+
72
+ # Try each directory loader, returning true if the object has
73
+ # children to follow
74
+ def try_directory_loaders(path, parent)
75
+ stat = lstat = File.lstat(path)
76
+ stat = File.stat(path) if lstat.symlink?
77
+
78
+ @directory_loaders.any? do |loader|
79
+ loader.to_proc.call(self, path: path, parent: parent,
80
+ lstat: lstat, stat: stat)
81
+ end
82
+
83
+ return if lstat.symlink? && !@follow_directory_links
84
+
85
+ stat.directory?
86
+ end
52
87
  end
53
88
  end
54
89
  end