""" clean_participant_fields.py Usage: python clean_participant_fields.py [ ...] Pass any number of __init__.py files, with settings.py always last. Reads PARTICIPANT_FIELDS from settings.py, checks each value for any string match in the two __init__.py files, and writes a cleaned settings_cleaned.py with unused fields removed. Prints a report of removed fields. """ import sys import re def load_file(path): with open(path, "r") as f: return f.read() def extract_participant_fields(settings_src): """ Locate the PARTICIPANT_FIELDS = [ ... ] block and extract all string values. Returns (list_of_field_strings, match_object) so we can reconstruct the file. Uses a regex that handles multiline lists with single or double quoted strings, and preserves the ability to locate the block for replacement later. """ # Match the full PARTICIPANT_FIELDS = [ ... ] block (non-greedy, DOTALL) block_pattern = re.compile( r"(PARTICIPANT_FIELDS\s*=\s*\[)(.*?)(\])", re.DOTALL ) match = block_pattern.search(settings_src) if not match: raise ValueError("Could not find PARTICIPANT_FIELDS = [...] in settings.py") block_content = match.group(2) # Extract all quoted string values from within the block field_pattern = re.compile(r"['\"]([^'\"]+)['\"]") fields = field_pattern.findall(block_content) return fields, match def find_unused_fields(fields, init_sources): """ A field is considered 'used' if its string name appears anywhere in either __init__.py source. Simple substring match — covers both dot-access (participant.field_name) and dict-style (participant.vars['field_name']). """ combined_src = "\n".join(init_sources) unused = [f for f in fields if f not in combined_src] return unused def rebuild_field_list(block_content, unused_fields): """ Remove lines (or partial lines) from the PARTICIPANT_FIELDS block that contain only an unused field string. Comments and formatting are preserved where possible. Lines containing a removed field are dropped entirely. Design decision (per user request): preserve all other lines — including comments and blank lines — so the rest of settings.py is untouched. We drop a line only if it contains a quoted string that is in unused_fields AND contains no field that is still used. """ lines = block_content.split("\n") cleaned_lines = [] field_pattern = re.compile(r"['\"]([^'\"]+)['\"]") for line in lines: found_fields = field_pattern.findall(line) if not found_fields: # No field references on this line — keep it (comments, blanks, etc.) cleaned_lines.append(line) continue # Keep the line only if at least one field on it is NOT unused # (i.e. it has a field that is still being used) if any(f not in unused_fields for f in found_fields): cleaned_lines.append(line) # else: all fields on this line are unused — drop it silently return "\n".join(cleaned_lines) def main(): if len(sys.argv) < 3: print("Usage: python clean_participant_fields.py [ ...] ") sys.exit(1) # Design decision: settings.py is always the last argument; all prior # arguments are treated as __init__.py files, so the script scales to # any number of app files without modification. *init_paths, settings_path = sys.argv[1:] init_sources = [load_file(p) for p in init_paths] settings_src = load_file(settings_path) print(f"Scanning {len(init_paths)} __init__ file(s): {', '.join(init_paths)}") fields, match = extract_participant_fields(settings_src) unused = find_unused_fields(fields, init_sources) # --- Report --- print(f"\n{'='*50}") print(f"PARTICIPANT_FIELDS cleanup report") print(f"{'='*50}") print(f"Total fields found : {len(fields)}") print(f"Fields removed : {len(unused)}") print(f"Fields kept : {len(fields) - len(unused)}") if unused: print(f"\nRemoved fields:") for f in unused: print(f" - '{f}'") else: print("\nNo unused fields found. Nothing to remove.") print(f"{'='*50}\n") if not unused: sys.exit(0) # --- Rebuild settings.py --- original_block_content = match.group(2) cleaned_block_content = rebuild_field_list(original_block_content, unused) # Replace only the inner content of the PARTICIPANT_FIELDS block cleaned_settings = ( settings_src[:match.start(2)] + cleaned_block_content + settings_src[match.end(2):] ) output_path = "settings_cleaned.py" with open(output_path, "w") as f: f.write(cleaned_settings) print(f"Written to: {output_path}") if __name__ == "__main__": main()