2222import uuid
2323import warnings
2424from abc import ABC , abstractmethod
25+ from importlib .metadata import PackageNotFoundError , files
2526from pathlib import Path
2627
2728import khiops
@@ -179,7 +180,18 @@ def _check_conda_env_bin_dir(conda_env_bin_dir):
179180
180181
181182def _infer_khiops_installation_method (trace = False ):
182- """Return the Khiops installation method"""
183+ """Return the Khiops installation method
184+
185+ Definitions :
186+ - 'conda' environment will contain binaries, shared libraries and python modules
187+ - 'conda-based' environment is quite similar to 'conda' except that
188+ it will not be activated previously nor during the execution
189+ and thus the CONDA_PREFIX environment variable will remain undefined
190+ - 'binary+pip' installs the binaries and the shared libraries system-wide
191+ but will keep the python modules
192+ in the python system folder or in a virtual environment (if one is used)
193+
194+ """
183195 # We are in a conda environment if
184196 # - if the CONDA_PREFIX environment variable exists and,
185197 # - if MODL, MODL_Coclustering and mpiexec files exists in
@@ -218,6 +230,29 @@ def _check_executable(bin_path):
218230 )
219231
220232
233+ def _get_current_module_installer ():
234+ """Tells how the python module was installed
235+ in order to detect installation incompatibilities
236+
237+ Returns
238+ str
239+ 'pip'
240+ 'conda'
241+ or 'unknown'
242+ """
243+
244+ try :
245+ # Each time a python module is installed a 'dist-info' folder is created
246+ # Normalized files can be found in this folder
247+ installer_files = [path for path in files ("khiops" ) if path .name == "INSTALLER" ]
248+ if len (installer_files ) > 0 :
249+ return installer_files [0 ].read_text ().strip ()
250+ except PackageNotFoundError :
251+ # The python module is not installed via standard tools like conda, pip...
252+ pass
253+ return "unknown"
254+
255+
221256class KhiopsRunner (ABC ):
222257 """Abstract Khiops Python runner to be re-implemented"""
223258
@@ -294,7 +329,7 @@ def root_temp_dir(self, dir_path):
294329 )
295330 else :
296331 os .makedirs (real_dir_path )
297- # There are no checks for non local filesystems (no `else` statement)
332+ # There are no checks for non- local filesystems (no `else` statement)
298333 self ._root_temp_dir = dir_path
299334
300335 def create_temp_file (self , prefix , suffix ):
@@ -397,46 +432,86 @@ def _build_status_message(self):
397432 Returns
398433 -------
399434 tuple
400- A 2 -tuple containing:
435+ A 3 -tuple containing in this order :
401436 - The status message
402- - A list of warning messages
437+ - A list of error messages (str)
438+ - A list of warning messages (WarningMessage)
403439 """
404- # Capture the status of the the samples dir
440+ # Capture the status of the samples dir
405441 warning_list = []
406442 with warnings .catch_warnings (record = True ) as caught_warnings :
407443 samples_dir_path = self .samples_dir
408444 if caught_warnings is not None :
409445 warning_list += caught_warnings
410446
447+ package_dir = Path (__file__ ).parents [2 ]
448+
411449 status_msg = "Khiops Python library settings\n "
412450 status_msg += f"version : { khiops .__version__ } \n "
413451 status_msg += f"runner class : { self .__class__ .__name__ } \n "
414452 status_msg += f"root temp dir : { self .root_temp_dir } \n "
415453 status_msg += f"sample datasets dir : { samples_dir_path } \n "
416- status_msg += f"package dir : { Path (__file__ ).parents [2 ]} \n "
417- return status_msg , warning_list
454+ status_msg += f"package dir : { package_dir } \n "
455+
456+ errors_list = []
457+
458+ # Detect known incompatible installations with a conda environment
459+ if "CONDA_PREFIX" in os .environ :
460+ # If a conda environment is detected it must match the module installation
461+ # This check may be superfluous because a mismatch is highly improbable
462+ if not package_dir .as_posix ().startswith (os .environ ["CONDA_PREFIX" ]):
463+ error = (
464+ f"Khiops Python library installation path '{ package_dir } ' "
465+ f"does not match the current Conda environment "
466+ f"'{ os .environ ['CONDA_PREFIX' ]} '. "
467+ f"Please install the Khiops Python library "
468+ f"in the current Conda environment.\n "
469+ )
470+ errors_list .append (error )
471+ # Ensure no mix between conda and pip exists within a conda environment
472+ current_module_installer = _get_current_module_installer ()
473+ if current_module_installer != "conda" :
474+ error = (
475+ f"Khiops Python library installation was installed by "
476+ f"'{ current_module_installer } ' "
477+ f"while running in the Conda environment "
478+ f"'{ os .environ ['CONDA_PREFIX' ]} '. "
479+ f"Please install the Khiops Python library "
480+ f"using a Conda installer.\n "
481+ )
482+ errors_list .append (error )
483+
484+ return status_msg , errors_list , warning_list
418485
419486 def print_status (self ):
420487 """Prints the status of the runner to stdout"""
421488 # Obtain the status_msg, errors and warnings
422- try :
423- status_msg , warning_list = self ._build_status_message ()
424- except (KhiopsEnvironmentError , KhiopsRuntimeError ) as error :
425- print (f"Khiops Python library status KO: { error } " )
426- return 1
489+
490+ status_msg , errors_list , warnings_list = self ._build_status_message ()
427491
428492 # Print status details
429493 print (status_msg , end = "" )
430494
431- # Print status
432- print ("Khiops Python library status OK" , end = "" )
433- if warning_list :
434- print (", with warnings:" )
435- for warning in warning_list :
436- print (f"warning: { warning .message } " )
495+ if errors_list or warnings_list :
496+ print ("Installation issues were detected:\n " )
497+ print ("---\n " )
498+
499+ # Print the errors (if any)
500+ if errors_list :
501+ print ("Errors to be fixed:" )
502+ for error in errors_list :
503+ print (f"\t Error: { error } \n " )
504+
505+ # Print the warnings (if any)
506+ if warnings_list :
507+ print ("Warnings:" )
508+ for warning in warnings_list :
509+ print (f"\t Warning: { warning .message } \n " )
510+
511+ if len (errors_list ) == 0 :
512+ return 0
437513 else :
438- print ("" )
439- return 0
514+ return 1
440515
441516 @abstractmethod
442517 def _initialize_khiops_version (self ):
@@ -955,21 +1030,28 @@ def _initialize_khiops_version(self):
9551030
9561031 self ._khiops_version = KhiopsVersion (khiops_version_str )
9571032
958- # Warn if the khiops version is too far from the Khiops Python library version
1033+ # Warn if the khiops version does not match the Khiops Python library version
1034+ # Currently the check is very strict
1035+ # (major.minor.patch must be the same), it could be relaxed later
9591036 compatible_khiops_version = khiops .get_compatible_khiops_version ()
960- if self ._khiops_version .major > compatible_khiops_version .major :
1037+ if (
1038+ (self ._khiops_version .major != compatible_khiops_version .major )
1039+ or (self ._khiops_version .minor != compatible_khiops_version .minor )
1040+ or (self ._khiops_version .patch != compatible_khiops_version .patch )
1041+ ):
9611042 warnings .warn (
962- f"Khiops version '{ self ._khiops_version } ' is ahead of "
963- f"the Khiops Python library version '{ khiops .__version__ } '. "
1043+ f"Khiops version '{ self ._khiops_version } ' does not match "
1044+ f"the Khiops Python library version '{ khiops .__version__ } ' "
1045+ "(different major.minor.patch version). "
9641046 "There may be compatibility errors and "
965- "we recommend you to update to the latest Khiops Python "
966- "library version. See https://khiops.org for more information." ,
1047+ "we recommend to update either Khiops or the Khiops Python library. "
1048+ "See https://khiops.org for more information." ,
9671049 stacklevel = 3 ,
9681050 )
9691051
9701052 def _build_status_message (self ):
9711053 # Call the parent's method
972- status_msg , warning_list = super ()._build_status_message ()
1054+ status_msg , errors_list , warnings_list = super ()._build_status_message ()
9731055
9741056 # Build the messages for install type and mpi
9751057 install_type_msg = _infer_khiops_installation_method ()
@@ -979,28 +1061,36 @@ def _build_status_message(self):
9791061 mpi_command_args_msg = "<empty>"
9801062
9811063 # Build the message
982- status_msg += "\n \n "
983- status_msg += "khiops local installation settings\n "
984- status_msg += f"version : { self .khiops_version } \n "
985- status_msg += f"Khiops path : { self .khiops_path } \n "
986- status_msg += f"Khiops CC path : { self .khiops_coclustering_path } \n "
987- status_msg += f"install type : { install_type_msg } \n "
988- status_msg += f"MPI command : { mpi_command_args_msg } \n "
989-
990- # Add output of khiops -s which gives the MODL_* binary status
991- status_msg += "\n \n "
992- khiops_executable = os .path .join (os .path .dirname (self .khiops_path ), "khiops" )
993- status_msg += f"Khiops executable status (output of '{ khiops_executable } -s')\n "
994- stdout , stderr , return_code = self .raw_run ("khiops" , ["-s" ], use_mpi = True )
995-
996- # On success retrieve the status and added to the message
997- if return_code == 0 :
998- status_msg += stdout
999- else :
1000- warning_list .append (stderr )
1001- status_msg += "\n "
1064+ with warnings .catch_warnings (record = True ) as caught_warnings :
1065+ status_msg += "\n \n "
1066+ status_msg += "khiops local installation settings\n "
1067+ status_msg += f"version : { self .khiops_version } \n "
1068+ status_msg += f"Khiops path : { self .khiops_path } \n "
1069+ status_msg += f"Khiops CC path : { self .khiops_coclustering_path } \n "
1070+ status_msg += f"install type : { install_type_msg } \n "
1071+ status_msg += f"MPI command : { mpi_command_args_msg } \n "
1072+
1073+ # Add output of khiops -s which gives the MODL_* binary status
1074+ status_msg += "\n \n "
1075+ khiops_executable = os .path .join (
1076+ os .path .dirname (self .khiops_path ), "khiops"
1077+ )
1078+ status_msg += (
1079+ f"Khiops executable status (output of '{ khiops_executable } -s')\n "
1080+ )
1081+ stdout , stderr , return_code = self .raw_run ("khiops" , ["-s" ], use_mpi = True )
1082+
1083+ # On success retrieve the status and added to the message
1084+ if return_code == 0 :
1085+ status_msg += stdout
1086+ else :
1087+ errors_list .append (stderr )
1088+ status_msg += "\n "
1089+
1090+ if caught_warnings is not None :
1091+ warnings_list += caught_warnings
10021092
1003- return status_msg , warning_list
1093+ return status_msg , errors_list , warnings_list
10041094
10051095 def _get_khiops_version (self ):
10061096 # Initialize the first time it is called
0 commit comments