Skip to content

Commit 929ea25

Browse files
committed
Modernize scaffolding and complete type hints
1 parent 3a508b8 commit 929ea25

16 files changed

Lines changed: 427 additions & 254 deletions

File tree

‎.github/dependabot.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: github-actions
4+
directory: /
5+
schedule:
6+
interval: monthly
7+
groups:
8+
github-actions:
9+
patterns:
10+
- '*'

‎.github/workflows/pypi.yml‎

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,54 @@
11
name: PyPI deploy
2+
23
on:
34
release:
45
types:
5-
- created
6+
- published
7+
8+
permissions:
9+
contents: read
610

711
jobs:
8-
build-and-publish:
12+
build:
13+
name: build distributions
914
runs-on: ubuntu-latest
10-
strategy:
11-
matrix:
12-
python-version: [3.13]
13-
1415
steps:
15-
- uses: actions/checkout@v2
16-
- name: Set up Python ${{ matrix.python-version }}
17-
uses: actions/setup-python@v2
16+
- uses: actions/checkout@v5
1817
with:
19-
python-version: ${{ matrix.python-version }}
20-
- name: Install dependencies
21-
run: python -m pip install --upgrade pip setuptools wheel build
22-
- name: Create the source distribution
18+
# setuptools-scm needs the full history and tags to derive the version
19+
fetch-depth: 0
20+
persist-credentials: false
21+
- uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.13'
24+
- name: Install build dependencies
25+
run: python -m pip install --upgrade pip build twine
26+
- name: Build sdist and wheel
2327
run: python -m build
24-
- name: Publish distribution to PyPI
25-
uses: pypa/gh-action-pypi-publish@master
28+
- name: Check distributions
29+
run: python -m twine check --strict dist/*
30+
- name: Upload distributions
31+
uses: actions/upload-artifact@v4
2632
with:
27-
password: ${{ secrets.PYPI_API_TOKEN }}
33+
name: dist
34+
path: dist/
35+
if-no-files-found: error
36+
37+
publish:
38+
name: publish to PyPI
39+
needs: build
40+
runs-on: ubuntu-latest
41+
environment:
42+
name: pypi
43+
url: https://pypi.org/p/gedcom7
44+
permissions:
45+
# required for PyPI trusted publishing (OIDC); no API token needed
46+
id-token: write
47+
steps:
48+
- name: Download distributions
49+
uses: actions/download-artifact@v4
50+
with:
51+
name: dist
52+
path: dist/
53+
- name: Publish distribution to PyPI
54+
uses: pypa/gh-action-pypi-publish@release/v1

‎.github/workflows/test.yml‎

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,69 @@
1-
name: unit tests
1+
name: tests
22

33
on:
44
push:
55
branches:
66
- main
77
pull_request:
88

9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
916
jobs:
10-
build:
17+
lint:
18+
name: lint and type check
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v5
22+
with:
23+
# setuptools-scm derives the version from tags
24+
fetch-depth: 0
25+
persist-credentials: false
26+
- uses: actions/setup-python@v5
27+
with:
28+
python-version: '3.13'
29+
cache: pip
30+
- name: Install dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install --group dev .
34+
- name: Lint
35+
run: ruff check --output-format=github .
36+
- name: Check formatting
37+
run: ruff format --check .
38+
- name: Static type check
39+
run: mypy
40+
41+
test:
42+
name: test on Python ${{ matrix.python-version }}
1143
runs-on: ubuntu-latest
1244
strategy:
45+
fail-fast: false
1346
matrix:
1447
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
1548
steps:
16-
- uses: actions/checkout@v2
17-
- name: Set up Python ${{ matrix.python-version }}
18-
uses: actions/setup-python@v2
49+
- uses: actions/checkout@v5
50+
with:
51+
fetch-depth: 0
52+
persist-credentials: false
53+
- uses: actions/setup-python@v5
1954
with:
2055
python-version: ${{ matrix.python-version }}
21-
- name: Install Python dependencies
56+
cache: pip
57+
- name: Install dependencies
2258
run: |
23-
python -m pip install --upgrade pip wheel
24-
pip install pytest mypy
25-
pip install .
26-
- name: Static type check
27-
run: |
28-
mypy gedcom7
59+
python -m pip install --upgrade pip
60+
pip install --group dev .
2961
- name: Test with pytest
30-
run: |
31-
pytest
62+
run: pytest --cov=gedcom7 --cov-report=term-missing --cov-report=xml
63+
- name: Upload coverage report
64+
if: matrix.python-version == '3.13'
65+
uses: actions/upload-artifact@v4
66+
with:
67+
name: coverage
68+
path: coverage.xml
69+
if-no-files-found: error

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,9 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
# Ruff
132+
.ruff_cache/
133+
134+
# pytest-benchmark
135+
.benchmarks/

‎README.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# gedcom7
22

3+
[![Tests](https://github.com/DavidMStraub/python-gedcom7/actions/workflows/test.yml/badge.svg)](https://github.com/DavidMStraub/python-gedcom7/actions/workflows/test.yml)
4+
[![PyPI](https://img.shields.io/pypi/v/gedcom7)](https://pypi.org/project/gedcom7/)
5+
[![Python versions](https://img.shields.io/pypi/pyversions/gedcom7)](https://pypi.org/project/gedcom7/)
6+
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7+
38
A [GEDCOM 7](https://gedcom.io/) parser for Python.
49

510
## Background
@@ -23,6 +28,31 @@ with open("my_gedcom.ged", "r", encoding="utf-8") as f:
2328
records = gedcom7.loads(string)
2429
```
2530

31+
## Development
32+
33+
Install the package together with the development dependencies:
34+
35+
```
36+
python -m pip install --group dev --editable .
37+
```
38+
39+
Then run the checks:
40+
41+
```
42+
pytest # tests
43+
mypy # static type check (strict)
44+
ruff check . # lint
45+
ruff format . # format
46+
```
47+
48+
## Releasing
49+
50+
The version is derived from git tags by
51+
[setuptools-scm](https://setuptools-scm.readthedocs.io/); there is no version
52+
string in the source tree. To release, push a `vX.Y.Z` tag and publish a GitHub
53+
release for it — the `PyPI deploy` workflow builds the distributions and
54+
uploads them via PyPI trusted publishing.
55+
2656
## Credits
2757

2858
Inspiration was drawn from the [Javascript parser](https://github.com/gedcom7code/js-parser).

‎gedcom7/__init__.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
"""Base module for gedcom7."""
22

3+
from importlib.metadata import PackageNotFoundError, version
4+
35
from .parser import loads
46

5-
__version__ = "0.4.1"
7+
__all__ = ["loads"]
8+
9+
try:
10+
__version__ = version("gedcom7")
11+
except PackageNotFoundError: # pragma: no cover - source checkout without install
12+
__version__ = "0.0.0.dev0"

‎gedcom7/cast.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313

1414
def cast_value(text: str, type_id: str) -> types.DataType | None:
15+
"""Cast a payload string to the data type appropriate for its structure type."""
1516
if not text:
1617
return None
1718
payload = const.payloads.get(type_id)
@@ -38,16 +39,16 @@ def _cast_integer(value: str) -> int:
3839
"""Cast a string to an integer."""
3940
try:
4041
return int(value)
41-
except ValueError:
42-
raise ValueError(f"Cannot interpret {value} as integer")
42+
except ValueError as exc:
43+
raise ValueError(f"Cannot interpret {value} as integer") from exc
4344

4445

4546
def _cast_list_text(value: str) -> list[str]:
4647
"""Cast a string to a list of strings."""
4748
return [el.strip() for el in value.split(",")]
4849

4950

50-
def _match(text: str, regex: str, type_name: str) -> re.Match:
51+
def _match(text: str, regex: str, type_name: str) -> re.Match[str]:
5152
"""Match a string and raise if not compatible."""
5253
match = re.fullmatch(regex, text)
5354
if not match:
@@ -70,7 +71,8 @@ def _cast_time(value: str) -> types.Time:
7071
"""Cast a string to a Time."""
7172
match = _match(value, grammar.time, "Time")
7273
return types.Time(
73-
tz=match.group("tz"),
74+
# the tz group is `(?P<tz>Z)?`, so it is either "Z" or absent
75+
tz="Z" if match.group("tz") else None,
7476
hour=int(match.group("hour")),
7577
minute=int(match.group("minute")),
7678
second=int(match.group("second")) if match.group("second") else None,

‎gedcom7/parser.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44

5-
from . import cast, const, grammar
5+
from . import const, grammar
66
from .types import GedcomStructure
77

88

‎gedcom7/py.typed‎

Whitespace-only changes.

‎gedcom7/types.py‎

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass, field
6-
from typing import Literal, Union
6+
from typing import Literal
77

88
from . import cast, const
99

@@ -16,8 +16,8 @@ class GedcomStructure:
1616
pointer: str
1717
text: str
1818
xref: str
19-
children: list["GedcomStructure"] = field(default_factory=list)
20-
parent: "GedcomStructure | None" = None
19+
children: list[GedcomStructure] = field(default_factory=list)
20+
parent: GedcomStructure | None = None
2121

2222
@property
2323
def type_id(self) -> str:
@@ -32,18 +32,19 @@ def type_id(self) -> str:
3232
return const.substructures[""][self.tag]
3333
return const.substructures[self.parent.type_id][self.tag]
3434

35-
def __post_init__(self):
35+
def __post_init__(self) -> None:
3636
"""Post-init steps: set parent on children."""
3737
for child in self.children:
3838
child.parent = self
3939

40-
def append_child(self, child: "GedcomStructure") -> None:
40+
def append_child(self, child: GedcomStructure) -> None:
4141
"""Append a child to the structure and set the child's parent to self."""
4242
child.parent = self
4343
self.children.append(child)
4444

4545
@property
46-
def value(self) -> "DataType | None":
46+
def value(self) -> DataType | None:
47+
"""Get the payload cast to its appropriate data type."""
4748
return cast.cast_value(text=self.text, type_id=self.type_id)
4849

4950

@@ -130,24 +131,19 @@ class DateRange:
130131
end: Date | None = None
131132

132133

133-
DateValue = Union[
134-
Date,
135-
DatePeriod,
136-
DateApprox,
137-
DateRange,
138-
]
139-
140-
DataType = Union[
141-
str,
142-
int,
143-
list[str],
144-
PersonalName,
145-
Time,
146-
Age,
147-
MediaType,
148-
DateExact,
149-
DateApprox,
150-
DateRange,
151-
DatePeriod,
152-
DateValue,
153-
]
134+
DateValue = Date | DatePeriod | DateApprox | DateRange
135+
136+
DataType = (
137+
str
138+
| int
139+
| list[str]
140+
| PersonalName
141+
| Time
142+
| Age
143+
| MediaType
144+
| DateExact
145+
| DateApprox
146+
| DateRange
147+
| DatePeriod
148+
| DateValue
149+
)

0 commit comments

Comments
 (0)