Repository: cogent3/cogent3 (language: python)

Issue:
Bump numpy from 2.0.0 to 2.0.1
Bumps [numpy](https://github.com/numpy/numpy) from 2.0.0 to 2.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/numpy/numpy/releases">numpy's releases</a>.</em></p>
<blockquote>
<h2>v2.0.1</h2>
<h1>NumPy 2.0.1 Release Notes</h1>
<p>NumPy 2.0.1 is a maintenance release that fixes bugs and regressions
discovered after the 2.0.0 release. NumPy 2.0.1 is the last planned
release in the 2.0.x series, 2.1.0rc1 should be out shortly.</p>
<p>The Python versions supported by this release are 3.9-3.12.</p>
<p><strong><em>NOTE:</em></strong>  Do not use the GitHub generated &quot;Source code&quot; files listed in the &quot;Assets&quot;, they are garbage.</p>
<h2>Improvements</h2>
<h3><code>np.quantile</code> with method <code>closest_observation</code> chooses nearest even order statistic</h3>
<p>This changes the definition of nearest for border cases from the nearest
odd order statistic to nearest even order statistic. The numpy
implementation now matches other reference implementations.</p>
<p>(<a href="https://redirect.github.com/numpy/numpy/pull/26656">gh-26656</a>)</p>
<h2>Contributors</h2>
<p>A total of 15 people contributed to this release. People with a &quot;+&quot; by
their names contributed a patch for the first time.</p>
<ul>
<li><a href="https://github.com/vahidmech"><code>@​vahidmech</code></a> +</li>
<li>Alex Herbert +</li>
<li>Charles Harris</li>
<li>Giovanni Del Monte +</li>
<li>Leo Singer</li>
<li>Lysandros Nikolaou</li>
<li>Matti Picus</li>
<li>Nathan Goldbaum</li>
<li>Patrick J. Roddy +</li>
<li>Raghuveer Devulapalli</li>
<li>Ralf Gommers</li>
<li>Rostan Tabet +</li>
<li>Sebastian Berg</li>
<li>Tyler Reddy</li>
<li>Yannik Wicke +</li>
</ul>
<h2>Pull requests merged</h2>
<p>A total of 24 pull requests were merged for this release.</p>
<ul>
<li><a href="https://redirect.github.com/numpy/numpy/pull/26711">#26711</a>: MAINT: prepare 2.0.x for further development</li>
<li><a href="https://redirect.github.com/numpy/numpy/pull/26792">#26792</a>: TYP: fix incorrect import in <code>ma/extras.pyi</code> stub</li>
<li><a href="https://redirect.github.com/numpy/numpy/pull/26793">#26793</a>: DOC: Mention '1.25' legacy printing mode in <code>set_printoptions</code></li>
<li><a href="https://redirect.github.com/numpy/numpy/pull/26794">#26794</a>: DOC: Remove mention of NaN and NAN aliases from constants</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/numpy/numpy/commit/4c9f4316aaf8e80d9d18f089d1267d9cd74d0192"><code>4c9f431</code></a> Merge pull request <a href="https://redirect.github.com/numpy/numpy/issues/27000">#27000</a> from charris/prepare-2.0.1</li>
<li><a href="https://github.com/numpy/numpy/commit/0e70e003121b4530fd32294f5cf953b1b400dcc7"><code>0e70e00</code></a> REL: Prepare for the NumPy 2.0.1 release [wheel build]</li>
<li><a href="https://github.com/numpy/numpy/commit/4d10ffcaab66ce47fc730034858cba9a95b359ce"><code>4d10ffc</code></a> Merge pull request <a href="https://redirect.github.com/numpy/numpy/issues/26995">#26995</a> from charris/backport-26985</li>
<li><a href="https://github.com/numpy/numpy/commit/764b6674912542b7b89d6faf0394e10049ed91d0"><code>764b667</code></a> BUG: Add object cast to avoid warning with limited API</li>
<li><a href="https://github.com/numpy/numpy/commit/9be6ad64c03ce91395fc3f6ee8d25f5e09174869"><code>9be6ad6</code></a> Merge pull request <a href="https://redirect.github.com/numpy/numpy/issues/26971">#26971</a> from charris/backport-26935</li>
<li><a href="https://github.com/numpy/numpy/commit/6d950e9af15d52c47f8e31141b206808fd665b72"><code>6d950e9</code></a> BUG: fix f2py tests to work with v2 API</li>
<li><a href="https://github.com/numpy/numpy/commit/89630c07a7c2af19c719453f6839ee7e009ed90d"><code>89630c0</code></a> Merge pull request <a href="https://redirect.github.com/numpy/numpy/issues/26962">#26962</a> from charris/backport-26919</li>
<li><a href="https://github.com/numpy/numpy/commit/88fa84092921dd5f9f41d680ca2b00aedf4ab40e"><code>88fa840</code></a> TST: Apply test suggestion by Nathan for rlstrip fixes</li>
<li><a href="https://github.com/numpy/numpy/commit/a9da01e11717f83bcb96a01d1ebed132f3cb938d"><code>a9da01e</code></a> BUG,MAINT: Fix utf-8 character stripping memory access</li>
<li><a href="https://github.com/numpy/numpy/commit/6afbbf8f3c8efa3c5c480e922a4904821e43812a"><code>6afbbf8</code></a> Merge pull request <a href="https://redirect.github.com/numpy/numpy/issues/26963">#26963</a> from charris/backport-26930</li>
<li>Additional commits viewable in <a href="https://github.com/numpy/numpy/compare/v2.0.0...v2.0.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=numpy&package-manager=pip&previous-version=2.0.0&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>
cookbook for apps
Apps need documentation!! This should be structured based on the type of apps, i.e., loaders, filtering steps, data sampling apps. 

- reorganise the current app documentation

### todo: cookbook
- [x] io
- [ ] dist
- [ ] tree 
- [ ] align
- [ ] evo 
- [ ] translate 
- [x] sample 

### todo: docstrings
- [ ] io
- [ ] dist
- [ ] tree 
- [ ] align
- [ ] evo 
- [x] translate 
- [x] sample 

Add big announcement about plugins to readme etc...



Discussion / hints:

related to #1378 
Following up on how to include code snippets, here is the [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html#examples) description of how they should be formatted.
Running `pytest` catches the code snippets in a simple example by adding:

- The code snippet in the docstring, with a pass and fail conditions
https://github.com/fredjaya/DemoProject/blob/ea776bc185b1cd252068619e2f0862a0d34b7c78/src/myproject/cli.py#L94-L101

- doctest options in the .toml
https://github.com/fredjaya/DemoProject/blob/ea776bc185b1cd252068619e2f0862a0d34b7c78/pyproject.toml?plain=1#L53-L55

Output is:
```
================ test session starts =================
platform linux -- Python 3.11.6, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/frederickjaya/GitHub/DemoProject
configfile: pyproject.toml
testpaths: tests, src/myproject
plugins: anyio-4.1.0, cov-4.1.0, xdist-3.5.0
collected 3 items                                    

tests/test_myproject.py ..                     [ 66%]
src/myproject/cli.py F                         [100%]

====================== FAILURES ======================
_______ [doctest] myproject.cli._parse_csv_arg _______
095 
096     >>> _parse_csv_arg("test")
097     ['test']
098     >>> _parse_csv_arg("test")
Expected:
    ['fail']
Got:
    ['test']

/home/frederickjaya/mambaforge/envs/c311/lib/python3.11/site-packages/myproject/cli.py:98: DocTestFailure
============== short test summary info ===============
FAILED src/myproject/cli.py::myproject.cli._parse_csv_arg

```
Thanks @fredjaya ! I'm going to create another issue to trial this for one of the apps.


Fix patch:
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index 432824952..5036fd424 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -1,4 +1,4 @@
-name: Format code using black and isort
+name: Lint code using ruff
 # modified from https://towardsdatascience.com/black-with-git-hub-actions-4ffc5c61b5fe
 # caching https://stackoverflow.com/questions/59127258/how-can-i-use-pip-cache-in-github-actions
 # black integration https://black.readthedocs.io/en/stable/integrations/github_actions.html
@@ -23,21 +23,15 @@ jobs:
         run: |
           python -m pip install --upgrade pip
           python -m pip install tomli
-          export click_version=$(python -c 'import tomli; print([line for line in tomli.load(open("pyproject.toml","rb"))["project"]["optional-dependencies"]["dev"] if "click" in line][0])')
-          export black_version=$(python -c 'import tomli; print([line for line in tomli.load(open("pyproject.toml","rb"))["project"]["optional-dependencies"]["dev"] if "black" in line][0])')
-          export isort_version=$(python -c 'import tomli; print([line for line in tomli.load(open("pyproject.toml","rb"))["project"]["optional-dependencies"]["dev"] if "isort" in line][0])')
-          echo "Click version: $click_version"
-          echo "Black version: $black_version"
-          echo "Isort version: $isort_version"
-          python -m pip install $click_version $black_version $isort_version
-      - name: Format code using black
-        run: black .
-      - name: Sort import statements using isort
-        run: isort .
+          export ruff_version=$(python -c 'import tomli; print([line for line in tomli.load(open("pyproject.toml","rb"))["project"]["optional-dependencies"]["dev"] if "ruff" in line][0])')
+          echo "Click version: ruff_version"
+          python -m pip install $ruff_version
+      - name: Format code using ruff
+        run: ruff check --select I --fix . && ruff format .
       - name: Commit changes
         uses: EndBug/add-and-commit@v9
         with:
           author_name: ${{ github.actor }}
           author_email: ${{ github.actor }}@users.noreply.github.com
-          message: "STY: pre-commit linting with black and isort"
+          message: "STY: pre-commit linting with ruff"
           add: "."
diff --git a/.hgtags b/.hgtags
index c13456379..3db71fd73 100644
--- a/.hgtags
+++ b/.hgtags
@@ -38,3 +38,4 @@ d77104f69e616ed3b077188540901cdb5d968966 2023.9.22a1
 0201d844deb847beceedefd288d82d235766c617 2023.12.15a1
 b8b3912265bd186f28d9ba90d79acac37f09d158 2024.2.5a1
 a726f3d6e29e43a9f7a114b066d146e8e8c62f92 2024.5.7a1
+67f6cf3a56665d9bd52a1d69b0a4e24f275da188 2024.7.19a1
diff --git a/README.md b/README.md
index 2cd8742ef..0aef00d5a 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,37 @@
 
 `cogent3` is a mature python library for analysis of genomic sequence data. We endeavour to provide a first-class experience within Jupyter notebooks, but the algorithms also support parallel execution on compute systems with 1000's of processors.
 
+## 📣 Feature Announcements 📣
+
+<details>
+  <summary> Supporting third-party apps as plugins 🔌 </summary>
+
+Cogent3 now provides support for plugins! Third-party developers can deploy their code as cogent3 apps with just a few lines. See the [demo project](https://github.com/cogent3/app_template).
+
+Post any questions you have in [cogent3 discussions](https://github.com/cogent3/cogent3/discussions).
+
+</details>
+
+<details>
+  <summary> The developers of Cogent3 and IQ-TREE2 announce piqtree2 🎉 </summary>
+
+Speaking of plugins, our first major third-party plugin is [piqtree2](https://pypi.org/project/piqtree2). Try it out and [give us feedback](https://github.com/iqtree/piqtree2/discussions).
+
+</details>
+
+<details>
+  <summary> New core data types improve efficiency and flexibility </summary>
+
+The cogent3 development team 👾 are hard at work modernising the core internals 💪🛠.
+
+In this release the `Sequence`, `SequenceCollection`, `MolType`, `GeneticCode`, and alphabet classes have all been rewritten from scratch with an eye to simplifying the code while improving its flexibility and performance. (We're working on alignments for the next release.)
+
+The "new-style" objects enhance performance by supporting the access of the underlying data in various formats (i.e. numpy arrays, bytes or strings). You can create "new-style" objects by setting the `new_type=True` argument in top-level functions (`make_seq`, `load_seq`, `make_unaligned_seqs`, `get_moltype`, `get_code`). These are not yet the default and are not fully integrated into the existing code. They can also differ in their API relative to the classes they replace. 
+
+We encourage experimentation in cases where integration with old objects is NOT required and [look forward to any feedback](https://github.com/cogent3/cogent3/discussions)!
+
+</details>
+
 ## Who is it for?
 
 ### Anyone who wants to analyse sequence divergence using robust statistical models
diff --git a/changelog.d/20240528_173634_Gavin.Huttley.md b/changelog.d/20240528_173634_Gavin.Huttley.md
deleted file mode 100644
index 8d7a6b7ec..000000000
--- a/changelog.d/20240528_173634_Gavin.Huttley.md
+++ /dev/null
@@ -1,43 +0,0 @@
-<!--
-A new scriv changelog fragment.
-
-Uncomment the section that is right (remove the HTML comment wrapper).
--->
-
-<!--
-### Contributors
-
-- A bullet item for the Contributors category.
-
--->
-### ENH
-
-- Defined an abstract base class for Gff records and support users providing
-  their own class via the cogent3.parse.gff.gff_parser(make_record) argument.
-  This enables users to devise a custom implementation tuned to the features
-  of different provider gff data.
-
-<!--
-### BUG
-
-- A bullet item for the BUG category.
-
--->
-<!--
-### DOC
-
-- A bullet item for the DOC category.
-
--->
-<!--
-### Deprecations
-
-- A bullet item for the Deprecations category.
-
--->
-<!--
-### Discontinued
-
-- A bullet item for the Discontinued category.
-
--->
diff --git a/changelog.d/20240716_142129_u6096186_seq_collections_refactor.md b/changelog.d/20240716_142129_u6096186_seq_collections_refactor.md
deleted file mode 100644
index 94504f354..000000000
--- a/changelog.d/20240716_142129_u6096186_seq_collections_refactor.md
+++ /dev/null
@@ -1,48 +0,0 @@
-<!--
-A new scriv changelog fragment.
-
-Uncomment the section that is right (remove the HTML comment wrapper).
--->
-### Contributors
-
-- katherine caley (@KatherineCaley)
-- fred jaya (@fredjaya)
-- gavin huttley (@GavinHuttley)
-
-### ENH
-
-- Introduced alpha versions of new implementations of core objects: `Sequence`,
-  `SequenceCollection`, `MolType`, `GeneticCode`, and `Alphabet`. These
-  "new-style" objects enhance performance by supporting the access of the
-  underlying data in various formats (i.e. numpy arrays, bytes or strings). The
-  "new-style" objects can be accessed by setting the `new_type=True` argument
-  in top-level functions (`make_seq`, `load_seq`, `make_unaligned_seqs`,
-  `get_moltype`, `get_code`). These are not yet the default and are not fully
-  integrated into the existing code, however, we encourage experimentation in
-  cases where integration with old objects is NOT required and look forward to
-  any feedback!
-
-<!--
-### BUG
-
-- A bullet item for the BUG category.
-
--->
-<!--
-### DOC
-
-- A bullet item for the DOC category.
-
--->
-<!--
-### Deprecations
-
-- A bullet item for the Deprecations category.
-
--->
-<!--
-### Discontinued
-
-- A bullet item for the Discontinued category.
-
--->
diff --git a/changelog.md b/changelog.md
index 858b33be3..a0339604c 100755
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,40 @@
 
+<a id='changelog-2024.7.19a1'></a>
+# Changes in release "2024.7.19a1"
+
+## Contributors
+
+- Contributions from @YapengLang, @KatherineCaley, @rmcar17, @fredjaya,
+  @GavinHuttley to cogent3 issues, code AND code reviews!
+- Contributions from @GavinHuttley and @khiron to Cogent3 repo
+  maintenance, developer tooling, issues etc.. related to the SMBE2024
+  workshop!
+
+## ENH
+
+- Defined an abstract base class for Gff records and support users providing
+  their own class via the cogent3.parse.gff.gff_parser(make_record) argument.
+  This enables users to devise a custom implementation tuned to the features
+  of different provider gff data.
+- Contributions from @KatherineCaley, @fredjaya and @GavinHuttley on massive
+  refactor of core objects concerning genetic codes, molecular types, 
+  alphabets, sequences and sequence collections ... huge efforts 💪 🚀 👷🏼‍♀️!
+- Introduced alpha versions of new implementations of core objects: `Sequence`,
+  `SequenceCollection`, `MolType`, `GeneticCode`, and `Alphabet`. These
+  "new-style" objects enhance performance by supporting the access of the
+  underlying data in various formats (i.e. numpy arrays, bytes or strings). The
+  "new-style" objects can be accessed by setting the `new_type=True` argument
+  in top-level functions (`make_seq`, `load_seq`, `make_unaligned_seqs`,
+  `get_moltype`, `get_code`). These are not yet the default and are not fully
+  integrated into the existing code, however, we encourage experimentation in
+  cases where integration with old objects is NOT required and look forward to
+  any feedback!
+
+## BUG
+
+- @YapengLang identified and fixed a bug in `cogent3.app.evo.model` where
+  settings for upper / lower bounds where being ignored.
+
 <a id='changelog-2024.5.7a1'></a>
 # Changes in release "2024.5.7a1"
 
diff --git a/doc/examples/demo-mpi-parallel.py b/doc/examples/demo-mpi-parallel.py
index 15920ea89..638e69726 100644
--- a/doc/examples/demo-mpi-parallel.py
+++ b/doc/examples/demo-mpi-parallel.py
@@ -1,12 +1,10 @@
 import math
 import os
 import time
-
 from collections import Counter
 
 from cogent3.util import parallel
 
-
 # the following environment variable is created by PBS on job execution
 PBS_NCPUS = os.environ.get("PBS_NCPUS", None)
 if PBS_NCPUS is None:
diff --git a/doc/examples/demo-multiprocess-parallel.py b/doc/examples/demo-multiprocess-parallel.py
index 7f5ec1f49..3cb36c1fd 100644
--- a/doc/examples/demo-multiprocess-parallel.py
+++ b/doc/examples/demo-multiprocess-parallel.py
@@ -1,6 +1,5 @@
 import math
 import time
-
 from collections import Counter
 
 from cogent3.util import parallel
diff --git a/doc/rst2script.py b/doc/rst2script.py
index bb2c00b13..f88fe5dd6 100755
--- a/doc/rst2script.py
+++ b/doc/rst2script.py
@@ -2,12 +2,12 @@
 """
 This takes doctest files and turns them into standalone scripts.
 """
+
 import pathlib
 import re
 
 import click
 
-
 not_wsp = re.compile(r"\S+")
 code_eval = re.compile(r"(?<=\s{4})([>]{3}|[.]{3})\s")
 code_block = re.compile(r".. (jupyter-execute|doctest)::")
diff --git a/doc/set_working_directory.py b/doc/set_working_directory.py
index 5a157d7f3..30caa1737 100644
--- a/doc/set_working_directory.py
+++ b/doc/set_working_directory.py
@@ -5,7 +5,6 @@
 import os
 import pathlib
 
-
 current = pathlib.Path(__file__).absolute().parent
 
 
diff --git a/noxfile.py b/noxfile.py
index 5b6449706..5d73d09dd 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -2,7 +2,6 @@
 
 import nox
 
-
 _py_versions = range(9, 13)
 
 
diff --git a/pyproject.toml b/pyproject.toml
index 7eeaf89fa..1180068fb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,15 +6,14 @@ build-backend = "flit_core.buildapi"
 name = "cogent3"
 description = """COmparative GENomics Toolkit 3: genomic sequence analysis within notebooks or on compute systems with 1000s of CPUs."""
 # remember to update cogent3/_version.py too!
-version = "2024.5.7a1"
+version = "2024.7.19a1"
 authors = [
     { name = "Gavin Huttley", email = "Gavin.Huttley@anu.edu.au"},
 ]
 keywords = ["biology", "genomics", "statistics", "phylogeny", "evolution", "bioinformatics"]
 readme = "README.md"
 license = { file = "LICENSE" }
-dependencies = ["chardet",
-        # until we know all dependencies work on numpy 2, pin to lesser version
+dependencies = ["charset-normalizer",
         "numpy",
         "numba>0.53; python_version>='3.9' and python_version <'3.12'",
         "numba>0.54; python_version>='3.10' and python_version <'3.12'",
@@ -48,9 +47,7 @@ Changelog = "https://github.com/cogent3/cogent3/blob/develop/changelog.md"
 
 [project.optional-dependencies]
 test = [
-    "black==24.4.2",
-    "isort==5.13.2",
-    "click==8.1.7", # black needs this pinned
+    "ruff==0.5.4",
     "kaleido",
     "pandas",
     "plotly",
@@ -69,7 +66,6 @@ doc  = ["click==8.1.7",
         "jupyterlab",
         "jupytext",
         "kaleido",
-        "matplotlib",
         "nbconvert>5.4",
         "nbformat",
         "nbsphinx",
@@ -85,19 +81,16 @@ doc  = ["click==8.1.7",
         "sphinxcontrib-bibtex",
         "sphinxcontrib-video"]
 extra = ["pandas", "plotly", "psutil", "kaleido"]
-dev = ["black==24.4.2",
-       "click==8.1.7",
+dev = ["ruff==0.5.4",
        "flit",
        "ipykernel",
        "ipython",
        "ipywidgets",
-       "isort==5.13.2",
        "jupyter-sphinx",
        "jupyter_client",
        "jupyterlab",
        "jupytext",
        "kaleido",
-       "matplotlib",
        "nbconvert>5.4",
        "nbformat",
        "nbsphinx",
@@ -139,33 +132,93 @@ filterwarnings = [
     'ignore:Model not reversible',
 ]
 
-[tool.black]
+[tool.ruff]
+exclude = [
+    "iqtree2",
+    ".bzr",
+    ".direnv",
+    ".eggs",
+    ".git",
+    ".git-rewrite",
+    ".hg",
+    ".ipynb_checkpoints",
+    ".mypy_cache",
+    ".nox",
+    ".pants.d",
+    ".pyenv",
+    ".pytest_cache",
+    ".pytype",
+    ".ruff_cache",
+    ".svn",
+    ".tox",
+    ".venv",
+    ".vscode",
+    "__pypackages__",
+    "_build",
+    "buck-out",
+    "build",
+    "dist",
+    "node_modules",
+    "site-packages",
+    "venv",
+]
+
+# Same as Black.
 line-length = 88
-target-version = ["py39", "py310", "py311", "py312"]
-exclude = '''
-/(
-    \.eggs
-  | \.git
-  | \.hg
-  | \.mypy_cache
-  | \.nox
-  | \.venv
-  | _build
-  | build
-  | dist
-  | tests/data
-)/
-'''
-
-[tool.isort]
-atomic=true
-force_grid_wrap=0
-include_trailing_comma=true
-lines_after_imports=2
-lines_between_types=1
-multi_line_output=3
-use_parentheses=true
-src_paths = ["src", "tests","doc","noxfile.py"]
+indent-width = 4
+
+target-version = "py39"
+
+[tool.ruff.lint]
+# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`)  codes by default.
+# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
+# McCabe complexity (`C901`) by default.
+select = ["ALL"]
+ignore = ["EXE002", "FA100", "E501", "D"]
+
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**/*.py" = [
+    "S101", # asserts allowed in tests...
+    "INP001", # __init__.py files are not required...
+    "ANN",
+    "N802",
+    "N803"
+]
+"noxfile.py" = [
+    "S101", # asserts allowed in tests...
+    "INP001", # __init__.py files are not required...
+    "ANN",
+    "N802",
+    "N803"
+]
+
+[tool.ruff.format]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "lf"
+docstring-code-format = true
+
+# Set the line length limit used when formatting code snippets in
+# docstrings.
+#
+# This only has an effect when the `docstring-code-format` setting is
+# enabled.
+docstring-code-line-length = "dynamic"
 
 [tool.scriv]
 format="md"
diff --git a/requirements.txt b/requirements.txt
index 2b6fca445..3187b8571 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,12 @@
 chardet==5.2.0
-pillow==10.3.0
-psutil==5.9.8
-numpy==2.0.0
+pillow==10.4.0
+psutil==6.0.0
+numpy==2.0.1
 numba>0.48.0;python_version<'3.9'
 numba>0.53; python_version>='3.9'
 numba>0.54; python_version>='3.10'
 numba>=0.57.0; python_version=='3.11'
-scipy==1.13.1
+scipy==1.14.0
 nox==2024.4.15
 plotly==5.22.0
 typing_extensions>=3.7
diff --git a/src/cogent3/__init__.py b/src/cogent3/__init__.py
index a4890d38d..cc934d1b0 100644
--- a/src/cogent3/__init__.py
+++ b/src/cogent3/__init__.py
@@ -7,7 +7,6 @@
 import pickle
 import sys
 import warnings
-
 from typing import Callable, Optional, Union
 
 from cogent3._version import __version__
@@ -20,6 +19,7 @@
 )
 from cogent3.core.annotation_db import load_annotations
 from cogent3.core.genetic_code import available_codes, get_code
+
 # note that moltype has to be imported last, because it sets the moltype in
 # the objects created by the other modules.
 from cogent3.core.moltype import (
@@ -46,7 +46,6 @@
 from cogent3.util.table import Table as _Table
 from cogent3.util.table import cast_str_to_array
 
-
 __copyright__ = "Copyright 2007-2023, The Cogent Project"
 __credits__ = "https://github.com/cogent3/cogent3/graphs/contributors"
 __license__ = "BSD-3"
@@ -72,7 +71,6 @@
 
 import logging
 
-
 # suppress numba warnings
 __numba_logger = logging.getLogger("numba")
 __numba_logger.setLevel(logging.WARNING)
diff --git a/src/cogent3/_version.py b/src/cogent3/_version.py
index b8ce52858..ff1693f0f 100644
--- a/src/cogent3/_version.py
+++ b/src/cogent3/_version.py
@@ -1,2 +1,2 @@
 # remember to update version in pyproject.toml too!
-__version__ = "2024.5.7a1"
+__version__ = "2024.7.19a1"
diff --git a/src/cogent3/align/__init__.py b/src/cogent3/align/__init__.py
index 777065e10..1fe34776d 100644
--- a/src/cogent3/align/__init__.py
+++ b/src/cogent3/align/__init__.py
@@ -9,7 +9,6 @@
     make_generic_scoring_dict,
 )
 
-
 __all__ = [
     "align",
     "dp_calculation",
diff --git a/src/cogent3/align/pairwise_pogs_numba.py b/src/cogent3/align/pairwise_pogs_numba.py
index d5bfc5f01..02a830bbf 100644
--- a/src/cogent3/align/pairwise_pogs_numba.py
+++ b/src/cogent3/align/pairwise_pogs_numba.py
@@ -1,9 +1,7 @@
 import numpy as np
-
 from numba import boolean, float64, int64, njit, optional, uint8
 from numba.core.types.containers import Tuple
 
-
 # turn off code coverage as njit-ted code not accessible to coverage
 
 
diff --git a/src/cogent3/align/pairwise_seqs_numba.py b/src/cogent3/align/pairwise_seqs_numba.py
index b4cf3418c..4a4e38556 100644
--- a/src/cogent3/align/pairwise_seqs_numba.py
+++ b/src/cogent3/align/pairwise_seqs_numba.py
@@ -1,9 +1,7 @@
 import numpy as np
-
 from numba import boolean, float64, int64, njit, optional, uint8
 from numba.core.types.containers import Tuple
 
-
 # turn off code coverage as njit-ted code not accessible to coverage
 
 
diff --git a/src/cogent3/align/pycompare.py b/src/cogent3/align/pycompare.py
index 4abcacdb4..7af6983b3 100644
--- a/src/cogent3/align/pycompare.py
+++ b/src/cogent3/align/pycompare.py
@@ -435,8 +435,9 @@ def _get_segments(self, min_gap):
             for x, y in self.paths[y_intercept][1:]:
                 last_x, last_y = result[-1]
                 if len(x - last_x) <= min_gap:
-                    result[-1] = last_x.merge(x, strict=False), last_y.merge(
-                        y, strict=False
+                    result[-1] = (
+                        last_x.merge(x, strict=False),
+                        last_y.merge(y, strict=False),
                     )
                 else:
                     result.append((x, y))
diff --git a/src/cogent3/align/traceback.py b/src/cogent3/align/traceback.py
index 39e27bbcf..3402ba34b 100644
--- a/src/cogent3/align/traceback.py
+++ b/src/cogent3/align/traceback.py
@@ -6,7 +6,6 @@
 from cogent3.core.alignment import Aligned, Alignment
 from cogent3.core.location import IndelMap
 
-
 IntOrNone = typing.Union[int, None]
 IntListType = list[int]
 CoordsListType = list[list[typing.Sequence[int]]]
@@ -37,7 +36,7 @@ def seq_traceback(s1, s2, aligned_positions, gap_value):
 
 
 def gap_traceback(
-    aligned_positions: list[list[IntOrNone, IntOrNone]]
+    aligned_positions: list[list[IntOrNone, IntOrNone]],
 ) -> tuple[IntListType, IntListType, CoordsListType, int]:
     """gap Vectors from state matrix and ending point"""
     consuming = [False, False]
@@ -64,7 +63,7 @@ def gap_traceback(
 
 
 def map_traceback(
-    aligned_positions: list[list[IntOrNone, IntOrNone]]
+    aligned_positions: list[list[IntOrNone, IntOrNone]],
 ) -> tuple[IntListType, IntListType, list[IndelMap]]:
     # using IndelMap's to keep track of gaps for indel alignment
     starts, ends, gap_vectors, alignment_len = gap_traceback(aligned_positions)
diff --git a/src/cogent3/app/__init__.py b/src/cogent3/app/__init__.py
index b31b065d9..a0fac0081 100644
--- a/src/cogent3/app/__init__.py
+++ b/src/cogent3/app/__init__.py
@@ -13,7 +13,6 @@
 from .composable import is_app, is_app_composable
 from .io import open_data_store
 
-
 # Entry_point for apps to register themselves as plugins
 APP_ENTRY_POINT = "cogent3.app"
 
diff --git a/src/cogent3/app/align.py b/src/cogent3/app/align.py
index 50145b98e..a2c0bdf64 100644
--- a/src/cogent3/app/align.py
+++ b/src/cogent3/app/align.py
@@ -1,5 +1,4 @@
 import warnings
-
 from bisect import bisect_left
 from itertools import combinations
 from typing import Optional, Union
@@ -36,7 +35,7 @@ class _GapOffset:
     --------
     From sequence coordinate to an alignment coordinate
 
-    >>> seq2aln = _GapOffset({1:3, 7:1})
+    >>> seq2aln = _GapOffset({1: 3, 7: 1})
     >>> seq_pos = 2
     >>> aln_pos = seq_pos + seq2aln[seq_pos]
     >>> aln_pos
@@ -44,7 +43,7 @@ class _GapOffset:
 
     From alignment coordinate to a sequence coordinate
 
-    >>> aln2seq = _GapOffset({1:3, 7:1}, invert=True)
+    >>> aln2seq = _GapOffset({1: 3, 7: 1}, invert=True)
     >>> seq_pos = aln_pos - aln2seq[aln_pos]
     >>> seq_pos
     2
@@ -467,15 +466,22 @@ def __init__(
         and an app for alignment with nucleotide model ``model="HKY85"``.
 
         >>> from cogent3 import make_unaligned_seqs, get_app
-        >>> aln = make_unaligned_seqs({
-        ... "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
-        ... "Bandicoot": "NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAAC",
-        ... "Rhesus": "GCCAGCTCATTACAGCATGAGAACAGTTTGTTACTCACT",
-        ... "FlyingFox": "GCCAGCTCTTTACAGCATGAGAACAGTTTATTATACACT"
-        ... }, moltype="dna")
+        >>> aln = make_unaligned_seqs(
+        ...     {
+        ...         "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
+        ...         "Bandicoot": "NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAAC",
+        ...         "Rhesus": "GCCAGCTCATTACAGCATGAGAACAGTTTGTTACTCACT",
+        ...         "FlyingFox": "GCCAGCTCTTTACAGCATGAGAACAGTTTATTATACACT",
+        ...     },
+        ...     moltype="dna",
+        ... )
         >>> app = get_app("progressive_align", model="HKY85")
         >>> result = app(aln)
-        >>> print(result.to_pretty(name_order=['Human', 'Bandicoot', 'Rhesus', 'FlyingFox']))
+        >>> print(
+        ...     result.to_pretty(
+        ...         name_order=["Human", "Bandicoot", "Rhesus", "FlyingFox"]
+        ...     )
+        ... )
             Human    GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT...
 
         Optionally, a pre-computed guide tree can be provided.
@@ -483,7 +489,11 @@ def __init__(
         >>> newick = "(Bandicoot:0.4,FlyingFox:0.05,(Rhesus:0.06," "Human:0.0):0.04);"
         >>> app_guided = get_app("progressive_align", model="HKY85", guide_tree=newick)
         >>> result = app_guided(aln)
-        >>> print(result.to_pretty(name_order=['Human', 'Bandicoot', 'Rhesus', 'FlyingFox']))
+        >>> print(
+        ...     result.to_pretty(
+        ...         name_order=["Human", "Bandicoot", "Rhesus", "FlyingFox"]
+        ...     )
+        ... )
             Human    GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT
         Bandicoot    NA.TCA.T.A.G.TTG.AACC.G...---......GTC..AC
            Rhesus    ..........................---...G.........
@@ -602,12 +612,12 @@ def __init__(
         """
         Parameters
         ----------
-        score_matrix : dict
+        score_matrix
             scoring dict, defaults to `make_dna_scoring_dict(10, -1, -8)` for
             DNA and `make_generic_scoring_dict(10, moltype)` for other moltype.
-        insertion_penalty : int
+        insertion_penalty
             penalty for gap insertion
-        extension_penalty : int
+        extension_penalty
             penalty for gap extension
         moltype
             molecular type of sequences, defaults to "dna"
@@ -617,6 +627,37 @@ def __init__(
         If the provided molecular type differs from the moltype of the
         SequenceCollection to be aligned, the sequences are converted to
         the provided moltype.
+
+        Examples
+        --------
+
+        Create and align two sequences using the Smith-Waterman algorithm.
+
+        >>> from cogent3 import make_unaligned_seqs, get_app
+        >>> seqs = make_unaligned_seqs(
+        ...     {
+        ...         "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
+        ...         "Bandicoot": "NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAAC",
+        ...     },
+        ...     moltype="dna",
+        ... )
+        >>> app = get_app("smith_waterman")
+        >>> result = app(seqs)
+        >>> print(result.to_pretty())
+            Human    AGCTCATTACAGCATGAGAACAGCAGTTTATTACTCA
+        Bandicoot    NA.......AT..T...A.C............GTC..
+
+        Align with a less stringent scoring matrix. i.e. for more divergent
+        sequences.
+
+        >>> from cogent3.align import make_dna_scoring_dict
+        >>> app = get_app(
+        ...     "smith_waterman", score_matrix=make_dna_scoring_dict(5, -2, -5)
+        ... )
+        >>> result = app(seqs)
+        >>> print(result.to_pretty())
+            Human    AGCTCATTACAGCATGAGAACAGCAGTTTATTACTCA
+        Bandicoot    NA.......AT..T...A.C............GTC..
         """
         self.moltype = get_moltype(moltype)
         self._score_matrix = score_matrix or (
@@ -687,7 +728,9 @@ def __init__(self, equifreq_mprobs=True):
         quality score. The default is equally frequent motif probabilities.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({"s1": "AATTGA", "s2": "AGGTCC", "s3": "AGGATG", "s4": "AGGCGT"})
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "AATTGA", "s2": "AGGTCC", "s3": "AGGATG", "s4": "AGGCGT"}
+        ... )
         >>> app = get_app("ic_score")
         >>> result = app(aln)
         >>> print(result)
@@ -759,12 +802,15 @@ def cogent3_score(aln: AlignedSeqsType) -> float:
     ``progressive_align()``.
 
     >>> from cogent3 import make_unaligned_seqs, get_app
-    >>> aln = make_unaligned_seqs({
-    ... "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
-    ... "Bandicoot": "NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAAC",
-    ... "Rhesus": "GCCAGCTCATTACAGCATGAGAACAGTTTGTTACTCACT",
-    ... "FlyingFox": "GCCAGCTCTTTACAGCATGAGAACAGTTTATTATACACT"
-    ... }, moltype="dna")
+    >>> aln = make_unaligned_seqs(
+    ...     {
+    ...         "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
+    ...         "Bandicoot": "NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAAC",
+    ...         "Rhesus": "GCCAGCTCATTACAGCATGAGAACAGTTTGTTACTCACT",
+    ...         "FlyingFox": "GCCAGCTCTTTACAGCATGAGAACAGTTTATTATACACT",
+    ...     },
+    ...     moltype="dna",
+    ... )
     >>> newick = "(Bandicoot:0.4,FlyingFox:0.05,(Rhesus:0.06," "Human:0.0):0.04);"
     >>> aligner = get_app("progressive_align", model="HKY85")
 
@@ -851,7 +897,9 @@ def __init__(
         Penalise gap extensions with ``gap_extend=1`` and insertions with
         ``gap_insert=2``.
 
-        >>> app_gap_penalty = get_app("sp_score", calc="pdist", gap_extend=1, gap_insert=2)
+        >>> app_gap_penalty = get_app(
+        ...     "sp_score", calc="pdist", gap_extend=1, gap_insert=2
+        ... )
         >>> result = app_gap_penalty(aln)
         >>> print(result)
         -13.0
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index 48d7fed47..f58723cf7 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -5,7 +5,6 @@
 import time
 import traceback
 import types
-
 from copy import deepcopy
 from enum import Enum
 from pathlib import Path
@@ -20,8 +19,12 @@
 from cogent3.util import progress_display as UI
 from cogent3.util.misc import docstring_to_summary_rest, get_object_provenance
 
-from .data_store import DataMember, get_data_source, get_unique_id
-
+from .data_store import (
+    DataMember,
+    DataStoreABC,
+    get_data_source,
+    get_unique_id,
+)
 
 _builtin_seqs = list, set, tuple
 
@@ -153,6 +156,21 @@ def _get_raw_hints(main_func, min_params):
     if depth > 2:
         raise TypeError(msg.format("return_type", return_type, depth))
 
+    if isinstance(first_param_type, str):
+        msg = (
+            "Apps do not yet support string type hints "
+            "(such as those caused by __future__ annotations). "
+            f"Bad type hint: {first_param_type}"
+        )
+        raise NotImplementedError(msg)
+    if isinstance(return_type, str):
+        msg = (
+            "Apps do not yet support string type hints "
+            "(such as those caused by __future__ annotations). "
+            f"Bad type hint: {return_type}"
+        )
+        raise NotImplementedError(msg)
+
     return first_param_type, return_type
 
 
@@ -541,10 +559,10 @@ class with the function bound as a static method.
     ...
     ...     def main(self, aln: AlignedSeqsType) -> T:
     ...         return aln.omit_bad_seqs(
-    ...                                 quantile=self.quantile,
-    ...                                 gap_fraction=self.gap_fraction,
-    ...                                 moltype=self.moltype
-    ...                                 )
+    ...             quantile=self.quantile,
+    ...             gap_fraction=self.gap_fraction,
+    ...             moltype=self.moltype,
+    ...         )
 
     ``drop_bad`` is a composable app with ``app_type=GENERIC``. The input
     data must be a sequence alignment instance. It returns the same type,
@@ -562,12 +580,12 @@ class with the function bound as a static method.
     >>> T = Union[AlignedSeqsType, SerialisableType]
     >>>
     >>> @define_app
-    ... def omit_seqs(aln: AlignedSeqsType, quantile=None, gap_fraction=1, moltype="dna") -> T:
+    ... def omit_seqs(
+    ...     aln: AlignedSeqsType, quantile=None, gap_fraction=1, moltype="dna"
+    ... ) -> T:
     ...     return aln.omit_bad_seqs(
-    ...                             quantile=quantile,
-    ...                             gap_fraction=gap_fraction,
-    ...                             moltype=moltype
-    ...                             )
+    ...         quantile=quantile, gap_fraction=gap_fraction, moltype=moltype
+    ...     )
 
     ``omit_seqs`` is now an app, allowing creating different variants which
     can be composed as per ones defined via a class.
@@ -687,6 +705,8 @@ def _as_completed(self, dstore, parallel=False, par_kw=None, **kwargs) -> Genera
 
     if isinstance(dstore, str):
         dstore = [dstore]
+    elif isinstance(dstore, DataStoreABC):
+        dstore = dstore.completed
     mapped = _proxy_input(dstore)
     if not mapped:
         return mapped
@@ -763,6 +783,8 @@ def _apply_to(
 
     if isinstance(dstore, (str, Path)):  # one filename
         dstore = [dstore]
+    elif isinstance(dstore, DataStoreABC):
+        dstore = dstore.completed
 
     # todo this should fail if somebody provides data that cannot produce a unique_id
     inputs = {}
diff --git a/src/cogent3/app/data_store.py b/src/cogent3/app/data_store.py
index 2854cdf82..c513949ef 100644
--- a/src/cogent3/app/data_store.py
+++ b/src/cogent3/app/data_store.py
@@ -7,7 +7,6 @@
 import re
 import reprlib
 import zipfile
-
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from enum import Enum
@@ -30,7 +29,6 @@
 from cogent3.util.parallel import is_master_process
 from cogent3.util.table import Table
 
-
 _NOT_COMPLETED_TABLE = "not_completed"
 _LOG_TABLE = "logs"
 _MD5_TABLE = "md5"
diff --git a/src/cogent3/app/dist.py b/src/cogent3/app/dist.py
index d9e12fcb3..64ad0df6e 100644
--- a/src/cogent3/app/dist.py
+++ b/src/cogent3/app/dist.py
@@ -1,11 +1,9 @@
 import itertools
-
 from copy import deepcopy
 from itertools import combinations
-from typing import Callable, Union
+from typing import Callable, Optional, Union
 
 import numpy
-
 from numpy import polyval, triu_indices
 
 from cogent3 import get_moltype, make_tree
@@ -25,7 +23,6 @@
     UnalignedSeqsType,
 )
 
-
 # The following coefficients are derived from a polynomial fit between Jaccard distance
 # and the proportion of different sites for mammalian DNA sequences. NOTE: the Jaccard
 # distance used kmers where k=10.
@@ -51,15 +48,22 @@ class fast_slow_dist:
 
     Uses fast (but less numerically robust) approach where possible, slow (robust)
     approach when not.
+
     """
 
-    def __init__(self, distance=None, moltype=None, fast_calc=None, slow_calc=None):
+    def __init__(
+        self,
+        distance: Optional[str] = None,
+        moltype: Optional[str] = None,
+        fast_calc: Optional[str] = None,
+        slow_calc: Optional[str] = None,
+    ):
         """
         Parameters
         ----------
-        moltype : str
+        moltype
             cogent3 moltype
-        distance : str
+        distance
             Name of a distance method available as both fast and slow calculator.
         fast_calc
             Name of a fast distance calculator. See cogent3.available_distances().
@@ -69,6 +73,33 @@ def __init__(self, distance=None, moltype=None, fast_calc=None, slow_calc=None):
         Notes
         -----
         If you provide fast_calc or slow_calc, you must specify the moltype.
+
+        Examples
+        --------
+
+        Create a sample alignment and app to calculate the pairwise hamming
+        distance.
+
+        >>> from cogent3 import make_aligned_seqs, get_app
+        >>> seqs = {
+        ...     "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
+        ...     "Bandicoot": "---NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAAC",
+        ...     "Rhesus": "GCCAGCTCATTACAGCATGAGAAC---AGTTTGTTACTCACT",
+        ...     "FlyingFox": "GCCAGCTCTTTACAGCATGAGAACAG---TTTATTATACACT",
+        ... }
+        >>> aln = make_aligned_seqs(seqs, moltype="dna")
+        >>> app = get_app("fast_slow_dist", distance="hamming", moltype="dna")
+        >>> result = app(aln)
+        >>> result.to_dict()
+        {('Bandicoot', 'FlyingFox'): 11.0, ('Bandicoot', 'Human'): 11.0, ('Bandicoot', 'Rhesus'): 12.0,...
+
+        Create an app with ``fast_calc="TN93"``.
+
+        >>> app = get_app("fast_slow_dist", fast_calc="TN93", moltype="dna")
+        >>> result = app(aln)
+        >>> result.to_dict()
+        {('Bandicoot', 'FlyingFox'): 0.4494289084991177,...
+
         """
         self._moltype = moltype if moltype is None else get_moltype(moltype)
         self._sm = None
@@ -180,10 +211,7 @@ def jaccard_dist(seq_coll: UnalignedSeqsType, k: int = 10) -> PairwiseDistanceTy
     {('s1', 's2'): 0.4, ('s2', 's1'): 0.4}
     """
 
-    kmers = {
-        name: set(seq_coll.named_seqs[name].iter_kmers(k, strict=True))
-        for name in seq_coll.names
-    }
+    kmers = {seq.name: set(seq.iter_kmers(k, strict=True)) for seq in seq_coll.seqs}
     seq_names = sorted(kmers.keys())
     num_seqs = len(seq_names)
 
@@ -353,6 +381,33 @@ def __init__(self, gap_insert: float = 12.0, gap_extend: float = 1.0):
             gap insertion penalty
         gap_extend
             gap extension penalty
+
+        Examples
+        --------
+
+        Create a sample alignment and app to calculate the pairwise difference in gaps.
+
+        >>> from cogent3 import make_aligned_seqs, get_app
+        >>> seqs = {
+        ...     "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
+        ...     "Bandicoot": "---NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAAC",
+        ...     "Rhesus": "GCCAGCTCATTACAGCATGAGAAC---AGTTTGTTACTCACT",
+        ...     "FlyingFox": "GCCAGCTCTTTACAGCATGAGAACAG---TTTATTATACACT",
+        ... }
+        >>> aln = make_aligned_seqs(seqs, moltype="dna")
+        >>> app = get_app("gap_dist")
+        >>> result = app(aln)
+        >>> result.to_dict()
+        {('Bandicoot', 'FlyingFox'): 30.0, ('Bandicoot', 'Human'): 15.0, ('Bandicoot', 'Rhesus'): 30.0,...
+
+        Calculate the pairwise difference in gaps with different ``gap_insert``
+        and ``gap_extend`` penalties.
+
+        >>> app = get_app("gap_dist", gap_insert=10, gap_extend=2.8)
+        >>> result = app(aln)
+        >>> result.to_dict()
+        {('Bandicoot', 'FlyingFox'): 36.8, ('Bandicoot', 'Human'): 18.4, ('Bandicoot', 'Rhesus'): 36.8,...
+
         """
         self._insert = gap_insert
         self._extend = gap_extend
diff --git a/src/cogent3/app/evo.py b/src/cogent3/app/evo.py
index ce5630151..33a70b420 100644
--- a/src/cogent3/app/evo.py
+++ b/src/cogent3/app/evo.py
@@ -2,7 +2,6 @@
 from typing import Callable, Iterable, Optional, Union
 
 import cogent3.util.io
-
 from cogent3 import load_tree, make_tree
 from cogent3.core.tree import TreeNode
 from cogent3.evolve.models import get_model
@@ -29,7 +28,8 @@
 )
 
 
-def _config_rules(param_rules, lower, upper):
+def _config_rules(param_rules, lower, upper, overwrite=False):
+    """fill the bounds in `param_rules` whenever no bound defined for a parameter"""
     param_rules = deepcopy(param_rules)
     for rule in param_rules:
         if rule.get("par_name", None) in (
@@ -41,8 +41,12 @@ def _config_rules(param_rules, lower, upper):
         ) or rule.get("is_constant"):
             continue
 
-        rule["lower"] = rule.get("lower", lower)  # default lower bound
-        rule["upper"] = rule.get("upper", upper)  # default upper bound
+        rule["lower"] = (
+            lower if overwrite else rule.get("lower", lower)
+        )  # default lower bound
+        rule["upper"] = (
+            upper if overwrite else rule.get("upper", upper)
+        )  # default upper bound
 
     return param_rules
 
@@ -140,13 +144,16 @@ def __init__(
         because we recommend it!)
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({
-        ...    "Human": "ATGCGGCTCGCGGAGGCCGCGCTCGCGGAG",
-        ...    "Mouse": "ATGCCCGGCGCCAAGGCAGCGCTGGCGGAG",
-        ...    "Opossum": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
-        ... })
-        >>> app = get_app("model", "F81", opt_args=dict(limit_action="ignore",
-        ... max_evaluations=10))
+        >>> aln = make_aligned_seqs(
+        ...     {
+        ...         "Human": "ATGCGGCTCGCGGAGGCCGCGCTCGCGGAG",
+        ...         "Mouse": "ATGCCCGGCGCCAAGGCAGCGCTGGCGGAG",
+        ...         "Opossum": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
+        ...     }
+        ... )
+        >>> app = get_app(
+        ...     "model", "F81", opt_args=dict(limit_action="ignore", max_evaluations=10)
+        ... )
         >>> result = app(aln)
         >>> result
         F81...
@@ -159,12 +166,14 @@ def __init__(
         string.
 
         >>> tree = "(Mouse,(Human,Gorilla),Opossum)"
-        >>> aln2 = make_aligned_seqs({
-        ...      "Human": "ATGCGGCTCGCGGAGGCCGCGCTCGCGGAG",
-        ...      "Gorilla": "ATGCGGCGCGCGGAGGCCGCGCTCGCGGAG",
-        ...      "Mouse": "ATGCCCGGCGCCAAGGCAGCGCTGGCGGAG",
-        ...      "Opossum": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
-        ... })
+        >>> aln2 = make_aligned_seqs(
+        ...     {
+        ...         "Human": "ATGCGGCTCGCGGAGGCCGCGCTCGCGGAG",
+        ...         "Gorilla": "ATGCGGCGCGCGGAGGCCGCGCTCGCGGAG",
+        ...         "Mouse": "ATGCCCGGCGCCAAGGCAGCGCTGGCGGAG",
+        ...         "Opossum": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
+        ...     }
+        ... )
         >>> app_tr = get_app("model", "F81", tree=tree)
 
         Or we could assign a function that estimates the tree for an alignment.
@@ -179,10 +188,10 @@ def __init__(
         https://cogent3.org/doc/app/evo-model-timehet
 
         >>> app_thet = get_app(
-        ...    "model",
-        ...    "HKY85",
-        ...    tree=tree,
-        ...    time_het=[dict(tip_names=["Human", "Opossum"], outgroup_name="Mouse")],
+        ...     "model",
+        ...     "HKY85",
+        ...     tree=tree,
+        ...     time_het=[dict(tip_names=["Human", "Opossum"], outgroup_name="Mouse")],
         ... )
 
         Specify the upper and lower bounds for certain branch length and rate
@@ -210,21 +219,22 @@ def __init__(
         ...     "HKY85",
         ...     tree=tree,
         ...     opt_args=dict(
-        ...         max_restarts=5, tolerance=1e-8, max_evaluations=1_000_000, limit_action="ignore"
+        ...         max_restarts=5,
+        ...         tolerance=1e-8,
+        ...         max_evaluations=1_000_000,
+        ...         limit_action="ignore",
         ...     ),
         ... )
 
         Specify settings in the likelihood function constructor.
 
         >>> app_alt_lf = get_app(
-        ...     "model", "HKY85", tree=tree, lf_args = dict(discrete_edges=["Opossum"])
+        ...     "model", "HKY85", tree=tree, lf_args=dict(discrete_edges=["Opossum"])
         ... )
 
         Splitting codons and fit models to each codon position class.
 
-        >>> app_sp_codon = get_app(
-        ...     "model", "HKY85", tree=tree, split_codons=True
-        ... )
+        >>> app_sp_codon = get_app("model", "HKY85", tree=tree, split_codons=True)
 
         A ``NotCompleted`` object (see https://cogent3.org/doc/app/not-completed.html)
         is returned if ``tree`` (or ``tree_func``) is not provided and the number of seqs
@@ -239,10 +249,11 @@ def __init__(
         (Note that we have deliberately configured the optimiser to raise an exception if
         it exits because it reached the maximum allowed evaluations.)
 
-        >>> app_limit_act = get_app("model", "GN", opt_args=dict(limit_action="raise",
-        ... max_evaluations=10))
+        >>> app_limit_act = get_app(
+        ...     "model", "GN", opt_args=dict(limit_action="raise", max_evaluations=10)
+        ... )
         >>> result = app_limit_act(aln)
-        >>> print(result.message) # doctest: +NORMALIZE_WHITESPACE
+        >>> print(result.message)  # doctest: +NORMALIZE_WHITESPACE
         Traceback ... FORCED EXIT from optimiser after 10 evaluations
         """
         if tree_func:
@@ -300,6 +311,13 @@ def _configure_lf(self, aln, identifier, initialise=None):
         lf = self._sm.make_likelihood_function(self._tree, **self._lf_args)
 
         lf.set_alignment(aln)
+
+        # just use the likelihood function instance to give us the rules
+        # which we can then impose the lower/upper bounds
+        rules = lf.get_param_rules()  # innate rules dict with default params
+        rules = _config_rules(rules, self._lower, self._upper, overwrite=True)
+        lf.apply_param_rules(rules)
+
         if self._param_rules:
             lf.apply_param_rules(self._param_rules)
 
@@ -322,12 +340,6 @@ def _configure_lf(self, aln, identifier, initialise=None):
                 lf.set_time_heterogeneity(
                     edge_sets=self._time_het, lower=self._lower, upper=self._upper
                 )
-        elif not self._param_rules:
-            # just use the likelihood function instance to give us the rules
-            # which we can then impose the lower/upper bounds
-            rules = lf.get_param_rules()
-            rules = _config_rules(rules, self._lower, self._upper)
-            lf.apply_param_rules(rules)
 
         if initialise:
             lf = initialise(lf, identifier)
@@ -664,8 +676,11 @@ def main(
         ...     "Opossum": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
         ... }
         >>> aln = make_aligned_seqs(data=data, moltype="dna")
-        >>> mod = get_app("model", "HKY85", opt_args=dict(max_evaluatuions=10,
-        ... limit_action="ignore"))
+        >>> mod = get_app(
+        ...     "model",
+        ...     "HKY85",
+        ...     opt_args=dict(max_evaluatuions=10, limit_action="ignore"),
+        ... )
         >>> result = mod(aln)
         >>> tabulator = get_app("tabulate_stats")
         >>> tabulated = tabulator(result)
diff --git a/src/cogent3/app/io.py b/src/cogent3/app/io.py
index 0b5417513..3474429ce 100644
--- a/src/cogent3/app/io.py
+++ b/src/cogent3/app/io.py
@@ -3,7 +3,6 @@
 import os
 import pickle
 import zipfile
-
 from enum import Enum
 from functools import singledispatch
 from gzip import compress as gzip_compress
@@ -14,7 +13,7 @@
 import numpy
 
 from cogent3.core.alignment import ArrayAlignment, SequenceCollection
-from cogent3.core.moltype import get_moltype
+from cogent3.core.moltype import MolType, get_moltype
 from cogent3.core.profile import (
     make_motif_counts_from_tabular,
     make_motif_freqs_from_tabular,
@@ -24,7 +23,6 @@
 from cogent3.format.alignment import FORMATTERS
 from cogent3.parse.sequence import PARSERS
 from cogent3.util.deserialise import deserialise_object
-from cogent3.util.misc import extend_docstring_from
 from cogent3.util.table import Table
 
 from .composable import LOADER, WRITER, NotCompleted, define_app
@@ -48,7 +46,6 @@
     UnalignedSeqsType,
 )
 
-
 _datastore_reader_map = {}
 
 
@@ -295,6 +292,10 @@ def __init__(self, moltype=None, format="fasta"):
             molecular type, string or instance
         format : str
             sequence file format
+
+        Examples
+        --------
+        See https://cogent3.org/doc/app/app_cookbook/load-aligned.html
         """
         self.moltype = moltype if moltype is None else get_moltype(moltype)
         self._parser = PARSERS[format.lower()]
@@ -310,14 +311,20 @@ def main(self, path: IdentifierType) -> T:
 class load_unaligned:
     """Loads unaligned sequences. Returns a SequenceCollection."""
 
-    def __init__(self, *, moltype=None, format="fasta"):
+    def __init__(
+        self, *, moltype: Optional[Union[str, MolType]] = None, format: str = "fasta"
+    ):
         """
         Parameters
         ----------
         moltype
             molecular type, string or instance
-        format : str
+        format
             sequence file format
+
+        Examples
+        --------
+        See https://cogent3.org/doc/app/app_cookbook/load-unaligned.html
         """
         self.moltype = moltype if moltype is None else get_moltype(moltype)
         self._parser = PARSERS[format.lower()]
@@ -358,6 +365,10 @@ def __init__(
             cogent3 type of tabular data
         strict
             all rows MUST have the same number of records
+
+        Examples
+        --------
+        See https://cogent3.org/doc/app/app_cookbook/load-tabular.html
         """
         self._sep = sep
         self._with_title = with_title
@@ -435,6 +446,13 @@ class load_json:
     """Loads json serialised cogent3 objects from a json file.
     Returns whatever object type was stored."""
 
+    def __init__(self):
+        """
+        Examples
+        --------
+        See: https://cogent3.org/doc/app/app_cookbook/load-json.html
+        """
+
     def main(self, path: IdentifierType) -> SerialisableType:
         """returns object deserialised from json at path"""
         data = _read_it(path)
@@ -501,6 +519,10 @@ def __init__(
             A function for creating a unique identifier based on the data
             source. The default function removes path information and
             filename + compression suffixes.
+
+        Examples
+        --------
+        See https://cogent3.org/doc/app/app_cookbook/write-json.html
         """
         if not isinstance(data_store, DataStoreABC):
             raise TypeError(f"invalid type {type(data_store)!r} for data_store")
@@ -526,7 +548,6 @@ def main(
 class write_seqs:
     """Write sequences in standard formats."""
 
-    @extend_docstring_from(write_json.__init__, pre=False)
     def __init__(
         self,
         data_store: DataStoreABC,
@@ -534,8 +555,20 @@ def __init__(
         format: str = "fasta",
     ):
         """
+        Parameters
+        ----------
+        data_store
+            A writeable data store.
+        id_from_source
+            A function for creating a unique identifier based on the data
+            source. The default function removes path information and
+            filename + compression suffixes.
         format
             sequence format
+
+        Examples
+        --------
+        See https://cogent3.org/doc/app/app_cookbook/write-seqs.html
         """
         if not isinstance(data_store, DataStoreABC):
             raise TypeError(f"invalid type {type(data_store)!r} for data_store")
@@ -560,7 +593,6 @@ def main(
 class write_tabular:
     """Writes tabular data in text format supported by the cogent3 Table object."""
 
-    @extend_docstring_from(write_json.__init__, pre=False)
     def __init__(
         self,
         data_store: DataStoreABC,
@@ -568,8 +600,18 @@ def __init__(
         format: str = "tsv",
     ):
         """
+        data_store
+            A writeable data store.
+        id_from_source
+            A function for creating a unique identifier based on the data
+            source. The default function removes path information and
+            filename + compression suffixes.
         format
             tabular format, e.g. 'csv' or 'tsv'
+
+        Examples
+        --------
+        See https://cogent3.org/doc/app/app_cookbook/write-tabular.html
         """
         if not isinstance(data_store, DataStoreABC):
             raise TypeError(f"invalid type {type(data_store)!r} for data_store")
@@ -597,7 +639,6 @@ def main(
 class write_db:
     """Write serialised objects to a database instance."""
 
-    @extend_docstring_from(write_json.__init__, pre=False)
     def __init__(
         self,
         data_store: DataStoreABC,
@@ -605,9 +646,19 @@ def __init__(
         serialiser: callable = DEFAULT_SERIALISER,
     ):
         """
+        data_store
+            A writeable data store.
+        id_from_source
+            A function for creating a unique identifier based on the data
+            source. The default function removes path information and
+            filename + compression suffixes.
         serialiser
             A callable for serialising input data. By default, it converts
             data into primitive python data types, pickling the result.
+
+        Examples
+        --------
+        See https://cogent3.org/doc/app/app_cookbook/write-db.html
         """
         if not isinstance(data_store, DataStoreABC):
             raise TypeError(f"invalid type {type(data_store)!r} for data_store")
diff --git a/src/cogent3/app/result.py b/src/cogent3/app/result.py
index aacdc75e1..66d7a8f3b 100644
--- a/src/cogent3/app/result.py
+++ b/src/cogent3/app/result.py
@@ -1,12 +1,10 @@
 import json
-
 from collections import OrderedDict
 from collections.abc import MutableMapping
 from functools import total_ordering
 from pathlib import Path
 
 import numpy
-
 from scipy.stats.distributions import chi2
 
 from cogent3._version import __version__
diff --git a/src/cogent3/app/sample.py b/src/cogent3/app/sample.py
index 44e5dbe8b..5ba807e4f 100644
--- a/src/cogent3/app/sample.py
+++ b/src/cogent3/app/sample.py
@@ -12,7 +12,6 @@
 from .translate import get_fourfold_degenerate_sets
 from .typing import AlignedSeqsType, SeqsCollectionType, SerialisableType
 
-
 # TODO need a function to filter sequences based on divergence, ala divergent
 # set.
 
@@ -60,14 +59,18 @@ def __init__(
 
         Create sample alignments with matching sequence names
 
-        >>> aln1 = make_aligned_seqs({"s1": "AAA", "s2": "CAA", "s3": "AAA"}, moltype="dna")
-        >>> aln2 = make_aligned_seqs({"s1": "GCG", "s2": "GGG", "s3": "GGT"}, moltype="dna")
+        >>> aln1 = make_aligned_seqs(
+        ...     {"s1": "AAA", "s2": "CAA", "s3": "AAA"}, moltype="dna"
+        ... )
+        >>> aln2 = make_aligned_seqs(
+        ...     {"s1": "GCG", "s2": "GGG", "s3": "GGT"}, moltype="dna"
+        ... )
 
         Concatenate alignments. By default, sequences without matching names in
         the corresponding alignment are omitted (intersect=True).
 
         >>> result = concat_alns([aln1, aln2])
-        >>> print(result.to_pretty(name_order=["s1","s2","s3"]))
+        >>> print(result.to_pretty(name_order=["s1", "s2", "s3"]))
         s1    AAAGCG
         s2    C...G.
         s3    ....GT
@@ -78,7 +81,7 @@ def __init__(
         >>> concat_missing = get_app("concat", moltype="dna", intersect=False)
         >>> aln3 = make_aligned_seqs({"s4": "GCG", "s5": "GGG"}, moltype="dna")
         >>> result = concat_missing([aln1, aln3])
-        >>> print(result.to_pretty(name_order=["s1","s2","s3","s4","s5"]))
+        >>> print(result.to_pretty(name_order=["s1", "s2", "s3", "s4", "s5"]))
         s1    AAA???
         s2    C.....
         s3    ......
@@ -89,7 +92,7 @@ def __init__(
 
         >>> concat_delim = get_app("concat", join_seq="N", moltype="dna")
         >>> result = concat_delim([aln1, aln2])
-        >>> print(result.to_pretty(name_order=["s1","s2","s3"]))
+        >>> print(result.to_pretty(name_order=["s1", "s2", "s3"]))
         s1    AAANGCG
         s2    C....G.
         s3    .....GT
@@ -180,7 +183,9 @@ def __init__(
         Create sample data with degenerate characters
 
         >>> from cogent3 import app_help, get_app, make_aligned_seqs
-        >>> aln = make_aligned_seqs({"s1": "ACGA-GACG", "s2": "GATGATGYT"}, moltype="dna")
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "ACGA-GACG", "s2": "GATGATGYT"}, moltype="dna"
+        ... )
 
         Create an app that omits aligned columns containing a degenerate
         character from an alignment
@@ -377,7 +382,9 @@ def __init__(
         positions from four-fold degenerate codons.
 
         >>> aln_ff = make_aligned_seqs({"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"})
-        >>> take_fourfold = get_app("take_codon_positions", fourfold_degenerate=True, moltype="dna")
+        >>> take_fourfold = get_app(
+        ...     "take_codon_positions", fourfold_degenerate=True, moltype="dna"
+        ... )
         >>> result = take_fourfold(aln_ff)
         >>> print(result.to_pretty())
         s1    AT
@@ -478,12 +485,9 @@ def __init__(self, *names: str, negate: bool = False):
         matching the provided names.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({
-        ...     "s1": "GCAAGC",
-        ...     "s2": "GCTTTT",
-        ...     "s3": "GC--GC",
-        ...     "s4": "GCAAGC"
-        ... })
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "GCAAGC", "s2": "GCTTTT", "s3": "GC--GC", "s4": "GCAAGC"}
+        ... )
         >>> app = get_app("take_named_seqs", "s1", "s2")
         >>> result = app(aln)
         >>> print(result.to_pretty())
@@ -550,13 +554,9 @@ def __init__(
         returned.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({
-        ...     "s1": "ACGT",
-        ...     "s2": "ACG-",
-        ...     "s3": "ACGN",
-        ...     "s4": "ACGG",
-        ...     "s5": "ACGG"
-        ... })
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "ACGT", "s2": "ACG-", "s3": "ACGN", "s4": "ACGG", "s5": "ACGG"}
+        ... )
         >>> app_first_n = get_app("take_n_seqs", number=3)
         >>> result = app_first_n(aln)
         >>> print(result.to_pretty())
@@ -578,13 +578,15 @@ def __init__(
         ``fixed_choice=True`` ensures the same sequences are returned when
         (randomly) sampling sequences across several alignments.
 
-        >>> aln2 = make_aligned_seqs({
-        ...     "s1": "GCGC",
-        ...     "s2": "GCG-",
-        ...     "s3": "GCG-",
-        ...     "s4": "GCGG",
-        ...     "s5": "GCGG",
-        ... })
+        >>> aln2 = make_aligned_seqs(
+        ...     {
+        ...         "s1": "GCGC",
+        ...         "s2": "GCG-",
+        ...         "s3": "GCG-",
+        ...         "s4": "GCGG",
+        ...         "s5": "GCGG",
+        ...     }
+        ... )
         >>> app_fixed = get_app("take_n_seqs", number=3, random=True, fixed_choice=True)
         >>> result1 = app_fixed(aln).names
         >>> result2 = app_fixed(aln2).names
@@ -806,7 +808,9 @@ def __init__(
         before sampling (i.e. codon positions are preserved).
 
         >>> aln = make_aligned_seqs({"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"})
-        >>> app_motif3 = get_app("fixed_length", length=6, motif_length=3, random=True, seed=9)
+        >>> app_motif3 = get_app(
+        ...     "fixed_length", length=6, motif_length=3, random=True, seed=9
+        ... )
         >>> result = app_motif3(aln)
         >>> print(result.to_pretty())
         s1    AGCTAT
@@ -912,15 +916,18 @@ def __init__(
         or more gaps.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({
-        ...     "s1": "---ACC---TT-",
-        ...     "s2": "---ACC---TT-",
-        ...     "s3": "---ACC---TT-",
-        ...     "s4": "--AACCG-GTT-",
-        ...     "s5": "--AACCGGGTTT",
-        ...     "s6": "AGAACCGGGTT-",
-        ...     "s7": "------------"
-        ... }, moltype="dna")
+        >>> aln = make_aligned_seqs(
+        ...     {
+        ...         "s1": "---ACC---TT-",
+        ...         "s2": "---ACC---TT-",
+        ...         "s3": "---ACC---TT-",
+        ...         "s4": "--AACCG-GTT-",
+        ...         "s5": "--AACCGGGTTT",
+        ...         "s6": "AGAACCGGGTT-",
+        ...         "s7": "------------",
+        ...     },
+        ...     moltype="dna",
+        ... )
         >>> app_frac_05 = get_app("omit_bad_seqs", gap_fraction=0.5)
         >>> result = app_frac_05(aln)
         >>> print(result.to_pretty())
@@ -1001,11 +1008,11 @@ def __init__(
 
         >>> from cogent3 import get_app, make_aligned_seqs
         >>> data = {
-        ... "a": "ACGT",
-        ... "b": "ACG-",  # identical to 'a' except has a gap
-        ... "c": "ACGG",  # duplicate
-        ... "d": "ACGG",  # duplicate
-        ... "e": "AGTC"   # unique
+        ...     "a": "ACGT",
+        ...     "b": "ACG-",  # identical to 'a' except has a gap
+        ...     "c": "ACGG",  # duplicate
+        ...     "d": "ACGG",  # duplicate
+        ...     "e": "AGTC",  # unique
         ... }
 
         Create an app that selects a representative of omits duplicate sequences.
@@ -1035,13 +1042,17 @@ def __init__(
         sequences with degenerate characters. We create sample data first that
         has a DNA ambiguity code.
 
-        >>> data = make_aligned_seqs({
-        ... "s1": "ATCG",
-        ... "s2": "ATYG",  # matches s1 with ambiguity
-        ... "s3": "GGTA",
-        ... },
-        ... moltype="DNA")
-        >>> app_dna = get_app("omit_duplicated", mask_degen=True, choose="longest", moltype="DNA")
+        >>> data = make_aligned_seqs(
+        ...     {
+        ...         "s1": "ATCG",
+        ...         "s2": "ATYG",  # matches s1 with ambiguity
+        ...         "s3": "GGTA",
+        ...     },
+        ...     moltype="DNA",
+        ... )
+        >>> app_dna = get_app(
+        ...     "omit_duplicated", mask_degen=True, choose="longest", moltype="DNA"
+        ... )
         >>> result = app_dna(data)
         >>> print(result.to_pretty())
         s1    ATCG
@@ -1135,7 +1146,9 @@ def __init__(self, gc: Union[str, int] = 1):
         stop codons.
 
         >>> from cogent3 import make_unaligned_seqs, make_aligned_seqs, get_app
-        >>> ualn = make_unaligned_seqs(data={"s1": "AAATTTCCC", "s2": "AAATTTTAA"}, moltype="dna")
+        >>> ualn = make_unaligned_seqs(
+        ...     data={"s1": "AAATTTCCC", "s2": "AAATTTTAA"}, moltype="dna"
+        ... )
         >>> app = get_app("trim_stop_codons")
         >>> result = app(ualn)
         >>> print(result.to_dict())
@@ -1147,7 +1160,9 @@ def __init__(self, gc: Union[str, int] = 1):
         For a list of all genetic codes, see
         https://cogent3.org/doc/cookbook/what_codes.html.
 
-        >>> aln = make_aligned_seqs(data={"s1": "AAATTTCCC", "s2": "AAATTTTAA"}, moltype="dna")
+        >>> aln = make_aligned_seqs(
+        ...     data={"s1": "AAATTTCCC", "s2": "AAATTTTAA"}, moltype="dna"
+        ... )
         >>> app = get_app("trim_stop_codons", gc=2)
         >>> result = app(aln)
         >>> print(result.to_dict())
diff --git a/src/cogent3/app/sqlite_data_store.py b/src/cogent3/app/sqlite_data_store.py
index 28ddaef12..c2453f9e0 100644
--- a/src/cogent3/app/sqlite_data_store.py
+++ b/src/cogent3/app/sqlite_data_store.py
@@ -5,7 +5,6 @@
 import os
 import re
 import sqlite3
-
 from pathlib import Path
 from typing import Optional, Union
 
@@ -26,7 +25,6 @@
 )
 from cogent3.util.misc import extend_docstring_from
 
-
 _RESULT_TABLE = "results"
 _MEMORY = ":memory:"
 _mem_pattern = re.compile(r"^\s*[:]{0,1}memory[:]{0,1}\s*$")
diff --git a/src/cogent3/app/translate.py b/src/cogent3/app/translate.py
index 27af4b9dd..13c5098ee 100644
--- a/src/cogent3/app/translate.py
+++ b/src/cogent3/app/translate.py
@@ -8,7 +8,6 @@
 from .composable import NotCompleted, define_app
 from .typing import SeqsCollectionType, SerialisableType
 
-
 GeneticCodeTypes = Union[str, int, GeneticCode]
 MolTypes = Union[str, MolType]
 
@@ -195,10 +194,12 @@ def __init__(
         which are translatable.
 
         >>> from cogent3 import make_unaligned_seqs, get_app
-        >>> aln = make_unaligned_seqs({
-        ...     "s1": "AATATAAATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTTCATAAAGTCATA",
-        ...     "s1_rc": "TATGACTTTATGAAGTAATAAACTGCTGTTCTCATGCTGTAATGAGCTGGCATTTATATT"
-        ... })
+        >>> aln = make_unaligned_seqs(
+        ...     {
+        ...         "s1": "AATATAAATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTTCATAAAGTCATA",
+        ...         "s1_rc": "TATGACTTTATGAAGTAATAAACTGCTGTTCTCATGCTGTAATGAGCTGGCATTTATATT",
+        ...     }
+        ... )
         >>> app = get_app("select_translatable")
         >>> result = app(aln)
         >>> print(result.to_dict())
diff --git a/src/cogent3/app/tree.py b/src/cogent3/app/tree.py
index f51b09ff3..147e3bea0 100644
--- a/src/cogent3/app/tree.py
+++ b/src/cogent3/app/tree.py
@@ -1,5 +1,4 @@
 import os
-
 from functools import singledispatch
 from typing import Union
 
@@ -12,7 +11,6 @@
 from .composable import define_app
 from .typing import PairwiseDistanceType, SerialisableType, TreeType
 
-
 NoneType = type(None)
 
 
diff --git a/src/cogent3/app/typing.py b/src/cogent3/app/typing.py
index 0147eec6c..a5f990f90 100644
--- a/src/cogent3/app/typing.py
+++ b/src/cogent3/app/typing.py
@@ -6,12 +6,10 @@
 import inspect
 import re
 import sys
-
 from typing import ForwardRef, TypeVar, Union
 
 from typing_extensions import get_args, get_origin
 
-
 if sys.version_info.minor >= 10:
     from types import UnionType
 
diff --git a/src/cogent3/cluster/UPGMA.py b/src/cogent3/cluster/UPGMA.py
index fb03d5647..7cb594bfe 100644
--- a/src/cogent3/cluster/UPGMA.py
+++ b/src/cogent3/cluster/UPGMA.py
@@ -11,13 +11,11 @@
 """
 
 import numpy
-
 from numpy import argmin, average, diag, ravel, take
 
 from cogent3.core.tree import PhyloNode
 from cogent3.util.dict_array import DictArray
 
-
 BIG_NUM = 1e305
 
 
diff --git a/src/cogent3/cluster/__init__.py b/src/cogent3/cluster/__init__.py
index 23da6db28..27cfa48fd 100644
--- a/src/cogent3/cluster/__init__.py
+++ b/src/cogent3/cluster/__init__.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-"""cluster: provides tools for clustering
-"""
+"""cluster: provides tools for clustering"""
 
 __all__ = ["UPGMA"]
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 05f498508..219e2a8cf 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -1,22 +1,22 @@
 """Code for handling multiple sequence alignments. In particular:
 
-    - SequenceCollection handles both aligned and unaligned sequences.
-    - Alignment and its subclasses handle multiple sequence alignments, storing
-      the raw sequences and a gap map. Useful for very long alignments, e.g.
-      genomics data.
-    - ArrayAlignment and its subclasses handle multiple sequence alignments as
-      arrays of characters. Especially useful for short alignments that contain
-      many sequences.
-
-    WARNING: The various alignment objects try to guess the input type from the
-    input, but this behavior has a few quirks. In particular, if the input is
-    a sequence of two-item sequences (e.g. a list of two-character strings),
-    each sequence will be unpacked and the first item will be used as the
-    label, the second as the sequence. For example, Alignment(['AA','CC','AA'])
-    produces an alignment of three 1-character strings labeled A, C and A
-    respectively. The reason for this is that the common case is that you have
-    passed in a stream of two-item label, sequence pairs. However, this can
-    cause confusion when testing.
+- SequenceCollection handles both aligned and unaligned sequences.
+- Alignment and its subclasses handle multiple sequence alignments, storing
+  the raw sequences and a gap map. Useful for very long alignments, e.g.
+  genomics data.
+- ArrayAlignment and its subclasses handle multiple sequence alignments as
+  arrays of characters. Especially useful for short alignments that contain
+  many sequences.
+
+WARNING: The various alignment objects try to guess the input type from the
+input, but this behavior has a few quirks. In particular, if the input is
+a sequence of two-item sequences (e.g. a list of two-character strings),
+each sequence will be unpacked and the first item will be used as the
+label, the second as the sequence. For example, Alignment(['AA','CC','AA'])
+produces an alignment of three 1-character strings labeled A, C and A
+respectively. The reason for this is that the common case is that you have
+passed in a stream of two-item label, sequence pairs. However, this can
+cause confusion when testing.
 """
 
 from __future__ import annotations
@@ -27,7 +27,6 @@
 import re
 import typing
 import warnings
-
 from collections import Counter, defaultdict
 from copy import deepcopy
 from functools import singledispatchmethod, total_ordering
@@ -44,7 +43,6 @@
 )
 
 import numpy
-
 from numpy import (
     arange,
     array,
@@ -64,7 +62,6 @@
 from numpy.random import choice, permutation, randint
 
 import cogent3  # will use to get at cogent3.parse.fasta.MinimalFastaParser,
-
 from cogent3._version import __version__
 from cogent3.core.annotation import Feature
 from cogent3.core.annotation_db import (
@@ -79,6 +76,7 @@
 from cogent3.core.location import FeatureMap, IndelMap
 from cogent3.core.profile import PSSM, MotifCountsArray
 from cogent3.core.sequence import ArraySequence, Sequence, frac_same
+
 # which is a circular import otherwise.
 from cogent3.format.alignment import save_to_filename
 from cogent3.format.fasta import seqs_to_fasta
@@ -98,7 +96,6 @@
 )
 from cogent3.util.union_dict import UnionDict
 
-
 DEFAULT_ANNOTATION_DB = BasicAnnotationDb
 
 
@@ -2125,13 +2122,14 @@ def to_html(
 
         .. code-block:: python
 
-            seq_col # is rendered by jupyter
+            seq_col  # is rendered by jupyter
 
         You can directly use the result for display in a notebook as
 
         .. code-block:: python
 
             from IPython.core.display import HTML
+
             HTML(seq_col.to_html())
         """
         css, styles = self.moltype.get_css_style(
@@ -2633,11 +2631,6 @@ def take_positions_if(self, f, negate=False):
         """Returns new Alignment containing cols where f(col) is True."""
         return self.take_positions(self.get_position_indices(f, negate=negate))
 
-    @c3warn.deprecated_args(
-        "2024.6",
-        reason="consistency with other methods",
-        old_new=[("allow_gaps", "allow_gap")],
-    )
     def iupac_consensus(self, alphabet=None, allow_gap=True):
         """Returns string containing IUPAC consensus sequence of the alignment."""
         if alphabet is None:
@@ -3096,13 +3089,14 @@ def to_html(
 
         .. code-block:: python
 
-            aln # is rendered by jupyter
+            aln  # is rendered by jupyter
 
         You can directly use the result for display in a notebook as
 
         .. code-block:: python
 
             from IPython.core.display import HTML
+
             HTML(aln.to_html())
         """
         css, styles = self.moltype.get_css_style(
@@ -4038,11 +4032,6 @@ def __str__(self):
             result.append(">" + str(l) + "\n" + "".join(seq2str(s)))
         return "\n".join(result) + "\n"
 
-    @c3warn.deprecated_args(
-        "2024.6",
-        reason="consistency with other methods",
-        old_new=[("allow_gaps", "allow_gap")],
-    )
     def iupac_consensus(self, alphabet=None, allow_gap=True):
         """Returns string containing IUPAC consensus sequence of the alignment."""
         if alphabet is None:
diff --git a/src/cogent3/core/alphabet.py b/src/cogent3/core/alphabet.py
index eac618fbd..afe811f7c 100644
--- a/src/cogent3/core/alphabet.py
+++ b/src/cogent3/core/alphabet.py
@@ -15,7 +15,6 @@
 
 import json
 import typing
-
 from functools import singledispatchmethod
 from itertools import product
 
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index b88da41a6..59d35d3e0 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -15,7 +15,6 @@
 import typing_extensions
 
 import cogent3.util.warning as c3warn
-
 from cogent3._version import __version__
 from cogent3.parse.gff import merged_gff_records
 from cogent3.util.deserialise import register_deserialiser
@@ -24,7 +23,6 @@
 from cogent3.util.progress_display import display_wrap
 from cogent3.util.table import Table
 
-
 OptionalInt = typing.Optional[int]
 OptionalStr = typing.Optional[str]
 OptionalStrList = typing.Optional[typing.Union[str, typing.List[str]]]
@@ -922,7 +920,7 @@ def count_distinct(
         --------
         To compute copy number by gene name within each genome
 
-        >>> counts_table = db.count_distinct(seqid=True, biotype='gene', name=True)
+        >>> counts_table = db.count_distinct(seqid=True, biotype="gene", name=True)
         """
         columns = {k for k, v in locals().items() if v is True}
         if not columns:
diff --git a/src/cogent3/core/genetic_code.py b/src/cogent3/core/genetic_code.py
index 5c687bb9e..f686ecdf9 100644
--- a/src/cogent3/core/genetic_code.py
+++ b/src/cogent3/core/genetic_code.py
@@ -5,14 +5,13 @@
 NOTE: Although the genetic code objects convert DNA to RNA and vice
 versa, lists of codons that they produce will be provided in DNA format.
 """
+
 import os
 import re
-
 from itertools import product
 
 from cogent3.util.table import Table
 
-
 maketrans = str.maketrans
 
 
@@ -44,12 +43,14 @@ class GeneticCode:
 
     Use the `get_code()` function to get one of the included code instances. These are created as follows.
 
-    >>> code_sequence = 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG'
+    >>> code_sequence = (
+    ...     "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG"
+    ... )
     >>> gc = GeneticCode(code_sequence)
-    >>> sgc['UUU'] == 'F'
-    >>> sgc['TTT'] == 'F'
-    >>> sgc['F'] == ['TTT', 'TTC']          #in arbitrary order
-    >>> sgc['*'] == ['TAA', 'TAG', 'TGA']   #in arbitrary order
+    >>> sgc["UUU"] == "F"
+    >>> sgc["TTT"] == "F"
+    >>> sgc["F"] == ["TTT", "TTC"]  # in arbitrary order
+    >>> sgc["*"] == ["TAA", "TAG", "TGA"]  # in arbitrary order
 
     code_sequence : 64 character string containing NCBI genetic code translation
 
diff --git a/src/cogent3/core/info.py b/src/cogent3/core/info.py
index 7bc6b872d..c96465323 100644
--- a/src/cogent3/core/info.py
+++ b/src/cogent3/core/info.py
@@ -3,6 +3,7 @@
 
 Info is a dictionary and is the annotation object of a Sequence object.
 """
+
 from warnings import warn
 
 from cogent3.parse.record import MappedRecord
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index b254a43b1..85492a6f9 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -3,7 +3,6 @@
 import functools
 import inspect
 import json
-
 from abc import ABC, abstractmethod
 from bisect import bisect_left, bisect_right
 from functools import total_ordering
@@ -11,7 +10,6 @@
 from typing import Any, Iterator, Optional, Sequence, Union
 
 import numpy
-
 from numpy.typing import NDArray
 
 from cogent3._version import __version__
@@ -25,7 +23,6 @@
     iterable,
 )
 
-
 strip = str.strip
 
 _DEFAULT_GAP_DTYPE = numpy.int32
diff --git a/src/cogent3/core/moltype.py b/src/cogent3/core/moltype.py
index 42d820645..eff2c3f7c 100644
--- a/src/cogent3/core/moltype.py
+++ b/src/cogent3/core/moltype.py
@@ -17,7 +17,6 @@
 import os
 import re
 import typing
-
 from collections import defaultdict
 from copy import deepcopy
 from random import choice
@@ -61,7 +60,6 @@
 )
 from cogent3.util.transform import KeepChars, first_index_in_set
 
-
 maketrans = str.maketrans
 translate = str.translate
 
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 140bf7e15..fe8d09714 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -4,7 +4,6 @@
 import re
 import typing
 import warnings
-
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from functools import singledispatch, singledispatchmethod
@@ -41,7 +40,6 @@
     negate_condition,
 )
 
-
 DEFAULT_ANNOTATION_DB = BasicAnnotationDb
 
 OptInt = Optional[int]
@@ -288,7 +286,24 @@ def __getitem__(
 
 
 class SeqsData(SeqsDataABC):
-    # refactor: docstring
+    """A collection of sequences underlying a SequenceCollection. The sequence
+    data is stored as numpy arrays, however the underlying data can be accessed
+    as strings, bytes, or numpy arrays.
+
+    Attributes
+    ----------
+    data
+        a dictionary of {name: sequence} pairs
+    alphabet
+        an instance of CharAlphabet valid for the sequences
+    make_seq
+        optional sequence constructor, takes 'seq' and 'name' as keyword arguments
+        and returns a Sequence instance. If not set, __getitem__ will return a
+        SeqDataView
+    reversed_seqs
+        a dictionary of {name: bool} pairs indicating if the sequence is reversed
+    """
+
     __slots__ = ("_data", "_alphabet", "_make_seq", "_reversed_seqs")
     # todo: kath
     # refactor: design
@@ -1976,13 +1991,14 @@ def to_html(
 
         .. code-block:: python
 
-            seq_col # is rendered by jupyter
+            seq_col  # is rendered by jupyter
 
         You can directly use the result for display in a notebook as
 
         .. code-block:: python
 
             from IPython.core.display import HTML
+
             HTML(seq_col.to_html())
         """
         css, styles = self.moltype.get_css_style(
@@ -2339,7 +2355,6 @@ def _(
     source: OptPathType = None,
     annotation_db: SupportsFeatures = None,
 ) -> SequenceCollection:
-
     moltype = new_moltype.get_moltype(moltype)
     if not moltype.is_compatible_alphabet(data.alphabet):
         raise ValueError(
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index 57ee78fe9..5bf9ec86f 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -2,7 +2,6 @@
 import itertools
 import json
 import typing
-
 from abc import ABC, abstractmethod
 
 import numba
@@ -12,7 +11,6 @@
 from cogent3.util.deserialise import register_deserialiser
 from cogent3.util.misc import get_object_provenance
 
-
 StrORBytes = typing.Union[str, bytes]
 StrORBytesORArray = typing.Union[str, bytes, numpy.ndarray]
 OptInt = typing.Optional[int]
diff --git a/src/cogent3/core/new_genetic_code.py b/src/cogent3/core/new_genetic_code.py
index a8f15b856..b3253d307 100644
--- a/src/cogent3/core/new_genetic_code.py
+++ b/src/cogent3/core/new_genetic_code.py
@@ -20,7 +20,6 @@
 from cogent3.core import new_alphabet, new_moltype
 from cogent3.util.table import Table
 
-
 OptStr = typing.Optional[str]
 SetStr = typing.Set[str]
 ConverterType = typing.Callable[[bytes, bytes], bytes]
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index bbec2f23b..6af06d3f3 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -2,7 +2,6 @@
 import functools
 import itertools
 import typing
-
 from collections import defaultdict
 from string import ascii_letters
 
@@ -16,7 +15,6 @@
     WeightCalculator,
 )
 
-
 OptStr = typing.Optional[str]
 OptFloat = typing.Optional[float]
 OptCallable = typing.Optional[typing.Callable]
@@ -267,61 +265,69 @@ def _build_pairing_rules() -> dict[frozenset[str], bool]:
     "*": "STOP",
 }
 
-IUPAC_PROTEIN_chars = (
-    "A",
-    "C",
-    "D",
-    "E",
-    "F",
-    "G",
-    "H",
-    "I",
-    "K",
-    "L",
-    "M",
-    "N",
-    "P",
-    "Q",
-    "R",
-    "S",
-    "T",
-    "U",
-    "V",
-    "W",
-    "Y",
+IUPAC_PROTEIN_chars = frozenset(
+    [
+        "A",
+        "C",
+        "D",
+        "E",
+        "F",
+        "G",
+        "H",
+        "I",
+        "K",
+        "L",
+        "M",
+        "N",
+        "P",
+        "Q",
+        "R",
+        "S",
+        "T",
+        "U",
+        "V",
+        "W",
+        "Y",
+    ]
 )
 
-PROTEIN_WITH_STOP_chars = (
-    "A",
-    "C",
-    "D",
-    "E",
-    "F",
-    "G",
-    "H",
-    "I",
-    "K",
-    "L",
-    "M",
-    "N",
-    "P",
-    "Q",
-    "R",
-    "S",
-    "T",
-    "U",
-    "V",
-    "W",
-    "Y",
-    "*",
+PROTEIN_WITH_STOP_chars = frozenset(
+    [
+        "A",
+        "C",
+        "D",
+        "E",
+        "F",
+        "G",
+        "H",
+        "I",
+        "K",
+        "L",
+        "M",
+        "N",
+        "P",
+        "Q",
+        "R",
+        "S",
+        "T",
+        "U",
+        "V",
+        "W",
+        "Y",
+        "*",
+    ]
 )
 
-IUPAC_PROTEIN_ambiguities = {"B": ["N", "D"], "X": IUPAC_PROTEIN_chars, "Z": ["Q", "E"]}
+IUPAC_PROTEIN_ambiguities = {
+    "B": frozenset(["N", "D"]),
+    "X": IUPAC_PROTEIN_chars,
+    "Z": frozenset(["Q", "E"]),
+}
 
 PROTEIN_WITH_STOP_ambiguities = {
-    "B": ["N", "D"],
+    "B": frozenset(["N", "D"]),
     "X": PROTEIN_WITH_STOP_chars,
-    "Z": ["Q", "E"],
+    "Z": frozenset(["Q", "E"]),
 }
 
 # styling for moltype display
@@ -543,8 +549,26 @@ def is_compatible_alphabet(
 
         return any(alpha == alphabet for alpha in self.iter_alphabets())
 
-    def make_seq(self, *, seq: str, name: OptStr = None, check_seq=True, **kwargs):
-        # refactor: docstring
+    def make_seq(
+        self, *, seq: str, name: OptStr = None, check_seq=True, **kwargs
+    ) -> new_sequence.Sequence:
+        """creates a Sequence object corresponding to the molecular type of
+        this instance.
+
+        Parameters
+        ----------
+        seq
+            the raw sequence data
+        name
+            the name of the sequence
+        check_seq
+            whether to validate the sequence data against the molecular type
+            if True, performs validation checks; set to False if the sequence
+            data is already known to be valid.
+        **kwargs
+            additional keyword arguments that may be required for creating the
+            Sequence object
+        """
         # refactor: design
         # for Sequence and SeqView object, a consistency check with moltype/alphabet
         # should be performed
@@ -1001,6 +1025,38 @@ def _(self, seq: bytes) -> int:
     def _(self, seq: str) -> int:
         return numpy.prod([len(self.ambiguities.get(c, c)) for c in seq])
 
+    def degenerate_from_seq(self, seq: str) -> str:
+        """Returns least degenerate symbol that encompasses a set of characters"""
+        symbols = frozenset(seq)
+
+        # if the length of the set is 1, return the single character
+        if len(symbols) == 1:
+            return next(iter(symbols))
+
+        # all degenerate symbols should be added to the sets that they encompass
+        degens = self.ambiguities.copy()
+        for degen1 in degens:
+            for degen2, char_set in self.ambiguities.items():
+                if self.ambiguities[degen1] <= char_set:
+                    degens[degen2] = degens[degen2].union(frozenset(degen1))
+
+        # reverse the mapping between degenerate symbols and their encompassing sets
+        inv_degens = {frozenset(val): key for key, val in list(degens.items())}
+
+        # add gap and missing characters to the mapping
+        inv_degens[frozenset(self.gap)] = self.gap
+        inv_degens[frozenset(self.degen_gapped_alphabet)] = self.missing
+
+        # if we exactly match a set of symbols, return the corresponding degenerate
+        if result := inv_degens.get(symbols):
+            return result
+
+        # if we don't exactly match, sort all encompassing sets and return the
+        # least degenerate one
+        encompassing = [chars for chars in inv_degens if chars.issuperset(symbols)]
+        encompassing = sorted(encompassing, key=len)
+        return inv_degens[encompassing[0]]
+
     def strand_symmetric_motifs(
         self, motif_length: int = 1
     ) -> set[tuple[str, str]]:  # refactor: docstring
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index bfce5ced0..85624aa1f 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -14,7 +14,6 @@
 import re
 import typing
 import warnings
-
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from functools import singledispatch, total_ordering
@@ -22,7 +21,6 @@
 from random import shuffle
 
 import numpy
-
 from numpy import array, floating, integer, issubdtype
 
 from cogent3._version import __version__
@@ -49,7 +47,6 @@
 )
 from cogent3.util.transform import for_seq, per_shortest
 
-
 OptGeneticCodeType = typing.Optional[typing.Union[int, str, "GeneticCode"]]
 OptStr = typing.Optional[str]
 OptInt = typing.Optional[int]
@@ -1992,7 +1989,21 @@ def _input_vals_neg_step(seqlen, start, stop, step):
 
 class SliceRecordABC(ABC):
     """Abstract base class for recording the history of operations to be applied
-    to some underlying data
+    to some underlying data. Provides slicing functionality for the underlying data.
+
+    Parameters
+    ----------
+    start
+        start of the slice (inclusive indexing)
+    stop
+        stop of the slice (exclusive indexing)
+    step
+        step of the slice
+    offset
+        can be set with any additional offset that exists before the start of
+        the underlying data
+    seq_len
+        length of the underlying data (not including offset)
 
     Notes
     -----
@@ -2377,11 +2388,19 @@ def _get_reverse_slice_from_reverse_seqview_(
 
 
 class SeqViewABC(ABC):
-    # refactor: docstring
+    """
+    An abstract base class for providing a view of a sequence.
+
+    This class defines an interface for sequence views, by which operations
+    performed on the sequence do not modify the original sequence data. Instead,
+    modifications and operations are stored on the returned view of the sequence
+    and can be realised by accessing the values of the view.
+    """
 
     __slots__ = ()
 
     @property
+    @abstractmethod
     def seqid(self) -> OptStr: ...
 
     @property
@@ -2416,7 +2435,33 @@ def __bytes__(self):
 
 
 class SeqView(SeqViewABC, SliceRecordABC):
-    # refactor: docstring
+    """
+    Provides a view of a sequence with support for slicing operations.
+
+    This class represents a view of a sequence, allowing for efficient slicing
+    without altering the original sequence data.
+
+    Parameters
+    ----------
+    seq
+        the original sequence data
+    alphabet
+        the alphabet object defining valid characters for the sequence
+    start
+        the starting index of the slice. Defaults to the start of the sequence
+    stop
+        the stopping index of the slice. Defaults to the end of the sequence
+    step
+        the step size for the slice. Defaults to 1
+    offset
+        an offset used for adjusting the view's starting position. Defaults to 0
+    seqid
+        the name or identifier of the sequence
+    seq_len
+        the length of the sequence. Defaults to the length of the input sequence
+
+    """
+
     __slots__ = (
         "seq",
         "alphabet",
diff --git a/src/cogent3/core/profile.py b/src/cogent3/core/profile.py
index 8b2393c89..b30ef23ea 100644
--- a/src/cogent3/core/profile.py
+++ b/src/cogent3/core/profile.py
@@ -1,5 +1,4 @@
 import numpy
-
 from numpy import array, digitize
 from numpy.random import random
 
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index fde1fadbb..1dfbcb342 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -19,7 +19,6 @@
 import os
 import re
 import warnings
-
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from functools import singledispatch, total_ordering
@@ -60,7 +59,6 @@
 from cogent3.format.fasta import seqs_to_fasta
 from cogent3.maths.stats.contingency import CategoryCounts
 from cogent3.maths.stats.number import CategoryCounter
-from cogent3.util import warning as c3warn
 from cogent3.util.dict_array import DictArrayTemplate
 from cogent3.util.misc import (
     DistanceFromMatrix,
@@ -70,7 +68,6 @@
 )
 from cogent3.util.transform import for_seq, per_shortest
 
-
 ARRAY_TYPE = type(array(1))
 DEFAULT_ANNOTATION_DB = BasicAnnotationDb
 
@@ -1427,11 +1424,6 @@ def sliding_windows(self, window, step, start=None, end=None):
             for pos in range(start, end, step):
                 yield self[pos : pos + window]
 
-    @c3warn.deprecated_args(
-        "2024.6",
-        reason="argument name consistency",
-        old_new=(("log_warnings", "warn"),),
-    )
     def get_in_motif_size(self, motif_length=1, warn=False):
         """returns sequence as list of non-overlapping motifs
 
diff --git a/src/cogent3/core/tree.py b/src/cogent3/core/tree.py
index 4fc18c471..3df647b30 100644
--- a/src/cogent3/core/tree.py
+++ b/src/cogent3/core/tree.py
@@ -30,7 +30,6 @@
 import json
 import numbers
 import re
-
 from copy import deepcopy
 from functools import reduce
 from itertools import combinations
diff --git a/src/cogent3/data/molecular_weight.py b/src/cogent3/data/molecular_weight.py
index 370acda13..edf6b5e12 100644
--- a/src/cogent3/data/molecular_weight.py
+++ b/src/cogent3/data/molecular_weight.py
@@ -1,7 +1,6 @@
 #!/usr/bin/env Python
 """Data for molecular weight calculations on proteins and nucleotides."""
 
-
 ProteinWeights = {
     "A": 71.09,
     "C": 103.16,
diff --git a/src/cogent3/draw/drawable.py b/src/cogent3/draw/drawable.py
index d919b46a1..a2fbbd70a 100644
--- a/src/cogent3/draw/drawable.py
+++ b/src/cogent3/draw/drawable.py
@@ -6,7 +6,6 @@
 from cogent3.util.misc import extend_docstring_from
 from cogent3.util.union_dict import UnionDict
 
-
 # user specified environment variable for plotly renderer
 PLOTLY_RENDERER = os.environ.get("PLOTLY_RENDERER", None)
 
diff --git a/src/cogent3/draw/letter.py b/src/cogent3/draw/letter.py
index 5e286adfe..668847aaa 100644
--- a/src/cogent3/draw/letter.py
+++ b/src/cogent3/draw/letter.py
@@ -2,7 +2,7 @@
 Character shapes that are renderable into Plotly plots. The SVG paths were derived from
 the mono-spaced font Inconsolata, which is under the SIL Open Font License. The characters
 are stored as SVG paths that are in array format for accessible data manipulation.
-The paths are ultimately converted into a String for use in Plotly. 
+The paths are ultimately converted into a String for use in Plotly.
 
 The SVG paths for each character were generated via the following process:
 
@@ -19,7 +19,6 @@
 
 from cogent3.util.union_dict import UnionDict
 
-
 _svg_paths = {
     "A": [
         ["M", 4.2679468235157464e-05, 0.0],
diff --git a/src/cogent3/evolve/best_likelihood.py b/src/cogent3/evolve/best_likelihood.py
index 61ab559aa..6e63d34db 100644
--- a/src/cogent3/evolve/best_likelihood.py
+++ b/src/cogent3/evolve/best_likelihood.py
@@ -11,7 +11,6 @@
 
 from cogent3 import make_aligned_seqs
 
-
 cite = "Goldman, N. (1993).  Statistical tests of models of DNA substitution.  J Mol Evol, 36: 182-98"
 
 
diff --git a/src/cogent3/evolve/coevolution.py b/src/cogent3/evolve/coevolution.py
index 1f6be97bf..8a4b875c2 100755
--- a/src/cogent3/evolve/coevolution.py
+++ b/src/cogent3/evolve/coevolution.py
@@ -1,4 +1,4 @@
-""" Description
+"""Description
 File created on 03 May 2007.
 
 Functions to perform coevolutionary analyses on
@@ -75,7 +75,6 @@
 from cogent3.maths.stats.number import CategoryCounter, CategoryFreqs
 from cogent3.maths.stats.special import ROUND_ERROR
 
-
 DEFAULT_EXCLUDES = "".join([IUPAC_gap, IUPAC_missing])
 DEFAULT_NULL_VALUE = nan
 
@@ -115,7 +114,7 @@ def join_positions(pos1, pos2):
     pos2: iterable object containing the second positions data
 
     Example:
-        >>> join_positions('ABCD','1234')
+        >>> join_positions("ABCD", "1234")
             ['A1', 'B2', 'C3', 'D4']
     """
     return ["".join([r1, r2]) for r1, r2 in zip(pos1, pos2)]
diff --git a/src/cogent3/evolve/distance.py b/src/cogent3/evolve/distance.py
index 91ba16d68..4764cf2e0 100644
--- a/src/cogent3/evolve/distance.py
+++ b/src/cogent3/evolve/distance.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
-"""Estimating pairwise distances between sequences.
-"""
+"""Estimating pairwise distances between sequences."""
+
 from itertools import combinations
 from warnings import warn
 
diff --git a/src/cogent3/evolve/fast_distance.py b/src/cogent3/evolve/fast_distance.py
index e8a4c1cb7..b9a954279 100644
--- a/src/cogent3/evolve/fast_distance.py
+++ b/src/cogent3/evolve/fast_distance.py
@@ -3,7 +3,6 @@
 from typing import Tuple
 
 import numpy as np
-
 from numpy import array, diag, dot, eye, float64, int32, log, sqrt, zeros
 from numpy.linalg import det, inv
 
@@ -15,7 +14,6 @@
 
 from .pairwise_distance_numba import fill_diversity_matrix
 
-
 # pending addition of protein distance metrics
 
 
diff --git a/src/cogent3/evolve/likelihood_calculation.py b/src/cogent3/evolve/likelihood_calculation.py
index 88c233880..34aaf74e9 100644
--- a/src/cogent3/evolve/likelihood_calculation.py
+++ b/src/cogent3/evolve/likelihood_calculation.py
@@ -8,6 +8,7 @@
 validating accuracy. The calculations can be performed for tree's that have polytomies
 in addition to binary trees.
 """
+
 import numpy
 
 from cogent3.evolve.likelihood_tree import LikelihoodTreeEdge
diff --git a/src/cogent3/evolve/likelihood_function.py b/src/cogent3/evolve/likelihood_function.py
index 46225c116..5fa41e192 100644
--- a/src/cogent3/evolve/likelihood_function.py
+++ b/src/cogent3/evolve/likelihood_function.py
@@ -1,7 +1,6 @@
 import json
 import random
 import typing
-
 from collections import defaultdict
 from copy import deepcopy
 
@@ -24,7 +23,6 @@
 from cogent3.util.dict_array import DictArrayTemplate
 from cogent3.util.misc import adjusted_gt_minprob, get_object_provenance
 
-
 # cogent3.evolve.parameter_controller.LikelihoodParameterController tells the
 # recalculation framework to use this subclass rather than the generic
 # recalculation Calculator.  It adds methods which are useful for examining
diff --git a/src/cogent3/evolve/likelihood_tree.py b/src/cogent3/evolve/likelihood_tree.py
index 04ad017d4..3640c6bc0 100644
--- a/src/cogent3/evolve/likelihood_tree.py
+++ b/src/cogent3/evolve/likelihood_tree.py
@@ -8,7 +8,6 @@
 
 from . import likelihood_tree_numba as likelihood_tree
 
-
 numpy.seterr(all="ignore")
 
 
diff --git a/src/cogent3/evolve/likelihood_tree_numba.py b/src/cogent3/evolve/likelihood_tree_numba.py
index f9c901475..48c3d2d86 100644
--- a/src/cogent3/evolve/likelihood_tree_numba.py
+++ b/src/cogent3/evolve/likelihood_tree_numba.py
@@ -1,8 +1,6 @@
 import numpy
-
 from numba import njit
 
-
 # turn off code coverage as njit-ted code not accessible to coverage
 
 
diff --git a/src/cogent3/evolve/models.py b/src/cogent3/evolve/models.py
index a2ef19e36..3ba243336 100644
--- a/src/cogent3/evolve/models.py
+++ b/src/cogent3/evolve/models.py
@@ -22,7 +22,6 @@
 from cogent3.evolve.substitution_model import kappa_y as _kappa_y
 from cogent3.util.table import Table
 
-
 nucleotide_models = []
 codon_models = []
 protein_models = []
diff --git a/src/cogent3/evolve/motif_prob_model.py b/src/cogent3/evolve/motif_prob_model.py
index 66d56e44d..b69c9c880 100644
--- a/src/cogent3/evolve/motif_prob_model.py
+++ b/src/cogent3/evolve/motif_prob_model.py
@@ -1,11 +1,8 @@
 import warnings
-
 from typing import Union
 
 import numpy
 
-import cogent3.util.warning as c3warn
-
 from cogent3.core.alphabet import Alphabet
 from cogent3.evolve.likelihood_tree import make_likelihood_tree_leaf
 from cogent3.recalculation.definition import CalcDefn, PartitionDefn
@@ -180,10 +177,7 @@ def make_motif_word_prob_defns(self):
         )
         return (monomer_probs, word_probs, mprobs_matrix)
 
-    @c3warn.deprecated_args(
-        "2024.6", reason="use warn instead", discontinued="auto", stack_level=4
-    )
-    def adapt_motif_probs(self, motif_probs, warn=False, auto=False):
+    def adapt_motif_probs(self, motif_probs, warn=False):
         try:
             motif_probs = adapt_motif_probs(self.monomer_alphabet, motif_probs)
         except ValueError:
diff --git a/src/cogent3/evolve/pairwise_distance_numba.py b/src/cogent3/evolve/pairwise_distance_numba.py
index 9fb65d70b..ae88d74e9 100644
--- a/src/cogent3/evolve/pairwise_distance_numba.py
+++ b/src/cogent3/evolve/pairwise_distance_numba.py
@@ -1,6 +1,5 @@
 from numba import njit
 
-
 # turn off code coverage as njit-ted code not accessible to coverage
 
 
diff --git a/src/cogent3/evolve/parameter_controller.py b/src/cogent3/evolve/parameter_controller.py
index 93cc9c5b7..be46a590a 100644
--- a/src/cogent3/evolve/parameter_controller.py
+++ b/src/cogent3/evolve/parameter_controller.py
@@ -527,12 +527,13 @@ def set_alignment(self, aligns, motif_pseudocount=None):
                 locus_name = f"for locus '{self.locus_names[index]}'"
             else:
                 locus_name = ""
-            assert not set(aln.names).symmetric_difference(
-                tip_names
-            ), "Tree tip names %s and aln seq names %s don't match %s" % (
-                self.tree.get_tip_names(),
-                aln.names,
-                locus_name,
+            assert not set(aln.names).symmetric_difference(tip_names), (
+                "Tree tip names %s and aln seq names %s don't match %s"
+                % (
+                    self.tree.get_tip_names(),
+                    aln.names,
+                    locus_name,
+                )
             )
             assert "root" not in aln.names, "'root' is a reserved name."
         with self.updates_postponed():
diff --git a/src/cogent3/evolve/solved_models.py b/src/cogent3/evolve/solved_models.py
index 47dcf2f54..c8db41349 100644
--- a/src/cogent3/evolve/solved_models.py
+++ b/src/cogent3/evolve/solved_models.py
@@ -3,7 +3,6 @@
 """
 
 import numpy
-
 from numpy.testing import assert_allclose
 
 from cogent3.evolve.substitution_model import (
diff --git a/src/cogent3/evolve/solved_models_numba.py b/src/cogent3/evolve/solved_models_numba.py
index e1b68cd7f..7a4c8c409 100644
--- a/src/cogent3/evolve/solved_models_numba.py
+++ b/src/cogent3/evolve/solved_models_numba.py
@@ -1,10 +1,8 @@
 import math
 
 import numpy as np
-
 from numba import njit
 
-
 # turn off code coverage as njit-ted code not accessible to coverage
 
 
diff --git a/src/cogent3/evolve/substitution_calculation.py b/src/cogent3/evolve/substitution_calculation.py
index 51b7aea88..2422289b8 100644
--- a/src/cogent3/evolve/substitution_calculation.py
+++ b/src/cogent3/evolve/substitution_calculation.py
@@ -14,7 +14,6 @@
     RatioParamDefn,
 )
 
-
 # Custom subclasses of Defn (see cogent3.recalulation) for use by
 # substitution models.
 
diff --git a/src/cogent3/evolve/substitution_model.py b/src/cogent3/evolve/substitution_model.py
index c9b0b6b24..aced35a6d 100644
--- a/src/cogent3/evolve/substitution_model.py
+++ b/src/cogent3/evolve/substitution_model.py
@@ -25,7 +25,7 @@
 called Q) is determined.
 
 >>> model = Nucleotide(equal_motif_probs=True)
->>> model.setparameterrules({'alpha': model.istransition})
+>>> model.setparameterrules({"alpha": model.istransition})
 >>> parameter_controller = model.make_likelihood_function(tree)
 """
 
@@ -34,12 +34,10 @@
 import json
 import typing
 import warnings
-
 from collections.abc import Callable
 from copy import deepcopy
 
 import numpy
-
 from numpy.linalg import svd
 
 from cogent3._version import __version__
@@ -69,7 +67,6 @@
 )
 from cogent3.util.misc import extend_docstring_from, get_object_provenance
 
-
 kappa_y = predicate.MotifChange("T", "C").aliased("kappa_y")
 kappa_r = predicate.MotifChange("A", "G").aliased("kappa_r")
 
diff --git a/src/cogent3/format/__init__.py b/src/cogent3/format/__init__.py
index b39d10bb5..b2c31e853 100644
--- a/src/cogent3/format/__init__.py
+++ b/src/cogent3/format/__init__.py
@@ -5,6 +5,7 @@
     xml:  xml base class
     file: general functions to read and write files
 """
+
 __all__ = [
     "alignment",
     "bedgraph",
diff --git a/src/cogent3/format/alignment.py b/src/cogent3/format/alignment.py
index 293744829..89c49fab8 100644
--- a/src/cogent3/format/alignment.py
+++ b/src/cogent3/format/alignment.py
@@ -8,7 +8,6 @@
 from cogent3.parse.record import FileFormatError
 from cogent3.util.io import atomic_write
 
-
 # todo convert formatters so str(formatter) returns correctly formatted
 # string, and rename method names, rename base class name (sequences, not
 # alignment)
diff --git a/src/cogent3/format/bedgraph.py b/src/cogent3/format/bedgraph.py
index ed33a0dbe..d195b19ad 100644
--- a/src/cogent3/format/bedgraph.py
+++ b/src/cogent3/format/bedgraph.py
@@ -1,6 +1,5 @@
 from cogent3.util.misc import get_merged_by_value_coords
 
-
 # following from https://cgwb.nci.nih.gov/goldenPath/help/bedgraph.html
 # track type=bedGraph name=track_label description=center_label
 #        visibility=display_mode color=r,g,b altColor=r,g,b
diff --git a/src/cogent3/format/clustal.py b/src/cogent3/format/clustal.py
index a8283f69f..de2f1b4d6 100644
--- a/src/cogent3/format/clustal.py
+++ b/src/cogent3/format/clustal.py
@@ -2,6 +2,7 @@
 """
 Writer for Clustal format.
 """
+
 from copy import copy
 
 from cogent3.core.alignment import SequenceCollection
diff --git a/src/cogent3/format/fasta.py b/src/cogent3/format/fasta.py
index 095596bcc..29fe3433e 100644
--- a/src/cogent3/format/fasta.py
+++ b/src/cogent3/format/fasta.py
@@ -1,8 +1,6 @@
-"""Writer for FASTA sequence format
-"""
+"""Writer for FASTA sequence format"""
 
 import textwrap
-
 from typing import Optional
 
 from cogent3.format.util import _AlignmentFormatter
diff --git a/src/cogent3/format/gde.py b/src/cogent3/format/gde.py
index 42485c45d..ec8effa62 100644
--- a/src/cogent3/format/gde.py
+++ b/src/cogent3/format/gde.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-"""Writer for GDE sequence format
-"""
+"""Writer for GDE sequence format"""
 
 from cogent3.format.util import _AlignmentFormatter
 
diff --git a/src/cogent3/format/paml.py b/src/cogent3/format/paml.py
index 3520841ae..7662136c5 100644
--- a/src/cogent3/format/paml.py
+++ b/src/cogent3/format/paml.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-"""Writer for PAML sequence format
-"""
+"""Writer for PAML sequence format"""
 
 from cogent3.format.util import _AlignmentFormatter
 
diff --git a/src/cogent3/format/phylip.py b/src/cogent3/format/phylip.py
index 98ac6aee1..aa20b2c61 100644
--- a/src/cogent3/format/phylip.py
+++ b/src/cogent3/format/phylip.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-"""Writer for PHYLIP sequence format
-"""
+"""Writer for PHYLIP sequence format"""
 
 from cogent3.format.util import _AlignmentFormatter
 
diff --git a/src/cogent3/format/table.py b/src/cogent3/format/table.py
index 1f707f84d..82ff4ad43 100644
--- a/src/cogent3/format/table.py
+++ b/src/cogent3/format/table.py
@@ -6,14 +6,13 @@
 Current formats include restructured text (keyed by 'rest'), latex, html,
 columns separated by a provided string, and a simple text format.
 """
+
 import re
 import textwrap
-
 from xml.sax.saxutils import escape
 
 import numpy
 
-
 known_formats = (
     "bedgraph",
     "phylip",
diff --git a/src/cogent3/format/util.py b/src/cogent3/format/util.py
index 5007c2419..428797747 100644
--- a/src/cogent3/format/util.py
+++ b/src/cogent3/format/util.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-"""Alignment formatter template class
-"""
+"""Alignment formatter template class"""
 
 
 class _AlignmentFormatter:
diff --git a/src/cogent3/maths/distance_transform.py b/src/cogent3/maths/distance_transform.py
index 7142f7e84..d6b02ff15 100644
--- a/src/cogent3/maths/distance_transform.py
+++ b/src/cogent3/maths/distance_transform.py
@@ -1,4 +1,4 @@
-""" matrix based distance metrics, and related coordinate transforms
+"""matrix based distance metrics, and related coordinate transforms
 
 functions to compute distance matrices row by row from abundance matrices,
 typically samples (rows) vs. species/OTU's (cols)
@@ -56,7 +56,6 @@
 from typing import Union
 
 import numpy
-
 from numpy import (
     abs,
     all,
@@ -71,9 +70,17 @@
     min,
     multiply,
     nan_to_num,
+    ravel,
+    seterr,
+    shape,
+    sqrt,
+    square,
+    sum,
+    take,
+    where,
+    zeros,
 )
 from numpy import ndim as rank
-from numpy import ravel, seterr, shape, sqrt, square, sum, take, where, zeros
 from numpy.linalg import norm
 
 
diff --git a/src/cogent3/maths/markov.py b/src/cogent3/maths/markov.py
index d836c32c5..5c6cb8d0c 100644
--- a/src/cogent3/maths/markov.py
+++ b/src/cogent3/maths/markov.py
@@ -11,7 +11,7 @@ class TransitionMatrix(object):
         [.9, .0, .1],
         [.0, .9, .1],
         [.5, .5, .0],], Float)
-    >>> T = TransitionMatrix(a, ['x', 'y', 'm'])
+    >>> T = TransitionMatrix(a, ["x", "y", "m"])
     """
 
     def __init__(self, matrix, tags, stationary_probs=None):
diff --git a/src/cogent3/maths/matrix_exponentiation.py b/src/cogent3/maths/matrix_exponentiation.py
index 7b938e7dd..d3ff6822c 100644
--- a/src/cogent3/maths/matrix_exponentiation.py
+++ b/src/cogent3/maths/matrix_exponentiation.py
@@ -12,7 +12,6 @@
 import warnings
 
 import numpy
-
 from numpy.linalg import eig, inv, solve
 
 
diff --git a/src/cogent3/maths/matrix_logarithm.py b/src/cogent3/maths/matrix_logarithm.py
index f16c44d5b..3e51de546 100644
--- a/src/cogent3/maths/matrix_logarithm.py
+++ b/src/cogent3/maths/matrix_logarithm.py
@@ -4,11 +4,24 @@
 
 WARNING: The methods are not robust!
 """
+
 from itertools import combinations
 
-from numpy import allclose, argmin, array, diag, dot, exp, eye
+from numpy import (
+    allclose,
+    argmin,
+    array,
+    diag,
+    dot,
+    exp,
+    eye,
+    isclose,
+    log,
+    ones,
+    pi,
+    zeros,
+)
 from numpy import inner as innerproduct
-from numpy import isclose, log, ones, pi, zeros
 from numpy.linalg import eig as eigenvectors
 from numpy.linalg import inv as inverse
 from numpy.linalg import norm
diff --git a/src/cogent3/maths/measure.py b/src/cogent3/maths/measure.py
index fb789780e..70488ef28 100644
--- a/src/cogent3/maths/measure.py
+++ b/src/cogent3/maths/measure.py
@@ -5,7 +5,6 @@
 from numpy.testing import assert_allclose, assert_equal
 
 import cogent3.util.misc
-
 from cogent3.maths.util import safe_p_log_p, validate_freqs_array
 
 
diff --git a/src/cogent3/maths/optimisers.py b/src/cogent3/maths/optimisers.py
index 2c9f177f2..ff9926f5a 100644
--- a/src/cogent3/maths/optimisers.py
+++ b/src/cogent3/maths/optimisers.py
@@ -1,10 +1,8 @@
-"""Local or Global-then-local optimisation with progress display
-"""
+"""Local or Global-then-local optimisation with progress display"""
 
 from __future__ import annotations
 
 import warnings
-
 from functools import singledispatch
 from typing import Tuple
 
@@ -16,7 +14,6 @@
 from .scipy_optimisers import Powell
 from .simannealingoptimiser import SimulatedAnnealing
 
-
 GlobalOptimiser = SimulatedAnnealing
 LocalOptimiser = Powell
 
@@ -140,9 +137,6 @@ def nf(x):
     return maximise(nf, *args, **kw)
 
 
-@c3warn.deprecated_args(
-    "2024.6", reason="has no effect in this function", discontinued="limit_action"
-)
 @UI.display_wrap
 def maximise(
     f,
diff --git a/src/cogent3/maths/period_numba.py b/src/cogent3/maths/period_numba.py
index cccc6fba4..85eaac2b0 100644
--- a/src/cogent3/maths/period_numba.py
+++ b/src/cogent3/maths/period_numba.py
@@ -1,9 +1,7 @@
 import numba
 import numpy
-
 from numba import njit
 
-
 # turn off code coverage as njit-ted code not accessible to coverage
 
 
diff --git a/src/cogent3/maths/scipy_optimize.py b/src/cogent3/maths/scipy_optimize.py
index e716fab9d..8ddce4ef8 100644
--- a/src/cogent3/maths/scipy_optimize.py
+++ b/src/cogent3/maths/scipy_optimize.py
@@ -21,10 +21,8 @@
 import builtins
 
 import numpy
-
 from numpy import absolute, asarray, atleast_1d, eye, sqrt, squeeze
 
-
 # These have been copied from Numeric's MLab.py
 # I don't think they made the transition to scipy_core
 
diff --git a/src/cogent3/maths/simannealingoptimiser.py b/src/cogent3/maths/simannealingoptimiser.py
index a0ee87172..b68e58088 100644
--- a/src/cogent3/maths/simannealingoptimiser.py
+++ b/src/cogent3/maths/simannealingoptimiser.py
@@ -10,7 +10,6 @@
 """
 
 import random
-
 from collections import deque
 
 import numpy
diff --git a/src/cogent3/maths/stats/__init__.py b/src/cogent3/maths/stats/__init__.py
index f04865c02..5c3aa663a 100644
--- a/src/cogent3/maths/stats/__init__.py
+++ b/src/cogent3/maths/stats/__init__.py
@@ -5,7 +5,6 @@
 working with statistical data.
 """
 
-
 __all__ = [
     "contingency",
     "distribution",
diff --git a/src/cogent3/maths/stats/contingency.py b/src/cogent3/maths/stats/contingency.py
index 6865e86f6..2cbe0ab98 100644
--- a/src/cogent3/maths/stats/contingency.py
+++ b/src/cogent3/maths/stats/contingency.py
@@ -233,9 +233,10 @@ def expected(self, expected):
         if expected.array.min() < 0:
             raise ValueError("negative values encountered")
 
-        assert_allclose(
-            self.observed.array.sum(), expected.array.sum()
-        ), "unequal totals of observed and expected"
+        (
+            assert_allclose(self.observed.array.sum(), expected.array.sum()),
+            "unequal totals of observed and expected",
+        )
 
         self._expected = expected
 
diff --git a/src/cogent3/maths/stats/distribution.py b/src/cogent3/maths/stats/distribution.py
index 7a891b69e..9613e5d4d 100644
--- a/src/cogent3/maths/stats/distribution.py
+++ b/src/cogent3/maths/stats/distribution.py
@@ -25,7 +25,6 @@
 )
 from cogent3.util import warning as c3warn
 
-
 # ndtri import b/c it should be available via this module
 
 
diff --git a/src/cogent3/maths/stats/ks.py b/src/cogent3/maths/stats/ks.py
index c55bcf6be..12db69f38 100644
--- a/src/cogent3/maths/stats/ks.py
+++ b/src/cogent3/maths/stats/ks.py
@@ -5,9 +5,10 @@
 Translated from R 2.4 by Gavin Huttley
 """
 
-from numpy import arange, array, asarray
-from numpy import dot as matrixmultiply
 from numpy import (
+    arange,
+    array,
+    asarray,
     exp,
     fabs,
     floor,
@@ -20,9 +21,9 @@
     sum,
     zeros,
 )
+from numpy import dot as matrixmultiply
 from scipy.special import binom
 
-
 PIO4 = pi / 4
 PIO2 = pi / 2
 INVSQRT2PI = 1 / sqrt(2 * pi)
diff --git a/src/cogent3/maths/stats/number.py b/src/cogent3/maths/stats/number.py
index 30a0e625e..a917c5ae5 100644
--- a/src/cogent3/maths/stats/number.py
+++ b/src/cogent3/maths/stats/number.py
@@ -2,7 +2,6 @@
 from collections.abc import MutableMapping
 
 import numpy
-
 from numpy.testing import assert_allclose
 
 
diff --git a/src/cogent3/maths/stats/period.py b/src/cogent3/maths/stats/period.py
index 151c454dd..8a2ee63ee 100644
--- a/src/cogent3/maths/stats/period.py
+++ b/src/cogent3/maths/stats/period.py
@@ -4,7 +4,6 @@
 
 from cogent3.maths.stats.special import igam
 
-
 try:
     from math import factorial
 except ImportError:  # python version < 2.6
diff --git a/src/cogent3/maths/stats/special.py b/src/cogent3/maths/stats/special.py
index d71b18826..460141d4c 100644
--- a/src/cogent3/maths/stats/special.py
+++ b/src/cogent3/maths/stats/special.py
@@ -6,7 +6,6 @@
 
 from cogent3.util import warning as c3warn
 
-
 log_epsilon = 1e-6  # for threshold in log/exp close to 1
 # For IEEE arithmetic (IBMPC):
 MACHEP = 1.11022302462515654042e-16  # 2**-53
diff --git a/src/cogent3/maths/util.py b/src/cogent3/maths/util.py
index fc07a3a61..a50f43a2f 100644
--- a/src/cogent3/maths/util.py
+++ b/src/cogent3/maths/util.py
@@ -1,11 +1,8 @@
-"""Provides small utility functions for numpy arrays.
-"""
+"""Provides small utility functions for numpy arrays."""
 
 import numpy
-
 from numpy import array, clip, cumsum, searchsorted, sort, sum
 
-
 err = numpy.seterr(divide="raise")
 
 
@@ -54,7 +51,7 @@ def row_uncertainty(a):
 
     Will return 0 for every empty row, but an empty array for every empty column,
     thanks to this sum behavior:
-    >>> sum(array([[]]),1)
+    >>> sum(array([[]]), 1)
     array([0])
     >>> sum(array([[]]))
     zeros((0,), 'l')
@@ -74,7 +71,7 @@ def column_uncertainty(a):
 
     Will return 0 for every empty row, but an empty array for every empty column,
     thanks to this sum behavior:
-    >>> sum(array([[]]),1)
+    >>> sum(array([[]]), 1)
     array([0])
     >>> sum(array([[]]))
     zeros((0,), 'l')
@@ -100,9 +97,9 @@ def row_degeneracy(a, cutoff=0.5):
 
     WARNING: watch out with floating point numbers.
     if the cutoff= 0.9 and in the array is also 0.9, it might not be found
-    >>> searchsorted(cumsum(array([.6,.3,.1])),.9)
+    >>> searchsorted(cumsum(array([0.6, 0.3, 0.1])), 0.9)
     2
-    >>> searchsorted(cumsum(array([.5,.4,.1])),.9)
+    >>> searchsorted(cumsum(array([0.5, 0.4, 0.1])), 0.9)
     1
 
     If the cutoff value is not found, the result is clipped to the
@@ -135,9 +132,9 @@ def column_degeneracy(a, cutoff=0.5):
 
     WARNING: watch out with floating point numbers.
     if the cutoff= 0.9 and in the array is also 0.9, it might not be found
-    >>> searchsorted(cumsum(array([.6,.3,.1])),.9)
+    >>> searchsorted(cumsum(array([0.6, 0.3, 0.1])), 0.9)
     2
-    >>> searchsorted(cumsum(array([.5,.4,.1])),.9)
+    >>> searchsorted(cumsum(array([0.5, 0.4, 0.1])), 0.9)
     1
 
     If the cutoff value is not found, the result is clipped to the
diff --git a/src/cogent3/parse/blast.py b/src/cogent3/parse/blast.py
index ebc58fa98..ede34ec67 100644
--- a/src/cogent3/parse/blast.py
+++ b/src/cogent3/parse/blast.py
@@ -1,5 +1,4 @@
-"""Parsers for blast, psi-blast and blat.
-"""
+"""Parsers for blast, psi-blast and blat."""
 
 from cogent3.parse.record_finder import (
     DelimitedRecordFinder,
@@ -7,7 +6,6 @@
     never_ignore,
 )
 
-
 strip = str.strip
 upper = str.upper
 
diff --git a/src/cogent3/parse/blast_xml.py b/src/cogent3/parse/blast_xml.py
index ffe8fe3d1..ac9866ad9 100644
--- a/src/cogent3/parse/blast_xml.py
+++ b/src/cogent3/parse/blast_xml.py
@@ -1,14 +1,11 @@
-"""Parsers for XML output of blast, psi-blast and blat.
-"""
+"""Parsers for XML output of blast, psi-blast and blat."""
 
 import xml.dom.minidom
-
 from operator import gt as _gt
 from operator import lt as _lt
 
 from cogent3.parse.blast import MinimalBlastParser9, MinimalPsiBlastParser9
 
-
 """
 CAUTION:
 This XML BLAST PARSER uses minidom. This means a bad performance for
diff --git a/src/cogent3/parse/cigar.py b/src/cogent3/parse/cigar.py
index 0abfabc2d..0e7213e4a 100644
--- a/src/cogent3/parse/cigar.py
+++ b/src/cogent3/parse/cigar.py
@@ -20,7 +20,6 @@
 from cogent3 import DNA, make_aligned_seqs
 from cogent3.core.location import IndelMap, LostSpan, Span
 
-
 _pattern = re.compile("([0-9]*)([DM])")
 
 
diff --git a/src/cogent3/parse/clustal.py b/src/cogent3/parse/clustal.py
index c7743049b..73a5cc23f 100644
--- a/src/cogent3/parse/clustal.py
+++ b/src/cogent3/parse/clustal.py
@@ -18,8 +18,8 @@
 silently deletes them. Does not check that the numbers actually correspond to
 the number of chars in the sequence printed so far.
 """
-from cogent3.parse.record import DelimitedSplitter, RecordError
 
+from cogent3.parse.record import DelimitedSplitter, RecordError
 
 strip = str.strip
 
diff --git a/src/cogent3/parse/cogent3_json.py b/src/cogent3/parse/cogent3_json.py
index ceaa580dd..420caf03f 100644
--- a/src/cogent3/parse/cogent3_json.py
+++ b/src/cogent3/parse/cogent3_json.py
@@ -1,5 +1,4 @@
-"""Supports JSON load/read/write operations on major Cogent3 objects.
-"""
+"""Supports JSON load/read/write operations on major Cogent3 objects."""
 
 import json
 
diff --git a/src/cogent3/parse/dialign.py b/src/cogent3/parse/dialign.py
index d76ec8fab..1c541b650 100644
--- a/src/cogent3/parse/dialign.py
+++ b/src/cogent3/parse/dialign.py
@@ -4,7 +4,6 @@
 
 from cogent3 import ASCII
 
-
 _header = re.compile(r"^\s+[=]+")
 _quality_scores = re.compile(r"^ +\d+[\s\d]*$")
 
diff --git a/src/cogent3/parse/ebi.py b/src/cogent3/parse/ebi.py
index 7c07f49b5..e1820001f 100644
--- a/src/cogent3/parse/ebi.py
+++ b/src/cogent3/parse/ebi.py
@@ -1,8 +1,7 @@
 #!/usr/bin/env python
-"""Provide a parser for SwissProt EBI format files.
-"""
-import sys
+"""Provide a parser for SwissProt EBI format files."""
 
+import sys
 from pprint import pprint
 
 from cogent3.core.sequence import Sequence
@@ -14,7 +13,6 @@
 )
 from cogent3.util.misc import NestedSplitter, curry, list_flatten
 
-
 maketrans = str.maketrans
 
 
@@ -1652,7 +1650,5 @@ def EbiParser(
 SQ   SEQUENCE   218 AA;  24367 MW;  F24AE5E8A102FAC6 CRC64;
      MISVMQQMIN NDSPEDSKES ITSVQQTPFF WPSAAAAIPS IQGESRSERE
 //
-""".split(
-            "\n"
-        )
+""".split("\n")
         pprint(list(EbiParser(lines, strict=False, selected_labels=[])))
diff --git a/src/cogent3/parse/fasta.py b/src/cogent3/parse/fasta.py
index e0f1ebd49..d5450e6d5 100644
--- a/src/cogent3/parse/fasta.py
+++ b/src/cogent3/parse/fasta.py
@@ -1,26 +1,22 @@
-"""Parsers for FASTA and related formats.
-"""
+"""Parsers for FASTA and related formats."""
 
 import os
 import pathlib
 import re
 import string
 import typing
-
 from collections.abc import Callable
 from functools import singledispatch
 
 import numpy
 
 import cogent3
-
 from cogent3.core.info import Info
 from cogent3.core.moltype import ASCII, BYTES
 from cogent3.parse.record import RecordError
 from cogent3.parse.record_finder import LabeledRecordFinder
 from cogent3.util.io import open_
 
-
 _white_space = re.compile(r"\s+")
 strip = str.strip
 
diff --git a/src/cogent3/parse/gbseq.py b/src/cogent3/parse/gbseq.py
index a917dbd2c..52a495656 100644
--- a/src/cogent3/parse/gbseq.py
+++ b/src/cogent3/parse/gbseq.py
@@ -2,12 +2,12 @@
 """Parser for NCBI Sequence Set XML format.
 DOCTYPE Bioseq-set PUBLIC "-//NCBI//NCBI Seqset/EN" "http://www.ncbi.nlm.nih.gov/dtd/NCBI_Seqset.dtd"
 """
+
 import io
 import xml.dom.minidom
 
 from cogent3.core import location, moltype
 
-
 """
 CAUTION:
 This XML PARSER uses minidom. This means a bad performance for
diff --git a/src/cogent3/parse/genbank.py b/src/cogent3/parse/genbank.py
index ebf4e9f37..738a3c215 100644
--- a/src/cogent3/parse/genbank.py
+++ b/src/cogent3/parse/genbank.py
@@ -10,7 +10,6 @@
     LabeledRecordFinder,
 )
 
-
 maketrans = str.maketrans
 strip = str.strip
 rstrip = str.rstrip
diff --git a/src/cogent3/parse/gff.py b/src/cogent3/parse/gff.py
index 8e63e0a6d..10deca524 100644
--- a/src/cogent3/parse/gff.py
+++ b/src/cogent3/parse/gff.py
@@ -6,13 +6,11 @@
 import pathlib
 import re
 import typing
-
 from abc import ABC
 
 from cogent3.util import warning as c3warn
 from cogent3.util.io import PathType, open_
 
-
 OptionalCallable = typing.Optional[typing.Callable]
 OptionalStrContainer = typing.Optional[typing.Union[str, typing.Sequence[str]]]
 OptionalIntList = typing.Optional[list[list[int]]]
diff --git a/src/cogent3/parse/jaspar.py b/src/cogent3/parse/jaspar.py
index 1ad462214..36ce5484e 100644
--- a/src/cogent3/parse/jaspar.py
+++ b/src/cogent3/parse/jaspar.py
@@ -5,7 +5,6 @@
 from cogent3.core.moltype import get_moltype
 from cogent3.core.profile import MotifCountsArray
 
-
 _brackets = re.compile(r"\[|\]")
 
 
diff --git a/src/cogent3/parse/locuslink.py b/src/cogent3/parse/locuslink.py
index 80fa61931..d9f97daef 100644
--- a/src/cogent3/parse/locuslink.py
+++ b/src/cogent3/parse/locuslink.py
@@ -51,7 +51,6 @@
 )
 from cogent3.parse.record_finder import LabeledRecordFinder
 
-
 maketrans = str.maketrans
 strip = str.strip
 rstrip = str.rstrip
diff --git a/src/cogent3/parse/ncbi_taxonomy.py b/src/cogent3/parse/ncbi_taxonomy.py
index a6bbe23bd..e8fee667f 100644
--- a/src/cogent3/parse/ncbi_taxonomy.py
+++ b/src/cogent3/parse/ncbi_taxonomy.py
@@ -1,11 +1,10 @@
 #!/usr/bin/env python
-"""Extracts data from NCBI nodes.dmp and names.dmp files.
-"""
+"""Extracts data from NCBI nodes.dmp and names.dmp files."""
+
 from functools import total_ordering
 
 from cogent3.core.tree import TreeNode
 
-
 strip = str.strip
 
 
diff --git a/src/cogent3/parse/newick.py b/src/cogent3/parse/newick.py
index 2813cf84f..1c96da016 100644
--- a/src/cogent3/parse/newick.py
+++ b/src/cogent3/parse/newick.py
@@ -18,7 +18,6 @@
 
 from cogent3.parse.record import FileFormatError
 
-
 EOT = None
 
 
diff --git a/src/cogent3/parse/nexus.py b/src/cogent3/parse/nexus.py
index cdce90719..c4c26cca7 100644
--- a/src/cogent3/parse/nexus.py
+++ b/src/cogent3/parse/nexus.py
@@ -5,13 +5,11 @@
 """
 
 import re
-
 from collections import defaultdict
 
 from cogent3.parse.record import RecordError
 from cogent3.util.io import open_
 
-
 strip = str.strip
 
 
diff --git a/src/cogent3/parse/paml_matrix.py b/src/cogent3/parse/paml_matrix.py
index 5e96db14e..df1acaf86 100644
--- a/src/cogent3/parse/paml_matrix.py
+++ b/src/cogent3/parse/paml_matrix.py
@@ -1,6 +1,5 @@
 import numpy
 
-
 three_letter_order = "ARNDCQEGHILKMFPSTWYV"
 aa_order = numpy.array([ord(aa) for aa in three_letter_order])
 reorder = numpy.argsort(aa_order)
diff --git a/src/cogent3/parse/psl.py b/src/cogent3/parse/psl.py
index 3b1b21918..369661691 100644
--- a/src/cogent3/parse/psl.py
+++ b/src/cogent3/parse/psl.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 """Parser for PSL format (default output by blat).
-   Compatible with blat v.34
+Compatible with blat v.34
 """
 
 from cogent3.util.table import Table
diff --git a/src/cogent3/parse/rdb.py b/src/cogent3/parse/rdb.py
index 72f5d70f0..a527af5db 100644
--- a/src/cogent3/parse/rdb.py
+++ b/src/cogent3/parse/rdb.py
@@ -10,7 +10,6 @@
 from cogent3.parse.record import RecordError
 from cogent3.parse.record_finder import DelimitedRecordFinder
 
-
 strip = str.strip
 maketrans = str.maketrans
 
diff --git a/src/cogent3/parse/record.py b/src/cogent3/parse/record.py
index b1e0d152a..2e23215e7 100644
--- a/src/cogent3/parse/record.py
+++ b/src/cogent3/parse/record.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
-"""Provides support functions and classes for parsers.
-"""
+"""Provides support functions and classes for parsers."""
+
 from copy import deepcopy
 
 from cogent3.util.misc import iterable
diff --git a/src/cogent3/parse/record_finder.py b/src/cogent3/parse/record_finder.py
index 9b9ce11d4..9a880b4e7 100644
--- a/src/cogent3/parse/record_finder.py
+++ b/src/cogent3/parse/record_finder.py
@@ -15,7 +15,6 @@
 
 from cogent3.parse.record import RecordError
 
-
 strip = str.strip
 rstrip = str.rstrip
 
diff --git a/src/cogent3/parse/sequence.py b/src/cogent3/parse/sequence.py
index 8047558d7..71d5cb4f4 100644
--- a/src/cogent3/parse/sequence.py
+++ b/src/cogent3/parse/sequence.py
@@ -20,7 +20,6 @@
 from cogent3.util import warning as c3warn
 from cogent3.util.io import get_format_suffixes, iter_splitlines, open_
 
-
 _lc_to_wc = "".join([[chr(x), "?"]["A" <= chr(x) <= "Z"] for x in range(256)])
 
 
@@ -91,7 +90,7 @@ def __call__(self, data, **kwargs) -> ParserOutputType:
 
     @__call__.register
     def _(self, data: str, **kwargs) -> ParserOutputType:
-        return self(pathlib.Path(data), **kwargs)
+        yield from self._parse(iter_splitlines(data), **kwargs)
 
     @__call__.register
     def _(self, data: pathlib.Path, **kwargs) -> ParserOutputType:
diff --git a/src/cogent3/parse/tinyseq.py b/src/cogent3/parse/tinyseq.py
index 61bfa6930..a7c66dbd5 100644
--- a/src/cogent3/parse/tinyseq.py
+++ b/src/cogent3/parse/tinyseq.py
@@ -7,7 +7,6 @@
 
 from cogent3.core import moltype
 
-
 """
 CAUTION:
 This XML PARSER uses minidom. This means a bad performance for
diff --git a/src/cogent3/parse/tree.py b/src/cogent3/parse/tree.py
index 76c2e049d..7c6030181 100644
--- a/src/cogent3/parse/tree.py
+++ b/src/cogent3/parse/tree.py
@@ -17,10 +17,10 @@
 difficulty.
 
 """
+
 from cogent3.core.tree import PhyloNode
 from cogent3.parse.record import RecordError
 
-
 strip = str.strip
 maketrans = str.maketrans
 
diff --git a/src/cogent3/parse/unigene.py b/src/cogent3/parse/unigene.py
index 35ceba406..7f9e55a41 100644
--- a/src/cogent3/parse/unigene.py
+++ b/src/cogent3/parse/unigene.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
-"""Parsers for the various files in the UniGene database.
-"""
+"""Parsers for the various files in the UniGene database."""
+
 from cogent3.parse.record import (
     ByPairs,
     LineOrientedConstructor,
@@ -12,7 +12,6 @@
 )
 from cogent3.parse.record_finder import GbFinder
 
-
 maketrans = str.maketrans
 strip = str.strip
 rstrip = str.rstrip
diff --git a/src/cogent3/phylo/consensus.py b/src/cogent3/phylo/consensus.py
index 4a3c68549..140264ebb 100644
--- a/src/cogent3/phylo/consensus.py
+++ b/src/cogent3/phylo/consensus.py
@@ -1,7 +1,7 @@
 #! /usr/bin/env python
 """This module implements methods for generating consensus trees from a list of trees"""
-import warnings
 
+import warnings
 from collections import defaultdict
 from itertools import product
 
diff --git a/src/cogent3/phylo/least_squares.py b/src/cogent3/phylo/least_squares.py
index fc6ef3bcf..051141270 100644
--- a/src/cogent3/phylo/least_squares.py
+++ b/src/cogent3/phylo/least_squares.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 import numpy
-
 from numpy.linalg import solve as solve_linear_equations
 
 from .tree_space import TreeEvaluator, ancestry2tree
@@ -10,7 +9,6 @@
     triangular_order,
 )
 
-
 # This is a fairly slow implementation and NOT suitable for large trees.
 
 # Trees are represented as "ancestry" matricies in which A[i,j] iff j is an
diff --git a/src/cogent3/phylo/nj.py b/src/cogent3/phylo/nj.py
index ee1792b61..a093d0f9a 100644
--- a/src/cogent3/phylo/nj.py
+++ b/src/cogent3/phylo/nj.py
@@ -9,7 +9,6 @@
 Generalised as described by Pearson, Robins & Zhang, 1999.
 """
 
-
 from collections import deque
 
 import numpy
diff --git a/src/cogent3/phylo/tree_distance.py b/src/cogent3/phylo/tree_distance.py
index ba6c75c60..e1c9aa938 100644
--- a/src/cogent3/phylo/tree_distance.py
+++ b/src/cogent3/phylo/tree_distance.py
@@ -2,10 +2,8 @@
 from typing import TYPE_CHECKING
 
 import numpy as np
-
 from scipy.optimize import linear_sum_assignment
 
-
 if TYPE_CHECKING:
     from cogent3.core.tree import TreeNode
 
diff --git a/src/cogent3/phylo/util.py b/src/cogent3/phylo/util.py
index add04c5c3..d09a8f94e 100644
--- a/src/cogent3/phylo/util.py
+++ b/src/cogent3/phylo/util.py
@@ -1,6 +1,5 @@
 import numpy
 
-
 # Distance matricies are presently represented as simple dictionaries, which
 # need to be converted into numpy arrays before being fed into phylogenetic
 # reconstruction algorithms.
diff --git a/src/cogent3/recalculation/calculation.py b/src/cogent3/recalculation/calculation.py
index 2840062b9..2da6d4ba7 100644
--- a/src/cogent3/recalculation/calculation.py
+++ b/src/cogent3/recalculation/calculation.py
@@ -7,7 +7,6 @@
 from cogent3.maths.optimisers import ParameterOutOfBoundsError, maximise
 from cogent3.maths.solve import find_root
 
-
 TRACE_DEFAULT = "COGENT3_TRACE" in os.environ
 TRACE_SCALE = 100000
 
diff --git a/src/cogent3/recalculation/definition.py b/src/cogent3/recalculation/definition.py
index 04b9b48c7..98a74209e 100644
--- a/src/cogent3/recalculation/definition.py
+++ b/src/cogent3/recalculation/definition.py
@@ -75,7 +75,6 @@
 from .scope import ParameterController, _Defn, _LeafDefn, _NonLeafDefn
 from .setting import ConstVal, Var
 
-
 # In this module we bring together scopes, settings and calculations.
 # Most of the classes are 'Defns' with their superclasses in scope.py.
 # These supply a make_cells() method which instantiates 'Cell'
diff --git a/src/cogent3/recalculation/scope.py b/src/cogent3/recalculation/scope.py
index a388af93d..b3b2e2929 100644
--- a/src/cogent3/recalculation/scope.py
+++ b/src/cogent3/recalculation/scope.py
@@ -1,11 +1,9 @@
 #!/usr/bin/env python
 
 import warnings
-
 from contextlib import contextmanager
 
 import numpy
-
 from scipy.stats.distributions import chi2
 
 from cogent3.maths.optimisers import MaximumEvaluationsReached
diff --git a/src/cogent3/util/deserialise.py b/src/cogent3/util/deserialise.py
index f931c4f7b..98b9d9fa1 100644
--- a/src/cogent3/util/deserialise.py
+++ b/src/cogent3/util/deserialise.py
@@ -1,13 +1,10 @@
 import json
 import re
-
 from importlib import import_module
 
 import cogent3
-
 from cogent3.util.io import open_, path_exists
 
-
 _deserialise_func_map = {}
 
 
diff --git a/src/cogent3/util/dict_array.py b/src/cogent3/util/dict_array.py
index 96809f0c0..75a92ae21 100644
--- a/src/cogent3/util/dict_array.py
+++ b/src/cogent3/util/dict_array.py
@@ -1,27 +1,26 @@
 """Wrapper for numpy arrays so that they can be indexed by name
 
-    >>> a = numpy.identity(3, int)
-    >>> b = DictArrayTemplate('abc', 'ABC').wrap(a)
-    >>> b[0]
-    ===========
-    A    B    C
-    -----------
-    1    0    0
-    -----------
-    >>> b['a']
-    ===========
-    A    B    C
-    -----------
-    1    0    0
-    -----------
-    >>> b.keys()
-    ['a', 'b', 'c']
-    >>> b['a'].keys()
-    ['A', 'B', 'C']
+>>> a = numpy.identity(3, int)
+>>> b = DictArrayTemplate("abc", "ABC").wrap(a)
+>>> b[0]
+===========
+A    B    C
+-----------
+1    0    0
+-----------
+>>> b["a"]
+===========
+A    B    C
+-----------
+1    0    0
+-----------
+>>> b.keys()
+['a', 'b', 'c']
+>>> b["a"].keys()
+['A', 'B', 'C']
 """
 
 import json
-
 from collections import defaultdict
 from itertools import combinations, product
 
diff --git a/src/cogent3/util/io.py b/src/cogent3/util/io.py
index f53f65c21..30f92d1c5 100644
--- a/src/cogent3/util/io.py
+++ b/src/cogent3/util/io.py
@@ -1,7 +1,6 @@
 import contextlib
 import shutil
 import uuid
-
 from bz2 import open as bzip_open
 from gzip import open as gzip_open
 from io import TextIOWrapper
@@ -14,11 +13,10 @@
 from urllib.request import urlopen
 from zipfile import ZipFile
 
-from chardet import detect
+from charset_normalizer import detect
 
 from cogent3.util.misc import _wout_period
 
-
 PathType = Union[str, PathLike, PurePath]
 # support prefixes for urls
 _urls = compile("^(http[s]*|file)")
@@ -365,11 +363,14 @@ def iter_splitlines(
     -----
     Loads chunks of data from the file, yields one line at a time
     """
-    path = Path(path)
-    if chunk_size and path.stat().st_size < chunk_size:
-        # file is smaller than provided chunk_size, just
-        # load it all
+    if _urls.search(str(path)):
         chunk_size = None
+    else:
+        path = Path(path)
+        if chunk_size and path.stat().st_size < chunk_size:
+            # file is smaller than provided chunk_size, just
+            # load it all
+            chunk_size = None
 
     with open_(path) as infile:
         last = ""
diff --git a/src/cogent3/util/misc.py b/src/cogent3/util/misc.py
index 7e3b6cc1d..22629dfe6 100644
--- a/src/cogent3/util/misc.py
+++ b/src/cogent3/util/misc.py
@@ -1,5 +1,4 @@
-"""Generally useful utility classes and methods.
-"""
+"""Generally useful utility classes and methods."""
 
 from __future__ import annotations
 
@@ -9,14 +8,12 @@
 import re
 import typing
 import warnings
-
 from random import randint
 from typing import Any, Callable, Tuple
 from urllib.parse import urlparse
 from warnings import warn
 
 import numpy
-
 from numpy import array, finfo, float64, ndarray, zeros
 
 
diff --git a/src/cogent3/util/parallel.py b/src/cogent3/util/parallel.py
index cf81811ac..47cd10f57 100644
--- a/src/cogent3/util/parallel.py
+++ b/src/cogent3/util/parallel.py
@@ -6,7 +6,6 @@
 
 from cogent3.util.misc import extend_docstring_from
 
-
 multiprocessing.set_start_method(
     "forkserver" if sys.platform == "darwin" else "spawn", force=True
 )
diff --git a/src/cogent3/util/table.py b/src/cogent3/util/table.py
index 24f39e03a..d94e52ee8 100644
--- a/src/cogent3/util/table.py
+++ b/src/cogent3/util/table.py
@@ -14,7 +14,6 @@
 import pathlib
 import pickle
 import re
-
 from collections import defaultdict
 from collections.abc import Callable, MutableMapping
 from itertools import product
@@ -30,7 +29,6 @@
 from cogent3.util.misc import extend_docstring_from, get_object_provenance
 from cogent3.util.union_dict import UnionDict
 
-
 try:
     from IPython.display import display
 except ImportError:
diff --git a/src/cogent3/util/transform.py b/src/cogent3/util/transform.py
index f32603d4e..6554ed747 100644
--- a/src/cogent3/util/transform.py
+++ b/src/cogent3/util/transform.py
@@ -13,7 +13,6 @@
 of lists.
 """
 
-
 maketrans = str.maketrans
 
 # standard combinatorial HOF's from Mertz
diff --git a/src/cogent3/util/union_dict.py b/src/cogent3/util/union_dict.py
index 3a4e92f56..9d5c902f5 100644
--- a/src/cogent3/util/union_dict.py
+++ b/src/cogent3/util/union_dict.py
@@ -1,5 +1,4 @@
-"""UnionDict extension of dict.
-"""
+"""UnionDict extension of dict."""
 
 
 class UnionDict(dict):
diff --git a/src/cogent3/util/warning.py b/src/cogent3/util/warning.py
index 28e089477..e0defb135 100644
--- a/src/cogent3/util/warning.py
+++ b/src/cogent3/util/warning.py
@@ -1,6 +1,5 @@
 import functools
 import inspect
-
 from typing import Any, Callable, Optional, Sequence, Tuple
 from warnings import catch_warnings, simplefilter
 from warnings import warn as _warn
@@ -113,7 +112,7 @@ def deprecated_args(
     ...                  discontinued='discontinued_arg',
     ... )
     >>> def my_function(new_name):
-    >>>     # do something here
+    >>> # do something here
 
     When `my_function` is called with the argument `old_arg`, a warning
     will be raised indicating that the argument is deprecated and should
@@ -195,7 +194,8 @@ def deprecated_callable(
     in favour of a new function.
 
     >>> @deprecated_callable(version='2023.6', reason='function rename', new='a_function')
-    >>> def my_function(arg): pass
+    >>> def my_function(arg):
+    ...     pass
 
     """
 


Test patch:
diff --git a/doc/doctest_rsts.py b/doc/doctest_rsts.py
index e510e901c..d6b53d196 100755
--- a/doc/doctest_rsts.py
+++ b/doc/doctest_rsts.py
@@ -2,6 +2,7 @@
 """
 This will doctest all files ending with .rst in this directory.
 """
+
 import os
 import pathlib
 import subprocess
@@ -9,12 +10,9 @@
 
 import click
 import nbformat
-
-from nbconvert.preprocessors import CellExecutionError, ExecutePreprocessor
-
 from cogent3.app.composable import define_app
 from cogent3.util.io import atomic_write
-
+from nbconvert.preprocessors import CellExecutionError, ExecutePreprocessor
 
 DATA_DIR = pathlib.Path(__file__).parent / "data"
 
diff --git a/src/cogent3/maths/stats/test.py b/src/cogent3/maths/stats/test.py
index 8da6102d6..3355c085c 100644
--- a/src/cogent3/maths/stats/test.py
+++ b/src/cogent3/maths/stats/test.py
@@ -1,5 +1,4 @@
-"""Provides standard statistical tests. Tests produce statistic and P-value.
-"""
+"""Provides standard statistical tests. Tests produce statistic and P-value."""
 
 import warnings
 
@@ -22,10 +21,13 @@
     ones,
     ravel,
     sqrt,
+    take,
+    tanh,
+    trace,
+    zeros,
 )
 from numpy import std as _std
 from numpy import sum as npsum
-from numpy import take, tanh, trace, zeros
 from numpy.random import permutation, randint
 from scipy.stats import binom, f, norm, t
 from scipy.stats.distributions import chi2
@@ -41,7 +43,6 @@
     one_minus_exp,
 )
 
-
 # defining globals for the alternate hypotheses
 ALT_TWO_SIDED = "2"
 ALT_LOW = "low"
diff --git a/tests/__init__.py b/tests/__init__.py
index dda49a3df..59ec6097b 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -2,7 +2,6 @@
 import os
 import pathlib
 
-
 os.chdir(pathlib.Path(__file__).parent)
 
 sub_modules = ["test_draw", "test_phylo"]
diff --git a/tests/benchmark.py b/tests/benchmark.py
index d16eeb766..e1d005b9b 100644
--- a/tests/benchmark.py
+++ b/tests/benchmark.py
@@ -11,7 +11,6 @@
 from cogent3.maths import optimisers
 from cogent3.util import parallel
 
-
 ALIGNMENT = load_aligned_seqs(filename="data/brca1.fasta")
 TREE = load_tree(filename="data/murphy.tree")
 
diff --git a/tests/benchmark_aligning.py b/tests/benchmark_aligning.py
index 7b417f09e..c5e606dd0 100644
--- a/tests/benchmark_aligning.py
+++ b/tests/benchmark_aligning.py
@@ -3,7 +3,6 @@
 import time
 
 import numpy
-
 from cogent3 import DNA
 from cogent3.align.align import classic_align_pairwise, make_dna_scoring_dict
 
diff --git a/tests/test_align/test_align.py b/tests/test_align/test_align.py
index fccba75f0..9920ae3b0 100644
--- a/tests/test_align/test_align.py
+++ b/tests/test_align/test_align.py
@@ -1,12 +1,10 @@
 import itertools
 import unittest
 
-import numpy
-import pytest
-
 import cogent3.align.progressive
 import cogent3.evolve.substitution_model
-
+import numpy
+import pytest
 from cogent3 import (
     get_moltype,
     load_aligned_seqs,
@@ -22,7 +20,6 @@
 )
 from cogent3.evolve.models import HKY85, get_model
 
-
 dna_model = cogent3.evolve.substitution_model.TimeReversibleNucleotide(
     model_gaps=False, equal_motif_probs=True
 )
diff --git a/tests/test_align/test_compare.py b/tests/test_align/test_compare.py
index 7566c01e5..370eeb131 100644
--- a/tests/test_align/test_compare.py
+++ b/tests/test_align/test_compare.py
@@ -1,7 +1,6 @@
 from collections import deque
 
 import pytest
-
 from cogent3 import make_seq
 from cogent3.align.pycompare import (
     Kmer,
diff --git a/tests/test_app/test_align.py b/tests/test_app/test_align.py
index f928d23d1..fbf8a0906 100644
--- a/tests/test_app/test_align.py
+++ b/tests/test_app/test_align.py
@@ -2,10 +2,6 @@
 
 import numpy
 import pytest
-
-from numpy import log2
-from numpy.testing import assert_allclose
-
 from cogent3 import (
     DNA,
     get_app,
@@ -34,7 +30,8 @@
 from cogent3.app.composable import NotCompleted
 from cogent3.core.alignment import Aligned, Alignment, ArrayAlignment
 from cogent3.core.location import gap_coords_to_map
-
+from numpy import log2
+from numpy.testing import assert_allclose
 
 _seqs = {
     "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
diff --git a/tests/test_app/test_app_mpi.py b/tests/test_app/test_app_mpi.py
index 1dc2d48b3..650383c28 100644
--- a/tests/test_app/test_app_mpi.py
+++ b/tests/test_app/test_app_mpi.py
@@ -1,7 +1,6 @@
 from pathlib import Path
 
 import pytest
-
 from cogent3 import get_app, open_data_store
 from cogent3.util import parallel
 
diff --git a/tests/test_app/test_composable.py b/tests/test_app/test_composable.py
index 4066bbb75..7df4c6d98 100644
--- a/tests/test_app/test_composable.py
+++ b/tests/test_app/test_composable.py
@@ -1,22 +1,16 @@
 import inspect
 import pickle
 import shutil
-
 from pathlib import Path
 from pickle import dumps, loads
 from typing import Set, Tuple
 from unittest.mock import Mock
 
 import pytest
-
-from numpy import array, ndarray
-from scitrack import CachingLogger
-
 from cogent3 import get_app, make_aligned_seqs, open_data_store
-from cogent3.app import align, evo
+from cogent3.app import align, evo, translate, tree
 from cogent3.app import io as io_app
 from cogent3.app import sample as sample_app
-from cogent3.app import translate, tree
 from cogent3.app import typing as c3types
 from cogent3.app.composable import (
     NON_COMPOSABLE,
@@ -42,6 +36,8 @@
 from cogent3.app.tree import quick_tree
 from cogent3.app.typing import AlignedSeqsType, PairwiseDistanceType
 from cogent3.core.alignment import Alignment, SequenceCollection
+from numpy import array, ndarray
+from scitrack import CachingLogger
 
 
 @pytest.fixture(scope="function")
@@ -1129,3 +1125,17 @@ def delme2(val: c3types.SerialisableType) -> dict:
 
     assert delme2.__doc__ == "my docstring"
     assert delme2.__init__.__doc__.split() == ["Notes", "-----", "body"]
+
+
+def test_bad_wrap():
+    def foo(a: "str") -> int:
+        return int(a)
+
+    with pytest.raises(NotImplementedError):
+        define_app(foo)
+
+    def bar(a: str) -> "int":
+        return int(a)
+
+    with pytest.raises(NotImplementedError):
+        define_app(bar)
diff --git a/tests/test_app/test_data_store.py b/tests/test_app/test_data_store.py
index 679e47b2c..1461996ad 100644
--- a/tests/test_app/test_data_store.py
+++ b/tests/test_app/test_data_store.py
@@ -1,17 +1,13 @@
 import json
 import pathlib
 import shutil
-
 from itertools import product
 from pathlib import Path
 from pickle import dumps, loads
 
 import pytest
-
-from scitrack import get_text_hexdigest
-
-import cogent3.app.io as io_app
-
+from cogent3.app import io as io_app
+from cogent3.app import sample as sample_app
 from cogent3.app.composable import NotCompleted
 from cogent3.app.data_store import (
     _MD5_TABLE,
@@ -30,6 +26,7 @@
 )
 from cogent3.util.table import Table
 from cogent3.util.union_dict import UnionDict
+from scitrack import get_text_hexdigest
 
 
 @pytest.fixture(scope="function")
@@ -743,3 +740,13 @@ def test_directory_data_store_write_compressed(tmp_path):
     )
     got = writer(seqs)  # pylint: disable=not-callable
     assert got, got
+
+
+def test_apply_to_not_completed(nc_dstore, tmp_path):
+    loader = io_app.load_unaligned()
+    num_seqs = sample_app.take_n_seqs(number=3, fixed_choice=False)
+    out_dstore = io_app.open_data_store(tmp_path / "output", suffix="fa", mode="w")
+    writer = io_app.write_seqs(data_store=out_dstore, format="fasta")
+    app = loader + num_seqs + writer
+    fini = app.apply_to(nc_dstore)
+    assert 0 < len(fini.completed) <= len(nc_dstore.completed)
diff --git a/tests/test_app/test_dist.py b/tests/test_app/test_dist.py
index 634662fe4..e3c919edc 100644
--- a/tests/test_app/test_dist.py
+++ b/tests/test_app/test_dist.py
@@ -1,13 +1,8 @@
 import itertools
-
 from tempfile import TemporaryDirectory
 from unittest import TestCase
 
 import pytest
-
-from numpy import log, polyval
-from numpy.testing import assert_allclose
-
 from cogent3 import (
     DNA,
     PROTEIN,
@@ -26,7 +21,8 @@
 )
 from cogent3.evolve.fast_distance import DistanceMatrix, HammingPair, TN93Pair
 from cogent3.maths.distance_transform import jaccard
-
+from numpy import log, polyval
+from numpy.testing import assert_allclose
 
 _seqs1 = {
     "Human": "GCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
diff --git a/tests/test_app/test_evo.py b/tests/test_app/test_evo.py
index e502188f6..fc5128955 100644
--- a/tests/test_app/test_evo.py
+++ b/tests/test_app/test_evo.py
@@ -1,14 +1,10 @@
 import pathlib
-
 from os.path import dirname, join
 from tempfile import TemporaryDirectory
 from unittest import TestCase
 from unittest.mock import MagicMock
 
 import pytest
-
-from numpy.testing import assert_allclose, assert_raises
-
 from cogent3 import (
     get_app,
     load_aligned_seqs,
@@ -26,7 +22,7 @@
 )
 from cogent3.evolve.models import get_model
 from cogent3.util.deserialise import deserialise_object
-
+from numpy.testing import assert_allclose, assert_raises
 
 data_dir = join(dirname(dirname(__file__)), "data")
 
@@ -493,10 +489,26 @@ def test_hyp_split_codon_select_models(self):
         aln = make_aligned_seqs(data=_data, moltype="dna")
         opt_args = dict(max_evaluations=10, limit_action="ignore")
         m1 = evo_app.model(
-            "F81", optimise_motif_probs=False, split_codons=True, opt_args=opt_args
+            "F81",
+            optimise_motif_probs=False,
+            split_codons=True,
+            opt_args=opt_args,
+            param_rules=[
+                {"par_name": "length", "upper": 10.0, "lower": 1e-09},
+            ],
+            lower=1e-06,
+            upper=1000000.0,
         )
         m2 = evo_app.model(
-            "GTR", optimise_motif_probs=False, split_codons=True, opt_args=opt_args
+            "GTR",
+            optimise_motif_probs=False,
+            split_codons=True,
+            opt_args=opt_args,
+            param_rules=[
+                {"par_name": "length", "upper": 10.0, "lower": 1e-09},
+            ],
+            lower=1e-06,
+            upper=1000000.0,
         )
         hyp = evo_app.hypothesis(m1, m2)
         r = hyp(aln)
@@ -919,3 +931,46 @@ def test_model_invalid_tree_func():
             "HKY85",
             tree_func="123",
         )
+
+
+def test_model_bounds_allpar():
+    upper = 10.0
+    lower = 0.5
+    app = get_app(
+        "model",
+        "HKY85",
+        optimise_motif_probs=True,
+        show_progress=False,
+        unique_trees=True,
+        lower=lower,
+        upper=upper,
+    )
+
+    aln = make_aligned_seqs(data=dict(s1="ACGT", s2="ACGC", s3="AAGT"))
+    result = app(aln)
+    rules = result.lf.get_param_rules()
+    par_bounds = {
+        (r["lower"], r["upper"])
+        for r in rules
+        if r["par_name"] == ("kappa" or "length")
+    }
+    assert par_bounds == {(lower, upper)}
+
+
+def test_model_bounds_kappa():
+    upper_kappa = 99
+    lower_kappa = 9
+    app = get_app(
+        "model",
+        "HKY85",
+        optimise_motif_probs=True,
+        show_progress=False,
+        unique_trees=True,
+        param_rules=[{"par_name": "kappa", "upper": upper_kappa, "lower": lower_kappa}],
+    )
+
+    aln = make_aligned_seqs(data=dict(s1="ACGT", s2="ACGC", s3="AAGT"))
+    result = app(aln)
+    rules = result.lf.get_param_rules()
+    kappa_bounds = {(r["lower"], r["upper"]) for r in rules if r["par_name"] == "kappa"}
+    assert kappa_bounds == {(lower_kappa, upper_kappa)}
diff --git a/tests/test_app/test_init.py b/tests/test_app/test_init.py
index 58e788295..0aa862da3 100644
--- a/tests/test_app/test_init.py
+++ b/tests/test_app/test_init.py
@@ -1,13 +1,11 @@
 """testing the default import"""
 
 import os
-
 from pathlib import Path
 from tempfile import TemporaryDirectory
 from unittest import TestCase
 
 import pytest
-
 from cogent3 import app_help, available_apps, get_app, open_data_store
 from cogent3.app.composable import LOADER, WRITER, is_app
 from cogent3.util.table import Table
diff --git a/tests/test_app/test_io.py b/tests/test_app/test_io.py
index 6c450165c..783fddcc0 100644
--- a/tests/test_app/test_io.py
+++ b/tests/test_app/test_io.py
@@ -9,9 +9,6 @@
 
 import numpy
 import pytest
-
-from numpy.testing import assert_allclose
-
 from cogent3 import get_app, get_moltype, open_data_store
 from cogent3.app import io as io_app
 from cogent3.app.composable import NotCompleted, source_proxy
@@ -29,7 +26,7 @@
 from cogent3.parse.sequence import PARSERS
 from cogent3.util.deserialise import deserialise_object
 from cogent3.util.table import Table
-
+from numpy.testing import assert_allclose
 
 DNA = get_moltype("dna")
 
diff --git a/tests/test_app/test_plugins.py b/tests/test_app/test_plugins.py
index 3b5ae1348..38347e5ca 100644
--- a/tests/test_app/test_plugins.py
+++ b/tests/test_app/test_plugins.py
@@ -1,280 +1,276 @@
-import random
-import string
-
-from importlib.metadata import EntryPoint
-from unittest.mock import patch
-
-import pytest
-
-from stevedore import extension
-from stevedore.extension import ExtensionManager
-
-import cogent3
-
-from cogent3.app import (
-    _make_apphelp_docstring,
-    app_help,
-    available_apps,
-    get_app,
-)
-from cogent3.app.composable import define_app
-from cogent3.util.table import Table
-
-
-@pytest.fixture
-def extension_manager_factory():
-    """Fixture to create mocked ExtensionManager instances with given extensions."""
-
-    def _factory(extensions):
-        with patch("stevedore.ExtensionManager") as mock_manager_constructor:
-            mock_manager = ExtensionManager.make_test_instance(
-                extensions=extensions, namespace="TESTING"
-            )
-            mock_manager_constructor.return_value = mock_manager
-            return mock_manager
-
-    return _factory
-
-
-@pytest.fixture
-def mock_extension_manager(extension_manager_factory, monkeypatch):
-    """Fixture to mock the ExtensionManager with the given extensions."""
-
-    def _mock_extension_manager(extensions):
-        # Create a mocked ExtensionManager with the specified mock extensions
-        mocked_manager = extension_manager_factory(extensions)
-        # Patch the __apps variable in cogent3.apps module to use the mocked_manager
-        monkeypatch.setattr(cogent3.app, "__apps", mocked_manager)
-        return mocked_manager
-
-    return _mock_extension_manager
-
-
-def create_extension(
-    plugin: object, name: str = None, module_name: str = "module1"
-) -> extension.Extension:
-    if name is None:
-        name = plugin.__name__
-    return extension.Extension(
-        name=name,
-        entry_point=EntryPoint(
-            name=name, value=f"{module_name}:{name}", group="TESTING"
-        ),
-        plugin=plugin,
-        obj=None,
-    )
-
-
-def test_install_app_class(mock_extension_manager):
-    @define_app
-    class uppercase:
-        """Test app that converts a string to uppercase"""
-
-        def main(self, data: str) -> str:
-            return data.upper()
-
-    mock_extension_manager([create_extension(uppercase)])
-
-    appercase = get_app("uppercase")
-    assert appercase("hello") == "HELLO"
-    assert appercase.__doc__ in _make_apphelp_docstring(appercase.__class__)
-
-
-def test_install_app_function(mock_extension_manager):
-    @define_app
-    def uppercase(data: str) -> str:
-        """Test function that converts a string to uppercase"""
-        return data.upper()
-
-    mock_extension_manager([create_extension(uppercase)])
-
-    appercase = get_app("uppercase")
-    assert appercase("hello") == "HELLO"
-    assert appercase.__doc__ in _make_apphelp_docstring(appercase.__class__)
-
-
-@pytest.mark.parametrize("app_doc", [None, "text"])
-@pytest.mark.parametrize("init_doc", [None, "text"])
-def test_app_docs(mock_extension_manager, app_doc, init_doc):
-    @define_app
-    class documented_app:
-        """This is a test app that has a __init__, and a docstring"""
-
-        def __init__(self):
-            self.constant = 2
-
-        def main(self, val: int) -> int:
-            return val + self.constant
-
-    mock_extension_manager([create_extension(documented_app)])
-
-    assert cogent3.app.get_app_manager().names() == ["documented_app"]
-    app = get_app("documented_app")
-    app.__class__.__doc__ = app_doc
-    app.__class__.__init__.__doc__ = init_doc
-    app_help("documented_app")
-    got = _make_apphelp_docstring(app.__class__)
-    assert "Options" in got
-
-
-def test_namespace_collision(mock_extension_manager):
-    @define_app
-    class app1:
-        def main(self, data: str) -> str:
-            return data.upper()
-
-    @define_app
-    class app2:
-        def main(self, data: str) -> str:
-            return data.lower()
-
-    # create two apps with the same name and different modules
-    mock_extension_manager(
-        [
-            create_extension(app1, module_name="module1"),
-            create_extension(app2, name="app1", module_name="module2"),
-        ]
-    )
-
-    assert cogent3.app.get_app_manager().names() == ["app1", "app1"]
-
-    with pytest.raises(NameError):
-        _ = get_app(
-            "app1"
-        )  # request app by name only, when there are multiple apps with the same name
-
-    app_by_module_name_1 = get_app("module1.app1")  # request app by name and module
-    app_by_module_name_2 = get_app("module2.app1")
-    assert app_by_module_name_1("Hello") == "HELLO"
-    assert app_by_module_name_2("Hello") == "hello"
-
-    composition = app_by_module_name_1 + app_by_module_name_2
-    assert composition("Hello") == "hello"
-
-
-def test_available_apps_local(mock_extension_manager):
-    """available_apps robust to local scope apps"""
-
-    @define_app
-    def dummy(val: int) -> int:
-        return val
-
-    mock_extension_manager([create_extension(dummy)])
-    apps = available_apps()
-    assert isinstance(apps, Table)
-    apps = apps.filtered(lambda x: dummy.__name__ == x, columns="name")
-    assert apps.shape[0] == 1
-
-
-def test_stevedore_finds_non_apps(mock_extension_manager):
-    """available_apps should return plugins that fail is_app() but emit a warning"""
-
-    def not_an_app(val: int) -> int:
-        return val
-
-    with pytest.warns(UserWarning, match=r".* is not a valid cogent3 app, skipping"):
-        mock_extension_manager([create_extension(not_an_app)])
-        apps = available_apps()
-        assert isinstance(apps, Table)
-        apps = apps.filtered(lambda x: not_an_app.__name__ == x, columns="name")
-        assert apps.shape[0] == 1
-
-
-def test_unknown_app_name(mock_extension_manager):
-    """get_app should raise a ValueError if the app name is not found"""
-
-    mock_extension_manager([])
-    unknown_app_name = "".join(random.choices(string.ascii_lowercase, k=10))
-
-    with pytest.raises(
-        ValueError, match=f"App '{unknown_app_name}' not found. Please check for typos."
-    ):
-        _ = get_app(unknown_app_name)
-
-
-def test_unknown_module_name(mock_extension_manager):
-    """get_app should raise a ValueError if the app name is not found"""
-
-    @define_app
-    def dummy(val: int) -> int:
-        return val
-
-    mock_extension_manager([create_extension(dummy, module_name="module1")])
-
-    assert dummy.__name__ in cogent3.app.get_app_manager().names()
-    assert get_app(dummy.__name__)(5) == 5
-    with pytest.raises(ValueError, match=".* not found. Please check for typos."):
-        _ = get_app(".".join(["module_2", dummy.__name__]))
-
-
-def test_app_help_from_instance(mock_extension_manager):
-    """_make_apphelp_docstring(instance) should return help on the app class"""
-
-    @define_app
-    class DummyApp:
-        def __init__(self, a: int = 7919):
-            self.a = a
-
-        def main(self, data: str = "foo") -> str:
-            return data.upper()
-
-    mock_extension_manager([create_extension(DummyApp, module_name="module1")])
-
-    assert DummyApp.__name__ in cogent3.app.get_app_manager().names()
-    dummy_instance = DummyApp()
-    got = _make_apphelp_docstring(dummy_instance)
-
-    assert "Options" in got  # test help is rendered
-    assert "DummyApp_app" in got  # test the help is for the correct app
-    assert (
-        "7919" in got
-    )  # test signature rendering is accurate and detailed and includes default of the 1000th prime
-
-
-def test_app_with_app_as_default(mock_extension_manager):
-    """apps can be initialized with other apps as arguments"""
-
-    @define_app
-    class AddApp:
-        def __init__(self, seed: int):
-            self.seed = seed
-
-        def main(self, data: int) -> int:
-            return data + self.seed
-
-    @define_app
-    class AppWithDefault:
-        def __init__(self, app: AddApp = AddApp(37)):
-            self.app = app
-
-        def main(self, data: int) -> int:
-            return self.app.main(data)
-
-    mock_extension_manager([create_extension(AddApp), create_extension(AppWithDefault)])
-
-    assert AppWithDefault.__name__ in cogent3.app.get_app_manager().names()
-    assert "AddApp(seed=37)" in _make_apphelp_docstring(AppWithDefault)
-    app_with_default_addapp = get_app("AppWithDefault")
-    assert app_with_default_addapp(5) == 42
-    app_with_custom_addapp = get_app("AppWithDefault", app=AddApp(10))
-    assert app_with_custom_addapp(5) == 15
-
-
-def test_app_help_from_function(mock_extension_manager):
-    """_make_apphelp_docstring on a decorated function should return help"""
-
-    @define_app
-    def square(val: int) -> int:
-        """app that returns the square of the input value"""
-        return val * val
-
-    mock_extension_manager([create_extension(square, module_name="module1")])
-
-    assert square.__name__ in cogent3.app.get_app_manager().names()
-    got = _make_apphelp_docstring(square)
-
-    assert "Options" in got  # test help is rendered
-    assert "square_app" in got  # test the help is for the correct app
-    assert (
-        "the square of the input" in got
-    )  # test the docstring is included in the help
+import random
+import string
+from importlib.metadata import EntryPoint
+from unittest.mock import patch
+
+import cogent3
+import pytest
+from cogent3.app import (
+    _make_apphelp_docstring,
+    app_help,
+    available_apps,
+    get_app,
+)
+from cogent3.app.composable import define_app
+from cogent3.util.table import Table
+from stevedore import extension
+from stevedore.extension import ExtensionManager
+
+
+@pytest.fixture
+def extension_manager_factory():
+    """Fixture to create mocked ExtensionManager instances with given extensions."""
+
+    def _factory(extensions):
+        with patch("stevedore.ExtensionManager") as mock_manager_constructor:
+            mock_manager = ExtensionManager.make_test_instance(
+                extensions=extensions, namespace="TESTING"
+            )
+            mock_manager_constructor.return_value = mock_manager
+            return mock_manager
+
+    return _factory
+
+
+@pytest.fixture
+def mock_extension_manager(extension_manager_factory, monkeypatch):
+    """Fixture to mock the ExtensionManager with the given extensions."""
+
+    def _mock_extension_manager(extensions):
+        # Create a mocked ExtensionManager with the specified mock extensions
+        mocked_manager = extension_manager_factory(extensions)
+        # Patch the __apps variable in cogent3.apps module to use the mocked_manager
+        monkeypatch.setattr(cogent3.app, "__apps", mocked_manager)
+        return mocked_manager
+
+    return _mock_extension_manager
+
+
+def create_extension(
+    plugin: object, name: str = None, module_name: str = "module1"
+) -> extension.Extension:
+    if name is None:
+        name = plugin.__name__
+    return extension.Extension(
+        name=name,
+        entry_point=EntryPoint(
+            name=name, value=f"{module_name}:{name}", group="TESTING"
+        ),
+        plugin=plugin,
+        obj=None,
+    )
+
+
+def test_install_app_class(mock_extension_manager):
+    @define_app
+    class uppercase:
+        """Test app that converts a string to uppercase"""
+
+        def main(self, data: str) -> str:
+            return data.upper()
+
+    mock_extension_manager([create_extension(uppercase)])
+
+    appercase = get_app("uppercase")
+    assert appercase("hello") == "HELLO"
+    assert appercase.__doc__ in _make_apphelp_docstring(appercase.__class__)
+
+
+def test_install_app_function(mock_extension_manager):
+    @define_app
+    def uppercase(data: str) -> str:
+        """Test function that converts a string to uppercase"""
+        return data.upper()
+
+    mock_extension_manager([create_extension(uppercase)])
+
+    appercase = get_app("uppercase")
+    assert appercase("hello") == "HELLO"
+    assert appercase.__doc__ in _make_apphelp_docstring(appercase.__class__)
+
+
+@pytest.mark.parametrize("app_doc", [None, "text"])
+@pytest.mark.parametrize("init_doc", [None, "text"])
+def test_app_docs(mock_extension_manager, app_doc, init_doc):
+    @define_app
+    class documented_app:
+        """This is a test app that has a __init__, and a docstring"""
+
+        def __init__(self):
+            self.constant = 2
+
+        def main(self, val: int) -> int:
+            return val + self.constant
+
+    mock_extension_manager([create_extension(documented_app)])
+
+    assert cogent3.app.get_app_manager().names() == ["documented_app"]
+    app = get_app("documented_app")
+    app.__class__.__doc__ = app_doc
+    app.__class__.__init__.__doc__ = init_doc
+    app_help("documented_app")
+    got = _make_apphelp_docstring(app.__class__)
+    assert "Options" in got
+
+
+def test_namespace_collision(mock_extension_manager):
+    @define_app
+    class app1:
+        def main(self, data: str) -> str:
+            return data.upper()
+
+    @define_app
+    class app2:
+        def main(self, data: str) -> str:
+            return data.lower()
+
+    # create two apps with the same name and different modules
+    mock_extension_manager(
+        [
+            create_extension(app1, module_name="module1"),
+            create_extension(app2, name="app1", module_name="module2"),
+        ]
+    )
+
+    assert cogent3.app.get_app_manager().names() == ["app1", "app1"]
+
+    with pytest.raises(NameError):
+        _ = get_app(
+            "app1"
+        )  # request app by name only, when there are multiple apps with the same name
+
+    app_by_module_name_1 = get_app("module1.app1")  # request app by name and module
+    app_by_module_name_2 = get_app("module2.app1")
+    assert app_by_module_name_1("Hello") == "HELLO"
+    assert app_by_module_name_2("Hello") == "hello"
+
+    composition = app_by_module_name_1 + app_by_module_name_2
+    assert composition("Hello") == "hello"
+
+
+def test_available_apps_local(mock_extension_manager):
+    """available_apps robust to local scope apps"""
+
+    @define_app
+    def dummy(val: int) -> int:
+        return val
+
+    mock_extension_manager([create_extension(dummy)])
+    apps = available_apps()
+    assert isinstance(apps, Table)
+    apps = apps.filtered(lambda x: dummy.__name__ == x, columns="name")
+    assert apps.shape[0] == 1
+
+
+def test_stevedore_finds_non_apps(mock_extension_manager):
+    """available_apps should return plugins that fail is_app() but emit a warning"""
+
+    def not_an_app(val: int) -> int:
+        return val
+
+    with pytest.warns(UserWarning, match=r".* is not a valid cogent3 app, skipping"):
+        mock_extension_manager([create_extension(not_an_app)])
+        apps = available_apps()
+        assert isinstance(apps, Table)
+        apps = apps.filtered(lambda x: not_an_app.__name__ == x, columns="name")
+        assert apps.shape[0] == 1
+
+
+def test_unknown_app_name(mock_extension_manager):
+    """get_app should raise a ValueError if the app name is not found"""
+
+    mock_extension_manager([])
+    unknown_app_name = "".join(random.choices(string.ascii_lowercase, k=10))
+
+    with pytest.raises(
+        ValueError, match=f"App '{unknown_app_name}' not found. Please check for typos."
+    ):
+        _ = get_app(unknown_app_name)
+
+
+def test_unknown_module_name(mock_extension_manager):
+    """get_app should raise a ValueError if the app name is not found"""
+
+    @define_app
+    def dummy(val: int) -> int:
+        return val
+
+    mock_extension_manager([create_extension(dummy, module_name="module1")])
+
+    assert dummy.__name__ in cogent3.app.get_app_manager().names()
+    assert get_app(dummy.__name__)(5) == 5
+    with pytest.raises(ValueError, match=".* not found. Please check for typos."):
+        _ = get_app(".".join(["module_2", dummy.__name__]))
+
+
+def test_app_help_from_instance(mock_extension_manager):
+    """_make_apphelp_docstring(instance) should return help on the app class"""
+
+    @define_app
+    class DummyApp:
+        def __init__(self, a: int = 7919):
+            self.a = a
+
+        def main(self, data: str = "foo") -> str:
+            return data.upper()
+
+    mock_extension_manager([create_extension(DummyApp, module_name="module1")])
+
+    assert DummyApp.__name__ in cogent3.app.get_app_manager().names()
+    dummy_instance = DummyApp()
+    got = _make_apphelp_docstring(dummy_instance)
+
+    assert "Options" in got  # test help is rendered
+    assert "DummyApp_app" in got  # test the help is for the correct app
+    assert (
+        "7919" in got
+    )  # test signature rendering is accurate and detailed and includes default of the 1000th prime
+
+
+def test_app_with_app_as_default(mock_extension_manager):
+    """apps can be initialized with other apps as arguments"""
+
+    @define_app
+    class AddApp:
+        def __init__(self, seed: int):
+            self.seed = seed
+
+        def main(self, data: int) -> int:
+            return data + self.seed
+
+    @define_app
+    class AppWithDefault:
+        def __init__(self, app: AddApp = AddApp(37)):
+            self.app = app
+
+        def main(self, data: int) -> int:
+            return self.app.main(data)
+
+    mock_extension_manager([create_extension(AddApp), create_extension(AppWithDefault)])
+
+    assert AppWithDefault.__name__ in cogent3.app.get_app_manager().names()
+    assert "AddApp(seed=37)" in _make_apphelp_docstring(AppWithDefault)
+    app_with_default_addapp = get_app("AppWithDefault")
+    assert app_with_default_addapp(5) == 42
+    app_with_custom_addapp = get_app("AppWithDefault", app=AddApp(10))
+    assert app_with_custom_addapp(5) == 15
+
+
+def test_app_help_from_function(mock_extension_manager):
+    """_make_apphelp_docstring on a decorated function should return help"""
+
+    @define_app
+    def square(val: int) -> int:
+        """app that returns the square of the input value"""
+        return val * val
+
+    mock_extension_manager([create_extension(square, module_name="module1")])
+
+    assert square.__name__ in cogent3.app.get_app_manager().names()
+    got = _make_apphelp_docstring(square)
+
+    assert "Options" in got  # test help is rendered
+    assert "square_app" in got  # test the help is for the correct app
+    assert (
+        "the square of the input" in got
+    )  # test the docstring is included in the help
diff --git a/tests/test_app/test_result.py b/tests/test_app/test_result.py
index 018f2cb0c..312bb258b 100644
--- a/tests/test_app/test_result.py
+++ b/tests/test_app/test_result.py
@@ -1,5 +1,4 @@
 import pathlib
-
 from unittest import TestCase
 
 from cogent3 import make_aligned_seqs, make_table
diff --git a/tests/test_app/test_sample.py b/tests/test_app/test_sample.py
index eb1ca31dc..b2ae2f36f 100644
--- a/tests/test_app/test_sample.py
+++ b/tests/test_app/test_sample.py
@@ -1,7 +1,6 @@
 from unittest import TestCase
 
 import pytest
-
 from cogent3 import DNA, make_aligned_seqs, make_unaligned_seqs
 from cogent3.app import composable, sample
 from cogent3.app.composable import NotCompleted
diff --git a/tests/test_app/test_sqlite_data_store.py b/tests/test_app/test_sqlite_data_store.py
index 1c12e94de..e9448f054 100644
--- a/tests/test_app/test_sqlite_data_store.py
+++ b/tests/test_app/test_sqlite_data_store.py
@@ -3,9 +3,6 @@
 from pickle import dumps, loads
 
 import pytest
-
-from scitrack import get_text_hexdigest
-
 from cogent3 import get_app, open_data_store
 from cogent3.app.composable import NotCompleted
 from cogent3.app.data_store import (
@@ -23,6 +20,7 @@
     open_sqlite_db_rw,
 )
 from cogent3.util.table import Table
+from scitrack import get_text_hexdigest
 
 
 @pytest.fixture(scope="function")
diff --git a/tests/test_app/test_tree.py b/tests/test_app/test_tree.py
index 594b3a71b..3f8d24d9b 100644
--- a/tests/test_app/test_tree.py
+++ b/tests/test_app/test_tree.py
@@ -1,9 +1,7 @@
 import pathlib
-
 from unittest import TestCase
 
 import pytest
-
 from cogent3 import DNA, load_aligned_seqs, make_aligned_seqs, make_tree
 from cogent3.app import dist
 from cogent3.app import tree as tree_app
@@ -11,7 +9,6 @@
 from cogent3.core.tree import PhyloNode, TreeNode
 from cogent3.evolve.fast_distance import DistanceMatrix
 
-
 DATA_DIR = pathlib.Path(__file__).parent.parent / "data"
 
 
diff --git a/tests/test_app/test_typing.py b/tests/test_app/test_typing.py
index 080cd4421..870975ee8 100644
--- a/tests/test_app/test_typing.py
+++ b/tests/test_app/test_typing.py
@@ -1,9 +1,7 @@
 import sys
-
 from typing import List, Set, Tuple, Union
 
 import pytest
-
 from cogent3.app.typing import (
     AlignedSeqsType,
     IdentifierType,
diff --git a/tests/test_cluster/test_UPGMA.py b/tests/test_cluster/test_UPGMA.py
index e399240fc..762768566 100644
--- a/tests/test_cluster/test_UPGMA.py
+++ b/tests/test_cluster/test_UPGMA.py
@@ -1,8 +1,5 @@
 from unittest import TestCase
 
-from numpy import array
-from numpy.testing import assert_allclose
-
 from cogent3 import make_tree
 from cogent3.cluster.UPGMA import (
     UPGMA_cluster,
@@ -14,6 +11,8 @@
 )
 from cogent3.core.tree import PhyloNode
 from cogent3.util.dict_array import DictArray
+from numpy import array
+from numpy.testing import assert_allclose
 
 
 class UPGMATests(TestCase):
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index d366eccac..cbcd423c2 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -3,17 +3,12 @@
 import os
 import pathlib
 import re
-
 from os import remove
 from tempfile import TemporaryDirectory, mktemp
 from unittest import TestCase
 
 import numpy
 import pytest
-
-from numpy import array, log2, nan
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3 import (
     get_app,
     get_code,
@@ -45,7 +40,8 @@
 from cogent3.maths.util import safe_p_log_p
 from cogent3.parse.fasta import MinimalFastaParser
 from cogent3.util.misc import get_object_provenance
-
+from numpy import array, log2, nan
+from numpy.testing import assert_allclose, assert_equal
 
 DNA = get_moltype("dna")
 RNA = get_moltype("rna")
diff --git a/tests/test_core/test_alphabet.py b/tests/test_core/test_alphabet.py
index b0206fad9..aa9ec413a 100644
--- a/tests/test_core/test_alphabet.py
+++ b/tests/test_core/test_alphabet.py
@@ -4,15 +4,10 @@
 """
 
 import pickle
-
 from unittest import TestCase
 
 import numpy
 import pytest
-
-from numpy import unravel_index
-from numpy.testing import assert_equal
-
 from cogent3.core.alphabet import (
     CharAlphabet,
     Enumeration,
@@ -27,7 +22,8 @@
     uint64,
 )
 from cogent3.core.moltype import RNA
-
+from numpy import unravel_index
+from numpy.testing import assert_equal
 
 DnaBases = CharAlphabet("TCAG")
 RnaBases = CharAlphabet("UCAG")
diff --git a/tests/test_core/test_annotation.py b/tests/test_core/test_annotation.py
index 9567b00a8..52252887e 100644
--- a/tests/test_core/test_annotation.py
+++ b/tests/test_core/test_annotation.py
@@ -1,7 +1,6 @@
 import unittest
 
 import pytest
-
 from cogent3 import (
     get_moltype,
     load_seq,
@@ -12,7 +11,6 @@
 from cogent3.core.annotation_db import BasicAnnotationDb
 from cogent3.core.location import FeatureMap, Span
 
-
 DNA = get_moltype("dna")
 
 
diff --git a/tests/test_core/test_annotation_db.py b/tests/test_core/test_annotation_db.py
index 0828e98de..f6430b071 100644
--- a/tests/test_core/test_annotation_db.py
+++ b/tests/test_core/test_annotation_db.py
@@ -3,7 +3,6 @@
 
 import numpy
 import pytest
-
 from cogent3 import DNA, SequenceCollection, _Table, load_seq
 from cogent3.core.annotation_db import (
     BasicAnnotationDb,
diff --git a/tests/test_core/test_core_standalone.py b/tests/test_core/test_core_standalone.py
index 812d6ad2d..2b5ee4d32 100644
--- a/tests/test_core/test_core_standalone.py
+++ b/tests/test_core/test_core_standalone.py
@@ -3,11 +3,9 @@
 import pathlib
 import tempfile
 import unittest
-
 from tempfile import TemporaryDirectory
 
 import pytest
-
 from cogent3 import (
     DNA,
     PROTEIN,
@@ -30,7 +28,6 @@
 from cogent3.core.alphabet import AlphabetError
 from cogent3.parse.record import FileFormatError
 
-
 DATA_DIR = pathlib.Path(__file__).parent.parent / "data"
 
 
diff --git a/tests/test_core/test_features.py b/tests/test_core/test_features.py
index e0b72faba..ff7b0006c 100755
--- a/tests/test_core/test_features.py
+++ b/tests/test_core/test_features.py
@@ -2,11 +2,11 @@
 
 import numpy
 import pytest
-
 from cogent3 import ASCII, DNA, get_moltype, make_aligned_seqs
 from cogent3.core.alignment import Alignment, SequenceCollection
 from cogent3.core.annotation import Feature
 from cogent3.core.annotation_db import BasicAnnotationDb, GffAnnotationDb
+
 # Complete version of manipulating sequence annotations
 from cogent3.util.deserialise import deserialise_object
 
diff --git a/tests/test_core/test_genetic_code.py b/tests/test_core/test_genetic_code.py
index 719b0f946..68490f3f1 100644
--- a/tests/test_core/test_genetic_code.py
+++ b/tests/test_core/test_genetic_code.py
@@ -1,10 +1,9 @@
 #!/usr/bin/env python
-""" Unit tests for Genetic Code classes.
-"""
+"""Unit tests for Genetic Code classes."""
+
 from unittest import TestCase
 
 import pytest
-
 from cogent3 import DNA, RNA
 from cogent3.core.genetic_code import (
     DEFAULT,
diff --git a/tests/test_core/test_info.py b/tests/test_core/test_info.py
index e922a8b60..f9038f067 100644
--- a/tests/test_core/test_info.py
+++ b/tests/test_core/test_info.py
@@ -1,8 +1,7 @@
 #!/usr/bin/env python
-"""Unit tests for Info class and associated objects (DbRef, DbRefs, etc.).
-"""
-import warnings
+"""Unit tests for Info class and associated objects (DbRef, DbRefs, etc.)."""
 
+import warnings
 from unittest import TestCase
 
 from cogent3.core.info import DbRef, DbRefs, Info, _make_list
diff --git a/tests/test_core/test_location.py b/tests/test_core/test_location.py
index 557db1201..2b533c9e1 100644
--- a/tests/test_core/test_location.py
+++ b/tests/test_core/test_location.py
@@ -1,12 +1,10 @@
-"""Unit tests for Span classes.
-"""
+"""Unit tests for Span classes."""
 
 from itertools import combinations
 from unittest import TestCase
 
 import numpy
 import pytest
-
 from cogent3 import DNA, make_seq
 from cogent3.core.location import (
     FeatureMap,
diff --git a/tests/test_core/test_maps.py b/tests/test_core/test_maps.py
index 5eca67498..d384f868e 100644
--- a/tests/test_core/test_maps.py
+++ b/tests/test_core/test_maps.py
@@ -26,7 +26,7 @@ def test_spans(self):
                 self.fail(repr((r, expected)))
 
     def test_get_by_annotation(self):
-        seq = DNA.make_seq("ATCGATCGAT" * 5, name="base")
+        seq = DNA.make_seq(seq="ATCGATCGAT" * 5, name="base")
         seq.add_feature(biotype="test_type", name="test_label", spans=[(5, 10)])
         seq.add_feature(biotype="test_type", name="test_label2", spans=[(15, 18)])
 
diff --git a/tests/test_core/test_moltype.py b/tests/test_core/test_moltype.py
index 6dc44b51a..a990114f0 100644
--- a/tests/test_core/test_moltype.py
+++ b/tests/test_core/test_moltype.py
@@ -1,12 +1,7 @@
 import pickle
-
 from unittest import TestCase
 
 import pytest
-
-# ind some of the standard alphabets to reduce typing
-from numpy.testing import assert_allclose
-
 from cogent3.core import sequence
 from cogent3.core.moltype import (
     DNA,
@@ -32,6 +27,8 @@
 )
 from cogent3.data.molecular_weight import DnaMW, RnaMW
 
+# ind some of the standard alphabets to reduce typing
+from numpy.testing import assert_allclose
 
 RnaBases = RNA.alphabets.base
 DnaBases = DNA.alphabets.base
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index d00ea33e5..8b5fbbc82 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -2,13 +2,11 @@
 import os
 import pathlib
 import re
-
 from warnings import catch_warnings, filterwarnings
 
 import numpy
 import pytest
-
-from cogent3 import load_seq, load_unaligned_seqs, open_
+from cogent3 import load_unaligned_seqs, open_
 from cogent3._version import __version__
 from cogent3.core import new_alignment, new_alphabet, new_moltype, new_sequence
 from cogent3.core.annotation import Feature
@@ -234,7 +232,6 @@ def test_seqs_data_seq_lengths(str_seqs_dict, arr_seqs_dict, alpha):
 
 
 def test_seqs_data_get_seq_array(str_seqs_dict, alpha):
-    # todo: kath, edit and parametrize for all moltypes
     # seq1
     expect = numpy.array([2, 1, 3, 0], dtype="uint8")
     sd = new_alignment.SeqsData(data=str_seqs_dict, alphabet=alpha)
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index 3c8eacf2f..03e2c9258 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -2,10 +2,8 @@
 
 import numpy
 import pytest
-
-from numpy.testing import assert_allclose
-
 from cogent3.core import new_alphabet, new_genetic_code, new_moltype
+from numpy.testing import assert_allclose
 
 
 @pytest.mark.parametrize(
diff --git a/tests/test_core/test_new_genetic_code.py b/tests/test_core/test_new_genetic_code.py
index d76aa8d87..714700517 100644
--- a/tests/test_core/test_new_genetic_code.py
+++ b/tests/test_core/test_new_genetic_code.py
@@ -1,5 +1,4 @@
 import pytest
-
 from cogent3.core import new_genetic_code
 
 
diff --git a/tests/test_core/test_new_moltype.py b/tests/test_core/test_new_moltype.py
index bcdb1a26f..fe19af4d5 100644
--- a/tests/test_core/test_new_moltype.py
+++ b/tests/test_core/test_new_moltype.py
@@ -1,6 +1,5 @@
 import numpy
 import pytest
-
 from cogent3.core import (
     moltype,
     new_alphabet,
@@ -519,3 +518,39 @@ def test_count_variants():
     assert p("H") == 3
     assert p("NRH") == 24
     assert p("AUGCNGUCAG-AURGAUC--GAUHCGAUACGWS") == 96
+
+
+def test_degenerate_from_seq():
+    """RnaMolType degenerate_from_seq should give correct results"""
+    rna = new_moltype.get_moltype("rna")
+    d = rna.degenerate_from_seq
+    # check monomers
+    assert d("A") == "A"
+    assert d("C") == "C"
+    # check seq of monomers
+    assert d("AAA") == "A"
+    # check some 2- to 4-way cases
+    assert d("AG") == "R"
+    assert d("CG") == "S"
+    assert d("ACG") == "V"
+    assert d("AGCU") == "N"
+    # check some cases with gaps
+    assert d("A---ACU") == "?"
+    assert d("AAA-") == "?"
+    assert d("---") == "-"
+    # check mixed case example
+    assert d("AAAAAA") == "A"
+    # check example with degenerate symbols in set
+    assert d("RS") == "V"
+    assert d("RN-") == "?"
+    # check that it works for proteins as well
+    protein = new_moltype.get_moltype("protein")
+    p = protein.degenerate_from_seq
+    assert p("A") == "A"
+    assert p("AAA") == "A"
+    assert p("DN") == "B"
+    assert p("---") == "-"
+    assert p("ACD") == "X"
+    assert p("ABD") == "X"
+    assert p("ACX") == "X"
+    assert p("AC-") == "?"
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index 1a9ef6f97..96c25fab9 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -2,14 +2,11 @@
 import os
 import re
 import typing
-
 from pickle import dumps
 
+import cogent3
 import numpy
 import pytest
-
-import cogent3
-
 from cogent3._version import __version__
 from cogent3.core import (
     new_alphabet,
diff --git a/tests/test_core/test_profile.py b/tests/test_core/test_profile.py
index d8b43ecb2..90289c86c 100644
--- a/tests/test_core/test_profile.py
+++ b/tests/test_core/test_profile.py
@@ -1,14 +1,12 @@
-"""Provides tests for classes and functions in profile.py
-"""
+"""Provides tests for classes and functions in profile.py"""
 
 from collections import Counter
 from unittest import TestCase
 
+from cogent3.core.profile import PSSM, MotifCountsArray, MotifFreqsArray
 from numpy import array, log2, nan, vstack
 from numpy.testing import assert_allclose
 
-from cogent3.core.profile import PSSM, MotifCountsArray, MotifFreqsArray
-
 
 class MotifCountsArrayTests(TestCase):
     def test_construct_succeeds(self):
@@ -284,9 +282,8 @@ def test_relative_entropy(self):
 
     def test_pairwise_jsd(self):
         """correctly constructs pairwise JSD dict"""
-        from numpy.random import random
-
         from cogent3.maths.measure import jsd
+        from numpy.random import random
 
         data = [[0.25, 0.25, 0.25, 0.25], [0.5, 0.5, 0, 0]]
         expect = jsd(data[0], data[1])
@@ -306,9 +303,8 @@ def test_pairwise_jsd(self):
 
     def test_pairwise_jsm(self):
         """correctly constructs pairwise JS metric dict"""
-        from numpy.random import random
-
         from cogent3.maths.measure import jsm
+        from numpy.random import random
 
         data = [[0.25, 0.25, 0.25, 0.25], [0.5, 0.5, 0, 0]]
         expect = jsm(data[0], data[1])
@@ -535,6 +531,6 @@ def test_score_seq_obj(self):
             [0.6, 0.15, 0.05, 0.2],
         ]
         pssm = PSSM(data, "ACTG")
-        seq = DNA.make_seq("".join("ACTG"[i] for i in [3, 1, 2, 0, 2, 2, 3]))
+        seq = DNA.make_seq(seq="".join("ACTG"[i] for i in [3, 1, 2, 0, 2, 2, 3]))
         scores = pssm.score_seq(seq)
         assert_allclose(scores, [-4.481, -5.703, -2.966], atol=1e-3)
diff --git a/tests/test_core/test_seq_aln_integration.py b/tests/test_core/test_seq_aln_integration.py
index ef505e63a..d41d9f965 100644
--- a/tests/test_core/test_seq_aln_integration.py
+++ b/tests/test_core/test_seq_aln_integration.py
@@ -1,12 +1,11 @@
 from unittest import TestCase
 
-from numpy import all as alltrue
-from numpy import array, transpose
-from numpy.testing import assert_equal
-
 from cogent3.core.alignment import Alignment, ArrayAlignment
 from cogent3.core.moltype import RNA
 from cogent3.core.sequence import ArraySequence, RnaSequence
+from numpy import all as alltrue
+from numpy import array, transpose
+from numpy.testing import assert_equal
 
 
 class AllTests(TestCase):
diff --git a/tests/test_core/test_sequence.py b/tests/test_core/test_sequence.py
index ec50f14d0..6b6b415e1 100644
--- a/tests/test_core/test_sequence.py
+++ b/tests/test_core/test_sequence.py
@@ -1,21 +1,14 @@
-"""Unit tests for Sequence class and its subclasses.
-"""
+"""Unit tests for Sequence class and its subclasses."""
 
 import json
 import os
 import re
-
 from pickle import dumps
 from unittest import TestCase
 
+import cogent3
 import numpy
 import pytest
-
-from numpy import array
-from numpy.testing import assert_allclose, assert_equal
-
-import cogent3
-
 from cogent3._version import __version__
 from cogent3.core.alignment import Aligned
 from cogent3.core.moltype import (
@@ -44,6 +37,8 @@
     _coerce_to_seqview,
 )
 from cogent3.util.misc import get_object_provenance
+from numpy import array
+from numpy.testing import assert_allclose, assert_equal
 
 
 class SequenceTests(TestCase):
@@ -1139,7 +1134,7 @@ class SequenceIntegrationTests(TestCase):  # not porting: not supporting ArraySe
 
     def test_regular_to_model(self):
         """Regular sequence should convert to model sequence"""
-        r = RNA.make_seq("AAA", name="x")
+        r = RNA.make_seq(seq="AAA", name="x")
         s = RNA.make_array_seq(seq=r)
         self.assertEqual(str(s), "AAA")
         self.assertEqual(s.moltype, RNA)
diff --git a/tests/test_core/test_tree.py b/tests/test_core/test_tree.py
index ef43a245a..2c5019703 100644
--- a/tests/test_core/test_tree.py
+++ b/tests/test_core/test_tree.py
@@ -1,27 +1,22 @@
-"""Tests of classes for dealing with trees and phylogeny.
-"""
+"""Tests of classes for dealing with trees and phylogeny."""
 
 import json
 import os
 import pathlib
 import random
-
 from copy import copy, deepcopy
 from tempfile import TemporaryDirectory
 from unittest import TestCase
 
 import pytest
-
-from numpy import array
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3 import load_tree, make_tree, open_
 from cogent3._version import __version__
 from cogent3.core.tree import PhyloNode, TreeError, TreeNode
 from cogent3.maths.stats.test import correlation
 from cogent3.parse.tree import DndParser
 from cogent3.util.misc import get_object_provenance
-
+from numpy import array
+from numpy.testing import assert_allclose, assert_equal
 
 base_path = os.path.dirname(os.path.dirname(__file__))
 data_path = os.path.join(base_path, "data")
@@ -879,7 +874,7 @@ def test_tips(self):
     def test_itertips(self):
         """TreeNode itertips should iterate over terminal descendants"""
         tree = self.TreeRoot
-        self.assertEqual([i.name for i in tree.iter_tips()], list("degh")),
+        self.assertEqual([i.name for i in tree.iter_tips()], list("degh"))
 
     def test_nontips(self):
         """TreeNode nontips should return all non-terminal descendants"""
diff --git a/tests/test_data/test_molecular_weight.py b/tests/test_data/test_molecular_weight.py
index 4aae3c304..cd0aa2fb7 100644
--- a/tests/test_data/test_molecular_weight.py
+++ b/tests/test_data/test_molecular_weight.py
@@ -1,11 +1,10 @@
 #!/usr/bin/env python
-"""Tests for molecular weight.
-"""
-from unittest import TestCase
+"""Tests for molecular weight."""
 
-from numpy.testing import assert_allclose
+from unittest import TestCase
 
 from cogent3.data.molecular_weight import ProteinMW, RnaMW
+from numpy.testing import assert_allclose
 
 
 class WeightCalculatorTests(TestCase):
diff --git a/tests/test_draw/test_dendrogram.py b/tests/test_draw/test_dendrogram.py
index bd497a66d..c6a41aedc 100644
--- a/tests/test_draw/test_dendrogram.py
+++ b/tests/test_draw/test_dendrogram.py
@@ -1,7 +1,5 @@
 from unittest import TestCase
 
-from numpy.testing import assert_allclose
-
 from cogent3 import make_tree
 from cogent3.draw.dendrogram import (
     CircularTreeGeometry,
@@ -10,6 +8,7 @@
 )
 from cogent3.util.deserialise import deserialise_object
 from cogent3.util.union_dict import UnionDict
+from numpy.testing import assert_allclose
 
 
 class TestDendro(TestCase):
diff --git a/tests/test_draw/test_dotplot.py b/tests/test_draw/test_dotplot.py
index adc5b897e..2660fa4c8 100644
--- a/tests/test_draw/test_dotplot.py
+++ b/tests/test_draw/test_dotplot.py
@@ -1,13 +1,11 @@
 from unittest import TestCase
 
 import numpy
-
 from cogent3 import get_moltype, make_unaligned_seqs
 from cogent3.core.alignment import Aligned, ArrayAlignment
 from cogent3.core.location import IndelMap
 from cogent3.draw.dotplot import Dotplot, _convert_input, get_align_coords
 
-
 DNA = get_moltype("dna")
 
 
diff --git a/tests/test_draw/test_draw_integration.py b/tests/test_draw/test_draw_integration.py
index b2129376d..06af30ff2 100644
--- a/tests/test_draw/test_draw_integration.py
+++ b/tests/test_draw/test_draw_integration.py
@@ -1,12 +1,11 @@
 import pathlib
 import unittest
 
-from numpy.testing import assert_allclose
-
 from cogent3 import load_aligned_seqs, make_aligned_seqs, make_table
 from cogent3.core.annotation_db import GffAnnotationDb
 from cogent3.draw.drawable import AnnotatedDrawable, Drawable, get_domain
 from cogent3.util.union_dict import UnionDict
+from numpy.testing import assert_allclose
 
 
 def load_alignment(annotate1=False, annotate2=False):
diff --git a/tests/test_draw/test_shapes.py b/tests/test_draw/test_shapes.py
index 6349b607b..7f6480b0e 100644
--- a/tests/test_draw/test_shapes.py
+++ b/tests/test_draw/test_shapes.py
@@ -1,8 +1,7 @@
 from unittest import TestCase
 
-from numpy.testing import assert_allclose
-
 from cogent3.draw import drawable
+from numpy.testing import assert_allclose
 
 
 class Test_Shapes(TestCase):
diff --git a/tests/test_evolve/test_best_likelihood.py b/tests/test_evolve/test_best_likelihood.py
index f1ed80513..aa9764943 100644
--- a/tests/test_evolve/test_best_likelihood.py
+++ b/tests/test_evolve/test_best_likelihood.py
@@ -1,9 +1,6 @@
 import math
-
 from unittest import TestCase
 
-from numpy.testing import assert_allclose
-
 from cogent3 import DNA, make_aligned_seqs
 from cogent3.evolve.best_likelihood import (
     BestLogLikelihood,
@@ -14,7 +11,7 @@
     get_G93_lnL_from_array,
     get_ML_probs,
 )
-
+from numpy.testing import assert_allclose
 
 IUPAC_DNA_ambiguities = "NRYWSKMBDHV"
 
diff --git a/tests/test_evolve/test_bootstrap.py b/tests/test_evolve/test_bootstrap.py
index d8b85b975..a549a3941 100644
--- a/tests/test_evolve/test_bootstrap.py
+++ b/tests/test_evolve/test_bootstrap.py
@@ -6,7 +6,6 @@
 from cogent3 import load_aligned_seqs, load_tree
 from cogent3.evolve import bootstrap, substitution_model
 
-
 base_path = os.getcwd()
 data_path = os.path.join(base_path, "data")
 
diff --git a/tests/test_evolve/test_coevolution.py b/tests/test_evolve/test_coevolution.py
index 54c6e1e67..342b05b11 100755
--- a/tests/test_evolve/test_coevolution.py
+++ b/tests/test_evolve/test_coevolution.py
@@ -2,19 +2,6 @@
 from tempfile import NamedTemporaryFile, mktemp
 from unittest import TestCase
 
-from numpy import (
-    arange,
-    array,
-    e,
-    greater_equal,
-    less_equal,
-    log,
-    nan,
-    sqrt,
-    zeros,
-)
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3 import (
     DNA,
     PROTEIN,
@@ -93,6 +80,18 @@
 )
 from cogent3.maths.stats.number import CategoryCounter
 from cogent3.parse.fasta import MinimalFastaParser
+from numpy import (
+    arange,
+    array,
+    e,
+    greater_equal,
+    less_equal,
+    log,
+    nan,
+    sqrt,
+    zeros,
+)
+from numpy.testing import assert_allclose, assert_equal
 
 
 def _fasta_to_dict(data):
diff --git a/tests/test_evolve/test_distance.py b/tests/test_evolve/test_distance.py
index d401a8721..8caf5be4d 100644
--- a/tests/test_evolve/test_distance.py
+++ b/tests/test_evolve/test_distance.py
@@ -1,13 +1,9 @@
 import os
 import warnings
-
 from unittest import TestCase
 
 import numpy
 import pytest
-
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3 import (
     DNA,
     PROTEIN,
@@ -38,7 +34,7 @@
 from cogent3.evolve.pairwise_distance_numba import (
     fill_diversity_matrix as numba_fill_diversity_matrix,
 )
-
+from numpy.testing import assert_allclose, assert_equal
 
 warnings.filterwarnings("ignore", "Not using MPI as mpi4py not found")
 
diff --git a/tests/test_evolve/test_likelihood_function.py b/tests/test_evolve/test_likelihood_function.py
index 5c32b8ea5..7a9b02bea 100644
--- a/tests/test_evolve/test_likelihood_function.py
+++ b/tests/test_evolve/test_likelihood_function.py
@@ -6,22 +6,17 @@
     testing the likelihood for specified pars
     getting ancestral probs
     simulating sequence (not possible to verify values as random)
-    
+
     checking that the object resets on tree change, model change, etc
 """
 
 import json
 import os
 import warnings
-
 from unittest import TestCase
 
 import numpy
 import pytest
-
-from numpy import dot, ones
-from numpy.testing import assert_allclose
-
 from cogent3 import (
     DNA,
     load_aligned_seqs,
@@ -45,7 +40,8 @@
 from cogent3.maths.matrix_exponential_integration import expected_number_subs
 from cogent3.maths.matrix_exponentiation import PadeExponentiator as expm
 from cogent3.maths.stats.information_criteria import aic, bic
-
+from numpy import dot, ones
+from numpy.testing import assert_allclose
 
 warnings.filterwarnings("ignore", "Motif probs overspecified")
 warnings.filterwarnings("ignore", "Ignoring tree edge lengths")
@@ -539,7 +535,7 @@ def use_root_seq(root_sequence):
             root = simalign.named_seqs["root"]
             self.assertEqual(str(root), str(root_sequence))
 
-        root_sequence = DNA.make_seq("GTAATT")
+        root_sequence = DNA.make_seq(seq="GTAATT")
         use_root_seq(root_sequence)  # as a sequence instance
         use_root_seq("GTAATC")  # as a string
 
diff --git a/tests/test_evolve/test_newq.py b/tests/test_evolve/test_newq.py
index be1af009e..ab8fb9c76 100644
--- a/tests/test_evolve/test_newq.py
+++ b/tests/test_evolve/test_newq.py
@@ -1,10 +1,6 @@
 import warnings
-
 from unittest import TestCase
 
-from numpy import dot, ones
-from numpy.testing import assert_allclose
-
 from cogent3 import (
     DNA,
     load_aligned_seqs,
@@ -22,7 +18,8 @@
     TimeReversibleNucleotide,
 )
 from cogent3.maths.matrix_exponentiation import PadeExponentiator as expm
-
+from numpy import dot, ones
+from numpy.testing import assert_allclose
 
 warnings.filterwarnings("ignore", "Motif probs overspecified")
 warnings.filterwarnings("ignore", "Model not reversible")
diff --git a/tests/test_evolve/test_ns_substitution_model.py b/tests/test_evolve/test_ns_substitution_model.py
index 652cf1c14..a1bef3e72 100644
--- a/tests/test_evolve/test_ns_substitution_model.py
+++ b/tests/test_evolve/test_ns_substitution_model.py
@@ -1,12 +1,7 @@
 import warnings
-
 from unittest import TestCase
 
 import numpy
-
-from numpy import array, dot, empty
-from numpy.testing import assert_allclose
-
 from cogent3 import DNA, make_aligned_seqs, make_tree
 from cogent3.evolve.ns_substitution_model import (
     DiscreteSubstitutionModel,
@@ -21,6 +16,8 @@
 )
 from cogent3.evolve.predicate import MotifChange
 from cogent3.evolve.substitution_model import TimeReversibleNucleotide
+from numpy import array, dot, empty
+from numpy.testing import assert_allclose
 
 
 def _make_likelihood(model, tree, results, is_discrete=False):
diff --git a/tests/test_evolve/test_parameter_controller.py b/tests/test_evolve/test_parameter_controller.py
index c61b3d72b..0968e28d4 100644
--- a/tests/test_evolve/test_parameter_controller.py
+++ b/tests/test_evolve/test_parameter_controller.py
@@ -1,15 +1,11 @@
 import os
 import warnings
-
 from unittest import TestCase
 
-from numpy.testing import assert_allclose, assert_almost_equal
-
 import cogent3.evolve.parameter_controller
 import cogent3.evolve.substitution_model
-
 from cogent3 import make_aligned_seqs, make_tree
-
+from numpy.testing import assert_allclose, assert_almost_equal
 
 base_path = os.getcwd()
 data_path = os.path.join(base_path, "data")
diff --git a/tests/test_evolve/test_simulation.py b/tests/test_evolve/test_simulation.py
index a368c57e9..b7b9e174e 100644
--- a/tests/test_evolve/test_simulation.py
+++ b/tests/test_evolve/test_simulation.py
@@ -5,6 +5,7 @@
  and a Kimura two (really one) parameter model.
 
 The test is to reestimate the parameter values as accurately as possible."""
+
 from cogent3 import make_tree
 from cogent3.evolve import substitution_model
 
diff --git a/tests/test_evolve/test_substitution_model.py b/tests/test_evolve/test_substitution_model.py
index 82c4c9617..f169eb4e8 100644
--- a/tests/test_evolve/test_substitution_model.py
+++ b/tests/test_evolve/test_substitution_model.py
@@ -1,12 +1,10 @@
 import os
-
 from unittest import TestCase
 
 from cogent3.evolve import predicate, substitution_model
 from cogent3.evolve.models import F81, GN, HKY85
 from cogent3.evolve.predicate import MotifChange
 
-
 base_path = os.getcwd()
 data_path = os.path.join(base_path, "data")
 
diff --git a/tests/test_format/test_clustal.py b/tests/test_format/test_clustal.py
index 28fcbad39..0b8763637 100644
--- a/tests/test_format/test_clustal.py
+++ b/tests/test_format/test_clustal.py
@@ -1,5 +1,4 @@
-"""Tests for Clustal sequence format writer.
-"""
+"""Tests for Clustal sequence format writer."""
 
 from unittest import TestCase
 
diff --git a/tests/test_format/test_fasta.py b/tests/test_format/test_fasta.py
index 644bdd676..265487470 100644
--- a/tests/test_format/test_fasta.py
+++ b/tests/test_format/test_fasta.py
@@ -1,5 +1,4 @@
-"""Tests for FASTA sequence format writer.
-"""
+"""Tests for FASTA sequence format writer."""
 
 from unittest import TestCase
 
diff --git a/tests/test_maths/test_distance_transform.py b/tests/test_maths/test_distance_transform.py
index 99ad0b4da..3b3b0464b 100644
--- a/tests/test_maths/test_distance_transform.py
+++ b/tests/test_maths/test_distance_transform.py
@@ -1,12 +1,8 @@
-"""Unit tests for distance_transform.py functions.
-"""
+"""Unit tests for distance_transform.py functions."""
 
 from random import randint, sample, seed
 from unittest import TestCase
 
-from numpy import array, ones, shape, sqrt
-from numpy.testing import assert_allclose
-
 from cogent3.maths.distance_transform import (
     binary_dist_chisq,
     binary_dist_chord,
@@ -41,6 +37,8 @@
     trans_specprof,
     zeros,
 )
+from numpy import array, ones, shape, sqrt
+from numpy.testing import assert_allclose
 
 
 class functionTests(TestCase):
diff --git a/tests/test_maths/test_geometry.py b/tests/test_maths/test_geometry.py
index c3a8ce55f..801558112 100644
--- a/tests/test_maths/test_geometry.py
+++ b/tests/test_maths/test_geometry.py
@@ -3,11 +3,6 @@
 from math import sqrt
 from unittest import TestCase
 
-from numpy import allclose, arange, array, insert, isclose, sum, take
-from numpy.linalg import norm
-from numpy.random import choice, dirichlet
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3.maths.geometry import (
     aitchison_distance,
     alr,
@@ -21,6 +16,10 @@
     multiplicative_replacement,
     sphere_points,
 )
+from numpy import allclose, arange, array, insert, isclose, sum, take
+from numpy.linalg import norm
+from numpy.random import choice, dirichlet
+from numpy.testing import assert_allclose, assert_equal
 
 
 class CenterOfMassTests(TestCase):
diff --git a/tests/test_maths/test_matrix_exponential_integration.py b/tests/test_maths/test_matrix_exponential_integration.py
index b99043379..6f3810572 100644
--- a/tests/test_maths/test_matrix_exponential_integration.py
+++ b/tests/test_maths/test_matrix_exponential_integration.py
@@ -1,14 +1,11 @@
 from unittest import TestCase
 
+import cogent3.maths.matrix_exponentiation as cmme
 import numpy as np
-
+from cogent3.maths import matrix_exponential_integration as expm
 from numpy import array, diag, dot, exp
 from numpy.testing import assert_allclose
 
-import cogent3.maths.matrix_exponentiation as cmme
-
-from cogent3.maths import matrix_exponential_integration as expm
-
 
 class TestIntegratingExponentiator(TestCase):
     def setUp(self) -> None:
diff --git a/tests/test_maths/test_matrix_logarithm.py b/tests/test_maths/test_matrix_logarithm.py
index a25fcecd5..432bbda13 100644
--- a/tests/test_maths/test_matrix_logarithm.py
+++ b/tests/test_maths/test_matrix_logarithm.py
@@ -2,14 +2,13 @@
 
 from unittest import TestCase
 
-from numpy import array
-from numpy.testing import assert_allclose
-
 from cogent3.maths.matrix_logarithm import (
     is_generator_unique,
     logm,
     logm_taylor,
 )
+from numpy import array
+from numpy.testing import assert_allclose
 
 
 class logarithm_tests(TestCase):
diff --git a/tests/test_maths/test_measure.py b/tests/test_maths/test_measure.py
index af664a6fd..08743d4e4 100644
--- a/tests/test_maths/test_measure.py
+++ b/tests/test_maths/test_measure.py
@@ -1,9 +1,5 @@
 from unittest import TestCase
 
-from numpy import diag_indices, dot, finfo, float64
-from numpy.random import random
-from numpy.testing import assert_allclose
-
 from cogent3.maths.matrix_exponentiation import PadeExponentiator
 from cogent3.maths.matrix_logarithm import logm
 from cogent3.maths.measure import (
@@ -12,6 +8,9 @@
     paralinear_continuous_time,
     paralinear_discrete_time,
 )
+from numpy import diag_indices, dot, finfo, float64
+from numpy.random import random
+from numpy.testing import assert_allclose
 
 
 def gen_q_p():
diff --git a/tests/test_maths/test_optimisers.py b/tests/test_maths/test_optimisers.py
index 18ace79e7..572acb7bd 100644
--- a/tests/test_maths/test_optimisers.py
+++ b/tests/test_maths/test_optimisers.py
@@ -1,11 +1,9 @@
 import os
 import sys
-
 from unittest import TestCase
 
 import numpy
 import pytest
-
 from cogent3.maths.optimisers import (
     MaximumEvaluationsReached,
     _standardise_data,
diff --git a/tests/test_maths/test_period.py b/tests/test_maths/test_period.py
index 662eab8e0..e8f636dad 100644
--- a/tests/test_maths/test_period.py
+++ b/tests/test_maths/test_period.py
@@ -1,8 +1,5 @@
 from unittest import TestCase
 
-from numpy import arange, array, convolve, exp, float64, pi, random, sin, zeros
-from numpy.testing import assert_allclose, assert_almost_equal, assert_equal
-
 from cogent3.maths.period import _autocorr_inner2 as py_autocorr_inner
 from cogent3.maths.period import _goertzel_inner as py_goertzel_inner
 from cogent3.maths.period import _ipdft_inner2 as py_ipdft_inner
@@ -10,6 +7,8 @@
 from cogent3.maths.period_numba import autocorr_inner as numba_autocorr_inner
 from cogent3.maths.period_numba import goertzel_inner as numba_goertzel_inner
 from cogent3.maths.period_numba import ipdft_inner as numba_ipdft_inner
+from numpy import arange, array, convolve, exp, float64, pi, random, sin, zeros
+from numpy.testing import assert_allclose, assert_almost_equal, assert_equal
 
 
 class TestPeriod(TestCase):
diff --git a/tests/test_maths/test_stats/test_contingency.py b/tests/test_maths/test_stats/test_contingency.py
index 0ffec9639..68bfda32c 100644
--- a/tests/test_maths/test_stats/test_contingency.py
+++ b/tests/test_maths/test_stats/test_contingency.py
@@ -1,11 +1,9 @@
 from unittest import TestCase
 
 import numpy
-
-from numpy.testing import assert_allclose
-
 from cogent3.maths.stats.contingency import CategoryCounts, calc_expected
 from cogent3.util.dict_array import DictArrayTemplate
+from numpy.testing import assert_allclose
 
 
 class ContingencyTests(TestCase):
diff --git a/tests/test_maths/test_stats/test_distribution.py b/tests/test_maths/test_stats/test_distribution.py
index 3dd7c6d1a..762594c0d 100644
--- a/tests/test_maths/test_stats/test_distribution.py
+++ b/tests/test_maths/test_stats/test_distribution.py
@@ -5,8 +5,6 @@
 
 from unittest import TestCase
 
-from numpy.testing import assert_allclose, assert_almost_equal
-
 from cogent3.maths.stats.distribution import (
     bdtr,
     bdtrc,
@@ -29,6 +27,7 @@
     tprob,
     zprob,
 )
+from numpy.testing import assert_allclose, assert_almost_equal
 
 
 class DistributionsTests(TestCase):
diff --git a/tests/test_maths/test_stats/test_information_criteria.py b/tests/test_maths/test_stats/test_information_criteria.py
index c9f676e91..695defcfc 100644
--- a/tests/test_maths/test_stats/test_information_criteria.py
+++ b/tests/test_maths/test_stats/test_information_criteria.py
@@ -1,8 +1,7 @@
 from unittest import TestCase
 
-from numpy.testing import assert_allclose
-
 from cogent3.maths.stats.information_criteria import aic, bic
+from numpy.testing import assert_allclose
 
 
 class InformationCriteria(TestCase):
diff --git a/tests/test_maths/test_stats/test_jackknife.py b/tests/test_maths/test_stats/test_jackknife.py
index ed601be05..92f351578 100644
--- a/tests/test_maths/test_stats/test_jackknife.py
+++ b/tests/test_maths/test_stats/test_jackknife.py
@@ -1,10 +1,8 @@
 from unittest import TestCase
 
 import numpy as np
-
-from numpy.testing import assert_allclose
-
 from cogent3.maths.stats.jackknife import JackknifeStats
+from numpy.testing import assert_allclose
 
 
 def pmcc(data, axis=1):
diff --git a/tests/test_maths/test_stats/test_ks.py b/tests/test_maths/test_stats/test_ks.py
index 41818de17..31bf011eb 100644
--- a/tests/test_maths/test_stats/test_ks.py
+++ b/tests/test_maths/test_stats/test_ks.py
@@ -1,7 +1,5 @@
 from unittest import TestCase
 
-from numpy.testing import assert_allclose
-
 from cogent3.maths.stats.ks import (
     pkolmogorov1x,
     pkolmogorov2x,
@@ -9,6 +7,7 @@
     psmirnov2x,
 )
 from cogent3.maths.stats.test import ks_boot, ks_test
+from numpy.testing import assert_allclose
 
 
 class KSTests(TestCase):
diff --git a/tests/test_maths/test_stats/test_number.py b/tests/test_maths/test_stats/test_number.py
index e10227068..91afb0300 100644
--- a/tests/test_maths/test_stats/test_number.py
+++ b/tests/test_maths/test_stats/test_number.py
@@ -1,11 +1,9 @@
 from unittest import TestCase
 
 import numpy
-
-from numpy.testing import assert_allclose
-
 from cogent3 import make_aligned_seqs
 from cogent3.maths.stats import number
+from numpy.testing import assert_allclose
 
 
 class TestNumber(TestCase):
diff --git a/tests/test_maths/test_stats/test_period.py b/tests/test_maths/test_stats/test_period.py
index 2e1c03e1a..e824d9847 100644
--- a/tests/test_maths/test_stats/test_period.py
+++ b/tests/test_maths/test_stats/test_period.py
@@ -1,9 +1,6 @@
 from unittest import TestCase
 
 import numpy
-
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3.maths.period import AutoCorrelation, Hybrid, Ipdft, ipdft
 from cogent3.maths.stats.period import (
     SeqToSymbols,
@@ -14,6 +11,7 @@
     g_statistic,
     seq_to_symbols,
 )
+from numpy.testing import assert_allclose, assert_equal
 
 
 class TestPeriodStat(TestCase):
diff --git a/tests/test_maths/test_stats/test_special.py b/tests/test_maths/test_stats/test_special.py
index 40109f97b..7def89e02 100644
--- a/tests/test_maths/test_stats/test_special.py
+++ b/tests/test_maths/test_stats/test_special.py
@@ -1,12 +1,8 @@
-"""Unit tests for special functions used in statistics.
-"""
+"""Unit tests for special functions used in statistics."""
 
 import math
-
 from unittest import TestCase
 
-from numpy.testing import assert_allclose
-
 from cogent3.maths.stats.special import (
     igami,
     incbi,
@@ -16,6 +12,7 @@
     ndtri,
     one_minus_exp,
 )
+from numpy.testing import assert_allclose
 
 
 class SpecialTests(TestCase):
diff --git a/tests/test_maths/test_stats/test_test.py b/tests/test_maths/test_stats/test_test.py
index 3e302801c..4a4cdf897 100644
--- a/tests/test_maths/test_stats/test_test.py
+++ b/tests/test_maths/test_stats/test_test.py
@@ -1,24 +1,7 @@
-"""Unit tests for statistical tests and utility functions.
-"""
+"""Unit tests for statistical tests and utility functions."""
 
 from unittest import TestCase
 
-from numpy import (
-    arange,
-    array,
-    asarray,
-    concatenate,
-    fill_diagonal,
-    isfinite,
-    isnan,
-    logical_and,
-    ones,
-    reshape,
-    testing,
-    tril,
-)
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3.maths.stats.number import NumberCounter
 from cogent3.maths.stats.test import (
     ALT_HIGH,
@@ -77,6 +60,21 @@
     tail,
     z_test,
 )
+from numpy import (
+    arange,
+    array,
+    asarray,
+    concatenate,
+    fill_diagonal,
+    isfinite,
+    isnan,
+    logical_and,
+    ones,
+    reshape,
+    testing,
+    tril,
+)
+from numpy.testing import assert_allclose, assert_equal
 
 
 def is_prob(value):
diff --git a/tests/test_maths/test_util.py b/tests/test_maths/test_util.py
index f94959cda..02b6b910f 100644
--- a/tests/test_maths/test_util.py
+++ b/tests/test_maths/test_util.py
@@ -1,5 +1,4 @@
-"""Provides tests for array.py
-"""
+"""Provides tests for array.py"""
 
 # SUPPORT2425
 # from __future__ import with_statement
@@ -7,9 +6,6 @@
 from unittest import TestCase
 from warnings import filterwarnings
 
-from numpy import array, transpose
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3.maths.util import (
     column_degeneracy,
     column_uncertainty,
@@ -18,7 +14,8 @@
     safe_log,
     safe_p_log_p,
 )
-
+from numpy import array, transpose
+from numpy.testing import assert_allclose, assert_equal
 
 filterwarnings("ignore", "invalid value encountered in", category=RuntimeWarning)
 
diff --git a/tests/test_parse/test_blast.py b/tests/test_parse/test_blast.py
index 6513a3bdf..5a84e9a5c 100644
--- a/tests/test_parse/test_blast.py
+++ b/tests/test_parse/test_blast.py
@@ -1,7 +1,5 @@
 from unittest import TestCase
 
-from numpy.testing import assert_allclose, assert_equal
-
 from cogent3.parse.blast import (
     FastacmdTaxonomyParser,
     GenericBlastParser9,
@@ -21,6 +19,7 @@
     make_label,
     query_finder,
 )
+from numpy.testing import assert_allclose, assert_equal
 
 
 class BlastTests(TestCase):
diff --git a/tests/test_parse/test_blast_xml.py b/tests/test_parse/test_blast_xml.py
index 76a4f24b2..020c327c9 100644
--- a/tests/test_parse/test_blast_xml.py
+++ b/tests/test_parse/test_blast_xml.py
@@ -1,5 +1,4 @@
 import xml.dom.minidom
-
 from unittest import TestCase
 
 from cogent3.parse.blast_xml import (
diff --git a/tests/test_parse/test_cigar.py b/tests/test_parse/test_cigar.py
index e8547afb4..0c57181fc 100644
--- a/tests/test_parse/test_cigar.py
+++ b/tests/test_parse/test_cigar.py
@@ -14,8 +14,8 @@
 class TestCigar(unittest.TestCase):
     def setUp(self):
         self.cigar_text = "3D2M3D6MDM2D3MD"
-        self.aln_seq = DNA.make_seq("---AA---GCTTAG-A--CCT-")
-        self.aln_seq1 = DNA.make_seq("CCAAAAAA---TAGT-GGC--G")
+        self.aln_seq = DNA.make_seq(seq="---AA---GCTTAG-A--CCT-")
+        self.aln_seq1 = DNA.make_seq(seq="CCAAAAAA---TAGT-GGC--G")
         self.map, self.seq = self.aln_seq.parse_out_gaps()
         self.map1, self.seq1 = self.aln_seq1.parse_out_gaps()
         self.slices = [(1, 4), (0, 8), (7, 12), (0, 1), (3, 5)]
diff --git a/tests/test_parse/test_clustal.py b/tests/test_parse/test_clustal.py
index a1be993a6..f813c64c2 100644
--- a/tests/test_parse/test_clustal.py
+++ b/tests/test_parse/test_clustal.py
@@ -1,5 +1,4 @@
-"""Unit tests for the clustal parsers.
-"""
+"""Unit tests for the clustal parsers."""
 
 from unittest import TestCase
 
@@ -11,7 +10,6 @@
 )
 from cogent3.parse.record import RecordError
 
-
 # Note: the data are all strings and hence immutable, so it's OK to define
 # them here instead of in setUp and then subclassing everything from that
 # base class. If the data were mutable, we'd need to take more precautions
@@ -36,9 +34,7 @@ def             -----------------------------------------CGCGAUGCAUGCAU-CGAU 18
 abc             UGACUAGUCAGCUAGCAUCGAUCAGU 145
 def             CGAUCAGUCAGUCGAU---------- 34
 xyz             UGCUGCAUCA---------------- 33
-                *     ***""".split(
-    "\n"
-)
+                *     ***""".split("\n")
 
 bad = ["dshfjsdfhdfsj", "hfsdjksdfhjsdf"]
 
diff --git a/tests/test_parse/test_dialign.py b/tests/test_parse/test_dialign.py
index 94c4dc548..4a75dc9fb 100644
--- a/tests/test_parse/test_dialign.py
+++ b/tests/test_parse/test_dialign.py
@@ -5,7 +5,6 @@
 from cogent3 import PROTEIN
 from cogent3.parse.dialign import DialignParser, parse_data_line
 
-
 data = """
                            DIALIGN 2.2.1 
                            *************
diff --git a/tests/test_parse/test_ebi.py b/tests/test_parse/test_ebi.py
index 3cd769c24..9bb70fabe 100644
--- a/tests/test_parse/test_ebi.py
+++ b/tests/test_parse/test_ebi.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-""" Provides tests for EbiParser and related classes and functions.
-"""
+"""Provides tests for EbiParser and related classes and functions."""
 
 from unittest import TestCase
 
diff --git a/tests/test_parse/test_fasta.py b/tests/test_parse/test_fasta.py
index 07a3e6964..9d67f769e 100644
--- a/tests/test_parse/test_fasta.py
+++ b/tests/test_parse/test_fasta.py
@@ -1,18 +1,14 @@
-"""Unit tests for FASTA and related parsers.
-"""
+"""Unit tests for FASTA and related parsers."""
 
 import os
 import pathlib
-
 from unittest import TestCase
 
 import numpy
 import pytest
-
 from cogent3.core.info import Info
-from cogent3.core.sequence import DnaSequence
+from cogent3.core.sequence import DnaSequence, Sequence
 from cogent3.core.sequence import ProteinSequence as Protein
-from cogent3.core.sequence import Sequence
 from cogent3.parse.fasta import (
     FastaParser,
     GroupFastaParser,
@@ -25,7 +21,6 @@
 )
 from cogent3.parse.record import RecordError
 
-
 base_path = os.path.dirname(os.path.dirname(__file__))
 data_path = os.path.join(base_path, "data")
 
diff --git a/tests/test_parse/test_gbseq.py b/tests/test_parse/test_gbseq.py
index aa939d247..1f3f165ac 100644
--- a/tests/test_parse/test_gbseq.py
+++ b/tests/test_parse/test_gbseq.py
@@ -1,10 +1,8 @@
 import xml.dom.minidom
-
 from unittest import TestCase
 
 from cogent3.parse.gbseq import GbSeqXmlParser
 
-
 data = """<?xml version="1.0"?>
  <!DOCTYPE GBSet PUBLIC "-//NCBI//NCBI GBSeq/EN" "http://www.ncbi.nlm.nih.gov/dtd/NCBI_GBSeq.dtd">
  <GBSet>
diff --git a/tests/test_parse/test_genbank.py b/tests/test_parse/test_genbank.py
index 0a93c67c7..9e42345db 100644
--- a/tests/test_parse/test_genbank.py
+++ b/tests/test_parse/test_genbank.py
@@ -1,10 +1,8 @@
-"""Unit tests for the GenBank database parsers.
-"""
+"""Unit tests for the GenBank database parsers."""
 
 from unittest import TestCase
 
 import pytest
-
 from cogent3.parse.genbank import (
     Location,
     LocationList,
@@ -109,9 +107,7 @@ def test_indent_splitter(self):
 ?
 REFERENCE   2  (bases 1 to 92123751)
   AUTHORS   International Human Genome Sequencing Consortium.
-  TITLE     Finishing the euchromatic sequence of the human genome""".split(
-            "\n"
-        )
+  TITLE     Finishing the euchromatic sequence of the human genome""".split("\n")
         self.assertEqual(
             list(indent_splitter(lines)),
             [
@@ -133,9 +129,7 @@ def test_parse_sequence(self):
         1 gggagcgcgg cgcgggagcc cgaggctgag actcaccgga ggaagcggcg cgagcgcccc
        61   gccatcgtcc \t\t cggctgaagt 123 \ngcagtg  \n
       121 cctgggctta agcagtcttc45ccacctcagc 
-//\n\n\n""".split(
-            "\n"
-        )
+//\n\n\n""".split("\n")
         result = parse_sequence(lines)
         self.assertEqual(
             result,
@@ -147,9 +141,7 @@ def test_block_consolidator(self):
         lines = """  ORGANISM  Homo sapiens
             Eukaryota; Metazoa; Chordata; Craniata; Vertebrata; Euteleostomi;
             Mammalia; Eutheria; Euarchontoglires; Primates; Catarrhini;
-            Hominidae; Homo.""".split(
-            "\n"
-        )
+            Hominidae; Homo.""".split("\n")
         label, data = block_consolidator(lines)
         self.assertEqual(label, "ORGANISM")
         self.assertEqual(
@@ -184,9 +176,7 @@ def test_parse_organism(self):
         lines = """  ORGANISM  Homo sapiens
         Eukaryota; Metazoa; Chordata Craniata; Vertebrata; Euteleostomi;
         Mammalia; Eutheria; Euarchontoglires; Primates \t abc.  2.; Catarrhini
-        Hominidae; Homo.""".split(
-            "\n"
-        )
+        Hominidae; Homo.""".split("\n")
         species, taxonomy = parse_organism(lines)
         self.assertEqual(species, "Homo sapiens")
         self.assertEqual(
@@ -399,9 +389,7 @@ def test_parse_source(self):
         s = """SOURCE      African elephant.
   ORGANISM  Mitochondrion Loxodonta africana
             Eukaryota; Metazoa; Chordata; Craniata; Vertebrata; Euteleostomi;
-            Mammalia; Eutheria; Proboscidea; Elephantidae; Loxodonta.""".split(
-            "\n"
-        )
+            Mammalia; Eutheria; Proboscidea; Elephantidae; Loxodonta.""".split("\n")
         r = parse_source(s)
         self.assertEqual(len(r), 3)
         self.assertEqual(r["source"], "African elephant.")
diff --git a/tests/test_parse/test_gff.py b/tests/test_parse/test_gff.py
index 770bfa292..5f1ac3926 100644
--- a/tests/test_parse/test_gff.py
+++ b/tests/test_parse/test_gff.py
@@ -1,17 +1,14 @@
-"""Unit tests for GFF and related parsers.
-"""
+"""Unit tests for GFF and related parsers."""
 
 import io
 import os
 import pathlib
 import re
-
 from io import StringIO
 from pathlib import Path
 from unittest import TestCase
 
 import pytest
-
 from cogent3.parse.gff import (
     GffRecord,
     gff_parser,
@@ -19,7 +16,6 @@
     parse_attributes_gff2,
 )
 
-
 headers = [
     """##gff-version 2 
 ##source-version <source> <version text> 
diff --git a/tests/test_parse/test_greengenes.py b/tests/test_parse/test_greengenes.py
index 53f3f7723..8f12ff608 100644
--- a/tests/test_parse/test_greengenes.py
+++ b/tests/test_parse/test_greengenes.py
@@ -6,7 +6,6 @@
     make_ignore_f,
 )
 
-
 # consider project name
 # remember to add yourself if you make changes
 
diff --git a/tests/test_parse/test_locuslink.py b/tests/test_parse/test_locuslink.py
index 2f5485103..11fd0b8f5 100644
--- a/tests/test_parse/test_locuslink.py
+++ b/tests/test_parse/test_locuslink.py
@@ -1,5 +1,4 @@
-"""Unit tests for locuslink-specific classes
-"""
+"""Unit tests for locuslink-specific classes"""
 
 from unittest import TestCase
 
diff --git a/tests/test_parse/test_ncbi_taxonomy.py b/tests/test_parse/test_ncbi_taxonomy.py
index ccc62e8d5..9e9dfc253 100644
--- a/tests/test_parse/test_ncbi_taxonomy.py
+++ b/tests/test_parse/test_ncbi_taxonomy.py
@@ -1,5 +1,4 @@
-"""Tests of parsers for dealing with NCBI Taxonomy files.
-"""
+"""Tests of parsers for dealing with NCBI Taxonomy files."""
 
 from unittest import TestCase
 
@@ -14,16 +13,13 @@
     NcbiTaxonParser,
 )
 
-
 good_nodes = """1\t|\t1\t|\tno rank\t|\t\t|\t8\t|\t0\t|\t1\t|\t0\t|\t0\t|\t0\t|\t0\t|\t0\t|\t\t|
 2\t|\t1\t|\tsuperkingdom\t|\t\t|\t0\t|\t0\t|\t11\t|\t0\t|\t0\t|\t0\t|\t0\t|\t0\t|\t\t|
 6\t|\t2\t|\tgenus\t|\t\t|\t0\t|\t1\t|\t11\t|\t1\t|\t0\t|\t1\t|\t0\t|\t0\t|\t|\t
 7\t|\t6\t|\tspecies\t|\tAC\t|\t0\t|\t1\t|\t11\t|\t1\t|\t0\t|\t1\t|\t0\t|\t0\t|\t\t|
 9\t|\t7\t|\tsubspecies\t|\tBA\t|\t0\t|\t1\t|\t11\t|\t1\t|\t0\t|\t1\t|\t0\t|\t0\t|\t\t|
 10\t|\t6\t|\tspecies\t|\tAC\t|\t0\t|\t1\t|\t11\t|\t1\t|\t0\t|\t1\t|\t0\t|\t0\t|\t\t|
-""".split(
-    "\n"
-)
+""".split("\n")
 
 bad_nodes = """1\t|\t1\t|\tno rank\t|\t\t|\t8\t|\t0\t|\t1\t|\t0\t|\t0\t|\t0\t|\t0\t|\t0\t|\t\t|
 2\t|\t1\t|\tsuperkingdom\t|\t\t|\t0\t|\t0\t|\t11\t|\t0\t|\t0\t|\t0\t|\t0\t|\t0\t|\t\t|
@@ -31,9 +27,7 @@
 7\t|\t6\t|\tspecies\t|\tAC\t|\t0\t|\t1\t|\t11\t|\t1\t|\t0\t|\t1\t|\t0\t|\t0\t|\t\t|
 9\t|\t777\t|\tsubspecies\t|\tBA\t|\t0\t|\t1\t|\t11\t|\t1\t|\t0\t|\t1\t|\t0\t|\t0\t|\t\t|
 10\t|\t666\t|\tspecies\t|\tAC\t|\t0\t|\t1\t|\t11\t|\t1\t|\t0\t|\t1\t|\t0\t|\t0\t|\t\t|
-""".split(
-    "\n"
-)
+""".split("\n")
 
 good_names = """1\t|\tall\t|\t\t|\tsynonym\t|
 1\t|\troot\t|\t\t|\tscientific name\t|
@@ -46,9 +40,7 @@
 7\t|\tAzorhizobium caulinodans\t|\t\t|\tscientific name\t|
 9\t|\tBuchnera aphidicola\t|\t\t|\tscientific name\t|
 10\t|\tFakus namus\t|\t\t|\tscientific name\t|
-""".split(
-    "\n"
-)
+""".split("\n")
 
 
 class NcbiTaxonTests(TestCase):
diff --git a/tests/test_parse/test_nexus.py b/tests/test_parse/test_nexus.py
index 5211fc978..00e096719 100644
--- a/tests/test_parse/test_nexus.py
+++ b/tests/test_parse/test_nexus.py
@@ -1,5 +1,4 @@
-"""Unit tests for the Nexus Parser
-"""
+"""Unit tests for the Nexus Parser"""
 
 from unittest import TestCase
 
@@ -17,7 +16,6 @@
     split_tree_info,
 )
 
-
 Nexus_tree = """#NEXUS 
 
 Begin trees;  [Treefile saved Wednesday, May 5, 2004  5:02 PM]
@@ -53,9 +51,7 @@
 		;
 tree PAUP_1 = [&R] (1,(2,(((3,4),(5,(((((6,10),9),(11,18)),((((7,15),19),17),(8,(12,(14,16))))),13))),20)),21);
 tree PAUP_2 = [&R] (1,(2,(((3,4),(5,(((((6,10),9),(11,18)),((((7,15),19),17),(8,(12,(14,16))))),13))),20)),21);
-End;""".split(
-    "\n"
-)
+End;""".split("\n")
 
 Nexus_tree_2 = """#NEXUS 
 
@@ -66,9 +62,7 @@
 >  (Tree is unrooted)
 ]
 tree nj = [&U] ((((((((((YA10260L1:0.01855,SARAG06_Y:0.00367):0.01965,(((YA270L1G0:0.01095,SARAD10_Y:0.00699):0.01744,YA270L1A0:0.04329):0.00028,((YA165L1C1:0.01241,SARAA02_Y:0.02584):0.00213,((YA165L1H0:0.00092,SARAF10_Y:-0.00092):0.00250,(YA165L1A0:0.00177,SARAH10_Y:0.01226):0.00198):0.00131):0.00700):0.01111):0.11201,(YA160L1F0:0.00348,SARAG01_Y:-0.00122):0.13620):0.01202,((((YRM60L1D0:0.00357,(YRM60L1C0:0.00477,SARAE10_Y:-0.00035):0.00086):0.00092,SARAE03_Y:0.00126):0.00125,SARAC11_Y:0.00318):0.00160,YRM60L1H0:0.00593):0.09975):0.07088,SARAA01_Y:0.02880):0.00190,SARAB04_Y:0.05219):0.00563,YRM60L1E0:0.06099):0.00165,(YRM60L1H0:0.00450,SARAF11_Y:0.01839):0.00288):0.00129,YRM60L1B1:0.00713):0.00194,(YRM60L1G0:0.00990,(YA165L1G0:0.00576,(YA160L1G0:0.01226,SARAA11_Y:0.00389):0.00088):0.00300):0.00614,SARAC06_Y:0.00381);
-end;""".split(
-    "\n"
-)
+end;""".split("\n")
 
 Nexus_tree_3 = """#NEXUS 
 
@@ -105,9 +99,7 @@
 		;
 tree PAUP_1 = [&R] (1,(2,(((3,4),(5,(((((6,10),9),(11,18)),((((7,15),19),17),(8,(12,(14,16))))),13))),20)),21);
 tree PAUP_2 = [&R] (1,(2,(((3,4),(5,(((((6,10),9),(11,18)),((((7,15),19),17),(8,(12,(14,16))))),13))),20)),21);
-End;""".split(
-    "\n"
-)
+End;""".split("\n")
 
 PAUP_log = """
 P A U P *
@@ -195,9 +187,7 @@
 Tree length = 509
 Consistency index (CI) = 0.7151
 Homoplasy index (HI) = 0.2849
-""".split(
-    "\n"
-)
+""".split("\n")
 
 line1 = "    40                root                  0             0           0"
 line2 = "outgroup25 (1)*         40                 40            24            52"
diff --git a/tests/test_parse/test_pamlmatrix.py b/tests/test_parse/test_pamlmatrix.py
index b4c70837f..01e10af3f 100644
--- a/tests/test_parse/test_pamlmatrix.py
+++ b/tests/test_parse/test_pamlmatrix.py
@@ -1,11 +1,9 @@
 from io import StringIO
 from unittest import TestCase
 
-from numpy.testing import assert_equal
-
 from cogent3.evolve.models import DSO78_freqs, DSO78_matrix
 from cogent3.parse.paml_matrix import PamlMatrixParser
-
+from numpy.testing import assert_equal
 
 data = """
        27									    
diff --git a/tests/test_parse/test_phylip.py b/tests/test_parse/test_phylip.py
index 4ca394184..469e3f377 100644
--- a/tests/test_parse/test_phylip.py
+++ b/tests/test_parse/test_phylip.py
@@ -1,5 +1,4 @@
-"""Unit tests for the phylip parser
-"""
+"""Unit tests for the phylip parser"""
 
 from io import StringIO
 from unittest import TestCase
diff --git a/tests/test_parse/test_psl.py b/tests/test_parse/test_psl.py
index 09511e7b5..98c142ad4 100644
--- a/tests/test_parse/test_psl.py
+++ b/tests/test_parse/test_psl.py
@@ -1,13 +1,12 @@
 #!/usr/bin/env python
 """Unit tests for the PSL parser.
-   Compatible with blat v.34
+Compatible with blat v.34
 """
 
 from unittest import TestCase
 
 from cogent3.parse.psl import MinimalPslParser, PslToTable
 
-
 fname = "data/test.psl"
 
 
diff --git a/tests/test_parse/test_pwm_parsers.py b/tests/test_parse/test_pwm_parsers.py
index 45a43ed4e..36a71b7c1 100644
--- a/tests/test_parse/test_pwm_parsers.py
+++ b/tests/test_parse/test_pwm_parsers.py
@@ -1,10 +1,9 @@
 from unittest import TestCase
 
-from numpy import array
-from numpy.testing import assert_allclose, assert_array_equal
-
 from cogent3.core.moltype import get_moltype
 from cogent3.parse import cisbp, jaspar
+from numpy import array
+from numpy.testing import assert_allclose, assert_array_equal
 
 
 class TestPwmParsers(TestCase):
diff --git a/tests/test_parse/test_rdb.py b/tests/test_parse/test_rdb.py
index 9565835c6..877edf7c9 100644
--- a/tests/test_parse/test_rdb.py
+++ b/tests/test_parse/test_rdb.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # test_rdb.py
-"""Unit test for RDB Parser
-"""
+"""Unit test for RDB Parser"""
+
 from unittest import TestCase
 
 from cogent3.core.info import Info
diff --git a/tests/test_parse/test_record.py b/tests/test_parse/test_record.py
index f3583425d..c52e4c012 100644
--- a/tests/test_parse/test_record.py
+++ b/tests/test_parse/test_record.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
-"""Unit tests for parser support libraries dealing with records.
-"""
+"""Unit tests for parser support libraries dealing with records."""
+
 from unittest import TestCase
 
 from cogent3.parse.record import (
diff --git a/tests/test_parse/test_record_finder.py b/tests/test_parse/test_record_finder.py
index 3f300a574..d60cde8fd 100644
--- a/tests/test_parse/test_record_finder.py
+++ b/tests/test_parse/test_record_finder.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-"""Unit tests for recordfinders: parsers that group the lines for a record.
-"""
+"""Unit tests for recordfinders: parsers that group the lines for a record."""
 
 from unittest import TestCase
 
diff --git a/tests/test_parse/test_sequence.py b/tests/test_parse/test_sequence.py
index 0e6975593..2bde87d6a 100644
--- a/tests/test_parse/test_sequence.py
+++ b/tests/test_parse/test_sequence.py
@@ -2,7 +2,6 @@
 import pathlib
 
 import pytest
-
 from cogent3.parse import sequence
 
 
@@ -53,3 +52,14 @@ def test_line_based_wrap_load_valid_input_types(gde_data):
 def test_get_unknown_format():
     with pytest.raises(ValueError):
         sequence.get_parser("nope")
+
+
+@pytest.mark.internet
+def test_line_based_url(DATA_DIR):
+    fname = "long_testseqs.fasta"
+    url = f"https://raw.githubusercontent.com/cogent3/cogent3/develop/doc/data/{fname}"
+
+    parser = sequence.LineBasedParser(lambda x: x)
+    from_url = list(parser(url))
+    from_local = list(parser(DATA_DIR / fname))
+    assert from_url == from_local
diff --git a/tests/test_parse/test_tinyseq.py b/tests/test_parse/test_tinyseq.py
index 2b3fee517..e501c9a5d 100644
--- a/tests/test_parse/test_tinyseq.py
+++ b/tests/test_parse/test_tinyseq.py
@@ -1,10 +1,8 @@
 import xml.dom.minidom
-
 from unittest import TestCase
 
 from cogent3.parse.tinyseq import TinyseqParser
 
-
 data = """<?xml version="1.0"?>
  <!DOCTYPE TSeqSet PUBLIC "-//NCBI//NCBI TSeq/EN" "http://www.ncbi.nlm.nih.gov/dtd/NCBI_TSeq.dtd">
  <TSeqSet>
diff --git a/tests/test_parse/test_tree.py b/tests/test_parse/test_tree.py
index 56b328096..ec7deede8 100644
--- a/tests/test_parse/test_tree.py
+++ b/tests/test_parse/test_tree.py
@@ -1,12 +1,10 @@
-"""Unit tests for tree parsers.
-"""
+"""Unit tests for tree parsers."""
 
 from unittest import TestCase
 
 from cogent3.core.tree import PhyloNode
 from cogent3.parse.tree import DndParser, DndTokenizer, RecordError
 
-
 sample = """
 (
 (
diff --git a/tests/test_parse/test_unigene.py b/tests/test_parse/test_unigene.py
index 521d6dd77..f803073f6 100644
--- a/tests/test_parse/test_unigene.py
+++ b/tests/test_parse/test_unigene.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
-"""Unit tests for unigene-specific classes
-"""
+"""Unit tests for unigene-specific classes"""
+
 from unittest import TestCase
 
 from cogent3.parse.record_finder import GbFinder
diff --git a/tests/test_phylo.py b/tests/test_phylo.py
index 65d7e19c1..0de68e4a1 100644
--- a/tests/test_phylo.py
+++ b/tests/test_phylo.py
@@ -2,11 +2,8 @@
 import pathlib
 import unittest
 import warnings
-
 from tempfile import TemporaryDirectory
 
-from numpy import exp, log
-
 from cogent3 import get_model, load_aligned_seqs, load_tree, make_tree
 from cogent3.phylo.consensus import get_splits, get_tree, majority_rule
 from cogent3.phylo.least_squares import wls
@@ -19,7 +16,7 @@
     make_trees,
 )
 from cogent3.util.io import remove_files
-
+from numpy import exp, log
 
 warnings.filterwarnings("ignore", "Not using MPI as mpi4py not found")
 
diff --git a/tests/test_tree_distance.py b/tests/test_tree_distance.py
index 867c207a0..df412aa99 100644
--- a/tests/test_tree_distance.py
+++ b/tests/test_tree_distance.py
@@ -1,7 +1,6 @@
 import pathlib
 
 import pytest
-
 from cogent3 import make_tree
 from cogent3.phylo.tree_distance import (
     lin_rajan_moret,
@@ -10,7 +9,6 @@
     unrooted_robinson_foulds,
 )
 
-
 ROOTED_DISTANCE_MEASURES = matching_cluster_distance, rooted_robinson_foulds
 UNROOTED_DISTANCE_MEASURES = lin_rajan_moret, unrooted_robinson_foulds
 
diff --git a/tests/test_util/test_deserialise.py b/tests/test_util/test_deserialise.py
index e9b35fcac..528e23390 100644
--- a/tests/test_util/test_deserialise.py
+++ b/tests/test_util/test_deserialise.py
@@ -1,14 +1,10 @@
 import json
 import os
-
 from tempfile import TemporaryDirectory
 from unittest import TestCase
 
 import numpy
 import pytest
-
-from numpy.testing import assert_allclose
-
 from cogent3 import (
     get_app,
     get_code,
@@ -30,6 +26,7 @@
     deserialise_likelihood_function,
     deserialise_object,
 )
+from numpy.testing import assert_allclose
 
 
 class TestDeserialising(TestCase):
diff --git a/tests/test_util/test_dictarray.py b/tests/test_util/test_dictarray.py
index a8cbb1d53..c1e3f8b26 100644
--- a/tests/test_util/test_dictarray.py
+++ b/tests/test_util/test_dictarray.py
@@ -1,13 +1,9 @@
 import json
 import os
-
 from tempfile import TemporaryDirectory
 from unittest import TestCase
 
 import numpy
-
-from numpy.testing import assert_allclose
-
 from cogent3 import DNA
 from cogent3.util.dict_array import (
     DictArray,
@@ -19,6 +15,7 @@
     convert_for_dictarray,
     convert_series,
 )
+from numpy.testing import assert_allclose
 
 
 class DictArrayTest(TestCase):
@@ -403,7 +400,7 @@ def test_to_string(self):
             "dim-1 dim-2 value\n0 0 3.123456789\n0 1 6.246913578\n1 0 9.370370367\n1 1 12.493827156",
         )
         with self.assertRaises(ValueError):
-            darr.to_string(format="md"),
+            darr.to_string(format="md")
 
     def test_to_table(self):
         """creates Table when ndim <= 2"""
diff --git a/tests/test_util/test_io.py b/tests/test_util/test_io.py
index b952ee4a1..bba834103 100644
--- a/tests/test_util/test_io.py
+++ b/tests/test_util/test_io.py
@@ -3,11 +3,9 @@
 import pathlib
 import tempfile
 import zipfile
-
 from urllib.parse import urlparse
 
 import pytest
-
 from cogent3.app.composable import NotCompleted
 from cogent3.util.io import (
     _path_relative_to_zip_parent,
diff --git a/tests/test_util/test_misc.py b/tests/test_util/test_misc.py
index 68d81e4a0..f4285083f 100644
--- a/tests/test_util/test_misc.py
+++ b/tests/test_util/test_misc.py
@@ -1,15 +1,10 @@
-"""Unit tests for utility functions and classes.
-"""
+"""Unit tests for utility functions and classes."""
 
 from copy import copy, deepcopy
 from os import remove, rmdir
 from unittest import TestCase
 
 import pytest
-
-from numpy import array
-from numpy.testing import assert_allclose
-
 from cogent3.util.misc import (
     ClassChecker,
     ConstrainedContainer,
@@ -45,6 +40,8 @@
     not_list_tuple,
     recursive_flatten,
 )
+from numpy import array
+from numpy.testing import assert_allclose
 
 
 class UtilsTests(TestCase):
@@ -1205,7 +1202,6 @@ def test_is_in_jupyter():
     # an ugly hack, the in_jupyter function relies entirely on whether a
     # get_ipython variable exists in the name space
     import cogent3.util.misc as module
-
     from cogent3.util.misc import in_jupyter
 
     module.get_ipython = lambda x: x
diff --git a/tests/test_util/test_parallel.py b/tests/test_util/test_parallel.py
index efda1ca27..9f0978cdd 100644
--- a/tests/test_util/test_parallel.py
+++ b/tests/test_util/test_parallel.py
@@ -1,11 +1,9 @@
 import multiprocessing
 import sys
 import time
-
 from unittest import TestCase, skipIf
 
 import numpy
-
 from cogent3.util import parallel
 
 
diff --git a/tests/test_util/test_recode_alignment.py b/tests/test_util/test_recode_alignment.py
index 5e0c77cd7..57162b1d2 100755
--- a/tests/test_util/test_recode_alignment.py
+++ b/tests/test_util/test_recode_alignment.py
@@ -1,7 +1,5 @@
 from unittest import TestCase
 
-from numpy.testing import assert_allclose
-
 from cogent3 import make_aligned_seqs
 from cogent3.core.alignment import ArrayAlignment
 from cogent3.evolve.models import DSO78_freqs, DSO78_matrix
@@ -13,6 +11,7 @@
     recode_dense_alignment,
     recode_freq_vector,
 )
+from numpy.testing import assert_allclose
 
 
 class RecodeAlignmentTests(TestCase):
diff --git a/tests/test_util/test_table.py b/tests/test_util/test_table.py
index 756f388d4..1bb60c184 100644
--- a/tests/test_util/test_table.py
+++ b/tests/test_util/test_table.py
@@ -1,22 +1,15 @@
-"""Unit tests for table.
-"""
+"""Unit tests for table."""
 
 import json
 import os
 import pathlib
 import pickle
-
 from collections import defaultdict
 from tempfile import TemporaryDirectory
 from unittest import TestCase, skipIf
 
 import numpy
 import pytest
-
-from numpy import arange
-from numpy.exceptions import ComplexWarning
-from numpy.testing import assert_equal
-
 from cogent3 import load_table, make_table, open_
 from cogent3.format.table import (
     formatted_array,
@@ -31,7 +24,9 @@
     cast_str_to_numeric,
     cast_to_array,
 )
-
+from numpy import arange
+from numpy.exceptions import ComplexWarning
+from numpy.testing import assert_equal
 
 try:
     from pandas import DataFrame
diff --git a/tests/test_util/test_transform.py b/tests/test_util/test_transform.py
index 4c69e8d9f..1b338ac88 100644
--- a/tests/test_util/test_transform.py
+++ b/tests/test_util/test_transform.py
@@ -1,9 +1,7 @@
 #!/usr/bin/env python
-"""Tests of transformation and composition functions .
-"""
-from unittest import TestCase
+"""Tests of transformation and composition functions ."""
 
-from numpy.testing import assert_allclose
+from unittest import TestCase
 
 from cogent3.util.transform import (
     KeepChars,
@@ -12,6 +10,7 @@
     per_longest,
     per_shortest,
 )
+from numpy.testing import assert_allclose
 
 
 class has_x(object):
diff --git a/tests/test_util/test_union_dict.py b/tests/test_util/test_union_dict.py
index 32cb8dc20..7327389c3 100644
--- a/tests/test_util/test_union_dict.py
+++ b/tests/test_util/test_union_dict.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 
-"""Unit tests for union_dict.
-"""
+"""Unit tests for union_dict."""
+
 from unittest import TestCase
 
 from cogent3.util.union_dict import UnionDict
diff --git a/tests/test_util/test_warning.py b/tests/test_util/test_warning.py
index 08cfb6af1..8f1afba15 100644
--- a/tests/test_util/test_warning.py
+++ b/tests/test_util/test_warning.py
@@ -2,7 +2,6 @@
 import warnings
 
 import pytest
-
 from cogent3.util.warning import deprecated_args, deprecated_callable
 
 
diff --git a/tests/timetrial.py b/tests/timetrial.py
index b16d306c2..88e155850 100644
--- a/tests/timetrial.py
+++ b/tests/timetrial.py
@@ -10,7 +10,6 @@
 import sys
 import time
 
-
 # Values that affect the running of the program.
 minimum_accepted_time = 2