Tuesday, February 19, 2008

Rake: Task Overwriting

Update at bottom

By default Rake Tasks append behavior every time they are defined. The following example shows that both definitions are executed.

require 'rubygems'
require 'rake'

task :the_task do
p "one"
end

task :the_task do
p "two"
end

Rake::Task[:the_task].invoke
# >> "one"
# >> "two"

I like this behavior, but sometimes you want to overwrite a task instead of appending to what's already there.

When you no longer want the existing behavior the overwrite method can come in handy.

require 'rubygems'
require 'rake'

class Rake::Task
def overwrite(&block)
@actions.clear
enhance(&block)
end
end

task :the_task do
p "one"
end

Rake::Task[:the_task].overwrite do
p "two"
end


Rake::Task[:the_task].invoke
# >> "two"

The overwrite method is good, but sometimes you want to redefine the task using one of the specialized Rake tasks. I recently wanted to redefine the test task, so I created the abandon method to remove the existing definition.

require 'rubygems'
require 'rake'
require 'rake/testtask'

class Rake::Task
def abandon
@actions.clear
end
end

task :the_task do
p "one"
end

Rake::Task[:the_task].abandon

Rake::TestTask.new(:the_task) do |t|
t.libs << "test"
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

Rake::Task[:the_task].invoke
# >> Expectations ..................
# >> Finished in 0.00442 seconds
# >>
# >> Success: 18 fulfilled

Hopefully you wont need this type of thing very often, but it can be handy when you want to overwrite a task that has been previously by a framework you've included.

Update below...
Ola Bini pointed out that I'm not clearing the prerequisites. Clearing the prerequisites is something you can do without any modifications to rake.

Rake::Task[:task_name].prerequisites.clear

Also, if you want prerequisites cleared as part of overwrite or abandon, you can easily add it to both tasks.

class Rake::Task
def overwrite(&block)
@actions.clear
prerequisites.clear
enhance(&block)
end
def abandon
prerequisites.clear
@actions.clear
end
end

8 comments:

  1. I've been thinking about adding an explicit API in rake to do something similar to your overwrite and abandon examples. The thing that makes me pause is that is doesn't play nice with plugins. Its one thing to clear a task as a project owner, since you are assuming responsibility for the entire project. However, as writer of a Rakefile module (such as the .rake files in a rails project), using abandon or overwrite clearly has the potential of overwriting someone else's overwrites. I've not come up with a good solution for that.

    ReplyDelete
  2. Hello Jim, thanks for the comment.

    I think your concern is a valid one, but no different than the same threat that Open Classes pose. With great power...


    I also think you might as well add overwrite and abandon to Rake since the cat's already out of the bag. RSpec wrote their own redefine_task method and after I finished with my blog post, I found someone else who had written their own overwrite method. It's basically public knowledge, so adding it to the framework is the next logic step, IMHO.

    Cheers, Jay

    ReplyDelete
  3. Anonymous11:36 AM

    Awesome post, to this day (more then 2 years of rails dev), I never understood why redefining a rake task didn't _redefine the rake task_. I guess I can see the benefit of appending the behavior. But It's not the first thing I would have thought, thanks!

    ReplyDelete
  4. I've seen a couple people suggest, instead of your abandon(), to basically do: Rake.application.instance_variable_get('@tasks').delete(task_name)

    E.g.
    http://matthewbass.com/2007/03/07/overriding-existing-rake-tasks/
    http://rubyizednrailified.blogspot.com/2008/07/remove-rake-tasks.html

    Do you have thoughts on this versus your approach of clearing the actions from the task?

    Also, in my case I was trying to override rdoctask stuff, which turns out to be a bit trickier. In order to override the standard Rails rdoctask, declared as Rake::RDocTask.new('app') do..., I had to remove the tasks 'doc:app', 'doc:reapp', 'doc:clobber_app', AND 'doc/app/index.html'. There would probably be a nice way to write this all into a call to a new method Rake::RDocTask.remove()...

    ReplyDelete
  5. Peter,
    I'd definitely try to get Jim's opinion since he's Rake's creator. His opinion will be the most important one.

    I'm guessing the 'abandon' method is probably better than screwing around with instance variables, but I honestly have no idea. Maybe the instance variable route is the way to go. This blog post is a bit old, so maybe it's dated.

    Cheers, Jay

    ReplyDelete
  6. Also need to clear @full_comment if you want to abandon the original description.

    class Rake::Task
      def abandon
        @full_comment = nil
        clear_actions
        clear_prerequisites
      end
    end

    Example:

    namespace :db do
      namespace :schema do |schema|
        schema[:dump].abandon
        desc "Nothing because it's too slow in Oracle"
        task :dump => :environment do
          # nothing
        end
      end
    end

    ReplyDelete
  7. Is there any kind of precedence in the "Make" world? Make has been around longer than Rake.

    You wrote "I like this behavior, but sometimes you want to overwrite a task instead of appending to what's already there."

    I don't quite see how the first behaviour is useful. What examples do you have for when it is useful to overwrite a task? To me that would just be a mess.

    Stephan

    ReplyDelete
  8. Now this is part of rake through the clear method:

    task :test do
    puts "first implementation"
    end

    task(:test).clear

    task :test do
    puts "second implementation"
    end

    ReplyDelete

Note: Only a member of this blog may post a comment.