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.
- data/.autotest +9 -0
- data/.gemtest +1 -0
- data/HISTORY.rdoc +4 -0
- data/Manifest.txt +26 -0
- data/README.rdoc +175 -0
- data/Rakefile +33 -0
- data/examples/alert_frequency.rb +101 -0
- data/examples/at_exit.rb +68 -0
- data/examples/caller_context_padding.rb +97 -0
- data/lib/deprecatable.rb +106 -0
- data/lib/deprecatable/alerter.rb +162 -0
- data/lib/deprecatable/call_site.rb +91 -0
- data/lib/deprecatable/call_site_context.rb +112 -0
- data/lib/deprecatable/deprecated_method.rb +210 -0
- data/lib/deprecatable/options.rb +138 -0
- data/lib/deprecatable/registry.rb +55 -0
- data/lib/deprecatable/util.rb +26 -0
- data/test/helpers.rb +18 -0
- data/test/test_deprecatable.rb +149 -0
- data/test/test_deprecatable_alerter.rb +41 -0
- data/test/test_deprecatable_call_site.rb +28 -0
- data/test/test_deprecatable_call_site_context.rb +57 -0
- data/test/test_deprecatable_deprecated_method.rb +61 -0
- data/test/test_deprecatable_options.rb +83 -0
- data/test/test_deprecatable_registry.rb +32 -0
- data/test/test_deprecatable_util.rb +13 -0
- metadata +146 -0
data/lib/deprecatable.rb
ADDED
@@ -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
|