deprecatable 1.0.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.
@@ -0,0 +1,106 @@
1
+ require 'deprecatable/options'
2
+ require 'deprecatable/registry'
3
+ require 'deprecatable/alerter'
4
+
5
+ # Allow methods to be deprecated and record and alert when those
6
+ # deprecated methods are called.
7
+ #
8
+ # There are configurable options for the extended class:
9
+ #
10
+ # For example:
11
+ #
12
+ # class Foo
13
+ # extend Deprecatable
14
+ #
15
+ # def bar
16
+ # ...
17
+ # end
18
+ #
19
+ # deprecate :bar, :message => "Foo#bar has been deprecated, use Foo#foo instead"
20
+ #
21
+ # end
22
+ #
23
+ module Deprecatable
24
+
25
+ VERSION = '1.0.0'
26
+
27
+ # Public: Deprecate a method in the included class.
28
+ #
29
+ # method_name - The method in this class to deprecate.
30
+ # options - a hash of the current understood options (default: {})
31
+ # :message - A String to output along with the rest of
32
+ # the notifcations about the deprecated
33
+ # method.
34
+ # :removal_date - The date on which the deprecated method
35
+ # will be removed.
36
+ # :removal_version - The version on which the deprecated
37
+ # method will be removed.
38
+ #
39
+ # returns the instance of DeprecatedMethod created to track this deprecation.
40
+ def deprecate( method_name, options = {} )
41
+ file, line = Util.location_of_caller
42
+ dm = DeprecatedMethod.new( self, method_name, file, line, options )
43
+
44
+ Deprecatable.registry.register( dm )
45
+
46
+ return dm
47
+ end
48
+
49
+ # The global Deprecatable::Registry instance. It is set here so it is
50
+ # allocated at parse time.
51
+ @registry = Deprecatable::Registry.new
52
+
53
+ # Public: Get the global Deprecatable::Registry instance
54
+ #
55
+ # Returns the global Deprecatable::Registry instance.
56
+ def self.registry
57
+ @registry
58
+ end
59
+
60
+ # The global options for Deprecatable. It is set here so it is allocated at
61
+ # parse time.
62
+ @options = Deprecatable::Options.new
63
+
64
+ # Public: Access the global Options
65
+ #
66
+ # Returns the global Deprecatable::Options instance.
67
+ def self.options
68
+ @options
69
+ end
70
+
71
+ # The global Alerter for Deprecatable. It is set here so it is allocated at
72
+ # parse time.
73
+ @alerter = Deprecatable::Alerter.new
74
+
75
+ # Public: Access the global Alerter
76
+ #
77
+ # Returns the global Alerter instance
78
+ def self.alerter
79
+ @alerter
80
+ end
81
+
82
+ # Public: Set the global Alerter
83
+ #
84
+ # alerter - Generally an instance of Alerter, but may be anything that
85
+ # responds_to? both :alert and :report. See the Alerter
86
+ # documetation for more information
87
+ #
88
+ # Returns nothing.
89
+ def self.alerter=( a )
90
+ @alerter = a
91
+ end
92
+ end
93
+
94
+ require 'deprecatable/util'
95
+ require 'deprecatable/call_site_context'
96
+ require 'deprecatable/call_site'
97
+ require 'deprecatable/deprecated_method'
98
+
99
+ # The at_exit handler is set at all times, and it will always fire, unless the
100
+ # process is killed with prejudice and/or the ruby process exists using 'exit!'
101
+ # instead of the normal 'exit'
102
+ at_exit do
103
+ if ::Deprecatable.options.has_at_exit_report? then
104
+ ::Deprecatable.alerter.final_report
105
+ end
106
+ end
@@ -0,0 +1,162 @@
1
+ require 'stringio'
2
+ module Deprecatable
3
+ # An Alerter formats and emits alerts, and formats and emits reports.
4
+ #
5
+ # If you wish to impelement your own Alerter class, then it must implement
6
+ # the following methods:
7
+ #
8
+ # * alert( DeprecatedMethod, CallSite )
9
+ # * final_reort()
10
+ #
11
+ # These are the two methods that are invoked by the Deprecatable system at
12
+ # various points.
13
+ #
14
+ class Alerter
15
+ # Public: Alert that the deprecated method was invoked at a specific call
16
+ # site.
17
+ #
18
+ # deprecated_method - an instance of DeprecatedMethod
19
+ # call_site - an instance of CallSite showing this particular
20
+ # invocation
21
+ #
22
+ # Returns nothing.
23
+ def alert( deprecated_method, call_site )
24
+ lines = deprecated_method_report( deprecated_method, call_site )
25
+ lines << "To turn this report off do one of the following:"
26
+ lines << "* in your ruby code set `Deprecatable.options.alert_frequency = :never`"
27
+ lines << "* set the environment variable `DEPRECATABLE_ALERT_FREQUENCY=\"never\"`"
28
+ lines << ""
29
+ lines.each { |l| warn_with_prefix l }
30
+ end
31
+
32
+ # Public: Render the final deprecation report showing when and where all
33
+ # deprecated methods in the Registry were calld.
34
+ #
35
+ # registry - An instance of Deprecatable::Registry
36
+ # (default: Deprecatable.registry)
37
+ #
38
+ # Returns nothing.
39
+ def final_report( registry = Deprecatable.registry )
40
+ lines = [ "Deprecatable 'at_exit' Report",
41
+ "=============================" ]
42
+ lines << ""
43
+ lines << "To turn this report off do one of the following:"
44
+ lines << ""
45
+ lines << "* in your ruby code set `Deprecatable.options.has_at_exit_report = false`"
46
+ lines << "* set the environment variable `DEPRECATABLE_HAS_AT_EXIT_REPORT=\"false\"`"
47
+ lines << ""
48
+
49
+ registry.items.each do |dm|
50
+ lines += deprecated_method_report( dm )
51
+ end
52
+ lines.each { |l| warn_without_prefix l }
53
+ end
54
+
55
+ ###################################################################
56
+ private
57
+ ###################################################################
58
+
59
+ # Format a report of the data in a DeprecatedMethod
60
+ #
61
+ # dm - A DeprecatedMethod instance
62
+ # call_site - A CallSite instance (default :nil)
63
+ #
64
+ # Returns an Array of Strings which are the lines of the report.
65
+ def deprecated_method_report( dm, call_site = nil )
66
+ m = "`#{dm.klass}##{dm.method}`"
67
+ lines = [ m ]
68
+ lines << "-" * m.length
69
+ lines << ""
70
+ lines << "* Originally defined at #{dm.file}:#{dm.line_number}"
71
+
72
+ if msg = dm.message then
73
+ lines << "* #{msg}"
74
+ end
75
+ if rd = dm.removal_date then
76
+ lines << "* Will be removed after #{rd}"
77
+ end
78
+
79
+ if rv = dm.removal_version then
80
+ lines << "* Will be removed in version #{rv}"
81
+ end
82
+ lines << ""
83
+
84
+ if call_site then
85
+ lines += call_site_report( call_site )
86
+ else
87
+ dm.call_sites.each do |cs|
88
+ lines += call_site_report( cs, true )
89
+ end
90
+ end
91
+ return lines
92
+ end
93
+
94
+ # Format a report about a CallSite
95
+ #
96
+ # cs - A CallSite instance
97
+ # include_count - Should the report include the invocation count from the
98
+ # CallSite instance. (default: false)
99
+ #
100
+ # Returns an Array of Strings which are the lines of the report.
101
+ def call_site_report( cs, include_count = false )
102
+ header = [ "Called" ]
103
+ header << "#{cs.invocation_count} time(s)" if include_count
104
+ header << "from #{cs.file}:#{cs.line_number}"
105
+
106
+ lines = [ header.join(' ') ]
107
+ lines << ""
108
+ cs.formatted_context_lines.each do |l|
109
+ lines << " #{l.rstrip}"
110
+ end
111
+ lines << ""
112
+ return lines
113
+ end
114
+
115
+ # Emit a warning message without a prefix to the message.
116
+ #
117
+ # Returns nothing.
118
+ def warn_without_prefix( msg = "" )
119
+ warn msg
120
+ end
121
+
122
+ # Emit a warning message WITH a prefix to the message.
123
+ #
124
+ # Returns nothing.
125
+ def warn_with_prefix( msg = "" )
126
+ warn "DEPRECATION WARNING: #{msg}"
127
+ end
128
+
129
+ # Emit a warning message.
130
+ #
131
+ # Returns nothing.
132
+ def warn( msg )
133
+ Kernel.warn( msg )
134
+ end
135
+ end
136
+
137
+ # StringIOAlerter is used to capture all alerts in an instance of StringIO
138
+ # instead of emitting them as Ruby warnings. This is mainly used in testing,
139
+ # and may have uses in other situations too.
140
+ class StringIOAlerter < Alerter
141
+ # Initialize the StringIOAlerter
142
+ #
143
+ # Returns nothing.
144
+ def initialize
145
+ @stringio = StringIO.new
146
+ end
147
+
148
+ # Capture the warning into the StringIO instance
149
+ #
150
+ # Returns nothing.
151
+ def warn( msg )
152
+ @stringio.puts msg
153
+ end
154
+
155
+ # Access the contens of the internal StringIO instance.
156
+ #
157
+ # Returns a String containing all the warnings so far.
158
+ def to_s
159
+ @stringio.string
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,91 @@
1
+ require 'deprecatable/call_site_context'
2
+ module Deprecatable
3
+ # CallSite represents a location in the source code where a DeprecatedMethod
4
+ # was invoked. It contains the location of the call site, the number of times
5
+ # that it was invoked, and an extraction of the source code around the
6
+ # invocation site
7
+ class CallSite
8
+ # Generate the hash key for a call site with the given file and line
9
+ # number.
10
+ #
11
+ # file - A String that is the filesystem path to a file.
12
+ # line_number - An Integer that is the line number in the given file.
13
+ #
14
+ # Returns a String that is generally used as a unique key.
15
+ def self.gen_key( file, line_number )
16
+ "#{file}:#{line_number}"
17
+ end
18
+
19
+ # Public: Get the fully expand path of the file of the CallSite
20
+ #
21
+ # Returns the String filesystem path of the file.
22
+ attr_reader :file
23
+
24
+ # Public: Get the line number of the CallSite in the file.
25
+ # Line numbers start at 1.
26
+ #
27
+ # Returns the line number of a line in the file.
28
+ attr_reader :line_number
29
+
30
+ # Public: Gets the number of lines before and after the line_nubmer
31
+ # to also capture when gettin the context.
32
+ #
33
+ # This number is the number both before AND after 'line_number' to
34
+ # capture. If this number is 2, then the total number of lines captured
35
+ # should be 5. 2 before, the line in question, and 2 after.
36
+ #
37
+ # Returns the number of lines
38
+ attr_reader :context_padding
39
+
40
+ # Public: The number of times this CallSite has been invoked.
41
+ #
42
+ # Returns the Integer number of times this call site has been invoked.
43
+ attr_reader :invocation_count
44
+
45
+ # Create a new instance of CallSite
46
+ #
47
+ # file - A String pathname of the file where the CallSite
48
+ # happend
49
+ # line_number - The Integer line number in the file.
50
+ # context_padding - The Integer number of lines both before and after
51
+ # the 'line_nubmer' to capture.
52
+ def initialize( file, line_number, context_padding )
53
+ @file = File.expand_path( file )
54
+ @line_number = line_number
55
+ @context_padding = context_padding
56
+ @invocation_count = 0
57
+ end
58
+
59
+ # The unique identifier of this CallSite.
60
+ #
61
+ # Returns the String key of this CallSite.
62
+ def key
63
+ CallSite.gen_key( file, line_number )
64
+ end
65
+
66
+ # Increment the invocation count by the amount given
67
+ #
68
+ # count - The amount to increment the invocation count by
69
+ # This should rarely, if ever be set.
70
+ # (default: 1)
71
+ #
72
+ # Returns the Integer invocation count.
73
+ def increment_invocation_count( count = 1 )
74
+ @invocation_count += count
75
+ end
76
+
77
+ # Retrieve the lazily loaded CallSiteContext.
78
+ #
79
+ # Returns an instances of CallSiteContext
80
+ def context
81
+ @context ||= CallSiteContext.new( @file, @line_number, @context_padding )
82
+ end
83
+
84
+ # Access the lines of the context in a nicely formatted way.
85
+ #
86
+ # Returns an Array of Strings containing the formatted context.
87
+ def formatted_context_lines
88
+ context.formatted_context_lines
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,112 @@
1
+ module Deprecatable
2
+ # CallSiteContext captures the actual file context of the call site.
3
+ # It goes to the source file and extracts the lines around the line in
4
+ # question with the given padding and keeps it available for emitting
5
+ class CallSiteContext
6
+ # Public: The raw lines from the source file containing the context.
7
+ #
8
+ # Returns an Array of Strings of the lines from the file.
9
+ attr_reader :context_lines
10
+
11
+ # Public: The raw line numbers from the source file. The lines of
12
+ # a source file start with 1. This is a parallel array to 'context_lines'
13
+ #
14
+ # Returns an Array of Integers of the line numbers from the flie.
15
+ attr_reader :context_line_numbers
16
+
17
+ # The marker used to prefix the formatted context line of the exact line of
18
+ # the context where the CallSite took place
19
+ #
20
+ # Returns a String.
21
+ def self.pointer
22
+ "--->"
23
+ end
24
+
25
+ # The prefix to put in front of the CallSite context padding lines.
26
+ #
27
+ # Returns a String of blanks the same length as 'pointer'
28
+ def self.not_pointer
29
+ " " * pointer.length
30
+ end
31
+
32
+ # Create a new CallSiteContext. Upon instantiation, this will go to the
33
+ # source file in question, and extract the CallSite line and a certain
34
+ # number of 'padding' lines around it.
35
+ #
36
+ # file - The String pathname of the file from which to extract lines.
37
+ # line_number - The 1 indexed line number within the file to be the center
38
+ # of the extracted context.
39
+ # padding - The Number of lines before and after 'line_number' to
40
+ # extract along with the text at 'line_number'
41
+ #
42
+ # Returns nothing.
43
+ def initialize( file, line_number, padding )
44
+ @file = file
45
+ @line_number = line_number
46
+ @padding = padding
47
+
48
+ @context_line_numbers = []
49
+ @context_lines = []
50
+ @context_index = @padding + 1
51
+ @formatted_context_lines = []
52
+
53
+ extract_context()
54
+ end
55
+
56
+ # Nicely format the context lines extracted from the file.
57
+ #
58
+ # Returns an Array of Strings containing the formatted lines.
59
+ def formatted_context_lines
60
+ if @formatted_context_lines.empty? then
61
+ number_width = ("%d" % @context_line_numbers.last).length
62
+ @context_lines.each_with_index do |line, idx|
63
+ prefix = (idx == @context_index) ? CallSiteContext.pointer : CallSiteContext.not_pointer
64
+ number = ("%d" % @context_line_numbers[idx]).rjust( number_width )
65
+ @formatted_context_lines << "#{prefix} #{number}: #{line}"
66
+ end
67
+ end
68
+ return @formatted_context_lines
69
+ end
70
+
71
+ ###########################################################################
72
+ private
73
+ ###########################################################################
74
+
75
+ # Extract the context from the source file. This goes to the file in
76
+ # question, and extracts the line_number and the padding lines both
77
+ # before and after the line_number. If the padding would cause the context
78
+ # to go before the first line of the file, or after the last line of the
79
+ # file, the padding is truncated accordingly.
80
+ #
81
+ # The result of this operation is the setting of many instance
82
+ # variables
83
+ #
84
+ # @context_lines - An Array of String containging the line_number
85
+ # line from the file and the 'padding' lines
86
+ # before and after it.
87
+ # @context_index - The index into @context_lines of the
88
+ # line_number line from the file
89
+ # @context_line_numbers - An Array of Integers that paralles
90
+ # @context_lines contianing the 1 indexed line
91
+ # numbers from the file corresponding to the lines
92
+ # in @context_lines.
93
+ #
94
+ # Returns nothing.
95
+ def extract_context
96
+ if File.readable?( @file ) then
97
+ file_lines = IO.readlines( @file )
98
+ @line_index = @line_number - 1
99
+
100
+ start_line = @line_index - @padding
101
+ start_line = 0 if start_line < 0
102
+
103
+ stop_line = @line_index + @padding
104
+ stop_line = (file_lines.size - 1) if stop_line >= file_lines.size
105
+
106
+ @context_index = @line_index - start_line
107
+ @context_line_numbers = (start_line+1..stop_line+1).to_a
108
+ @context_lines = file_lines[start_line, @context_line_numbers.size]
109
+ end
110
+ end
111
+ end
112
+ end