Skip to content

Commit

Permalink
Merge pull request #246 from fonttools/check-extras-installed
Browse files Browse the repository at this point in the history
raise appropriate error when extras not installed
  • Loading branch information
anthrotype authored Nov 7, 2022
2 parents c7281e7 + 6957f48 commit 4d8a960
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 23 deletions.
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@ jobs:
run: pip install tox coverage
- name: Run Tox
run: tox -e py-cov
- name: Re-run Tox (without cattrs)
- name: Re-run Tox without cattrs
if: startsWith(matrix.platform, 'ubuntu-latest') && startsWith(matrix.python-version, '3.10')
run: |
.tox/py-cov/bin/pip uninstall -y cattrs
tox -e py-cov --skip-pkg-install
tox -e py-nocattrs
- name: Re-run Tox without msgpack
if: startsWith(matrix.platform, 'ubuntu-latest') && startsWith(matrix.python-version, '3.10')
run: |
tox -e py-nomsgpack
- name: Produce coverage files
run: |
coverage combine
Expand Down
12 changes: 12 additions & 0 deletions src/ufoLib2/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
from __future__ import annotations

from typing import Any


class Error(Exception):
"""The base exception for ufoLib2."""


class ExtrasNotInstalledError(Error):
"""The extras required for this feature are not installed."""

def __init__(self, extras: str) -> None:
super().__init__(f"Extras not installed: ufoLib2[{extras}]")

def __call__(self, *args: Any, **kwargs: Any) -> None:
raise self
10 changes: 4 additions & 6 deletions src/ufoLib2/serde/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from importlib import import_module
from typing import IO, Any, AnyStr, Callable, Type

from ufoLib2.errors import ExtrasNotInstalledError
from ufoLib2.typing import PathLike, T

_SERDE_FORMATS_ = ("json", "msgpack")
Expand Down Expand Up @@ -75,12 +76,9 @@ def serde(cls: Type[T]) -> Type[T]:

try:
serde_submodule = import_module(f"ufoLib2.serde.{fmt}")
except ImportError as e:
exc = e

def raise_error(*args: Any, **kwargs: Any) -> None:
raise exc

except ImportError as exc:
raise_error = ExtrasNotInstalledError(fmt)
raise_error.__cause__ = exc
for method in ("loads", "load", "dumps", "dump"):
setattr(cls, f"{fmt}_{method}", raise_error)
else:
Expand Down
3 changes: 3 additions & 0 deletions tests/serde/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def test_dumps_loads(
) -> None:
if not have_orjson:
monkeypatch.setattr(ufoLib2.serde.json, "have_orjson", have_orjson)
else:
pytest.importorskip("orjson")

font = ufo_UbuTestData
data = font.json_dumps() # type: ignore
Expand Down Expand Up @@ -82,6 +84,7 @@ def test_dump_load(

@pytest.mark.parametrize("indent", [1, 3], ids=["indent-1", "indent-3"])
def test_indent_not_2_orjson(indent: int) -> None:
pytest.importorskip("orjson")
with pytest.raises(ValueError):
ufoLib2.serde.json.dumps(None, indent=indent)

Expand Down
4 changes: 3 additions & 1 deletion tests/serde/test_msgpack.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from pathlib import Path

import msgpack # type: ignore
import pytest

import ufoLib2.objects

# isort: off
pytest.importorskip("cattrs")
pytest.importorskip("msgpack")

import msgpack # type: ignore # noqa

import ufoLib2.serde.msgpack # noqa: E402

Expand Down
60 changes: 47 additions & 13 deletions tests/serde/test_serde.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import importlib
import sys
from typing import Any, Dict, List

import pytest
from attrs import define

import ufoLib2.objects
from ufoLib2.errors import ExtrasNotInstalledError
from ufoLib2.serde import _SERDE_FORMATS_, serde

cattrs = None
try:
import cattrs # type: ignore
except ImportError:
pass

def test_raise_import_error(monkeypatch: Any) -> None:
# pretend we can't import the module (e.g. msgpack not installed)
monkeypatch.setitem(sys.modules, "ufoLib2.serde.msgpack", None)

with pytest.raises(ImportError, match="ufoLib2.serde.msgpack"):
importlib.import_module("ufoLib2.serde.msgpack")
msgpack = None
try:
import msgpack # type: ignore
except ImportError:
pass


EXTRAS_REQUIREMENTS = {
"json": ["cattrs"],
"msgpack": ["cattrs", "msgpack"],
}


def assert_extras_not_installed(extras: str, missing_dependency: str) -> None:
# sanity check that the dependency is not installed
with pytest.raises(ImportError, match=missing_dependency):
importlib.import_module(missing_dependency)

@serde
@define
Expand All @@ -24,10 +41,28 @@ class Foo:

foo = Foo(1)

with pytest.raises(ImportError, match="ufoLib2.serde.msgpack"):
# since the method is only added dynamically at runtime, mypy complains that
# "Foo" has no attribute "msgpack_dumps" -- so I shut it up
foo.msgpack_dumps() # type: ignore
with pytest.raises(
ExtrasNotInstalledError, match=rf"Extras not installed: ufoLib2\[{extras}\]"
) as exc_info:
dumps_method = getattr(foo, f"{extras}_dumps")
dumps_method()

assert isinstance(exc_info.value.__cause__, ModuleNotFoundError)


@pytest.mark.skipif(cattrs is not None, reason="cattrs installed, not applicable")
def test_json_cattrs_not_installed() -> None:
assert_extras_not_installed("json", "cattrs")


@pytest.mark.skipif(cattrs is not None, reason="cattrs installed, not applicable")
def test_msgpack_cattrs_not_installed() -> None:
assert_extras_not_installed("msgpack", "cattrs")


@pytest.mark.skipif(msgpack is not None, reason="msgpack installed, not applicable")
def test_msgpack_not_installed() -> None:
assert_extras_not_installed("msgpack", "msgpack")


BASIC_EMPTY_OBJECTS: List[Dict[str, Any]] = [
Expand Down Expand Up @@ -61,9 +96,8 @@ class Foo:
ids=lambda x: x["class_name"], # type: ignore
)
def test_serde_all_objects(fmt: str, object_info: Dict[str, Any]) -> None:
if fmt in ("json", "msgpack"):
# skip these format tests if cattrs is not installed
pytest.importorskip("cattrs")
for req in EXTRAS_REQUIREMENTS[fmt]:
pytest.importorskip(req)

klass = getattr(ufoLib2.objects, object_info["class_name"])
loads = getattr(klass, f"{fmt}_loads")
Expand Down
3 changes: 3 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ deps =
-r requirements.txt
-r requirements-dev.txt
commands =
nocattrs: pip uninstall -y cattrs
noorjson: pip uninstall -y orjson
nomsgpack: pip uninstall -y msgpack
cov: coverage run --parallel-mode -m pytest {posargs}
!cov: pytest {posargs}

Expand Down

0 comments on commit 4d8a960

Please sign in to comment.