Skip to content

Commit 28b5413

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 2718702 commit 28b5413

1 file changed

Lines changed: 151 additions & 51 deletions

File tree

khiops/core/internals/runner.py

Lines changed: 151 additions & 51 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
@@ -129,7 +130,7 @@ def _khiops_env_file_exists(env_dir):
129130

130131

131132
def _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

181190
def _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+
221266
class 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"\tError: {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"\tWarning: {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

Comments
 (0)