diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b96bbafd6..d1864602a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -132,6 +132,30 @@ jobs: run: sphinx-build -b html . _build working-directory: docs + tests-no-jsonschema: + name: Test jsonschema uninstalled + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set Up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Update pip + run: python -m pip install --upgrade pip + - name: Install Dependencies + run: | + python -m pip install -e .[test,docs] + python -m pip uninstall -y jsonschema + python -m pip freeze + - name: Run pytest + run: | + python -m pytest -v tests/no_jsonschema_tests.py + - name: Run HTML build + # the docs should build without matplotlib (just issuing warnings) + run: sphinx-build -b html . _build + working-directory: docs + check: # This job does nothing and is only used for the branch protection diff --git a/README.rst b/README.rst index c14f741cb..ef35bf02e 100644 --- a/README.rst +++ b/README.rst @@ -54,6 +54,12 @@ Using pip pip install sphinx-needs +To use schema validation features of sphinx-needs, you need to also install ``jsonschema``, which is available *via* the ``schema`` extra: + +.. code-block:: bash + + pip install sphinx-needs[schema] + If you wish to also use the plotting features of sphinx-needs (see ``needbar`` and ``needpie``), you need to also install ``matplotlib``, which is available *via* the ``plotting`` extra: .. code-block:: bash diff --git a/docker/Dockerfile b/docker/Dockerfile index 5fdbb505f..a00bbc460 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -47,11 +47,11 @@ RUN pip3 install --no-cache-dir \ # Install Sphinx-Needs RUN \ if [ -n "$NEEDS_VERSION" ] && [ "$NEEDS_VERSION" = "pre-release" ]; then \ - pip3 install --no-cache-dir "sphinx-needs[plotting] @ git+https://github.com/useblocks/sphinx-needs"; \ + pip3 install --no-cache-dir "sphinx-needs[plotting,schema] @ git+https://github.com/useblocks/sphinx-needs"; \ elif [ -n "$NEEDS_VERSION" ]; then \ - pip3 install --no-cache-dir "sphinx-needs[plotting] @ git+https://github.com/useblocks/sphinx-needs@$NEEDS_VERSION"; \ + pip3 install --no-cache-dir "sphinx-needs[plotting,schema] @ git+https://github.com/useblocks/sphinx-needs@$NEEDS_VERSION"; \ else \ - pip3 install --no-cache-dir sphinx-needs[plotting]; \ + pip3 install --no-cache-dir sphinx-needs[plotting,schema]; \ fi ## Clean up @@ -61,4 +61,4 @@ git WORKDIR /sphinxneeds # Start as user -USER ${DOCKER_USERNAME} \ No newline at end of file +USER ${DOCKER_USERNAME} diff --git a/docs/installation.rst b/docs/installation.rst index c8d81feda..4f5b8af5d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -31,6 +31,12 @@ Using pip pip install sphinx-needs +To use schema validation features of sphinx-needs, you need to also install ``jsonschema``, which is available *via* the ``schema`` extra: + +.. code-block:: bash + + pip install sphinx-needs[schema] + If you wish to also use the plotting features of sphinx-needs (see :ref:`needbar` and :ref:`needpie`), you need to also install ``matplotlib``, which is available *via* the ``plotting`` extra: .. code-block:: bash diff --git a/pyproject.toml b/pyproject.toml index 0405e03e4..f790942b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ dependencies = [ "sphinx>=7.4,<9", "requests-file~=2.1", # external links "requests~=2.32", # external links - "jsonschema[format]>=3.2.0", # schema validation for needsimport and ontology "sphinx-data-viewer~=0.1.5", # needservice debug output "sphinxcontrib-jquery~=4.0", # needed for datatables in sphinx>=6 "tomli; python_version < '3.11'", # for needs_from_toml configuration @@ -40,9 +39,13 @@ dependencies = [ [project.optional-dependencies] plotting = ["matplotlib>=3.3.0"] # for needpie / needbar +schema = [ + "jsonschema[format]>=3.2.0", +] # for schema validation for needsimport and ontology test = [ "defusedxml~=0.7.1", "matplotlib>=3.3.0", + "jsonschema[format]>=3.2.0", "pytest~=8.0", "pytest-cov~=6.0", "syrupy~=4.0", diff --git a/sphinx_needs/needsfile.py b/sphinx_needs/needsfile.py index f8354704c..a16f15137 100644 --- a/sphinx_needs/needsfile.py +++ b/sphinx_needs/needsfile.py @@ -15,7 +15,6 @@ from functools import lru_cache from typing import Any -from jsonschema import Draft7Validator from sphinx.environment import BuildEnvironment from sphinx_needs.config import NeedsSphinxConfig @@ -23,6 +22,7 @@ from sphinx_needs.logging import get_logger, log_warning from sphinx_needs.need_item import NeedItem from sphinx_needs.needs_schema import FieldLiteralValue, FieldsSchema +from sphinx_needs.utils import import_jsonschema log = get_logger(__name__) @@ -276,7 +276,12 @@ def check_needs_data(data: Any) -> Errors: """ needs_schema = _load_schema() - validator = Draft7Validator(needs_schema) + jsonschema = import_jsonschema() + if jsonschema is None: + # skipping schema validation due to missing dependency + return Errors([]) + + validator = jsonschema.Draft7Validator(needs_schema) schema_errors = list(validator.iter_errors(data)) # In future there may be additional types of validations. diff --git a/sphinx_needs/schema/core.py b/sphinx_needs/schema/core.py index cfcf10d4a..659af7359 100644 --- a/sphinx_needs/schema/core.py +++ b/sphinx_needs/schema/core.py @@ -2,10 +2,9 @@ from __future__ import annotations +import typing from typing import Any, cast -from jsonschema import Draft202012Validator, FormatChecker, ValidationError - from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.need_item import NeedItem from sphinx_needs.schema.config import ( @@ -27,6 +26,7 @@ save_debug_files, ) from sphinx_needs.schema.utils import get_properties_from_schema +from sphinx_needs.utils import import_jsonschema from sphinx_needs.views import NeedsView # TODO(Marco): error for conflicting unevaluatedProperties @@ -62,6 +62,9 @@ _needs_schema: dict[str, Any] = {} """The needs schema as it would be written to needs.json, generated by generate_needs_schema().""" +if typing.TYPE_CHECKING: + from jsonschema import ValidationError + def merge_static_schemas(config: NeedsSphinxConfig) -> bool: """ @@ -561,7 +564,13 @@ def get_localschema_errors( :raises jsonschema_rs.ValidationError: If the schema is invalid cannot be built. """ - validator = Draft202012Validator(schema, format_checker=FormatChecker()) + jsonschema = import_jsonschema() + if jsonschema is None: + # skip schema validation if extra is not installed + return [] + validator = jsonschema.Draft202012Validator( + schema, format_checker=jsonschema.FormatChecker() + ) return list(validator.iter_errors(instance=need)) diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index 97c532caf..9f767f544 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -8,6 +8,7 @@ from collections.abc import Callable from dataclasses import dataclass from functools import lru_cache, reduce, wraps +from types import ModuleType from typing import TYPE_CHECKING, Any, Protocol, TypeVar from urllib.parse import urlparse @@ -419,6 +420,19 @@ def import_matplotlib() -> matplotlib | None: return matplotlib +@lru_cache +def import_jsonschema() -> ModuleType | None: + """Import and return matplotlib, or return None if it cannot be imported. + + Also sets the interactive backend to ``Agg``, if ``DISPLAY`` is not set. + """ + try: + import jsonschema + except ImportError: + return None + return jsonschema + + def save_matplotlib_figure( app: Sphinx, figure: FigureBase, basename: str, fromdocname: str ) -> nodes.image: diff --git a/tests/no_jsonschema_tests.py b/tests/no_jsonschema_tests.py new file mode 100644 index 000000000..851e28488 --- /dev/null +++ b/tests/no_jsonschema_tests.py @@ -0,0 +1,13 @@ +"""These tests should only be run in an environment without sphinx_needs installed.""" + +import pytest + + +@pytest.mark.parametrize( + "test_app", + [{"buildername": "html", "srcdir": "doc_test/doc_needsfile"}], + indirect=True, +) +def test_needsfile(test_app): + """Test the build fails correctly, if matplotlib is not installed.""" + test_app.build()