cute_print 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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