myprecious 0.0.8 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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