Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of optional packages #27

Merged
merged 4 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 3 additions & 14 deletions src/pythonjsonlogger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions src/pythonjsonlogger/exception.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion src/pythonjsonlogger/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/pythonjsonlogger/orjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions src/pythonjsonlogger/utils.py
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions tests/test_missing.py
Original file line number Diff line number Diff line change
@@ -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
Loading