Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ name: Deploy Sphinx docs to Pages

on:
push:
branches: [main]
paths:
- "src/pmotools/**"
- "man/**"
- ".github/workflows/docs.yml"
tags:
- 'v*.*.*'
- 'test*'
workflow_dispatch:

permissions:
Expand Down
140 changes: 79 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,83 @@
# pmotools

A collection of tools to interact with [portable microhaplotype object (pmo) file format](https://github.com/PlasmoGenEpi/portable-microhaplotype-object)
A collection of tools to interact with the [portable microhaplotype object (PMO) file format](https://github.com/PlasmoGenEpi/portable-microhaplotype-object).

# Setup
Documentation on the format and pmotools-python, including detailed tutorials can be found [here](https://plasmogenepi.github.io/PMO_Docs/).

The manual for pmotools-python can be found [here](https://plasmogenepi.github.io/pmotools-python/index.html).

# Installation

Requires Python 3.11+.

Install using pip. Currently only supports python 3.11+
```bash
pip install .
pip install pmotools
```

This installs the `pmotools-python` command-line tool and the `pmotools` Python library.

# Usage

This package is built to either be used as a library in python projects and a command line interface already created which can be called from the commandline `pmotools-python` which will install with `pip install .`.
pmotools-python can be used from the command line or imported as a Python library.

## Command line

## Auto completion
After installation, the `pmotools-python` command is available:

```bash
pmotools-python --help
pmotools-python validate_pmo --help
```

If you want to add auto-completion to the scripts master function [pmotools-python](scripts/pmotools-runner.py) you can add the following to your `~/.bash_completion`. This can also be found in etc/bash_completion in the current directory. Or can be generated with `pmotools-python --bash-completion`
Examples:

```bash
# bash completion for pmotools-python
# add the below to your ~/.bash_completion

_pmotools_python_complete()
{
# Make sure underscores (and =) are NOT treated as word breaks
# so options like --pmo_files or --file=path complete as one token.
local _OLD_WB="${COMP_WORDBREAKS-}"
COMP_WORDBREAKS="${COMP_WORDBREAKS//_/}"
COMP_WORDBREAKS="${COMP_WORDBREAKS//=}"

local cur prev
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"

# 1) Completing the command name (1st arg): list all commands
if [[ ${COMP_CWORD} -eq 1 ]]; then
# Our CLI prints machine-friendly list via --list-plain:
# "<command>\t<group>\t<help>"
local lines cmds
lines="$(${COMP_WORDS[0]} --list-plain 2>/dev/null)"
cmds="$(printf '%s\n' "${lines}" | awk -F'\t' '{print $1}')"
COMPREPLY=( $(compgen -W "${cmds}" -- "${cur}") )

# restore wordbreaks before returning
COMP_WORDBREAKS="$_OLD_WB"
return 0
fi

# 2) Completing flags for a leaf command: scrape leaf -h
if [[ "${cur}" == -* ]]; then
local helps opts
helps="$(${COMP_WORDS[0]} ${COMP_WORDS[1]} -h 2>/dev/null)"
# Pull out flag tokens and split comma-separated forms
# Keep underscores intact in the tokens.
opts="$(printf '%s\n' "${helps}" \
| sed -n 's/^[[:space:]]\{0,\}\(-[-[:alnum:]_][-[:alnum:]_]*\)\(, *-[[:alnum:]_][-[:alnum:]_]*\)\{0,\}.*/\1/p' \
| sed 's/, / /g')"
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )

COMP_WORDBREAKS="$_OLD_WB"
return 0
fi

# 3) Otherwise, fall back to filename completion for positional args
COMPREPLY=( $(compgen -f -- "${cur}") )

# restore original word breaks
COMP_WORDBREAKS="$_OLD_WB"
return 0
}

complete -F _pmotools_python_complete pmotools-python
pmotools-python validate_pmo --pmo my_data.pmo.json
pmotools-python combine_pmos --pmo_files run1.pmo.json run2.pmo.json --output combined.pmo.json
```

Commands are grouped by task:

| Group | Purpose |
| --- | --- |
| `convertors_to_json` | Convert tables and metadata into PMO inputs |
| `extractors_from_pmo` | Subset PMOs by metadata, samples, targets, or read counts |
| `pmo_to_table` | Export PMO contents to spreadsheets or other formats |
| `extract_basic_info_from_pmo` | Inspect metadata and counts |
| `working_with_multiple_pmos` | Combine PMOs |
| `validation` | Validate PMO files against the JSON schema |

See the [command-line manual](https://plasmogenepi.github.io/pmotools-python/commands/index.html) for full documentation of each command.

## Python library

Import pmotools in your own code to read, validate, write, and build PMO files:

```python
from pmotools.pmo_engine.pmo_reader import PMOReader
from pmotools.pmo_engine.pmo_checker import PMOChecker

pmo = PMOReader.read_in_pmo("example.pmo.json")

checker = PMOChecker()
checker.validate_pmo_json(pmo)
```

Core modules:

- `pmotools.pmo_engine` — read, write, validate, and export PMOs
- `pmotools.pmo_builder` — build PMOs from tables and metadata
- `pmotools.scripts` — the same logic used by the CLI

See the [API reference](https://plasmogenepi.github.io/pmotools-python/api/modules.html) for module-level documentation.

## Auto completion

To enable bash tab completion for `pmotools-python`, append the script from `etc/bash_completion` to your `~/.bash_completion`, or generate it with:

```bash
pmotools-python --bash-completion >> ~/.bash_completion
source ~/.bash_completion
```

## Developer Setup
Expand Down Expand Up @@ -144,3 +148,17 @@ make update_autodocs
make html
```
You can open the html to review changes.

### Releasing pmotools-python

To release pmotools-python you need to update `version` in `pyproject.toml`, adhering to semantic versioning conventions.

**Note:** It is not always the case, but sometimes a release of pmotools-python will coincide with a new schema version. See the section below for notes on updating the schema version.

Once you have merged these updates into the `main` branch, create a new release with notes. GitHub actions will automatically deploy to PyPi.

### Updating the schema version

You can release pmotools-python without updating the schema version. However, for the schema to be updated and become the default within pmotools-python, you must update the schema and then do a release of pmotools-python by following the instructions above.

Update `__schema_version__` in `src/pmotools/__init__.py` to the new version of the schema. Do not include the `v` here (e.g. 1.0.0 not v1.0.0).
6 changes: 5 additions & 1 deletion man/source/conf.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
import sys
from pmotools import get_pmotools_version

sys.path.insert(0, os.path.abspath("../../src"))


# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
Expand All @@ -14,7 +16,9 @@
project = "pmotools-python"
copyright = "2026, Nicholas Hathaway, Kathryn Murie"
author = "Nicholas Hathaway, Kathryn Murie"
release = "v1.1.0"

release = get_pmotools_version() # full version, e.g. "v1.1.0"
version = release # use the full string everywhere

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
1 change: 1 addition & 0 deletions man/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

Welcome to pmotools-python's documentation!
===========================================
**Version:** |version|

.. toctree::
:maxdepth: 2
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pmotools"
version = "v1.0.0"
version = "v1.1.0"
description = "Tools for building and analyzing PMO files"
readme = "README.md"
authors = [
Expand Down
2 changes: 1 addition & 1 deletion src/pmotools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

#: Version of the PMO schema this package targets. Single source of truth;
#: bump when the schema changes. Independent of the package release version.
__schema_version__ = "1.0.0"
__schema_version__ = "1.1.0"

try:
# Distribution version from installed metadata (matches [project].version)
Expand Down
20 changes: 6 additions & 14 deletions tests/test_pmo_builder/test_merge_to_pmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@
class TestMergeToPMO(unittest.TestCase):
def setUp(self):
self.ref_list = [{"name": "name1"}, {"name": "name2"}, {"name": "name3"}]
self.pmo_header_v1_0_0 = {
"pmo_version": "1.0.0",
"creation_date": "2025-07-22",
"generation_method": {
"program_name": "pmotools-python",
"program_version": "1.0.0",
},
}
self.pmo_header_v1_1_0 = {
"pmo_version": "1.1.0",
"creation_date": "2025-07-22",
Expand Down Expand Up @@ -299,10 +291,10 @@ def test_merge_to_pmo_minimum_info_raise_multi_panel_no_lib(self):
def test_generate_pmo_header(self, mock_date):
mock_date.today.return_value = date(2025, 7, 22)
mock_date.side_effect = lambda *args, **kwargs: date(*args, **kwargs)
actual = _generate_pmo_header("1.0.0")
# expected = {'pmo_version': '1.0.0', 'creation_date': '2025-07-22', 'generation_method': {
# 'program_name': 'pmotools-python', 'program_version': '1.0.0'}}
self.assertEqual(actual, self.pmo_header_v1_0_0)
actual = _generate_pmo_header(
pmotools_version="1.1.0", pmo_schema_version="1.1.0"
)
self.assertEqual(actual, self.pmo_header_v1_1_0)

def test_replace_key_with_id(self):
test_target_list = [
Expand Down Expand Up @@ -357,7 +349,7 @@ def test_make_lookup(self):
@patch("pmotools.pmo_builder.merge_to_pmo._replace_names_with_IDs")
@patch("pmotools.pmo_builder.merge_to_pmo._generate_pmo_header")
def test_merge_to_pmo(self, mock_generate_pmo_header, _):
mock_generate_pmo_header.return_value = self.pmo_header_v1_0_0
mock_generate_pmo_header.return_value = self.pmo_header_v1_1_0
actual = merge_to_pmo(
specimen_info=[{"specimens": "specinfo"}],
library_sample_info=[{"library_samples": "library_samples"}],
Expand All @@ -376,7 +368,7 @@ def test_merge_to_pmo(self, mock_generate_pmo_header, _):
)

expected = {
"pmo_header": self.pmo_header_v1_0_0,
"pmo_header": self.pmo_header_v1_1_0,
"library_sample_info": [{"library_samples": "library_samples"}],
"specimen_info": [{"specimens": "specinfo"}],
"sequencing_info": [{"sequencing": "sequencing"}],
Expand Down
Loading