diff --git a/.gitignore b/.gitignore index 8e8280a..ca0a16e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,77 @@ dist docs/_build docs/spec docs/stories -__pycache__ + +# Python + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +fmf.1 + +# Testing +.mypy_cache +.pytest_cache +*.tgz +# Created by pytest-html reporting plugin +/assets/style.css +/report.html + +# Virtual environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Jetbrains +.idea/ + +# Vim + +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ + +# Auto-generated tag files +tags + +# Persistent undo +[._]*.un~ + +# Visual Studio Code +.vscode diff --git a/fmf/_compat/__init__.py b/fmf/_compat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fmf/_compat/jsonschema.py b/fmf/_compat/jsonschema.py new file mode 100644 index 0000000..8eb5cf0 --- /dev/null +++ b/fmf/_compat/jsonschema.py @@ -0,0 +1,51 @@ +"""Compatibility layer for jsonschema validation.""" + +from typing import Any, Optional + +import jsonschema +from jsonschema.validators import Draft4Validator + + +def get_validator( + schema: Any, + schema_store: Optional[dict[str, Any]] = None + ) -> Draft4Validator: + """Create a validator instance based on available jsonschema version.""" + # Validate schema is a dict + if not isinstance(schema, dict): # TODO remove once mypy/pyright is added + from fmf.utils import JsonSchemaError + raise JsonSchemaError(f'Invalid schema type: {type(schema)}. Schema must be a dictionary.') + + schema_store = schema_store or {} + + try: + from jsonschema import validators + from referencing import Registry, Resource + from referencing.jsonschema import DRAFT4 + + # Modern approach with referencing + resources = [] + for uri, contents in schema_store.items(): + # Try to create resource from contents (will use $schema if present) + try: + resource = Resource.from_contents(contents) + except Exception: + # If that fails, explicitly create as Draft4 + resource = DRAFT4.create_resource(contents) + resources.append((uri, resource)) + + registry = Registry().with_resources(resources) + + # Create validator using Draft4 meta-schema + validator_cls = validators.validator_for(schema, default=Draft4Validator) + return validator_cls(schema=schema, registry=registry) + + except ImportError: + # Legacy approach with RefResolver + 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 Draft4Validator(schema, resolver=resolver) diff --git a/fmf/utils.py b/fmf/utils.py index 4e70fb6..0c92148 100644 --- a/fmf/utils.py +++ b/fmf/utils.py @@ -10,7 +10,7 @@ import time import warnings from io import StringIO -from typing import Any, List, NamedTuple +from typing import Any, NamedTuple, Optional import jsonschema from filelock import FileLock, Timeout @@ -18,6 +18,7 @@ from ruamel.yaml.comments import CommentedMap import fmf.base +from fmf._compat.jsonschema import get_validator # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Constants @@ -419,7 +420,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 @@ -913,33 +914,21 @@ def dict_to_yaml(data, width=None, sort=False): 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. + errors: list[Any] - schema_store is a dict of schema references and their content. - Return a named tuple utils.JsonSchemaValidationResult - with the following two items: +def validate_data( + data: Any, + schema: Any, + schema_store: Optional[dict[str, Any]] = None + ) -> JsonSchemaValidationResult: + """Validate data against a JSON Schema. - result ... boolean representing the validation result - errors ... A list of validation errors - - Raises utils.JsonSchemaError if the supplied schema was invalid. + Validates the given data using the specified JSON Schema and optional + schema references. """ - 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) + validator = get_validator(schema, schema_store) try: validator.validate(data)