botanicus-thor 0.9.8

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,344 @@
1
+ # coding: utf-8
2
+
3
+ require "thor"
4
+ require "thor/util"
5
+ require "open-uri"
6
+ require "fileutils"
7
+ require "yaml"
8
+ require "digest/md5"
9
+ require "readline"
10
+ require "pathname"
11
+
12
+ class Thor::Runner < Thor
13
+ def self.globs_for(path)
14
+ ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
15
+ end
16
+
17
+ map "-T" => :list, "-i" => :install, "-u" => :update
18
+
19
+ desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
20
+ method_options :as => :optional, :relative => :boolean
21
+ def install(name)
22
+ initialize_thorfiles
23
+
24
+ # If a directory name is provided as the argument, look for a 'main.thor'
25
+ # task in said directory.
26
+ begin
27
+ if File.directory?(File.expand_path(name))
28
+ base, package = File.join(name, "main.thor"), :directory
29
+ contents = open(base).read
30
+ else
31
+ base, package = name, :file
32
+ contents = open(name).read
33
+ end
34
+ rescue OpenURI::HTTPError
35
+ raise Error, "Error opening URI `#{name}'"
36
+ rescue Errno::ENOENT
37
+ raise Error, "Error opening file `#{name}'"
38
+ end
39
+
40
+ puts "Your Thorfile contains: "
41
+ puts contents
42
+ print "Do you wish to continue [y/N]? "
43
+ response = Readline.readline
44
+
45
+ return false unless response =~ /^\s*y/i
46
+
47
+ as = options["as"] || begin
48
+ first_line = contents.split("\n")[0]
49
+ (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
50
+ end
51
+
52
+ unless as
53
+ print "Please specify a name for #{name} in the system repository [#{name}]: "
54
+ as = Readline.readline
55
+ as = name if as.empty?
56
+ end
57
+
58
+ FileUtils.mkdir_p(thor_root)
59
+ FileUtils.touch(File.join(thor_root, "thor.yml"))
60
+
61
+ yaml_file = File.join(thor_root, "thor.yml")
62
+ FileUtils.touch(yaml_file)
63
+
64
+ thor_yaml[as] = {
65
+ :filename => Digest::MD5.hexdigest(name + as),
66
+ :location => (options[:relative] || File.exists?(name)) ? name : File.expand_path(name),
67
+ :constants => Thor::Util.constants_in_contents(contents, base)
68
+ }
69
+
70
+ save_yaml(thor_yaml)
71
+
72
+ puts "Storing thor file in your system repository"
73
+
74
+ destination = File.join(thor_root, thor_yaml[as][:filename])
75
+
76
+ if package == :file
77
+ File.open(destination, "w") { |f| f.puts contents }
78
+ else
79
+ FileUtils.cp_r(name, destination)
80
+ end
81
+
82
+ thor_yaml[as][:filename] # Indicate sucess
83
+ end
84
+
85
+ desc "uninstall NAME", "uninstall a named Thor module"
86
+ def uninstall(name)
87
+ raise Error, "Can't find module `#{name}'" unless thor_yaml[name]
88
+
89
+ puts "Uninstalling #{name}."
90
+
91
+ FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}"))
92
+
93
+ thor_yaml.delete(name)
94
+ save_yaml(thor_yaml)
95
+
96
+ puts "Done."
97
+ end
98
+
99
+ desc "update NAME", "update a Thor file from its original location"
100
+ def update(name)
101
+ raise Error, "Can't find module `#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location]
102
+
103
+ puts "Updating `#{name}' from #{thor_yaml[name][:location]}"
104
+ old_filename = thor_yaml[name][:filename]
105
+ self.options = self.options.merge("as" => name)
106
+ filename = install(thor_yaml[name][:location])
107
+ unless filename == old_filename
108
+ File.delete(File.join(thor_root, old_filename))
109
+ end
110
+ end
111
+
112
+ desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
113
+ method_options :internal => :boolean
114
+ def installed
115
+ thor_root_glob.each do |f|
116
+ next if f =~ /thor\.yml$/
117
+ load_thorfile(f) unless Thor.subclass_files.keys.include?(File.expand_path(f))
118
+ end
119
+
120
+ klasses = Thor.subclasses
121
+ klasses -= [Thor, Thor::Runner] unless options["internal"]
122
+ display_klasses(true, klasses)
123
+ end
124
+
125
+ desc "list [SEARCH]",
126
+ "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
127
+ method_options :substring => :boolean,
128
+ :group => :optional,
129
+ :all => :boolean
130
+ def list(search="")
131
+ initialize_thorfiles
132
+ search = ".*#{search}" if options["substring"]
133
+ search = /^#{search}.*/i
134
+ group = options[:group] || "standard"
135
+
136
+ classes = Thor.subclasses.select do |k|
137
+ (options[:all] || k.group_name == group) &&
138
+ Thor::Util.constant_to_thor_path(k.name) =~ search
139
+ end
140
+ display_klasses(false, classes)
141
+ end
142
+
143
+ # Override Thor#help so we can give info about not-yet-loaded tasks
144
+ def help(task = nil)
145
+ initialize_thorfiles(task) if task && task.include?(?:)
146
+ super
147
+ end
148
+
149
+ def method_missing(meth, *args)
150
+ meth = meth.to_s
151
+ super(meth.to_sym, *args) unless meth.include?(?:)
152
+
153
+ initialize_thorfiles(meth)
154
+ task = Thor[meth]
155
+ task.parse(task.klass.new, ARGV[1..-1])
156
+ end
157
+
158
+ def self.thor_root
159
+ return File.join(ENV["HOME"], '.thor') if ENV["HOME"]
160
+
161
+ if ENV["HOMEDRIVE"] && ENV["HOMEPATH"] then
162
+ return File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"], '.thor')
163
+ end
164
+
165
+ return File.join(ENV["APPDATA"], '.thor') if ENV["APPDATA"]
166
+
167
+ begin
168
+ File.expand_path("~")
169
+ rescue
170
+ if File::ALT_SEPARATOR then
171
+ "C:/"
172
+ else
173
+ "/"
174
+ end
175
+ end
176
+ end
177
+
178
+ def self.thor_root_glob
179
+ # On Windows thor_root will be something like this:
180
+ #
181
+ # C:\Documents and Settings\james\.thor
182
+ #
183
+ # If we don't #gsub the \ character, Dir.glob will fail.
184
+ files = Dir["#{thor_root.gsub(/\\/, '/')}/*"]
185
+ files.map! do |file|
186
+ File.directory?(file) ? File.join(file, "main.thor") : file
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def thor_root
193
+ self.class.thor_root
194
+ end
195
+
196
+ def thor_root_glob
197
+ self.class.thor_root_glob
198
+ end
199
+
200
+ def thor_yaml
201
+ @y ||= begin
202
+ yaml_file = File.join(thor_root, "thor.yml")
203
+ yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
204
+ yaml || {}
205
+ end
206
+ end
207
+
208
+ def save_yaml(yaml)
209
+ yaml_file = File.join(thor_root, "thor.yml")
210
+ File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml }
211
+ end
212
+
213
+ def display_klasses(with_modules = false, klasses = Thor.subclasses)
214
+ klasses -= [Thor, Thor::Runner] unless with_modules
215
+ raise Error, "No Thor tasks available" if klasses.empty?
216
+
217
+ if with_modules && !thor_yaml.empty?
218
+ max_name = thor_yaml.max { |(xk, xv), (yk, yv)| xk.to_s.size <=> yk.to_s.size }.first.size
219
+ modules_label = "Modules"
220
+ namespaces_label = "Namespaces"
221
+ column_width = [max_name + 4, modules_label.size + 1].max
222
+
223
+ print "%-#{column_width}s" % modules_label
224
+ puts namespaces_label
225
+ print "%-#{column_width}s" % ("-" * modules_label.size)
226
+ puts "-" * namespaces_label.size
227
+
228
+ thor_yaml.each do |name, info|
229
+ print "%-#{column_width}s" % name
230
+ puts info[:constants].map { |c| Thor::Util.constant_to_thor_path(c) }.join(", ")
231
+ end
232
+
233
+ puts
234
+ end
235
+
236
+ # Calculate the largest base class name
237
+ max_base = klasses.max do |x,y|
238
+ Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
239
+ end.name.size
240
+
241
+ # Calculate the size of the largest option description
242
+ max_left_item = klasses.max do |x,y|
243
+ (x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
244
+ end
245
+
246
+ max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
247
+
248
+ unless klasses.empty?
249
+ puts # add some spacing
250
+ klasses.each { |k| display_tasks(k, max_base, max_left); }
251
+ else
252
+ puts "\033[1;34mNo Thor tasks available\033[0m"
253
+ end
254
+ end
255
+
256
+ def display_tasks(klass, max_base, max_left)
257
+ if klass.tasks.values.length > 1
258
+
259
+ base = Thor::Util.constant_to_thor_path(klass.name)
260
+
261
+ if base.empty?
262
+ base = 'default'
263
+ puts "\033[1;35m#{base}\033[0m"
264
+ else
265
+ puts "\033[1;34m#{base}\033[0m"
266
+ end
267
+
268
+ puts "-" * base.length
269
+
270
+ klass.tasks.each true do |name, task|
271
+ format_string = "%-#{max_left + max_base + 5}s"
272
+ print format_string % task.formatted_usage(true)
273
+ puts task.description
274
+ end
275
+
276
+ unless klass.opts.empty?
277
+ puts "\nglobal options: #{Options.new(klass.opts)}"
278
+ end
279
+
280
+ puts # add some spacing
281
+ end
282
+ end
283
+
284
+ def initialize_thorfiles(relevant_to = nil)
285
+ thorfiles(relevant_to).each do |f|
286
+ load_thorfile(f) unless Thor.subclass_files.keys.include?(File.expand_path(f))
287
+ end
288
+ end
289
+
290
+ def load_thorfile(path)
291
+ txt = File.read(path)
292
+ begin
293
+ Thor::Tasks.class_eval(txt, path)
294
+ rescue Object => e
295
+ $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
296
+ end
297
+ end
298
+
299
+ # Finds Thorfiles by traversing from your current directory down to the root
300
+ # directory of your system. If at any time we find a Thor file, we stop.
301
+ #
302
+ # ==== Example
303
+ # If we start at /Users/wycats/dev/thor ...
304
+ #
305
+ # 1. /Users/wycats/dev/thor
306
+ # 2. /Users/wycats/dev
307
+ # 3. /Users/wycats <-- we find a Thorfile here, so we stop
308
+ #
309
+ # Suppose we start at c:\Documents and Settings\james\dev\thor ...
310
+ #
311
+ # 1. c:\Documents and Settings\james\dev\thor
312
+ # 2. c:\Documents and Settings\james\dev
313
+ # 3. c:\Documents and Settings\james
314
+ # 4. c:\Documents and Settings
315
+ # 5. c:\ <-- no Thorfiles found!
316
+ def thorfiles(relevant_to=nil)
317
+ thorfiles = []
318
+
319
+ # This may seem a little odd at first. Suppose you're working on a Rails
320
+ # project and you traverse into the "app" directory. Because of the below
321
+ # you can execute "thor -T" and see any tasks you might have in the root
322
+ # directory of your Rails project.
323
+ Pathname.pwd.ascend do |path|
324
+ thorfiles = Thor::Runner.globs_for(path).map { |g| Dir[g] }.flatten
325
+ break unless thorfiles.empty?
326
+ end
327
+
328
+ # We want to load system-wide Thorfiles first so the local Thorfiles will
329
+ # override them.
330
+ files = (relevant_to ? thorfiles_relevant_to(relevant_to) : thor_root_glob)
331
+ files += thorfiles - ["#{thor_root}/thor.yml"]
332
+
333
+ files.map! do |file|
334
+ File.directory?(file) ? File.join(file, "main.thor") : file
335
+ end
336
+ end
337
+
338
+ def thorfiles_relevant_to(meth)
339
+ klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
340
+ thor_yaml.select do |k, v|
341
+ v[:constants] && v[:constants].include?(klass_str)
342
+ end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
343
+ end
344
+ end
data/lib/thor/task.rb ADDED
@@ -0,0 +1,85 @@
1
+ # coding: utf-8
2
+
3
+ require 'thor/error'
4
+ require 'thor/util'
5
+
6
+ class Thor
7
+ class Task < Struct.new(:meth, :description, :usage, :opts, :klass)
8
+
9
+ def self.dynamic(meth, klass)
10
+ new(meth, "A dynamically-generated task", meth.to_s, nil, klass)
11
+ end
12
+
13
+ def initialize(*args)
14
+ # keep the original opts - we need them later on
15
+ @options = args[3] || {}
16
+ super
17
+ end
18
+
19
+ def parse(obj, args)
20
+ list, hash = parse_args(args)
21
+ obj.options = hash
22
+ run(obj, *list)
23
+ end
24
+
25
+ def run(obj, *params)
26
+ raise NoMethodError, "the `#{meth}' task of #{obj.class} is private" if
27
+ (obj.private_methods + obj.protected_methods).include?(meth)
28
+
29
+ obj.invoke(meth, *params)
30
+ rescue ArgumentError => e
31
+
32
+ # backtrace sans anything in this file
33
+ backtrace = e.backtrace.reject {|frame| frame =~ /^#{Regexp.escape(__FILE__)}/}
34
+ # also nix anything in thor.rb
35
+ backtrace = backtrace.reject { |frame| frame =~ /\/thor.rb/ }
36
+
37
+ # and sans anything that got us here
38
+ backtrace -= caller
39
+ raise e unless backtrace.empty?
40
+
41
+ # okay, they really did call it wrong
42
+ raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
43
+ rescue NoMethodError => e
44
+ begin
45
+ raise e unless e.message =~ /^undefined method `#{meth}' for #{Regexp.escape(obj.inspect)}$/
46
+ rescue
47
+ raise e
48
+ end
49
+ raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
50
+ end
51
+
52
+ def namespace(remove_default = true)
53
+ Thor::Util.constant_to_thor_path(klass, remove_default)
54
+ end
55
+
56
+ def with_klass(klass)
57
+ new = self.dup
58
+ new.klass = klass
59
+ new
60
+ end
61
+
62
+ def opts
63
+ return super unless super.kind_of? Hash
64
+ @_opts ||= Options.new(super)
65
+ end
66
+
67
+ def full_opts
68
+ @_full_opts ||= Options.new((klass.opts || {}).merge(@options))
69
+ end
70
+
71
+ def formatted_usage(namespace = false)
72
+ (namespace ? self.namespace + ':' : '') + usage +
73
+ (opts ? " " + opts.formatted_usage : "")
74
+ end
75
+
76
+ protected
77
+
78
+ def parse_args(args)
79
+ return [[], {}] if args.nil?
80
+ hash = full_opts.parse(args)
81
+ list = full_opts.non_opts
82
+ [list, hash]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+
3
+ require 'thor/task'
4
+
5
+ class Thor::TaskHash < Hash
6
+ def initialize(klass)
7
+ super()
8
+ @klass = klass
9
+ end
10
+
11
+ def each(local = false, &block)
12
+ super() { |k, t| yield k, t.with_klass(@klass) }
13
+ @klass.superclass.tasks.each { |k, t| yield k, t.with_klass(@klass) } unless local || @klass == Thor
14
+ end
15
+
16
+ def [](name)
17
+ if task = super(name) || (@klass == Thor && @klass.superclass.tasks[name])
18
+ return task.with_klass(@klass)
19
+ end
20
+
21
+ Thor::Task.dynamic(name, @klass)
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+
3
+ require "thor/task"
4
+
5
+ class Thor::PackageTask < Thor::Task
6
+ attr_accessor :spec
7
+ attr_accessor :opts
8
+
9
+ def initialize(gemspec, opts = {})
10
+ super(:package, "build a gem package")
11
+ @spec = gemspec
12
+ @opts = {:dir => File.join(Dir.pwd, "pkg")}.merge(opts)
13
+ end
14
+
15
+ def run
16
+ FileUtils.mkdir_p(@opts[:dir])
17
+ Gem::Builder.new(spec).build
18
+ FileUtils.mv(spec.file_name, File.join(@opts[:dir], spec.file_name))
19
+ end
20
+ end
data/lib/thor/tasks.rb ADDED
@@ -0,0 +1,88 @@
1
+ # coding: utf-8
2
+
3
+ require "thor"
4
+ require "fileutils"
5
+
6
+ class Thor
7
+ def self.package_task(spec)
8
+ desc "package", "package up the gem"
9
+ define_method :package do
10
+ FileUtils.mkdir_p(File.join(Dir.pwd, "pkg"))
11
+ Gem::Builder.new(spec).build
12
+ FileUtils.mv(spec.file_name, File.join(Dir.pwd, "pkg", spec.file_name))
13
+ end
14
+ end
15
+
16
+ def self.install_task(spec)
17
+ package_task spec
18
+
19
+ null, sudo, gem = RUBY_PLATFORM =~ /mswin|mingw/ ? ['NUL', '', 'gem.bat'] :
20
+ ['/dev/null', 'sudo', 'gem']
21
+
22
+ desc "install", "install the gem"
23
+ define_method :install do
24
+ old_stderr, $stderr = $stderr.dup, File.open(null, "w")
25
+ package
26
+ $stderr = old_stderr
27
+ system %{#{sudo} #{Gem.ruby} -S #{gem} install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
28
+ end
29
+ end
30
+
31
+ def self.spec_task(file_list, opts = {})
32
+ name = opts.delete(:name) || "spec"
33
+ rcov_dir = opts.delete(:rcov_dir) || "coverage"
34
+ file_list = file_list.map {|f| %["#{f}"]}.join(" ")
35
+ verbose = opts.delete(:verbose)
36
+ opts = {:format => "specdoc", :color => true}.merge(opts)
37
+
38
+ rcov_opts = convert_task_options(opts.delete(:rcov) || {})
39
+ rcov = !rcov_opts.empty?
40
+ options = convert_task_options(opts)
41
+
42
+ if rcov
43
+ FileUtils.rm_rf(File.join(Dir.pwd, rcov_dir))
44
+ end
45
+
46
+ desc(name, "spec task")
47
+ define_method(name) do
48
+ require 'rbconfig'
49
+ cmd = RbConfig::CONFIG['ruby_install_name'] << " "
50
+ if rcov
51
+ cmd << "-S #{where('rcov')} -o #{rcov_dir} #{rcov_opts} "
52
+ end
53
+ cmd << where('spec')
54
+ cmd << " -- " if rcov
55
+ cmd << " "
56
+ cmd << file_list
57
+ cmd << " "
58
+ cmd << options
59
+ puts cmd if verbose
60
+ system(cmd)
61
+ exit($?.exitstatus)
62
+ end
63
+ end
64
+
65
+ private
66
+ def self.convert_task_options(opts)
67
+ opts.map do |key, value|
68
+ case value
69
+ when true
70
+ "--#{key}"
71
+ when Array
72
+ value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
73
+ when nil, false
74
+ ""
75
+ else
76
+ "--#{key} #{value.inspect}"
77
+ end
78
+ end.join(" ")
79
+ end
80
+
81
+ def where(file)
82
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
83
+ file_with_path = File.join(path, file)
84
+ next unless File.exist?(file_with_path) && File.executable?(file_with_path)
85
+ return File.expand_path(file_with_path)
86
+ end
87
+ end
88
+ end
data/lib/thor/util.rb ADDED
@@ -0,0 +1,77 @@
1
+ # coding: utf-8
2
+
3
+ require 'thor/error'
4
+
5
+ module ObjectSpace
6
+
7
+ class << self
8
+
9
+ # @return <Array[Class]> All the classes in the object space.
10
+ def classes
11
+ klasses = []
12
+ ObjectSpace.each_object(Class) {|o| klasses << o}
13
+ klasses
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ class Thor
20
+ module Tasks; end
21
+
22
+ module Util
23
+
24
+ def self.full_const_get(obj, name)
25
+ list = name.split("::")
26
+ list.shift if list.first.empty?
27
+ list.each do |x|
28
+ # This is required because const_get tries to look for constants in the
29
+ # ancestor chain, but we only want constants that are HERE
30
+ obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
31
+ end
32
+ obj
33
+ end
34
+
35
+ def self.constant_to_thor_path(str, remove_default = true)
36
+ str = str.to_s.gsub(/^Thor::Tasks::/, "")
37
+ str = snake_case(str).squeeze(":")
38
+ str.gsub!(/^default/, '') if remove_default
39
+ str
40
+ end
41
+
42
+ def self.constant_from_thor_path(str)
43
+ make_constant(to_constant(str))
44
+ rescue NameError => e
45
+ raise e unless e.message =~ /^uninitialized constant (.*)$/
46
+ raise Error, "There was no available namespace `#{str}'."
47
+ end
48
+
49
+ def self.to_constant(str)
50
+ str = 'default' if str.empty?
51
+ str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
52
+ end
53
+
54
+ def self.constants_in_contents(str, file = __FILE__)
55
+ klasses = ObjectSpace.classes.dup
56
+ Module.new.class_eval(str, file)
57
+ klasses = ObjectSpace.classes - klasses
58
+ klasses = klasses.select {|k| k < Thor }
59
+ klasses.map! {|k| k.to_s.gsub(/#<Module:\w+>::/, '')}
60
+ end
61
+
62
+ def self.make_constant(str, base = [Thor::Tasks, Object])
63
+ which = base.find do |obj|
64
+ full_const_get(obj, str) rescue nil
65
+ end
66
+ return full_const_get(which, str) if which
67
+ raise NameError, "uninitialized constant #{str}"
68
+ end
69
+
70
+ def self.snake_case(str)
71
+ return str.downcase if str =~ /^[A-Z_]+$/
72
+ str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
73
+ return $+.downcase
74
+ end
75
+
76
+ end
77
+ end