A question about how file_loaded? works

ruby

#1

I’m wondering what is the matter with how I’ve used file_loaded? in these snippets of code?
In a file called File Stuff.rb is the following

load 'C:\SketchUp_Plugins\File1.rb'
file_loaded('File1.rb')
puts"\nIn File stuff.rb and file_loaded?('File1') is #{file_loaded?('File1')}."
load 'C:\SketchUp_Plugins\File2.rb'
file_loaded('File2.rb')
puts"\nIn File stuff.rb and file_loaded?('File2') is #{file_loaded?('File2')}."

method1
method2

In a file called File1.rb is

def method1
  model = Sketchup.active_model
  puts"\nIn method1 of File1.rb and model is #{model}"
end
puts"\nIn File1.rb and it has loaded."

In a file called File2.rb is

def method2
  model = Sketchup.active_model
  puts"\nIn method2 of File2.rb and model is #{model}"
end
puts"\nIn File2.rb and it has loaded."

The output on my HP Z800 Windows10 machine from the SketchUp 2018 Ruby Console is

Why does file_loaded? return false for File1 and File2 after they have been loaded?

Thank you in advance for any help you can give.


#2

It might be a simple oversight, but "File1" != "File1.rb".
(I don’t know why some OS decided to hide file extensions, as long as they are part of the file name string and not a separate file attribute, extensions should be visible.)

These methods are in fact not a Ruby feature, but added by SketchUp’s API, and they just put the given string into a hash map and look it up again. You could use any other unique identifier like file_loaded('Hanswurst').

You usually do it within the file that is being loaded (not like you did just for testing in Stuff.rb), and the purpose is to allow conditional code blocks that will be executed no more than once in a session (menu registration etc.).


#3

Oh right, now I get it. I thought SketchUp looked up the name of the file it just loaded regardless of the file extension. Now I see that file_loaded associates a string with the file which as you say can be anything.

It certainly made a fool of me!

Thank you for the help!


#4

No it does NOT associate anything at all. And it is a simple Array (not a Hash) that contains ANY old string ANY of the extensions on a user’s machine cares to push into that ONE SHARED array.

If you wish to see what is what in these extra API methods, read the "Tools/sketchup.rb" file for yourself. … do not make assumptions.


Do you now understand that relying upon this “functionality” can break if another coder also uses simple file names as in your example ?

You are much better off implementing your own variables to keep track of what is loaded inside and unique to each extension module.

Myself, I simple use a module variable @@menu_loaded = false at the top of the first file, and then at the bottom of the last file I load the menus and set the variable to true.

If you wish to track all of your extension’s files, then create a @@files_loaded ||= [] module var at top of 1st file, and then as files are loaded, push there names into this array …
@@files_loaded << File.basename(__FILE__)


ADD: Oh. … If you really want a true and automatic indicator of what files are loaded then check the global $LOADED_FEATURES array. (It is also aliased as $")
To list it, just type $" and then ENTER at the console.


#5

Thanks for this information.
I never thought of reading the Tools/sketchup.rb lso I will take a look at that and also check out the other ways of ticking off stuff that has been loaded which you outlined.
Thank you again.


#6

This is some ancient code from sketchup.rb. It is not a part of the SketchUp Ruby API or the language itself, just an ancient file that still has to be shipped as there are numerous extensions relying on it.


#7

Thank you. I’ll be revisiting my code to sort this out soon.


#8

Is is actually documented in the Top Level Namespace within the API docs.
But most of the methods are marked as deprecated as they were poorly implemented, some still have bugs, etc.

Best to read want they did, and do it better within your own code libraries.


#9

Won’t @@menu_loaded die as soon as the file has loaded so that if you reload, for instance when testing, you will get doubled up menus?


#10

NO. That is the whole point of module variables (provided that you declare them INSIDE your module(s).

It is local variables that are garbage collected if they are outside of a module at the top level that are “killed” at the end of the file evaluation.

The point is, there is no good reason for you to be evaluating any code at all outside your top level author/company module.


#11

Here is an implementation along the lines I think you mean. The three quoted chunks of code are three separate files which I have called fms_Temp.rb, Temp.rb and Temp2.rb.
fms_Temp.rb comes first:-

# encoding: UTF-8

require 'sketchup.rb'
require 'extensions.rb'

	# Sketchup's Extension class provides a user interface in Sketchup for installing and removing plugins (now called extensions). 
	module FMS_Temp_module
		@@file_Temp_loaded = false
		@@file_Temp2_loaded = false
		#Register the FMS_Temp_module Tools with SU's extension manager
		loader = File.join(File.dirname(__FILE__), "fms_Temp", "Temp.rb")
		tempExtension = SketchupExtension.new("Temp", loader)
    
		#extension details
		tempExtension.description="Adds TempWhatsitThingumy to the Extensions menu for creating TempWhatsitThingumies."
		tempExtension.version = "1.0"
		tempExtension.creator = "Francis Mc Shane"
		tempExtension.copyright = "2018"
		
		Sketchup.register_extension(tempExtension, true) # show on 1st install 

	end # module FMS_Temp_module

then Temp.rb

module FMS_Temp_module
  def self.set_file_Temp_loaded(bool)
    @@file_Temp_loaded = bool
  end
  def self.get_file_Temp_loaded
    @@file_Temp_loaded
  end


  class Temp_class1

    def initialize(whatsit)
      @whatsit = whatsit
    end
    def method_temp_class1
    
      puts"In method_temp_class1 of class Temp_class1 and @whatsit is #{@whatsit}. Change this text to show a change to this method's output message."
    
    end
    
  end # of class Temp_class1
  
end # of module FMS_Temp_module

  #test Temp_class1
  animal = FMS_Temp_module::Temp_class1.new("Giraffe")
  animal.method_temp_class1
  #end of teset
  
  require File.join(File.dirname(__FILE__), "Temp2.rb")

  if FMS_Temp_module::get_file_Temp2_loaded
  then
    puts ("In file Temp.rb FMS_Temp_module::get_file_Temp2_loaded is #{FMS_Temp_module::get_file_Temp2_loaded} which means it HAS loaded.")
  else
    puts ("In file Temp.rb FMS_Temp_module::get_file_Temp2_loaded is #{FMS_Temp_module::get_file_Temp2_loaded} which means it has NOT loaded.")
  end#end of unless
  
  puts("Change this text to show file Temp.rb has changed.")
  
  FMS_Temp_module.set_file_Temp_loaded(true)

then Temp2.rb

module FMS_Temp_module
  def self.set_file_Temp2_loaded(bool)
    @@file_Temp2_loaded = bool
  end
  def self.get_file_Temp2_loaded
    @@file_Temp2_loaded
  end

  require File.join(File.dirname(__FILE__), "Temp.rb")

  class Temp_class2 < FMS_Temp_module::Temp_class1

    def initialize(whatsit="Car", thingumy="James")
      super(whatsit)
      @thingumy = thingumy
    end
    def method_temp_class2
    
      puts"In method_temp_class2 of class method_temp_class2 and @whatsit is #{@whatsit}. Change this text to show a change to this method's first message."
      puts"In method_temp_class2 of class Temp_class2 and @thingumy is #{@thingumy}. Change this text to show a change to this method's 2nd message."
    
    end
    
  end # of class Temp_class2
  
    
end # of module FMS_Temp_module

  unless FMS_Temp_module.get_file_Temp2_loaded
    extensions_menu = UI.menu("Extensions")
    extensions_menu.add_item("TempWhatsitThingumy") {
      stuff=UI.inputbox(["Input whatsit","Input thingumy"], ["Car", "James"], "Input 2 items.")
      obj = FMS_Temp_module::Temp_class2.new(stuff[0], stuff[1])
      obj.method_temp_class2
      }
  end
  
  puts("Change this text to show file Temp2.rb has changed.")
  
  FMS_Temp_module.set_file_Temp2_loaded(true)

Hope I got it right. It works well for me.


#12

It seems you are making it more complicated than you have to. You don’t need accessor methods for setting boolean values for individual files. You can just wrap the code you want to run once with an if-statement.

# Some code here...
def some_method
  true
end

def some_other_method
  false
end

# Only run this once, e.g. for creating menu.
unless @loaded
  @loaded = true
  
  menu = UI.menu("Plugins").add_submenu("My Plugin")
  menu.add_item("Do stuff") { some_method }
  menu.add_item("Do Other Stuff") { some_other_method }
end

When used directly in a class or module, not in an instance method, @instance_variables belong the the module/class itself. It has the benefit over @@class_variables that it isn’t shared with subclasses, so the risk of collisions is far less. @instance_variables also has the benefi of defaulting to nil when not set. Typically I only name this var loaded as I have just one such variable in the context anyway. No need to prefix it with file name or class name or the like.


#13

I used the @@class_variable because Dan suggested it. It does work. Wouldn’t the module @instance_variable die as soon as the script loaded and then if you loaded it again it wouldn’t know it had been loaded before?


#14

Instance vars don’t die.


#15

What instance is it attached to?


#16

The instance of the class class, or module class it is defined within. It’s a bit meta but it works and risk less errors down the road than @@class variables.

The Rubocop Ruby Style Guide advises against class variables.

In this example @@loaded in one class would collide with that of the other class:

class FirstClass
  # Only run this once.
  unless @@loaded
    @@loaded = true
    
    menu = UI.menu("Plugins").add_submenu("My Plugin")
    menu.add_item("Do stuff") { some_method }
    menu.add_item("Do Other Stuff") { some_other_method }
  end
end

class SecondClass < FirstClass
  # Only run this once.
  unless @@loaded
    @@loaded = true
    
    # Maybe do some observer stuff here.
  end
  
end

Also it throws an error unless you first define @@loaded.


#17

Ok thank you for that. I just thought you had to create an instance to attach instance variables to. So I would have had to do a instance_of_module = Module.new (value_for_instance_variable)

I’ll play around with it.

Thanks again


#18

You might miss the point that A module, is an instance of CLASS Module. So you can actually use instance variables inside A module.

I do for certain things that change often.
But I use module variables for stuff like this where it is a semi-global variable, that may change only once per session. I use @@vars for plugin options hashes etc.

There is no danger that the @@vars will propagate to subclasses if it is just a namespace module and it will not be included, ie mixed into classes. There are times when we DO use mixin modules with shared @@vars. (I’ve posted examples here in the forum.)

Ruby is multi-paradigm. There is more than one way to do things.


Agree with Julia. There is no need to create accessor methods if you are only going use / read an @@var once of twice. Just use it.

  1. But more important, you are breaking a rule of good practice in a shared environment.
    You have code running (evaluating) outside of your top level namespace module.
    DONT DO THIS. There is no good reason to do it at all. (Unless you are actually creating global objects that are going to be used by EVERYONE. Which is a BIG no-no anyway.)

  2. You have a circle coding reference going on where the first file “requires” the 2nd, and the 2nd “requires” the first. Dont do this either. Often we use a “loader” file that loads all the other files in the plugin in the order that they need to be loaded.


#19

(Somewhat off topic but I also use @@class variables myself, e.g. for tool parameters I want to keep when a new instance of the tool is initialized and activated later,)


#20
FMS_Temp_module = Module::new {
  # module code
}

… is the same things as …

module FMS_Temp_module
  # module code
end

The latter is the interpreted code document that the Ruby interpreter converts into the former.