22
22
import uuid
23
23
import warnings
24
24
from abc import ABC , abstractmethod
25
+ from importlib .metadata import PackageNotFoundError , files
25
26
from pathlib import Path
26
27
27
28
import khiops
@@ -179,7 +180,18 @@ def _check_conda_env_bin_dir(conda_env_bin_dir):
179
180
180
181
181
182
def _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
+ """
183
195
# We are in a conda environment if
184
196
# - if the CONDA_PREFIX environment variable exists and,
185
197
# - if MODL, MODL_Coclustering and mpiexec files exists in
@@ -218,6 +230,29 @@ def _check_executable(bin_path):
218
230
)
219
231
220
232
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
+
221
256
class KhiopsRunner (ABC ):
222
257
"""Abstract Khiops Python runner to be re-implemented"""
223
258
@@ -294,7 +329,7 @@ def root_temp_dir(self, dir_path):
294
329
)
295
330
else :
296
331
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)
298
333
self ._root_temp_dir = dir_path
299
334
300
335
def create_temp_file (self , prefix , suffix ):
@@ -397,46 +432,86 @@ def _build_status_message(self):
397
432
Returns
398
433
-------
399
434
tuple
400
- A 2 -tuple containing:
435
+ A 3 -tuple containing in this order :
401
436
- The status message
402
- - A list of warning messages
437
+ - A list of error messages (str)
438
+ - A list of warning messages (WarningMessage)
403
439
"""
404
- # Capture the status of the the samples dir
440
+ # Capture the status of the samples dir
405
441
warning_list = []
406
442
with warnings .catch_warnings (record = True ) as caught_warnings :
407
443
samples_dir_path = self .samples_dir
408
444
if caught_warnings is not None :
409
445
warning_list += caught_warnings
410
446
447
+ package_dir = Path (__file__ ).parents [2 ]
448
+
411
449
status_msg = "Khiops Python library settings\n "
412
450
status_msg += f"version : { khiops .__version__ } \n "
413
451
status_msg += f"runner class : { self .__class__ .__name__ } \n "
414
452
status_msg += f"root temp dir : { self .root_temp_dir } \n "
415
453
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
418
485
419
486
def print_status (self ):
420
487
"""Prints the status of the runner to stdout"""
421
488
# 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 ()
427
491
428
492
# Print status details
429
493
print (status_msg , end = "" )
430
494
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
437
513
else :
438
- print ("" )
439
- return 0
514
+ return 1
440
515
441
516
@abstractmethod
442
517
def _initialize_khiops_version (self ):
@@ -955,21 +1030,28 @@ def _initialize_khiops_version(self):
955
1030
956
1031
self ._khiops_version = KhiopsVersion (khiops_version_str )
957
1032
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
959
1036
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
+ ):
961
1042
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). "
964
1046
"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." ,
967
1049
stacklevel = 3 ,
968
1050
)
969
1051
970
1052
def _build_status_message (self ):
971
1053
# 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 ()
973
1055
974
1056
# Build the messages for install type and mpi
975
1057
install_type_msg = _infer_khiops_installation_method ()
@@ -979,28 +1061,36 @@ def _build_status_message(self):
979
1061
mpi_command_args_msg = "<empty>"
980
1062
981
1063
# 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
1002
1092
1003
- return status_msg , warning_list
1093
+ return status_msg , errors_list , warnings_list
1004
1094
1005
1095
def _get_khiops_version (self ):
1006
1096
# Initialize the first time it is called
0 commit comments