Skip to content

Flesh out more of jsonschema stubs #7950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 32 additions & 28 deletions stubs/jsonschema/jsonschema/_format.pyi
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from collections.abc import Iterable
from typing import Any
from collections.abc import Callable, Iterable
from typing import Any, TypeVar

_F = TypeVar("_F", bound=Callable[..., Any])

class FormatChecker:
checkers: Any
checkers: dict[str, tuple[Callable[[Any], bool], Exception | tuple[Exception, ...]]]

def __init__(self, formats: Iterable[str] | None = ...) -> None: ...
def checks(self, format, raises=...): ...
cls_checks: Any
def check(self, instance, format) -> None: ...
def conforms(self, instance, format) -> bool: ...
def checks(self, format: str, raises: Exception | tuple[Exception, ...] = ...) -> Callable[[_F], _F]: ...
@classmethod
def cls_checks(cls, format: str, raises: Exception | tuple[Exception, ...] = ...) -> Callable[[_F], _F]: ...
def check(self, instance: Any, format: str) -> None: ...
def conforms(self, instance: Any, format: str) -> bool: ...

draft3_format_checker: FormatChecker
draft4_format_checker: FormatChecker
Expand All @@ -16,28 +20,28 @@ draft7_format_checker: FormatChecker
draft201909_format_checker: FormatChecker
draft202012_format_checker: FormatChecker

def is_email(instance) -> bool: ...
def is_ipv4(instance) -> bool: ...
def is_ipv6(instance) -> bool: ...
def is_email(instance: object) -> bool: ...
def is_ipv4(instance: object) -> bool: ...
def is_ipv6(instance: object) -> bool: ...

# is_host_name is only defined if fqdn is installed.
def is_host_name(instance) -> bool: ...
def is_idn_host_name(instance) -> bool: ...
def is_uri(instance) -> bool: ...
def is_uri_reference(instance) -> bool: ...
def is_iri(instance) -> bool: ...
def is_iri_reference(instance) -> bool: ...
def is_datetime(instance) -> bool: ...
def is_time(instance) -> bool: ...
def is_regex(instance) -> bool: ...
def is_date(instance) -> bool: ...
def is_draft3_time(instance) -> bool: ...
def is_css_color_code(instance) -> bool: ...
def is_css21_color(instance) -> bool: ...
def is_json_pointer(instance) -> bool: ...
def is_relative_json_pointer(instance) -> bool: ...
def is_uri_template(instance) -> bool: ...
def is_host_name(instance: object) -> bool: ...
def is_idn_host_name(instance: object) -> bool: ...
def is_uri(instance: object) -> bool: ...
def is_uri_reference(instance: object) -> bool: ...
def is_iri(instance: object) -> bool: ...
def is_iri_reference(instance: object) -> bool: ...
def is_datetime(instance: object) -> bool: ...
def is_time(instance: object) -> bool: ...
def is_regex(instance: object) -> bool: ...
def is_date(instance: object) -> bool: ...
def is_draft3_time(instance: object) -> bool: ...
def is_css_color_code(instance: object) -> bool: ...
def is_css21_color(instance: object) -> bool: ...
def is_json_pointer(instance: object) -> bool: ...
def is_relative_json_pointer(instance: object) -> bool: ...
def is_uri_template(instance: object) -> bool: ...

# is_duration is only defined if isoduration is installed.
def is_duration(instance) -> bool: ...
def is_uuid(instance) -> bool: ...
def is_duration(instance: object) -> bool: ...
def is_uuid(instance: object) -> bool: ...
60 changes: 36 additions & 24 deletions stubs/jsonschema/jsonschema/exceptions.pyi
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
from _typeshed import Self, SupportsRichComparison
from collections import deque
from collections.abc import Callable, Container, Iterable, Sequence
from typing import Any

WEAK_MATCHES: Any
STRONG_MATCHES: Any
from jsonschema import _utils, protocols

_RelevanceFuncType = Callable[[ValidationError], SupportsRichComparison]

WEAK_MATCHES: frozenset[str]
STRONG_MATCHES: frozenset[str]

class _Error(Exception):
message: Any
path: Any
schema_path: Any
context: Any
cause: Any
validator: Any
message: str
path: deque[str]
relative_path: deque[str]
schema_path: deque[str]
relative_schema_path: deque[str]
context: list[ValidationError] | None
cause: Exception | None
validator: protocols.Validator | None
validator_value: Any
instance: Any
schema: Any
parent: Any
parent: _Error | None
def __init__(
self,
message,
validator=...,
path=...,
message: str,
validator: _utils.Unset | None | protocols.Validator = ...,
path: Sequence[str] = ...,
cause: Any | None = ...,
context=...,
context: Sequence[ValidationError] = ...,
validator_value=...,
instance=...,
schema=...,
schema_path=...,
parent: Any | None = ...,
instance: Any = ...,
schema: Any = ...,
schema_path: Sequence[str] = ...,
parent: _Error | None = ...,
) -> None: ...
@classmethod
def create_from(cls, other): ...
def create_from(cls: type[Self], other: _Error) -> Self: ...
@property
def absolute_path(self): ...
def absolute_path(self) -> Sequence[str]: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally prefer to be as precise as possible with return types in typeshed, so that would imply using deque[str] here instead of Sequence[str]. If you think it's an implementation detail which precise type is returned here, however, then you may want to ignore me here :)

The same point applies to absolute_schema_path below.

Suggested change
def absolute_path(self) -> Sequence[str]: ...
def absolute_path(self) -> deque[str]: ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think it's an implementation detail which precise type is returned here, however, then you may want to ignore me here :)

Yeah, I considered annotating it as deque -- that was in my first draft -- but then decided exactly this. Callers should not be relying on that value being a deque IMO.
In a way, your comment helps validate my decision. It means I was thinking the right kinds of things!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is incorrect (at least with jsonschema 4.4.0) because path entries may be indexes into a JSON array. For example:

import jsonschema

SCHEMA = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "array",
    "items": {"type": "string"},
}


document = ["a", 12345, "c", "d"]

try:
    jsonschema.validate(document, SCHEMA)
except jsonschema.ValidationError as e:
    print(repr(e))
    for path_entry in e.absolute_path:
        print(repr(path_entry), type(path_entry))
$ pip show jsonschema | \grep Version
Version: 4.4.0

$ python temp.py
<ValidationError: "12345 is not of type 'string'">
1 <class 'int'>

I think Sequence[str | int] is the correct annotation? I'll make a PR with that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Apologies for the oversight. These can be ints in precisely the case you mention. I'll double-check for other cases of this. (I'm not a typeshed maintainer, but I'll also go 👍 your PR.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to worry, glad it was straightforward to sort out!

@property
def absolute_schema_path(self): ...
def absolute_schema_path(self) -> Sequence[str]: ...
@property
def json_path(self): ...
def json_path(self) -> str: ...
# TODO: this type could be made more precise using TypedDict to
# enumerate the types of the members
def _contents(self) -> dict[str, Any]: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a bit involved, but you could consider returning a TypedDict here for more precise typing (each key will have a specific type associated with it for the corresponding value, if I understand the source code correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to punt on this for now, and just add a TODO comment to make the type more precise with TypedDict, if that's okay. Right now, I want to get this changeset "over the finish line" and then come back when I have some more time to work on jsonschema and on the typing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure


class ValidationError(_Error): ...
class SchemaError(_Error): ...
Expand Down Expand Up @@ -68,8 +80,8 @@ class ErrorTree:
@property
def total_errors(self): ...

def by_relevance(weak=..., strong=...): ...
def by_relevance(weak: Container[str] = ..., strong: Container[str] = ...) -> _RelevanceFuncType: ...

relevance: Any
relevance: _RelevanceFuncType

def best_match(errors, key=...): ...
def best_match(errors: Iterable[ValidationError], key: _RelevanceFuncType = ...): ...