padrino-core 0.16.1 → 0.17.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/padrino-core/application/application_setup.rb +1 -0
  3. data/lib/padrino-core/application/routing.rb +10 -6
  4. data/lib/padrino-core/cli/base.rb +9 -2
  5. data/lib/padrino-core/cli/binstub.rb +1 -1
  6. data/lib/padrino-core/cli/console.rb +1 -1
  7. data/lib/padrino-core/cli/launcher.rb +3 -3
  8. data/lib/padrino-core/cli/rake.rb +0 -2
  9. data/lib/padrino-core/cli/rake_tasks.rb +14 -4
  10. data/lib/padrino-core/loader.rb +2 -2
  11. data/lib/padrino-core/logger.rb +63 -14
  12. data/lib/padrino-core/mounter/application_extension.rb +1 -1
  13. data/lib/padrino-core/mounter.rb +22 -10
  14. data/lib/padrino-core/path_router/compiler.rb +1 -1
  15. data/lib/padrino-core/reloader.rb +91 -3
  16. data/lib/padrino-core/server.rb +3 -3
  17. data/lib/padrino-core/version.rb +1 -1
  18. data/padrino-core.gemspec +1 -0
  19. data/test/fixtures/apps/external_constants/app.rb +7 -0
  20. data/test/fixtures/apps/external_constants/helpers/external_constants_helpers.rb +4 -0
  21. data/test/fixtures/dependencies/autoload/a.rb +5 -0
  22. data/test/fixtures/dependencies/autoload/b.rb +2 -0
  23. data/test/fixtures/dependencies/autoload/lazy.rb +2 -0
  24. data/test/fixtures/dependencies/gc/t.rb +5 -0
  25. data/test/fixtures/dependencies/gc/u.rb +3 -0
  26. data/test/fixtures/dependencies/gc/v.rb +10 -0
  27. data/test/fixtures/dependencies/gc/w.rb +2 -0
  28. data/test/fixtures/dependencies/gc/x.rb +2 -0
  29. data/test/fixtures/dependencies/gc/y.rb +2 -0
  30. data/test/fixtures/dependencies/parent/admin.rb +5 -0
  31. data/test/fixtures/dependencies/parent/trackable.rb +2 -0
  32. data/test/fixtures/dependencies/parent/user.rb +2 -0
  33. data/test/fixtures/dependencies/partial/a.rb +3 -0
  34. data/test/fixtures/dependencies/partial/b.rb +2 -0
  35. data/test/fixtures/dependencies/reload/forced.rb +1 -0
  36. data/test/fixtures/dependencies/reload/untracked.rb +3 -0
  37. data/test/fixtures/dependencies/reload/untracked_forced.rb +3 -0
  38. data/test/fixtures/dependencies/untracked/a.rb +2 -0
  39. data/test/fixtures/dependencies/untracked/b.rb +2 -0
  40. data/test/helper.rb +21 -0
  41. data/test/test_binstub.rb +21 -0
  42. data/test/test_cli.rb +110 -0
  43. data/test/test_dependencies.rb +163 -0
  44. data/test/test_launcher.rb +51 -0
  45. data/test/test_logger.rb +136 -5
  46. data/test/test_mounter.rb +17 -0
  47. data/test/test_rake_tasks.rb +37 -0
  48. data/test/test_reloader_complex.rb +3 -0
  49. data/test/test_reloader_external.rb +3 -0
  50. data/test/test_reloader_simple.rb +67 -0
  51. data/test/test_reloader_system.rb +32 -0
  52. data/test/test_routing.rb +62 -1
  53. data/test/test_server.rb +32 -0
  54. metadata +70 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d26e98619afc016e402bb8adc53a86eb524582b6649c83617f7b0a47ed0f4488
4
- data.tar.gz: c116e7d488a8c4172430cff76190a4efdfe4c742b849f59cf2c4759df4dca7e6
3
+ metadata.gz: 44a11b5c0a503449fdce5f86ea6ceff3c6fe7f33e7d28223983b8507c1975aca
4
+ data.tar.gz: b3dcecde6843769f9b74336e1b94d1ecbd5cd2d2ae607d5982c2380be1452085
5
5
  SHA512:
6
- metadata.gz: c77acb81e7f6fab90e9ef7c0d7fc42a84c7e06c90067a0983bca0ceb155312d334a8b8969e22b491e8a39b183a48746c4beb29d1c7f82e9279476daf2999bbe0
7
- data.tar.gz: 81f16ab2aa09134872e15b6e45a0e465da6f171a960eb5957b3b099f107441409822454a48a22083fb99c3d0ac8aac5d9e3169899827bec116fb012c854f12ae
6
+ metadata.gz: dbad97028d154c692aa99e3f9d7b68301c69da4843e905cbafbd59bd2b24026186efe0eaadcdb0df0bc2aff14d803952d507efa5153988d5ede8b29a7d2de60e
7
+ data.tar.gz: f9dca7338b15c2276b2546afc826f9e4c39eb2ef8f226dc90fa2666d4ce3b3a2814230aea1f23f4e99985ae45f76bd8c5068e4c5918f8c7aa5fb5b11522aa3d5
@@ -121,6 +121,7 @@ module Padrino
121
121
  def default_routes
122
122
  configure :development do
123
123
  get '*__sinatra__/:image.png' do
124
+ halt 404 unless %w[404 500].include?(params[:image])
124
125
  content_type :png
125
126
  send_file("#{__dir__}/../images/#{params[:image]}.png")
126
127
  end
@@ -341,7 +341,7 @@ module Padrino
341
341
  params = args.last.is_a?(Hash) ? args.pop : {}
342
342
  fragment = params.delete(:fragment) || params.delete(:anchor)
343
343
  path = make_path_with_params(args, value_to_param(params))
344
- rebase_url(fragment ? path << '#' << fragment.to_s : path)
344
+ rebase_url(fragment ? "#{path}##{fragment}" : path)
345
345
  end
346
346
  alias url_for url
347
347
 
@@ -372,7 +372,7 @@ module Padrino
372
372
 
373
373
  def rebase_url(url)
374
374
  if url.start_with?('/')
375
- new_url = ''
375
+ new_url = String.new
376
376
  new_url << conform_uri(ENV['RACK_BASE_URI']) if ENV['RACK_BASE_URI']
377
377
  new_url << conform_uri(uri_root) if defined?(uri_root)
378
378
  new_url << url
@@ -599,12 +599,14 @@ module Padrino
599
599
  # path i.e :index or :show
600
600
  if path.is_a?(Symbol)
601
601
  name = path
602
- path = map&.dup || (path == :index ? '/' : path.to_s)
602
+ path = map&.dup || (path == :index ? '/' : path.to_s.dup)
603
603
  end
604
604
 
605
605
  # Build our controller
606
606
  controller = Array(@_controller).map(&:to_s)
607
607
 
608
+ path = path.dup if path.is_a?(String)
609
+
608
610
  case path
609
611
  when String # path i.e "/index" or "/show"
610
612
  # Now we need to parse our 'with' params
@@ -860,8 +862,10 @@ module Padrino
860
862
  def static_file?(path_info)
861
863
  return unless (public_dir = settings.public_folder)
862
864
  public_dir = File.expand_path(public_dir)
863
- path = File.expand_path(public_dir + unescape(path_info))
864
- return unless path.start_with?(public_dir)
865
+ path = "#{public_dir}#{unescape(path_info)}"
866
+ return unless Rack::Utils.valid_path?(path)
867
+ path = File.expand_path(path)
868
+ return unless path.start_with?("#{public_dir}/")
865
869
  return unless File.file?(path)
866
870
  path
867
871
  end
@@ -963,7 +967,7 @@ module Padrino
963
967
  verb = request.request_method
964
968
  candidacies, allows = routes.partition { |route| route.verb == verb }
965
969
  if candidacies.empty?
966
- response['Allows'] = allows.map(&:verb).join(', ')
970
+ response['Allow'] = allows.map(&:verb).uniq.join(', ')
967
971
  halt 405
968
972
  end
969
973
  end
@@ -35,7 +35,13 @@ module Padrino
35
35
  if defined? Pry
36
36
  Pry.start
37
37
  else
38
- require 'irb'
38
+ begin
39
+ require 'irb'
40
+ rescue LoadError => e
41
+ raise unless e.path == 'irb'
42
+
43
+ abort "<= padrino console needs irb. Since Ruby 4.0 it is a bundled gem: add gem 'irb' to your Gemfile and run bundle install."
44
+ end
39
45
  begin
40
46
  require 'irb/completion'
41
47
  rescue LoadError
@@ -55,7 +61,7 @@ module Padrino
55
61
  require 'padrino-gen/command'
56
62
  ARGV.shift
57
63
  ARGV << 'help' if ARGV.empty?
58
- Padrino.bin_gen(*ARGV)
64
+ exit 1 unless Padrino.bin_gen(*ARGV)
59
65
  rescue StandardError
60
66
  puts '<= You need padrino-gen! Run: gem install padrino-gen'
61
67
  end
@@ -76,6 +82,7 @@ module Padrino
76
82
  abort 'Please specify code or file' if code_or_file.nil?
77
83
 
78
84
  require File.expand_path('config/boot.rb')
85
+ Padrino.mounted_apps.each(&:setup_application!)
79
86
 
80
87
  if File.exist?(code_or_file)
81
88
  eval(File.read(code_or_file), nil, code_or_file)
@@ -21,7 +21,7 @@ module Padrino
21
21
  if File.file?(binstub)
22
22
  exec Gem.ruby, binstub, *ARGV
23
23
  else
24
- puts 'Please run `bundle install --binstubs` from your project root to generate bundle-specific executables'
24
+ puts 'Please run `bundle install && bundle binstubs --all` from your project root to generate bundle-specific executables'
25
25
  exit!
26
26
  end
27
27
  end
@@ -16,5 +16,5 @@ end
16
16
  # Load apps
17
17
  Padrino.mounted_apps.each do |app|
18
18
  puts "=> Loading Application #{app.app_class}"
19
- app.app_obj.setup_application!
19
+ app.setup_application!
20
20
  end
@@ -18,7 +18,7 @@ module Padrino
18
18
  method_option :pid, type: :string, aliases: '-i', desc: 'File to store pid.'
19
19
  method_option :debug, type: :boolean, desc: 'Set debugging flags.'
20
20
  method_option :options, type: :array, aliases: '-O', desc: <<~DESC.chomp
21
- --options NAME=VALUE NAME2=VALUE2'. pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$PROGRAM_NAME} --server_options
21
+ --options NAME=VALUE NAME2=VALUE2. pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$PROGRAM_NAME} start --server_options'
22
22
  DESC
23
23
  method_option :server_options, type: :boolean, desc: "Tells the current server handler's options that can be used with --options"
24
24
 
@@ -48,7 +48,7 @@ module Padrino
48
48
  # https://github.com/rack/rack/blob/master/lib/rack/server.rb\#L100
49
49
  def server_options(options)
50
50
  info = []
51
- server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
51
+ server = Rackup::Handler.get(options[:server]) || Rackup::Handler.default
52
52
  if server.respond_to?(:valid_options)
53
53
  info << ''
54
54
  info << "Server-specific options for #{server.name}:"
@@ -62,7 +62,7 @@ module Padrino
62
62
  return '' unless has_options
63
63
  end
64
64
  info.join("\n")
65
- rescue NameError
65
+ rescue NameError, LoadError
66
66
  "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
67
67
  end
68
68
 
@@ -13,8 +13,6 @@ module PadrinoTasks
13
13
  def self.init(init = false)
14
14
  Padrino::Tasks.files.flatten.uniq.each do |rakefile|
15
15
  Rake.application.add_import(rakefile)
16
- rescue StandardError
17
- puts "<= Failed load #{ext}"
18
16
  end
19
17
 
20
18
  load(File.expand_path('rake_tasks.rb', __dir__))
@@ -1,17 +1,27 @@
1
+ # Model paths that :skeleton left out, loaded by a later :environment in the same run
2
+ # (e.g. `rake ar:migrate db:seed`).
3
+ # ceiling: they load after config/apps.rb and the after_load hooks, so a hook that walks the models
4
+ # doesn't see them in that case; loading them before the hooks would mean re-running Padrino.load!.
5
+ skipped_model_paths = []
6
+
1
7
  # Loads the Padrino applications mounted within the project.
2
8
  # Setting up the required environment for Padrino.
3
9
  task :environment do
4
10
  require File.expand_path('config/boot.rb', Rake.application.original_dir)
5
-
6
- Padrino.mounted_apps.each do |app|
7
- app.app_obj.setup_application!
11
+ unless skipped_model_paths.empty?
12
+ Padrino.dependency_paths.concat(skipped_model_paths)
13
+ Padrino.require_dependencies(*skipped_model_paths)
14
+ skipped_model_paths.clear
8
15
  end
16
+
17
+ Padrino.mounted_apps.each(&:setup_application!)
9
18
  end
10
19
 
11
20
  task :skeleton do
12
21
  PADRINO_ROOT ||= Rake.application.original_dir
13
22
  require 'padrino-core'
14
- Padrino.send(:dependency_paths).reject! { |path| path.include?('/models/') }
23
+ skipped_model_paths.concat(Padrino.dependency_paths.select { |path| path.include?('/models/') })
24
+ Padrino.dependency_paths.reject! { |path| path.include?('/models/') }
15
25
  require File.expand_path('config/boot.rb', Rake.application.original_dir)
16
26
  end
17
27
 
@@ -31,7 +31,7 @@ module Padrino
31
31
  #
32
32
  # @example
33
33
  # after_load do
34
- # DataMapper.finalize
34
+ # SomeORM.finalize
35
35
  # end
36
36
  #
37
37
  def after_load(&block)
@@ -150,7 +150,7 @@ module Padrino
150
150
  error = fatal = loaded = nil
151
151
 
152
152
  files.dup.each do |file|
153
- Reloader.safe_load(file, options)
153
+ Reloader.enabled? || options[:force] ? Reloader.safe_load(file, options) : Reloader.untracked_load(file)
154
154
  files.delete(file)
155
155
  loaded = true
156
156
  rescue NameError, LoadError => error
@@ -30,6 +30,10 @@ module Padrino
30
30
  #
31
31
  # @example using ruby default logger
32
32
  # require 'logger'
33
+ # Padrino.logger = ::Logger.new(STDOUT)
34
+ #
35
+ # @example using ruby default logger with Padrino formatting and #colorize!
36
+ # require 'logger'
33
37
  # new_logger = ::Logger.new(STDOUT)
34
38
  # new_logger.extend(Padrino::Logger::Extensions)
35
39
  # Padrino.logger = new_logger
@@ -85,8 +89,8 @@ module Padrino
85
89
  if args.size > 1
86
90
  bench(args[0], args[1], args[2], name)
87
91
  else
88
- location = resolve_source_location(caller(1).shift)
89
- args.unshift(location) if location && enable_source_location?
92
+ location = resolve_source_location(caller(1).shift) if enable_source_location?
93
+ args.unshift(location) if location
90
94
  push(args * '', name)
91
95
  end
92
96
  end
@@ -94,7 +98,7 @@ module Padrino
94
98
  define_method(:"#{name}?") { number >= level }
95
99
  end
96
100
 
97
- SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/
101
+ SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in [`'].+?')?$/
98
102
 
99
103
  ##
100
104
  # Returns true if :source_location is set to true.
@@ -111,7 +115,11 @@ module Padrino
111
115
  return unless path && line
112
116
 
113
117
  root = Padrino.root
114
- path = File.realpath(path) if Pathname.new(path).relative?
118
+ if Pathname.new(path).relative?
119
+ return unless File.exist?(path) # code from irb, eval or ruby -e has no file
120
+
121
+ path = File.realpath(path)
122
+ end
115
123
  return unless path.start_with?(root) && !path.start_with?(Padrino.root('vendor'))
116
124
 
117
125
  "[#{path.gsub("#{root}/", '')}:#{line}] "
@@ -240,6 +248,33 @@ module Padrino
240
248
  end
241
249
  end
242
250
 
251
+ ##
252
+ # Plain versions of the Padrino-specific methods of {Extensions}. A logger assigned
253
+ # without {Extensions} gets the ones it lacks, so Padrino can log to it while its
254
+ # standard logging methods stay untouched.
255
+ #
256
+ module Fallbacks
257
+ def devel(*); end
258
+
259
+ def devel?
260
+ false
261
+ end
262
+
263
+ def bench(action, began_at, message, level = :debug, _color = :yellow)
264
+ return unless Levels.key?(level)
265
+
266
+ public_send level, "#{action.to_s.upcase} (#{Kernel.format('%0.4fs', Time.now - began_at)}) #{message}"
267
+ end
268
+
269
+ def exception(boom, verbosity = :long, level = :error)
270
+ Extensions.instance_method(:exception).bind(self).call(boom, verbosity, level)
271
+ end
272
+
273
+ def colorize(string, *_colors)
274
+ string
275
+ end
276
+ end
277
+
243
278
  module Colorize
244
279
  # Colors for levels
245
280
  unless defined?(ColoredLevels)
@@ -332,16 +367,25 @@ module Padrino
332
367
  end
333
368
 
334
369
  def self.logger=(logger)
335
- unless logger.class.ancestors.include?(Padrino::Logger::Extensions)
336
- warn <<~MESSAGE
337
- WARNING! `Padrino.logger = new_logger` no longer extends it with #colorize! and other features.
338
- To do it with a custom logger you have to manually `new_logger.extend(Padrino::Logger::Extensions)`
339
- before passing to `Padrino.logger = new_logger`.
340
- MESSAGE
341
- end
370
+ add_fallbacks(logger) unless logger.is_a?(Padrino::Logger::Extensions)
342
371
  @_logger = logger
343
372
  end
344
373
 
374
+ ##
375
+ # Defines the {Fallbacks} methods a logger does not have yet.
376
+ #
377
+ def self.add_fallbacks(logger)
378
+ return if logger.nil?
379
+
380
+ missing = Fallbacks.instance_methods.reject { |name| logger.respond_to?(name) }
381
+ return if missing.empty?
382
+ return warn("WARNING! Padrino.logger is frozen and lacks #{missing.join(', ')}, logging from Padrino will fail.") if logger.frozen?
383
+
384
+ fallbacks = ::Module.new
385
+ missing.each { |name| fallbacks.send(:define_method, name, Fallbacks.instance_method(name)) }
386
+ logger.extend(fallbacks)
387
+ end
388
+
345
389
  ##
346
390
  # Setup a new logger.
347
391
  #
@@ -443,7 +487,11 @@ module Padrino
443
487
  return unless @buffer.size.positive?
444
488
  self.class.mutex.synchronize do
445
489
  @buffer.each do |line|
446
- line.encode!(@sanitize_encoding, invalid: :replace, undef: :replace) if @sanitize_encoding
490
+ if @sanitize_encoding
491
+ line = line.encode(@sanitize_encoding, invalid: :replace, undef: :replace)
492
+ elsif line.encoding != Encoding::UTF_8
493
+ line = line.encode(@log.external_encoding || Encoding::UTF_8, invalid: :replace, undef: :replace)
494
+ end
447
495
  @log.write(line)
448
496
  end
449
497
  @buffer.clear
@@ -475,7 +523,7 @@ module Padrino
475
523
  # The message
476
524
  #
477
525
  def <<(message = nil)
478
- message << "\n" unless message[-1] == "\n"
526
+ message += "\n" unless message[-1] == "\n"
479
527
  self.class.mutex.synchronize { @buffer << message }
480
528
  flush if @auto_flush
481
529
  message
@@ -483,7 +531,8 @@ module Padrino
483
531
  alias write <<
484
532
 
485
533
  def format(message, level)
486
- Kernel.format(@format_message, stylized_level(level), colorize(Time.now.strftime(@format_datetime), :yellow), message.to_s.strip)
534
+ datetime = @format_datetime.empty? ? '' : Time.now.strftime(@format_datetime)
535
+ Kernel.format(@format_message, stylized_level(level), colorize(datetime, :yellow), message.to_s.strip)
487
536
  end
488
537
 
489
538
  ##
@@ -25,7 +25,7 @@ module Padrino
25
25
  end
26
26
 
27
27
  def app_name
28
- @__app_name ||= mounter_options[:app_name] || self.to_s.underscore.to_sym
28
+ @__app_name ||= mounter_options[:app_name] || Inflections.underscore(self.to_s).to_sym
29
29
  end
30
30
 
31
31
  def setup_application!
@@ -87,17 +87,13 @@ module Padrino
87
87
  end
88
88
 
89
89
  ##
90
- # Maps Padrino application onto a Padrino::Router.
91
- # For use in constructing a Rack application.
92
- #
93
- # @param [Padrino::Router]
94
- #
95
- # @return [Padrino::Router]
90
+ # Applies the mount settings (uri_root, root, public folder, cascade) to the
91
+ # application and sets it up, so its routes, controllers and mailers are loaded.
96
92
  #
97
93
  # @example
98
- # @app.map_onto(router)
94
+ # @app.setup_application!
99
95
  #
100
- def map_onto(router)
96
+ def setup_application!
101
97
  app_data = self
102
98
  app_obj = @app_obj
103
99
 
@@ -117,7 +113,22 @@ module Padrino
117
113
  app_obj.public_folder = Padrino.root('public', uri_root) unless public_folder_exists
118
114
  end
119
115
  app_obj.setup_application! # Initializes the app here with above settings.
120
- router.map(to: app_obj, path: uri_root, host: app_data.app_host)
116
+ end
117
+
118
+ ##
119
+ # Maps Padrino application onto a Padrino::Router.
120
+ # For use in constructing a Rack application.
121
+ #
122
+ # @param [Padrino::Router]
123
+ #
124
+ # @return [Padrino::Router]
125
+ #
126
+ # @example
127
+ # @app.map_onto(router)
128
+ #
129
+ def map_onto(router)
130
+ setup_application!
131
+ router.map(to: app_obj, path: uri_root, host: app_host)
121
132
  end
122
133
 
123
134
  ###
@@ -183,7 +194,8 @@ module Padrino
183
194
  def locate_app_object
184
195
  @_app_object ||= begin
185
196
  ensure_app_file!
186
- Padrino.require_dependencies(app_file)
197
+ # Always tracked, since Application#reload! must re-run the app file.
198
+ Reloader.safe_load(app_file) if File.file?(app_file)
187
199
  app_constant
188
200
  end
189
201
  end
@@ -105,7 +105,7 @@ module Padrino
105
105
  # Decode uri escape sequences
106
106
  #
107
107
  def decode_uri(string)
108
- string.split(/%2F|%2f/, -1).map { |part| Rack::Utils.unescape(part) }.join('%2F')
108
+ string.split(/%2F|%2f/, -1).map { |part| Rack::Utils.unescape(part.gsub('+', '%2B')) }.join('%2F')
109
109
  end
110
110
  end
111
111
  end
@@ -27,6 +27,17 @@ module Padrino
27
27
  @_exclude ||= Set.new(%w[test spec tmp features config public db].map { |path| Padrino.root(path) })
28
28
  end
29
29
 
30
+ ##
31
+ # Tracking files for reloading slows down boot, so it's only enabled in development
32
+ # by default. Files loaded while it's disabled are never reloaded. To reload code in
33
+ # other environments, set +Padrino::Reloader.enabled = true+ before +Padrino.load!+.
34
+ #
35
+ attr_writer :enabled
36
+
37
+ def enabled?
38
+ @enabled.nil? ? Padrino.env == :development : @enabled
39
+ end
40
+
30
41
  ##
31
42
  # Specified constants can be excluded from the code unloading process.
32
43
  #
@@ -48,6 +59,7 @@ module Padrino
48
59
  def reload!
49
60
  rotation do |file|
50
61
  next unless file_changed?(file)
62
+ next skip_untracked(file) if untracked_files.include?(file)
51
63
  reload_special(file) || reload_regular(file)
52
64
  end
53
65
  end
@@ -57,6 +69,7 @@ module Padrino
57
69
  #
58
70
  def clear!
59
71
  MTIMES.clear
72
+ untracked_files.clear
60
73
  Storage.clear!
61
74
  end
62
75
 
@@ -87,6 +100,7 @@ module Padrino
87
100
  began_at = Time.now
88
101
  file = figure_path(file)
89
102
  return unless options[:force] || file_changed?(file)
103
+ return skip_untracked(file) if untracked_files.include?(file)
90
104
  return require(file) if feature_excluded?(file)
91
105
 
92
106
  Storage.prepare(file) # might call #safe_load recursively
@@ -95,6 +109,7 @@ module Padrino
95
109
  with_silence { require(file) }
96
110
  Storage.commit(file)
97
111
  update_modification_time(file)
112
+ untracked_files.delete(file)
98
113
  rescue Exception => e
99
114
  unless options[:cyclic]
100
115
  logger.exception e, :short
@@ -105,13 +120,33 @@ module Padrino
105
120
  end
106
121
  end
107
122
 
123
+ ##
124
+ # A Kernel::require for when tracking is disabled. Only the modification time is
125
+ # recorded, so later changes are detected but the file is not reloaded.
126
+ #
127
+ def untracked_load(file)
128
+ file = figure_path(file)
129
+ features = $LOADED_FEATURES.dup
130
+ begin
131
+ with_silence { require(file) }
132
+ rescue Exception
133
+ rollback_untracked(file, $LOADED_FEATURES - features)
134
+ raise
135
+ end
136
+ return if feature_excluded?(file) || !File.file?(file)
137
+ update_modification_time(file)
138
+ untracked_files << file
139
+ end
140
+
108
141
  ##
109
142
  # Removes the specified class and constant.
110
143
  #
111
144
  def remove_constant(const)
112
145
  return if constant_excluded?(const)
113
146
  base, _, object = const.to_s.rpartition('::')
114
- base = base.empty? ? Object : base.constantize
147
+ base = base.empty? ? Object : Inflections.constantize(base)
148
+ return if base.autoload?(object, false) || !base.const_defined?(object, false)
149
+ return unless base.const_get(object, false).equal?(const)
115
150
  base.send :remove_const, object
116
151
  logger.devel "Removed constant #{const} from #{base}"
117
152
  rescue NameError
@@ -153,6 +188,39 @@ module Padrino
153
188
  file
154
189
  end
155
190
 
191
+ ##
192
+ # Files loaded while tracking was disabled.
193
+ #
194
+ def untracked_files
195
+ @untracked_files ||= Set.new
196
+ end
197
+
198
+ ##
199
+ # Like Storage.rollback, removes classes defined by a file that failed partway and by
200
+ # the app files it required, so they can be required again. Only the failure pays for
201
+ # walking Object.constants, which keeps boot as fast as a plain require.
202
+ #
203
+ def rollback_untracked(file, features)
204
+ features = features.reject { |feature| feature_excluded?(feature) || untracked_files.include?(feature) }
205
+ features.each { |feature| $LOADED_FEATURES.delete(feature) }
206
+ files = Set.new(features << file)
207
+ Object.constants.each do |name|
208
+ next if Object.autoload?(name)
209
+ next unless files.include?(Object.const_source_location(name)&.first)
210
+ klass = Object.const_get(name)
211
+ remove_constant(klass) if klass.is_a?(Class)
212
+ end
213
+ end
214
+
215
+ ##
216
+ # Warns that a changed file can't be reloaded because it was loaded without tracking.
217
+ #
218
+ def skip_untracked(file)
219
+ logger.warn "Skipping reload of #{file}, it was loaded with Padrino::Reloader disabled. " \
220
+ 'Set Padrino::Reloader.enabled = true before Padrino.load! to reload it'
221
+ update_modification_time(file)
222
+ end
223
+
156
224
  ##
157
225
  # Reloads the file if it's special. For now it's only I18n locale files.
158
226
  #
@@ -244,21 +312,41 @@ module Padrino
244
312
  # Tells if a constant should be excluded from Reloader routines.
245
313
  #
246
314
  def constant_excluded?(const)
247
- external_constant?(const) || (exclude_constants - include_constants).any? { |excluded_constant| constant_name(const).start_with?(excluded_constant) }
315
+ return true if external_constant?(const)
316
+ name = constant_name(const)
317
+ (exclude_constants - include_constants).any? do |excluded|
318
+ excluded = excluded.to_s
319
+ name == excluded || name.start_with?("#{excluded}::")
320
+ end
248
321
  end
249
322
 
250
323
  ##
251
324
  # Tells if a constant is defined only outside of Padrino project path.
252
325
  # If a constant has any methods defined inside of the project path it's
253
326
  # considered internal and will be included in further testing.
327
+ # A constant without methods (an exception class, say) is judged by
328
+ # where its name was defined instead.
254
329
  #
255
330
  def external_constant?(const)
256
331
  sources = object_sources(const)
257
- # consider methodless constants not external
332
+ sources = [constant_source(const)].compact if sources.empty?
333
+ # consider methodless constants of unknown origin not external
258
334
  return false if sources.empty?
259
335
  sources.none? { |source| source.start_with?(Padrino.root) }
260
336
  end
261
337
 
338
+ ##
339
+ # Returns the file in which the constant's name was defined, if known.
340
+ #
341
+ def constant_source(const)
342
+ base, _, object = constant_name(const).to_s.rpartition('::')
343
+ return if object.empty?
344
+ base = base.empty? ? Object : Inflections.constantize(base)
345
+ base.const_source_location(object)&.first
346
+ rescue NameError
347
+ nil
348
+ end
349
+
262
350
  ##
263
351
  # Gets all the sources in which target class is defined.
264
352
  #
@@ -1,7 +1,7 @@
1
1
  module Padrino
2
2
  ##
3
- # Runs the Padrino apps as a self-hosted server using:
4
- # thin, mongrel, or WEBrick in that order.
3
+ # Runs the Padrino apps as a self-hosted server using the first available
4
+ # handler in Server::Handlers (thin, puma, falcon, ..., WEBrick).
5
5
  #
6
6
  # @example
7
7
  # Padrino.run! # with these defaults => host: '127.0.0.1', port: '3000', adapter: the first found
@@ -31,7 +31,7 @@ module Padrino
31
31
  DEFAULT_ADDRESS = { Host: '127.0.0.1', Port: 3000 }
32
32
 
33
33
  # Server Handlers
34
- Handlers = %i[thin puma spider-gazelle mongrel trinidad webrick]
34
+ Handlers = %i[thin puma falcon spider-gazelle mongrel trinidad webrick]
35
35
 
36
36
  # Starts the application on the available server with specified options.
37
37
  def self.start(app, options = {})
@@ -6,7 +6,7 @@
6
6
  #
7
7
  module Padrino
8
8
  # The version constant for the current version of Padrino.
9
- VERSION = '0.16.1' unless defined?(Padrino::VERSION)
9
+ VERSION = '0.17.0' unless defined?(Padrino::VERSION)
10
10
 
11
11
  #
12
12
  # The current Padrino version.
data/padrino-core.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.require_paths = ['lib']
22
22
  s.rdoc_options = ['--charset=UTF-8']
23
23
 
24
+ s.add_dependency('ostruct', '>= 0.6')
24
25
  s.add_dependency('padrino-support', Padrino.version)
25
26
  s.add_dependency('rackup', '~> 2.1')
26
27
  s.add_dependency('sinatra', '~> 4')
@@ -0,0 +1,7 @@
1
+ PADRINO_ROOT = __dir__ unless defined? PADRINO_ROOT
2
+
3
+ class SystemExternalConstantsDemo < Padrino::Application
4
+ set :reload, true
5
+ end
6
+
7
+ Padrino.load!
@@ -0,0 +1,4 @@
1
+ require 'padrino_reloader_fake_gem'
2
+
3
+ class ExternalConstantsAppError < StandardError
4
+ end
@@ -0,0 +1,5 @@
1
+ autoload :AutoloadLazy, File.expand_path('lazy.rb', __dir__)
2
+
3
+ class AutoloadUser
4
+ include AutoloadConcern
5
+ end
@@ -0,0 +1,2 @@
1
+ module AutoloadConcern
2
+ end
@@ -0,0 +1,2 @@
1
+ class AutoloadLazy
2
+ end