mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-14 11:05:41 +00:00
Remove plugins.txt support
This commit is contained in:
@ -402,5 +402,7 @@ The following [plugin](../extend/plugins.md) configuration options are available
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_PLUGINS_ENABLED | plugins_enabled | Enable plugin support | False |
|
||||
| INVENTREE_PLUGIN_NOINSTALL | plugin_noinstall | Disable Plugin installation via API - only use plugins.txt file | False |
|
||||
| INVENTREE_PLUGIN_FILE | plugins_plugin_file | Location of plugin installation file | *Not specified* |
|
||||
| INVENTREE_PLUGIN_DIR | plugins_plugin_dir | Location of external plugin directory | *Not specified* |
|
||||
|
||||
!!! info "Plugins Directory"
|
||||
If the `INVENTREE_PLUGIN_DIR` environment variable is not specified, the default location is the `plugins` directory within the InvenTree installation.
|
||||
|
@ -283,37 +283,6 @@ def get_backup_dir(create=True):
|
||||
return bd
|
||||
|
||||
|
||||
def get_plugin_file():
|
||||
"""Returns the path of the InvenTree plugins specification file.
|
||||
|
||||
Note: It will be created if it does not already exist!
|
||||
"""
|
||||
# Check if the plugin.txt file (specifying required plugins) is specified
|
||||
plugin_file = get_setting('INVENTREE_PLUGIN_FILE', 'plugin_file')
|
||||
|
||||
if not plugin_file:
|
||||
# If not specified, look in the same directory as the configuration file
|
||||
config_dir = get_config_file().parent
|
||||
plugin_file = config_dir.joinpath('plugins.txt')
|
||||
else:
|
||||
# Make sure we are using a modern Path object
|
||||
plugin_file = Path(plugin_file)
|
||||
|
||||
if not plugin_file.exists():
|
||||
logger.warning(
|
||||
'Plugin configuration file does not exist - creating default file'
|
||||
)
|
||||
logger.info("Creating plugin file at '%s'", plugin_file)
|
||||
ensure_dir(plugin_file.parent)
|
||||
|
||||
# If opening the file fails (no write permission, for example), then this will throw an error
|
||||
plugin_file.write_text(
|
||||
'# InvenTree Plugins (uses PIP framework to install)\n\n'
|
||||
)
|
||||
|
||||
return plugin_file
|
||||
|
||||
|
||||
def get_plugin_dir():
|
||||
"""Returns the path of the custom plugins directory."""
|
||||
return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
|
||||
|
@ -143,8 +143,6 @@ PLUGINS_INSTALL_DISABLED = get_boolean_setting(
|
||||
'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
|
||||
)
|
||||
|
||||
PLUGIN_FILE = config.get_plugin_file()
|
||||
|
||||
# Plugin test settings
|
||||
PLUGIN_TESTING = get_setting(
|
||||
'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING
|
||||
@ -160,8 +158,6 @@ PLUGIN_RETRY = get_setting(
|
||||
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
|
||||
) # How often should plugin loading be tried?
|
||||
|
||||
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
|
||||
|
||||
STATICFILES_DIRS = []
|
||||
|
||||
# Translated Template settings
|
||||
|
@ -1180,23 +1180,6 @@ class TestSettings(InvenTreeTestCase):
|
||||
# make sure to clean up
|
||||
settings.TESTING_ENV = False
|
||||
|
||||
def test_initial_install(self):
|
||||
"""Test if install of plugins on startup works."""
|
||||
from plugin import registry
|
||||
|
||||
if not settings.DOCKER:
|
||||
# Check an install run
|
||||
response = registry.install_plugin_file()
|
||||
self.assertEqual(response, 'first_run')
|
||||
|
||||
# Set dynamic setting to True and rerun to launch install
|
||||
InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
|
||||
registry.reload_plugins(full_reload=True)
|
||||
|
||||
# Check that there was another run
|
||||
response = registry.install_plugin_file()
|
||||
self.assertEqual(response, True)
|
||||
|
||||
def test_helpers_cfg_file(self):
|
||||
"""Test get_config_file."""
|
||||
# normal run - not configured
|
||||
@ -1216,24 +1199,6 @@ class TestSettings(InvenTreeTestCase):
|
||||
str(config.get_config_file()).lower(),
|
||||
)
|
||||
|
||||
def test_helpers_plugin_file(self):
|
||||
"""Test get_plugin_file."""
|
||||
# normal run - not configured
|
||||
|
||||
valid = ['inventree/plugins.txt', 'inventree/data/plugins.txt']
|
||||
|
||||
self.assertTrue(
|
||||
any(opt in str(config.get_plugin_file()).lower() for opt in valid)
|
||||
)
|
||||
|
||||
# with env set
|
||||
with self.in_env_context({
|
||||
'INVENTREE_PLUGIN_FILE': '_testfolder/my_special_plugins.txt'
|
||||
}):
|
||||
self.assertIn(
|
||||
'_testfolder/my_special_plugins.txt', str(config.get_plugin_file())
|
||||
)
|
||||
|
||||
def test_helpers_setting(self):
|
||||
"""Test get_setting."""
|
||||
TEST_ENV_NAME = '123TEST'
|
||||
|
@ -91,7 +91,6 @@ tracing:
|
||||
# Set this variable to True to enable InvenTree Plugins, or use the environment variable INVENTREE_PLUGINS_ENABLED
|
||||
plugins_enabled: False
|
||||
#plugin_noinstall: True
|
||||
#plugin_file: '/path/to/plugins.txt'
|
||||
#plugin_dir: '/path/to/plugins/'
|
||||
|
||||
# Set this variable to True to enable auto-migrations, or use the environment variable INVENTREE_AUTO_UPDATE
|
||||
|
@ -40,18 +40,6 @@ class PluginAppConfig(AppConfig):
|
||||
logger.info('Loading InvenTree plugins')
|
||||
|
||||
if not registry.is_loading:
|
||||
# this is the first startup
|
||||
try:
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
if InvenTreeSetting.get_setting(
|
||||
'PLUGIN_ON_STARTUP', create=False, cache=False
|
||||
):
|
||||
# make sure all plugins are installed
|
||||
registry.install_plugin_file()
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
|
||||
# Perform a full reload of the plugin registry
|
||||
registry.reload_plugins(
|
||||
full_reload=True, force_reload=True, collect=True
|
||||
|
@ -177,27 +177,37 @@ def get_modules(pkg, path=None):
|
||||
elif type(path) is not list:
|
||||
path = [path]
|
||||
|
||||
for finder, name, _ in pkgutil.walk_packages(path):
|
||||
try:
|
||||
if sys.version_info < (3, 12):
|
||||
module = finder.find_module(name).load_module(name)
|
||||
else:
|
||||
spec = finder.find_spec(name)
|
||||
module = module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
pkg_names = getattr(module, '__all__', None)
|
||||
for k, v in vars(module).items():
|
||||
if not k.startswith('_') and (pkg_names is None or k in pkg_names):
|
||||
context[k] = v
|
||||
context[name] = module
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
pass
|
||||
except Exception as error:
|
||||
# this 'protects' against malformed plugin modules by more or less silently failing
|
||||
try:
|
||||
packages = pkgutil.walk_packages(path)
|
||||
except Exception as e:
|
||||
raise IntegrationPluginError(pkg.__name__, str(e))
|
||||
|
||||
# log to stack
|
||||
log_error({name: str(error)}, 'discovery')
|
||||
try:
|
||||
for finder, name, _ in packages:
|
||||
try:
|
||||
if sys.version_info < (3, 12):
|
||||
try:
|
||||
module = finder.find_module(name).load_module(name)
|
||||
except Exception as e:
|
||||
raise IntegrationPluginError(name, str(e))
|
||||
|
||||
else:
|
||||
spec = finder.find_spec(name)
|
||||
module = module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
pkg_names = getattr(module, '__all__', None)
|
||||
for k, v in vars(module).items():
|
||||
if not k.startswith('_') and (pkg_names is None or k in pkg_names):
|
||||
context[k] = v
|
||||
context[name] = module
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
pass
|
||||
except Exception as error:
|
||||
# this 'protects' against malformed plugin modules by more or less silently failing
|
||||
|
||||
# log to stack
|
||||
log_error({name: str(error)}, 'discovery')
|
||||
|
||||
return [v for k, v in context.items()]
|
||||
|
||||
@ -206,8 +216,8 @@ def get_classes(module) -> list:
|
||||
"""Get all classes in a given module."""
|
||||
try:
|
||||
return inspect.getmembers(module, inspect.isclass)
|
||||
except Exception:
|
||||
return []
|
||||
except Exception as e:
|
||||
raise IntegrationPluginError(module.__name__, str(e))
|
||||
|
||||
|
||||
def get_plugins(pkg, baseclass, path=None):
|
||||
@ -223,7 +233,12 @@ def get_plugins(pkg, baseclass, path=None):
|
||||
# Iterate through each module in the package
|
||||
for mod in modules:
|
||||
# Iterate through each class in the module
|
||||
for item in get_classes(mod):
|
||||
try:
|
||||
classes = get_classes(mod)
|
||||
except IntegrationPluginError:
|
||||
continue
|
||||
|
||||
for item in classes:
|
||||
plugin = item[1]
|
||||
if issubclass(plugin, baseclass) and plugin.NAME:
|
||||
plugins.append(plugin)
|
||||
|
@ -155,98 +155,6 @@ def plugins_dir():
|
||||
return pd.absolute()
|
||||
|
||||
|
||||
def install_plugins_file():
|
||||
"""Install plugins from the plugins file."""
|
||||
logger.info('Installing plugins from plugins file')
|
||||
|
||||
pf = settings.PLUGIN_FILE
|
||||
|
||||
if not pf or not pf.exists():
|
||||
logger.warning('Plugin file %s does not exist', str(pf))
|
||||
return
|
||||
|
||||
plugin_dir = plugins_dir()
|
||||
|
||||
cmd = ['install', '-U', '--target', str(plugin_dir), '-r', str(pf)]
|
||||
|
||||
try:
|
||||
pip_command(*cmd)
|
||||
except subprocess.CalledProcessError as error:
|
||||
output = error.output.decode('utf-8')
|
||||
logger.exception('Plugin file installation failed: %s', str(output))
|
||||
log_error('pip')
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.exception('Plugin file installation failed: %s', exc)
|
||||
log_error('pip')
|
||||
return False
|
||||
|
||||
# Update static files
|
||||
plugin.staticfiles.collect_plugins_static_files()
|
||||
plugin.staticfiles.clear_plugins_static_files()
|
||||
|
||||
# At this point, the plugins file has been installed
|
||||
return True
|
||||
|
||||
|
||||
def update_plugins_file(install_name, remove=False):
|
||||
"""Add a plugin to the plugins file."""
|
||||
logger.info('Adding plugin to plugins file: %s', install_name)
|
||||
|
||||
pf = settings.PLUGIN_FILE
|
||||
|
||||
if not pf or not pf.exists():
|
||||
logger.warning('Plugin file %s does not exist', str(pf))
|
||||
return
|
||||
|
||||
def compare_line(line: str):
|
||||
"""Check if a line in the file matches the installname."""
|
||||
return line.strip().split('==')[0] == install_name.split('==')[0]
|
||||
|
||||
# First, read in existing plugin file
|
||||
try:
|
||||
with pf.open(mode='r') as f:
|
||||
lines = f.readlines()
|
||||
except Exception as exc:
|
||||
logger.exception('Failed to read plugins file: %s', str(exc))
|
||||
return
|
||||
|
||||
# Reconstruct output file
|
||||
output = []
|
||||
|
||||
found = False
|
||||
|
||||
# Check if plugin is already in file
|
||||
for line in lines:
|
||||
# Ignore processing for any commented lines
|
||||
if line.strip().startswith('#'):
|
||||
output.append(line)
|
||||
continue
|
||||
|
||||
if compare_line(line):
|
||||
found = True
|
||||
if not remove:
|
||||
# Replace line with new install name
|
||||
output.append(install_name)
|
||||
else:
|
||||
output.append(line)
|
||||
|
||||
# Append plugin to file
|
||||
if not found and not remove:
|
||||
output.append(install_name)
|
||||
|
||||
# Write file back to disk
|
||||
try:
|
||||
with pf.open(mode='w') as f:
|
||||
for line in output:
|
||||
f.write(line)
|
||||
|
||||
if not line.endswith('\n'):
|
||||
f.write('\n')
|
||||
except Exception as exc:
|
||||
logger.exception('Failed to add plugin to plugins file: %s', str(exc))
|
||||
|
||||
|
||||
def install_plugin(url=None, packagename=None, user=None, version=None):
|
||||
"""Install a plugin into the python virtual environment.
|
||||
|
||||
@ -309,9 +217,6 @@ def install_plugin(url=None, packagename=None, user=None, version=None):
|
||||
except subprocess.CalledProcessError as error:
|
||||
handle_pip_error(error, 'plugin_install')
|
||||
|
||||
# Save plugin to plugins file
|
||||
update_plugins_file(full_pkg)
|
||||
|
||||
# Reload the plugin registry, to discover the new plugin
|
||||
from plugin.registry import registry
|
||||
|
||||
@ -376,9 +281,6 @@ def uninstall_plugin(cfg: plugin.models.PluginConfig, user=None, delete_config=T
|
||||
# No matching install target found
|
||||
raise ValidationError(_('Plugin installation not found'))
|
||||
|
||||
# Update the plugins file
|
||||
update_plugins_file(package_name, remove=True)
|
||||
|
||||
if delete_config:
|
||||
# Remove the plugin configuration from the database
|
||||
cfg.delete()
|
||||
|
@ -428,19 +428,6 @@ class PluginsRegistry:
|
||||
|
||||
self.mixin_modules = collected_mixins
|
||||
|
||||
def install_plugin_file(self):
|
||||
"""Make sure all plugins are installed in the current environment."""
|
||||
if settings.PLUGIN_FILE_CHECKED:
|
||||
logger.info('Plugin file was already checked')
|
||||
return True
|
||||
|
||||
from plugin.installer import install_plugins_file
|
||||
|
||||
if install_plugins_file():
|
||||
settings.PLUGIN_FILE_CHECKED = True
|
||||
return 'first_run'
|
||||
return False
|
||||
|
||||
# endregion
|
||||
|
||||
# region general internal loading / activating / deactivating / unloading
|
||||
|
@ -25,7 +25,6 @@
|
||||
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_URL" icon="fa-link" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_NAVIGATION" icon="fa-sitemap" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PLUGIN_ON_STARTUP" %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user