Dead code sweep: 77 unreferenced Python symbols, 5 templates, 49 static assets, and two dormant subsystems
- Dominant language
- Python
- Stars
- 30
- Forks
- 3
- Avg merge
- 9h 28m
- Merged PRs (30d)
- 42
Description
🤖 Written by Claude
Companion to #1475 (dead URLs) and #1765 (the dead `classification_groupings` page). Those two cover the *routing* surface. This one covers everything else the sweep found: unreferenced Python symbols, templates and static assets, plus two dormant subsystems that need a human call.
Nothing here has been changed — this is a report.
## Method
`vulture` on its own was not usable: 2,965 hits at 60% confidence, almost all Django false positives (admin classes, form fields, `@receiver` handlers). It was kept only for unreachable-code detection.
The main pass was a purpose-built scanner:
1. AST-extract every class / function / method — 17,480 definitions, migrations excluded from the definition set but **included** as references.
2. Tokenise every file in the tree — `.py`, `.html`, `.js`, `.json`, `.md`, `.yml`, `.sh`, `.scss`, migrations, `static_files` — and count identifier occurrences.
3. Filter out this codebase's genuine dynamic-dispatch families, so they are never reported: `AnalysisNode` / `NodeView` / `AnalysisNodeSerializer` subclass discovery, `ImportTaskFactory.__subclasses__()`, `@export_column`, `@parser_path`, `@admin_action`, `@admin_model_action`, `@search_receiver`, `@receiver`, `@app.task`, plus Django hooks (`clean_` on forms, `get_` / `validate_` on serializers, `handle`, `get_queryset`, and ~60 more).
4. Confirm the survivors with a single `grep -rnw -F -f` over the whole tree.
**Every symbol below appears exactly once in the entire repository — its own definition line.** 77 symbols, ~580 lines.
Two scanner bugs were found and fixed mid-run, both of which had produced false positives: a `data/` directory of templates being skipped (which had wrongly flagged `CohortGenotypeCollection.get_common_filter_info`, used in `view_vcf.html`), and a regex that missed `{% static 'js/...' %}` paths.
## 1. The one that is probably a bug, not just dead code
`snpdb/admin_partition_archive.py` is never imported by anything. Django's admin autodiscovery only loads `/admin.py`, and `snpdb/admin.py` imports `snpdb.admin_partition_archive_mixin` but not `snpdb.admin_partition_archive`. So `@admin.register(PartitionArchive)` at line 12 never executes and **the PartitionArchive admin screen does not exist**, along with its `mark_restored` and `clear_dump` actions.
Either wire up the import or delete the module — but the current state is almost certainly not what was intended.
## 2. Dead Python symbols — 77 items, ~580 lines
| app | items | lines |
|---|---|---|
| classification | 21 | 187 |
| annotation | 10 | 135 |
| library | 14 | 97 |
| genes | 10 | 54 |
| seqauto | 5 | 44 |
| snpdb | 8 | 30 |
| pathtests | 3 | 14 |
| ontology, email_manager, variantgrid, beacon, review | 6 | 19 |
Largest individual items:
* `annotation/models/models_version_diff.py:56` `VersionDiff.create_version_diffs` — **92 lines** (see §6)
* `classification/evidence_key_rename.py:179` `EvidenceSelectKeyRenamer` — 59 lines. A migration helper that no migration ever used; its sibling `EvidenceKeyRenamer` in the same file *is* used.
* `library/django_utils/file_uploads.py:40` `filepond_load_initial` — 32 lines
* `classification/admin/condition_text_admin.py:65` `ConditionTextMatchUserFilter` — 22 lines. An admin `SimpleListFilter` that was never added to any `list_filter`.
* `classification/serializers.py` — the entire 18-line file. It already carries the comment `# Is this used?` above the class.
* `classification/views/classification_candidate_search_view.py:33` `view_classification_candidate_search` — a view function with no route. (Adjacent to #1475's territory, but it is a Python symbol rather than a registered URL name, so it would not appear in that scan.)
* `library/enums/time_enums.py` — `TimePeriod`, 28 lines. Its only two apparent callers, `Lab.classifications_activity` and `Lab.classifications_activity_by_day` at `snpdb/models/models.py:722-732`, are inside a triple-quoted commented-out block headed `# these methods have been superseeded by having full classification activity by lab`. The block and the module go together.
### Two that need a manual check before deletion
`annotation/models/models.py:314` `ClinVar.get_highest_oncogenicity_display` and `:358` `ClinVar.get_suspect_reason_code_display`.
Both are unreferenced by name, but there are generic display-lookup helpers that build the name at runtime — `snpdb/templatetags/model_helpers.py:75` and `library/django_utils/__init__.py:104`, both `getattr(model, f"get_{name}_display")`. ClinVar does not currently render through either (`model_helpers` is loaded by the AnnotationVersion, SequencingRun, EnrichmentKit, UploadPipeline and QC templates), and neither underlying field declares `choices`, so Django is not auto-generating them either. Low risk, but confirm rather than assume.
### Full list
classification — 21 items, 187 lines
| location | kind | name | lines |
|---|---|---|---|
| `classification/admin/condition_text_admin.py:65` | class | `ConditionTextMatchUserFilter` | 22 |
| `classification/criteria_strengths.py:195` | method | `CriteriaStrengths.report_me` | 3 |
| `classification/evidence_key_rename.py:179` | class | `EvidenceSelectKeyRenamer` | 59 |
| `classification/models/classification_grouping.py:71` | function | `classification_sort_order` | 2 |
| `classification/models/classification_json_definitions.py:4` | class | `ClassificationJsonLabDict` | 4 |
| `classification/models/classification_json_definitions.py:23` | class | `ClassificationJsonSampleDict` | 3 |
| `classification/models/classification_json_definitions.py:58` | class | `ClassificationJsonDictv3` | 13 |
| `classification/models/classification_utils.py:277` | method | `PatchMeta.remove_patch_value` | 3 |
| `classification/models/classification_utils.py:313` | class | `UserClassificationStats` | 11 |
| `classification/models/evidence_key.py:745` | method | `VCDataCell.clear_validation` | 2 |
| `classification/models/evidence_mixin.py:44` | class | `SomaticValueDict` | 3 |
| `classification/models/evidence_mixin_summary_cache.py:45` | class | `ClassificationSummaryCachedDictDate` | 3 |
| `classification/models/uploaded_file_types.py:43` | method | `FileHandle.download_to` | 7 |
| `classification/serializers.py:8` | class | `ClassificationSerializer` | 11 |
| `classification/views/classification_candidate_search_view.py:33` | function | `view_classification_candidate_search` | 7 |
| `classification/views/classification_email_view.py:53` | method | `EmailLabSummaryData._get_discordance_report_summaries` | 12 |
| `classification/views/classification_export_utils.py:232` | class | `VCFEncoding` | 3 |
| `classification/views/classification_grouping_datatables.py:254` | method | `ClassificationGroupingColumns.classification_modification_filter_to_grouping` | 7 |
| `classification/views/classification_grouping_datatables.py:289` | method | `ClassificationGroupingColumns.scv_filter` | 3 |
| `classification/views/discordance_report_views.py:257` | method | `DiscordanceReportTemplateData.resolve_label` | 2 |
| `classification/views/exports/classification_export_formatter.py:205` | method | `ClassificationExportFormatter._streaming_zip` | 7 |
annotation — 10 items, 135 lines
| location | kind | name | lines |
|---|---|---|---|
| `annotation/models/models.py:314` | method | `ClinVar.get_highest_oncogenicity_display` | 4 |
| `annotation/models/models.py:358` | method | `ClinVar.get_suspect_reason_code_display` | 2 |
| `annotation/models/models.py:380` | method | `ClinVar.short_summary` | 3 |
| `annotation/models/models.py:2550` | method | `AnnotationVersion.get_partition_names` | 6 |
| `annotation/models/models.py:2708` | method | `AnnotationVersion.get_human_protein_atlas_annotation` | 2 |
| `annotation/models/models_citations.py:588` | method | `CitationFetchRequest._fetch_now` | 4 |
| `annotation/models/models_enums.py:67` | class | `ClinGenClassification` | 8 |
| `annotation/models/models_version_diff.py:56` | method | `VersionDiff.create_version_diffs` | 92 |
| `annotation/regexes.py:50` | method | `DbRefRegex.link_for` | 3 |
| `annotation/vep_field_formatters.py:44` | function | `get_format_alphamissense_class_func` | 11 |
library — 14 items, 97 lines
| location | kind | name | lines |
|---|---|---|---|
| `library/django_utils/__init__.py:89` | function | `get_expanded_field` | 9 |
| `library/django_utils/__init__.py:169` | function | `get_redis` | 3 |
| `library/django_utils/__init__.py:174` | function | `get_lower_choice` | 3 |
| `library/django_utils/file_uploads.py:40` | function | `filepond_load_initial` | 32 |
| `library/genomics/fasta_wrapper.py:27` | method | `FastaFileWrapper.get_seq` | 2 |
| `library/keycloak.py:142` | method | `Keycloak.welcome_user` | 2 |
| `library/pandas_utils.py:43` | function | `df_handle_below_minimum_floats` | 10 |
| `library/utils/database_utils.py:116` | function | `get_queryset_column_names` | 7 |
| `library/utils/database_utils.py:146` | function | `iter_dictfetchall` | 6 |
| `library/utils/file_utils.py:132` | function | `add_permissions_to_file` | 8 |
| `library/utils/hash_utils.py:41` | function | `stable_dict_hash` | 5 |
| `library/utils/misc_utils.py:32` | class | `Struct` | 4 |
| `library/utils/text_utils.py:68` | function | `none_to_blank_string` | 2 |
| `library/utils/text_utils.py:108` | class | `VCFDialect` | 4 |
genes — 10 items, 54 lines
| location | kind | name | lines |
|---|---|---|---|
| `genes/forms.py:36` | class | `GeneForm` | 5 |
| `genes/gene_fusions.py:244` | function | `get_gene_fusion_allele` | 6 |
| `genes/hgvs/biocommons_hgvs/data_provider.py:102` | method | `DjangoTranscriptDataProvider._get_contig_interval_tree` | 2 |
| `genes/hgvs/biocommons_hgvs/data_provider.py:163` | method | `DjangoTranscriptDataProvider._get_gene` | 3 |
| `genes/hgvs/hgvs_matcher.py:295` | method | `HGVSMatcher._normalized_check` | 3 |
| `genes/hgvs/hgvs_matcher.py:717` | function | `get_hgvs_variant` | 8 |
| `genes/models_enums.py:28` | class | `HGVSKind` | 8 |
| `genes/models_enums.py:72` | method | `PanelAppConfidence.get_css_class` | 7 |
| `genes/serializers.py:64` | class | `HGNCSerializer` | 10 |
| `genes/transcript_errors.py:8` | class | `NoTranscriptVersion` | 2 |
seqauto — 5 items, 44 lines
| location | kind | name | lines |
|---|---|---|---|
| `seqauto/illumina/samplesheet.py:24` | function | `is_simplified_single_end_convention` | 5 |
| `seqauto/illumina/samplesheet.py:173` | function | `samplesheet_is_valid` | 18 |
| `seqauto/models/models_enums.py:22` | class | `SequencingFileType` | 11 |
| `seqauto/models/models_seqauto.py:1071` | function | `get_variant_caller_from_vcf_file` | 7 |
| `seqauto/views_rest.py:182` | class | `QCViewSet` | 3 |
snpdb — 8 items, 30 lines
| location | kind | name | lines |
|---|---|---|---|
| `snpdb/models/models.py:908` | method | `UserAwards.icon_for_award` | 5 |
| `snpdb/models/models_clingen_allele.py:34` | class | `ClinGenAllele.ClinGenNonChromosomeLiftoverError` | 2 |
| `snpdb/models/models_clingen_allele.py:243` | method | `ClinGenAllele.get_variant_string` | 3 |
| `snpdb/models/models_cohort.py:763` | method | `CohortGenotype.get_sample_genotypes` | 5 |
| `snpdb/models/models_enums.py:63` | class | `AnnotationLevel` | 3 |
| `snpdb/search.py:70` | method | `SearchInput.matches_pattern` | 5 |
| `snpdb/templatetags/genome_build_tags.py:23` | class | `BuildUrlDict` | 5 |
| `snpdb/variant_pk_lookup.py:49` | method | `VariantPKLookup._get_locus_hash` | 2 |
pathtests — 3 items, 14 lines
| location | kind | name | lines |
|---|---|---|---|
| `pathtests/models.py:166` | class | `RelatedGeneLists` | 5 |
| `pathtests/models_enums.py:37` | class | `ClinicalSetting` | 5 |
| `pathtests/models_enums.py:44` | class | `PathologyTestType` | 4 |
ontology — 1 items, 6 lines
| location | kind | name | lines |
|---|---|---|---|
| `ontology/ontology_builder.py:26` | method | `OperationCounter.count_op` | 6 |
email_manager — 1 items, 5 lines
| location | kind | name | lines |
|---|---|---|---|
| `email_manager/views/email_manager_views.py:51` | method | `EmailColumns.recipient_renderer` | 5 |
variantgrid — 2 items, 4 lines
| location | kind | name | lines |
|---|---|---|---|
| `variantgrid/perm_path.py:24` | function | `_view_to_string` | 2 |
| `variantgrid/perm_path.py:43` | function | `re_perm_path` | 2 |
beacon — 1 items, 2 lines
| location | kind | name | lines |
|---|---|---|---|
| `beacon/schema.py:140` | function | `supported_granularities` | 2 |
review — 1 items, 2 lines
| location | kind | name | lines |
|---|---|---|---|
| `review/widgets/multi_lab_selector.py:8` | class | `MultiChoiceLabWidget` | 2 |
## 3. Dead templates — 5
Nothing references these: no view, no `{% include %}`, no `{% extends %}`, no string anywhere.
| template | last functional commit |
|---|---|
| `classification/templates/classification/view_classification_versions.html` | 2020-10-01 |
| `classification/templates/classification/tags/classification_links.html` | 2020-10-01 |
| `variantgrid/templates/default_templates/external_base_content_as_submenu_page_content.html` | 2020-09-30 |
| `flags/templates/flags/debug.html` | 2020-09-30 — body is literally `Hello` |
| `uicore/templates/uicore/ajax/ajax_form.html` | 2023-08-31 — `LazyRender` takes `template_name` per call site, and its one caller (`classification/views/discordance_report_triage_view.py:59`) passes a different template |
Deliberately **not** listed, because they resolve dynamically and are live: `analysis/node_editors/*_editor.html` (built in `analysis/views/nodes/node_view.py:82`), `snpdb/menu/menu_*_base.html` (listed in `snpdb/processors.py:76-85`), and the `registration/` templates that django-registration-redux renders by convention.
## 4. Dead static assets — 49 files
* **47 vendored highlight.js themes** — `variantgrid/static_files/default_static/js/lib/highlight/styles/*.css`, roughly 5,000 lines. Only `default.css` is used, by `analysis/templates/analysis/node_editors/grid_editor_debug_tab.html:4`.
* `variantgrid/static_files/default_static/error_pages/error_page.js` — defines `getContactEmailMailto()`, which appears nowhere else in the repo. Note it hardcodes an individual's email address in obfuscated form.
* `variantgrid/static_files/default_static/js/lib/loaders/double-helix.loader.js` — the only loader in that directory not pulled in by `loading_animations.html` / `analysis_includes.html`.
## 5. Smaller items
**Two one-off management commands are permanently disabled.** Both open `handle()` with a raise, leaving the whole body unreachable — these were the only two genuine hits from vulture at 100% confidence:
* `classification/management/commands/classification_set_legacy_allele_origin.py:45` — `raise ValueError("Don't run this - haven't confirmed it's the right thing to do yet. See SACGF/variantgrid_private#2926")`
* `snpdb/management/commands/fix_locus_ref_n.py:36` — `raise ValueError("Don't run this - I think it's better to just re-import variants")`
Intentional guard rails rather than accidents, so they may be worth keeping — but if the decisions behind them have since been made, the commands can go.
**Nine redundant direct pins in `requirements.in`.** `pillow`, `pyyaml`, `boto3`, `networkx`, `inflection`, `reportlab`, `psycopg2`, `ipython`, `cryptography` are all pulled in transitively (`easy-thumbnails`, `drf-spectacular`, `django-storages`/`celery`, `pronto`, `matplotlib`, `hgvs`, `pandas`, `PyJWT`…). Harmless, and the `cryptography>=50.0.0` floor looks like a deliberate security pin worth keeping explicit. Flagged for completeness only. `ped_parser` is a genuine dependency despite never being imported — it is invoked as an external CLI, checked at `variantgrid/deployment_validation/tool_version_checks.py:56`.
**Nothing else is dormant at the app level.** Every app was checked for last non-sweep commit. `vcauth` (35 lines, last touched 2023-11) is live — in `INSTALLED_APPS`, registers the custom User admin. `expression` is an empty shell but `default_settings.py:974` already documents why: `'expression', # Empty but referenced by other migrations - can delete after squash`. `mme`, `beacon`, `paper`, `pathtests`, `review`, `user_messages` and `email_manager` all have real 2026 work.
## 6. Two subsystems that need a human decision
These are the "lots hanging off it, nothing touched in years" cases. Both are too entangled to delete on a scanner's say-so.
### Annotation VersionDiff
**Surface:** `annotation/models/models_version_diff.py` (211 lines, 3 models + migrations), `annotation/templates/annotation/version_diffs.html`, `annotation/templates/annotation/view_version_diff.html`, `annotation/templatetags/version_diff_graph_tags.py`, `snpdb/templatetags/model_fields_version_diff_tags.py`, views and routes in `annotation/`.
**Evidence it is dormant:** last functional change 2021-02-03 ("remove EnsemblGeneAnnotation models"); everything since is lint/sweep. `version_diffs` is `False` in `default_settings.py:1199` *and* explicitly `False` in `shariantcommon.py:229`, so it is menu-hidden in every environment in this repo. Its main entry point, the 92-line `create_version_diffs`, is the single largest dead symbol in the sweep.
**Question:** remove the whole feature (including a migration to drop the tables), drop only the dead `create_version_diffs` and leave the rest wired in case a private deployment enables it, or leave it alone entirely?
### Mutational Signatures
**Surface:** 4 models in `analysis/models/mutational_signatures.py`, `analysis/tasks/mutational_signatures_task.py`, `view_mutational_signature` view + template + route in `analysis/`, 6 `MUTATIONAL_SIGNATURE_*` settings constants, a conditional column in `snpdb/grids.py:155`, and an upload-pipeline step in `upload/tasks/vcf/genotype_vcf_tasks.py:91` that fires for every `VariantsType.SOMATIC_ONLY` sample.
**Evidence it is dormant:** last functional work 2020-12-11 ("Get mutational signatures working again"); the only commit since is a 2024-02 field rename that came in with `#974 - remove Sequence.length`.
**Unlike VersionDiff, this one is still wired into VCF import**, so it is not menu-hidden — it runs. Worth checking production for `MutationalSignature` rows and recent pipeline steps before deciding.
**Question:** is anyone using this? If not, removing it touches the import pipeline, so it wants its own branch and a full import test rather than being folded into a cleanup sweep.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with snpdb/admin_partition_archive.py and snpdb/admin.py to verify whether the PartitionArchive admin module should be imported or removed. Then review the listed symbol locations and the two generic display helpers before changing any reported code. Done means each candidate has been manually confirmed, the dormant subsystems have an explicit decision, and the resulting cleanup preserves required behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 32/100