Module: RubyLabs::Source

Defined in:
lib/rubylabs.rb

Overview

Source

The Source module provides access the source code for a lab project. When IRB reads the modules from a file, the source is saved in a global array named SCRIPT_LINES__. The methods defined in this module scan the source code to look for tags that mark the first and last lines of methods described in the book.

A method name can be passed either as a String or a Symbol. For example, to print a listing of the isort method a user can call

Source.listing("isort")

or

Source.listing(:isort)

#– Code that will be accessed by methods in this module should be delimited by :begin and :end tags. See the definition of isort in iterationlab.rb for an example of how to name a method and its helper methods.

Constant Summary collapse

@@probes =
Hash.new
@@file =
Hash.new
@@size =
Hash.new
@@base =
Hash.new
@@helpers =
Hash.new
@@line =
nil

Class Method Summary collapse

Class Method Details

.checkout(name, newfilename = nil) ⇒ Object

Save a copy of the source code for method name in a file. If a file name is not specified, the output file name is the name of the method with “.rb” appended. Prompts the user before overwriting an existing file.

Example – Write the source for the method isort to “isort.rb”:

Source.checkout("isort")

Example – Write the source for isort to “mysort.rb”

Source.checkout("isort", "mysort.rb")

:call-seq:

Source.checkout(name)


478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/rubylabs.rb', line 478

def Source.checkout(name, newfilename = nil)
  begin
    id = Source.find(name)
    if newfilename.nil?
      newfilename = id.to_s
    end
    if newfilename[-1] == ?? || newfilename[1] == ?!
      newfilename.chop!
    end
    if newfilename !~ /\.rb$/
      newfilename += ".rb"
    end
    if File.exists?(newfilename)
      print "Replace existing #{newfilename}? [yn] "
      if STDIN.gets[0] != ?y
        puts "File not written"
        return false
      end
    end
    File.open(newfilename, "w") do |f|
      f.puts "# #{name} method exported from #{File.basename(@@file[id])}"
      f.puts
      Source.print_source(f, id)
      @@helpers[id].each do |m|
        f.puts 
        xid = Source.find(m)
        Source.print_source(f, xid)
      end     
    end
  rescue Exception => e
    puts e
  end
  puts "Saved a copy of source in #{newfilename}"
  return true
end

.clear(name = nil) ⇒ Object

Remove all the probes on a designated method, or if no method name is passed, remove all probes from all methods.

:call-seq:

Source.clear(name)


600
601
602
603
604
605
606
# File 'lib/rubylabs.rb', line 600

def Source.clear(name = nil)
  @@probes.each do |id, plist|
    next if ! name.nil? && id != name
    plist.clear
  end
  return true
end

.find(name) ⇒ Object

Internal use only – locate the filename, starting line number, and length of method name, record the information for any methods that need it. This information only needs to be found once, so it is recorded in a set of class variables. Revisit this decision if monitoring user-defined methods.…



614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'lib/rubylabs.rb', line 614

def Source.find(name)   # :nodoc:
  id = name.to_sym
  return id if @@file[id]     # return if we looked for this source previously
  
  filename, base, size, helpers = nil, nil, nil, nil
  
  catch (:found) do
    SCRIPT_LINES__.each do |file, lines|
      line_num = 0
      lines.each do |s|
        line_num += 1
        if match = s.match(/:(begin|end)\s+(.*)/)
          verb = match[1]
          names = match[2].split.collect{|x| eval(x)}
          if names[0] == id
            if verb == "begin"
              filename = file
              base = line_num + 1
              helpers = names[1..-1]
            else
              size = line_num - base
              throw :found
            end
          end
        end
      end
    end
  end
  
  raise "Can't find method named '#{name}'" if size.nil?

  @@file[id] = filename
  @@size[id] = size
  @@base[id] = base
  @@probes[id] = Hash.new
  @@helpers[id] = helpers
  return id      
end

.info(name) ⇒ Object

Internal use only – show info about method to verify it’s being found by Source.lines



687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/rubylabs.rb', line 687

def Source.info(name)   # :nodoc:
  unless id = Source.find(name)
    puts "Can't find method named '#{name}'"
    return
  end

  printf "file:     %s\n", @@file[id]
  printf "size:     %d\n", @@size[id]
  printf "base:     %d\n", @@base[id]
  printf "helpers:  %s\n", @@helpers[id].inspect
  printf "probes:   %s\n", @@probes[id].inspect
end

.lines(spec, id) ⇒ Object

Internal use only – make an array of line numbers to use for probing method name. Argument can be a single line number, an array of line numbers, or a pattern. Checks to make sure line numbers are valid.



657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
# File 'lib/rubylabs.rb', line 657

def Source.lines(spec, id)    # :nodoc:
  if spec.class == Fixnum && Source.range_check(spec, id)
    return [spec]
  elsif spec.class == Array
    res = Array.new
    spec.each do |line|
      raise "line number must be an integer" unless line.class == Fixnum
      res << line if Source.range_check(line, id)
    end
    return res
  elsif spec.class == String
    res = Array.new
    for i in @@base[id]..(@@base[id]+@@size[id]-1)
      line = SCRIPT_LINES__[@@file[id]][i-1].chomp
      res << i - @@base[id] + 1 if line.index(spec)
    end
    return res
  else
    raise "invalid spec: '#{spec}' (must be an integer, array of integers, or a pattern)"
  end    
end

.listing(name) ⇒ Object

Print a listing (source code along with line numbers) for method name.

:call-seq:

Source.listing(name)


451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/rubylabs.rb', line 451

def Source.listing(name)
  begin
    id = Source.find(name)
    for i in @@base[id]..(@@base[id]+@@size[id]-1)
      line = SCRIPT_LINES__[@@file[id]][i-1].chomp
      printf "%3d:  %s\n", i - @@base[id] + 1, line.gsub(/\t/,"  ")
    end
  rescue Exception => e
    puts e
  end
  return true
end

Helper method called from Source.checkout – print the code for method id in the file f



517
518
519
520
521
522
# File 'lib/rubylabs.rb', line 517

def Source.print_source(f, id)    # :nodoc:
  for i in @@base[id]..(@@base[id]+@@size[id]-1)
    line = SCRIPT_LINES__[@@file[id]][i-1].chomp
    f.puts line.gsub(/\t/,"  ")
  end      
end

.probe(name, spec, expr = :count) ⇒ Object



552
553
554
555
556
557
558
559
560
561
562
563
# File 'lib/rubylabs.rb', line 552

def Source.probe(name, spec, expr = :count)
  begin
    id = Source.find(name)
    Source.lines(spec, id).each do |n|
      n += @@base[id] - 1
      @@probes[id][n] = expr
    end
  rescue Exception => e
    puts e
  end
  return true
end

.probesObject

Print a description of all the currently defined probes.

:call-seq:

Source.probes()


583
584
585
586
587
588
589
590
591
# File 'lib/rubylabs.rb', line 583

def Source.probes
  @@probes.each do |name, plist|
    plist.each do |line, exprs|
      n = line - @@base[name] + 1
      printf "%s %2d: %s\n", name, n, exprs
    end
  end
  return true
end

.probing(filename, method, line) ⇒ Object

– Method for internal use only –

Return probes (if any) attached to the specified file, method, and line number. Intended to be called from a trace func callback (which is why the file is one of the parameters).



570
571
572
573
574
575
# File 'lib/rubylabs.rb', line 570

def Source.probing(filename, method, line)  # :nodoc:
  return nil if line == @@line
  @@line = line
  return nil unless @@probes[method] && @@file[method] == filename
  return @@probes[method][line] 
end

.range_check(n, id) ⇒ Object

:nodoc:



679
680
681
682
683
# File 'lib/rubylabs.rb', line 679

def Source.range_check(n, id)   # :nodoc:
  max = @@size[id]
  raise "line number must be between 1 and #{max}" unless n >= 1 && n <= max
  return true
end