[ENH]: Implement a command to print the running environment details #33

Merged
synchon merged 11 commits from feature/wtf-cmd into main 2022-10-21 11:00:49 +00:00
4 changed files with 264 additions and 4 deletions

View file

@ -8,12 +8,20 @@ import pathlib
from typing import Dict, List, Union from typing import Dict, List, Union
import click import click
import yaml
from ..utils.logging import configure_logging, logger, warn_with_log from ..utils.logging import configure_logging, logger, warn_with_log
from .functions import collect as api_collect from .functions import collect as api_collect
from .functions import queue as api_queue from .functions import queue as api_queue
from .functions import run as api_run from .functions import run as api_run
from .parser import parse_yaml from .parser import parse_yaml
from .utils import (
_get_dependency_information,
_get_environment_information,
_get_junifer_version,
_get_python_information,
_get_system_information,
)
def _parse_elements(element: str, config: Dict) -> Union[List, None]: def _parse_elements(element: str, config: Dict) -> Union[List, None]:
@ -190,6 +198,21 @@ def queue(
@cli.command() @cli.command()
def selftest() -> None: @click.option("--long", "long_", is_flag=True)
"""Selftest command for CLI.""" def wtf(long_: bool) -> None:
pass """Wtf command for CLI.
Parameters
----------
long_ : bool
Whether to report long version or not.
"""
report = {
"junifer": _get_junifer_version(),
"python": _get_python_information(),
"dependencies": _get_dependency_information(long_=long_),
"system": _get_system_information(),
"environment": _get_environment_information(long_=long_),
}
click.echo(yaml.dump(report, sort_keys=False))

View file

@ -0,0 +1,89 @@
"""Provide tests for utils."""
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
import platform as pl
import pytest
from junifer._version import __version__
from junifer.api.utils import (
_get_dependency_information,
_get_environment_information,
_get_junifer_version,
_get_python_information,
_get_system_information,
)
def test_get_junifer_version() -> None:
"""Test _get_junifer_version()."""
assert _get_junifer_version()["version"] == __version__
def test_get_python_information() -> None:
"""Test _get_python_information()."""
python_information = _get_python_information()
assert python_information["version"] == pl.python_version()
assert python_information["implementation"] == pl.python_implementation()
def test_get_dependency_information_short() -> None:
"""Test short version of _get_dependency_information()."""
dependency_information = _get_dependency_information(long_=False)
assert [key for key in dependency_information.keys()] == [
"click",
"numpy",
"datalad",
"pandas",
"nibabel",
"nilearn",
"sqlalchemy",
"yaml",
]
def test_get_dependency_information_long() -> None:
"""Test long version of _get_dependency_information()."""
dependency_information = _get_dependency_information(long_=True)
dependency_information_keys = [
key for key in dependency_information.keys()
]
for key in [
"click",
"numpy",
"datalad",
"pandas",
"nibabel",
"nilearn",
"sqlalchemy",
"yaml",
]:
assert key in dependency_information_keys
def test_get_system_information() -> None:
"""Test _get_system_information()."""
system_information = _get_system_information()
assert system_information["platform"] == pl.platform()
@pytest.mark.parametrize(
"format_",
[
"short",
"long",
],
)
def test_get_environment_information(format_: str) -> None:
"""Test _get_environment_information().
Parameters
----------
format_ : str
The parametrized report version.
"""
environment_information = _get_environment_information(long_=format_)
assert "PATH" in environment_information.keys()

View file

@ -11,7 +11,7 @@ import pytest
import yaml import yaml
from click.testing import CliRunner from click.testing import CliRunner
from junifer.api.cli import collect, run from junifer.api.cli import collect, run, wtf
# Create click test runner # Create click test runner
@ -68,3 +68,19 @@ def test_run_and_collect_commands(
collect_result = runner.invoke(collect, collect_args) collect_result = runner.invoke(collect, collect_args)
# Check # Check
assert collect_result.exit_code == 0 assert collect_result.exit_code == 0
def test_wtf_short() -> None:
"""Test short version of wtf command."""
# Invoke wtf command
wtf_result = runner.invoke(wtf)
# Check
assert wtf_result.exit_code == 0
def test_wtf_long() -> None:
"""Test long version of wtf command."""
# Invoke wtf command
wtf_result = runner.invoke(wtf, "--long")
# Check
assert wtf_result.exit_code == 0

132
junifer/api/utils.py Normal file
View file

@ -0,0 +1,132 @@
"""Provide utility functions for the api sub-package."""
fraimondo commented 2022-10-20 06:40:24 +00:00 (Migrated from github.com)

I would go full environment versions here, not only dependencies.

I would go full environment versions here, not only dependencies.
synchon commented 2022-10-20 07:15:55 +00:00 (Migrated from github.com)

Ignored built-in modules as it doesn't make sense.

Ignored built-in modules as it doesn't make sense.
fraimondo commented 2022-10-20 15:58:44 +00:00 (Migrated from github.com)

Can we read this from the pyproject.toml file?

Or we need to manually hardcode here?

I have the impression that it will end-up like the _version.py file, which is created on build/install

Can we read this from the pyproject.toml file? Or we need to manually hardcode here? I have the impression that it will end-up like the `_version.py` file, which is created on build/install
synchon commented 2022-10-20 16:18:42 +00:00 (Migrated from github.com)

The only reason I want to avoid reading from pyproject.toml is because of the extra I/O. The dependencies are not going to change frequently so hardcoding it doesn't seem like a bad idea.

I don't really get the analogy with the _version.py though.

The only reason I want to avoid reading from `pyproject.toml` is because of the extra I/O. The dependencies are not going to change frequently so hardcoding it doesn't seem like a bad idea. I don't really get the analogy with the `_version.py` though.
fraimondo commented 2022-10-20 16:24:17 +00:00 (Migrated from github.com)

On build/install time, a file _dependencies.py is created with an array that contains the dependencies.

Soon, we will start having "conditional" dependencies in which some specific markers will require some dependencies. I'm just trying to avoid having always in mind that we need to manage dependencies in two different sites.

On build/install time, a file `_dependencies.py` is created with an array that contains the dependencies. Soon, we will start having "conditional" dependencies in which some specific markers will require some dependencies. I'm just trying to avoid having always in mind that we need to manage dependencies in two different sites.
fraimondo commented 2022-10-20 16:29:13 +00:00 (Migrated from github.com)

e.g.: pip install junifer[mri] pip install junifer[eeg] pip install junifer[surf] pip install junifer[dmri] ... etc

e.g.: `pip install junifer[mri]` `pip install junifer[eeg]` `pip install junifer[surf]` `pip install junifer[dmri]` ... etc
synchon commented 2022-10-20 16:39:44 +00:00 (Migrated from github.com)

On build/install time, a file _dependencies.py is created with an array that contains the dependencies.

Soon, we will start having "conditional" dependencies in which some specific markers will require some dependencies. I'm just trying to avoid having always in mind that we need to manage dependencies in two different sites.

I don't remember seeing _dependencies.py but will check it.

> On build/install time, a file `_dependencies.py` is created with an array that contains the dependencies. > > Soon, we will start having "conditional" dependencies in which some specific markers will require some dependencies. I'm just trying to avoid having always in mind that we need to manage dependencies in two different sites. I don't remember seeing `_dependencies.py` but will check it.
synchon commented 2022-10-20 16:43:20 +00:00 (Migrated from github.com)

e.g.: pip install junifer[mri] pip install junifer[eeg] pip install junifer[surf] pip install junifer[dmri] ... etc

I see what you mean. These conditional dependencies would go in the [project.optional-dependencies] block of the pyproject.toml like we have now for dev and docs. So, the "base" dependencies for example, numpy or nilearn stay the same and we can check for them. But, I agree that maintaining dependencies in two places is a bit troublesome. Let me see if I can find a way to hook into the setuptools or something similar to extract information without requiring extra I/O.

> e.g.: `pip install junifer[mri]` `pip install junifer[eeg]` `pip install junifer[surf]` `pip install junifer[dmri]` ... etc I see what you mean. These conditional dependencies would go in the `[project.optional-dependencies]` block of the `pyproject.toml` like we have now for `dev` and `docs`. So, the "base" dependencies for example, `numpy` or `nilearn` stay the same and we can check for them. But, I agree that maintaining dependencies in two places is a bit troublesome. Let me see if I can find a way to hook into the setuptools or something similar to extract information without requiring extra I/O.
synchon commented 2022-10-21 10:39:30 +00:00 (Migrated from github.com)

Now implemented with an improved regex search.

Now implemented with an improved regex search.
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
import os
import platform as pl
import re
from importlib.metadata import distribution
from typing import Dict
from .._version import __version__
from ..utils.logging import get_versions
def _get_junifer_version() -> Dict[str, str]:
"""Get junifer version information.
Returns
-------
dict
A dictionary containing junifer version.
"""
return {
"version": __version__,
}
def _get_python_information() -> Dict[str, str]:
"""Get installed Python information.
Parameters
----------
dict
A dictionary containing Python information.
"""
return {
"version": pl.python_version(),
"implementation": pl.python_implementation(),
}
def _get_dependency_information(long_: bool) -> Dict[str, str]:
"""Get Python environment dependency information.
Parameters
----------
long_ : bool
Whether to report long version.
Returns
-------
dict
A dictionary containing Python dependency information.
"""
dependency_versions = get_versions()
pruned_dependency_versions = {}
# Report long version
if long_:
for key, value in dependency_versions.items():
# Ignore built-in modules and self
if value != "None" and key != "junifer":
pruned_dependency_versions[key] = value
# Report short version
else:
# Get dependencies for junifer
dist = distribution("junifer")
# Compile regex pattern
re_pattern = re.compile("[a-z-]+")
for pkg_with_version in dist.requires:
# Perform regex search
matches = re.findall(pattern=re_pattern, string=pkg_with_version)
# Fix issue with PyYAML name registration
if matches[0] == "pyyaml":
key = "yaml"
else:
key = matches[0]
if key in dependency_versions.keys():
# Check if pkg part of optional dependencies
if "extra" not in matches:
pruned_dependency_versions[key] = dependency_versions[key]
return pruned_dependency_versions
def _get_system_information() -> Dict[str, str]:
"""Get system information.
Returns
-------
dict
A dictionary containing system information.
"""
return {
"platform": pl.platform(),
}
def _get_environment_information(long_: bool) -> Dict[str, str]:
"""Get system environment information.
Parameters
----------
long_ : bool
Whether to report long version.
Returns
-------
dict
A dictionary containing system environment information.
"""
environment_values = {}
# Report long version
if long_:
for key, value in os.environ.items():
environment_values[key] = value
# Report short version
else:
for key in ["LC_CTYPE", "LC_TERMINAL", "LC_TERMINAL_VERSION", "PATH"]:
if key in os.environ.keys():
environment_values[key] = os.environ[key]
return environment_values