2222import uuid
2323import warnings
2424from abc import ABC , abstractmethod
25+ from importlib .metadata import PackageNotFoundError , files
2526from pathlib import Path
2627
2728import khiops
@@ -129,7 +130,7 @@ def _khiops_env_file_exists(env_dir):
129130
130131
131132def _infer_env_bin_dir_for_conda_based_installations ():
132- """Infer reference directory for Conda-based Khiops installations"""
133+ """Infer reference directory for *supposed* Conda-based Khiops installations"""
133134 assert os .path .basename (Path (__file__ ).parents [2 ]) == "khiops" , (
134135 f"The { os .path .basename (__file__ )} file has been moved. "
135136 "Please fix the `Path.parents` in this method "
@@ -141,11 +142,19 @@ def _infer_env_bin_dir_for_conda_based_installations():
141142
142143 # Windows: Match %CONDA_PREFIX%\Lib\site-packages\khiops\core\internals\runner.py
143144 if platform .system () == "Windows" :
144- conda_env_dir = current_file_path .parents [5 ]
145+ # safeguard to prevent an IndexError on borderline installations
146+ if len (current_file_path .parents ) < 6 :
147+ conda_env_dir = ""
148+ else :
149+ conda_env_dir = current_file_path .parents [5 ]
145150 # Linux/macOS:
146151 # Match $CONDA_PREFIX/[Ll]ib/python3.X/site-packages/khiops/core/internals/runner.py
147152 else :
148- conda_env_dir = current_file_path .parents [6 ]
153+ # safeguard to prevent an IndexError on borderline installations
154+ if len (current_file_path .parents ) < 7 :
155+ conda_env_dir = ""
156+ else :
157+ conda_env_dir = current_file_path .parents [6 ]
149158 env_bin_dir = os .path .join (str (conda_env_dir ), "bin" )
150159
151160 return env_bin_dir
@@ -179,7 +188,20 @@ def _check_conda_env_bin_dir(conda_env_bin_dir):
179188
180189
181190def _infer_khiops_installation_method (trace = False ):
182- """Return the Khiops installation method"""
191+ """Return the Khiops installation method
192+
193+ Definitions :
194+ - 'conda' environment will contain binaries, shared libraries and python modules
195+ - 'conda-based' environment is quite similar to 'conda' except that
196+ it will not be activated previously nor during the execution
197+ and thus the CONDA_PREFIX environment variable will remain undefined
198+ and the path to the bin directory inside the conda environment
199+ will not be added to the PATH
200+ - 'binary+pip' installs the binaries and the shared libraries system-wide
201+ but will keep the python modules
202+ in the python system folder or in a virtual environment (if one is used)
203+
204+ """
183205 # We are in a conda environment if
184206 # - if the CONDA_PREFIX environment variable exists and,
185207 # - if MODL, MODL_Coclustering and mpiexec files exists in
@@ -218,6 +240,29 @@ def _check_executable(bin_path):
218240 )
219241
220242
243+ def _get_current_module_installer ():
244+ """Tells how the python module was installed
245+ in order to detect installation incompatibilities
246+
247+ Returns
248+ str
249+ 'pip'
250+ 'conda'
251+ or 'unknown'
252+ """
253+
254+ try :
255+ # Each time a python module is installed a 'dist-info' folder is created
256+ # Normalized files can be found in this folder
257+ installer_files = [path for path in files ("khiops" ) if path .name == "INSTALLER" ]
258+ if len (installer_files ) > 0 :
259+ return installer_files [0 ].read_text ().strip ()
260+ except PackageNotFoundError :
261+ # The python module is not installed via standard tools like conda, pip...
262+ pass
263+ return "unknown"
264+
265+
221266class KhiopsRunner (ABC ):
222267 """Abstract Khiops Python runner to be re-implemented"""
223268
@@ -294,7 +339,7 @@ def root_temp_dir(self, dir_path):
294339 )
295340 else :
296341 os .makedirs (real_dir_path )
297- # There are no checks for non local filesystems (no `else` statement)
342+ # There are no checks for non- local filesystems (no `else` statement)
298343 self ._root_temp_dir = dir_path
299344
300345 def create_temp_file (self , prefix , suffix ):
@@ -397,46 +442,86 @@ def _build_status_message(self):
397442 Returns
398443 -------
399444 tuple
400- A 2 -tuple containing:
445+ A 3 -tuple containing in this order :
401446 - The status message
402- - A list of warning messages
447+ - A list of error messages (str)
448+ - A list of warning messages (WarningMessage)
403449 """
404- # Capture the status of the the samples dir
450+ # Capture the status of the samples dir
405451 warning_list = []
406452 with warnings .catch_warnings (record = True ) as caught_warnings :
407453 samples_dir_path = self .samples_dir
408454 if caught_warnings is not None :
409455 warning_list += caught_warnings
410456
457+ package_dir = Path (__file__ ).parents [2 ]
458+
411459 status_msg = "Khiops Python library settings\n "
412460 status_msg += f"version : { khiops .__version__ } \n "
413461 status_msg += f"runner class : { self .__class__ .__name__ } \n "
414462 status_msg += f"root temp dir : { self .root_temp_dir } \n "
415463 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
464+ status_msg += f"package dir : { package_dir } \n "
465+
466+ errors_list = []
467+
468+ # Detect known incompatible installations with a conda environment
469+ if "CONDA_PREFIX" in os .environ :
470+ # If a conda environment is detected it must match the module installation
471+ # This check may be superfluous because a mismatch is highly improbable
472+ if not package_dir .as_posix ().startswith (os .environ ["CONDA_PREFIX" ]):
473+ error = (
474+ f"Khiops Python library installation path '{ package_dir } ' "
475+ f"does not match the current Conda environment "
476+ f"'{ os .environ ['CONDA_PREFIX' ]} '. "
477+ f"Please install the Khiops Python library "
478+ f"in the current Conda environment.\n "
479+ )
480+ errors_list .append (error )
481+ # Ensure no mix between conda and pip exists within a conda environment
482+ current_module_installer = _get_current_module_installer ()
483+ if current_module_installer != "conda" :
484+ error = (
485+ f"Khiops Python library installation was installed by "
486+ f"'{ current_module_installer } ' "
487+ f"while running in the Conda environment "
488+ f"'{ os .environ ['CONDA_PREFIX' ]} '. "
489+ f"Please install the Khiops Python library "
490+ f"using a Conda installer.\n "
491+ )
492+ errors_list .append (error )
493+
494+ return status_msg , errors_list , warning_list
418495
419496 def print_status (self ):
420497 """Prints the status of the runner to stdout"""
421498 # 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
499+
500+ status_msg , errors_list , warnings_list = self ._build_status_message ()
427501
428502 # Print status details
429503 print (status_msg , end = "" )
430504
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 } " )
505+ if errors_list or warnings_list :
506+ print ("Installation issues were detected:\n " )
507+ print ("---\n " )
508+
509+ # Print the errors (if any)
510+ if errors_list :
511+ print ("Errors to be fixed:" )
512+ for error in errors_list :
513+ print (f"\t Error: { error } \n " )
514+
515+ # Print the warnings (if any)
516+ if warnings_list :
517+ print ("Warnings:" )
518+ for warning in warnings_list :
519+ print (f"\t Warning: { warning .message } \n " )
520+
521+ if len (errors_list ) == 0 :
522+ return 0
437523 else :
438- print ("" )
439- return 0
524+ return 1
440525
441526 @abstractmethod
442527 def _initialize_khiops_version (self ):
@@ -955,21 +1040,28 @@ def _initialize_khiops_version(self):
9551040
9561041 self ._khiops_version = KhiopsVersion (khiops_version_str )
9571042
958- # Warn if the khiops version is too far from the Khiops Python library version
1043+ # Warn if the khiops version does not match the Khiops Python library version
1044+ # Currently the check is very strict
1045+ # (major.minor.patch must be the same), it could be relaxed later
9591046 compatible_khiops_version = khiops .get_compatible_khiops_version ()
960- if self ._khiops_version .major > compatible_khiops_version .major :
1047+ if (
1048+ (self ._khiops_version .major != compatible_khiops_version .major )
1049+ or (self ._khiops_version .minor != compatible_khiops_version .minor )
1050+ or (self ._khiops_version .patch != compatible_khiops_version .patch )
1051+ ):
9611052 warnings .warn (
962- f"Khiops version '{ self ._khiops_version } ' is ahead of "
963- f"the Khiops Python library version '{ khiops .__version__ } '. "
1053+ f"Khiops version '{ self ._khiops_version } ' does not match "
1054+ f"the Khiops Python library version '{ khiops .__version__ } ' "
1055+ "(different major.minor.patch version). "
9641056 "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." ,
1057+ "we recommend to update either Khiops or the Khiops Python library. "
1058+ "See https://khiops.org for more information." ,
9671059 stacklevel = 3 ,
9681060 )
9691061
9701062 def _build_status_message (self ):
9711063 # Call the parent's method
972- status_msg , warning_list = super ()._build_status_message ()
1064+ status_msg , errors_list , warnings_list = super ()._build_status_message ()
9731065
9741066 # Build the messages for install type and mpi
9751067 install_type_msg = _infer_khiops_installation_method ()
@@ -979,28 +1071,36 @@ def _build_status_message(self):
9791071 mpi_command_args_msg = "<empty>"
9801072
9811073 # 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 "
1074+ with warnings .catch_warnings (record = True ) as caught_warnings :
1075+ status_msg += "\n \n "
1076+ status_msg += "khiops local installation settings\n "
1077+ status_msg += f"version : { self .khiops_version } \n "
1078+ status_msg += f"Khiops path : { self .khiops_path } \n "
1079+ status_msg += f"Khiops CC path : { self .khiops_coclustering_path } \n "
1080+ status_msg += f"install type : { install_type_msg } \n "
1081+ status_msg += f"MPI command : { mpi_command_args_msg } \n "
1082+
1083+ # Add output of khiops -s which gives the MODL_* binary status
1084+ status_msg += "\n \n "
1085+ khiops_executable = os .path .join (
1086+ os .path .dirname (self .khiops_path ), "khiops"
1087+ )
1088+ status_msg += (
1089+ f"Khiops executable status (output of '{ khiops_executable } -s')\n "
1090+ )
1091+ stdout , stderr , return_code = self .raw_run ("khiops" , ["-s" ], use_mpi = True )
1092+
1093+ # On success retrieve the status and added to the message
1094+ if return_code == 0 :
1095+ status_msg += stdout
1096+ else :
1097+ errors_list .append (stderr )
1098+ status_msg += "\n "
1099+
1100+ if caught_warnings is not None :
1101+ warnings_list += caught_warnings
10021102
1003- return status_msg , warning_list
1103+ return status_msg , errors_list , warnings_list
10041104
10051105 def _get_khiops_version (self ):
10061106 # Initialize the first time it is called
0 commit comments