1919def pip_command (* args ):
2020 """Build and run a pip command using using the current python executable.
2121
22- returns: subprocess.check_output
23- throws: subprocess.CalledProcessError
22+ Returns: The output of the pip command
23+
24+ Raises:
25+ subprocess.CalledProcessError: If the pip command fails
2426 """
2527 python = sys .executable
2628
2729 command = [python , '-m' , 'pip' ]
30+
2831 command .extend (args )
2932
3033 command = [str (x ) for x in command ]
@@ -63,39 +66,55 @@ def handle_pip_error(error, path: str) -> list:
6366 raise ValidationError (errors [0 ])
6467
6568
66- def check_package_path (packagename : str ):
67- """Determine the install path of a particular package.
69+ def get_install_info (packagename : str ) -> dict :
70+ """Determine the install information for a particular package.
6871
69- - If installed, return the installation path
70- - If not installed, return False
72+ - Uses 'pip show' to determine the install location of a package.
7173 """
72- logger .debug ('check_package_path : %s' , packagename )
74+ logger .debug ('get_install_info : %s' , packagename )
7375
7476 # Remove version information
75- for c in '<>=! ' :
77+ for c in '<>=!@ ' :
7678 packagename = packagename .split (c )[0 ]
7779
80+ info = {}
81+
7882 try :
7983 result = pip_command ('show' , packagename )
8084
8185 output = result .decode ('utf-8' ).split ('\n ' )
8286
8387 for line in output :
84- # Check if line matches pattern "Location: ..."
85- match = re .match (r'^Location:\s+(.+)$' , line .strip ())
88+ parts = line .split (':' )
89+
90+ if len (parts ) >= 2 :
91+ key = str (parts [0 ].strip ().lower ().replace ('-' , '_' ))
92+ value = str (parts [1 ].strip ())
8693
87- if match :
88- return match .group (1 )
94+ info [key ] = value
8995
9096 except subprocess .CalledProcessError as error :
91- log_error ('check_package_path ' )
97+ log_error ('get_install_info ' )
9298
9399 output = error .output .decode ('utf-8' )
100+ info ['error' ] = output
94101 logger .exception ('Plugin lookup failed: %s' , str (output ))
95- return False
96102
97- # If we get here, the package is not installed
98- return False
103+ return info
104+
105+
106+ def plugins_file_hash ():
107+ """Return the file hash for the plugins file."""
108+ import hashlib
109+
110+ pf = settings .PLUGIN_FILE
111+
112+ if not pf or not pf .exists ():
113+ return None
114+
115+ with pf .open ('rb' ) as f :
116+ # Note: Once we support 3.11 as a minimum, we can use hashlib.file_digest
117+ return hashlib .sha256 (f .read ()).hexdigest ()
99118
100119
101120def install_plugins_file ():
@@ -108,8 +127,10 @@ def install_plugins_file():
108127 logger .warning ('Plugin file %s does not exist' , str (pf ))
109128 return
110129
130+ cmd = ['install' , '--disable-pip-version-check' , '-U' , '-r' , str (pf )]
131+
111132 try :
112- pip_command ('install' , '-r' , str ( pf ) )
133+ pip_command (* cmd )
113134 except subprocess .CalledProcessError as error :
114135 output = error .output .decode ('utf-8' )
115136 logger .exception ('Plugin file installation failed: %s' , str (output ))
@@ -120,17 +141,25 @@ def install_plugins_file():
120141 log_error ('pip' )
121142 return False
122143
123- # Update static files
144+ # Collect plugin static files
124145 plugin .staticfiles .collect_plugins_static_files ()
125- plugin .staticfiles .clear_plugins_static_files ()
126146
127147 # At this point, the plugins file has been installed
128148 return True
129149
130150
131- def update_plugins_file (install_name , remove = False ):
151+ def update_plugins_file (install_name , full_package = None , version = None , remove = False ):
132152 """Add a plugin to the plugins file."""
133- logger .info ('Adding plugin to plugins file: %s' , install_name )
153+ if remove :
154+ logger .info ('Removing plugin from plugins file: %s' , install_name )
155+ else :
156+ logger .info ('Adding plugin to plugins file: %s' , install_name )
157+
158+ # If a full package name is provided, use that instead
159+ if full_package and full_package != install_name :
160+ new_value = full_package
161+ else :
162+ new_value = f'{ install_name } =={ version } ' if version else install_name
134163
135164 pf = settings .PLUGIN_FILE
136165
@@ -140,7 +169,7 @@ def update_plugins_file(install_name, remove=False):
140169
141170 def compare_line (line : str ):
142171 """Check if a line in the file matches the installname."""
143- return line . strip (). split ( '==' )[ 0 ] == install_name . split ( '==' )[ 0 ]
172+ return re . match ( rf'^ { install_name } [\s=@]' , line . strip ())
144173
145174 # First, read in existing plugin file
146175 try :
@@ -166,13 +195,13 @@ def compare_line(line: str):
166195 found = True
167196 if not remove :
168197 # Replace line with new install name
169- output .append (install_name )
198+ output .append (new_value )
170199 else :
171200 output .append (line )
172201
173202 # Append plugin to file
174203 if not found and not remove :
175- output .append (install_name )
204+ output .append (new_value )
176205
177206 # Write file back to disk
178207 try :
@@ -203,15 +232,8 @@ def install_plugin(url=None, packagename=None, user=None, version=None):
203232
204233 logger .info ('install_plugin: %s, %s' , url , packagename )
205234
206- # Check if we are running in a virtual environment
207- # For now, just log a warning
208- in_venv = sys .prefix != sys .base_prefix
209-
210- if not in_venv :
211- logger .warning ('InvenTree is not running in a virtual environment' )
212-
213235 # build up the command
214- install_name = ['install' , '-U' ]
236+ install_name = ['install' , '-U' , '--disable-pip-version-check' ]
215237
216238 full_pkg = ''
217239
@@ -246,23 +268,25 @@ def install_plugin(url=None, packagename=None, user=None, version=None):
246268 ret ['result' ] = ret ['success' ] = _ ('Installed plugin successfully' )
247269 ret ['output' ] = str (result , 'utf-8' )
248270
249- if packagename and (path := check_package_path (packagename )):
250- # Override result information
251- ret ['result' ] = _ (f'Installed plugin into { path } ' )
271+ if packagename and (info := get_install_info (packagename )):
272+ if path := info .get ('location' ):
273+ ret ['result' ] = _ (f'Installed plugin into { path } ' )
274+ ret ['version' ] = info .get ('version' )
252275
253276 except subprocess .CalledProcessError as error :
254277 handle_pip_error (error , 'plugin_install' )
255278
256- # Save plugin to plugins file
257- update_plugins_file (full_pkg )
279+ if version := ret .get ('version' ):
280+ # Save plugin to plugins file
281+ update_plugins_file (packagename , full_package = full_pkg , version = version )
258282
259- # Reload the plugin registry, to discover the new plugin
260- from plugin .registry import registry
283+ # Reload the plugin registry, to discover the new plugin
284+ from plugin .registry import registry
261285
262- registry .reload_plugins (full_reload = True , force_reload = True , collect = True )
286+ registry .reload_plugins (full_reload = True , force_reload = True , collect = True )
263287
264- # Update static files
265- plugin .staticfiles .collect_plugins_static_files ()
288+ # Update static files
289+ plugin .staticfiles .collect_plugins_static_files ()
266290
267291 return ret
268292
@@ -303,23 +327,24 @@ def uninstall_plugin(cfg: plugin.models.PluginConfig, user=None, delete_config=T
303327 _ ('Plugin cannot be uninstalled as it is currently active' )
304328 )
305329
330+ if not cfg .is_installed ():
331+ raise ValidationError (_ ('Plugin is not installed' ))
332+
306333 validate_package_plugin (cfg , user )
307334 package_name = cfg .package_name
308- logger .info ('Uninstalling plugin: %s' , package_name )
309-
310- cmd = ['uninstall' , '-y' , package_name ]
311-
312- try :
313- result = pip_command (* cmd )
314335
315- ret = {
316- 'result' : _ ('Uninstalled plugin successfully' ),
317- 'success' : True ,
318- 'output' : str (result , 'utf-8' ),
319- }
336+ pkg_info = get_install_info (package_name )
320337
321- except subprocess .CalledProcessError as error :
322- handle_pip_error (error , 'plugin_uninstall' )
338+ if path := pkg_info .get ('location' ):
339+ # Uninstall the plugin using pip
340+ logger .info ('Uninstalling plugin: %s from %s' , package_name , path )
341+ try :
342+ pip_command ('uninstall' , '-y' , package_name )
343+ except subprocess .CalledProcessError as error :
344+ handle_pip_error (error , 'plugin_uninstall' )
345+ else :
346+ # No matching install target found
347+ raise ValidationError (_ ('Plugin installation not found' ))
323348
324349 # Update the plugins file
325350 update_plugins_file (package_name , remove = True )
@@ -334,4 +359,4 @@ def uninstall_plugin(cfg: plugin.models.PluginConfig, user=None, delete_config=T
334359 # Reload the plugin registry
335360 registry .reload_plugins (full_reload = True , force_reload = True , collect = True )
336361
337- return ret
362+ return { 'result' : _ ( 'Uninstalled plugin successfully' ), 'success' : True }
0 commit comments