cute_print 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.config/cucumber.yml +1 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +6 -0
  5. data/Gemfile +18 -0
  6. data/Gemfile.lock +99 -0
  7. data/LICENSE +20 -0
  8. data/README.md +109 -0
  9. data/Rakefile +14 -0
  10. data/VERSION +1 -0
  11. data/basic101.gemspec +64 -0
  12. data/cute_print.gemspec +96 -0
  13. data/features/.nav +6 -0
  14. data/features/configuring/configure_output.feature +21 -0
  15. data/features/configuring/configure_position_format.feature +30 -0
  16. data/features/configuring/readme.md +1 -0
  17. data/features/configuring/reset_configuration.feature +27 -0
  18. data/features/inspect_call_chain.feature +39 -0
  19. data/features/inspect_objects/inspect.feature +29 -0
  20. data/features/inspect_objects/label_and_inspect.feature +16 -0
  21. data/features/inspect_objects/print_source_location.feature +41 -0
  22. data/features/inspect_objects/readme.md +1 -0
  23. data/features/readme.md +1 -0
  24. data/features/support/env.rb +9 -0
  25. data/features/support/helpers/example.rb +54 -0
  26. data/features/support/helpers/temp_dir.rb +15 -0
  27. data/features/support/step_definitions.rb +23 -0
  28. data/lib/cute_print/configure.rb +37 -0
  29. data/lib/cute_print/core_ext/object.rb +5 -0
  30. data/lib/cute_print/core_ext.rb +1 -0
  31. data/lib/cute_print/default_printer.rb +14 -0
  32. data/lib/cute_print/finds_foreign_caller.rb +19 -0
  33. data/lib/cute_print/mixin.rb +44 -0
  34. data/lib/cute_print/printer.rb +106 -0
  35. data/lib/cute_print/ruby_generator.rb +17 -0
  36. data/lib/cute_print/ruby_parser/block.rb +12 -0
  37. data/lib/cute_print/ruby_parser/method_call.rb +27 -0
  38. data/lib/cute_print/ruby_parser/parsed_code.rb +38 -0
  39. data/lib/cute_print/ruby_parser/wraps_sexp.rb +19 -0
  40. data/lib/cute_print/ruby_parser.rb +60 -0
  41. data/lib/cute_print/stderr_out.rb +13 -0
  42. data/lib/cute_print.rb +26 -0
  43. data/spec/cute_print_spec.rb +69 -0
  44. data/spec/printer_spec.rb +64 -0
  45. data/spec/spec_helper.rb +13 -0
  46. data/spec/support/captures_stderr.rb +5 -0
  47. data/tasks/cucumber.rake +8 -0
  48. data/tasks/default.rake +1 -0
  49. data/tasks/jeweler.rake +29 -0
  50. data/tasks/spec.rake +5 -0
  51. data/tasks/test.rake +2 -0
  52. data/tasks/yard.rake +3 -0
  53. data/test_support/captures_stderr.rb +16 -0
  54. data/test_support/captures_stdout.rb +16 -0
  55. metadata +130 -0
@@ -0,0 +1,9 @@
1
+ require "rspec/expectations"
2
+
3
+ # Instead of letting cucumber load all of the .rb, we configure
4
+ # cucumber to require them (see .config/cucumber.yml).
5
+ glob = File.join(File.dirname(__FILE__), '**/*.rb')
6
+ ruby_paths = Dir[glob].sort
7
+ ruby_paths.each do |path|
8
+ require path
9
+ end
@@ -0,0 +1,54 @@
1
+ require_relative "../../../test_support/captures_stderr.rb"
2
+ require_relative "../../../test_support/captures_stdout.rb"
3
+
4
+ require_relative "temp_dir"
5
+
6
+ class Example
7
+
8
+ include CapturesStderr
9
+ include CapturesStdout
10
+
11
+ def initialize(contents, opts = {})
12
+ @contents = contents
13
+ @filename = opts.fetch(:filename, "example.rb")
14
+ @temp_dir = TempDir.new
15
+ create_file
16
+ end
17
+
18
+ def run
19
+ @stdout = capture_stdout do
20
+ @stderr = capture_stderr do
21
+ load path
22
+ end
23
+ end
24
+ end
25
+
26
+ def stderr
27
+ filter_output(@stderr)
28
+ end
29
+
30
+ def stdout
31
+ filter_output(@stdout)
32
+ end
33
+
34
+ private
35
+
36
+ def create_file
37
+ File.open(path, 'w') do |file|
38
+ file.write @contents
39
+ end
40
+ end
41
+
42
+ def path
43
+ File.join(@temp_dir.path, @filename)
44
+ end
45
+
46
+ def filter_output(output)
47
+ redact_tmp_path(output)
48
+ end
49
+
50
+ def redact_tmp_path(output)
51
+ output.sub(/\/tmp\/.*\//, "/tmp/.../")
52
+ end
53
+
54
+ end
@@ -0,0 +1,15 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+
4
+ require_relative "temp_dir"
5
+
6
+ class TempDir
7
+
8
+ attr_reader :path
9
+
10
+ def initialize
11
+ @path = Dir.mktmpdir
12
+ at_exit { FileUtils.remove_entry(@path) }
13
+ end
14
+
15
+ end
@@ -0,0 +1,23 @@
1
+ require_relative "../../lib/cute_print"
2
+
3
+ Before do
4
+ CutePrint.configure { |c| c.reset }
5
+ end
6
+
7
+ Given(/^a file with:$/) do |contents|
8
+ @example = Example.new(contents)
9
+ @example.run
10
+ end
11
+
12
+ Given(/^a file named "(.*?)" with:$/) do |filename, contents|
13
+ @example = Example.new(contents, :filename => filename)
14
+ @example.run
15
+ end
16
+
17
+ Then(/^stderr should be$/) do |expected|
18
+ expect(@example.stderr).to eq expected
19
+ end
20
+
21
+ Then(/^stdout should be$/) do |expected|
22
+ expect(@example.stdout).to eq expected
23
+ end
@@ -0,0 +1,37 @@
1
+ require "forwardable"
2
+
3
+ module CutePrint
4
+
5
+ class Configure
6
+
7
+ extend Forwardable
8
+
9
+ # Configure an instance of printer.
10
+ # @api private
11
+ # @param [Printer] printer
12
+ # @yield [Configure]
13
+ def initialize(printer)
14
+ @printer = printer
15
+ yield self
16
+ end
17
+
18
+ def_delegator :@printer, :set_defaults, :reset
19
+
20
+ def self.delegate_accessor(name)
21
+ def_delegator :@printer, name
22
+ def_delegator :@printer, "#{name}="
23
+ end
24
+
25
+ # @!attribute [rw] position_format
26
+ # @return [String] The position format
27
+ # @see Printer#position_format
28
+ delegate_accessor :position_format
29
+
30
+ # @!attribute [rw] out
31
+ # @return [#puts] The file to write to
32
+ # @see Printer#out
33
+ delegate_accessor :out
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "../mixin"
2
+
3
+ class Object
4
+ include CutePrint::Mixin
5
+ end
@@ -0,0 +1 @@
1
+ require_relative "core_ext/object"
@@ -0,0 +1,14 @@
1
+ require_relative "printer"
2
+
3
+ module CutePrint
4
+
5
+ # @api private
6
+ class DefaultPrinter
7
+
8
+ def self.printer
9
+ @printer ||= Printer.new
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,19 @@
1
+ module CutePrint
2
+
3
+ # @api private
4
+ module FindsForeignCaller
5
+
6
+ def nearest_foreign_caller
7
+ caller.find do |s|
8
+ path = s.split(":").first
9
+ !File.expand_path(path).include?(File.expand_path(lib_path))
10
+ end
11
+ end
12
+
13
+ def lib_path
14
+ File.dirname(__FILE__)
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "default_printer"
2
+ require_relative "printer"
3
+
4
+ module CutePrint
5
+
6
+ # Methods mixed into Object, and so available globally.
7
+ #
8
+ # @note The methods in this module are part of the public API, but
9
+ # the module itself is not.
10
+
11
+ module Mixin
12
+
13
+ # @see Printer#q
14
+ def q(*args, &block)
15
+ CutePrint::DefaultPrinter.printer.q(*args, &block)
16
+ end
17
+
18
+ # @see Printer#ql
19
+ def ql(*args, &block)
20
+ CutePrint::DefaultPrinter.printer.ql(*args, &block)
21
+ end
22
+
23
+ # @see Printer#qq
24
+ def qq(*args, &block)
25
+ CutePrint::DefaultPrinter.printer.qq(*args, &block)
26
+ end
27
+
28
+ # Debug a call chain by printing self and then returning self.
29
+ # @return [Object] self
30
+ def tapq
31
+ q self
32
+ self
33
+ end
34
+
35
+ # Debug a call chain by printing self, with source position, and
36
+ # then returning self.
37
+ # @return [Object] self
38
+ def tapql
39
+ ql self
40
+ self
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,106 @@
1
+ require_relative "finds_foreign_caller"
2
+ require_relative "ruby_parser"
3
+ require_relative "stderr_out"
4
+
5
+ module CutePrint
6
+ class Printer
7
+
8
+ include FindsForeignCaller
9
+
10
+ DEFAULT_POSITION_FORMAT = "%<filename>s:%<line_number>d: "
11
+
12
+ # The object to write to. Defaults to $stderr.
13
+ # @return [#puts]
14
+ attr_accessor :out
15
+
16
+ # The position format. To format a position, String#% is called
17
+ # with a hash having these keys:
18
+ #
19
+ # * :path
20
+ # * :filename
21
+ # * :line_number
22
+ #
23
+ # The default format prints the filename and line number.
24
+ # @see Printer::DEFAULT_POSITION_FORMAT
25
+ # @return [String]
26
+ attr_accessor :position_format
27
+
28
+ # Create an instance. If attributes are supplied, they override
29
+ # the defaults. For example:
30
+ #
31
+ # CutePrint.new(:out => $stdout)
32
+ #
33
+ # @api private
34
+ def initialize(attrs = {})
35
+ set_defaults
36
+ attrs.each { |name, value| send "#{name}=", value }
37
+ end
38
+
39
+ # Set all attributes to their defaults.
40
+ def set_defaults
41
+ @out = StderrOut.new
42
+ @position_format = DEFAULT_POSITION_FORMAT
43
+ end
44
+
45
+ # Inspect and write one or more objects.
46
+ #
47
+ # If called without a block, prints the inspected arguments, one
48
+ # on a line.
49
+ #
50
+ # If called with a block, prints the source code of the block and
51
+ # the inspected result of the block.
52
+ def q(*values, &block)
53
+ print(__method__, values, block) do |line|
54
+ @out.puts line
55
+ end
56
+ end
57
+
58
+ # Inspect and write one or more objects, with source position.
59
+ #
60
+ # If called without a block, prints the inspected arguments, one
61
+ # on a line.
62
+ #
63
+ # If called with a block, prints the source code of the block and
64
+ # the inspected result of the block.
65
+ def ql(*values, &block)
66
+ path, line_number = nearest_foreign_caller.split(":")
67
+ line_number = line_number.to_i
68
+ print(__method__, values, block) do |line|
69
+ position = format_position(path, line_number)
70
+ @out.puts "#{position}#{line}"
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def print(method, values, block)
77
+ if block && !values.empty?
78
+ raise ArgumentError, "arguments and block are mutually exclusive"
79
+ end
80
+ if block
81
+ ruby_parser = RubyParser.from_block(block)
82
+ parsed_code = ruby_parser.parse
83
+ method_call = parsed_code.first_call_to_method(method)
84
+ block_code = method_call.block.to_ruby
85
+ yield "%s is %s" % [
86
+ block_code,
87
+ block.call.inspect,
88
+ ]
89
+ else
90
+ values.each do |value|
91
+ yield value.inspect
92
+ end
93
+ end
94
+ end
95
+
96
+ def format_position(path, line_number)
97
+ position_values = {
98
+ path: path,
99
+ filename: File.basename(path),
100
+ line_number: line_number,
101
+ }
102
+ @position_format % position_values
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,17 @@
1
+ require "ruby2ruby"
2
+
3
+ require_relative "ruby_generator"
4
+
5
+ module CutePrint
6
+
7
+ # @api private
8
+ class RubyGenerator
9
+
10
+ def self.to_ruby(sexp)
11
+ sexp = sexp.deep_clone
12
+ Ruby2Ruby.new.process(sexp)
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "wraps_sexp"
2
+
3
+ module CutePrint
4
+ class RubyParser
5
+
6
+ # @api private
7
+ class Block
8
+ include WrapsSexp
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "block"
2
+ require_relative "wraps_sexp"
3
+
4
+ module CutePrint
5
+ class RubyParser
6
+
7
+ # @api private
8
+ class MethodCall
9
+
10
+ include WrapsSexp
11
+
12
+ def self.call_to_method?(sexp, method_name)
13
+ call?(sexp) && sexp[1][2] == method_name
14
+ end
15
+
16
+ def self.call?(sexp)
17
+ sexp[0] == :iter && sexp[1][0] == :call
18
+ end
19
+
20
+ def block
21
+ Block.new(@sexp[3])
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ require_relative "method_call"
2
+ require_relative "wraps_sexp"
3
+
4
+ module CutePrint
5
+ class RubyParser
6
+
7
+ # How this class works is cribbed this excellent code:
8
+ #
9
+ # https://github.com/sconover/wrong/blob/30475fc5ac9d0f73135d229b1b44c045156a7e7a/lib/wrong/d.rb
10
+ #
11
+ # @api private
12
+
13
+ class ParsedCode
14
+
15
+ include WrapsSexp
16
+
17
+ def first_call_to_method(method_name)
18
+ MethodCall.new(method_call_node(method_name))
19
+ end
20
+
21
+ private
22
+
23
+ def method_call_node(method_name)
24
+ if MethodCall.call_to_method?(@sexp, method_name)
25
+ return @sexp
26
+ end
27
+ @sexp.each_sexp do |node|
28
+ if MethodCall.call_to_method?(node, method_name)
29
+ return node
30
+ end
31
+ end
32
+ raise "Method call not found"
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "../ruby_generator"
2
+
3
+ module CutePrint
4
+ class RubyParser
5
+
6
+ # @api private
7
+ module WrapsSexp
8
+
9
+ def initialize(sexp)
10
+ @sexp = sexp
11
+ end
12
+
13
+ def to_ruby
14
+ RubyGenerator.to_ruby(@sexp)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ require "ruby_parser"
2
+
3
+ require_relative "ruby_parser/parsed_code"
4
+
5
+ module CutePrint
6
+
7
+ # This class is very much cribbed from this excellent code:
8
+ #
9
+ # https://github.com/sconover/wrong/blob/30475fc5ac9d0f73135d229b1b44c045156a7e7a/lib/wrong/chunk.rb
10
+ #
11
+ # @api private
12
+
13
+ class RubyParser
14
+
15
+ def self.from_block(block)
16
+ path, line_number = block.to_proc.source_location
17
+ new(path, line_number, &block)
18
+ end
19
+
20
+ def initialize(path, line_number, &block)
21
+ @path = path
22
+ @line_number = line_number
23
+ @block = block
24
+ end
25
+
26
+ def parse
27
+ @parsed ||= parse_source(read_source)
28
+ end
29
+
30
+ private
31
+
32
+ def read_source
33
+ File.read(@path)
34
+ end
35
+
36
+ def parser
37
+ @parser ||= ::RubyParser.new
38
+ end
39
+
40
+ # Try parsing just the starting line. While that doesn't work,
41
+ # add another line and try again.
42
+ def parse_source(source)
43
+ lines = source.lines.to_a
44
+ starting_line_index = @line_number - 1
45
+ ending_line_index = starting_line_index
46
+ sexp = nil
47
+ while !sexp && ending_line_index < lines.size
48
+ begin
49
+ snippet = lines.to_a[starting_line_index..ending_line_index].join
50
+ sexp = parser.parse(snippet)
51
+ return ParsedCode.new(sexp)
52
+ rescue Racc::ParseError
53
+ ending_line_index += 1
54
+ raise if ending_line_index >= lines.size
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ module CutePrint
2
+
3
+ # Writing to an instance of this class, rather than directly to
4
+ # $stderr, allows the tests to capture output by assigning to
5
+ # $stderr.
6
+ class StderrOut
7
+
8
+ def puts(*args)
9
+ $stderr.puts(*args)
10
+ end
11
+
12
+ end
13
+ end
data/lib/cute_print.rb ADDED
@@ -0,0 +1,26 @@
1
+ require_relative "cute_print/configure"
2
+ require_relative "cute_print/core_ext"
3
+ require_relative "cute_print/default_printer"
4
+
5
+ # Like Kernel#p, only fancier. For example, this code:
6
+ #
7
+ # require 'cute_print'
8
+ # q { 1 + 2 }
9
+ #
10
+ # prints this to $stderr:
11
+ #
12
+ # (1 + 2) is 3
13
+ module CutePrint
14
+
15
+ # Configure the library. For example:
16
+ #
17
+ # CutePrint.configure do |c|
18
+ # c.out = $stdout
19
+ # end
20
+ #
21
+ # @yieldparam config [Configure]
22
+ def self.configure(&block)
23
+ Configure.new(DefaultPrinter.printer, &block)
24
+ end
25
+
26
+ end
@@ -0,0 +1,69 @@
1
+ require_relative "spec_helper"
2
+
3
+ require "stringio"
4
+
5
+ require "cute_print"
6
+
7
+ # Test the library as the user uses it. The other specs test
8
+ # internals.
9
+
10
+ describe CutePrint do
11
+
12
+ before(:each) do
13
+ CutePrint.configure { |c| c.reset }
14
+ end
15
+
16
+ describe "#q" do
17
+ When(:stderr) do
18
+ capture_stderr do
19
+ q 123
20
+ end
21
+ end
22
+ Then { stderr == "123\n" }
23
+ end
24
+
25
+ describe "#ql" do
26
+ When(:stderr) do
27
+ capture_stderr do
28
+ @location = [File.basename(__FILE__), __LINE__ + 1].join(":")
29
+ ql 123
30
+ end
31
+ end
32
+ Then { stderr == "#{@location}: 123\n" }
33
+ end
34
+
35
+ describe "#tapq" do
36
+ When do
37
+ @stderr = capture_stderr do
38
+ @result = ["1", "2"].map(&:to_i).tapq.inject(:+)
39
+ end
40
+ end
41
+ Then { @result == 3}
42
+ Then { @stderr == "[1, 2]\n" }
43
+ end
44
+
45
+ describe "#tapql" do
46
+ When do
47
+ @stderr = capture_stderr do
48
+ @location = [File.basename(__FILE__), __LINE__ + 1].join(":")
49
+ @result = ["1", "2"].map(&:to_i).tapql.inject(:+)
50
+ end
51
+ end
52
+ Then { @result == 3}
53
+ Then { @stderr == "#{@location}: [1, 2]\n" }
54
+ end
55
+
56
+ describe 'configure output' do
57
+ Given(:io) { StringIO.new }
58
+ Given do
59
+ CutePrint.configure do |c|
60
+ c.out = io
61
+ end
62
+ end
63
+ When do
64
+ q 123
65
+ end
66
+ Then { io.string == "123\n" }
67
+ end
68
+
69
+ end
@@ -0,0 +1,64 @@
1
+ require_relative "spec_helper"
2
+
3
+ require "cute_print/printer"
4
+
5
+ module CutePrint
6
+
7
+ describe Printer do
8
+
9
+ describe "#q" do
10
+
11
+ context "single value" do
12
+ Given(:out) { StringIO.new }
13
+ Given(:printer) { Printer.new(:out => out) }
14
+ When { printer.q [1, 2] }
15
+ Then { out.string == "[1, 2]\n" }
16
+ end
17
+
18
+ context "multiple values" do
19
+ Given(:out) { StringIO.new }
20
+ Given(:printer) { Printer.new(:out => out) }
21
+ When { printer.q 1, 2 }
22
+ Then { out.string == "1\n2\n" }
23
+ end
24
+
25
+ context "arguments and closure" do
26
+ Given(:out) { StringIO.new }
27
+ Given(:printer) { Printer.new(:out => out) }
28
+ When(:result) { printer.q("foo") {1 + 2} }
29
+ Then { result == Failure(ArgumentError) }
30
+ end
31
+
32
+ context "closure (one line)" do
33
+ Given(:out) { StringIO.new }
34
+ Given(:printer) { Printer.new(:out => out) }
35
+ When { printer.q {1 + 2} }
36
+ Then { out.string == "(1 + 2) is 3\n" }
37
+ end
38
+
39
+ context "closure (two lines)" do
40
+ Given(:out) { StringIO.new }
41
+ Given(:printer) { Printer.new(:out => out) }
42
+ When do
43
+ printer.q do
44
+ (1 + 2)
45
+ end
46
+ end
47
+ Then { out.string == "(1 + 2) is 3\n" }
48
+ end
49
+
50
+ end
51
+
52
+ describe "#ql" do
53
+ Given(:out) { StringIO.new }
54
+ Given(:printer) { Printer.new(:out => out) }
55
+ When do
56
+ @location = [File.basename(__FILE__), __LINE__ + 1].join(":")
57
+ printer.ql [1, 2]
58
+ end
59
+ Then { out.string == "#{@location}: [1, 2]\n" }
60
+ end
61
+
62
+ end
63
+
64
+ end