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)


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
513
514
515
516
517
518
519
# File 'lib/rubylabs.rb', line 485

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)


607
608
609
610
611
612
613
# File 'lib/rubylabs.rb', line 607

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.…



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
652
653
654
655
656
657
658
# File 'lib/rubylabs.rb', line 621

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



694
695
696
697
698
699
700
701
702
703
704
705
# File 'lib/rubylabs.rb', line 694

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.



664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
# File 'lib/rubylabs.rb', line 664

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)


458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/rubylabs.rb', line 458

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



524
525
526
527
528
529
# File 'lib/rubylabs.rb', line 524

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



559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/rubylabs.rb', line 559

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()


590
591
592
593
594
595
596
597
598
# File 'lib/rubylabs.rb', line 590

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).



577
578
579
580
581
582
# File 'lib/rubylabs.rb', line 577

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:



686
687
688
689
690
# File 'lib/rubylabs.rb', line 686

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