Skip to content

Add new --disable-formats flag to replace --disable-format #261

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 6 commits into from
May 4, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Unreleased
.. vendor-insert-here

- Update vendored schemas (2023-05-03)
- A new option, ``--disable-formats`` replaces and enhances the
``--disable-format`` flag. ``--disable-formats`` takes a format to disable
and may be passed multiple times, allowing users to opt out of any specific
format checks. ``--disable-format "*"`` can be used to disable all format
checking. ``--disable-format`` is still supported, but is deprecated and
emits a warning.

0.22.0
------
Expand Down
45 changes: 43 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,52 @@ following options can be used to control this behavior.
``--disable-format``
~~~~~~~~~~~~~~~~~~~~

Disable all ``"format"`` checks.
.. warning::

This option is deprecated. Use ``--disable-formats "*"`` instead.

Disable all format checks.

``--disable-formats``
~~~~~~~~~~~~~~~~~~~~~

Disable specified ``"format"`` checks.

Use ``--disable-formats "*"`` to disable all format checking.

Because ``"format"`` checking is not done by all JSON Schema tools, it is
possible that a file may validate under a schema with a different tool, but
fail with ``check-jsonschema`` if ``--disable-format`` is not set.
fail with ``check-jsonschema`` if ``--disable-formats`` is not set.

This option may be specified multiple times or as a comma-delimited list and
supports the following formats as arguments:

- ``date``
- ``date-time``
- ``duration``
- ``email``
- ``hostname``
- ``idn-email``
- ``idn-hostname``
- ``ipv4``
- ``ipv6``
- ``iri``
- ``iri-reference``
- ``json-pointer``
- ``regex``
- ``relative-json-pointer``
- ``time``
- ``uri``
- ``uri-reference``
- ``uri-template``
- ``uuid``

Example usage:

.. code-block:: bash

# disables all three of time, date-time, and iri
--disable-formats time,date-time --disable-formats iri

``--format-regex``
~~~~~~~~~~~~~~~~~~
Expand Down
3 changes: 3 additions & 0 deletions src/check_jsonschema/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .main_command import main

__all__ = ("main",)
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
from __future__ import annotations

import enum
import os
import textwrap

import click

from .catalog import CUSTOM_SCHEMA_NAMES, SCHEMA_CATALOG
from .checker import SchemaChecker
from .formats import FormatOptions, RegexFormatBehavior
from .instance_loader import InstanceLoader
from .parsers import SUPPORTED_FILE_FORMATS
from .reporter import REPORTER_BY_NAME, Reporter
from .schema_loader import (
from ..catalog import CUSTOM_SCHEMA_NAMES, SCHEMA_CATALOG
from ..checker import SchemaChecker
from ..formats import KNOWN_FORMATS, RegexFormatBehavior
from ..instance_loader import InstanceLoader
from ..parsers import SUPPORTED_FILE_FORMATS
from ..reporter import REPORTER_BY_NAME, Reporter
from ..schema_loader import (
BuiltinSchemaLoader,
MetaSchemaLoader,
SchemaLoader,
SchemaLoaderBase,
)
from .transforms import TRANSFORM_LIBRARY, Transform
from ..transforms import TRANSFORM_LIBRARY
from .param_types import CommaDelimitedList
from .parse_result import ParseResult, SchemaLoadingMode
from .warnings import deprecation_warning_callback

BUILTIN_SCHEMA_NAMES = [f"vendor.{k}" for k in SCHEMA_CATALOG.keys()] + [
f"custom.{k}" for k in CUSTOM_SCHEMA_NAMES
Expand All @@ -28,68 +30,6 @@
)


class SchemaLoadingMode(enum.Enum):
filepath = "filepath"
builtin = "builtin"
metaschema = "metaschema"


class ParseResult:
def __init__(self) -> None:
# primary options: schema + instances
self.schema_mode: SchemaLoadingMode = SchemaLoadingMode.filepath
self.schema_path: str | None = None
self.instancefiles: tuple[str, ...] = ()
# cache controls
self.disable_cache: bool = False
self.cache_filename: str | None = None
# filetype detection (JSON, YAML, TOML, etc)
self.default_filetype: str = "json"
# data-transform (for Azure Pipelines and potentially future transforms)
self.data_transform: Transform | None = None
# fill default values on instances during validation
self.fill_defaults: bool = False
# regex format options
self.disable_format: bool = False
self.format_regex: RegexFormatBehavior = RegexFormatBehavior.default
# error and output controls
self.verbosity: int = 1
self.traceback_mode: str = "short"
self.output_format: str = "text"

def set_schema(
self, schemafile: str | None, builtin_schema: str | None, check_metaschema: bool
) -> None:
mutex_arg_count = sum(
1 if x else 0 for x in (schemafile, builtin_schema, check_metaschema)
)
if mutex_arg_count == 0:
raise click.UsageError(
"Either --schemafile, --builtin-schema, or --check-metaschema "
"must be provided"
)
if mutex_arg_count > 1:
raise click.UsageError(
"--schemafile, --builtin-schema, and --check-metaschema "
"are mutually exclusive"
)

if schemafile:
self.schema_mode = SchemaLoadingMode.filepath
self.schema_path = schemafile
elif builtin_schema:
self.schema_mode = SchemaLoadingMode.builtin
self.schema_path = builtin_schema
else:
self.schema_mode = SchemaLoadingMode.metaschema

@property
def format_opts(self) -> FormatOptions:
return FormatOptions(
enabled=not self.disable_format, regex_behavior=self.format_regex
)


def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
if "NO_COLOR" in os.environ:
ctx.color = False
Expand All @@ -101,15 +41,30 @@ def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
}[value]


def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
return textwrap.indent(
"\n".join(
textwrap.wrap(
", ".join(values),
width=75,
break_long_words=False,
break_on_hyphens=False,
),
),
" ",
)


@click.command(
"check-jsonschema",
help="""\
Check JSON and YAML files against a JSON Schema.

The schema is specified either with '--schemafile' or with '--builtin-schema'.

'check-jsonschema' supports and checks the following formats by default:
date, email, ipv4, regex, uuid
'check-jsonschema' supports format checks with appropriate libraries installed,
including the following formats by default:
date, email, ipv4, ipv6, regex, uuid

\b
For the "regex" format, there are multiple modes which can be specified with
Expand All @@ -121,17 +76,13 @@ def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
\b
The '--builtin-schema' flag supports the following schema names:
"""
+ textwrap.indent(
"\n".join(
textwrap.wrap(
", ".join(BUILTIN_SCHEMA_NAMES),
width=75,
break_long_words=False,
break_on_hyphens=False,
),
),
" ",
),
+ pretty_helptext_list(BUILTIN_SCHEMA_NAMES)
+ """\

\b
The '--disable-formats' flag supports the following formats:
"""
+ pretty_helptext_list(KNOWN_FORMATS),
)
@click.help_option("-h", "--help")
@click.version_option()
Expand Down Expand Up @@ -170,13 +121,29 @@ def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
),
)
@click.option(
"--disable-format", is_flag=True, help="Disable all format checks in the schema."
"--disable-format",
is_flag=True,
help="{deprecated} Disable all format checks in the schema.",
callback=deprecation_warning_callback(
"--disable-format",
is_flag=True,
append_message="Users should now pass '--disable-formats \"*\"' for "
"the same functionality.",
),
)
@click.option(
"--disable-formats",
multiple=True,
help="Disable specific format checks in the schema. "
"Pass '*' to disable all format checks.",
type=CommaDelimitedList(choices=("*", *KNOWN_FORMATS)),
metavar="{*|FORMAT,FORMAT,...}",
)
@click.option(
"--format-regex",
help=(
"Set the mode of format validation for regexes. "
"If '--disable-format' is used, this option has no effect."
"If `--disable-formats regex` is used, this option has no effect."
),
default=RegexFormatBehavior.default.value,
type=click.Choice([x.value for x in RegexFormatBehavior], case_sensitive=False),
Expand Down Expand Up @@ -249,6 +216,7 @@ def main(
no_cache: bool,
cache_filename: str | None,
disable_format: bool,
disable_formats: tuple[list[str], ...],
format_regex: str,
default_filetype: str,
traceback_mode: str,
Expand All @@ -264,7 +232,13 @@ def main(
args.set_schema(schemafile, builtin_schema, check_metaschema)
args.instancefiles = instancefiles

args.disable_format = disable_format
normalized_disable_formats: tuple[str, ...] = tuple(
f for sublist in disable_formats for f in sublist
)
if disable_format or "*" in normalized_disable_formats:
args.disable_all_formats = True
else:
args.disable_formats = normalized_disable_formats
args.format_regex = RegexFormatBehavior(format_regex)
args.disable_cache = no_cache
args.default_filetype = default_filetype
Expand Down
45 changes: 45 additions & 0 deletions src/check_jsonschema/cli/param_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

import typing as t

import click


class CommaDelimitedList(click.ParamType):
def __init__(
self,
*,
convert_values: t.Callable[[str], str] | None = None,
choices: t.Iterable[str] | None = None,
) -> None:
super().__init__()
self.convert_values = convert_values
self.choices = list(choices) if choices is not None else None

def get_metavar(self, param: click.Parameter) -> str:
if self.choices is not None:
return "{" + ",".join(self.choices) + "}"
return "TEXT,TEXT,..."

def convert(
self, value: str, param: click.Parameter | None, ctx: click.Context | None
) -> list[str]:
value = super().convert(value, param, ctx)

# if `--foo` is a comma delimited list and someone passes
# `--foo ""`, take that as `foo=[]` rather than foo=[""]
resolved = value.split(",") if value else []

if self.convert_values is not None:
resolved = [self.convert_values(x) for x in resolved]

if self.choices is not None:
bad_values = [x for x in resolved if x not in self.choices]
if bad_values:
self.fail(
f"the values {bad_values} were not valid choices",
param=param,
ctx=ctx,
)

return resolved
Loading