Sublime Forum

Solved with Solution Within - View->File ... On Save - Automatically Refresh Core Plugin Files ( With nested folder support )

#1

Note: I am going to do a few minor tweaks - but I may leave it as is for this post…

Since I don’t like hard-coding values which may not make sense ( the -3 does for .py since it’s for python files to be reloaded ) I will add a var designating which extension it is to track so either .py is entered, or just py and I’ll then change the value as -1 shift is common and easier to identify than -3… but, I may leave it too… since the 0-5 for User. exists and I’m not sure if I see much of a point of adding it - different languages wouldn’t be using different folder names ( or? )… we’ll see…

Hopefully this helps someone though.

New

##
## Main Plugin Auto-Refresh System - Josh 'Acecool' Moser
##


##
## Imports
##
import sublime_plugin


##
## This Event Listener is specifically to enable auto-refreshing of plugin core files when they're edited... It is purposefully kept separate so others can learn from it / use as a drop-in module for thier own plugins...
##
class AcecoolCodeMappingSystemDynamicPluginReloaderEventListener( sublime_plugin.EventListener ):
	##
	## Important Data...
	##

	## The package name used for reloading files listed below...
	PackageName = 'AcecoolCodeMappingSystem'


	##
	## When a package Python file has been saved, we auto-refresh only that particular file... - Note: If you update this function you will need to save twice - the first time uses the current method in memory for output, and the second time shows you what the function has become...
	##
	def on_post_save_async( self, _view ):
		## Grab the file-name of the file which was saved
		_file = _view.file_name( )

		## Grab the extension of the saved file - note this doesn't work on files with more than 1 extension or decimal - as per Python file-naming / import conventions, do not add decimals to the file-name. Only use 1 extension such as file.ext - anything else will not be importable using the import call, you'll need to manually execute the code behind the import call which is messy and can lead to issues if you're unfamiliar with the system and it can lead to other problems..
		_ext = _file.split( '.' )[ - 1 ] == 'py'

		## Determine the index of the package name, if it exists..
		_index = _file.find( self.PackageName )

		## If we've saved a python file inside of our Package, then we assemble the include...
		if ( _ext and _index > 0 ):
			## Note: Either of the following _plugin assignments will work, simply uncomment one and comment the other - the comment details exactly what happens with each example...

			## For simplicty convert all folders to decimals...
			_file = _file.replace( '\\', '.' )

			## This method takes Path\To\||PackageName\InternalFolders\FileName||[[.py]] - [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3... - After we capture what's needed ignoring the rest, we replace backslashes with decimals.
			_plugin = _file[ _index : - 3 ]

			## This method takes: Path\To\PackageName||\Internal\Folders\FileName||[[.py]] - we leave the left \\ ( by not adding + 1 to _index + _len ) to convert it to a decimal so we don't need to manually add it... [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3...
			## _plugin = self.PackageName + _file[ _index + len( self.PackageName ) : -3 ]

			## User Folder Clause - If User is found in the file-name, and the index is LESS than the name of the package-name, then we are in Packages\User\ACMS\ so we need to account for that...
			_plugin_user = _file[ _index - 5 : - 3 ]
			if ( _plugin_user.startswith( 'User.' ) ):
				_plugin = 'User.' + _plugin

			## Print it out to make sure it works..
			print( '>> Dynamic Package File Reloader > On Save Event Triggered for Package File: "' + _file + '" - Which converts to Import Plugin: "' + _plugin + '"' )

			## Reload our Package File...
			sublime_plugin.reload_plugin( _plugin )

Here’s a new version - I added support for User. folders - since I am using _file with the replaced chars for more than 1 area I now set a designated reference for it, then I simply check to see if -5 chars starts with User. and if so I add User. to the main plugin path…

And if it wasn’t clear, it works for unlimited nested paths which is nice… since my addon will use Py files in the User folder, this was something I needed

Old

Solution:

The dynamic version is what I use because I like having nested folders, etc… and because the logic is less expensive compared to the non-dynamic variant, and it requires less user interaction to set up… Simply drop it in and done…

Notes: The class name can be set to what-ever you want as far as I know, and the PackageName needs to be set to the folder-name the system will ‘subscribe’ to to look for changes / saved files within that package name within \Sublime Text 3\Packages<PackageName>\* — The package must be extracted ( I know that when a package is updated the change is automatically loaded into memory so this shouldn’t be necessary for packaged addons, but when you’re developing an addon, it is much easier to deal with loose files and this is what it is for )…

##
## Main Plugin Auto-Refresh System - Dynamic Variant - Josh 'Acecool' Moser
##


##
## Imports
##
import sublime_plugin


##
## This Event Listener is specifically to enable auto-refreshing of plugin core files when they're edited... It is purposefully kept separate so others can learn from it / use as a drop-in module for thier own plugins...
## Note: The class name can be anything you want as far as I know...
##
class AcecoolCodeMappingSystemDynamicPluginReloaderEventListener( sublime_plugin.EventListener ):
	##
	## Important Data...
	##

	## The package name used for reloading files listed below...
	## Note: This needs to be set to the folder-name this auto-refresh system will subscribe to for change-detection and auto-reloading of files within...
	PackageName = 'AcecoolCodeMappingSystem'


	##
	## When a package Python file has been saved, we auto-refresh only that particular file... - Note: If you update this function you will need to save twice - the first time uses the current method in memory for output, and the second time shows you what the function has become...
	##
	def on_post_save_async( self, _view ):
		## Grab the file-name of the file which was saved
		_file = _view.file_name( )

		## Grab the extension of the saved file - note this doesn't work on files with more than 1 extension or decimal - as per Python file-naming / import conventions, do not add decimals to the file-name. Only use 1 extension such as file.ext - anything else will not be importable using the import call, you'll need to manually execute the code behind the import call which is messy and can lead to issues if you're unfamiliar with the system and it can lead to other problems..
		_ext = _file.split( '.' )[ - 1 ] == 'py'

		## Determine the index of the package name, if it exists..
		_index = _file.find( self.PackageName )

		## If we've saved a python file inside of our Package, then we assemble the include...
		if ( _ext and _index > 0 ):
			## Note: Either of the following _plugin assignments will work, simply uncomment one and comment the other - the comment details exactly what happens with each example...

			## This method takes Path\To\||PackageName\InternalFolders\FileName||[[.py]] - [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3... - After we capture what's needed ignoring the rest, we replace backslashes with decimals.
			_plugin = _file[ _index : - 3 ].replace( '\\', '.' )

			## This method takes: Path\To\PackageName||\Internal\Folders\FileName||[[.py]] - we leave the left \\ ( by not adding + 1 to _index + _len ) to convert it to a decimal so we don't need to manually add it... [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3...
			## _plugin = self.PackageName + _file[ _index + len( self.PackageName ) : -3 ].replace( '\\', '.' )

			## Print it out to make sure it works..
			print( '>> Dynamic Package File Reloader > On Save Event Triggered for Package File: "' + _file + '" - Which converts to Import Plugin: "' + _plugin + '"' )

			## Reload our Package File...
			sublime_plugin.reload_plugin( _plugin )

Hopefully this helps the next person who needs something to auto-refresh…




Original Post below:


I’m working on the edit_settings_plus command and I’ve run into a new problem…

When I save the file which contains it - the print statements in run doesn’t change when I execute the command…

I’ve tried calling sublime_plugin.reload_plugin( ‘Acecool_CMS.Acecool_CMS’ ) for Acecool_CMS/Acecool_CMS.py and while it may say reloading plugin in console - the print statements and logic only updates when I restart sublime text…

I used to have this issue on imported files but sublime_plugin.reload_plugin solves that… But why isn’t it working for an Application Command?

I can’t seem to find anything related ( sublime-syntax files update for me just fine, sublime-menu files update flawlessly too on save, etc… ) also the other logic in the file updates and I can print outside of the class edit_settings_plus and it’ll update - just not the actual command…

If it was a matter of updating the class - I’d think overwriting it would work but it doesn’t…

Any suggestions would be welcome!

So far I’ve got a lot of functionality added - the base_file, user_file, default can be text or List with basic entries all lined up - file_list can be a List with Dict entries containing base_file, user_file, default and some are optional… I still need to process a few things before it’s done but having to relaunch every edit would make something that wouldn’t take long and exponentially increase the time it takes…

0 Likes

#2

With the following snippet placed to a settings_plus.py changing the print statement and saving causes the new text to be printed to console upon the next call of the command.

import sublime
import sublime_plugin


class EditSettingsPlusCommand(sublime_plugin.ApplicationCommand):
    def run(self, base_file=None, user_file=None, default=None):
        print("test2")

Console output:

reloading plugin User.applcomm
>>> sublime.run_command("edit_settings_plus")
test
reloading plugin User.applcomm
>>> sublime.run_command("edit_settings_plus")
test2

So reloading the plugin and its command works well with ST3157.

Calling sublime_plugin.reload_plugin() for plugins within the root of a package is not necessary or even may cause issues as ST automatically calls that function, if it detects a change of the plugin.

Doing the same with placing settings_plus.py into a sub directory and creating a top-level settings_plus_loader.py plugin with following content works as well.

from .subdir.settings_plus import *

import sublime_plugin

sublime_plugin.reload_plugin('SettingsPlus.subdir.settings_plus')

You just need to re-save that loader after changing settings_plus.py to let ST reload it.


With that said you might either try your plugin with a vanilla install of ST to check, whether your issue is caused by any other plugin. If that doesn’t work, too, it is your plugin to blame.

Any uncaught exception being raised during plugin reloading might cause the whole reloading stuff to die.

0 Likes

#3

Thanks, I’ll look into it - I did try a loader file too but the file name is Accecool_CMS.py and Acecool_CMS_Reloader.py

I do see things happening in the console when I save from another plugin reloading when I save the core mapped file as it is designed to do…

I’ll see about disabling some of the other addons I installed ( the live package reloader, refreshers, etc… ) to try to solve the issue and work from there…

The plugin itself is incredibly basic with the edit settings plus command and the settings / menu files being the only components to it and I’ve moved my core files there for the mapping system which are included from a different package… That shouldn’t alter how the file reloads…

Additionally, I did not everything else in the file did update - but the edit_settings_plus command run function never changed without reloading sublime text… That’s why I thought it was an issue with Sublime Text with how it caches which is why I created the Reloader file to ensure that one file was included and reloaded…

Edit: I got around to trying your mod - basically removing everything except what was needed and I got the same result…

edit_settings_plus_loader.py ( renamed ) contains:

##
## This file only exists to try and reload the primary file... - Josh 'Acecool' Moser
##
import sublime_plugin

##
from Acecool_CMS.AcecoolLib_Python import *
sublime_plugin.reload_plugin( 'Acecool_CMS.AcecoolLib_Python' )

from Acecool_CMS.AcecoolLib_Sublime import *
sublime_plugin.reload_plugin( 'Acecool_CMS.AcecoolLib_Sublime' )

from Acecool_CMS.edit_settings_plus import *
sublime_plugin.reload_plugin( 'Acecool_CMS.edit_settings_plus' )


## from .edit_settings_plus import *
## sublime_plugin.reload_plugin( '.edit_settings_plus' )

The last 2 caused an error - can’t use . at the beginning…

0 Likes

#4

Unfortunately with all of those, I never received the proper level of reloading because reload_plugin should be called in a function or in a file executed by an event handler…

I’m posting this for anyone else who wants a plugin reloader which works nicely… An alternative to this would be to simply have the Package Name set in the class and use str.indexOf( self.PackageName ) > 0 to include the file…, however that method is slightly different but more dynamic and allows unlimited nested directories with automated includes - so I’ll set that as example 2…

Here’s my solution:

##
## Main Plugin Auto-Refresh System - Manual and Non-Nested Variant - Josh 'Acecool' Moser
##


##
## Imports
##
import sublime_plugin


##
## This Event Listener is specifically to enable auto-refreshing of plugin core files when they're edited... It is purposefully kept separate so others can learn from it / use as a drop-in module for thier own plugins...
##
class AcecoolCodeMappingSystemPluginReloaderEventListener( sublime_plugin.EventListener ):
	##
	## Important Data...
	##

	## The package name used for reloading files listed below...
	PackageName = 'AcecoolCodeMappingSystem'

	## List of core files to reload on save....
	core_files = [
		## Deprecating - THIS FILE - The core Plugin Reloading System - This can be merged into Plugin...
		'AcecoolCodeMappingSystemPluginReloader.py',

		## Generic Python Library
		'AcecoolLib_Python.py',

		## Sublime Text Specific Library
		'AcecoolLib_Sublime.py',

		## Old Sublime Text Specific Library - Working on moving data over...
		'AcecoolLib_SublimeText3.py',

		## Deprecated -	The class linker for CodeMap support...
		'AcecoolCodeMappingSystemClassLinker.py',

		## Deprecating - The Definitions file - Soon to be deprecated as everything is moving to config files...
		'AcecoolCodeMappingSystemDefinitions.py',

		## The core Plugin File
		'AcecoolCodeMappingSystemPlugin.py',

		## The XCodeMapper Objects - This isn't part of the plugin framework - it is what processes the files - the rest is for Sublime Text specific integration... although this does have some Sublime Text callbacks for symbols - will need to add a specific way to map it so this is more generic..
		'AcecoolCodeMappingSystem.py',

		## This is the edit_settings_plus command file used for opening more than a single configuration file at once..
		'edit_settings_plus.py',
	]


	##
	## When a core file has been saved, we auto-refresh only that particular file... - Note: If you update this function you will need to save twice - the first time uses the current method in memory for output, and the second time shows you what the function has become...
	##
	def on_post_save_async( self, _view ):
		## Split the full\path\to\file.py and extract file.py ( - 1 ) from the returned list.
		_file = _view.file_name( ).split( '\\' )[ - 1 ]

		## If our _file name exists within the core_files list, then...
		if ( _file in self.core_files ):
			## Notify the console... A simple notification which can be used as a starting point for debugging if something goes wrong on save...
			print( '>> ' + self.PackageName + ' Core File Save Event Detected for file: ' + _file )

			## Reload that particular file - here we remove the .py extension ( it will remove anything after the first . - as per Python naming conventions, do not add decimals to the file name and do not use multiple extensions - only .py otherwise it may not load properly )
			sublime_plugin.reload_plugin( self.PackageName + '.' + _file.split( '.' )[ 0 ] )

The name of the class can be anything really - and I put it into AcecoolCodeMappingSystemPluginReloader.py inside of the main package folder - although I am likely going to merge it

And now the dynamic variant: This one looks for the first instance of self.PackageName within the file which was detected to have been saved… If the PackageName is included, then we’re looking at a CORE file and we will want to reload it to reflect the changes made…

There are several ways to do it - you can either set _plugin = self.PackageName + _file[ _index + len( self.PackageName ) : - 3 ] ( which excludes .py from the outcome and everything before the internal folder-structure to filename ( we keep the prefix \ to convert to a decimal in order to not need to add the decimal manually )

Or _plugin = _file[ _index : - 3 ] which is shorter and includes the PackageName\Internal\Folders\To\FileName excluding .py again with -3… This means nothing else is needed…

Either will work - both are added to work as an learning method in the code…

The dynamic version is what I use because I like having nested folders, etc… and because the logic is less expensive compared to the non-dynamic variant, and it requires less user interaction to set up… Simply drop it in and done…

##
## Main Plugin Auto-Refresh System - Dynamic Variant - Josh 'Acecool' Moser
##


##
## Imports
##
import sublime_plugin


##
## This Event Listener is specifically to enable auto-refreshing of plugin core files when they're edited... It is purposefully kept separate so others can learn from it / use as a drop-in module for thier own plugins...
##
class AcecoolCodeMappingSystemDynamicPluginReloaderEventListener( sublime_plugin.EventListener ):
	##
	## Important Data...
	##

	## The package name used for reloading files listed below...
	PackageName = 'AcecoolCodeMappingSystem'


	##
	## When a package Python file has been saved, we auto-refresh only that particular file... - Note: If you update this function you will need to save twice - the first time uses the current method in memory for output, and the second time shows you what the function has become...
	##
	def on_post_save_async( self, _view ):
		## Grab the file-name of the file which was saved
		_file = _view.file_name( )

		## Grab the extension of the saved file - note this doesn't work on files with more than 1 extension or decimal - as per Python file-naming / import conventions, do not add decimals to the file-name. Only use 1 extension such as file.ext - anything else will not be importable using the import call, you'll need to manually execute the code behind the import call which is messy and can lead to issues if you're unfamiliar with the system and it can lead to other problems..
		_ext = _file.split( '.' )[ - 1 ] == 'py'

		## Determine the index of the package name, if it exists..
		_index = _file.find( self.PackageName )

		## If we've saved a python file inside of our Package, then we assemble the include...
		if ( _ext and _index > 0 ):
			## Note: Either of the following _plugin assignments will work, simply uncomment one and comment the other - the comment details exactly what happens with each example...

			## This method takes Path\To\||PackageName\InternalFolders\FileName||[[.py]] - [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3... - After we capture what's needed ignoring the rest, we replace backslashes with decimals.
			_plugin = _file[ _index : - 3 ].replace( '\\', '.' )

			## This method takes: Path\To\PackageName||\Internal\Folders\FileName||[[.py]] - we leave the left \\ ( by not adding + 1 to _index + _len ) to convert it to a decimal so we don't need to manually add it... [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3...
			## _plugin = self.PackageName + _file[ _index + len( self.PackageName ) : -3 ].replace( '\\', '.' )

			## Print it out to make sure it works..
			print( '>> Dynamic Package File Reloader > On Save Event Triggered for Package File: "' + _file + '" - Which converts to Import Plugin: "' + _plugin + '"' )

			## Reload our Package File...
			sublime_plugin.reload_plugin( _plugin )

Hopefully this helps the next person who needs something to auto-refresh…

On a side note: while I can delete the reloader.py file contents into the plugin file - if you end up with a syntax errors, especially involving an import or something else - it is possible for the system to break… However, with the file separate, it will not… So I am going to keep the files separate…

0 Likes

#5
##
## Main Plugin Auto-Refresh System - Josh 'Acecool' Moser
##


##
## Imports
##
import sublime_plugin


##
## This Event Listener is specifically to enable auto-refreshing of plugin core files when they're edited... It is purposefully kept separate so others can learn from it / use as a drop-in module for thier own plugins...
##
class AcecoolCodeMappingSystemDynamicPluginReloaderEventListener( sublime_plugin.EventListener ):
	##
	## Important Data...
	##

	## The package name used for reloading files listed below...
	PackageName = 'AcecoolCodeMappingSystem'


	##
	## When a package Python file has been saved, we auto-refresh only that particular file... - Note: If you update this function you will need to save twice - the first time uses the current method in memory for output, and the second time shows you what the function has become...
	##
	def on_post_save_async( self, _view ):
		## Grab the file-name of the file which was saved
		_file = _view.file_name( )

		## Grab the extension of the saved file - note this doesn't work on files with more than 1 extension or decimal - as per Python file-naming / import conventions, do not add decimals to the file-name. Only use 1 extension such as file.ext - anything else will not be importable using the import call, you'll need to manually execute the code behind the import call which is messy and can lead to issues if you're unfamiliar with the system and it can lead to other problems..
		_ext = _file.split( '.' )[ - 1 ] == 'py'

		## Determine the index of the package name, if it exists..
		_index = _file.find( self.PackageName )

		## If we've saved a python file inside of our Package, then we assemble the include...
		if ( _ext and _index > 0 ):
			## Note: Either of the following _plugin assignments will work, simply uncomment one and comment the other - the comment details exactly what happens with each example...

			## For simplicty convert all folders to decimals...
			_file = _file.replace( '\\', '.' )

			## This method takes Path\To\||PackageName\InternalFolders\FileName||[[.py]] - [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3... - After we capture what's needed ignoring the rest, we replace backslashes with decimals.
			_plugin = _file[ _index : - 3 ]

			## This method takes: Path\To\PackageName||\Internal\Folders\FileName||[[.py]] - we leave the left \\ ( by not adding + 1 to _index + _len ) to convert it to a decimal so we don't need to manually add it... [[...]] being subtracted from the return using -3, ||...|| being what's captured starting with _index + len( ... ) through -3...
			## _plugin = self.PackageName + _file[ _index + len( self.PackageName ) : -3 ]

			## User Folder Clause - If User is found in the file-name, and the index is LESS than the name of the package-name, then we are in Packages\User\ACMS\ so we need to account for that...
			_plugin_user = _file[ _index - 5 : - 3 ]
			if ( _plugin_user.startswith( 'User.' ) ):
				_plugin = 'User.' + _plugin

			## Print it out to make sure it works..
			print( '>> Dynamic Package File Reloader > On Save Event Triggered for Package File: "' + _file + '" - Which converts to Import Plugin: "' + _plugin + '"' )

			## Reload our Package File...
			sublime_plugin.reload_plugin( _plugin )

Here’s a new version - I added support for User. folders - since I am using _file with the replaced chars for more than 1 area I now set a designated reference for it, then I simply check to see if -5 chars starts with User. and if so I add User. to the main plugin path…

And if it wasn’t clear, it works for unlimited nested paths which is nice… since my addon will use Py files in the User folder, this was something I needed

0 Likes