Skip to content

Commit 48d012b

Browse files
author
Thierry RAMORASOAVINA
committed
Detect "unhappy" installation states
- Warn when the version tuple (major, minor, patch) of Khiops does not match the Khiops Python library one - Fix the types of the returned objects in `_build_status_message` - Detect when the library is installed by something else than conda in a conda environment - Detect when the conda execution environment does not match the installation one (highly improbable case)
1 parent 645223c commit 48d012b

File tree

1 file changed

+138
-48
lines changed

1 file changed

+138
-48
lines changed

khiops/core/internals/runner.py

Lines changed: 138 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import uuid
2323
import warnings
2424
from abc import ABC, abstractmethod
25+
from importlib.metadata import PackageNotFoundError, files
2526
from pathlib import Path
2627

2728
import khiops
@@ -179,7 +180,18 @@ def _check_conda_env_bin_dir(conda_env_bin_dir):
179180

180181

181182
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+
"""
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+
221256
class 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"\tError: {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"\tWarning: {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

Comments
 (0)