diff --git a/docs/changelog.md b/docs/changelog.md index 720708c..144f370 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.2.0](https://github.com/nhairs/python-json-logger/compare/v3.1.0...v3.2.0) - UNRELEASED + +### Changed +- `pythonjsonlogger.[ORJSON,MSGSPEC]_AVAILABLE` no longer imports the respective package when determining availability. +- `pythonjsonlogger.[orjson,msgspec]` now throws a `pythonjsonlogger.exception.MissingPackageError` when required libraries are not available. These contain more information about what is missing whilst still being an `ImportError`. + ## [3.1.0](https://github.com/nhairs/python-json-logger/compare/v3.0.1...v3.1.0) - 2023-05-28 This splits common funcitonality out to allow supporting other JSON encoders. Although this is a large refactor, backwards compatibility has been maintained. diff --git a/src/pythonjsonlogger/__init__.py b/src/pythonjsonlogger/__init__.py index ed3ae60..2eee544 100644 --- a/src/pythonjsonlogger/__init__.py +++ b/src/pythonjsonlogger/__init__.py @@ -9,23 +9,12 @@ ## Application import pythonjsonlogger.json +import pythonjsonlogger.utils ### CONSTANTS ### ============================================================================ -try: - import orjson - - ORJSON_AVAILABLE = True -except ImportError: - ORJSON_AVAILABLE = False - - -try: - import msgspec - - MSGSPEC_AVAILABLE = True -except ImportError: - MSGSPEC_AVAILABLE = False +ORJSON_AVAILABLE = pythonjsonlogger.utils.package_is_available("orjson") +MSGSPEC_AVAILABLE = pythonjsonlogger.utils.package_is_available("msgspec") ### DEPRECATED COMPATIBILITY diff --git a/src/pythonjsonlogger/exception.py b/src/pythonjsonlogger/exception.py new file mode 100644 index 0000000..1233f1a --- /dev/null +++ b/src/pythonjsonlogger/exception.py @@ -0,0 +1,27 @@ +### IMPORTS +### ============================================================================ +## Future +from __future__ import annotations + +## Standard Library + +## Installed + +## Application + + +### CLASSES +### ============================================================================ +class PythonJsonLoggerError(Exception): + "Generic base clas for all Python JSON Logger exceptions" + + +class MissingPackageError(ImportError, PythonJsonLoggerError): + "A required package is missing" + + def __init__(self, name: str, extras_name: str | None = None) -> None: + msg = f"The {name!r} package is required but could not be found." + if extras_name is not None: + msg += f" It can be installed using 'python-json-logger[{extras_name}]'." + super().__init__(msg) + return diff --git a/src/pythonjsonlogger/msgspec.py b/src/pythonjsonlogger/msgspec.py index 9208240..8646f85 100644 --- a/src/pythonjsonlogger/msgspec.py +++ b/src/pythonjsonlogger/msgspec.py @@ -9,11 +9,15 @@ from typing import Any ## Installed -import msgspec.json ## Application from . import core from . import defaults as d +from .utils import package_is_available + +# We import msgspec after checking it is available +package_is_available("msgspec", throw_error=True) +import msgspec.json # pylint: disable=wrong-import-position,wrong-import-order ### FUNCTIONS diff --git a/src/pythonjsonlogger/orjson.py b/src/pythonjsonlogger/orjson.py index 3e9ea30..16db842 100644 --- a/src/pythonjsonlogger/orjson.py +++ b/src/pythonjsonlogger/orjson.py @@ -9,11 +9,15 @@ from typing import Any ## Installed -import orjson ## Application from . import core from . import defaults as d +from .utils import package_is_available + +# We import msgspec after checking it is available +package_is_available("orjson", throw_error=True) +import orjson # pylint: disable=wrong-import-position,wrong-import-order ### FUNCTIONS diff --git a/src/pythonjsonlogger/utils.py b/src/pythonjsonlogger/utils.py new file mode 100644 index 0000000..d810a13 --- /dev/null +++ b/src/pythonjsonlogger/utils.py @@ -0,0 +1,40 @@ +"""Utilities for Python JSON Logger""" + +### IMPORTS +### ============================================================================ +## Future +from __future__ import annotations + +## Standard Library +import importlib.util + +## Installed + +## Application +from .exception import MissingPackageError + + +### FUNCTIONS +### ============================================================================ +def package_is_available( + name: str, *, throw_error: bool = False, extras_name: str | None = None +) -> bool: + """Determine if the given package is available for import. + + Args: + name: Import name of the package to check. + throw_error: Throw an error if the package is unavailable. + extras_name: Extra dependency name to use in `throw_error`'s message. + + Raises: + MissingPackageError: When `throw_error` is `True` and the return value would be `False` + + Returns: + If the package is available for import. + """ + available = importlib.util.find_spec(name) is not None + + if not available and throw_error: + raise MissingPackageError(name, extras_name) + + return available diff --git a/tests/test_missing.py b/tests/test_missing.py new file mode 100644 index 0000000..0878014 --- /dev/null +++ b/tests/test_missing.py @@ -0,0 +1,67 @@ +### IMPORTS +### ============================================================================ +## Future +from __future__ import annotations + +## Standard Library + +## Installed +import pytest + +## Application +import pythonjsonlogger +from pythonjsonlogger.utils import package_is_available +from pythonjsonlogger.exception import MissingPackageError + +### CONSTANTS +### ============================================================================ +MISSING_PACKAGE_NAME = "package_name_is_definintely_not_available" +MISSING_PACKAGE_EXTRA = "package_extra_that_is_unique" + + +### TESTS +### ============================================================================ +def test_package_is_available(): + assert package_is_available("json") + return + + +def test_package_not_available(): + assert not package_is_available(MISSING_PACKAGE_NAME) + return + + +def test_package_not_available_throw(): + with pytest.raises(MissingPackageError) as e: + package_is_available(MISSING_PACKAGE_NAME, throw_error=True) + assert MISSING_PACKAGE_NAME in e.value.msg + assert MISSING_PACKAGE_EXTRA not in e.value.msg + return + + +def test_package_not_available_throw_extras(): + with pytest.raises(MissingPackageError) as e: + package_is_available( + MISSING_PACKAGE_NAME, throw_error=True, extras_name=MISSING_PACKAGE_EXTRA + ) + assert MISSING_PACKAGE_NAME in e.value.msg + assert MISSING_PACKAGE_EXTRA in e.value.msg + return + + +## Python JSON Logger Specific +## ----------------------------------------------------------------------------- +if not pythonjsonlogger.ORJSON_AVAILABLE: + + def test_orjson_import_error(): + with pytest.raises(MissingPackageError, match="orjson"): + import pythonjsonlogger.orjson + return + + +if not pythonjsonlogger.MSGSPEC_AVAILABLE: + + def test_msgspec_import_error(): + with pytest.raises(MissingPackageError, match="msgspec"): + import pythonjsonlogger.msgspec + return