From 569d40ed7c96249492a335a5679c612fb0aecb1c Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Mon, 21 Oct 2024 20:59:19 +1100 Subject: [PATCH 1/4] Improve handling of optional packages --- pyproject.toml | 2 +- src/pythonjsonlogger/__init__.py | 17 ++------ src/pythonjsonlogger/exception.py | 27 +++++++++++++ src/pythonjsonlogger/msgspec.py | 6 ++- src/pythonjsonlogger/orjson.py | 6 ++- src/pythonjsonlogger/utils.py | 40 ++++++++++++++++++ tests/test_missing.py | 67 +++++++++++++++++++++++++++++++ 7 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 src/pythonjsonlogger/exception.py create mode 100644 src/pythonjsonlogger/utils.py create mode 100644 tests/test_missing.py diff --git a/pyproject.toml b/pyproject.toml index 40ec377..45a4b2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-json-logger" -version = "3.1.0" +version = "3.2.0" description = "JSON Log Formatter for the Python Logging Package" authors = [ {name = "Zakaria Zajac", email = "zak@madzak.com"}, 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..8dd47c5 --- /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, matches="orjson"): + import pythonjsonlogger.orjson + return + + +if not pythonjsonlogger.MSGSPEC_AVAILABLE: + + def test_orjson_import_error(): + with pytest.raises(MissingPackageError, matches="msgspec"): + import pythonjsonlogger.msgspec + return From 79be9de2ab70b83441a520a1fe0c0521d5db481d Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Mon, 21 Oct 2024 21:07:40 +1100 Subject: [PATCH 2/4] Fix typo --- tests/test_missing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_missing.py b/tests/test_missing.py index 8dd47c5..ccc2181 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -54,7 +54,7 @@ def test_package_not_available_throw_extras(): if not pythonjsonlogger.ORJSON_AVAILABLE: def test_orjson_import_error(): - with pytest.raises(MissingPackageError, matches="orjson"): + with pytest.raises(MissingPackageError, match="orjson"): import pythonjsonlogger.orjson return @@ -62,6 +62,6 @@ def test_orjson_import_error(): if not pythonjsonlogger.MSGSPEC_AVAILABLE: def test_orjson_import_error(): - with pytest.raises(MissingPackageError, matches="msgspec"): + with pytest.raises(MissingPackageError, match="msgspec"): import pythonjsonlogger.msgspec return From becbf3b27d4e919b927c201e3185a55efc5b0731 Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Mon, 21 Oct 2024 21:13:38 +1100 Subject: [PATCH 3/4] Don't bump version, update docs --- docs/changelog.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) 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/pyproject.toml b/pyproject.toml index 45a4b2f..40ec377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-json-logger" -version = "3.2.0" +version = "3.1.0" description = "JSON Log Formatter for the Python Logging Package" authors = [ {name = "Zakaria Zajac", email = "zak@madzak.com"}, From dd40cdc5a24a840d26693b2c76f6f74b82656d9d Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Mon, 21 Oct 2024 21:28:40 +1100 Subject: [PATCH 4/4] Fix typo --- tests/test_missing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_missing.py b/tests/test_missing.py index ccc2181..0878014 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -61,7 +61,7 @@ def test_orjson_import_error(): if not pythonjsonlogger.MSGSPEC_AVAILABLE: - def test_orjson_import_error(): + def test_msgspec_import_error(): with pytest.raises(MissingPackageError, match="msgspec"): import pythonjsonlogger.msgspec return