diff --git a/pyproject.toml b/pyproject.toml index 647b146..969be04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,11 @@ classifiers = [ "Topic :: Scientific/Engineering :: Bio-Informatics", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -requires-python = ">=3.8" +requires-python = ">=3.10" description = "Computable object representation and validation for gene fusions" license = {file = "LICENSE"} dependencies = [ @@ -140,6 +138,7 @@ ignore = [ [tool.ruff.lint.per-file-ignores] # ANN001 - missing-type-function-argument +# ANN102 - missing-type-cls # ANN2 - missing-return-type # ANN201 - missing-return-type-undocumented-public-function # ANN102 - missing-type-cls @@ -147,4 +146,4 @@ ignore = [ # B011 - assert-false # N805 - invalid-first-argument-name-for-method "tests/*" = ["ANN001", "ANN2", "ANN102", "S101", "B011"] -"src/fusor/models.py" = ["ANN201", "N805", "ANN001", "ANN2"] +"src/fusor/models.py" = ["ANN201", "N805", "ANN001", "ANN2", "ANN102"] diff --git a/src/fusor/fusor.py b/src/fusor/fusor.py index db9a86b..017b79a 100644 --- a/src/fusor/fusor.py +++ b/src/fusor/fusor.py @@ -1,7 +1,6 @@ """Module for modifying fusion objects.""" import logging import re -from typing import Dict, List, Optional, Tuple from urllib.parse import quote from biocommons.seqrepo import SeqRepo @@ -65,9 +64,9 @@ class FUSOR: def __init__( self, - seqrepo_data_path: Optional[str] = None, - gene_database: Optional[GeneDatabase] = None, - uta_db_url: Optional[str] = None, + seqrepo_data_path: str | None = None, + gene_database: GeneDatabase | None = None, + uta_db_url: str | None = None, ) -> None: """Initialize FUSOR class. @@ -88,7 +87,7 @@ def __init__( self.seqrepo = self.cool_seq_tool.seqrepo_access.sr @staticmethod - def _contains_element_type(kwargs: Dict, elm_type: StructuralElementType) -> bool: + def _contains_element_type(kwargs: dict, elm_type: StructuralElementType) -> bool: """Check if fusion contains element of a specific type. Helper method for inferring fusion type. :param Dict kwargs: keyword args given to fusion method @@ -97,13 +96,13 @@ def _contains_element_type(kwargs: Dict, elm_type: StructuralElementType) -> boo False otherwise. """ for c in kwargs["structural_elements"]: - if (isinstance(c, Dict) and c.get("type") == elm_type) or ( + if (isinstance(c, dict) and c.get("type") == elm_type) or ( isinstance(c, BaseStructuralElement) and c.type == elm_type ): return True return False - def fusion(self, fusion_type: Optional[FusionType] = None, **kwargs) -> Fusion: + def fusion(self, fusion_type: FusionType | None = None, **kwargs) -> Fusion: """Construct fusion object. :param Optional[FusionType] fusion_type: explicitly specify fusion type. @@ -170,9 +169,9 @@ def fusion(self, fusion_type: Optional[FusionType] = None, **kwargs) -> Fusion: @staticmethod def categorical_fusion( structural_elements: CategoricalFusionElements, - regulatory_element: Optional[RegulatoryElement] = None, - critical_functional_domains: Optional[List[FunctionalDomain]] = None, - r_frame_preserved: Optional[bool] = None, + regulatory_element: RegulatoryElement | None = None, + critical_functional_domains: list[FunctionalDomain] | None = None, + r_frame_preserved: bool | None = None, ) -> CategoricalFusion: """Construct a categorical fusion object :param CategoricalFusionElements structural_elements: elements @@ -200,9 +199,9 @@ def categorical_fusion( @staticmethod def assayed_fusion( structural_elements: AssayedFusionElements, - causative_event: Optional[CausativeEvent] = None, - assay: Optional[Assay] = None, - regulatory_element: Optional[RegulatoryElement] = None, + causative_event: CausativeEvent | None = None, + assay: Assay | None = None, + regulatory_element: RegulatoryElement | None = None, ) -> AssayedFusion: """Construct an assayed fusion object :param AssayedFusionElements structural_elements: elements constituting the @@ -229,9 +228,9 @@ async def transcript_segment_element( self, tx_to_genomic_coords: bool = True, use_minimal_gene_descr: bool = True, - seq_id_target_namespace: Optional[str] = None, + seq_id_target_namespace: str | None = None, **kwargs, - ) -> Tuple[Optional[TranscriptSegmentElement], Optional[List[str]]]: + ) -> tuple[TranscriptSegmentElement | None, list[str] | None]: """Create transcript segment element :param bool tx_to_genomic_coords: `True` if going from transcript @@ -325,7 +324,7 @@ async def transcript_segment_element( def gene_element( self, gene: str, use_minimal_gene_descr: bool = True - ) -> Tuple[Optional[GeneElement], Optional[str]]: + ) -> tuple[GeneElement | None, str | None]: """Create gene element :param str gene: Gene @@ -347,10 +346,10 @@ def templated_sequence_element( end: int, sequence_id: str, strand: Strand, - label: Optional[str] = None, + label: str | None = None, add_location_id: bool = False, residue_mode: ResidueMode = ResidueMode.RESIDUE, - seq_id_target_namespace: Optional[str] = None, + seq_id_target_namespace: str | None = None, ) -> TemplatedSequenceElement: """Create templated sequence element @@ -389,7 +388,7 @@ def templated_sequence_element( def linker_element( sequence: str, residue_type: CURIE = "SO:0000348", - ) -> Tuple[Optional[LinkerElement], Optional[str]]: + ) -> tuple[LinkerElement | None, str | None]: """Create linker element :param str sequence: Sequence @@ -439,8 +438,8 @@ def functional_domain( start: int, end: int, use_minimal_gene_descr: bool = True, - seq_id_target_namespace: Optional[str] = None, - ) -> Tuple[Optional[FunctionalDomain], Optional[str]]: + seq_id_target_namespace: str | None = None, + ) -> tuple[FunctionalDomain | None, str | None]: """Build functional domain instance. :param DomainStatus status: Status for domain. Must be either `lost` @@ -507,7 +506,7 @@ def regulatory_element( regulatory_class: RegulatoryClass, gene: str, use_minimal_gene_descr: bool = True, - ) -> Tuple[Optional[RegulatoryElement], Optional[str]]: + ) -> tuple[RegulatoryElement | None, str | None]: """Create RegulatoryElement :param RegulatoryClass regulatory_class: one of {"promoter", "enhancer"} :param str gene: gene term to fetch normalized descriptor for @@ -538,8 +537,8 @@ def _location_descriptor( start: int, end: int, sequence_id: str, - label: Optional[str] = None, - seq_id_target_namespace: Optional[str] = None, + label: str | None = None, + seq_id_target_namespace: str | None = None, use_location_id: bool = False, ) -> LocationDescriptor: """Create location descriptor @@ -600,7 +599,7 @@ def add_additional_fields( self, fusion: Fusion, add_all: bool = True, - fields: Optional[List[AdditionalFields]] = None, + fields: list[AdditionalFields] | None = None, target_namespace: str = "ga4gh", ) -> Fusion: """Add additional fields to Fusion object. @@ -668,7 +667,7 @@ def add_location_id(self, fusion: Fusion) -> Fusion: return fusion @staticmethod - def _location_id(location: Dict) -> CURIE: + def _location_id(location: dict) -> CURIE: """Return GA4GH digest for location :param dict location: VRS Location represented as a dict @@ -761,7 +760,7 @@ def add_gene_descriptor(self, fusion: Fusion) -> Fusion: def _normalized_gene_descriptor( self, query: str, use_minimal_gene_descr: bool = True - ) -> Tuple[Optional[GeneDescriptor], Optional[str]]: + ) -> tuple[GeneDescriptor | None, str | None]: """Return gene descriptor from normalized response. :param str query: Gene query diff --git a/src/fusor/models.py b/src/fusor/models.py index a62d725..9e14af2 100644 --- a/src/fusor/models.py +++ b/src/fusor/models.py @@ -1,7 +1,7 @@ """Model for fusion class""" from abc import ABC from enum import Enum -from typing import Any, Dict, List, Literal, Optional, Set, Type, Union +from typing import Any, Literal from ga4gh.vrsatile.pydantic import return_value from ga4gh.vrsatile.pydantic.vrsatile_models import ( @@ -63,9 +63,9 @@ class FunctionalDomain(BaseModel): type: Literal[FUSORTypes.FUNCTIONAL_DOMAIN] = FUSORTypes.FUNCTIONAL_DOMAIN status: DomainStatus associated_gene: GeneDescriptor - id: Optional[CURIE] = Field(None, alias="_id") - label: Optional[StrictStr] = None - sequence_location: Optional[LocationDescriptor] = None + id: CURIE | None = Field(None, alias="_id") + label: StrictStr | None = None + sequence_location: LocationDescriptor | None = None _get_id_val = field_validator("id")(return_value) @@ -124,13 +124,13 @@ class TranscriptSegmentElement(BaseStructuralElement): FUSORTypes.TRANSCRIPT_SEGMENT_ELEMENT ] = FUSORTypes.TRANSCRIPT_SEGMENT_ELEMENT transcript: CURIE - exon_start: Optional[StrictInt] = None - exon_start_offset: Optional[StrictInt] = 0 - exon_end: Optional[StrictInt] = None - exon_end_offset: Optional[StrictInt] = 0 + exon_start: StrictInt | None = None + exon_start_offset: StrictInt | None = 0 + exon_end: StrictInt | None = None + exon_end_offset: StrictInt | None = 0 gene_descriptor: GeneDescriptor - element_genomic_start: Optional[LocationDescriptor] = None - element_genomic_end: Optional[LocationDescriptor] = None + element_genomic_start: LocationDescriptor | None = None + element_genomic_end: LocationDescriptor | None = None @model_validator(mode="before") def check_exons(cls, values): @@ -386,9 +386,9 @@ class RegulatoryElement(BaseModel): type: Literal[FUSORTypes.REGULATORY_ELEMENT] = FUSORTypes.REGULATORY_ELEMENT regulatory_class: RegulatoryClass - feature_id: Optional[str] = None - associated_gene: Optional[GeneDescriptor] = None - feature_location: Optional[LocationDescriptor] = None + feature_id: str | None = None + associated_gene: GeneDescriptor | None = None + feature_location: LocationDescriptor | None = None _get_ref_id_val = field_validator("feature_id")(return_value) @@ -434,7 +434,7 @@ class FusionType(str, Enum): ASSAYED_FUSION = FUSORTypes.ASSAYED_FUSION.value @classmethod - def values(cls) -> Set: # noqa: ANN102 + def values(cls) -> set: """Provide all possible enum values.""" return {c.value for c in cls} @@ -443,15 +443,15 @@ class AbstractFusion(BaseModel, ABC): """Define Fusion class""" type: FusionType - regulatory_element: Optional[RegulatoryElement] = None - structural_elements: List[BaseStructuralElement] + regulatory_element: RegulatoryElement | None = None + structural_elements: list[BaseStructuralElement] @classmethod def _access_object_attr( - cls: Type[FusionType], - obj: Union[Dict, BaseModel], + cls, + obj: dict | BaseModel, attr_name: str, - ) -> Optional[Any]: # noqa: ANN401 + ) -> Any | None: # noqa: ANN401 """Help enable safe access of object properties while performing validation for Pydantic class objects. Because the validator could be handling either existing Pydantic class objects, or candidate dictionaries, we need a flexible @@ -476,10 +476,10 @@ def _access_object_attr( @classmethod def _fetch_gene_id( - cls: Type[FusionType], - obj: Union[Dict, BaseModel], + cls, + obj: dict | BaseModel, gene_descriptor_field: str, - ) -> Optional[str]: + ) -> str | None: """Get gene ID if element includes a gene annotation. :param obj: element to fetch gene from. Might not contain a gene (e.g. it's a @@ -591,10 +591,10 @@ class Assay(BaseModelForbidExtra): """Information pertaining to the assay used in identifying the fusion.""" type: Literal["Assay"] = "Assay" - assay_name: Optional[StrictStr] = None - assay_id: Optional[CURIE] = None - method_uri: Optional[CURIE] = None - fusion_detection: Optional[Evidence] = None + assay_name: StrictStr | None = None + assay_id: CURIE | None = None + method_uri: CURIE | None = None + fusion_detection: Evidence | None = None _get_assay_id_val = field_validator("assay_id")(return_value) _get_method_uri_val = field_validator("method_uri")(return_value) @@ -611,14 +611,12 @@ class Assay(BaseModelForbidExtra): ) -AssayedFusionElements = List[ - Union[ - TranscriptSegmentElement, - GeneElement, - TemplatedSequenceElement, - LinkerElement, - UnknownGeneElement, - ] +AssayedFusionElements = list[ + TranscriptSegmentElement + | GeneElement + | TemplatedSequenceElement + | LinkerElement + | UnknownGeneElement ] @@ -640,7 +638,7 @@ class CausativeEvent(BaseModelForbidExtra): type: Literal[FUSORTypes.CAUSATIVE_EVENT] = FUSORTypes.CAUSATIVE_EVENT event_type: EventType - event_description: Optional[StrictStr] = None + event_description: StrictStr | None = None model_config = ConfigDict( json_schema_extra={ @@ -662,8 +660,8 @@ class AssayedFusion(AbstractFusion): type: Literal[FUSORTypes.ASSAYED_FUSION] = FUSORTypes.ASSAYED_FUSION structural_elements: AssayedFusionElements - causative_event: Optional[CausativeEvent] = None - assay: Optional[Assay] = None + causative_event: CausativeEvent | None = None + assay: Assay | None = None model_config = ConfigDict( json_schema_extra={ @@ -698,14 +696,12 @@ class AssayedFusion(AbstractFusion): ) -CategoricalFusionElements = List[ - Union[ - TranscriptSegmentElement, - GeneElement, - TemplatedSequenceElement, - LinkerElement, - MultiplePossibleGenesElement, - ] +CategoricalFusionElements = list[ + TranscriptSegmentElement + | GeneElement + | TemplatedSequenceElement + | LinkerElement + | MultiplePossibleGenesElement ] @@ -717,8 +713,8 @@ class CategoricalFusion(AbstractFusion): """ type: Literal[FUSORTypes.CATEGORICAL_FUSION] = FUSORTypes.CATEGORICAL_FUSION - r_frame_preserved: Optional[StrictBool] = None - critical_functional_domains: Optional[List[FunctionalDomain]] = None + r_frame_preserved: StrictBool | None = None + critical_functional_domains: list[FunctionalDomain] | None = None structural_elements: CategoricalFusionElements model_config = ConfigDict( @@ -808,4 +804,4 @@ class CategoricalFusion(AbstractFusion): ) -Fusion = Union[CategoricalFusion, AssayedFusion] +Fusion = CategoricalFusion | AssayedFusion diff --git a/tests/test_fusor.py b/tests/test_fusor.py index 5230d90..c4e113d 100644 --- a/tests/test_fusor.py +++ b/tests/test_fusor.py @@ -1,6 +1,5 @@ """Module for testing the FUSOR class.""" import copy -from typing import Dict import pytest from ga4gh.vrsatile.pydantic.vrsatile_models import GeneDescriptor, LocationDescriptor @@ -337,7 +336,7 @@ def fusion_ensg_sequence_id(templated_sequence_element_ensg): return CategoricalFusion(**params) -def compare_gene_descriptor(actual: Dict, expected: Dict): +def compare_gene_descriptor(actual: dict, expected: dict): """Test that actual and expected gene descriptors match.""" assert actual["id"] == expected["id"] assert actual["type"] == expected["type"]