myprecious 0.0.8 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5d502021d1c25f2475553b82cb157470bce16f0a2e0f9680dcfa22a0acd47e6
4
- data.tar.gz: 0bfea940fdaa87306851066fd19a90ff5cb871eb2ad43b221a3c9949086b2c79
3
+ metadata.gz: be0d5a3297a77bea22a403968fa7c3a23f2fd7c2d4ce96b957c832788f29de91
4
+ data.tar.gz: d82b540dbb7be2ffb7601fe571cc07482d2fbe87563b8d78d937bc791bd22d3f
5
5
  SHA512:
6
- metadata.gz: edb4f9229db398ec9705c1a2a8348ee0a5e5ddf181050d5aaf0decb854a102a3996983ba96074af9833734e09bb28345d1edc6711932277e8819c5fd8f5c4527
7
- data.tar.gz: ff0fb7720fa0f961f01a9f48d77b75e958cb6581b54d93c2520d899eb9e0c2abac075fee2322b6f9b79e7c004984da96d7572e69f79250212fd365be96e03980
6
+ metadata.gz: 79e7317fa010abdc6ab9d7207d7be1229d85e2ba2a53cb2225e1a8bed7c95063d51294fab4917f1449e106210e1aa3ef5c52fa743bde9e6fb85f582a6942694a
7
+ data.tar.gz: 0fb7d8ec2c159fe58867b2f5cbbfa95a9a27c7f0e6f020ea2f69fb2ecf55b49b2ed385857c3a6162670c07a39871b54724f2005c71dd6897e691438701d33316
data/bin/myprecious CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'rubygems'
3
4
  require 'myprecious'
4
- puts MyPrecious.update
5
+
6
+ MyPrecious::Program.run(
7
+ on_error: (:exit_program! if ENV['TRACE_ERRORS'].to_s.empty?)
8
+ )
data/lib/myprecious.rb CHANGED
@@ -1,121 +1,550 @@
1
- require 'gems'
2
1
  require 'date'
3
2
  require 'git'
3
+ require 'ostruct'
4
+ require 'pathname'
5
+ require 'rake/toolkit_program'
6
+ require 'uri'
4
7
 
5
- class MyPrecious
6
- def self.update
7
- g = Git.open(Dir.pwd)
8
- repo_name = g.repo.path.split(".git")[0].split("/").last
9
- repo_name = repo_name + "-dependency-tracking"
10
- gem_lines = {}
11
- gem_name_pos = 0
12
- gem_version_pos = 1
13
- gem_latest_pos = 2
14
- gem_date_pos = 3
15
- gem_age_pos = 4
16
- gem_license_pos = 5
17
- gem_change_pos = 6
18
- default_length = 7
19
- already_fetched_gems = {}
20
- if File.file?(repo_name+'.md')
21
- File.open(repo_name+".md", "r").each_with_index do |line, line_number|
22
- #puts line + " dds " + line_number.to_s
23
- word_array = line.split('|')
24
- if line_number == 1
25
- default_length = word_array.size
26
- word_array.each_with_index do |word, index|
27
- if word.strip == 'Gem'
28
- gem_name_pos = index
29
- elsif word.strip == 'Our Version'
30
- gem_version_pos = index
31
- elsif word.strip == 'Latest Version'
32
- gem_latest_pos = index
33
- elsif word.strip == 'Date available'
34
- gem_date_pos = index
35
- elsif word.strip == 'Age (in days)'
36
- gem_age_pos = index
37
- elsif word.strip == 'License Type'
38
- gem_change_pos = index
39
- elsif word.strip == 'Change Log'
40
- gem_change_pos = index
41
- end
8
+ # Declare the module here so it doesn't cause problems for files in the
9
+ # "myprecious" directory (or _does_ cause problems if they try to declare
10
+ # it a class)
11
+ module MyPrecious
12
+ DATA_DIR = Pathname('~/.local/lib/myprecious').expand_path
13
+ ONE_DAY = 60 * 60 * 24
14
+ end
15
+ require 'myprecious/data_caches'
16
+
17
+ module MyPrecious
18
+ extend Rake::DSL
19
+
20
+ Program = Rake::ToolkitProgram
21
+ Program.title = "myprecious Dependecy Reporting Tool"
22
+
23
+ def self.common_program_args(parser, args)
24
+ parser.on(
25
+ '-o', '--out FILE',
26
+ "Output file to generate",
27
+ ) {|fpath| args.output_file = Pathname(fpath)}
28
+
29
+ args.target = Pathname.pwd
30
+ parser.on(
31
+ '-C', '--dir PATH',
32
+ "Path to inspect",
33
+ ) do |fpath|
34
+ fpath = Pathname(fpath)
35
+ parser.invalid_args!("#{fpath} does not exist.") unless fpath.exist?
36
+ args.target = fpath
37
+ end
38
+
39
+ parser.on(
40
+ '--[no-]cache',
41
+ "Control caching of gem information"
42
+ ) {|v| MyPrecious.caching_disabled = v}
43
+ end
44
+
45
+ # Declare the tasks exposed as subcommands in this block
46
+ Program.command_tasks do
47
+ desc "Generate report on Ruby gems"
48
+ task('ruby-gems').parse_args(into: OpenStruct.new) do |parser, args|
49
+ parser.expect_positional_cardinality(0)
50
+ common_program_args(parser, args)
51
+ end
52
+ task 'ruby-gems' do
53
+ require 'myprecious/ruby_gems'
54
+ args = Program.args
55
+ out_fpath = args.output_file || Reporting.default_output_fpath(args.target)
56
+
57
+ col_order = Reporting.read_column_order_from(out_fpath)
58
+
59
+ # Get all gems used via RubyGemInfo.each_gem_used, accumulating version requirements
60
+ gems = RubyGemInfo.accum_gem_lock_info(args.target)
61
+
62
+ out_fpath.open('w') do |outf|
63
+ # Header
64
+ outf.puts "Last updated: #{Time.now.rfc2822}; Use for directional purposes only, this data is not real time and might be slightly inaccurate"
65
+ outf.puts
66
+
67
+ Reporting.header_lines(
68
+ col_order,
69
+ RubyGemInfo.method(:col_title)
70
+ ).each {|l| outf.puts l}
71
+
72
+ # Iterate all gems in name order, pulling column values from the RubyGemInfo objects
73
+ gems.keys.sort_by {|n| n.downcase}.map {|name| gems[name]}.each do |gem|
74
+ Reporting.on_dependency(gem.name) do
75
+ outf.puts col_order.markdown_columns(MarkdownAdapter.new(gem))
42
76
  end
43
- elsif line_number > 2
44
- gem_name_index = word_array[gem_name_pos].strip
45
- #extract just the name of the gem from the first column
46
- #since that column contains a markdown-formatted hyperlink
47
- gem_name_index = gem_name_index[/\[(.*?)\]/,1]
48
- gem_lines[gem_name_index] = line_number
49
77
  end
50
-
51
78
  end
52
- #puts gem_lines
53
- else
54
- File.open(repo_name+'.md', 'w') { |write_file|
55
- write_file.puts "Last updated:" + Date.today.to_s + "; Use for directional purposes only, this data is not real time and might be slightly inaccurate" + "\n\n"
56
- write_file.puts "Gem | Our Version | Latest Version | Date available | Age (in days) | License Type | Change Log"
57
- write_file.puts "--- | --- | --- | --- | --- | --- | ---"
58
- }
79
+
59
80
  end
60
-
61
- file = File.new("Gemfile", "r")
62
-
63
- final_write = File.readlines(repo_name+'.md')
64
-
65
- while (line = file.gets)
66
- gem_line = line.strip
67
- if (gem_line.include? 'gem') && !gem_line.start_with?('#') && !gem_line.start_with?('source')
68
- name = gem_line.split(' ')[1].split(',')[0].tr(" '\"", "")
81
+
82
+ desc "Generate report on Python packages"
83
+ task('python-packages').parse_args(into: OpenStruct.new) do |parser, args|
84
+ parser.expect_positional_cardinality(0)
85
+ common_program_args(parser, args)
86
+
87
+ parser.on(
88
+ '-r', '--requirements-file FILE',
89
+ "requirements.txt-style file to read"
90
+ ) do |file|
91
+ if args.requirements_file
92
+ invalid_args!("Only one requirements file may be specified.")
93
+ end
94
+ args.requirements_file = file
95
+ end
96
+ end
97
+ task 'python-packages' do
98
+ require 'myprecious/python_packages'
99
+ args = Program.args
100
+ out_fpath = args.output_file || Reporting.default_output_fpath(args.target)
101
+
102
+ col_order = Reporting.read_column_order_from(out_fpath)
103
+
104
+ req_file = args.requirements_file
105
+ req_file ||= PyPackageInfo.guess_req_file(args.target)
106
+ req_file = args.target.join(req_file) unless req_file.nil?
107
+ if req_file.nil? || !req_file.exist?
108
+ invalid_args!("Unable to guess requirement file name; specify with '-r FILE' option.")
109
+ end
110
+ pkgs = PyPackageInfo::Reader.new(req_file).each_installed_package
111
+
112
+ out_fpath.open('w') do |outf|
113
+ # Header
114
+ outf.puts "Last updated: #{Time.now.rfc2822}; Use for directional purposes only, this data is not real time and might be slightly inaccurate"
115
+ outf.puts
116
+
117
+ Reporting.header_lines(
118
+ col_order,
119
+ PyPackageInfo.method(:col_title)
120
+ ).each {|l| outf.puts l}
121
+
122
+ pkgs = pkgs.to_a
123
+ pkgs.sort_by! {|pkg| pkg.name.downcase}
124
+ pkgs.each do |pkg|
125
+ Reporting.on_dependency(pkg.name) do
126
+ outf.puts col_order.markdown_columns(MarkdownAdapter.new(pkg))
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ desc "Clear all myprecious data caches"
133
+ task('clear-caches').parse_args(into: OpenStruct.new) do |parser, args|
134
+ args.prompt = true
135
+ parser.on('--[no-]confirm', "Control confirmation prompt") {|v| args.prompt = v}
136
+
137
+ # This option exists to force users to specify the full "--no-confirm"
138
+ parser.on('--no-confir', "Warn about incomplete --no-confirm") do
139
+ parser.invalid_args!("Incomplete --no-confirm flag")
140
+ end
141
+ end
142
+ task 'clear-caches' do
143
+ # Load all "myprecious/*.rb" files so we know where all the caches are
144
+ Dir[File.expand_path('myprecious/*.rb', __dir__)].each {|f| require f}
145
+
146
+ next if Program.args.prompt && !yes_no('Delete all cached data (y/n)?')
147
+ MyPrecious.data_caches.each do |cache|
69
148
  begin
70
- puts name + " is being fetched, and processed"
71
- gems_latest_info = Gems.info name
72
- current_version = Gem::Specification.find_all_by_name(name).max ? Gem::Specification.find_all_by_name(name).max.version : gems_latest_info["version"]
73
-
74
- gems_info = Gems.versions name
75
- latest_build = ''
76
- gems_info.each do |gem_info|
77
- if gem_info["number"].to_s == gems_latest_info["version"].to_s
78
- latest_build = Date.parse gem_info["built_at"]
79
- end
80
- if gem_info["number"].to_s == current_version.to_s && !already_fetched_gems[name]
81
- already_fetched_gems[name] = true
82
- current_build = Date.parse gem_info["built_at"]
83
-
84
- days_complete = latest_build - current_build
85
- #puts name
86
- #puts gem_lines
87
- if gem_lines[name].nil?
88
- array_to_write = Array.new(default_length) { |i| "" }
89
- else
90
- array_to_write = final_write[gem_lines[name]].split('|')
91
- end
92
- array_to_write[gem_name_pos] = "[" + name + "]" + "(" + gems_latest_info["homepage_uri"].to_s + ")"
93
- array_to_write[gem_version_pos] = current_version.to_s
94
- array_to_write[gem_latest_pos] = gems_latest_info["version"].to_s
95
- array_to_write[gem_date_pos] = (latest_build).to_s
96
- array_to_write[gem_age_pos] = days_complete.to_i.to_s
97
- if !gem_info["licenses"].nil?
98
- array_to_write[gem_license_pos] = gem_info["licenses"][0]
99
- else
100
- array_to_write[gem_license_pos] = "N/A"
101
- end
102
- array_to_write[gem_change_pos] = gems_latest_info["changelog_uri"].to_s + "\n"
103
- if !gem_lines[name].nil?
104
- final_write[gem_lines[name]] = array_to_write.join("|")
105
- else
106
- final_write << array_to_write.join("|")
107
- end
108
- end
149
+ rm_r cache
150
+ rescue Errno::ENOENT
151
+ # No problem, we wanted to delete this anyway
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ ##
158
+ # Prompt user for a yes/no answer
159
+ #
160
+ # It doesn't matter if they've redirected STDIN/STDOUT -- this grabs the TTY
161
+ # directly.
162
+ #
163
+ def self.yes_no(prompt)
164
+ Pathname('/dev/tty').open('r+') do |term|
165
+ loop do
166
+ term.write("#{prompt} ")
167
+ case term.gets[0..-2]
168
+ when 'y', 'Y', 'yes', 'Yes', 'YES'
169
+ return true
170
+ when 'n', 'N', 'no', 'No', 'NO'
171
+ return false
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ ##
178
+ # Tool for getting information about the Git repository associated with a
179
+ # directory
180
+ #
181
+ class GitInfoExtractor
182
+ URL_PATTERN = /\/([^\/]+)\.git$/
183
+
184
+ def initialize(dir)
185
+ super()
186
+ @dir = dir
187
+ end
188
+ attr_reader :dir
189
+
190
+ def git_info
191
+ @git_info ||= Git.open(self.dir)
192
+ end
193
+
194
+ def origin_remote
195
+ git_info.remotes.find {|r| r.name == 'origin'}
196
+ end
197
+
198
+ def repo_name
199
+ @repo_name ||= (URL_PATTERN =~ origin_remote.url) && $1
200
+ end
201
+ end
202
+
203
+ ##
204
+ # Common behavior in dependency report generation
205
+ #
206
+ # The methods here are not specific to any language or dependency management
207
+ # framework. They do work with ColumnOrder and an expected set of attributes
208
+ # on the dependency information objects.
209
+ #
210
+ module Reporting
211
+ ##
212
+ # Compute the default output filepath for a directory
213
+ #
214
+ def default_output_fpath(dir)
215
+ dir / (GitInfoExtractor.new(dir).repo_name + "-dependency-tracking.md")
216
+ end
217
+ module_function :default_output_fpath
218
+
219
+ ##
220
+ # Read the column order from the file at the given path
221
+ #
222
+ # If +fpath+ indicates a file that does not exist, return the default
223
+ # ColumnOrder.
224
+ #
225
+ def read_column_order_from(fpath)
226
+ result = ColumnOrder.new
227
+ begin
228
+ prev_line = nil
229
+ fpath.open {|inf| inf.each_line do |line|
230
+ if prev_line && /^-+(?:\|-+)*$/ =~ line.gsub(' ', '')
231
+ result.read_order_from_headers(prev_line)
232
+ break
109
233
  end
110
- rescue Exception => e
111
- puts e
112
- puts name
234
+ prev_line = line
235
+ end}
236
+ rescue Errno::ENOENT
237
+ # No problem
238
+ end
239
+ return result
240
+ end
241
+ module_function :read_column_order_from
242
+
243
+ ##
244
+ # Generate header lines for the output Markdown table
245
+ #
246
+ # Returns an Array of strings, currently two lines giving header text and
247
+ # divider row.
248
+ #
249
+ def header_lines(order, titlizer)
250
+ col_titles = order.map {|c| titlizer.call(c)}
251
+
252
+ # Check that all attributes in #order round-trip through the title name
253
+ order.zip(col_titles) do |attr, title|
254
+ unless title.kind_of?(Symbol) || ColumnOrder.col_from_text_name(title) == attr
255
+ raise "'#{attr}' does not round-trip (rendered as #{result.inspect})"
113
256
  end
114
257
  end
258
+
259
+ return [
260
+ col_titles.join(" | "),
261
+ (["---"] * col_titles.length).join(" | "),
262
+ ]
263
+ end
264
+ module_function :header_lines
265
+ # TODO: Mark dependencies with colors a la https://stackoverflow.com/a/41247934
266
+
267
+ ##
268
+ # Converts an attribute name to the column title for generic attributes
269
+ #
270
+ # Dependency info classes (like RubyGemInfo) can delegate common column
271
+ # title generation to this function.
272
+ #
273
+ def common_col_title(attr)
274
+ case attr
275
+ when :current_version then 'Our Version'
276
+ when :age then 'Age (in days)'
277
+ when :latest_version then 'Latest Version'
278
+ when :latest_released then 'Date Available'
279
+ when :recommended_version then 'Recommended Version'
280
+ when :license then 'License Type'
281
+ when :changelog then 'Change Log'
282
+ when :obsolescence then 'How Bad'
283
+ else
284
+ warn("'#{attr}' column does not have a mapped name")
285
+ attr
286
+ end
287
+ end
288
+ module_function :common_col_title
289
+
290
+ ##
291
+ # Determine obsolescence level from days
292
+ #
293
+ # Returns one of +nil+, +:mild+, +:moderate+, or +:severe+.
294
+ #
295
+ # +at_least_moderate:+ allows putting a floor of +:moderate+ obsolescence
296
+ # on the result.
297
+ #
298
+ def obsolescence_by_age(days, at_least_moderate: false)
299
+ return case
300
+ when days < 270
301
+ at_least_moderate ? :moderate : nil
302
+ when days < 500
303
+ at_least_moderate ? :moderate : :mild
304
+ when days < 730
305
+ :moderate
306
+ else
307
+ :severe
308
+ end
309
+ end
310
+ module_function :obsolescence_by_age
311
+
312
+ ##
313
+ # Wrap output from processing of an individual dependency with intro and
314
+ # outro messages
315
+ #
316
+ def on_dependency(name)
317
+ progress_out = $stdout
318
+ progress_out.puts("--- Reporting on #{name}...")
319
+ yield
320
+ progress_out.puts(" (done)")
321
+ end
322
+ module_function :on_dependency
323
+ end
324
+
325
+ ##
326
+ # Order of columns in a Markdown table
327
+ #
328
+ # Contains the default column ordering when constructed. Columns are
329
+ # identified by the Symbol commonly used as an attribute on a dependency
330
+ # info object (e.g. RubyGemInfo instance). Objects of this class behave
331
+ # to some extent like frozen Array instances.
332
+ #
333
+ class ColumnOrder
334
+ DEFAULT = %i[name current_version age latest_version latest_released recommended_version license changelog].freeze
335
+ COLUMN_FROM_TEXT_NAME = {
336
+ 'gem' => :name,
337
+ 'package' => :name,
338
+ 'module' => :name,
339
+ 'our version' => :current_version,
340
+ 'how bad' => :obsolescence,
341
+ 'latest version' => :latest_version,
342
+ 'date available' => :latest_released,
343
+ 'age (in days)' => :age,
344
+ 'license type' => :license,
345
+ /change ?log/ => :changelog,
346
+ 'recommended version' => :recommended_version,
347
+ }
348
+
349
+ def initialize
350
+ super
351
+ @order = DEFAULT
352
+ end
353
+
354
+ ##
355
+ # Get the +n+-th column attribute Symbol
356
+ #
357
+ def [](n)
358
+ @order[n]
359
+ end
360
+
361
+ def length
362
+ @order.length
363
+ end
364
+
365
+ def each(&blk)
366
+ @order.each(&blk)
367
+ end
368
+ include Enumerable
369
+
370
+ ##
371
+ # Update the column order to match those in the given line
372
+ #
373
+ # Columns not included in the line are appended in the order they
374
+ # appear in the default order.
375
+ #
376
+ def read_order_from_headers(headers_line)
377
+ headers = headers_line.split('|').map {|h| h.strip.squeeze(' ')}
378
+ @order = headers.map {|h| self.class.col_from_text_name(h)}.compact
379
+
380
+ # Add in any missing columns at the end
381
+ @order.concat(DEFAULT - @order)
382
+
383
+ return @order.dup
384
+ end
385
+
386
+ ##
387
+ # Render a line to include in a Markdown table for the given dependency
388
+ #
389
+ # The dependency must know how to respond to (Ruby) messages (i.e.
390
+ # have attributes) for all columns currently included in the order as
391
+ # represented by this instance.
392
+ #
393
+ def markdown_columns(dependency)
394
+ map {|attr| dependency.send(attr)}.join(" | ")
395
+ end
396
+
397
+ ##
398
+ # Given a text name, derive the equivalent column attribute
399
+ #
400
+ def self.col_from_text_name(n)
401
+ n = n.downcase
402
+ entry = COLUMN_FROM_TEXT_NAME.find {|k, v| k === n}
403
+ return entry && entry[1]
404
+ end
405
+ end
406
+
407
+ ##
408
+ # Extension of String that can accomodate some additional commentary
409
+ #
410
+ # The +update_info+ attribute is used to pass information about changes
411
+ # to licensing between the current and recommended version of a dependency,
412
+ # and may be +nil+.
413
+ #
414
+ class LicenseDescription < String
415
+ attr_accessor :update_info
416
+ end
417
+
418
+ ##
419
+ # Dependency info wrapper to generate nice Markdown columns
420
+ #
421
+ # This wrapper takes basic data from the underlying dependency info object
422
+ # and returns enhanced Markdown for selected columns (e.g. +name+).
423
+ #
424
+ class MarkdownAdapter
425
+ def initialize(dep)
426
+ super()
427
+ @dependency = dep
428
+ end
429
+ attr_reader :dependency
430
+
431
+ ##
432
+ # Generate Markdown linking the +name+ to the homepage for the dependency
433
+ #
434
+ def name
435
+ cswatch = begin
436
+ color_swatch + ' '
437
+ rescue StandardError
438
+ ''
439
+ end
440
+ "#{cswatch}[#{dependency.name}](#{dependency.homepage_uri})"
441
+ rescue StandardError
442
+ dependency.name
443
+ end
444
+
445
+ ##
446
+ # Include information about temporal difference between current and
447
+ # recommended versions
448
+ #
449
+ def recommended_version
450
+ recommended_version = dependency.recommended_version
451
+ if dependency.current_version < recommended_version
452
+ span_comment = begin
453
+ if days_newer = dependency.days_between_current_and_recommended
454
+ " -- #{days_newer} days newer"
455
+ else
456
+ ""
457
+ end
458
+ end
459
+ "**#{recommended_version}**#{span_comment}"
460
+ else
461
+ recommended_version
462
+ end
463
+ rescue StandardError
464
+ recommended_version || "(error)"
465
+ end
466
+
467
+ ##
468
+ # Include update info in the license column
469
+ #
470
+ def license
471
+ value = dependency.license
472
+ if value.update_info
473
+ "#{value}<br/>(#{value.update_info})"
474
+ else
475
+ value
476
+ end
477
+ rescue StandardError
478
+ "(error)"
479
+ end
480
+
481
+ ##
482
+ # Render short links for http: or https: changelog URLs
483
+ #
484
+ def changelog
485
+ base_val = begin
486
+ dependency.changelog
487
+ rescue StandardError
488
+ return "(error)"
489
+ end
490
+
491
+ begin
492
+ uri = URI.parse(base_val)
493
+ if ['http', 'https'].include?(uri.scheme)
494
+ return "[on #{uri.hostname}](#{base_val})"
495
+ end
496
+ rescue StandardError
497
+ end
498
+ return base_val
499
+ end
500
+
501
+ def obsolescence
502
+ color_swatch
503
+ rescue StandardError
504
+ ''
505
+ end
506
+
507
+ ##
508
+ # Get a CSS-style hex color code corresponding to the obsolescence of the dependency
509
+ #
510
+ def color
511
+ case dependency.obsolescence
512
+ when :mild then "dde418"
513
+ when :moderate then "f9b733"
514
+ when :severe then "fb0e0e"
515
+ else "4dda1b"
516
+ end
517
+ end
518
+
519
+ ##
520
+ # Markdown for an obsolescence color swatch
521
+ #
522
+ # Sourced from: https://stackoverflow.com/a/41247934
523
+ #
524
+ def color_swatch
525
+ "![##{color}](https://placehold.it/15/#{color}/000000?text=+)"
526
+ end
527
+
528
+ ##
529
+ # Delegate other attribute queries to the base dependency object
530
+ #
531
+ # Errors are caught and rendered as "(error)"
532
+ #
533
+ def method_missing(meth, *args, &blk)
534
+ dependency.send(meth, *args, &blk)
535
+ rescue NoMethodError
536
+ raise
537
+ rescue StandardError
538
+ "(error)"
539
+ end
540
+ end
541
+
542
+ module URIModuleMethods
543
+ def try_parse(s)
544
+ parse(s)
545
+ rescue URI::InvalidURIError
546
+ nil
115
547
  end
116
- File.open(repo_name+'.md', 'w') { |f| f.write(final_write.join) }
117
- file.close
118
548
  end
549
+ URI.extend(URIModuleMethods)
119
550
  end
120
-
121
- MyPrecious.update