Skip to content

Commit b638151

Browse files
author
Hamish Downer
committed
Make clear if error came from people or already selected
And name the file/gsheet-tab in more places
1 parent 21e1af8 commit b638151

File tree

4 files changed

+91
-14
lines changed

4 files changed

+91
-14
lines changed

src/sortition_algorithms/adapters.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ def _address_from_row(person: list[str]) -> tuple[str, ...]:
108108

109109

110110
class AbstractDataSource(abc.ABC):
111+
@property
112+
@abc.abstractmethod
113+
def people_data_container(self) -> str: ...
114+
115+
@property
116+
@abc.abstractmethod
117+
def already_selected_data_container(self) -> str: ...
118+
111119
@abc.abstractmethod
112120
@contextmanager
113121
def read_feature_data(
@@ -145,6 +153,11 @@ def customise_people_parse_error(
145153
self, error: ParseTableMultiError, headers: Sequence[str]
146154
) -> SelectionMultilineError: ...
147155

156+
@abc.abstractmethod
157+
def customise_already_selected_parse_error(
158+
self, error: ParseTableMultiError, headers: Sequence[str]
159+
) -> SelectionMultilineError: ...
160+
148161

149162
class SelectionData:
150163
def __init__(self, data_source: AbstractDataSource, gen_rem_tab: bool = True) -> None:
@@ -182,6 +195,7 @@ def load_people(self, settings: Settings, features: FeatureCollection) -> tuple[
182195
features=features,
183196
settings=settings,
184197
feature_column_name=self.feature_column_name,
198+
data_container=self.data_source.people_data_container,
185199
)
186200
except ParseTableMultiError as error:
187201
new_error = self.data_source.customise_people_parse_error(error, headers)
@@ -204,9 +218,10 @@ def load_already_selected(self, settings: Settings, features: FeatureCollection)
204218
features=features,
205219
settings=settings,
206220
feature_column_name=self.feature_column_name,
221+
data_container=self.data_source.already_selected_data_container,
207222
)
208223
except ParseTableMultiError as error:
209-
new_error = self.data_source.customise_people_parse_error(error, headers)
224+
new_error = self.data_source.customise_already_selected_parse_error(error, headers)
210225
raise new_error from error
211226
return people, report
212227

@@ -261,6 +276,14 @@ def __init__(self, features_data: str, people_data: str, already_selected_data:
261276
self.selected_file_written = False
262277
self.remaining_file_written = False
263278

279+
@property
280+
def people_data_container(self) -> str:
281+
return "people CSV data"
282+
283+
@property
284+
def already_selected_data_container(self) -> str:
285+
return "already selected CSV data"
286+
264287
@contextmanager
265288
def read_feature_data(
266289
self, report: RunReport
@@ -315,6 +338,12 @@ def customise_people_parse_error(
315338
# given the info is in strings, we can't usefully add anything
316339
return error
317340

341+
def customise_already_selected_parse_error(
342+
self, error: ParseTableMultiError, headers: Sequence[str]
343+
) -> SelectionMultilineError:
344+
# given the info is in strings, we can't usefully add anything
345+
return error
346+
318347

319348
class CSVFileDataSource(AbstractDataSource):
320349
def __init__(
@@ -332,6 +361,14 @@ def __init__(
332361
self.selected_file = selected_file
333362
self.remaining_file = remaining_file
334363

364+
@property
365+
def people_data_container(self) -> str:
366+
return f" CSV file '{self.people_file}'"
367+
368+
@property
369+
def already_selected_data_container(self) -> str:
370+
return f" CSV file '{self.already_selected_file}'"
371+
335372
@contextmanager
336373
def read_feature_data(
337374
self, report: RunReport
@@ -395,6 +432,14 @@ def customise_people_parse_error(
395432
*[str(e) for e in error.all_errors],
396433
])
397434

435+
def customise_already_selected_parse_error(
436+
self, error: ParseTableMultiError, headers: Sequence[str]
437+
) -> SelectionMultilineError:
438+
return SelectionMultilineError([
439+
f"Parser error(s) while reading already selected people from {self.already_selected_file}",
440+
*[str(e) for e in error.all_errors],
441+
])
442+
398443

399444
class GSheetTabNamer:
400445
def __init__(self) -> None:
@@ -463,6 +508,14 @@ def __init__(
463508
self.tab_namer = GSheetTabNamer()
464509
self._report = RunReport()
465510

511+
@property
512+
def people_data_container(self) -> str:
513+
return f"'{self.people_tab_name}' tab"
514+
515+
@property
516+
def already_selected_data_container(self) -> str:
517+
return f"'{self.already_selected_tab_name}' tab"
518+
466519
@property
467520
def client(self) -> gspread.client.Client:
468521
if self._client is None:
@@ -719,14 +772,22 @@ def customise_features_parse_error(
719772
self, error: ParseTableMultiError, headers: Sequence[str]
720773
) -> SelectionMultilineError:
721774
return SelectionMultilineError([
722-
f"Parser error(s) while reading features from {self.feature_tab_name} worksheet",
775+
f"Parser error(s) while reading features from '{self.feature_tab_name}' worksheet",
723776
*self._annotate_parse_errors_with_cell_names(error, headers),
724777
])
725778

726779
def customise_people_parse_error(
727780
self, error: ParseTableMultiError, headers: Sequence[str]
728781
) -> SelectionMultilineError:
729782
return SelectionMultilineError([
730-
f"Parser error(s) while reading people from {self.people_tab_name} worksheet",
783+
f"Parser error(s) while reading people from '{self.people_tab_name}' worksheet",
784+
*self._annotate_parse_errors_with_cell_names(error, headers),
785+
])
786+
787+
def customise_already_selected_parse_error(
788+
self, error: ParseTableMultiError, headers: Sequence[str]
789+
) -> SelectionMultilineError:
790+
return SelectionMultilineError([
791+
f"Parser error(s) while reading people from '{self.already_selected_tab_name}' worksheet",
731792
*self._annotate_parse_errors_with_cell_names(error, headers),
732793
])

src/sortition_algorithms/people.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,30 +148,35 @@ def find_person_by_position_in_category(self, feature_name: str, feature_value:
148148

149149

150150
# simple helper function to tidy the code below
151-
def _check_columns_exist_or_multiple(people_head: list[str], column_list: Iterable[str], error_label: str) -> None:
151+
def _check_columns_exist_or_multiple(
152+
people_head: list[str], column_list: Iterable[str], error_label: str, data_container: str = "people data"
153+
) -> None:
152154
people_head_lower = [h.lower() for h in people_head]
153155
for column in column_list:
154156
column = column.lower()
155157
column_count = people_head_lower.count(column)
156158
if column_count == 0:
157-
msg = f"No '{column}' column {error_label} found in people data!"
159+
msg = f"No '{column}' column {error_label} found in {data_container}!"
158160
raise BadDataError(msg)
159161
elif column_count > 1:
160-
msg = f"MORE THAN 1 '{column}' column {error_label} found in people data!"
162+
msg = f"MORE THAN 1 '{column}' column {error_label} found in {data_container}!"
161163
raise BadDataError(msg)
162164

163165

164-
def _check_people_head(people_head: list[str], features: FeatureCollection, settings: Settings) -> None:
166+
def _check_people_head(
167+
people_head: list[str], features: FeatureCollection, settings: Settings, data_container: str = "people data"
168+
) -> None:
165169
# check that id_column and all the features, columns_to_keep and
166170
# check_same_address_columns are in the people data fields...
167171
# check both for existence and duplicate column names
168-
_check_columns_exist_or_multiple(people_head, [settings.id_column], "(unique id)")
169-
_check_columns_exist_or_multiple(people_head, list(features.keys()), "(a feature)")
170-
_check_columns_exist_or_multiple(people_head, settings.columns_to_keep, "(to keep)")
172+
_check_columns_exist_or_multiple(people_head, [settings.id_column], "(unique id)", data_container)
173+
_check_columns_exist_or_multiple(people_head, list(features.keys()), "(a feature)", data_container)
174+
_check_columns_exist_or_multiple(people_head, settings.columns_to_keep, "(to keep)", data_container)
171175
_check_columns_exist_or_multiple(
172176
people_head,
173177
settings.check_same_address_columns,
174178
"(to check same address)",
179+
data_container,
175180
)
176181

177182

@@ -256,9 +261,10 @@ def read_in_people(
256261
features: FeatureCollection,
257262
settings: Settings,
258263
feature_column_name: str = "feature",
264+
data_container: str = "people data",
259265
) -> tuple[People, RunReport]:
260266
report = RunReport()
261-
_check_people_head(people_head, features, settings)
267+
_check_people_head(people_head, features, settings, data_container)
262268
# we need to iterate through more than once, so save as list here
263269
stripped_people_body = [normalise_dict(row) for row in people_body]
264270
report.add_lines(check_for_duplicate_people(stripped_people_body, settings))

tests/test_adapters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ def test_gsheet_customise_features_parse_error():
681681
)
682682
assert "Some error - see cell B4" in str(new_error)
683683
assert "Min greater than max - see cells C6 D6" in str(new_error)
684-
assert "Categories worksheet" in str(new_error)
684+
assert "'Categories' worksheet" in str(new_error)
685685

686686

687687
def test_gsheet_customise_people_parse_error():
@@ -695,7 +695,7 @@ def test_gsheet_customise_people_parse_error():
695695
parse_features_error, headers=("nationbuilder_id", "name", "gender")
696696
)
697697
assert "Another error - see cell C4" in str(new_error)
698-
assert "Respondents worksheet" in str(new_error)
698+
assert "'Respondents' worksheet" in str(new_error)
699699

700700

701701
def test_csv_load_feature_from_file_failure(tmp_path: Path):

tests/test_people.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,19 @@ def test_read_in_people_missing_feature_column(self):
311311
# Remove gender column from header
312312
people_head.remove("gender")
313313

314-
with pytest.raises(errors.BadDataError, match="No 'gender' column \\(a feature\\) found"):
314+
with pytest.raises(errors.BadDataError, match="No 'gender' column \\(a feature\\) found in people data"):
315315
read_in_people(people_head, people_body, features, settings)
316316

317+
def test_read_in_people_missing_feature_column_custom_container(self):
318+
"""Test read_in_people with missing feature column."""
319+
features, settings, people_head, people_body = self.create_test_data()
320+
321+
# Remove gender column from header
322+
people_head.remove("gender")
323+
324+
with pytest.raises(errors.BadDataError, match="No 'gender' column \\(a feature\\) found in dusty corner"):
325+
read_in_people(people_head, people_body, features, settings, data_container="dusty corner")
326+
317327
def test_read_in_people_missing_columns_to_keep(self):
318328
"""Test read_in_people with missing columns_to_keep."""
319329
features, settings, people_head, people_body = self.create_test_data()

0 commit comments

Comments
 (0)