Skip to content

Commit de892d9

Browse files
committed
Don't exit if multiple matches found for a filter
Previously, specifying a filter that returned multiple results would result in an error. This made sense for API v1.0 because filters could only be specified once. However, this is not necessarily the case for v1.1, which does support multiple filters. In addition, it was inconsistent for v1.0, as specifying multiple filters on the command line would result in a warning, not an error. Resolve this by only warning the user if multiple matches are found for a given filter. This requires adding a check to ensure at least three characters are provided for the filter so as not to make it too generic. Signed-off-by: Stephen Finucane <[email protected]>
1 parent 4e0f7ba commit de892d9

File tree

9 files changed

+139
-72
lines changed

9 files changed

+139
-72
lines changed

git_pw/api.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,51 @@ def new_func(ctx, *args, **kwargs):
286286

287287
value = list(kwargs[param.name] or [])
288288
if value and len(value) > 1 and value != param.default:
289-
msg = ('Filtering by multiple %ss is not supported with API '
289+
msg = ('The `--%s` filter was specified multiple times. '
290+
'Filtering by multiple %ss is not supported with API '
290291
'version 1.0. If the server supports it, use version '
291292
'1.1 instead. Refer to https://git.io/vN3vX for more '
292293
'information.')
293294

294-
LOG.warning(msg, param.name)
295+
LOG.warning(msg, param.name, param.name)
295296

296297
return ctx.invoke(f, *args, **kwargs)
297298

298299
return update_wrapper(new_func, f)
300+
301+
302+
def retrieve_filter_ids(resource_type, filter_name, filter_value):
303+
"""Retrieve IDs for items passed through by filter.
304+
305+
Some filters require client-side filtering, e.g. filtering patches by
306+
submitter names.
307+
308+
Arguments:
309+
resource_type: The filter's resource endpoint name.
310+
filter_name: The name of the filter.
311+
filter_value: The value of the filter.
312+
313+
Returns:
314+
A list of querystring key-value pairs to use in the actual request.
315+
"""
316+
if len(filter_value) < 3:
317+
# protect agaisnt really generic (and essentially meaningless) queries
318+
LOG.error('Filters must be at least 3 characters long')
319+
sys.exit(1)
320+
321+
# NOTE(stephenfin): This purposefully ignores the possiblity of a second
322+
# page because it's unlikely and likely unnecessary
323+
items = index(resource_type, [('q', filter_value)])
324+
if len(items) == 0:
325+
LOG.warning('No matching %s found: %s', filter_name, filter_value)
326+
elif len(items) > 1 and version() < (1, 1):
327+
# we don't support multiple filters in 1.0
328+
msg = ('More than one match for found for `--%s=%s`. '
329+
'Filtering by multiple %ss is not supported with '
330+
'API version 1.0. If the server supports it, use '
331+
'version 1.1 instead. Refer to https://git.io/vN3vX '
332+
'for more information.')
333+
334+
LOG.warning(msg, filter_name, filter_value, filter_name)
335+
336+
return [(filter_name, item['id']) for item in items]

git_pw/bundle.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,7 @@ def list_cmd(owner, limit, page, sort, name):
141141
if (api.version() >= (1, 1) and '@' not in own) or own.isdigit():
142142
params.append(('owner', own))
143143
else:
144-
users = api.index('users', [('q', own)])
145-
if len(users) == 0:
146-
LOG.error('No matching owner found: %s', own)
147-
sys.exit(1)
148-
elif len(users) > 1:
149-
LOG.error('More than one owner found: %s', own)
150-
sys.exit(1)
151-
152-
params.append(('owner', users[0]['id']))
144+
params.extend(api.retrieve_filter_ids('users', 'owner', own))
153145

154146
params.extend([
155147
('q', name),

git_pw/patch.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -220,30 +220,14 @@ def list_cmd(state, submitter, delegate, archived, limit, page, sort, name):
220220
if (api.version() >= (1, 1) and '@' in subm) or subm.isdigit():
221221
params.append(('submitter', subm))
222222
else:
223-
people = api.index('people', [('q', subm)])
224-
if len(people) == 0:
225-
LOG.error('No matching submitter found: %s', subm)
226-
sys.exit(1)
227-
elif len(people) > 1:
228-
LOG.error('More than one submitter found: %s', subm)
229-
sys.exit(1)
230-
231-
params.append(('submitter', people[0]['id']))
223+
params.extend(api.retrieve_filter_ids('people', 'submitter', subm))
232224

233225
for delg in delegate:
234226
# we support server-side filtering by username (but not email) in 1.1
235227
if (api.version() >= (1, 1) and '@' not in delg) or delg.isdigit():
236228
params.append(('delegate', delg))
237229
else:
238-
users = api.index('users', [('q', delg)])
239-
if len(users) == 0:
240-
LOG.error('No matching delegates found: %s', delg)
241-
sys.exit(1)
242-
elif len(users) > 1:
243-
LOG.error('More than one delegate found: %s', delg)
244-
sys.exit(1)
245-
246-
params.append(('delegate', users[0]['id']))
230+
params.extend(api.retrieve_filter_ids('users', 'delegate', delg))
247231

248232
params.extend([
249233
('q', name),

git_pw/series.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,7 @@ def list_cmd(submitter, limit, page, sort, name):
128128
if (api.version() >= (1, 1) and '@' in subm) or subm.isdigit():
129129
params.append(('submitter', subm))
130130
else:
131-
people = api.index('people', [('q', subm)])
132-
if len(people) == 0:
133-
LOG.error('No matching submitter found: %s', subm)
134-
sys.exit(1)
135-
elif len(people) > 1:
136-
LOG.error('More than one submitter found: %s', subm)
137-
sys.exit(1)
138-
139-
params.append(('submitter', people[0]['id']))
131+
params.extend(api.retrieve_filter_ids('people', 'submitter', subm))
140132

141133
params.extend([
142134
('q', name),
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
Filtering patches, bundles and series by user will now only display a
5+
warning if multiple matches are found for a given filter and you're using
6+
API v1.0. Previously this would have been an unconditional error.

tests/test_api.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,65 @@ def test_version(mock_server):
7171
mock_server.return_value = 'https://example.com/api/1.1'
7272

7373
assert api.version() == (1, 1)
74+
75+
76+
@mock.patch.object(api, 'index')
77+
def test_retrieve_filter_ids_too_short(mock_index):
78+
with pytest.raises(SystemExit):
79+
api.retrieve_filter_ids('users', 'owner', 'f')
80+
81+
assert not mock_index.called
82+
83+
84+
@mock.patch.object(api, 'LOG')
85+
@mock.patch.object(api, 'index')
86+
def test_retrieve_filter_ids_no_matches(mock_index, mock_log):
87+
mock_index.return_value = []
88+
89+
ids = api.retrieve_filter_ids('users', 'owner', 'foo')
90+
91+
assert mock_log.warning.called
92+
assert ids == []
93+
94+
95+
@mock.patch.object(api, 'LOG')
96+
@mock.patch.object(api, 'version')
97+
@mock.patch.object(api, 'index')
98+
def test_retrieve_filter_ids_multiple_matches_1_0(mock_index, mock_version,
99+
mock_log):
100+
mock_index.return_value = [
101+
{'id': 1}, {'id': 2}, # incomplete but good enough
102+
]
103+
mock_version.return_value = (1, 0)
104+
105+
ids = api.retrieve_filter_ids('users', 'owner', 'foo')
106+
107+
assert mock_log.warning.called
108+
assert ids == [('owner', 1), ('owner', 2)]
109+
110+
111+
@mock.patch.object(api, 'LOG')
112+
@mock.patch.object(api, 'version')
113+
@mock.patch.object(api, 'index')
114+
def test_retrieve_filter_ids_multiple_matches_1_1(mock_index, mock_version,
115+
mock_log):
116+
mock_index.return_value = [
117+
{'id': 1}, {'id': 2}, # incomplete but good enough
118+
]
119+
mock_version.return_value = (1, 1)
120+
121+
ids = api.retrieve_filter_ids('users', 'owner', 'foo')
122+
123+
assert not mock_log.warning.called
124+
assert ids == [('owner', 1), ('owner', 2)]
125+
126+
127+
@mock.patch.object(api, 'LOG')
128+
@mock.patch.object(api, 'index')
129+
def test_retrieve_filter_ids(mock_index, mock_log):
130+
mock_index.return_value = [{'id': 1}]
131+
132+
ids = api.retrieve_filter_ids('users', 'owner', 'foo')
133+
134+
assert not mock_log.warning.called
135+
assert ids == [('owner', 1)]

tests/test_bundle.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,32 +232,31 @@ def test_list_with_filters(self, mock_index, mock_version):
232232

233233
mock_index.assert_has_calls(calls)
234234

235-
@mock.patch('git_pw.bundle.LOG')
236-
def test_list_with_invalid_filters(self, mock_log, mock_index,
237-
mock_version):
238-
"""Validate behavior with filters applied.
235+
@mock.patch('git_pw.api.LOG')
236+
def test_list_with_wildcard_filters(self, mock_log, mock_index,
237+
mock_version):
238+
"""Validate behavior with a "wildcard" filter.
239239
240-
Try to filter against a submitter filter that's too broad. This should
241-
error out saying that too many possible submitters were found.
240+
Patchwork API v1.0 did not support multiple filters correctly. Ensure
241+
the user is warned as necessary if a filter has multiple matches.
242242
"""
243243

244244
people_rsp = [self._get_users(), self._get_users()]
245245
bundle_rsp = [self._get_bundle()]
246246
mock_index.side_effect = [people_rsp, bundle_rsp]
247247

248248
runner = CLIRunner()
249-
result = runner.invoke(bundle.list_cmd, ['--owner', 'john.doe'])
249+
runner.invoke(bundle.list_cmd, ['--owner', 'john.doe'])
250250

251-
assert result.exit_code == 1, result
252-
assert mock_log.error.called
251+
assert mock_log.warning.called
253252

254253
@mock.patch('git_pw.api.LOG')
255254
def test_list_with_multiple_filters(self, mock_log, mock_index,
256255
mock_version):
257256
"""Validate behavior with use of multiple filters.
258257
259258
Patchwork API v1.0 did not support multiple filters correctly. Ensure
260-
the user is warned as necessary.
259+
the user is warned as necessary if they specify multiple filters.
261260
"""
262261

263262
people_rsp = [self._get_users()]

tests/test_patch.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -337,35 +337,31 @@ def test_list_with_filters(self, mock_index, mock_version):
337337

338338
mock_index.assert_has_calls(calls)
339339

340-
@mock.patch('git_pw.patch.LOG')
341-
def test_list_with_invalid_filters(self, mock_log, mock_index,
342-
mock_version):
343-
"""Validate behavior with filters applied.
340+
@mock.patch('git_pw.api.LOG')
341+
def test_list_with_wildcard_filters(self, mock_log, mock_index,
342+
mock_version):
343+
"""Validate behavior with a "wildcard" filter.
344344
345-
Try to filter against a sumbmitter filter that's too broad. This should
346-
error out saying that too many possible submitters were found.
345+
Patchwork API v1.0 did not support multiple filters correctly. Ensure
346+
the user is warned as necessary if a filter has multiple matches.
347347
"""
348348

349349
people_rsp = [self._get_person(), self._get_person()]
350350
patch_rsp = [self._get_patch()]
351351
mock_index.side_effect = [people_rsp, patch_rsp]
352352

353353
runner = CLIRunner()
354-
result = runner.invoke(patch.list_cmd, ['--submitter',
355-
354+
runner.invoke(patch.list_cmd, ['--submitter', '[email protected]'])
356355

357-
assert result.exit_code == 1, result
358-
assert mock_log.error.called
359-
360-
mock_index.side_effect = [people_rsp, patch_rsp]
356+
assert mock_log.warning.called
361357

362358
@mock.patch('git_pw.api.LOG')
363359
def test_list_with_multiple_filters(self, mock_log, mock_index,
364360
mock_version):
365361
"""Validate behavior with use of multiple filters.
366362
367363
Patchwork API v1.0 did not support multiple filters correctly. Ensure
368-
the user is warned as necessary.
364+
the user is warned as necessary if they specify multiple filters.
369365
"""
370366

371367
people_rsp = [self._get_person()]

tests/test_series.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -187,33 +187,31 @@ def test_list_with_filters(self, mock_index, mock_version):
187187

188188
mock_index.assert_has_calls(calls)
189189

190-
@mock.patch('git_pw.series.LOG')
191-
def test_list_with_invalid_filters(self, mock_log, mock_index,
192-
mock_version):
193-
"""Validate behavior with filters applied.
190+
@mock.patch('git_pw.api.LOG')
191+
def test_list_with_wildcard_filters(self, mock_log, mock_index,
192+
mock_version):
193+
"""Validate behavior with a "wildcard" filter.
194194
195-
Try to filter against a sumbmitter filter that's too broad. This should
196-
error out saying that too many possible submitters were found.
195+
Patchwork API v1.0 did not support multiple filters correctly. Ensure
196+
the user is warned as necessary if a filter has multiple matches.
197197
"""
198198

199199
people_rsp = [self._get_people(), self._get_people()]
200200
series_rsp = [self._get_series()]
201201
mock_index.side_effect = [people_rsp, series_rsp]
202202

203203
runner = CLIRunner()
204-
result = runner.invoke(series.list_cmd, ['--submitter',
205-
204+
runner.invoke(series.list_cmd, ['--submitter', '[email protected]'])
206205

207-
assert result.exit_code == 1, result
208-
assert mock_log.error.called
206+
assert mock_log.warning.called
209207

210208
@mock.patch('git_pw.api.LOG')
211209
def test_list_with_multiple_filters(self, mock_log, mock_index,
212210
mock_version):
213211
"""Validate behavior with use of multiple filters.
214212
215213
Patchwork API v1.0 did not support multiple filters correctly. Ensure
216-
the user is warned as necessary.
214+
the user is warned as necessary if they specify multiple filters.
217215
"""
218216

219217
people_rsp = [self._get_people()]

0 commit comments

Comments
 (0)