Skip to content

Complete HGVS object handling cleanup in mappers.py and hgvs_utils.py - #870

Open
Peter-J-Freeman wants to merge 22 commits into
developfrom
regex_removal
Open

Complete HGVS object handling cleanup in mappers.py and hgvs_utils.py#870
Peter-J-Freeman wants to merge 22 commits into
developfrom
regex_removal

Conversation

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator
  • replace regex and string-based intronic position checks with HGVS position helpers
  • use structured HGVS edit types instead of formatted-string inspection
  • reduce unnecessary HGVS object stringification and reparsing
  • improve direct HGVS object construction and preserve position metadata
  • fix duplicated start-offset checks to correctly inspect start and end positions
  • support structured intronic checks across CDS and UTR coordinates

- replace regex and string-based intronic position checks with HGVS
position helpers
- use structured HGVS edit types instead of formatted-string inspection
- reduce unnecessary HGVS object stringification and reparsing
- improve direct HGVS object construction and preserve position metadata
- fix duplicated start-offset checks to correctly inspect start and end
positions
- support structured intronic checks across CDS and UTR coordinates
@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

@John-F-Wagstaff This is not to be merged yet, it is just to get a review of the structure and the 2 filed completed so far. I cannot say they are totally regex and string object free, but I think they should be close

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.99764% with 385 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.84%. Comparing base (e2036de) to head (18251b0).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
VariantFormatter/variantformatter.py 81.79% 79 Missing ⚠️
VariantValidator/modules/vvMixinCore.py 73.78% 70 Missing ⚠️
VariantValidator/modules/hgvs_utils.py 85.82% 37 Missing ⚠️
VariantFormatter/gapGenes.py 78.66% 32 Missing ⚠️
VariantFormatter/formatter.py 85.57% 29 Missing ⚠️
VariantValidator/modules/complex_descriptions.py 89.75% 25 Missing ⚠️
VariantValidator/modules/liftover.py 87.86% 25 Missing ⚠️
VariantValidator/modules/vvDatabase.py 87.01% 20 Missing ⚠️
VariantFormatter/simpleVariantFormatter.py 88.51% 17 Missing ⚠️
VariantValidator/modules/valoutput.py 88.99% 12 Missing ⚠️
... and 11 more
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #870      +/-   ##
===========================================
+ Coverage    82.51%   84.84%   +2.32%     
===========================================
  Files           42       48       +6     
  Lines        13913    14554     +641     
===========================================
+ Hits         11481    12348     +867     
+ Misses        2432     2206     -226     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- format_converters.py
  - remove unnecessary regex and replace with direct string operations
  - use HGVS position helpers for intronic and offset checks
  - simplify VCF conversion and reference handling
  - simplify LRG genomic, transcript and protein mapping
  - reduce repeated parsing, stringification and object access
  - remove dead, duplicated and redundant branches
  - optimise allele, repeat, mitochondrial and protein handling

- hgvs_utils.py
  - improve direct HGVS object construction
  - preserve structured HGVS position metadata
  - reduce unnecessary string conversion and reparsing

- mappers.py
  - replace string-based position inspection with HGVS position helpers
  - use structured HGVS edit and position attributes
  - reduce unnecessary HGVS serialization

- rna_formatter.py
  - retain HGVS objects through RNA processing
  - defer string conversion to output boundaries
  - remove unnecessary string and regex processing

- utils.py
  - replace unnecessary regex with direct string operations
  - simplify user input processing
  - reduce temporary string creation

- tests/test_utils.py
  - update regression tests for refactored utility handling

- tests/test_vvMixinConverters.py
  - update regression tests for optimised converter paths

- retain regex where genuine pattern matching remains appropriate
- preserve existing validation behaviour while reducing processing overhead
)


def position_is_intronic(hgvs_object, check_start=False, check_end=False, check_start_and_end=False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we are going to have to specify what we want in the args anyway then, given both the readability issues and the fact that each usage point will only ever use the one version, it would probably be better to have a set of functions, something like:
pos_is_intronic # (or pos_any_intronic for any intronic end
pos_fully_intronic # for both
pos_start_intronic # for just start
and
pos_end_intronic # for just end

each of these would be close to one line functions but this would improve the readability of the usage sites and even remove at least 1 if/else check as well.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, split all into sub functions? even the UTR ones. I will do this now while I am refactoring a couple more scripts if I understand correctly

1 finction = 1 posiiton.

I think we lose both. Prob not needed

@John-F-Wagstaff John-F-Wagstaff Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably still want the both and either options, because it simplifies the if statements.

Overly complex if statements are one of the most annoying things we have to put up with from a readability perspective. Having the both/either version also means that we only have to run is_objet once rather than twice when we do use them, which is a small but repeatable performance gain.

@John-F-Wagstaff John-F-Wagstaff Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the UTR functions I am not so certain, we do use this less. Also one of the two, start/end is functionally equivalent to checking for fully 3'/5/ UTR depending on which UTR end we are using. It may be worth checking the logic in the call sites and considering a fully UTR/partially UTR/any UTR versions of these functions, as opposed to the current start and end versions.

We may also want UTR non end specific helper functions, if the question is "does this affect the CDS" then this should translate to "is this fully UTR", regardless of the end.

Unfortunately I am less certain here. I have paid less attention to the UTR detection code than the intronic/exonic code in the past, due to the alignment issues I have ended up looking at being focused on the intron-exon boundaries.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be overkill

for now I am going to add

def is_object(hgvs_object):
    """
    Check that the supplied value is a parsed HGVS object.

    Parameters
    ----------
    hgvs_object
        Object expected to be a parsed HGVS SequenceVariant.

    Raises
    ------
    HgvsPositionException
        If the supplied value does not have an HGVS accession attribute.
    """
    try:
        hgvs_object.ac
    except AttributeError:
        raise HgvsPositionException(
            f"Variant {hgvs_object} is not a parsed hgvs object"
        )


def start_position_is_intronic(hgvs_object):
    """
    Return whether the start position is intronic.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.start.offset != 0


def end_position_is_intronic(hgvs_object):
    """
    Return whether the end position is intronic.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.end.offset != 0


def both_positions_are_intronic(hgvs_object):
    """
    Return whether both the start and end positions are intronic.
    """
    return (
        start_position_is_intronic(hgvs_object)
        and end_position_is_intronic(hgvs_object)
    )


def either_position_is_intronic(hgvs_object):
    """
    Return whether either the start or end position is intronic.
    """
    return (
        start_position_is_intronic(hgvs_object)
        or end_position_is_intronic(hgvs_object)
    )


def start_offset_is_positive(hgvs_object):
    """
    Return whether the start position has a positive intronic offset.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.start.offset > 0


def end_offset_is_positive(hgvs_object):
    """
    Return whether the end position has a positive intronic offset.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.end.offset > 0


def both_offsets_are_positive(hgvs_object):
    """
    Return whether both start and end offsets are positive.
    """
    return (
        start_offset_is_positive(hgvs_object)
        and end_offset_is_positive(hgvs_object)
    )


def either_offset_is_positive(hgvs_object):
    """
    Return whether either the start or end offset is positive.
    """
    return (
        start_offset_is_positive(hgvs_object)
        or end_offset_is_positive(hgvs_object)
    )


def start_offset_is_negative(hgvs_object):
    """
    Return whether the start position has a negative intronic offset.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.start.offset < 0


def end_offset_is_negative(hgvs_object):
    """
    Return whether the end position has a negative intronic offset.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.end.offset < 0


def both_offsets_are_negative(hgvs_object):
    """
    Return whether both start and end offsets are negative.
    """
    return (
        start_offset_is_negative(hgvs_object)
        and end_offset_is_negative(hgvs_object)
    )


def either_offset_is_negative(hgvs_object):
    """
    Return whether either the start or end offset is negative.
    """
    return (
        start_offset_is_negative(hgvs_object)
        or end_offset_is_negative(hgvs_object)
    )


def start_is_5_prime_utr(hgvs_object):
    """
    Return whether the start position is in the 5-prime UTR.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.start.base < 0


def end_is_5_prime_utr(hgvs_object):
    """
    Return whether the end position is in the 5-prime UTR.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.end.base < 0


def both_positions_are_5_prime_utr(hgvs_object):
    """
    Return whether both positions are in the 5-prime UTR.
    """
    return (
        start_is_5_prime_utr(hgvs_object)
        and end_is_5_prime_utr(hgvs_object)
    )


def either_position_is_5_prime_utr(hgvs_object):
    """
    Return whether either position is in the 5-prime UTR.
    """
    return (
        start_is_5_prime_utr(hgvs_object)
        or end_is_5_prime_utr(hgvs_object)
    )


def start_is_3_prime_utr(hgvs_object):
    """
    Return whether the start position is in the 3-prime UTR.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.start.datum == Datum.CDS_END


def end_is_3_prime_utr(hgvs_object):
    """
    Return whether the end position is in the 3-prime UTR.
    """
    is_object(hgvs_object)
    return hgvs_object.posedit.pos.end.datum == Datum.CDS_END


def both_positions_are_3_prime_utr(hgvs_object):
    """
    Return whether both positions are in the 3-prime UTR.
    """
    return (
        start_is_3_prime_utr(hgvs_object)
        and end_is_3_prime_utr(hgvs_object)
    )


def either_position_is_3_prime_utr(hgvs_object):
    """
    Return whether either position is in the 3-prime UTR.
    """
    return (
        start_is_3_prime_utr(hgvs_object)
        or end_is_3_prime_utr(hgvs_object)
    )

plug it in and see if we can replace all instances with them. If we need others, will add. Start small, work out

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks OK but you are still using longer names than needed, since these will all almost only be used in if statements we want shorter if we can avoid harming the self-documenting nature of the names. For example is could be removed from most of them, 3p or 5p probably works as well as 3_prime.

Also the both for the UTR is overkill, unless the start position ends up after the end position (which causes validation errors) both ends will always be inside the UTR if the innermost end of the span is in the UTR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so for 5' if the end is inside the 5' UTR the start will always be inside too, and for 3' UTR if the start is inside then the end will always be too.

import copy
from . import seq_data
from . import utils
from . import hgvs_position_utils

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For brevity in the call sites, so we don't get excess multi line if statements, you may actually want to pull in the individual functions instead, that halves the line length for simpler calls.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what I will do for the tewaking is get it all plugged in, remove what se dont use and refactor. The descriptive names help me think, but can shoten them as a lasr commit, as well as doing the specific imports, Otherwise brain may explode :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I would find it harder that way round, but that depends on thinking styles, the output is the important concern.

Comment thread VariantValidator/modules/hgvs_utils.py Outdated

# Recover sequences
vcf_del_seq = sf.fetch_seq(str(reverse_normalized_hgvs_genomic.ac), adj_start, end)
vcf_del_seq = sf.fetch_seq(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is inv normally without a functioning ref here? (I know that it lacks a defined alt deliberately)
fetch_seq
is, annoyingly, one of the slowest functions that we use, if you can use ref for the del seq and reverse compliment on the ref instead for the ins seq that will be quicker in most if not all cases. You repeat this bellow too.

Comment thread VariantValidator/modules/hgvs_utils.py Outdated
my_seq = Seq(vcf_del_seq)
# alt = bs + str(my_seq.reverse_complement())
alt = str(my_seq.reverse_complement())
my_seq = Seq(vcf_del_seq)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BioPython Seq objects are quite heavy to make, and we do have internal reverse compliment functions already defined. Would that work better here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we do, a light revcomp function in utils. Done this now

else:
if str(ins_seq) == 'None':
ins_seq = ''

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs considering as above with the using ref, vs extra seqrepo seq fetch comment.

- Apply the **object-lifecycle mantra** throughout the affected validation paths:
  - Retain parsed HGVS `SequenceVariant` objects for as long as possible.
  - Inspect HGVS positions, offsets and UTR state directly from objects.
  - Avoid unnecessary object-to-string conversions and regex inspection.
  - Defer string conversion to genuine parsing, warning, logging and output boundaries.
  - Avoid reparsing where an existing HGVS object can be retained or constructed directly.
  - Use object/type guards so string-specific parsing and conversion paths only run while
  the input remains string syntax.
  - Skip irrelevant parsing stages once an HGVS `SequenceVariant` has been produced,
  reducing unnecessary parsing, regex and string-processing overhead.

### `complex_descriptions.py`
- Replace manual intronic position checks with HGVS position helpers.
- Retain string processing only where unsupported/fuzzy input syntax requires it.
- Correct end-interval logging.

### `expanded_repeats.py`
- Retain expanded-repeat variants as HGVS objects through conversion and normalization.
- Avoid unnecessary conversion back to HGVS strings.
- Simplify object-based interval handling.

### `format_converters.py`
- Preserve HGVS objects through formatting and conversion stages.
- Retain normalized delins variants as objects rather than stringifying them.
- Use position helpers for intronic offset and interval checks.
- Keep mitochondrial validation and normalization on the corrected HGVS object.
- Defer string conversion to warnings and formatted output.
- Guard allele-syntax parsing so it only runs for string input; parsed HGVS objects bypass
 the irrelevant allele parser entirely.
- Use parser guards both to preserve the HGVS object lifecycle and to avoid unnecessary
downstream processing.

### `hgvs_position_utils.py`
- Expand the HGVS position helper API.
- Add explicit start/end intronic position helpers.
- Add positive and negative offset helpers.
- Add combined both/either/only-one position predicates.
- Add 5-prime and 3-prime UTR inspection helpers.
- Add 3-prime UTR datum modification helpers.
- Group and document helpers by position semantics.

### `hgvs_utils.py`
- Replace string-based HGVS position inspection with object-level helpers.
- Preserve HGVS objects through mapping and normalization paths.
- Use combined intronic position predicates where appropriate.

### `initial_formatting.py`
- Operate directly on parsed HGVS accessions when removing redundant gene symbols.
- Preserve the `SequenceVariant` object during initial formatting.
- Restrict regex inspection to the original submitted description where syntax information
 is required.

### `mappers.py`
- Replace string/regex-based intronic position inspection with HGVS object helpers.
- Use explicit start/end offset helpers for boundary correction.
- Compare HGVS position objects directly where possible.
- Represent 3-prime UTR coordinates using HGVS `CDS_END` datum semantics rather than
string-based `*` coordinates.

### `vvMixinConverters.py`
- Retain HGVS objects through RNA processing and translation.
- Defer RNA string formatting until the final RNA-output boundary.
- Avoid unnecessary HGVS object stringification during intermediate processing.

### TODO: Review guarded string-processing paths

- `initial_format_conversions()` now uses `isinstance(variant.quibble, str)` guards to
prevent string-specific processing from running after an earlier stage has produced an
HGVS `SequenceVariant`.
- These guards preserve the HGVS object lifecycle and provide an immediate optimisation by
 skipping parsing, regex and string-processing stages that are no longer relevant.
- The guards are intentionally being used as safe boundaries in this refactor rather than
expanding the scope further.
- Review the following guarded paths individually in the next cleanup pass to determine
whether they can be refactored to work directly with HGVS objects, narrowed to genuine
raw-input handling, or bypassed entirely once an object exists:
  - `allele_parser()`
  - unsupported conversion (`con`) syntax checking
  - `use_checking.pre_parsing_global_common_mistakes()`
  - `methyl_syntax.methyl_syntax()`
  - `uncertain_pos()`
  - `convert_expanded_repeat()`
  - `indel_catching()`
  - `final_hgvs_convert()`
  - `use_checking.refseq_common_mistakes()`
- Preserve string handling where the function genuinely parses syntax that cannot yet be
represented by a standard HGVS `SequenceVariant`, such as compound allele syntax.
- The aim of the follow-up is not to remove all string handling, but to make the
string-to-object boundary explicit and ensure each processing stage runs only when
relevant.
This completes the cleaning and optimisation of the initial variant
formatting flow, reducing unnecessary string conversion and regex
processing while retaining parsed HGVS objects through validation and
mapping wherever possible (i.e. retaining objects once created
throughout the initial validation lifecycle, rather than the old
approach of stringing and re-parsing.

- `format_converters.py`
  - Cleaned alternate genomic-context handling for `NW_`, `NT_`, and
    `NG_` transcript variants.
  - Added explicit validation of intronic positions against the submitted
    alternate alignment before attempting primary-assembly mapping.
  - Added primary-assembly exon-structure comparison and fallback between
    GRCh37 and GRCh38 where the requested assembly does not contain the
    corresponding intron.
  - Preserved alignment orientation when processing variants that cannot
    be represented on a primary chromosome.
  - Updated compound variant routing to ensure correct validation of both
    reference bases and exon boundaries before downstream formatting.
  - Improved handling of alternate-context variants that remain valid only
    on the submitted genomic reference.

- `hgvs_utils.py`
  - Reduced unnecessary HGVS object stringification and regex-based
    inspection.
  - Moved additional HGVS operations to direct object and property
    inspection.

- `mappers.py`
  - Replaced position string parsing with HGVS position helper functions.
  - Cleaned intronic offset and transcript-boundary handling.
  - Corrected object-based inspection of HGVS positions and uncertainty.

- `methyl_syntax.py`
  - Reduced regex and string processing in initial methylation syntax
    handling.

- `use_checking.py`
  - Cleaned and simplified initial input and structure validation.
  - Updated `c.` and `n.` structure checks to use parsed HGVS position
    objects and shared position helpers.
  - Added alternate genomic-context mapping through `vm` where an
    explicit `NW_`, `NT_`, or `NG_` context is supplied.
  - Improved validation of intronic exon boundaries and uncertain
    reference sequence.
  - Retained validation of `N` reference bases where the underlying
    reference sequence is uncertain.
  - Simplified pre-parsing checks while preserving correction and warning
    behaviour.

- `utils.py`
  - Reduced regex use and unnecessary string manipulation in early
    user-input processing.

- `variant.py`
  - Cleaned initial variant handling to retain HGVS objects and avoid
    unnecessary parse/string/reparse cycles.

- `vvMixinConverters.py`
  - Gated initial conversion functions so they are only entered for
    variant types requiring those conversions.
  - Reduced unnecessary conversion and formatting work during the initial
    validation flow.

- `vvMixinCore.py`
  - Cleaned initial validation and formatting routing.
  - Gated conversion paths to prevent inappropriate or redundant
    conversion attempts.
  - Updated compound variant routing so reference-base validation and
    exon-boundary validation occur correctly before subsequent mapping
    and formatting.

Together these changes complete the current cleanup of the initial
formatting pathway and provide a more object-oriented validation flow
with fewer regexes, fewer unnecessary HGVS string conversions, and more
explicit handling of alternate genomic alignments and intronic exon
boundaries.
Add additional unit and functional tests to cover the files cleaned
during the VV structure checking and formatting checks.

Also, fixes tests which lacked the test_ prefix so were not running.
- Clean and optimise exon_numbering.py
  - Simplify control flow and repeated operations
  - Remove unnecessary processing while preserving behaviour

- Clean and optimise variant.py
  - Remove redundant operations and unnecessary string handling
  - Preserve Variant object behaviour and output contracts

- Clean and optimise vcf_to_pvcf.py
  - Simplify VCF parsing and conversion
  - Reduce unnecessary regex and string processing
  - Preserve existing validation behaviour

- Clean and optimise vvMixinConverters.py
  - Remove unnecessary HGVS object stringification
  - Use hgvs_position_utils helpers for position inspection
  - Consolidate reverse complement handling through utils
  - Optimise transcript filtering and relevant transcript handling
  - Simplify repeated dictionary and list operations
  - Reduce unnecessary sequence fetches
  - Clean noreplace_myevm_t_to_g mapping fallback logic
  - Clean and optimise myevm_t_to_g while preserving gap handling
  - Preserve normalisation and reference replacement behaviour
  - Preserve existing HGVS mapping behaviour

- Clean and optimise valoutput.py
  - Simplify output construction and repeated lookups
  - Preserve dictionary, JSON and table output behaviour
  - Preserve the table schema required by the web interface
  - Preserve existing empty-value and VCF ID semantics

- Update vvMixinConverters tests for cleaned mapping behaviour

- Full functional suite passes
  - 2085 passed
  - 6 skipped
  - Runtime reduced from approximately 10 minutes to 8:02
  - Statement coverage approximately 84.27%
  - 11,536 of 13,689 statements covered

Completed optimisation and cleanup modules to date:

- complex_descriptions.py
- exon_numbering.py
- expanded_repeats.py
- format_converters.py
- hgvs_position_utils.py
- hgvs_utils.py
- initial_formatting.py
- mappers.py
- methyl_syntax.py
- rna_formatter.py
- use_checking.py
- utils.py
- valoutput.py
- variant.py
- vcf_to_pvcf.py
- vvMixinConverters.py

TODO:

- Clean and optimise liftover.py
- Preserve the fast transcript-mediated liftover path
- Prefer transcripts with ungapped source and target alignments
- Prefer the least-gapped common transcript when none are perfect
- Select the best transcript for each source/target reference pair
- Support appropriate NC_, NT_ and NW_ reference combinations
- Keep alignment gap assessment in TranscriptMapData
- Profile the slow intergenic PyLiftover fallback
- Identify the source of intergenic liftover performance costs
- Investigate faster local alternatives to PyLiftover
- Preserve correctness and portability of intergenic liftover
- Remove unnecessary strings, regexes and temporary collections
- Simplify control flow without changing established behaviour
- Clean and optimise liftover.py
  - Apply established VariantValidator cleanup rules
  - Remove unnecessary string handling and repeated operations
  - Use exact comparisons and startswith checks where appropriate
  - Simplify control flow while preserving existing behaviour
  - Preserve the fast transcript-mediated liftover path
  - Preserve existing intergenic liftover behaviour

- Further clean hgvs_utils.py
  - Remove remaining unnecessary string and regex operations
  - Preserve existing HGVS conversion and validation behaviour

- Clean and optimise vvMixinInit.py
  - Remove unnecessary repeated operations and string handling
  - Simplify initialisation and validation paths
  - Preserve Validator initialisation and public behaviour

- Clean and optimise vvDatabase.py
  - Simplify database handling and repeated operations
  - Preserve existing database update and access behaviour

- Clean vvDBInit.py
  - Simplify database pool initialisation and connections
  - Preserve MySQL Connector and MariaDB fallback support
  - Preserve connection health checks and retry behaviour

- Clean vvDBInsert.py
  - Simplify insert and update query construction
  - Remove unnecessary temporary variables
  - Preserve database queries, return values and API behaviour

- Clean and optimise vvDBGet.py
  - Simplify database GET query construction and control flow
  - Preserve existing getter APIs and return values
  - Profile repeated GET usage across the full test suite
  - Identify repeated database lookups suitable for caching
  - Cache only getters demonstrated to benefit from reuse
  - Avoid indiscriminate caching of database operations

- Add configurable database GET caching
  - Add process-level caching for repeated database lookups
  - Share cached values between Validator instances in a process
  - Add bounded least-recently-used cache behaviour
  - Roll off least-recently-used entries above the size limit
  - Prevent unbounded cache growth on long-running servers
  - Set the default cache limit to 20,000 entries
  - Add vvDB_GET_CACHE to enable or disable the cache
  - Add vvDB_GET_CACHE_SIZE to configure the cache size
  - Allow VV_DB_GET_CACHE to override the cache setting
  - Allow VV_DB_GET_CACHE_SIZE to override the cache size
  - Retain explicit cache clearing for invalidation

- Expand database and settings tests
  - Test database GET success, empty results and retries
  - Test cached getter reuse
  - Test cache enable and disable behaviour
  - Test explicit cache clearing
  - Test configured cache size limits
  - Test least-recently-used eviction and refresh
  - Test database access with caching disabled
  - Test database insert behaviour after cleanup
  - Test database cache settings and environment parsing

- Preserve the VariantValidator cleanup principles
  - Avoid unnecessary HGVS object stringification
  - Prefer HGVS properties over parsing string representations
  - Remove unnecessary regex use
  - Use exact equality where an exact value is required
  - Use startswith and endswith for prefix and suffix checks
  - Avoid repeated calculations, lookups and collections
  - Simplify control flow without changing behaviour
  - Avoid unnecessary optimisation of rarely executed paths
  - Preserve public APIs, warnings and validation behaviour
  - Optimise demonstrated hot paths rather than speculating

Completed optimisation and cleanup modules to date:

- complex_descriptions.py
- exon_numbering.py
- expanded_repeats.py
- format_converters.py
- hgvs_position_utils.py
- hgvs_utils.py
- initial_formatting.py
- liftover.py
- mappers.py
- methyl_syntax.py
- rna_formatter.py
- use_checking.py
- utils.py
- valoutput.py
- variant.py
- vcf_to_pvcf.py
- vvDatabase.py
- vvDBGet.py
- vvDBInit.py
- vvDBInsert.py
- vvMixinConverters.py
- vvMixinInit.py

Validation:

- Full functional suite remains green during repeated test runs
- Database caching reduces repeated database access
- Cache performance benchmarked over repeated full-suite runs
- Clean and optimise gapped_mapping.py
  - Apply established VariantValidator cleanup rules throughout the module
  - Remove unnecessary HGVS object stringification
  - Prefer HGVS object properties and position helpers over string parsing
  - Reuse hgvs_position_utils helpers for intronic and offset handling
  - Remove unnecessary regex and repeated operations
  - Simplify repeated conditional and mapping logic
  - Extract reusable helpers from large gap-mapping functions
  - Consolidate repeated transcript and genomic mapping operations
  - Clean transcript disparity handling while preserving running-option behaviour
  - Clean transcript gap detection and gap warning generation
  - Clean transcript position and reference reconstruction helpers
  - Simplify duplication and insertion 5-prime shift handling
  - Preserve NM_ and NR_ transcript-specific behaviour
  - Preserve documented alignment-gap recovery branches
  - Remove genuinely dead and obsolete code
  - Remove development diagnostic logging from cleaned paths
  - Preserve existing gap correction, warning and mapping behaviour

- Clean and optimise lovd_api.py
  - Apply established cleanup rules
  - Simplify repeated operations and control flow
  - Preserve LOVD API behaviour

- Clean and optimise seq_data.py
  - Remove unnecessary repeated sequence handling
  - Simplify sequence data access paths
  - Preserve existing sequence lookup behaviour

- Clean and optimise seq_state_to_expanded_repeat.py
  - Simplify expanded-repeat sequence-state conversion
  - Remove unnecessary repeated operations
  - Preserve existing conversion behaviour
  - Expand regression coverage for cleaned paths

- Clean and optimise transcript_map_data.py
  - Simplify transcript mapping data access
  - Remove unnecessary repeated operations
  - Preserve transcript/genomic alignment selection behaviour

- Expand regression coverage
  - Preserve existing gapped-alignment regression behaviour
  - Exercise known genomic/transcript disparity cases with real HGVS variants
  - Preserve insertion, deletion, duplication and substitution handling around gaps
  - Preserve forward and reverse alignment behaviour
  - Update sequence-state expanded-repeat tests
  - Retain database and settings regression coverage

- Preserve the VariantValidator cleanup principles
  - Avoid unnecessary HGVS object stringification
  - Prefer HGVS properties over parsing string representations
  - Use shared HGVS position helpers for coordinate and offset inspection
  - Remove unnecessary regex use
  - Use exact equality where an exact value is required
  - Use startswith and endswith for prefix and suffix checks
  - Avoid repeated calculations, lookups and collections
  - Extract helpers where they remove substantial duplicated logic
  - Simplify control flow without changing behaviour
  - Preserve intentional defensive and documented gap-handling branches
  - Remove only genuinely dead code
  - Preserve public APIs, warnings and validation behaviour
  - Optimise demonstrated hot paths rather than speculating

Completed optimisation and cleanup modules to date:

- complex_descriptions.py
- exon_numbering.py
- expanded_repeats.py
- format_converters.py
- gapped_mapping.py
- hgvs_position_utils.py
- hgvs_utils.py
- initial_formatting.py
- liftover.py
- lovd_api.py
- mappers.py
- methyl_syntax.py
- rna_formatter.py
- seq_data.py
- seq_state_to_expanded_repeat.py
- transcript_map_data.py
- use_checking.py
- utils.py
- valoutput.py
- variant.py
- vcf_to_pvcf.py
- vvDatabase.py
- vvDBGet.py
- vvDBInit.py
- vvDBInsert.py
- vvMixinConverters.py
- vvMixinInit.py

Validation:

- Full functional suite remains green after cleanup
- Existing gapped-mapping regression tests remain green
- Known gap-associated HGVS corrections retain expected output
- Sequence-state expanded-repeat tests remain green
- Database and settings tests remain green
- Clean and optimise vvMixinCore.py
  - Apply established VariantValidator cleanup rules to core
  orchestration paths
  - Simplify control flow and remove unnecessary intermediate operations
  - Remove dead and obsolete code where no longer used
  - Preserve Validator public behaviour and validation orchestration
  - Clean transcript information retrieval and database update handling
  - Consolidate repeated transcript metadata handling
  - Simplify RefSeq and Ensembl transcript information paths
  - Preserve existing transcript database recovery and warning behaviour
  - Clean update_transcript_record wrapper

- Clean and optimise gene2transcripts.py
  - Apply established VariantValidator cleanup rules throughout the module
  - Simplify query handling and transcript selection
  - Remove unnecessary repeated operations and legacy control flow
  - Remove dead batch-handling behaviour from the Validator wrapper
  - Preserve supported gene symbol, HGNC and transcript queries
  - Preserve RefSeq and Ensembl transcript handling
  - Preserve genomic span and alignment reporting behaviour
  - Preserve LOVD syntax-check integration
  - Update regression expectations for cleaned invalid-query handling

- Expand and clean hgvs2ref
  - Restrict reference retrieval to supported g., c. and n. HGVS
  descriptions
  - Return explicit errors for unsupported p., r. and m. descriptions
  - Preserve HGVS parser errors for malformed descriptions
  - Retrieve genomic reference sequence directly for g. descriptions
  - Convert coding descriptions to transcript coordinates for sequence
  retrieval
  - Support non-coding transcript reference sequence retrieval
  - Add explicit handling for intronic transcript positions
  - Support transcript variants with explicit genomic context
  - Support NC_(NM_) and NC_(NR_) descriptions
  - Map context-qualified intronic transcript variants to genomic
  coordinates
  - Return informative errors when exon structure cannot be established
  - Return explicit exon-boundary errors for invalid intronic coordinates
  - Preserve underlying sequence retrieval errors where appropriate

- Extend format_converters.py
  - Add reusable alignment/exon structure handling required by hgvs2ref
  - Reuse existing transcript/genomic alignment logic rather than
  duplicating it
  - Use HGVS objects and shared position helpers instead of string
  parsing
  - Preserve existing format conversion behaviour

- Expand hgvs2ref regression coverage
  - Add dedicated tests/test_hgvs2reference.py
  - Test genomic single-base and interval sequence retrieval
  - Test coding and non-coding transcript sequence retrieval
  - Test malformed HGVS descriptions
  - Test invalid reference accessions
  - Test unsupported protein, RNA and mitochondrial descriptions
  - Test transcript-only intronic descriptions
  - Cover reference retrieval and error-return contracts
  - Remove obsolete expectations for previously accepted unsupported
  HGVS types

- Update gene2transcripts regression coverage
  - Update invalid transcript-like query expectations
  - Preserve existing gene and transcript query coverage

- Preserve the VariantValidator cleanup principles
  - Avoid unnecessary HGVS object stringification
  - Prefer HGVS properties over parsing string representations
  - Use shared HGVS position helpers for coordinate and offset inspection
  - Remove unnecessary regex use
  - Use exact equality where an exact value is required
  - Use startswith and endswith for prefix and suffix checks
  - Avoid repeated calculations, lookups and collections
  - Extract reusable helpers instead of duplicating mapping logic
  - Simplify orchestration without moving domain logic into entry points
  - Remove genuinely dead and obsolete code
  - Preserve public APIs where still supported
  - Prefer explicit errors over silently supporting invalid or undefined
  behaviour
  - Optimise demonstrated hot paths rather than speculating

Completed optimisation and cleanup modules to date:

- complex_descriptions.py
- exon_numbering.py
- expanded_repeats.py
- format_converters.py
- gapped_mapping.py
- gene2transcripts.py
- hgvs_position_utils.py
- hgvs_utils.py
- initial_formatting.py
- liftover.py
- lovd_api.py
- mappers.py
- methyl_syntax.py
- rna_formatter.py
- seq_data.py
- seq_state_to_expanded_repeat.py
- transcript_map_data.py
- use_checking.py
- utils.py
- valoutput.py
- variant.py
- vcf_to_pvcf.py
- vvDatabase.py
- vvDBGet.py
- vvDBInit.py
- vvDBInsert.py
- vvMixinConverters.py
- vvMixinCore.py
- vvMixinInit.py

Validation:

- gene2transcripts functional tests pass with updated invalid-query
expectations
- Dedicated hgvs2ref regression coverage added
- Core Validator orchestration retained through cleanup
- Existing HGVS object and position-helper cleanup principles retained
- integrate VariantFormatter into the VariantValidator codebase and test suite
- collect combined VariantValidator and VariantFormatter coverage in CI
- retain HGVS objects through processing where possible and reduce unnecessary
  string parsing and reconstruction
- add HGVS position helpers for intronic and offset handling
- improve identity variant handling without reparsing HGVS descriptions
- preserve identity variants through transcript/genomic mapping where supported
  by the underlying alignment
- improve transcript-mediated genomic mapping and round-trip behaviour
- add regression coverage for PRLR identity variants across gapped GRCh37
  transcript alignments
- document alignment-dependent identity, deletion and genomic round-trip
  behaviour
- update affected VariantValidator and VariantFormatter tests
@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

@John-F-Wagstaff. That's pretty much a wrap, other than a bit of clean up r.e. the module imports.

Outcomes key points

  • Faster
  • More accurate round-trip handling in some of the gap code
  • NW_(NM) now treated as an independent entity, and will not produce primary assembly output if they don't match. And will fail if the intronic position is not correct wrt the supplied NW_(NM) boundary. I know this was something you wanted to fix.
  • A configurable cache for the key validator db lookups
  • Several bugs squished.
  • And a necessary structural change r.e. VF for maintainability. VV also no longer used VF at all. Just VF using VV and also it now uses more VV functions than before, so can accept more input types

So, in terms of to do, its check it, see if it's all OK, clean up if required.

It looks like fetching from VVTA now has caches disabled. Is this correct. Do we want to look at this and add it as a configurable item in the same way for key lookups?

There are a few remaining evm creations left, but that will need to work on your clean up globals.

Hope its mostly OK

@John-F-Wagstaff

John-F-Wagstaff commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

I really really don't like integrating VariantFormatter into VariantValidator I did not outright object but this is not my prefered solution at all. EDIT even if you do want to do it it is a big change and should be done separately, not be folded into an already large pull request as a surprise bolt on to the last patch.

The VV code base is already too large and complex, this is not a good thing. Mixing the two code bases into the same project makes this worse. I had an in progress patch to remove the 1 VV usage of VF code and switch the VF tests to optional. (with the patch to remove of the VF CLI which you added to VV while this was in progress and put it into into VF currently to-do)

this feels like a step back to me.

At the very least given the nature of the code we do't ever want to add VF usage to VV, in future we want to make sure that VF is never called from VV.

@Peter-J-Freeman

Peter-J-Freeman commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

yep, we can discuss this in detail. There are a couple of reasons I prefer this setup personally, and can think about what to do going forward, but lets discuss all options. I'm open to all input. For now though, this really helped and it does make VF easier to maintain.

At the very least given the nature of the code we do't ever want to add VF usage to VV, in future we want to make sure that VF is never called from VV.

Done!

@Peter-J-Freeman

Peter-J-Freeman commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

If we separate it out again, we need to split the tests too. Thats for sure :)

So lets discuss this befoe the meeting with Nat and co

@John-F-Wagstaff

John-F-Wagstaff commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Ideally if we do add it we would merge the history of the two projects as well, not just add the files but that is quite a complex undertaking, and would change the commit hashes on all our existing commits (post VF creation) in order to get them in the right order too.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

need to figure out why CI is failing too. All tests pass locally

- Update configuration and metadata for the latest VVDB, VVTA and
SeqRepo builds
- Update regression tests to account for changes in the latest
transcript/genomic alignments
- Update expected mappings and exon boundaries affected by the new
alignment data
@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

Just putting this here as a potential plan so I don't forget. Got to move on to other things tomorrow but will email you about a meeting

Proposed VariantValidator / VariantFormatter Packaging Model

We will retain VariantValidator and VariantFormatter in a single GitHub repository, but package them as two independently installable Python distributions.

This is a common and well-established software-development pattern, generally referred to as a monorepo. It is particularly suitable where closely related packages are developed, tested, and released together, but users may not need every component.

Repository Structure

The repository would be organised approximately as:

variantValidator/
├── VariantValidator/
├── VariantFormatter/
├── packaging/
│   ├── variantvalidator/
│   │   └── pyproject.toml
│   └── variantformatter/
│       └── pyproject.toml
├── pyproject.toml
├── tests/
├── docs/
└── ...

Each packaging pyproject.toml defines a separate Python distribution:

  • VariantValidator packages and installs the VariantValidator code.
  • VariantFormatter packages and installs the VariantFormatter code and declares VariantValidator as a dependency.

This gives users clean installation choices.

To install VariantValidator alone:

pip install VariantValidator

To install VariantFormatter:

pip install VariantFormatter

Installing VariantFormatter will also install its required VariantValidator dependency.

Development and pip install -e .

The repository root can retain a development-oriented pyproject.toml so that the existing development workflow:

git clone <repository>
cd variantValidator
pip install -e .

continues to install both VariantValidator and VariantFormatter in editable mode for developers working on the complete repository.

The package-specific configurations provide more selective development installs.

For VariantValidator only:

pip install -e ./packaging/variantvalidator

For VariantFormatter and its VariantValidator dependency:

pip install -e ./packaging/variantformatter

The intended behaviour is therefore:

pip install -e .
    → complete VV + VF development environment

pip install -e ./packaging/variantvalidator
    → VV only

pip install -e ./packaging/variantformatter
    → VF + required VV dependency

This means the normal developer workflow does not become more complicated simply because the released distributions have been separated.

Shared Versioning and Releases

Both packages can use setuptools_scm to derive their version from the same Git repository and Git tag.

For example:

Git tag: v4.1.0
       │
       ├── VariantValidator 4.1.0
       └── VariantFormatter 4.1.0

This avoids maintaining separate version numbers or synchronising releases between two repositories. A single release/tag identifies the corresponding versions of both tools.

PyPI Distribution

Although the source is maintained in one repository, the release process produces two independent distributions:

VariantValidator repository
        │
        ├── build VariantValidator
        │      └── publish → PyPI: VariantValidator
        │
        └── build VariantFormatter
               └── publish → PyPI: VariantFormatter

These can initially be built and uploaded manually using build and twine, and subsequently automated through GitHub Actions and PyPI Trusted Publishing.

For example, the distributions could be built with:

python -m build packaging/variantvalidator
python -m build packaging/variantformatter

and checked before publication using:

twine check dist/*

The packages can then be uploaded to PyPI using twine, or publication can be handled automatically by GitHub Actions.

Resulting Model

The resulting model provides the advantages of both approaches:

  • one GitHub repository;
  • one development workflow;
  • one shared version and release history;
  • one integrated test suite;
  • two independently installable Python distributions;
  • users of VariantValidator do not have to install VariantFormatter;
  • installing VariantFormatter automatically installs its VariantValidator dependency; and
  • developers working on the complete project retain the simple pip install -e . workflow used today.

This also preserves the natural dependency relationship between the two tools:

VariantFormatter
       │
       │ depends on
       ▼
VariantValidator

VariantFormatter depends on VariantValidator, but VariantValidator does not require VariantFormatter.

The monorepo therefore keeps the two closely related tools together for development, testing, versioning, and release management while providing clean, conventional Python packaging for end users.

For me, it makes it a lot easier to maintain. Especially when developing VF.

Separate VariantValidator and VariantFormatter into independently
installable Python distributions while retaining both packages within
the existing repository.

- Add separate pyproject.toml files for VariantValidator and
  VariantFormatter.
- Configure setuptools_scm versioning for both distributions.
- Make VariantFormatter explicitly depend on VariantValidator.
- Keep VariantValidator independent of VariantFormatter.
- Update Docker installation to install both distributions.
- Update CI to verify both installed distributions before testing.
- Split VariantValidator and VariantFormatter tests into separate
  test directories.
- Split previously mixed test modules between the two packages.
- Update version tests for the separate package metadata.
- Update package manifests and included package resources.
- Update installation documentation for the new package layout.
- Update LOVD HGVS syntax checker installation instructions.
- Ensure LOVD HGVS syntax checker resources are installed during
  Docker setup.
- Add package metadata for the website, documentation, source,
  issue tracker, and terms of use.
- Update package classifiers and metadata.
- Retain AGPL-3.0 licensing pending the planned licensing update
  for this release.

Verify packaging isolation outside the repository checkout.
VariantValidator imports and operates without VariantFormatter, while
VariantFormatter requires VariantValidator as expected.

The complete test suite passes with both distributions installed, and
the VariantValidator test suite passes independently when
VariantFormatter is removed.
- validate parsed genomic HGVS accessions against the selected build
- apply the build check after HGVS parsing and recovery
- keep the build check within the HGVS input path
- avoid applying HGVS accession validation to VCF-like input
- return a GenomeBuildError for incompatible genomic references
- add regression coverage for GRCh37/GRCh38 accession mismatches
- add CLI and Python API documentation for hgvs2reference
- update the User Manual to include hgvs2reference alongside the other
tools
- update the documentation landing page to reflect the current software
suite
- update MkDocs navigation to include the hgvs2reference CLI and Python
API
- improve organisation of the documentation by interface (Web,
Installation, User Manual and REST API)
- ensure documentation links and navigation match the current project
structure
- remove references to documentation pages that do not exist
- improve consistency across tool descriptions and documentation sections
- extend regression coverage for mapping workflows
- improve mappers functional and unit test coverage
- add VariantFormatter unit tests
- expand gene2transcripts, hgvs_utils and vvMixinConverters tests
Also removes a stale import in vvMixinConverters.py which shows at least
 1 reverse complement func has been killed
@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

Having had a more detailed look through the first few using git on my own machine, rather than the github interface, it is looking fine so far, modulo a few niggles that only might be worth fixing, but it also looks like it would be relatively easy to split up some of the early patches into per-file/ function sections.

Would you like me to have a look with an eye to possibly re-basing them into something more focused? Depending on how things go in later patches, and what you think of the suggestions, there are also a few places that I could pop in fixes directly to the patch, instead of appending them on the end too if I do.

Moves a few regularly used dict and list creations out of functions
These were missed in the previous clean up
@Peter-J-Freeman

Peter-J-Freeman commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Hi John,

Yes, if its OK. Any niggles you spot it is worth hitting them now. As you saw, I spotted a few too in the last few merges. Let's take this oportunity to have a good thorough clean up and optimisation. Plus, anything we can do to sort the copyright too with easy wins.

Remove the remaining low hanging regexes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants