Как составить задачи Thor в отдельных классах/модулях/файлах?
У меня возникли проблемы с получением Thor, поэтому, надеюсь, кто-то может указать, что я делаю неправильно.
У меня есть основной класс class MyApp < Thor
, который я хочу разбить на отдельные файлы для нескольких пространств имен, например thor create:app_type
и thor update:app_type
. Я не могу найти примеров, которые показывают, как нужно разбить приложение Thor на части, и то, что я пробовал, похоже, не работает.
Возьмем, к примеру, этот класс, который я пытаюсь вырваться из основного класса Thor:
module Things
module Grouping
desc "something", "Do something cool in this group"
def something
....
end
end
end
Когда я пытаюсь включить или потребовать это в свой основной класс:
class App < Thor
....
require 'grouping_file'
include Things::Grouping
....
end
Я получаю исключение: '<module:Grouping>': undefined method 'desc' for Things::Grouping:Module (NoMethodError)
Возможно ли иметь несколько пространств имен для задач Thor, и если да, то как их разбить, чтобы у вас не было одного монолитного класса, который занимает несколько сотен строк?
Ответы
Ответ 1
Используйте модуль с расширением, скажем Foo
, внутри которого вы будете определять все подмодули и подклассы.
Запустите определение этого модуля в одном файле foo.thor
, который находится в директории, из которой вы будете запускать все задачи Thor. В верхней части модуля Foo
в этом foo.thor
определите этот метод:
# Load all our thor files
module Foo
def self.load_thorfiles(dir)
Dir.chdir(dir) do
thor_files = Dir.glob('**/*.thor').delete_if { |x| not File.file?(x) }
thor_files.each do |f|
Thor::Util.load_thorfile(f)
end
end
end
end
Затем в нижней части основного файла foo.thor
добавьте:
Foo.load_thorfiles('directory_a')
Foo.load_thorfiles('directory_b')
Это будет рекурсивно включать все файлы *.thor
в эти каталоги. Модули Nest в вашем основном модуле Foo
для пространственного хранения ваших задач. Не имеет значения, где находятся файлы или что они вызывают в этот момент, до тех пор, пока вы включите все ваши связанные с Тором каталоги с помощью метода, описанного выше.
Ответ 2
Почему это не работает: когда вы используете desc
внутри класса Thor
, вы на самом деле вызываете метод класса Thor.desc
. Когда вы делаете это в модуле, он вызывает YourModule.desc
, который, очевидно, не существует.
Есть два способа предложить исправить это.
Исправить одно: использовать Module.included
Вы хотите, чтобы эти задачи повторно использовались в нескольких классах Thor?
Когда модуль используется как include
в Ruby, вызывается метод класса included
. http://www.ruby-doc.org/core/classes/Module.html#M000458
module MyModule
def self.included(thor)
thor.class_eval do
desc "Something", "Something cool"
def something
# ...
end
end
end
end
Исправить два: разделение классов Thor на несколько файлов
Вы просто хотели отдельно определить задачи в другом файле?
Если это так, просто заново откройте свой класс App в другом файле. Ваш Thorfile будет выглядеть примерно так:
# Thorfile
Dir['./lib/thor/**/*.rb'].sort.each { |f| load f }
Тогда ваш lib/thor/app.rb
будет содержать некоторые задачи для App
, а другой файл lib/thor/app-grouping.rb
будет содержать еще несколько задач для одного и того же класса App
.
Ответ 3
У меня была такая же проблема, и я чуть не сдался, но потом у меня появилась идея:
Если вы записываете свои задачи в Thorfile
, а не как классы ruby, вы можете просто require
в файлах Ruby, содержащих подклассы Thor, и они появятся в списке доступных задач при запуске thor -T
.
Все это управляется классом Thor::Runner
. Если вы просмотрите это, вы увидите метод #thorfiles
, который отвечает за поиск файлов с именем Thorfile
в текущем рабочем каталоге.
Все, что я должен был сделать, чтобы: а) разбить задачи Thor на несколько файлов, тогда как b) не нужно было иметь один Thorfile
для создания локального подкласса Thor::Runner
, перезаписать его метод #thorfile
тем, что вернул список приложений Thor для конкретного приложения и затем вызвал его метод #start
, и все это сработало:
class MyApp::Runner < ::Thor::Runner
private
def thorfiles(*args)
Dir['thortasks/**/*.rb']
end
end
MyApp::Runner.start
Поэтому у меня может быть любое количество классов Ruby, определяющих задачи Thor под thortasks
например.
class MyApp::MyThorNamespace < ::Thor
namespace :mynamespace
# Unless you include the namespace in the task name the -T task list
# will list everything under the top-level namespace
# (which I think is a bug in Thor)
desc "#{namespace}:task", "Does something"
def task
# do something
end
end
Я почти отказался от Thor, пока не понял это, но не так много библиотек, которые занимаются созданием генераторов, а также созданием задач с именами, поэтому я рад, что нашел решение.
Ответ 4
Документация Thor действительно нуждается в улучшении. Ниже перечислены часы чтения кода, спецификаций, выпусков и google-fu. Я не могу сказать, что это то, как он должен работать, но он, безусловно, будет работать при настройке таким образом.
Когда класс наследует от Thor, он получает несколько важных методов класса.
- Регистр. Это позволяет вам зарегистрировать новую подкоманду в качестве задачи
- class_options. Это дает вам хэш всех параметров класса.
- задачи. Это дает вам хэш всех определенных задач.
Мы можем использовать их для включения задач из многих классов в один бегун.
Я включил несколько дополнительных файлов, чтобы вы могли увидеть приложение для всего рабочего тела. Grantesd не делает много...
#############################################################
#my_app/bin/my_app #
# #
#This file is the executable that requires the MyApp module,#
#then starts the runner. #
#############################################################
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') unless $LOAD_PATH.include(File.dirname(__FILE__) + '/../lib')
require "rubygems" # ruby1.9 doesn't "require" it though
require "my_app"
MyApp::Runner.start
########################################################
#my_app/lib/my_app.rb #
# #
#This is the main module, used to control the requires #
#the my_app requires should be done last to make sure #
#everything else is defined first. #
########################################################
require 'thor'
require 'thor/group'
module MyApp
#include other helper apps here
require 'my_app/runner' #first so all subcommands can register
require 'my_app/more'
require 'my_app/config'
end
###################################################################
#my_app/lib/my_app/runner.rb #
# #
#This is the main runner class. #
#ALL class_methods should be defined here except for Thor::Groups #
###################################################################
class MyApp::Runner < ::Thor
class_option :config, :type => :string,
:desc => "configuration file. accepts ENV $MYAPP_CONFIG_FILE",
:default => ENV["MYAPP_CONFIG_FILE"] || "~/.my_apprc"
method_option :rf, :type => :numeric,
:desc => "repeat greeting X times",
:default => 3
desc "foo","prints foo"
def foo
puts "foo" * options.rf
end
end
#######################################################################
#my_app/lib/my_app/more.rb #
# #
#A Thor Group example. #
#Class_options defined for a Thor Group become method_options when #
#used as a subcommand. #
#Since MyApp::Runner is already defined when this class is evaluated #
#It can automatcially register itself as a subcommand for the runner, #
#######################################################################
class Revamp::Init < ::Thor::Group
class_option :repeat, :type => :numeric,
:desc => "repeat greeting X times",
:default => 3
desc "prints woot"
def woot
puts "woot! " * options.repeat
end
desc "prints toow"
def toow
puts "!toow" * options.repeat
end
#This line registers this group as a sub command of the runner
MyApp::Runner.register MyApp::More, :more, "more", "print more stuff"
#This line copies the class_options for this class to the method_options of the :more task
MyApp::Runner.tasks["more"].options = MyApp::More.class_options
end
#####################################################################
#my_app/lib/my_app/config.rb #
# #
#For normal Thor classes, each task must be registered individually #
#####################################################################
class MyApp::Config < Thor
method_option :dr, :type => :numeric,
:desc => "repeat greeting X times",
:default => 3
desc "show_default", "show the default config"
def show_default
puts "default " * options.dr
end
MyApp::Runner.register MyApp::Config, :show_default, "show_default", "print default"
MyApp::Runner.tasks["show_default"].options = MyApp::Config.tasks["show_default"].options
method_option :cr, :type => :numeric,
:desc => "repeat greeting X times",
:default => 3
desc "show_config", "show the config"
def show_config
puts "config " * options.cr
end
MyApp::Runner.register MyApp::Config, :show_config, "show_config", "print config"
MyApp::Runner.tasks["show_config"].options = MyApp::Config.tasks["show_config"].options
end
Ответ 5
Вы можете счесть это полезным: https://github.com/lastobelus/cleanthor
Я хотел иметь исполняемый файл на основе оригинала для драгоценного камня с подкомандами с именами, но организовать файлы задач в соответствии с нормальной структурой ruby gem lib/mygem/*/.rb.
Я также хотел иметь Thorfile с корневым уровнем, так что работа с тобой, обычно в каталоге проекта во время разработки, также показывала все задачи gem.
Решение включало следующие шаги:
- подклассом
Thor::Runner
в Mygem::Thor::Runner
и переопределением его частных методов thorfiles
и method_missing
. В method_missing
я также удалил имя gem из команды, если она появилась.
- исполняемые вызовы gem
Mygem::Thor::Runner.start
- подклассификация
Thor::Task
в Mygem::Thor::Task
и
- переопределение своего частного метода класса
namespace
. Пользовательский метод namespace
выделяет часть Mygem::Thor::Tasks
иерархии модулей задач.
- переопределение своего частного метода
thorfiles
для возврата Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rb')]
- теперь задачи могут быть организованы в
lib/mygem/thor/tasks/**/*.rb
. Все они должны унаследовать от Mygem::Thor::Task
- Thorfile в корне проекта также загружает все задачи в
lib/mygem/thor/tasks/**/*.rb
Ответ 6
desc - метод класса, вам нужно использовать расширение вместо include.
Посмотрите здесь для объяснения.