npsearch 2.0.1 → 2.1.0

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
  SHA1:
3
- metadata.gz: 7ede73d531ae96b5790db5dfe81704e44981470d
4
- data.tar.gz: bbcbcff58473ba403044aded6e2882e150821a7b
3
+ metadata.gz: af22531e55865ab286dd6599917196765d72af12
4
+ data.tar.gz: 5a3bf459332ff8bc70c3c6e431cae9e09fe0494c
5
5
  SHA512:
6
- metadata.gz: 9c2a40a4ee188e2a3c159e66c92e4949b41775684ae4a4d87943e562ca361b2619534cc69d4c1ac6d263467edbb4b08aba982d9d48026d48882720b8e30b9a12
7
- data.tar.gz: 3d258345776914e041dbb5dfd4447b88bd7feb6176a654f147598b41cb4fa011c4051d8934a8fe9df4e7ab324b229cf252d9c6c8cacfc7c9da1a02e34cff8c1d
6
+ metadata.gz: 899fed317d7ceb7a62d52fb2b3e0e24e835f630c058a9dacf05e267019a900c5c2357588e9f1bdd674731eb951a44f16d5898747efb872e6ae1ceaf5efb8acf4
7
+ data.tar.gz: 0aab6be7e635dd63b2e8e2d4d5eda128f7f39b41977f5b08fb090408dd612ac98d41d3fe60a086ea5a16d2cff56bccbce79b8168c3f2af2e3fff222b1657b908
data/.gitignore CHANGED
@@ -15,4 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
- coverage
18
+ coverage
19
+ .temp
data/README.md CHANGED
@@ -32,8 +32,9 @@ NpSearch orders the results based on the following characteristics:
32
32
  ### Installation Requirements
33
33
  * Ruby (>= 2.0.0)
34
34
  * SignalP 4.1 (Available from [here](http://www.cbs.dtu.dk/cgi-bin/nph-sw_request?signalp))
35
- * USearch (Available from [here]())
36
- * A web browser.
35
+ * CD-HIT (Available from [here](http://weizhongli-lab.org/cd-hit/) - Suggested Installation via [Homebrew](http://brew.sh) or [Linuxbrew](http://linuxbrew.sh) - `brew install homebrew/science/cd-hit`)
36
+ * EMBOSS (Available from [here](http://emboss.sourceforge.net) - Suggested Installation via [Homebrew](http://brew.sh) or [Linuxbrew](http://linuxbrew.sh) - `brew install homebrew/science/emboss`)
37
+
37
38
 
38
39
  ## Installation
39
40
  Simply run the following command in the terminal.
data/Rakefile CHANGED
@@ -1,14 +1,23 @@
1
- require 'bundler/gem_tasks'
2
1
  require 'rake/testtask'
3
2
 
4
3
  task default: [:build]
5
- desc 'Installs the ruby gem'
6
- task :build do
7
- exec("gem build np_search.gemspec && gem install ./NpSearch-#{NpSearch::VERSION}.gem")
4
+
5
+ desc 'Builds and installs'
6
+ task install: [:build] do
7
+ require_relative 'lib/npsearch/version'
8
+ sh "gem install ./npsearch-#{NpSearch::VERSION}.gem"
9
+ end
10
+
11
+ desc 'Runs tests, generates documentation, builds gem (default)'
12
+ task build: [:test] do
13
+ sh 'gem build npsearch.gemspec'
8
14
  end
9
15
 
16
+ desc 'Runs tests'
10
17
  task :test do
11
18
  Rake::TestTask.new do |t|
12
- t.pattern = 'test/test_np_search.rb'
19
+ t.libs.push 'lib'
20
+ t.test_files = FileList['test/test_*.rb']
21
+ t.verbose = true
13
22
  end
14
23
  end
data/bin/npsearch CHANGED
@@ -1,56 +1,53 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'optparse'
3
-
4
- require 'npsearch'
5
- require 'npsearch/arg_validator'
6
- require 'npsearch/version'
3
+ require 'English'
4
+ require 'tempfile'
7
5
 
8
6
  opt = {}
9
7
  optparse = OptionParser.new do |opts|
10
8
  opts.banner = <<Banner
9
+ * Description: A tool to identify novel neuropeptides.
11
10
 
12
- * Usage: npsearch [Options] -i [Input File]
13
-
14
- * Mandatory Options:
11
+ * Usage: npsearch [Options] [Input File]
15
12
 
13
+ * Options
16
14
  Banner
17
15
 
18
- opt[:input_file] = nil
19
- opts.on('-i', '--input [file]',
20
- 'Path to the input fasta file') do |f|
21
- opt[:input_file] = f
22
- end
23
-
24
- opts.separator ''
25
- opts.separator '* Optional Options:'
26
-
27
- opt[:signalp_path] = File.join(ENV['HOME'], 'signalp/signalp')
28
- opts.on('-s', '--signalp_path', String,
16
+ opt[:signalp_path] = 'signalp'
17
+ opts.on('-s', '--signalp_path path_to_signalp',
29
18
  'The full path to the signalp script. This can be downloaded from',
30
19
  ' CBS. See https://www.github.com/wurmlab/NpSearch for more',
31
20
  ' information') do |p|
32
21
  opt[:signalp_path] = p
33
22
  end
34
23
 
35
- opt[:usearch_path] = File.join(ENV['HOME'], 'bin/uclust')
36
- opts.on('-u', '--usearch_path', String,
37
- 'The full path to the usearch binary. This script can be downloaded',
38
- ' from .... See https://www.github.com/wurmlab/NpSearch for more',
39
- ' information') do |p|
40
- opt[:usearch_path] = p
24
+ opt[:temp_dir] = File.join(Dir.pwd, '.temp',
25
+ Dir::Tmpname.make_tmpname('', nil))
26
+ opts.on('-d', '--temp_dir path_to_temp_dir',
27
+ 'The full path to the temp dir. NpSearch will create the folder and',
28
+ ' then delete the folder once it has finished using them.',
29
+ ' Default: Hidden folder in the current working dirctory') do |p|
30
+ opt[:temp_dir] = p
41
31
  end
42
32
 
43
33
  opt[:num_threads] = 1
44
- opts.on('-n', '--num_threads', Integer,
34
+ opts.on('-n', '--num_threads num_of_threads', Integer,
45
35
  'The number of threads to use when analysing the input file') do |n|
46
36
  opt[:num_threads] = n
47
37
  end
48
38
 
49
- opt[:orf_min_length] = 10
50
- opts.on('-m', '--orf_min_length N', Integer,
39
+ opt[:min_orf_length] = 30
40
+ opts.on('-m', '--min_orf_length N', Integer,
51
41
  'The minimum length of a potential neuropeptide precursor.',
52
42
  ' Default: 30') do |n|
53
- opt[:orf_min_length] = n
43
+ opt[:min_orf_length] = n
44
+ end
45
+
46
+ opt[:max_seq_length] = 600
47
+ opts.on('-m', '--max_seq_length N', Integer,
48
+ 'The maximum length of a potential neuropeptide precursor.',
49
+ ' Default: 600') do |n|
50
+ opt[:max_seq_length] = n
54
51
  end
55
52
 
56
53
  opts.on('-h', '--help', 'Display this screen') do
@@ -59,16 +56,31 @@ Banner
59
56
  end
60
57
 
61
58
  opts.on('-v', '--version', 'Shows version') do
59
+ require 'npsearch/version'
62
60
  puts NpSearch::VERSION
63
61
  exit
64
62
  end
65
63
  end
66
- optparse.parse!
64
+ begin
65
+ optparse.parse!
66
+ if ARGV.length > 1
67
+ $stderr.puts "Error: It seems that you have #{ARGV.length} input fasta" \
68
+ ' files. Please ensure that you have a single input fasta' \
69
+ " file\n"
70
+ exit 1
71
+ elsif ARGV.empty?
72
+ $stderr.puts optparse
73
+ exit 1
74
+ end
75
+ rescue OptionParser::ParseError
76
+ $stderr.print 'Error: ' + $ERROR_INFO.to_s + "\n"
77
+ exit 1
78
+ end
67
79
 
68
- # Temporary hard coding my defaults...
69
- opt[:num_threads] = 8
70
- opt[:signalp_path] = '/Volumes/Data/data/programs/signalp-4.1/signalp'
71
- opt[:usearch_path] = '/Volumes/Data/data/programs/bin/usearch'
80
+ opt[:input_file] = ARGV[0]
81
+
82
+ require 'npsearch'
83
+ require 'npsearch/arg_validator'
72
84
 
73
85
  NpSearch.init(opt)
74
86
  NpSearch.run
@@ -1,264 +1,93 @@
1
+ require 'bio'
2
+ # Top level module / namespace.
1
3
  module NpSearch
2
- class ArgValidators
3
-
4
-
5
- # Changes the logger level to output extra info when the verbose option is
6
- # true.
7
- def initialize(verbose_opt)
8
- LOG.level = Logger::INFO if verbose_opt == true
9
- end
4
+ # A class that validates the command line opts
5
+ class ArgumentsValidators
6
+ class << self
7
+ def run(opt)
8
+ assert_file_present('input fasta file', opt[:input_file])
9
+ assert_input_file_not_empty(opt[:input_file])
10
+ assert_input_file_probably_fasta(opt[:input_file])
11
+ opt[:type] = assert_input_sequence(opt[:input_file])
12
+ opt[:num_threads] = check_num_threads(opt[:num_threads])
13
+ assert_binaries('SignalP 4.1 Script', opt[:signalp_path])
14
+ opt
15
+ end
10
16
 
11
- # Runs all the arguments method...
12
- def arg(motif, input, output_dir, orf_min_length, extract_orf,
13
- signalp_file, help_banner)
14
- comp_arg(input, motif, output_dir, extract_orf, help_banner)
15
- input_type = guess_input_type(input)
16
- extract_orf_conflict(input_type, extract_orf)
17
- input_sp_file_conflict(input_type, signalp_file)
18
- orf_min_length(orf_min_length)
19
- input_type
20
- end
17
+ private
21
18
 
22
- # Ensures that the compulsory input arguments are supplied...
23
- def comp_arg(input, motif, output_dir, extract_orf, help_banner)
24
- comp_arg_error(motif, 'Query Motif ("-m" option)') if extract_orf == false
25
- comp_arg_error(input, 'Input file ("-i option")')
26
- comp_arg_error(output_dir, 'Output Folder ("-o" option)')
27
- return unless input.nil? || (motif.nil? && extract_orf == false)
28
- puts help_banner
29
- exit
30
- end
19
+ def assert_file_present(desc, file, exit_code = 1)
20
+ return if file && File.exist?(File.expand_path(file))
21
+ $stderr.puts "*** Error: Couldn't find the #{desc}: #{file}."
22
+ exit exit_code
23
+ end
31
24
 
32
- # Ensures that a message is provided for all missing compulsory args.
33
- # Run from comp_arg method
34
- def comp_arg_error(arg, message)
35
- puts 'Usage Error: No ' + message + ' is supplied' if arg.nil?
36
- end
25
+ def assert_input_file_not_empty(file)
26
+ return unless File.zero?(File.expand_path(file))
27
+ $stderr.puts "*** Error: The input_file (#{file})" \
28
+ ' seems to be empty.'
29
+ exit 1
30
+ end
37
31
 
38
- # Guesses the type of data within the input file on the first 100 lines of
39
- # the file (ignores all identifiers (lines that start with a '>').
40
- # It has a 80% threshold.
41
- def guess_input_type(input_file)
42
- input_file_format(input_file)
43
- sequences = []
44
- File.open(input_file, 'r') do |file_stream|
45
- file_stream.readlines[0..100].each do |line|
46
- sequences << line.to_s unless line.match(/^>/)
32
+ def assert_input_file_probably_fasta(file)
33
+ File.open(file, 'r') do |f|
34
+ fasta = (f.readline[0] == '>') ? true : false
35
+ return fasta if fasta
47
36
  end
37
+ $stderr.puts "*** Error: The input_file (#{file})" \
38
+ ' does not seems to be a fasta file.'
39
+ exit 1
48
40
  end
49
- type = Bio::Sequence.new(sequences).guess(0.8)
50
- if type == Bio::Sequence::NA
51
- input_type = 'genetic'
52
- elsif type == Bio::Sequence::AA
53
- input_type = 'protein'
54
- end
55
- input_type
56
- end
57
41
 
58
- # Ensures that the input file a) exists b) is not empty and c) is a fasta
59
- # file. Run from the guess_input_type method.
60
- def input_file_format(input_file)
61
- unless File.exist?(input_file)
62
- fail ArgumentError("Critical Error: The input file '#{input_file}'" \
63
- ' does not exist.')
64
- end
65
- if File.zero?(input_file)
66
- fail ArgumentError("Critical Error: The input file '#{input_file}'" \
67
- ' is empty.')
42
+ def assert_input_sequence(file)
43
+ type = type_of_sequences(file)
44
+ return type unless type.nil?
45
+ $stderr.puts '*** Error: The input files seems to contain a mixture of'
46
+ $stderr.puts ' both protein and nucleotide data.'
47
+ $stderr.puts ' Please correct this and try again.'
48
+ exit 1
68
49
  end
69
- unless File.probably_fasta?(input_file)
70
- fail ArgumentError("Critical Error: The input file '#{input_file}'" \
71
- ' does not seem to be in fasta format. Only' \
72
- ' input files in fasta format are supported.')
73
- end
74
- end
75
-
76
- # Ensures that the extract_orf option is only used with genetic data.
77
- def extract_orf_conflict(input_type, extract_orf)
78
- return unless input_type == 'protein' && extract_orf == true
79
- fail ArgumentError('Usage Error: Conflicting arguments detected:' \
80
- ' Protein data detected within the input file,' \
81
- ' when using the Extract_ORF option (option' \
82
- ' "-e"). This option is only available when' \
83
- ' input file contains genetic data.')
84
- end
85
-
86
- # Ensures that the protein data (or open reading frames) are supplied as
87
- # the input file when the signal p output file is passed.
88
- def input_sp_file_conflict(input_type, signalp_file)
89
- return unless input_type == 'genetic' && !signalp_file.nil?
90
- fail ArgumentError('Usage Error: Conflicting arguments detected' \
91
- ': Genetic data detected within the input file' \
92
- ' when using the Signal P Input Option (Option' \
93
- ' "-s"). The Signal P input Option requires the' \
94
- ' input of two files: the Signal P Script Result' \
95
- ' files (at the "-s" option) and the protein' \
96
- ' data file used to run the Signal P Script.')
97
- end
98
50
 
99
- # Ensures that the ORF minimum length is a number. Any digits after the
100
- # decimal place are ignored.
101
- def orf_min_length(orf_min_length)
102
- return unless orf_min_length.to_i < 1
103
- fail ArgumentError('Usage Error: The Open Reading Frames minimum' \
104
- ' length can only be a full integer.')
105
- end
106
- end
107
-
108
- class Validators
109
- # Checks for the presence of the output directory; if not found, it asks
110
- # the user whether they want to create the output directory.
111
- def output_dir(output_dir)
112
- unless File.directory? output_dir # If output_dir doesn't exist
113
- fail IOError, "\n\nThe output directory deoes not exist\n\n"
114
- end
115
- rescue IOError
116
- puts # a blank line
117
- puts 'The output directory does not exist.'
118
- puts # a blank line
119
- puts "The directory '#{output_dir}' will be created in this location."
120
- puts 'Do you to continue? [y/n]'
121
- print '> '
122
- inp = $stdin.gets.chomp
123
- until inp.downcase == 'n' || inp.downcase == 'y' || inp == ''
124
- puts # a blank line
125
- puts "The input: '#{inp}' is not recognised - 'y' or 'n' are the" \
126
- ' only recognisable inputs.'
127
- puts 'Please try again.'
128
- puts "The directory '#{output_dir}' will be created in this" \
129
- ' location.'
130
- puts 'Do you to continue? [y/n]'
131
- print '> '
132
- inp = $stdin.gets.chomp
51
+ def type_of_sequences(file)
52
+ fasta_content = IO.binread(file)
53
+ # the first sequence does not need to have a fasta definition line
54
+ sequences = fasta_content.split(/^>.*$/).delete_if(&:empty?)
55
+ # get all sequence types
56
+ sequence_types = sequences.collect { |seq| guess_sequence_type(seq) }
57
+ .uniq.compact
58
+ return nil if sequence_types.empty?
59
+ sequence_types.first if sequence_types.length == 1
133
60
  end
134
- if inp.downcase == 'y' || inp == ''
135
- FileUtils.mkdir_p "#{output_dir}"
136
- puts 'Created output directory...'
137
- elsif inp.downcase == 'n'
138
- raise ArgumentError('Critical Error: An output directory is' \
139
- ' required; please create an output directory' \
140
- ' and then try again.')
141
- end
142
- end
143
61
 
144
- # Ensures that the Signal P Script is present. If not found in the home
145
- # directory, it asks the user for its location.
146
- def signalp_dir
147
- signalp_dir = "#{Dir.home}/SignalPeptide"
148
- if File.exist? "#{signalp_dir}/signalp"
149
- signalp_directory = signalp_dir
150
- else
151
- begin
152
- fail IOError('The Signal P Script directory cannot be found at' \
153
- " the following location: '#{signalp_dir}/'.")
154
- rescue IOError
155
- puts # a blank line
156
- puts 'Error: The Signal P Script directory cannot be found at the' \
157
- " following location: '#{signalp_dir}/'."
158
- puts # a blank line
159
- puts 'Please enter the full path or a relative path to the Signal' \
160
- ' P Script directory (i.e. to the folder containing the' \
161
- ' Signal P script). Refer to the online tutorial for more help'
162
- print '> '
163
- inp = $stdin.gets.chomp
164
- until (File.exist? "#{signalp_dir}/signalp") ||
165
- (File.exist? "#{inp}/signalp")
166
- puts # a blank line
167
- puts 'The Signal P directory cannot be found at the following' \
168
- " location: '#{inp}'"
169
- puts 'Please enter the full path or a relative path to the Signal' \
170
- ' Peptide directory again.'
171
- print '> '
172
- inp = $stdin.gets.chomp
173
- end
174
- signalp_directory = inp
175
- puts # a blank line
176
- puts "The Signal P directory has been found at '#{signalp_directory}'"
177
- FileUtils.ln_s "#{signalp_directory}", "#{Dir.home}/SignalPeptide",
178
- force: true
179
- puts # a blank line
180
- end
62
+ def guess_sequence_type(seq)
63
+ # removing non-letter and ambiguous characters
64
+ cleaned_sequence = seq.gsub(/[^A-Z]|[NX]/i, '')
65
+ return nil if cleaned_sequence.length < 10 # conservative
66
+ type = Bio::Sequence.new(cleaned_sequence).guess(0.9)
67
+ type == Bio::Sequence::NA ? :genetic : :protein
181
68
  end
182
- signalp_directory
183
- end
184
69
 
185
- # Ensures that the supported version of the Signal P Script has been linked
186
- # to NpSearch. Run from the 'sp_results' method.
187
- def sp_version(input_file)
188
- File.open(input_file, 'r') do |file_stream|
189
- first_line = file_stream.readline
190
- if first_line.match(/# SignalP-4.1/)
191
- return true
192
- else
193
- return false
70
+ def check_num_threads(num_threads)
71
+ num_threads = Integer(num_threads)
72
+ unless num_threads > 0
73
+ $stderr.puts 'Number of threads can not be lower than 0'
74
+ $stderr.puts 'Changing number of threads to 1'
75
+ num_threads = 1
194
76
  end
77
+ return num_threads unless num_threads > 256
78
+ $stderr.puts "Number of threads set at #{num_threads} is" \
79
+ ' unusually high.'
195
80
  end
196
- end
197
81
 
198
- # Ensures that the critical columns in the tabular results produced by the
199
- # Signal P script are conserved. Run from the 'sp_results' method.
200
- def sp_column(_input_file)
201
- File.open('signalp_out.txt', 'r') do |file_stream|
202
- secondline = file_stream.readlines[1]
203
- row = secondline.gsub(/\s+/m, ' ').chomp.split(' ')
204
- if row[1] != 'name' && row[4] != 'Ymax' && row[5] != 'pos' &&
205
- row[9] != 'D'
206
- return true
207
- else
208
- return false
209
- end
82
+ def assert_binaries(desc, bin)
83
+ return if command?(bin.to_s)
84
+ $stderr.puts "NpSearch is unable to use the #{desc} at #{bin}"
210
85
  end
211
- end
212
86
 
213
- # Ensure that the right version of the Signal P script is used (via
214
- # 'sp_version' Method). If the wrong signal p script has been linked to
215
- # NpSearch, check whether the critical columns in the tabular results
216
- # produced by the Signal P Script are conserved (via 'sp_column'
217
- # Method).
218
- def sp_results(signalp_output_file)
219
- return if sp_version(signalp_output_file)
220
- # i.e. if Signal P is the wrong version
221
- if sp_column(signalp_output_file) # If wrong version but correct columns
222
- puts # a blank line
223
- puts 'Warning: The wrong version of signalp has been linked.' \
224
- ' However, the signal peptide output file still seems to' \
225
- ' be in the right format.'
226
- else
227
- puts # a blank line
228
- puts 'Warning: The wrong version of the signal p has been linked' \
229
- ' and the signal peptide output is in an unrecognised format.'
230
- puts 'Continuing may give you meaningless results.'
231
- end
232
- puts # a blank line
233
- puts 'Do you still want to continue? [y/n]'
234
- print '> '
235
- inp = $stdin.gets.chomp
236
- until inp.downcase == 'n' || inp.downcase == 'y'
237
- puts # a blank line
238
- puts "The input: '#{inp}' is not recognised - 'y' or 'n' are the" \
239
- ' only recognisable inputs.'
240
- puts 'Please try again.'
87
+ # Return `true` if the given command exists and is executable.
88
+ def command?(command)
89
+ system("which #{command} > /dev/null 2>&1")
241
90
  end
242
- if inp.downcase == 'y'
243
- puts 'Continuing.'
244
- elsif inp.downcase == 'n'
245
- fail IOError('Critical Error: NpSearch only supports SignalP 4.1' \
246
- ' (downloadable form CBS) Please ensure the version' \
247
- ' of the signal p script is downloaded.')
248
- end
249
- end
250
-
251
- # Guesses the type of the data in the supplied motif. It ignores all
252
- # non-word characters (e.g. '|' that is used for regex). It has a 90%
253
- # threshold.
254
- def motif_type(motif)
255
- motif_seq = Bio::Sequence.new(motif.gsub(/\W/, ''))
256
- type = motif_seq.guess(0.9)
257
- return unless type.to_s != 'Bio::Sequence::AA'
258
- fail IOError('Critical Error: There seems to be an error in' \
259
- ' processing the motif. Please ensure that the motif' \
260
- ' contains amino acid residues that you wish to search' \
261
- ' for.')
262
91
  end
263
92
  end
264
93
  end
@@ -1,5 +1,6 @@
1
1
  require 'slim'
2
2
 
3
+ # Top level module / namespace.
3
4
  module NpSearch
4
5
  # Class that generates the output
5
6
  class Output
@@ -8,17 +9,17 @@ module NpSearch
8
9
  templates_path = File.expand_path(File.join(__FILE__, '../../../',
9
10
  'templates/contents.slim'))
10
11
  contents_temp = File.read(templates_path)
11
- html_content = Slim::Template.new { contents_temp }.render(NpSearch)
12
- File.open("#{input_file}.out.html", 'w') { |f| f.puts html_content }
12
+ h_content = Slim::Template.new { contents_temp }.render(NpSearch)
13
+ File.open("#{input_file}.npsearch.html", 'w') { |f| f.puts h_content }
13
14
  end
14
15
 
15
16
  def to_fasta(input_file, sorted_sequences, input_type)
16
- File.open("#{input_file}.out.fa", 'w') do |f|
17
+ File.open("#{input_file}.npsearch.fa", 'w') do |f|
17
18
  sorted_sequences.each do |s|
18
19
  if input_type == :protein
19
- f.puts ">#{s.id}\n#{s.signalp}#{s.seq}"
20
+ f.puts ">#{s.defline}\n#{s.signalp}#{s.seq}"
20
21
  elsif input_type == :nucleotide
21
- f.puts ">#{s.id}-(frame:#{s.translated_frame})"
22
+ f.puts ">#{s.defline}-(frame:#{s.translated_frame})"
22
23
  f.puts "#{s.signalp}#{s.seq}"
23
24
  end
24
25
  end
data/lib/npsearch/pool.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
2
  # From http://burgestrand.se/code/ruby-thread-pool/
3
3
  #
4
- # Copyright © 2012, Kim Burgestrand kim@burgestrand.se
4
+ # Copyright 2012, Kim Burgestrand kim@burgestrand.se
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  # of this software and associated documentation files (the "Software"), to deal