Skip to content

Commit aa55895

Browse files
committed
Add type hints, modernise packaging, improve docs and CI
- Add type annotations across all modules (closes #29) - Migrate from setup.cfg/setup.py to pyproject.toml (PEP 621) - Switch from flake8/isort to ruff (enforce together with mypy) - Add Python 3.13 to the test matrix - Better user documentation (#22)
1 parent c2ae2f9 commit aa55895

22 files changed

Lines changed: 504 additions & 449 deletions

.github/workflows/ci.yml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: Python package CI
22
on:
33
push:
4-
branches:
4+
branches:
55
- master
66
pull_request:
77
branches:
88
- master
99
schedule:
10-
- cron: '0 0 * * 0'
10+
- cron: '0 0 * * 0'
1111
jobs:
1212
build:
1313
runs-on: ubuntu-latest
@@ -19,20 +19,23 @@ jobs:
1919
- '3.10'
2020
- '3.11'
2121
- '3.12'
22+
- '3.13'
2223
steps:
23-
- uses: actions/checkout@v2
24+
- uses: actions/checkout@v4
2425
- name: Set up Python ${{ matrix.python-version }}
25-
uses: actions/setup-python@v2
26+
uses: actions/setup-python@v4
2627
with:
2728
python-version: ${{ matrix.python-version }}
2829
- name: Install dependencies
2930
run: |
3031
pip install --upgrade pip
31-
pip install flake8 pytest .
32-
- name: Lint with flake8
32+
pip install ruff mypy pytest .
33+
- name: Lint with ruff
34+
run: |
35+
ruff check .
36+
- name: Type check with mypy
3337
run: |
34-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
35-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
38+
mypy mutalyzer_hgvs_parser/
3639
- name: Test with pytest
3740
run: |
3841
pytest

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,9 @@ coverage.xml
3737
.hypothesis/
3838
.pytest_cache/
3939

40+
# Docs build output
41+
docs/_build/
42+
docs/hgvs_mutalyzer.g
43+
4044
# Other files
4145
*.png

.readthedocs.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ build:
1010
tools:
1111
python: "3.12"
1212

13+
python:
14+
install:
15+
- method: pip
16+
path: .
17+
extra_requirements:
18+
- docs
19+
1320
# Build documentation in the "docs/" directory with Sphinx
1421
sphinx:
1522
configuration: docs/conf.py

MANIFEST.in

Lines changed: 0 additions & 7 deletions
This file was deleted.

docs/api/hgvs_parser.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Hgvs parser
2-
===========================================
1+
HGVS Parser
2+
===========
33

44
.. automodule:: mutalyzer_hgvs_parser.hgvs_parser
55
:members:

docs/conf.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
from subprocess import call
2-
3-
call('pip install sphinx-argparse ..', shell=True)
1+
from email.utils import parseaddr
2+
from pathlib import Path
43

54
from mutalyzer_hgvs_parser import _get_metadata
65

6+
# Generate a single combined grammar file for download from the documentation.
7+
_ebnf_dir = Path(__file__).parent.parent / "mutalyzer_hgvs_parser" / "ebnf"
8+
_grammar_files = ["top.g", "dna.g", "protein.g", "reference.g", "common.g"]
9+
_combined = "\n\n".join((_ebnf_dir / f).read_text() for f in _grammar_files)
10+
(Path(__file__).parent / "hgvs_mutalyzer.g").write_text(_combined)
711

8-
author = _get_metadata('Author')
9-
copyright = _get_metadata('Author')
12+
author, _ = parseaddr(_get_metadata('Author-email'))
13+
copyright = author
1014
project = _get_metadata('Name')
1115
release = _get_metadata('Version')
1216

1317
autoclass_content = 'both'
18+
exclude_patterns = ['_build']
1419
extensions = ['sphinx.ext.autodoc', 'sphinxarg.ext']
15-
master_doc = 'index'
20+
root_doc = 'index'

docs/grammar.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
Grammar
22
=======
33

4-
The derived :download:`EBNF grammar <../mutalyzer_hgvs_parser/ebnf/hgvs_mutalyzer_3.g>`
5-
does not consider all the HGVS_ nomenclature recommendations. Currently,
6-
the focus is mostly on descriptions at the DNA level. Examples of
4+
The :download:`EBNF grammar <hgvs_mutalyzer.g>`, derived from the HGVS_
5+
nomenclature recommendations and first formally described in `Laros et al.
6+
(2011) <https://pmc.ncbi.nlm.nih.gov/articles/PMC3194197/>`_, is split across
7+
several files in the ``mutalyzer_hgvs_parser/ebnf/`` directory and combined
8+
here for reference.
9+
10+
The grammar does not consider all the HGVS_ nomenclature recommendations.
11+
Examples of
712
descriptions not supported:
813

914
- ``LRG_199t1:c.[2376G>C];[3103del]``

docs/library.rst

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ to a dictionary model.
1717
{'id': 'NG_012337.1', 'selector': {'id': 'SDHD_v001'}}
1818
1919
20-
An alternative start rule for the grammar can be used.
20+
An alternative start rule can be used to parse a partial description.
21+
See :doc:`usage` for the list of available start rules.
2122

2223
.. code:: python
2324
@@ -27,16 +28,64 @@ An alternative start rule for the grammar can be used.
2728
{'location': {'type': 'point', 'position': 274}, 'type': 'deletion', 'source': 'reference'}
2829
2930
31+
The ``"source"`` field
32+
----------------------
33+
34+
The ``"source"`` field indicates where a sequence or location originates:
35+
36+
``"description"``
37+
The sequence is written explicitly in the HGVS string.
38+
39+
.. code:: python
40+
41+
>>> to_model('274G>T', 'variant')['deleted']
42+
[{'sequence': 'G', 'source': 'description'}]
43+
44+
``"reference"``
45+
The sequence or location is implied by HGVS convention; it must be
46+
retrieved from the (same) reference sequence.
47+
48+
.. code:: python
49+
50+
>>> to_model('274del', 'variant')['source']
51+
'reference'
52+
53+
>>> to_model('NG_012337.1:g.274_275ins100_200')['variants'][0]['inserted']
54+
[{'location': {...}, 'source': 'reference'}]
55+
56+
``{"id": "..."}``
57+
The sequence comes from an explicitly named external reference.
58+
59+
.. code:: python
60+
61+
>>> to_model('NG_012337.1:g.274_275insNG_012337.3:g.100_200')['variants'][0]['inserted']
62+
[{'type': 'description_dna', ..., 'source': {'id': 'NG_012337.3'}, ...}]
63+
64+
3065
The ``parse()`` function
3166
------------------------
3267

33-
The ``parse()`` function can be used to parse for syntax correctness purposes
34-
an HGVS description. Its output is a lark parse tree.
68+
The ``parse()`` function checks whether a description is syntactically valid
69+
and raises an exception if not. Most users will want ``to_model()`` instead,
70+
which both checks validity and returns the dictionary model in one step.
71+
``parse()`` is useful when you only need to validate and do not need the model,
72+
as it skips the conversion step.
3573

3674
.. code:: python
3775
3876
>>> from mutalyzer_hgvs_parser import parse
3977
>>> parse("LRG_1:100del")
40-
Tree('description', [Tree('reference', [Token('ID', 'LRG_1')]), Tree('variants',
41-
[Tree('variant', [Tree('location', [Tree('point', [Token('NUMBER', '100')])]), Tree('deletion', [])])])])
78+
Tree('description', ...)
79+
80+
The return value is a `lark Tree
81+
<https://lark-parser.readthedocs.io/en/stable/classes.html#tree>`_ object.
82+
Working with it directly requires familiarity with lark.
83+
84+
85+
The ``HgvsParser`` class
86+
------------------------
87+
88+
For advanced use cases, such as supplying a custom grammar file or controlling
89+
whitespace handling, the ``HgvsParser`` class can be used directly. See the
90+
:doc:`API documentation <api/hgvs_parser>` for details.
4291

docs/usage.rst

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,45 @@ To obtain the model of a description add the ``-c`` flag.
5757
}
5858
5959
60-
Grammar start rule
61-
------------------
62-
63-
By default, the Mutalyzer
64-
:download:`grammar <../mutalyzer_hgvs_parser/ebnf/hgvs_mutalyzer_3.g>` is used,
65-
with ``description`` as the start (top) rule. It is however possible
66-
to choose a different start rule with the ``-r`` option.
60+
Parsing a partial description
61+
-----------------------------
62+
63+
By default the parser expects a complete HGVS description
64+
(e.g. ``NG_012337.1:g.274G>T``). If you only have a *part* of a
65+
description — a bare variant, a location, or a reference — you can tell
66+
the parser where to start by passing a **start rule** with the ``-r``
67+
option.
68+
69+
A start rule is the name of the grammar rule that the input must match.
70+
The most useful alternatives to the default (``description``) are:
71+
72+
.. list-table::
73+
:header-rows: 1
74+
:widths: 20 80
75+
76+
* - Start rule
77+
- Matches
78+
* - ``description``
79+
- A complete HGVS description *(default)*
80+
* - ``variant``
81+
- A single variant without a reference, e.g. ``274G>T`` or ``10del``
82+
* - ``variants``
83+
- A semicolon-separated list of variants, e.g. ``[274G>T;280del]``
84+
* - ``location``
85+
- A position or range, e.g. ``274`` or ``10_20``
86+
* - ``reference``
87+
- A reference identifier, e.g. ``NG_012337.1`` or ``NG_012337.1(SDHD_v001)``
88+
* - ``inserted``
89+
- An inserted sequence, e.g. ``ATG`` or ``NG_012337.1:c.1_10``
6790

6891
.. code-block:: console
6992
7093
$ mutalyzer_hgvs_parser -r variant '274G>T'
7194
Successfully parsed:
7295
274G>T
7396
74-
The ``-c`` flag can be employed together with a different start rule.
97+
The ``-c`` flag can be combined with ``-r`` to convert a partial
98+
description to its model.
7599

76100
.. code-block:: console
77101
@@ -101,8 +125,15 @@ The ``-c`` flag can be employed together with a different start rule.
101125
Parse tree representation
102126
-------------------------
103127

104-
If pydot_ is installed, an image of the lark parse tree can be obtained
105-
with the ``-i`` option.
128+
An image of the parse tree can be obtained with the ``-i`` option.
129+
This requires Graphviz_ to be installed on your system (e.g.
130+
``apt install graphviz`` on Debian/Ubuntu) and pydot_:
131+
132+
.. code-block:: console
133+
134+
pip install mutalyzer-hgvs-parser[plot]
135+
136+
106137
107138
.. code-block:: console
108139
@@ -115,4 +146,5 @@ with the ``-i`` option.
115146
.. image:: images/tree.png
116147
:alt: Parse tree representation.
117148

149+
.. _Graphviz: https://graphviz.org/
118150
.. _pydot: https://pypi.org/project/pydot/

mutalyzer_hgvs_parser/__init__.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
1+
from __future__ import annotations
2+
13
from importlib.metadata import metadata
24

35
from .convert import to_model
46
from .hgvs_parser import parse
57

8+
__all__ = ["parse", "to_model"]
9+
610

7-
def _get_metadata(name):
11+
def _get_metadata(name: str) -> str:
812
meta = metadata(__package__)
13+
return meta.get(name) or ""
914

10-
return meta.get(name, "")
15+
16+
def _get_homepage() -> str:
17+
meta = metadata(__package__)
18+
for project_url in meta.get_all("Project-URL") or []:
19+
label, url = project_url.split(", ", 1)
20+
if label == "Homepage":
21+
return url
22+
return ""
1123

1224

13-
_copyright_notice = "Copyright (c) {} <{}>".format(
14-
_get_metadata("Author"), _get_metadata("Author-email")
15-
)
25+
_copyright_notice = "Copyright (c) {}".format(_get_metadata("Author-email"))
1626

1727
usage = [_get_metadata("Summary"), _copyright_notice]
1828

1929

20-
def doc_split(func):
21-
return func.__doc__.split("\n\n")[0]
30+
def doc_split(func: object) -> str:
31+
return (func.__doc__ or "").split("\n\n")[0]
2232

2333

24-
def version(name):
34+
def version(name: str) -> str:
2535
return "{} version {}\n\n{}\nHomepage: {}".format(
2636
_get_metadata("Name"),
2737
_get_metadata("Version"),
2838
_copyright_notice,
29-
_get_metadata("Home-page"),
39+
_get_homepage(),
3040
)

0 commit comments

Comments
 (0)