Skip to content

Commit

Permalink
Opportunistically use refferencing library
Browse files Browse the repository at this point in the history
  • Loading branch information
martinhoyer committed Nov 1, 2024
1 parent fb2794c commit 7c68e35
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 50 deletions.
1 change: 1 addition & 0 deletions fmf.spec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ BuildRequires: python%{python3_pkgversion}-jsonschema
BuildRequires: git-core
%{?python_provide:%python_provide python%{python3_pkgversion}-%{name}}
Requires: git-core
Recommends: python%{python3_pkgversion}-referencing

%description -n python%{python3_pkgversion}-%{name}
The fmf Python module and command line tool implement a flexible
Expand Down
Empty file added fmf/_compat/__init__.py
Empty file.
91 changes: 91 additions & 0 deletions fmf/_compat/jsonschema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Compatibility layer for jsonschema validation."""

from __future__ import annotations

import warnings
from typing import Any, Dict, List, NamedTuple, Optional, Union

import jsonschema

try:
from jsonschema import validators
from referencing import Registry, create
from referencing.jsonschema import DRAFT4
MODERN_JSONSCHEMA = True
except ImportError:
MODERN_JSONSCHEMA = False


class JsonSchemaValidationResult(NamedTuple):
"""Represents JSON Schema validation result."""
result: bool
errors: List[Any]


def create_validator(
schema: Dict[str, Any],
schema_store: Optional[Dict[str, Any]] = None
) -> Union[jsonschema.validators.Validator, jsonschema.validators.Draft4Validator]:
"""Create a validator instance based on available jsonschema version."""
schema_store = schema_store or {}

if MODERN_JSONSCHEMA:
# Modern approach with referencing
registry: Registry = Registry().with_resources([
(uri, create({'$schema': 'http://json-schema.org/draft-04/schema#', **contents}))
for uri, contents in schema_store.items()
])
cls = validators.create(meta_schema=DRAFT4, validators={})
return cls(schema=schema, registry=registry)
else:
# Legacy approach with RefResolver
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)
try:
resolver = jsonschema.RefResolver.from_schema(
schema, store=schema_store)
except AttributeError as error:
from fmf.utils import JsonSchemaError
raise JsonSchemaError(f'Provided schema cannot be loaded: {error}')
return jsonschema.Draft4Validator(schema, resolver=resolver)


def validate_data(
data: Any,
schema: Dict[str, Any],
schema_store: Optional[Dict[str, Any]] = None
) -> JsonSchemaValidationResult:
"""
Validate data with given JSON Schema and schema references.
Args:
data: The data to validate
schema: The JSON Schema to validate against
schema_store: Optional dict of schema references and their content
Returns:
JsonSchemaValidationResult with:
result: boolean representing the validation result
errors: A list of validation errors
Raises:
JsonSchemaError: If the supplied schema was invalid
"""
validator = create_validator(schema, schema_store)

try:
validator.validate(data)
return JsonSchemaValidationResult(True, [])

# Data file validated by schema contains errors
except jsonschema.exceptions.ValidationError:
return JsonSchemaValidationResult(False, list(validator.iter_errors(data)))

# Schema file is invalid
except (
jsonschema.exceptions.SchemaError,
jsonschema.exceptions.RefResolutionError,
jsonschema.exceptions.UnknownType
) as error:
from fmf.utils import JsonSchemaError
raise JsonSchemaError(f'Errors found in provided schema: {error}')
56 changes: 6 additions & 50 deletions fmf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
import time
import warnings
from io import StringIO
from typing import Any, List, NamedTuple
from typing import NamedTuple

import jsonschema
from filelock import FileLock, Timeout
from ruamel.yaml import YAML, scalarstring
from ruamel.yaml.comments import CommentedMap

import fmf.base
# F401 utils is entry point, while _compat is being used
# isort with autopep8 made it to two lines. Ruff needed.
from fmf._compat.jsonschema import JsonSchemaValidationResult # noqa: F401
from fmf._compat.jsonschema import validate_data # noqa: F401

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Constants
Expand Down Expand Up @@ -419,7 +422,7 @@ class Logging:
_level = LOG_WARN

# Already initialized loggers by their name
_loggers = dict()
_loggers: dict[str, logging.Logger] = dict()

def __init__(self, name='fmf'):
# Use existing logger if already initialized
Expand Down Expand Up @@ -911,53 +914,6 @@ def dict_to_yaml(data, width=None, sort=False):
# Validation
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

class JsonSchemaValidationResult(NamedTuple):
""" Represents JSON Schema validation result """

result: bool
errors: List[Any]


def validate_data(data, schema, schema_store=None):
"""
Validate data with given JSON Schema and schema references.
schema_store is a dict of schema references and their content.
Return a named tuple utils.JsonSchemaValidationResult
with the following two items:
result ... boolean representing the validation result
errors ... A list of validation errors
Raises utils.JsonSchemaError if the supplied schema was invalid.
"""
schema_store = schema_store or {}
try:
resolver = jsonschema.RefResolver.from_schema(
schema, store=schema_store)
except AttributeError as error:
raise JsonSchemaError(f'Provided schema cannot be loaded: {error}')

validator = jsonschema.Draft4Validator(schema, resolver=resolver)

try:
validator.validate(data)
return JsonSchemaValidationResult(True, [])

# Data file validated by schema contains errors
except jsonschema.exceptions.ValidationError:
return JsonSchemaValidationResult(False, list(validator.iter_errors(data)))

# Schema file is invalid
except (
jsonschema.exceptions.SchemaError,
jsonschema.exceptions.RefResolutionError,
jsonschema.exceptions.UnknownType
) as error:
raise JsonSchemaError(f'Errors found in provided schema: {error}')


class PatternReplacement(NamedTuple):
pattern: str
replacement: str
Expand Down

0 comments on commit 7c68e35

Please sign in to comment.