Skip to content

Commit a1daea2

Browse files
authored
Improve handling of optional packages (#27)
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
1 parent bc807f5 commit a1daea2

File tree

7 files changed

+153
-16
lines changed

7 files changed

+153
-16
lines changed

docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [3.2.0](https://github.com/nhairs/python-json-logger/compare/v3.1.0...v3.2.0) - UNRELEASED
8+
9+
### Changed
10+
- `pythonjsonlogger.[ORJSON,MSGSPEC]_AVAILABLE` no longer imports the respective package when determining availability.
11+
- `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`.
12+
713
## [3.1.0](https://github.com/nhairs/python-json-logger/compare/v3.0.1...v3.1.0) - 2023-05-28
814

915
This splits common funcitonality out to allow supporting other JSON encoders. Although this is a large refactor, backwards compatibility has been maintained.

src/pythonjsonlogger/__init__.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,12 @@
99

1010
## Application
1111
import pythonjsonlogger.json
12+
import pythonjsonlogger.utils
1213

1314
### CONSTANTS
1415
### ============================================================================
15-
try:
16-
import orjson
17-
18-
ORJSON_AVAILABLE = True
19-
except ImportError:
20-
ORJSON_AVAILABLE = False
21-
22-
23-
try:
24-
import msgspec
25-
26-
MSGSPEC_AVAILABLE = True
27-
except ImportError:
28-
MSGSPEC_AVAILABLE = False
16+
ORJSON_AVAILABLE = pythonjsonlogger.utils.package_is_available("orjson")
17+
MSGSPEC_AVAILABLE = pythonjsonlogger.utils.package_is_available("msgspec")
2918

3019

3120
### DEPRECATED COMPATIBILITY

src/pythonjsonlogger/exception.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
### IMPORTS
2+
### ============================================================================
3+
## Future
4+
from __future__ import annotations
5+
6+
## Standard Library
7+
8+
## Installed
9+
10+
## Application
11+
12+
13+
### CLASSES
14+
### ============================================================================
15+
class PythonJsonLoggerError(Exception):
16+
"Generic base clas for all Python JSON Logger exceptions"
17+
18+
19+
class MissingPackageError(ImportError, PythonJsonLoggerError):
20+
"A required package is missing"
21+
22+
def __init__(self, name: str, extras_name: str | None = None) -> None:
23+
msg = f"The {name!r} package is required but could not be found."
24+
if extras_name is not None:
25+
msg += f" It can be installed using 'python-json-logger[{extras_name}]'."
26+
super().__init__(msg)
27+
return

src/pythonjsonlogger/msgspec.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99
from typing import Any
1010

1111
## Installed
12-
import msgspec.json
1312

1413
## Application
1514
from . import core
1615
from . import defaults as d
16+
from .utils import package_is_available
17+
18+
# We import msgspec after checking it is available
19+
package_is_available("msgspec", throw_error=True)
20+
import msgspec.json # pylint: disable=wrong-import-position,wrong-import-order
1721

1822

1923
### FUNCTIONS

src/pythonjsonlogger/orjson.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99
from typing import Any
1010

1111
## Installed
12-
import orjson
1312

1413
## Application
1514
from . import core
1615
from . import defaults as d
16+
from .utils import package_is_available
17+
18+
# We import msgspec after checking it is available
19+
package_is_available("orjson", throw_error=True)
20+
import orjson # pylint: disable=wrong-import-position,wrong-import-order
1721

1822

1923
### FUNCTIONS

src/pythonjsonlogger/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Utilities for Python JSON Logger"""
2+
3+
### IMPORTS
4+
### ============================================================================
5+
## Future
6+
from __future__ import annotations
7+
8+
## Standard Library
9+
import importlib.util
10+
11+
## Installed
12+
13+
## Application
14+
from .exception import MissingPackageError
15+
16+
17+
### FUNCTIONS
18+
### ============================================================================
19+
def package_is_available(
20+
name: str, *, throw_error: bool = False, extras_name: str | None = None
21+
) -> bool:
22+
"""Determine if the given package is available for import.
23+
24+
Args:
25+
name: Import name of the package to check.
26+
throw_error: Throw an error if the package is unavailable.
27+
extras_name: Extra dependency name to use in `throw_error`'s message.
28+
29+
Raises:
30+
MissingPackageError: When `throw_error` is `True` and the return value would be `False`
31+
32+
Returns:
33+
If the package is available for import.
34+
"""
35+
available = importlib.util.find_spec(name) is not None
36+
37+
if not available and throw_error:
38+
raise MissingPackageError(name, extras_name)
39+
40+
return available

tests/test_missing.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
### IMPORTS
2+
### ============================================================================
3+
## Future
4+
from __future__ import annotations
5+
6+
## Standard Library
7+
8+
## Installed
9+
import pytest
10+
11+
## Application
12+
import pythonjsonlogger
13+
from pythonjsonlogger.utils import package_is_available
14+
from pythonjsonlogger.exception import MissingPackageError
15+
16+
### CONSTANTS
17+
### ============================================================================
18+
MISSING_PACKAGE_NAME = "package_name_is_definintely_not_available"
19+
MISSING_PACKAGE_EXTRA = "package_extra_that_is_unique"
20+
21+
22+
### TESTS
23+
### ============================================================================
24+
def test_package_is_available():
25+
assert package_is_available("json")
26+
return
27+
28+
29+
def test_package_not_available():
30+
assert not package_is_available(MISSING_PACKAGE_NAME)
31+
return
32+
33+
34+
def test_package_not_available_throw():
35+
with pytest.raises(MissingPackageError) as e:
36+
package_is_available(MISSING_PACKAGE_NAME, throw_error=True)
37+
assert MISSING_PACKAGE_NAME in e.value.msg
38+
assert MISSING_PACKAGE_EXTRA not in e.value.msg
39+
return
40+
41+
42+
def test_package_not_available_throw_extras():
43+
with pytest.raises(MissingPackageError) as e:
44+
package_is_available(
45+
MISSING_PACKAGE_NAME, throw_error=True, extras_name=MISSING_PACKAGE_EXTRA
46+
)
47+
assert MISSING_PACKAGE_NAME in e.value.msg
48+
assert MISSING_PACKAGE_EXTRA in e.value.msg
49+
return
50+
51+
52+
## Python JSON Logger Specific
53+
## -----------------------------------------------------------------------------
54+
if not pythonjsonlogger.ORJSON_AVAILABLE:
55+
56+
def test_orjson_import_error():
57+
with pytest.raises(MissingPackageError, match="orjson"):
58+
import pythonjsonlogger.orjson
59+
return
60+
61+
62+
if not pythonjsonlogger.MSGSPEC_AVAILABLE:
63+
64+
def test_msgspec_import_error():
65+
with pytest.raises(MissingPackageError, match="msgspec"):
66+
import pythonjsonlogger.msgspec
67+
return

0 commit comments

Comments
 (0)