Skip to content

Commit 95f98c7

Browse files
Add new --disable-formats flag to replace --disable-format (#261)
* Implement specific format disabling '--disable-formats' disables any given format string, and can be passed multiple times. '--disable-format' is deprecated. '--disable-formats "*"' can be used to disable all formats -- and in the implementation, it has slightly different behavior from disabling the full list of known formats. * Document `--disable-formats` Docs for the new option, a changelog entry, and a minor fix to some helptext. * Refactor CLI component into multiple modules Minor refactor, no new or changed code -- just renames. * --disable-formats support for comma delimited args - Convert to a custom metavar and a nicely printed list in the helptext - Use a new CommaDelimitedList type to capture comma delimited args, then join the results to keep the tuple of strings type which is desired. Add new test cases for this. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Improve doc of --disable-formats --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 539f4d9 commit 95f98c7

File tree

11 files changed

+374
-96
lines changed

11 files changed

+374
-96
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ Unreleased
1111
.. vendor-insert-here
1212
1313
- Update vendored schemas (2023-05-03)
14+
- A new option, ``--disable-formats`` replaces and enhances the
15+
``--disable-format`` flag. ``--disable-formats`` takes a format to disable
16+
and may be passed multiple times, allowing users to opt out of any specific
17+
format checks. ``--disable-format "*"`` can be used to disable all format
18+
checking. ``--disable-format`` is still supported, but is deprecated and
19+
emits a warning.
1420

1521
0.22.0
1622
------

docs/usage.rst

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,52 @@ following options can be used to control this behavior.
185185
``--disable-format``
186186
~~~~~~~~~~~~~~~~~~~~
187187

188-
Disable all ``"format"`` checks.
188+
.. warning::
189+
190+
This option is deprecated. Use ``--disable-formats "*"`` instead.
191+
192+
Disable all format checks.
193+
194+
``--disable-formats``
195+
~~~~~~~~~~~~~~~~~~~~~
196+
197+
Disable specified ``"format"`` checks.
198+
199+
Use ``--disable-formats "*"`` to disable all format checking.
189200

190201
Because ``"format"`` checking is not done by all JSON Schema tools, it is
191202
possible that a file may validate under a schema with a different tool, but
192-
fail with ``check-jsonschema`` if ``--disable-format`` is not set.
203+
fail with ``check-jsonschema`` if ``--disable-formats`` is not set.
204+
205+
This option may be specified multiple times or as a comma-delimited list and
206+
supports the following formats as arguments:
207+
208+
- ``date``
209+
- ``date-time``
210+
- ``duration``
211+
- ``email``
212+
- ``hostname``
213+
- ``idn-email``
214+
- ``idn-hostname``
215+
- ``ipv4``
216+
- ``ipv6``
217+
- ``iri``
218+
- ``iri-reference``
219+
- ``json-pointer``
220+
- ``regex``
221+
- ``relative-json-pointer``
222+
- ``time``
223+
- ``uri``
224+
- ``uri-reference``
225+
- ``uri-template``
226+
- ``uuid``
227+
228+
Example usage:
229+
230+
.. code-block:: bash
231+
232+
# disables all three of time, date-time, and iri
233+
--disable-formats time,date-time --disable-formats iri
193234
194235
``--format-regex``
195236
~~~~~~~~~~~~~~~~~~

src/check_jsonschema/cli/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .main_command import main
2+
3+
__all__ = ("main",)

src/check_jsonschema/cli.py renamed to src/check_jsonschema/cli/main_command.py

Lines changed: 61 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
from __future__ import annotations
22

3-
import enum
43
import os
54
import textwrap
65

76
import click
87

9-
from .catalog import CUSTOM_SCHEMA_NAMES, SCHEMA_CATALOG
10-
from .checker import SchemaChecker
11-
from .formats import FormatOptions, RegexFormatBehavior
12-
from .instance_loader import InstanceLoader
13-
from .parsers import SUPPORTED_FILE_FORMATS
14-
from .reporter import REPORTER_BY_NAME, Reporter
15-
from .schema_loader import (
8+
from ..catalog import CUSTOM_SCHEMA_NAMES, SCHEMA_CATALOG
9+
from ..checker import SchemaChecker
10+
from ..formats import KNOWN_FORMATS, RegexFormatBehavior
11+
from ..instance_loader import InstanceLoader
12+
from ..parsers import SUPPORTED_FILE_FORMATS
13+
from ..reporter import REPORTER_BY_NAME, Reporter
14+
from ..schema_loader import (
1615
BuiltinSchemaLoader,
1716
MetaSchemaLoader,
1817
SchemaLoader,
1918
SchemaLoaderBase,
2019
)
21-
from .transforms import TRANSFORM_LIBRARY, Transform
20+
from ..transforms import TRANSFORM_LIBRARY
21+
from .param_types import CommaDelimitedList
22+
from .parse_result import ParseResult, SchemaLoadingMode
23+
from .warnings import deprecation_warning_callback
2224

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

3032

31-
class SchemaLoadingMode(enum.Enum):
32-
filepath = "filepath"
33-
builtin = "builtin"
34-
metaschema = "metaschema"
35-
36-
37-
class ParseResult:
38-
def __init__(self) -> None:
39-
# primary options: schema + instances
40-
self.schema_mode: SchemaLoadingMode = SchemaLoadingMode.filepath
41-
self.schema_path: str | None = None
42-
self.instancefiles: tuple[str, ...] = ()
43-
# cache controls
44-
self.disable_cache: bool = False
45-
self.cache_filename: str | None = None
46-
# filetype detection (JSON, YAML, TOML, etc)
47-
self.default_filetype: str = "json"
48-
# data-transform (for Azure Pipelines and potentially future transforms)
49-
self.data_transform: Transform | None = None
50-
# fill default values on instances during validation
51-
self.fill_defaults: bool = False
52-
# regex format options
53-
self.disable_format: bool = False
54-
self.format_regex: RegexFormatBehavior = RegexFormatBehavior.default
55-
# error and output controls
56-
self.verbosity: int = 1
57-
self.traceback_mode: str = "short"
58-
self.output_format: str = "text"
59-
60-
def set_schema(
61-
self, schemafile: str | None, builtin_schema: str | None, check_metaschema: bool
62-
) -> None:
63-
mutex_arg_count = sum(
64-
1 if x else 0 for x in (schemafile, builtin_schema, check_metaschema)
65-
)
66-
if mutex_arg_count == 0:
67-
raise click.UsageError(
68-
"Either --schemafile, --builtin-schema, or --check-metaschema "
69-
"must be provided"
70-
)
71-
if mutex_arg_count > 1:
72-
raise click.UsageError(
73-
"--schemafile, --builtin-schema, and --check-metaschema "
74-
"are mutually exclusive"
75-
)
76-
77-
if schemafile:
78-
self.schema_mode = SchemaLoadingMode.filepath
79-
self.schema_path = schemafile
80-
elif builtin_schema:
81-
self.schema_mode = SchemaLoadingMode.builtin
82-
self.schema_path = builtin_schema
83-
else:
84-
self.schema_mode = SchemaLoadingMode.metaschema
85-
86-
@property
87-
def format_opts(self) -> FormatOptions:
88-
return FormatOptions(
89-
enabled=not self.disable_format, regex_behavior=self.format_regex
90-
)
91-
92-
9333
def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
9434
if "NO_COLOR" in os.environ:
9535
ctx.color = False
@@ -101,15 +41,30 @@ def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
10141
}[value]
10242

10343

44+
def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
45+
return textwrap.indent(
46+
"\n".join(
47+
textwrap.wrap(
48+
", ".join(values),
49+
width=75,
50+
break_long_words=False,
51+
break_on_hyphens=False,
52+
),
53+
),
54+
" ",
55+
)
56+
57+
10458
@click.command(
10559
"check-jsonschema",
10660
help="""\
10761
Check JSON and YAML files against a JSON Schema.
10862
10963
The schema is specified either with '--schemafile' or with '--builtin-schema'.
11064
111-
'check-jsonschema' supports and checks the following formats by default:
112-
date, email, ipv4, regex, uuid
65+
'check-jsonschema' supports format checks with appropriate libraries installed,
66+
including the following formats by default:
67+
date, email, ipv4, ipv6, regex, uuid
11368
11469
\b
11570
For the "regex" format, there are multiple modes which can be specified with
@@ -121,17 +76,13 @@ def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
12176
\b
12277
The '--builtin-schema' flag supports the following schema names:
12378
"""
124-
+ textwrap.indent(
125-
"\n".join(
126-
textwrap.wrap(
127-
", ".join(BUILTIN_SCHEMA_NAMES),
128-
width=75,
129-
break_long_words=False,
130-
break_on_hyphens=False,
131-
),
132-
),
133-
" ",
134-
),
79+
+ pretty_helptext_list(BUILTIN_SCHEMA_NAMES)
80+
+ """\
81+
82+
\b
83+
The '--disable-formats' flag supports the following formats:
84+
"""
85+
+ pretty_helptext_list(KNOWN_FORMATS),
13586
)
13687
@click.help_option("-h", "--help")
13788
@click.version_option()
@@ -170,13 +121,29 @@ def set_color_mode(ctx: click.Context, param: str, value: str) -> None:
170121
),
171122
)
172123
@click.option(
173-
"--disable-format", is_flag=True, help="Disable all format checks in the schema."
124+
"--disable-format",
125+
is_flag=True,
126+
help="{deprecated} Disable all format checks in the schema.",
127+
callback=deprecation_warning_callback(
128+
"--disable-format",
129+
is_flag=True,
130+
append_message="Users should now pass '--disable-formats \"*\"' for "
131+
"the same functionality.",
132+
),
133+
)
134+
@click.option(
135+
"--disable-formats",
136+
multiple=True,
137+
help="Disable specific format checks in the schema. "
138+
"Pass '*' to disable all format checks.",
139+
type=CommaDelimitedList(choices=("*", *KNOWN_FORMATS)),
140+
metavar="{*|FORMAT,FORMAT,...}",
174141
)
175142
@click.option(
176143
"--format-regex",
177144
help=(
178145
"Set the mode of format validation for regexes. "
179-
"If '--disable-format' is used, this option has no effect."
146+
"If `--disable-formats regex` is used, this option has no effect."
180147
),
181148
default=RegexFormatBehavior.default.value,
182149
type=click.Choice([x.value for x in RegexFormatBehavior], case_sensitive=False),
@@ -249,6 +216,7 @@ def main(
249216
no_cache: bool,
250217
cache_filename: str | None,
251218
disable_format: bool,
219+
disable_formats: tuple[list[str], ...],
252220
format_regex: str,
253221
default_filetype: str,
254222
traceback_mode: str,
@@ -264,7 +232,13 @@ def main(
264232
args.set_schema(schemafile, builtin_schema, check_metaschema)
265233
args.instancefiles = instancefiles
266234

267-
args.disable_format = disable_format
235+
normalized_disable_formats: tuple[str, ...] = tuple(
236+
f for sublist in disable_formats for f in sublist
237+
)
238+
if disable_format or "*" in normalized_disable_formats:
239+
args.disable_all_formats = True
240+
else:
241+
args.disable_formats = normalized_disable_formats
268242
args.format_regex = RegexFormatBehavior(format_regex)
269243
args.disable_cache = no_cache
270244
args.default_filetype = default_filetype
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
5+
import click
6+
7+
8+
class CommaDelimitedList(click.ParamType):
9+
def __init__(
10+
self,
11+
*,
12+
convert_values: t.Callable[[str], str] | None = None,
13+
choices: t.Iterable[str] | None = None,
14+
) -> None:
15+
super().__init__()
16+
self.convert_values = convert_values
17+
self.choices = list(choices) if choices is not None else None
18+
19+
def get_metavar(self, param: click.Parameter) -> str:
20+
if self.choices is not None:
21+
return "{" + ",".join(self.choices) + "}"
22+
return "TEXT,TEXT,..."
23+
24+
def convert(
25+
self, value: str, param: click.Parameter | None, ctx: click.Context | None
26+
) -> list[str]:
27+
value = super().convert(value, param, ctx)
28+
29+
# if `--foo` is a comma delimited list and someone passes
30+
# `--foo ""`, take that as `foo=[]` rather than foo=[""]
31+
resolved = value.split(",") if value else []
32+
33+
if self.convert_values is not None:
34+
resolved = [self.convert_values(x) for x in resolved]
35+
36+
if self.choices is not None:
37+
bad_values = [x for x in resolved if x not in self.choices]
38+
if bad_values:
39+
self.fail(
40+
f"the values {bad_values} were not valid choices",
41+
param=param,
42+
ctx=ctx,
43+
)
44+
45+
return resolved

0 commit comments

Comments
 (0)