"""JSONSchema spec paths module.""" from __future__ import annotations import os import warnings from collections.abc import Iterator from collections.abc import Sequence from contextlib import contextmanager from pathlib import Path from typing import Any from typing import TypeVar from typing import overload from pathable import AccessorPath from referencing import Specification from referencing._core import Resolved from referencing.jsonschema import DRAFT202012 from jsonschema_path.accessors import SchemaAccessor from jsonschema_path.handlers import default_handlers from jsonschema_path.handlers.protocols import SupportsRead from jsonschema_path.readers import FilePathReader from jsonschema_path.readers import FileReader from jsonschema_path.readers import PathReader from jsonschema_path.typing import ResolverHandlers from jsonschema_path.typing import Schema from jsonschema_path.typing import SchemaKey from jsonschema_path.typing import SchemaNode from jsonschema_path.typing import SchemaValue from jsonschema_path.typing import is_str_sequence TDefault = TypeVar("TDefault") # Python 3.11+ shortcut: typing.Self TSchemaPath = TypeVar("TSchemaPath", bound="SchemaPath") SPEC_SEPARATOR = "#" NOTSET = object() class SchemaPath(AccessorPath[SchemaNode, SchemaKey, SchemaValue]): @classmethod def _parse_args( cls, args: Sequence[Any], sep: str = SPEC_SEPARATOR, ) -> tuple[SchemaKey, ...]: parts: list[SchemaKey] = [] append = parts.append extend = parts.extend for a in args: if isinstance(a, cls): extend(a.parts) continue # Fast-path: benchmarks overwhelmingly pass `str`/`int` parts. if isinstance(a, int): append(a) continue if isinstance(a, bytes): a = a.decode("ascii") if isinstance(a, str): if a and a != ".": if sep in a: for x in a.split(sep): if x and x != ".": append(x) else: append(a) continue # PathLike is relatively expensive to check; keep it after common types. if isinstance(a, os.PathLike): a = os.fspath(a) if isinstance(a, bytes): a = a.decode("ascii") if isinstance(a, str): if a and a != ".": if sep in a: for x in a.split(sep): if x and x != ".": append(x) else: append(a) continue raise TypeError( "argument must be str, int, bytes, os.PathLike, or SchemaPath; got %r" % (type(a),) ) return tuple(parts) @classmethod def from_dict( cls: type[TSchemaPath], data: Schema, *args: Any, separator: str = SPEC_SEPARATOR, specification: Specification[Schema] = DRAFT202012, base_uri: str = "", handlers: ResolverHandlers = default_handlers, resolved_cache_maxsize: int = 0, spec_url: str | None = None, ref_resolver_handlers: ResolverHandlers | None = None, ) -> TSchemaPath: if spec_url is not None: warnings.warn( "spec_url parameter is deprecated. " "Use base_uri instead.", DeprecationWarning, ) base_uri = spec_url if ref_resolver_handlers is not None: warnings.warn( "ref_resolver_handlers parameter is deprecated. " "Use handlers instead.", DeprecationWarning, ) handlers = ref_resolver_handlers accessor: SchemaAccessor = SchemaAccessor.from_schema( data, specification=specification, base_uri=base_uri, handlers=handlers, resolved_cache_maxsize=resolved_cache_maxsize, ) return cls(accessor, *args, separator=separator) @classmethod def from_path( cls: type[TSchemaPath], path: Path, resolved_cache_maxsize: int = 0, ) -> TSchemaPath: reader = PathReader(path) data, base_uri = reader.read() return cls.from_dict( data, base_uri=base_uri, resolved_cache_maxsize=resolved_cache_maxsize, ) @classmethod def from_file_path( cls: type[TSchemaPath], file_path: str, resolved_cache_maxsize: int = 0, ) -> TSchemaPath: reader = FilePathReader(file_path) data, base_uri = reader.read() return cls.from_dict( data, base_uri=base_uri, resolved_cache_maxsize=resolved_cache_maxsize, ) @classmethod def from_file( cls: type[TSchemaPath], fileobj: SupportsRead, base_uri: str = "", spec_url: str | None = None, resolved_cache_maxsize: int = 0, ) -> TSchemaPath: reader = FileReader(fileobj) data, _ = reader.read() return cls.from_dict( data, base_uri=base_uri, spec_url=spec_url, resolved_cache_maxsize=resolved_cache_maxsize, ) @property def base_uri(self) -> str: assert isinstance(self.accessor, SchemaAccessor) return self.accessor.base_uri def str_keys(self) -> Sequence[str]: keys = list(self.keys()) if not is_str_sequence(keys): raise TypeError( f"Expected string keys, got {[type(x) for x in keys]}" ) return keys def str_items(self) -> Iterator[tuple[str, SchemaPath]]: for key, value in self.items(): if not isinstance(key, str): raise TypeError(f"Expected string keys, got {type(key)}") yield key, value @overload def read_str(self) -> str: ... @overload def read_str(self, default: TDefault) -> str | TDefault: ... def read_str(self, default: object = NOTSET) -> object: try: value = self.read_value() except KeyError: if default is not NOTSET: return default raise if not isinstance(value, str): raise TypeError(f"Expected a string value, got {type(value)}") return value @overload def read_str_or_list(self) -> str | list[str]: ... @overload def read_str_or_list( self, default: TDefault ) -> str | list[str] | TDefault: ... def read_str_or_list(self, default: object = NOTSET) -> object: try: value = self.read_value() except KeyError: if default is not NOTSET: return default raise if not isinstance(value, (str, list)): raise TypeError( f"Expected a string or a list of strings, got {type(value)}" ) return value @overload def read_bool(self) -> bool: ... @overload def read_bool(self, default: TDefault) -> bool | TDefault: ... def read_bool(self, default: object = NOTSET) -> object: try: value = self.read_value() except KeyError: if default is not NOTSET: return default raise if not isinstance(value, bool): if default is not NOTSET: return default raise TypeError(f"Expected a bool value, got {type(value)}") return value def as_uri(self) -> str: return f"#/{str(self)}" @contextmanager def open(self) -> Any: """Open the path.""" with self.resolve() as resolved: yield resolved.contents @contextmanager def resolve(self) -> Iterator[Resolved[SchemaNode]]: """Resolve the path.""" assert isinstance(self.accessor, SchemaAccessor) with self.accessor.resolve(self.parts) as resolved: yield resolved