import hashlib
import importlib.metadata
import json
import platform
import sys
from gettext import gettext
from importlib import resources
import click
import yaml
# Import from pyhf._version instead of pyhf to avoid a circular import, as
# pyhf.utils is imported while pyhf.__init__ is still executing
from pyhf._version import version as __version__
__all__ = [
"EqDelimStringParamType",
"citation",
"digest",
"environment_info",
"options_from_eqdelimstring",
]
def __dir__():
return __all__
[docs]
def options_from_eqdelimstring(opts):
document = "\n".join(
f"{opt.split('=', 1)[0]}: {opt.split('=', 1)[1]}" for opt in opts
)
return yaml.safe_load(document)
class EqDelimStringParamType(click.ParamType):
name = "equal-delimited option"
def convert(self, value, param, ctx):
try:
return options_from_eqdelimstring([value])
except IndexError:
self.fail(f"{value:s} is not a valid equal-delimited string", param, ctx)
class VolumeMountPath(click.Path):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = f"{self.name}:{gettext('path')}"
def convert(self, value, param, ctx):
try:
path_host, path_mount = value.split(":")
except ValueError:
# too many values to unpack / not enough values to unpack
self.fail(f"{value!r} is not a valid colon-separated option", param, ctx)
return (
super().convert(path_host, param, ctx),
self.coerce_path_result(path_mount),
)
[docs]
def digest(obj, algorithm="sha256"):
"""
Get the digest for the provided object. Note: object must be JSON-serializable.
The hashing algorithms supported are in :mod:`hashlib`, part of Python's Standard Libraries.
Example:
>>> import pyhf
>>> obj = {'a': 2.0, 'b': 3.0, 'c': 1.0}
>>> pyhf.utils.digest(obj)
'a38f6093800189b79bc22ef677baf90c75705af2cfc7ff594159eca54eaa7928'
>>> pyhf.utils.digest(obj, algorithm='md5')
'2c0633f242928eb55c3672fed5ba8612'
>>> pyhf.utils.digest(obj, algorithm='sha1')
'49a27f499e763766c9545b294880df277be6f545'
Raises:
ValueError: If the object is not JSON-serializable or if the algorithm is not supported.
Args:
obj (:obj:`jsonable`): A JSON-serializable object to compute the digest of. Usually a :class:`~pyhf.workspace.Workspace` object.
algorithm (:obj:`str`): The hashing algorithm to use.
Returns:
digest (:obj:`str`): The digest for the JSON-serialized object provided and hash algorithm specified.
"""
try:
stringified = json.dumps(obj, sort_keys=True, ensure_ascii=False).encode("utf8")
except TypeError as exc:
msg = "The supplied object is not JSON-serializable for calculating a hash."
raise ValueError(msg) from exc
try:
hash_alg = getattr(hashlib, algorithm)
except AttributeError as exc:
msg = f"{algorithm} is not an algorithm provided by Python's hashlib library."
raise ValueError(msg) from exc
return hash_alg(stringified).hexdigest()
[docs]
def citation(oneline=False):
"""
Get the bibtex citation for pyhf
Example:
>>> import pyhf
>>> pyhf.utils.citation(oneline=True)
'@software{pyhf, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark}, title = "{pyhf: v0.7.6}", version = {0.7.6}, doi = {10.5281/zenodo.1169739}, url = {https://doi.org/10.5281/zenodo.1169739}, note = {https://github.com/scikit-hep/pyhf/releases/tag/v0.7.6}}@article{pyhf_joss, doi = {10.21105/joss.02823}, url = {https://doi.org/10.21105/joss.02823}, year = {2021}, publisher = {The Open Journal}, volume = {6}, number = {58}, pages = {2823}, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark and Kyle Cranmer}, title = {pyhf: pure-Python implementation of HistFactory statistical models}, journal = {Journal of Open Source Software}}'
Keyword Args:
oneline (:obj:`bool`): Whether to provide citation with new lines (default) or as a one-liner.
Returns:
citation (:obj:`str`): The citation for this software
"""
ref = resources.files("pyhf") / "data" / "citation.bib"
with resources.as_file(ref) as path:
data = path.read_text().strip()
if oneline:
data = "".join(data.splitlines())
return data
[docs]
def environment_info():
"""
Produce OS / environment information useful for filing a bug report.
The output is formatted as a Markdown bullet list for easy copy-paste
into GitHub issues.
Example:
>>> import pyhf
>>> print(pyhf.utils.environment_info()) # doctest: +ELLIPSIS
* os version: ...
* kernel version: ...
* python version: ...
* pyhf version: ...
* numpy version: ...
* scipy version: ...
* iminuit version: ...
* jax version: ...
* jaxlib version: ...
Returns:
:obj:`str`: The operating system and environment information
for the host machine.
"""
os_version = "Cannot be determined"
if sys.platform == "linux":
try:
# platform.freedesktop_os_release added in Python 3.10
# FIXME: Remove when Python 3.9 support dropped
from platform import freedesktop_os_release
except ImportError:
# c.f. https://docs.python.org/3/library/platform.html#platform.freedesktop_os_release
from pathlib import Path
def freedesktop_os_release():
# Values may contain "=" and files may contain comment lines
# c.f. https://www.freedesktop.org/software/systemd/man/os-release.html
for os_release_path in (
Path("/etc/os-release"),
Path("/usr/lib/os-release"),
):
try:
with os_release_path.open(encoding="utf8") as read_file:
return {
key: value.strip("\"'")
for key, _, value in (
line.strip().partition("=")
for line in read_file
if "=" in line and not line.lstrip().startswith("#")
)
}
except OSError: # noqa: PERF203
continue
raise OSError
try:
os_release = freedesktop_os_release()
# ValueError covers UnicodeDecodeError from a non-UTF-8 os-release file
except (OSError, ValueError):
pass
else:
# VERSION is optional in the os-release spec (e.g. rolling releases)
os_version = " ".join(
part
for part in (
os_release.get("NAME"),
os_release.get("VERSION", os_release.get("VERSION_ID")),
)
if part
) or os_release.get("PRETTY_NAME", "Cannot be determined")
elif sys.platform == "darwin":
os_version = f"macOS {platform.mac_ver()[0]}"
lines = [
f"* os version: {os_version}",
f"* kernel version: {platform.system()} {platform.release()} {platform.machine()}",
f"* python version: {platform.python_implementation()} {platform.python_version()} [{platform.python_compiler().strip()}]",
f"* pyhf version: {__version__}",
]
# Guard all lookups as optional backends may be absent and even core
# dependencies could be installed in a strange way that they are importable
# without dist-info metadata. Show "not installed" instead of raising.
for package in ("numpy", "scipy", "iminuit", "jax", "jaxlib"):
try:
version = importlib.metadata.version(package)
except importlib.metadata.PackageNotFoundError:
version = "not installed"
lines.append(f"* {package} version: {version}")
return "\n".join(lines) + "\n"