Skip to content

Commit ef256f2

Browse files
committed
Demo custom constraint errors on download command
This command has one of the most complex input specification implemented as nested constraints. Here is a demo of a (semi-structured) error message rendering before this change: ``` ❯ datalad download wurst [ERROR ] 1 command parameter constraint violation spec=['wurst'] does not match any of 3 alternatives - does not match any of 2 alternatives - not a recognized mapping - does not match any of 2 alternatives - the JSON object must be str, bytes or bytearray, not list - not a string - does not match any of 2 alternatives - not enough values to unpack (expected 2, got 1) - does not match any of 2 alternatives - Expecting value: line 1 column 1 (char 0) - URL is missing 'scheme' component - not '-', or a path to an existing file ``` There is a lot of information already, but clarity is lacking. This change applied the `WithDescription` meta constraint to bring clarity to the error messages. The new message for the same invalid call used above now looks like this ``` ❯ datalad download wurst [ERROR ] 1 command parameter constraint violation spec=['wurst'] does not provide URL->(PATH|-) mapping(s) - not a single item - not a dict, length-2-iterable, or space-delimited str - not a URL with a path component from which a filename can be derived - not a JSON-encoded str with an object or length-2-array - not a list of any such item - not a path to a file with one such item per-line, nor '-' to read any such item from STDIN ``` The custom message use the fact that the order of constraints and therefore the order in which violations are detected is deterministic. The respective individual message can reference prior messages such that the result is a more cohesive whole that, importantly, can avoid needless repetition of information. Conceptually (and technically), although this message is composed of the output of multiple constraints, it is neverthess still a *single* message, produced by a *single* top-level constraint (`AnyOf`). Therefore, we need not worry much about this information being taken apart and thereby becoming harder to comprehend or incomplete. The particular set of example message may inform #274 and guidelines on how constraints and their messaging should be implemented.
1 parent e064b3b commit ef256f2

File tree

1 file changed

+47
-19
lines changed

1 file changed

+47
-19
lines changed

datalad_next/commands/download.py

+47-19
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
EnsurePath,
3636
EnsureURL,
3737
EnsureValue,
38+
WithDescription,
3839
)
3940
from datalad_next.constraints.dataset import EnsureDataset
4041
from datalad_next.url_operations.any import AnyUrlOperations
@@ -101,37 +102,64 @@ class Download(ValidatedInterface):
101102
# The special value '-' is used to indicate stdout
102103
# if given as a single string, we support single-space-delimited items:
103104
# "<url> <path>"
104-
url2path_constraint = EnsureMapping(
105-
key=url_constraint,
106-
value=EnsureValue('-') | EnsurePath(),
107-
delimiter=' ',
108-
# we disallow length-2 sequences to be able to distinguish from
109-
# a length-2 list of URLs.
110-
# the key issue is the flexibility of EnsurePath -- pretty much
111-
# anything could be a valid unix path
112-
allow_length2_sequence=False,
105+
url2path_constraint = WithDescription(
106+
EnsureMapping(
107+
key=url_constraint,
108+
value=EnsureValue('-') | EnsurePath(),
109+
delimiter=' ',
110+
# we disallow length-2 sequences to be able to distinguish from
111+
# a length-2 list of URLs.
112+
# the key issue is the flexibility of EnsurePath -- pretty much
113+
# anything could be a valid unix path
114+
allow_length2_sequence=False,
115+
),
116+
error_message=f'not a dict, length-2-iterable, or space-delimited str',
113117
)
114118
# each specification items is either a mapping url->path, just a url, or a
115119
# JSON-encoded url->path mapping. the order is complex-to-simple for the
116120
# first two (to be able to distinguish a mapping from an encoded URL. The
117121
# JSON-encoding is tried last, it want match accidentally)
118-
spec_item_constraint = url2path_constraint | (
119-
(
120-
EnsureJSON() | EnsureURLFilenamePairFromURL()
121-
) & url2path_constraint)
122+
urlonly_item_constraint = WithDescription(
123+
EnsureURLFilenamePairFromURL() & url2path_constraint,
124+
error_message='not a URL with a path component '
125+
'from which a filename can be derived',
126+
)
127+
json_item_constraint = WithDescription(
128+
EnsureJSON() & url2path_constraint,
129+
error_message='not a JSON-encoded str with an object or length-2-array',
130+
)
131+
any_item_constraint = WithDescription(
132+
AnyOf(
133+
# TODO explain
134+
url2path_constraint,
135+
urlonly_item_constraint,
136+
json_item_constraint,
137+
),
138+
error_message='not a single item\n{__itemized_causes__}',
139+
)
122140

123141
# we support reading specification items (matching any format defined
124142
# above) as
125143
# - a single item
126144
# - as a list of items
127145
# - a list given in a file, or via stdin (or any file-like in Python)
128-
spec_constraint = AnyOf(
129-
spec_item_constraint,
130-
EnsureListOf(spec_item_constraint),
131-
EnsureGeneratorFromFileLike(
132-
spec_item_constraint,
133-
exc_mode='yield',
146+
spec_constraint = WithDescription(
147+
AnyOf(
148+
any_item_constraint,
149+
WithDescription(
150+
EnsureListOf(any_item_constraint),
151+
error_message='not a list of any such item',
152+
),
153+
WithDescription(
154+
EnsureGeneratorFromFileLike(
155+
any_item_constraint,
156+
exc_mode='yield',
157+
),
158+
error_message="not a path to a file with one such item per-line, "
159+
"nor '-' to read any such item from STDIN",
160+
),
134161
),
162+
error_message="does not provide URL->(PATH|-) mapping(s)\n{__itemized_causes__}"
135163
)
136164

137165
force_choices = EnsureChoice('overwrite-existing')

0 commit comments

Comments
 (0)