Source code for pyhf.utils

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"