diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index c995ae7..5c9e79f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,4 +1,5 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# It will also update the test coverage using coveralls.io # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python name: Python package @@ -12,32 +13,44 @@ on: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest + python -m pip install flake8 pytest pytest-cov if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Install package run: | python -m pip install . + - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --statistics - - name: Test with pytest + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test with pytest and output coverage report run: | - pytest + pytest --cov=mavehgvs --cov-report=xml:coverage-${{ matrix.python-version }}.xml --cov-report=term + + - name: Send coverage to coveralls.io + # only calculate coverage for the Python version being used by MaveDB + if: matrix.python-version == '3.11' + uses: coverallsapp/github-action@v2 + with: + file: coverage-${{ matrix.python-version }}.xml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index c831f7a..ea8306c 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,4 +1,4 @@ -# This workflow will upload a Python Package using Twine when a release is created +# This workflow will upload a Python Package to PyPI when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. @@ -16,24 +16,51 @@ permissions: contents: read jobs: - deploy: + release-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Build release distributions + run: | + python -m pip install --upgrade pip + python -m pip install hatch + python -m hatch build + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + pypi-publish: runs-on: ubuntu-latest + needs: + - release-build + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + # Dedicated environments with protections for publishing are strongly recommended. + # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules + environment: + name: pypi + # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: + url: https://pypi.org/p/mavehgvs steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install hatch - - name: Build package - run: hatch build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_MAVEHGVS }} + - name: Retrieve release distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1912aef..693cee5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 26.5.1 hooks: - id: black language_version: python3.11 - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + rev: 7.3.0 hooks: - id: flake8 diff --git a/README.md b/README.md index c9cd0aa..ffc785c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![Build Status](https://travis-ci.com/VariantEffect/mavehgvs.svg?branch=main)](https://travis-ci.com/VariantEffect/mavehgvs) [![Coverage Status](https://coveralls.io/repos/github/VariantEffect/mavehgvs/badge.svg?branch=main)](https://coveralls.io/github/VariantEffect/mavehgvs?branch=main) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) diff --git a/pyproject.toml b/pyproject.toml index c3f948a..8ed647c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,3 +50,7 @@ exclude = [ [tool.setuptools.package-data] "mavehgvs" = ["py.typed"] + +[tool.black] +line-length = 88 +target-version = ['py311'] diff --git a/src/mavehgvs/__init__.py b/src/mavehgvs/__init__.py index 2a3438d..853b097 100644 --- a/src/mavehgvs/__init__.py +++ b/src/mavehgvs/__init__.py @@ -3,7 +3,7 @@ from mavehgvs.variant import Variant from mavehgvs.util import parse_variant_strings -__version__ = "0.7.0" +__version__ = "0.8.0" __all__ = [ "__version__", diff --git a/src/mavehgvs/patterns/protein.py b/src/mavehgvs/patterns/protein.py index 0979e62..dd342d0 100644 --- a/src/mavehgvs/patterns/protein.py +++ b/src/mavehgvs/patterns/protein.py @@ -13,7 +13,9 @@ """ pro_equal: str = ( - rf"(?P(?:(?P{aa_pos})?(?P=))|(?P\(=\)))" + rf"(?P(?:(?:(?P{aa_pos})|" + + rf"(?:(?P{aa_pos})_(?P{aa_pos})))?(?P=))|" + + rf"(?P\(=\)))" ) """str: Pattern matching protein equality or synonymous variant. """ diff --git a/src/mavehgvs/patterns/util.py b/src/mavehgvs/patterns/util.py index 7ac5cfa..1175905 100644 --- a/src/mavehgvs/patterns/util.py +++ b/src/mavehgvs/patterns/util.py @@ -1,5 +1,4 @@ -"""Utility functions for working with mavehgvs regex pattern strings. -""" +"""Utility functions for working with mavehgvs regex pattern strings.""" import re from typing import Sequence, Optional diff --git a/src/mavehgvs/variant.py b/src/mavehgvs/variant.py index ea3a0cb..ae959e2 100644 --- a/src/mavehgvs/variant.py +++ b/src/mavehgvs/variant.py @@ -396,19 +396,47 @@ def _variant_dictionary_to_string( # noqa: max-complexity: 25 raise MaveHgvsParseError("variant dictionary missing required keys") if variant_type == "equal": - expected_keys = ["variant_type", "prefix"] - if prefix == "p": - expected_keys.extend(["position", "target"]) - else: - expected_keys.extend(["start_position", "end_position"]) - if sorted(vdict.keys()) != sorted(expected_keys): - raise MaveHgvsParseError("variant dictionary contains invalid keys") - if prefix == "p": - variant_string = f"{vdict['target']}{vdict['position']}=" - elif vdict["start_position"] == vdict["end_position"]: - variant_string = f"{vdict['start_position']}=" + # special case for fully-identical variants + if sorted(vdict.keys()) == ["prefix", "variant_type"]: + variant_string = "=" + elif ( + sorted(vdict.keys()) == ["prefix", "synonymous", "variant_type"] + and prefix == "p" + ): + variant_string = "(=)" else: - variant_string = f"{vdict['start_position']}_{vdict['end_position']}=" + expected_keys = [ + "variant_type", + "prefix", + "start_position", + "end_position", + ] + if prefix == "p": + expected_keys.extend(["start_target", "end_target"]) + if sorted(vdict.keys()) != sorted(expected_keys): + raise MaveHgvsParseError("variant dictionary contains invalid keys") + if vdict["start_position"] == vdict["end_position"]: + if prefix == "p": + if vdict["start_target"] == vdict["end_target"]: + variant_string = ( + f"{vdict['start_target']}{vdict['start_position']}=" + ) + else: + raise MaveHgvsParseError( + "amino acid mismatch at same position" + ) + else: + variant_string = f"{vdict['start_position']}=" + else: + if prefix == "p": + variant_string = ( + f"{vdict['start_target']}{vdict['start_position']}_" + f"{vdict['end_target']}{vdict['end_position']}=" + ) + else: + variant_string = ( + f"{vdict['start_position']}_{vdict['end_position']}=" + ) elif variant_type == "sub": if sorted(vdict.keys()) != sorted( ["variant_type", "prefix", "position", "target", "variant"] diff --git a/tests/test_patterns/test_protein.py b/tests/test_patterns/test_protein.py index e2b2aeb..f35d0b2 100644 --- a/tests/test_patterns/test_protein.py +++ b/tests/test_patterns/test_protein.py @@ -24,6 +24,7 @@ def setUpClass(cls): "=", "(=)", "Cys22=", + "Gly12_Glu14=", ] cls.invalid_strings = ["=22", "Arg18(=)", "Cys-22", "=="] diff --git a/tests/test_variant.py b/tests/test_variant.py index 74fc7d5..d52fd84 100644 --- a/tests/test_variant.py +++ b/tests/test_variant.py @@ -80,6 +80,7 @@ def test_sub(self) -> None: "c.12=", "g.88_99=", "c.43-6_595+12=", + "p.Glu12_Gly14=", ] for s in variant_strings: @@ -229,6 +230,7 @@ def test_creation(self): invalid_variant_strings = [ "p.[Glu27Trp;=;Ter345Lys]", "p.[(=);Gly18del;Glu27Trp;Ter345Lys]", + "p.[Gln7_Asn19=;Glu27Trp;Ter345Lys]", "c.[12T>A;=;78+5_78+10del]", "c.[1_3=;12T>A;78+5_78+10del]", "p.[Glu27fs;Arg48Lys]", @@ -296,11 +298,46 @@ def test_equal(self): { "variant_type": "equal", "prefix": "p", - "position": "27", - "target": "Glu", + }, + "p.=", + ), + ( + { + "variant_type": "equal", + "prefix": "p", + "synonymous": True, + }, + "p.(=)", + ), + ( + { + "variant_type": "equal", + "prefix": "c", + }, + "c.=", + ), + ( + { + "variant_type": "equal", + "prefix": "p", + "start_position": "27", + "start_target": "Glu", + "end_position": "27", + "end_target": "Glu", }, "p.Glu27=", ), + ( + { + "variant_type": "equal", + "prefix": "p", + "start_position": "12", + "start_target": "Glu", + "end_position": "14", + "end_target": "Gly", + }, + "p.Glu12_Gly14=", + ), ( { "variant_type": "equal", @@ -523,6 +560,14 @@ def test_delins(self): "end_target": "Cys", "variant": "AlaGly", }, + { + "variant_type": "equal", + "prefix": "p", + "start_position": "27", + "start_target": "Glu", + "end_position": "27", + "end_target": "Asp", + }, ] for d, s in valid_dict_tuples: @@ -561,6 +606,13 @@ def test_extra_keys(self): "variant": "Ser", "position": "Ala", }, + { + "variant_type": "fs", + "prefix": "p", + "position": 80, + "target": "Cys", + "start_position": 23, + }, ] for d in invalid_dicts: