-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
2,040 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
"""Wrap another Python package without any modifications.""" | ||
from argparse import REMAINDER, ArgumentParser, _SubParsersAction | ||
from dataclasses import dataclass | ||
from logging import ERROR, WARNING, log | ||
from shutil import which | ||
from subprocess import CalledProcessError, call as sub_call | ||
from sys import exit as sys_exit, version_info | ||
from typing import Any, Literal, Optional, TypedDict | ||
|
||
if version_info.minor < 9: # noqa: PLR2004 | ||
from typing import Dict | ||
else: | ||
Dict = dict # type: ignore | ||
|
||
from packaging.markers import Marker | ||
|
||
try: | ||
import tomllib | ||
except (ImportError, ModuleNotFoundError): | ||
import toml | ||
|
||
try: | ||
from ba_timeseries_gradients.parser import get_parser as gradients_parser | ||
except (ImportError, ModuleNotFoundError): | ||
gradients_parser = None | ||
|
||
from cpac.utils import get_project_root | ||
|
||
WRAPPED = {} | ||
"""memoization of wrapped packages""" | ||
|
||
|
||
class ScriptInfo(TypedDict): | ||
"""Info needed for bare package wrapping.""" | ||
|
||
command: str | ||
helpstring: Optional[str] | ||
url: str | ||
|
||
|
||
@dataclass | ||
class WrappedBare: | ||
"""Info needed for bare package wrapping.""" | ||
|
||
name: str | ||
command: str | ||
_helpstring: Optional[str] | ||
url: str | ||
|
||
@property | ||
def helpstring(self): | ||
"""Get the helpstring.""" | ||
if self._helpstring is None: | ||
return f"Extra script {self.command} not found. See {self.url} for more information, or run `pip install cpac[{self.name}]` to install it." | ||
return self._helpstring | ||
|
||
|
||
def add_bare_wrapper(parser: _SubParsersAction, command: str) -> None: | ||
"""Add a bare wrapper to the parser. | ||
Parameters | ||
---------- | ||
parser : _SubParsersAction | ||
The subparsers to add the wrapper to | ||
command : str | ||
The command to wrap | ||
""" | ||
from cpac.__main__ import ExtendAction | ||
|
||
bare_parser: ArgumentParser = parser.add_parser( | ||
command, usage=get_wrapped(command).helpstring | ||
) | ||
bare_parser.add_argument("args", nargs=REMAINDER) | ||
bare_parser.register("action", "extend", ExtendAction) | ||
|
||
|
||
def call(name: str, command: list) -> None: | ||
"""Call a bare-wrapped command. | ||
Parameters | ||
---------- | ||
name : str | ||
The name of the package to call | ||
command : list | ||
The command to run | ||
""" | ||
if version_info.minor < 11: # noqa: PLR2004 | ||
pyproject = toml.load(get_project_root() / "pyproject.toml") | ||
else: | ||
with open(get_project_root() / "pyproject.toml", "rb") as _f: | ||
pyproject = tomllib.load(_f) | ||
script = get_wrapped(name) | ||
try: | ||
package_info = get_nested( | ||
pyproject, ["tool", "poetry", "dependencies", script.command] | ||
) | ||
except KeyError as ke: | ||
raise KeyError(f"Package {name} not defined in dependencies") from ke | ||
marker = Marker(package_info["marker"]) if "marker" in package_info else None | ||
if marker and marker.evaluate() is False: | ||
raise EnvironmentError( | ||
f"Current environment does not meet requirements ({marker}) for package {name}" | ||
) | ||
if not check_for_package(script.command): | ||
sub_call(["poetry", "install", "--extras", name], cwd=get_project_root()) | ||
try: | ||
sub_call([script.command, *command]) | ||
except CalledProcessError as cpe: | ||
log(ERROR, str(cpe)) | ||
sys_exit(cpe.returncode) | ||
sys_exit(0) | ||
|
||
|
||
def check_for_package(package_name: str) -> bool: | ||
"""Check if a package is installed.""" | ||
return which(package_name) is not None | ||
|
||
|
||
def get_nested( | ||
dct: dict, keys: list, error: Literal["raise", "warn", None] = "raise" | ||
) -> Any: | ||
"""Get a nested dictionary value. | ||
Parameters | ||
---------- | ||
dct : dict | ||
The dictionary to search | ||
keys : Iterable | ||
The keys to search for | ||
error : Literal['raise', 'warn', None] | ||
How to handle errors: | ||
'raise' : raise an error | ||
'warn' : log a warning and return None | ||
None : return None without a warning | ||
Returns | ||
------- | ||
Any | ||
The value of the nested key or None | ||
Examples | ||
-------- | ||
>>> dct = {'a': {'b': {'c': 1}}} | ||
>>> get_nested(dct, ['a', 'b', 'c']) | ||
1 | ||
>>> get_nested(dct, ['a', 'b', 'd']) | ||
KeyError: 'd' | ||
>>> get_nested(dct, ['a', 'b', 'd'], error=None) | ||
None | ||
""" | ||
key = keys.pop(0) | ||
if key in dct: | ||
if len(keys) == 0: | ||
return dct[key] | ||
return get_nested(dct[key], keys, error) | ||
if error == "raise": | ||
return dct[key] | ||
if error == "warn": | ||
log(WARNING, "Key %s not found in %s", key, dct) | ||
return None | ||
|
||
|
||
def get_wrapped(name: str) -> WrappedBare: | ||
"""Get the name of the script to run. | ||
Parameters | ||
---------- | ||
name : str | ||
The name of the package to run | ||
Returns | ||
------- | ||
str | ||
The name of the script to run | ||
""" | ||
if name not in WRAPPED: | ||
scripts: Dict[str, ScriptInfo] = { | ||
"gradients": { | ||
"command": "ba_timeseries_gradients", | ||
"helpstring": None, | ||
"url": "https://cmi-dair.github.io/ba-timeseries-gradients/ba_timeseries_gradients.html", | ||
} | ||
} | ||
if gradients_parser is not None: | ||
scripts["gradients"]["helpstring"] = ( | ||
gradients_parser() | ||
.format_help() | ||
.replace("usage: ba_timeseries_gradients", "cpac gradients") | ||
) | ||
if name in scripts: | ||
WRAPPED[name] = WrappedBare( | ||
name, | ||
scripts[name]["command"], | ||
scripts[name]["helpstring"], | ||
scripts[name]["url"], | ||
) | ||
else: | ||
raise KeyError(f"Package {name} not defined in scripts") | ||
return WRAPPED[name] | ||
|
||
|
||
__all__ = ["add_bare_wrapper", "call", "WRAPPED"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters