Skip to content

Commit

Permalink
Improve handling of optional packages (#27)
Browse files Browse the repository at this point in the history
Improves handling of optional packages by:

- No importing them just to check if available
- Raises a more specific type of error (and message)

### Test Plan

- Run unit tests
  • Loading branch information
nhairs authored Oct 21, 2024
1 parent bc807f5 commit a1daea2
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 16 deletions.
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

0 comments on commit a1daea2

Please sign in to comment.