From c6839bcfb653150c01cda4c7802ef6cd9f817e30 Mon Sep 17 00:00:00 2001 From: Jesse Mortenson Date: Wed, 3 Jun 2026 16:42:13 -0500 Subject: [PATCH 1/4] People lint CLI: add option --ignore-role-warnings --- openstates/cli/people.py | 86 ++++++++++++++------------ openstates/utils/people/lint_people.py | 50 ++++++++------- 2 files changed, 72 insertions(+), 64 deletions(-) diff --git a/openstates/cli/people.py b/openstates/cli/people.py index 0848fef3..ed5d945a 100644 --- a/openstates/cli/people.py +++ b/openstates/cli/people.py @@ -127,7 +127,7 @@ def print_summary(self) -> None: # pragma: no cover click.secho(name, bold=True) for type, count in collection.items(): click.secho( - f" {type:<25} {count:4d} {count/self.person_count*100:.0f}% " + f" {type:<25} {count:4d} {count / self.person_count * 100:.0f}% " ) else: click.secho(name + " - none", bold=True) @@ -251,7 +251,7 @@ def write_csv(files: list[Path], jurisdiction_id: str, output_filename: str) -> def lint_dir( - abbr: str, verbose: bool, municipal: bool, date: str, fix: bool, save_all: bool + abbr: str, verbose: bool, municipal: bool, date: str, fix: bool, save_all: bool, ignore_role_warnings: bool ) -> int: # pragma: no cover state_dir = get_data_path(abbr) legislative_filenames = (state_dir / "legislature").glob("*.yml") @@ -265,7 +265,7 @@ def lint_dir( settings = yaml.safe_load(f) try: - validator = Validator(abbr, settings, fix, save_all) + validator = Validator(abbr, settings, fix, save_all, ignore_role_warnings) except BadVacancy: sys.exit(-1) @@ -398,17 +398,17 @@ def load_directory_to_database(files: list[Path], purge: bool) -> None: def create_person( - fname: str, - lname: str, - name: str, - state: str, - district: str, - party: str, - rtype: str, - url: str, - image: str, - email: str, - start_date: str, + fname: str, + lname: str, + name: str, + state: str, + district: str, + party: str, + rtype: str, + url: str, + image: str, + email: str, + start_date: str, ) -> None: role = Role( type=rtype, @@ -497,17 +497,17 @@ def to_csv(abbreviations: list[str], upload: bool) -> None: @click.option("--email", prompt="Email", help="Email") @click.option("--start-date", prompt="Start Date", help="Start Date YYYY-MM-DD") def new( - fname: str, - lname: str, - name: str, - state: str, - district: str, - party: str, - rtype: str, - url: str, - image: str, - email: str, - start_date: str, + fname: str, + lname: str, + name: str, + state: str, + district: str, + party: str, + rtype: str, + url: str, + image: str, + email: str, + start_date: str, ) -> None: """ Create a new person record. @@ -558,11 +558,11 @@ def summarize(abbreviations: list[str], roster: bool) -> None: @click.option("--death", is_flag=True) @click.option("--vacant", is_flag=True) def retire( - date: str, - filenames: list[str], - reason: typing.Optional[str], - death: bool, - vacant: bool, + date: str, + filenames: list[str], + reason: typing.Optional[str], + death: bool, + vacant: bool, ) -> None: """ Retire a legislator, given END_DATE and FILENAME. @@ -623,7 +623,7 @@ def sync_images(abbreviations: list[str], skip_existing: bool) -> None: @click.option( "--save-all/--no-save-all", default=False, - help="Enable/disable automatic reforamting of YAML.", + help="Enable/disable automatic reformatting of YAML.", ) @click.option( "--municipal/--no-municipal", @@ -636,13 +636,19 @@ def sync_images(abbreviations: list[str], skip_existing: bool) -> None: default=None, help="Lint roles using a certain date instead of today.", ) +@click.option( + "--ignore-role-warnings/--do-not-ignore-role-warnings", + default=False, + help="Do not emit warnings for people with no active roles.", +) def lint( - abbreviations: list[str], - verbose: bool, - municipal: bool, - date: str, - fix: bool, - save_all: bool, + abbreviations: list[str], + verbose: bool, + municipal: bool, + date: str, + fix: bool, + save_all: bool, + ignore_role_warnings: bool, ) -> None: """ Lint YAML files. @@ -656,7 +662,7 @@ def lint( for abbr in abbreviations: click.secho("==== {} ====".format(abbr), bold=True) - error_count += lint_dir(abbr, verbose, municipal, date, fix, save_all) + error_count += lint_dir(abbr, verbose, municipal, date, fix, save_all, ignore_role_warnings) if error_count: click.secho(f"exiting with {error_count} errors", fg="red") @@ -749,8 +755,8 @@ def merge(abbr: str, input_dir: str, retirement: str, reset_offices: bool) -> No existing_people: list[Person] = [] directory = get_data_path(abbr) for filename in itertools.chain( - directory.glob("legislature/*.yml"), - directory.glob("retired/*.yml"), + directory.glob("legislature/*.yml"), + directory.glob("retired/*.yml"), ): existing_people.append(Person.load_yaml(filename)) diff --git a/openstates/utils/people/lint_people.py b/openstates/utils/people/lint_people.py index 651c5184..2248f258 100644 --- a/openstates/utils/people/lint_people.py +++ b/openstates/utils/people/lint_people.py @@ -48,9 +48,9 @@ class Missing: def validate_roles( - person: Person, - roles_key: str, - retired: bool = False, + person: Person, + roles_key: str, + retired: bool = False, ) -> list[str]: active = [role for role in getattr(person, roles_key) if role.is_active()] if len(active) == 0 and not retired: @@ -63,9 +63,10 @@ def validate_roles( def validate_roles_key( - person: Person, - person_type: PersonType, - fix: bool, + person: Person, + person_type: PersonType, + fix: bool, + ignore_role_warnings: bool, ) -> CheckResult: resp = CheckResult([], [], []) role_issues = validate_roles( @@ -80,7 +81,7 @@ def validate_roles_key( # municipals missing roles is a warning to avoid blocking lint if fix: resp.fixes = [MOVED_TO_RETIRED] - else: + elif not ignore_role_warnings: resp.warnings.extend(role_issues) else: resp.errors.extend(role_issues) @@ -111,7 +112,7 @@ def validate_offices(person: Person) -> list[str]: return errors -def validate_name(person: Person, person_type: PersonType, fix: bool) -> CheckResult: +def validate_name(person: Person, person_type: PersonType, fix: bool, ignore_role_warnings: bool) -> CheckResult: """some basic checks on a persons name""" errors = [] fixes = [] @@ -155,7 +156,7 @@ def validate_jurisdictions(person: Person, municipalities: list[str]) -> list[st def get_expected_districts( - settings: dict[str, dict], abbr: str + settings: dict[str, dict], abbr: str ) -> _EXPECTED_DISTRICTS_TYPE: expected = {} @@ -194,7 +195,7 @@ def get_expected_districts( def compare_districts( - expected: _EXPECTED_DISTRICTS_TYPE, actual: _ACTUAL_DISTRICTS_TYPE + expected: _EXPECTED_DISTRICTS_TYPE, actual: _ACTUAL_DISTRICTS_TYPE ) -> list[str]: errors = [] @@ -222,9 +223,10 @@ def compare_districts( class Validator: - def __init__(self, abbr: str, settings: dict, fix: bool, save_all: bool): + def __init__(self, abbr: str, settings: dict, fix: bool, save_all: bool, ignore_role_warnings: bool): self.fix = fix self.save_all = save_all + self.ignore_role_warnings = ignore_role_warnings self.expected = get_expected_districts(settings, abbr) self.errors: defaultdict[str, list[str]] = defaultdict(list) self.warnings: defaultdict[str, list[str]] = defaultdict(list) @@ -244,13 +246,13 @@ def __init__(self, abbr: str, settings: dict, fix: bool, save_all: bool): raise ValueError(f"invalid municipality id {m}") def process_validator_result( - self, - validator_func: typing.Callable[[Person, PersonType, bool], CheckResult], - person: Person, - person_type: PersonType, - original_filename: Path, + self, + validator_func: typing.Callable[[Person, PersonType, bool, bool], CheckResult], + person: Person, + person_type: PersonType, + original_filename: Path, ) -> None: - result = validator_func(person, person_type, self.fix) + result = validator_func(person, person_type, self.fix, self.ignore_role_warnings) self.errors[original_filename.name].extend(result.errors) self.warnings[original_filename.name].extend(result.warnings) if result.fixes: @@ -258,10 +260,10 @@ def process_validator_result( dump_obj(person, filename=original_filename) def validate_person( - self, - data: dict[str, typing.Any], - filename: Path, - person_type: PersonType, + self, + data: dict[str, typing.Any], + filename: Path, + person_type: PersonType, ) -> None: print_filename = filename.name try: @@ -326,9 +328,9 @@ def validate_old_district_names(self, person: Person) -> list[str]: errors = [] for role in person.roles: if ( - role.district - and role.district not in self.expected[role.type] - and role.district not in self.legacy_districts[role.type] + role.district + and role.district not in self.expected[role.type] + and role.district not in self.legacy_districts[role.type] ): errors.append(f"unknown district name: {role.type} {role.district}") return errors From 308fcc134715e6878eb192f365ee0eac1812688a Mon Sep 17 00:00:00 2001 From: Jesse Mortenson Date: Wed, 3 Jun 2026 16:45:25 -0500 Subject: [PATCH 2/4] Label 6.25.2 release --- CHANGELOG.md | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 556d2c64..5f99574e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog + +## 6.25.2 - Jun 3, 2026 +* Adds an --ignore-role-warnings flag to people lint CLI to reduce verbosity + +## 6.25.1 - May 5, 2026 * Fixes a syntax error that caused a bug when POSTing data in http-resilience mode, if supplying headers or other kwargs. diff --git a/pyproject.toml b/pyproject.toml index e1744639..3fa25d4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openstates" -version = "6.25.1" +version = "6.25.2" description = "core infrastructure for the openstates project" authors = ["James Turk "] license = "MIT" From e0c2892c89f4a299385fc74858fb5c0f45e42d17 Mon Sep 17 00:00:00 2001 From: Jesse Mortenson Date: Wed, 3 Jun 2026 17:01:33 -0500 Subject: [PATCH 3/4] People Lint: fix tests --- openstates/utils/tests/test_lint.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstates/utils/tests/test_lint.py b/openstates/utils/tests/test_lint.py index 9631d291..a7490bcd 100644 --- a/openstates/utils/tests/test_lint.py +++ b/openstates/utils/tests/test_lint.py @@ -54,21 +54,21 @@ ], ) def test_validate_name_errors(person, expected): - assert validate_name(person, PersonType.LEGISLATIVE, fix=False).errors == expected - assert validate_name(person, PersonType.LEGISLATIVE, fix=False).warnings == [] - assert validate_name(person, PersonType.LEGISLATIVE, fix=False).fixes == [] + assert validate_name(person, PersonType.LEGISLATIVE, fix=False, ignore_role_warnings=False).errors == expected + assert validate_name(person, PersonType.LEGISLATIVE, fix=False, ignore_role_warnings=False).warnings == [] + assert validate_name(person, PersonType.LEGISLATIVE, fix=False, ignore_role_warnings=False).fixes == [] def test_validate_name_fixes(): person = Person(id=EXAMPLE_OCD_PERSON_ID, name="Phillip Swoozle", roles=[]) - result = validate_name(person, PersonType.LEGISLATIVE, fix=True) + result = validate_name(person, PersonType.LEGISLATIVE, fix=True, ignore_role_warnings=False) assert result.errors == [] assert len(result.fixes) == 2 assert person.given_name == "Phillip" assert person.family_name == "Swoozle" # no fixes on an OK name - result = validate_name(person, PersonType.LEGISLATIVE, fix=True) + result = validate_name(person, PersonType.LEGISLATIVE, fix=True, ignore_role_warnings=False) assert result.errors == result.fixes == [] From 59527903047c5b55b78fb9ddccc7cb1184b59f81 Mon Sep 17 00:00:00 2001 From: Jesse Mortenson Date: Wed, 3 Jun 2026 17:04:41 -0500 Subject: [PATCH 4/4] People Lint: fix tests --- openstates/utils/tests/test_lint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstates/utils/tests/test_lint.py b/openstates/utils/tests/test_lint.py index a7490bcd..079e1404 100644 --- a/openstates/utils/tests/test_lint.py +++ b/openstates/utils/tests/test_lint.py @@ -244,7 +244,7 @@ def test_compare_districts_overfill(): def test_person_duplicates(): - v = Validator("ak", {}, False, False) + v = Validator("ak", {}, False, False, False) people = [ # duplicates across people @@ -296,7 +296,7 @@ def test_filename_id_test(): name="Jane Smith", roles=[], ) - v = Validator("ak", {}, False, False) + v = Validator("ak", {}, False, False, False) v.validate_person(person, Path("bad-filename"), PersonType.LEGISLATIVE) for err in v.errors["bad-filename"]: if "not in filename" in err: