[med-svn] [Git][med-team/python-cogent][upstream] New upstream version 2026.10.15
Karsten Schöke (@karso)
gitlab at salsa.debian.org
Mon Oct 19 12:36:08 BST 2026
Karsten Schöke pushed to branch upstream at Debian Med / python-cogent
Commits:
3606e8d9 by Karsten Schöke at 2026-10-19T13:15:36+02:00
New upstream version 2026.10.15
- - - - -
21 changed files:
- .github/dependabot.yml
- .github/workflows/codeql.yml
- .github/workflows/release.yml
- .github/workflows/testing_develop.yml
- changelog.md
- noxfile.py
- pyproject.toml
- src/cogent3/align/pairwise.py
- src/cogent3/core/alignment.py
- src/cogent3/core/alphabet.py
- src/cogent3/core/moltype.py
- src/cogent3/core/sequence.py
- src/cogent3/util/union_dict.py
- tests/test_app/test_evo.py
- tests/test_app/test_init.py
- tests/test_app/test_io.py
- tests/test_app/test_sqlite_data_store.py
- tests/test_core/test_annotation_db.py
- tests/test_evolve/test_scale_rules.py
- tests/test_util/test_union_dict.py
- uv.lock
Changes:
=====================================
.github/dependabot.yml
=====================================
@@ -1,6 +1,6 @@
version: 2
updates:
- - package-ecosystem: pip
+ - package-ecosystem: uv
directory: "/"
schedule:
interval: weekly
@@ -8,6 +8,10 @@ updates:
open-pull-requests-limit: 10
cooldown:
default-days: 7
+ ignore:
+ # quartodoc passes parse_numpy an argument that griffe 2 removed
+ - dependency-name: griffe
+ update-types: ["version-update:semver-major"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
=====================================
.github/workflows/codeql.yml
=====================================
@@ -45,7 +45,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init at cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
+ uses: github/codeql-action/init at 24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,7 +59,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild at cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
+ uses: github/codeql-action/autobuild at 24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -72,6 +72,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze at cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
+ uses: github/codeql-action/analyze at 24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: "/language:${{matrix.language}}"
=====================================
.github/workflows/release.yml
=====================================
@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.11", "3.12", "3.13", "3.14"]
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: "actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1
with:
@@ -26,20 +26,15 @@ jobs:
python-version: "${{ matrix.python-version }}"
- name: Install uv
- uses: astral-sh/setup-uv at 20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
+ uses: astral-sh/setup-uv at c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
- - name: "Installs for ${{ matrix.python-version }}"
- run: |
- uv venv venv -p python${{ matrix.python-version }}
- uv tool install -p venv nox
-
- name: "Run nox for ${{ matrix.python-version }}"
shell: bash
run: |
- nox -db uv --force-python python -s test
+ uv run --locked -p python${{ matrix.python-version }} --only-group test nox -db uv --force-python python -s test
docbuild:
@@ -58,7 +53,7 @@ jobs:
python-version: "3.13"
- name: Install uv
- uses: astral-sh/setup-uv at 20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
+ uses: astral-sh/setup-uv at c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
@@ -66,13 +61,8 @@ jobs:
- name: Install quarto
uses: quarto-dev/quarto-actions/setup at 8a96df13519ee81fd526f2dfca5962811136661b # v2.2.0
- - name: "Installs for ${{ matrix.python-version }}"
- run: |
- uv venv venv -p python${{ matrix.python-version }}
- uv tool install -p venv nox
-
- name: "test the docs code"
- run: "nox -db uv --force-python python -s testdocs"
+ run: "uv run --locked -p python3.13 --only-group test nox -db uv --force-python python -s testdocs"
build:
name: Build wheel and sdist
@@ -100,7 +90,7 @@ jobs:
run: python -m build --wheel --sdist
- name: Upload sdist and wheel
- uses: actions/upload-artifact at 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact at cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: c3-wheel-sdist
path: |
@@ -117,7 +107,7 @@ jobs:
steps:
- name: Download sdist and wheel
- uses: actions/download-artifact at 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ uses: actions/download-artifact at 9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: c3-wheel-sdist
path: ./dist
@@ -137,7 +127,7 @@ jobs:
steps:
- name: Download sdist and wheel
- uses: actions/download-artifact at 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ uses: actions/download-artifact at 9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: c3-wheel-sdist
path: ./dist
=====================================
.github/workflows/testing_develop.yml
=====================================
@@ -24,7 +24,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.11", "3.14"]
+ python-version: ["3.11", "3.14", "3.15"]
steps:
- uses: "actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1
@@ -38,22 +38,17 @@ jobs:
python-version: "${{ matrix.python-version }}"
- name: Install uv
- uses: astral-sh/setup-uv at 20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
+ uses: astral-sh/setup-uv at c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
- - name: "Installs for ${{ matrix.python-version }}"
- run: |
- uv venv venv -p python${{ matrix.python-version }}
- uv tool install -p venv nox
-
- name: "Run nox for ${{ matrix.python-version }}"
shell: bash
run: |
dn=$(pwd)
cov="--cov-report lcov:lcov-${{matrix.os}}-${{matrix.python-version}}.lcov --cov-report term --cov-append --cov cogent3 --cov-config=${dn}/.coveragerc"
- nox -db uv --force-python python -s test -- $cov
+ uv run --locked -p python${{ matrix.python-version }} --only-group test nox -db uv --force-python python -s test -- $cov
- name: Coveralls Parallel
uses: coverallsapp/github-action at 8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8
@@ -89,16 +84,13 @@ jobs:
python-version: "${{ matrix.python-version }}"
- name: Install uv
- uses: astral-sh/setup-uv at 20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
+ uses: astral-sh/setup-uv at c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
- name: "Run Type Checking for ${{ matrix.python-version }}"
- run: |
- uv venv venv -p python${{ matrix.python-version }}
- uv pip install -p venv -e . --group typing
- venv/bin/mypy src/cogent3/core/alignment.py src/cogent3/core/alphabet.py src/cogent3/core/annotation.py src/cogent3/core/genetic_code.py src/cogent3/core/location.py src/cogent3/core/moltype.py src/cogent3/core/seq_storage.py src/cogent3/core/sequence.py src/cogent3/core/seqview.py src/cogent3/core/slice_record.py src/cogent3/core/tree.py
+ run: uv run --locked -p python${{ matrix.python-version }} --only-group test nox -db uv --force-python python -s type_check
finish:
=====================================
changelog.md
=====================================
@@ -1,4 +1,33 @@
+<a id='changelog-2026.10.15'></a>
+# Changes in release "2026.10.15"
+
+Minor bug fix release plus bumping py version.
+
+## Contributors
+
+- @GavinHuttley
+
+## Enhancements
+
+- Added support for Python 3.15 and numpy 2.5. The optional `cogent3-h5seqs`
+ dependency is installed only on Python 3.14 and below (for now), and
+ `piqtree` only on platforms where it publishes wheels.
+
+## Bug fixes
+
+- `UnionDict` merging no longer loops forever when both sides share a nested
+ dict two or more levels down, e.g.
+ `{"d": {"e": {"k": 1}}} | {"d": {"e": {"m": 2}}}`. Nested dicts are now
+ copied on construction and assignment, so `|` leaves both operands
+ untouched and keys can no longer share a child dict. A dict that contains
+ itself now raises `ValueError` instead of `RecursionError`.
+
+## Documentation
+
+- Removed code snippets and references to modules deleted in 2026.9.10,
+ including `cogent3.util.io`.
+
<a id='changelog-2026.9.10'></a>
# Changes in release "2026.9.10"
=====================================
noxfile.py
=====================================
@@ -8,7 +8,21 @@ import nox
if sys.version_info >= (3, 12):
os.environ["COVERAGE_CORE"] = "sysmon"
-_py_versions = range(11, 15)
+_py_versions = range(11, 16)
+
+_typed_modules = [
+ "src/cogent3/core/alignment.py",
+ "src/cogent3/core/alphabet.py",
+ "src/cogent3/core/annotation.py",
+ "src/cogent3/core/genetic_code.py",
+ "src/cogent3/core/location.py",
+ "src/cogent3/core/moltype.py",
+ "src/cogent3/core/seq_storage.py",
+ "src/cogent3/core/sequence.py",
+ "src/cogent3/core/seqview.py",
+ "src/cogent3/core/slice_record.py",
+ "src/cogent3/core/tree.py",
+]
@nox.session(python=False)
@@ -17,7 +31,7 @@ def fmt(session: nox.Session) -> None:
session.run("ruff", "format", ".", external=True)
- at nox.session(python=[f"3.{v}" for v in _py_versions])
+ at nox.session(python=[f"3.{v}" for v in _py_versions], venv_backend="uv")
def test_slow(session):
session.install("-e", ".", "--group", "test")
session.chdir("tests")
@@ -28,10 +42,10 @@ def test_slow(session):
)
- at nox.session(python=[f"3.{v}" for v in _py_versions])
+ at nox.session(python=[f"3.{v}" for v in _py_versions], venv_backend="uv")
def test(session):
session.install("-e", ".", "--group", "test")
- session.run("pip", "list")
+ session.run("uv", "pip", "list", external=True)
# doctest modules within cogent3/app
session.chdir("src/cogent3/app")
session.run(
@@ -39,8 +53,6 @@ def test(session):
"-s",
"-x",
"--doctest-modules",
- "--ignore=composable.py",
- "--ignore=sqlite_data_store.py",
".",
)
@@ -55,7 +67,13 @@ def test(session):
)
- at nox.session(python=[f"3.{v}" for v in _py_versions])
+ at nox.session(python="3.14", venv_backend="uv")
+def type_check(session):
+ session.install("-e", ".", "--group", "typing")
+ session.run("mypy", *_typed_modules)
+
+
+ at nox.session(python=[f"3.{v}" for v in _py_versions], venv_backend="uv")
def test_module_docs(session):
"""doctest examples in a module"""
session.install("-e", ".", "--group", "test")
@@ -71,7 +89,7 @@ def test_module_docs(session):
)
- at nox.session(python=[f"3.{v}" for v in _py_versions])
+ at nox.session(python=[f"3.{v}" for v in _py_versions], venv_backend="uv")
def testdocs(session):
"""render the docs, which executes every code cell"""
py = pathlib.Path(session.bin_paths[0]) / "python"
=====================================
pyproject.toml
=====================================
@@ -5,7 +5,7 @@ build-backend = "uv_build"
[project]
name = "cogent3"
description = """COmparative GENomics Toolkit 3: genomic sequence analysis within notebooks or on compute systems with 1000s of CPUs."""
-version = "2026.9.10"
+version = "2026.10.15"
authors = [{ name = "Gavin Huttley", email = "Gavin.Huttley at anu.edu.au" }]
keywords = [
@@ -22,23 +22,23 @@ license-files = ["LICENSE"]
dependencies = [
"charset_normalizer",
"citeable",
- # prevent release candidate of numpy 2.4
- # until numba modified to handle removal
- # of numpy.trapz
- "numpy<2.5",
+ # numba lags numpy releases, so cap numpy at the newest
+ # minor version that numba supports
+ "numpy<2.6",
"numba>=0.57.0; python_version=='3.11'",
"numba>=0.59.0; python_version=='3.12'",
"numba>=0.61.0; python_version=='3.13'",
"numba>=0.63.0rc1; python_version=='3.14'",
+ "numba>=0.68.0; python_version=='3.15'",
"scipy",
- "scinexus[loky]>=2026.5.16",
+ "scinexus[loky]>=2026.10.2",
"stevedore",
"tqdm",
"typeguard>=4.0,<5",
"typing_extensions",
]
# remember to update version in requires-python and classifiers
-requires-python = ">=3.11,<3.15"
+requires-python = ">=3.11,<3.16"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Science/Research",
@@ -49,6 +49,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: 3.15",
]
[project.urls]
@@ -73,9 +74,13 @@ extra = [
[dependency-groups]
test = [
- "cogent3-h5seqs>=0.8.0",
- "piqtree>=0.8.0; python_version > '3.11'",
- "ruff==0.16.5",
+ # h5py, which cogent3-h5seqs needs, has no python 3.15 wheels yet
+ "cogent3-h5seqs>=0.8.0; python_version <= '3.14'",
+ # piqtree publishes wheels only for linux x86_64, macOS arm64 and windows
+ # AMD64, and only for python 3.12 to 3.14, so restrict it to those or
+ # resolution fails elsewhere
+ "piqtree>=0.8.0; python_version > '3.11' and python_version <= '3.14' and ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'win32' and platform_machine == 'AMD64'))",
+ "ruff==0.16.10",
"kaleido",
"pandas",
"plotly",
@@ -83,20 +88,22 @@ test = [
"pytest",
"pytest-cov",
"pytest-xdist",
- "nox",
+ "nox>=2026.8.10",
]
-typing = ["mypy==2.3.1"]
+typing = ["mypy==2.4.0"]
doc = [
"click",
"cogent3[extra]",
- "cogent3-h5seqs>=0.8.0",
+ "cogent3-h5seqs>=0.8.0; python_version <= '3.14'",
"ipykernel",
"jupyter_client",
"jupytext",
"nbclient",
"nbconvert>5.4",
"nbformat",
- "piqtree>=0.8.0; python_version > '3.11'",
+ # gated as in the test group: dev includes doc, so an ungated requirement
+ # here stops uv building the project environment at all
+ "piqtree>=0.8.0; python_version > '3.11' and python_version <= '3.14' and ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'win32' and platform_machine == 'AMD64'))",
"quartodoc",
# quartodoc 0.11.1 requires only griffe>=0.33, but griffe 2 dropped the
# allow_section_blank_line argument it passes to parse_numpy
@@ -104,7 +111,6 @@ doc = [
"iplotx>=1.7.1",
]
dev = [
- "nox",
"scriv",
"types-tqdm",
{ include-group = "doc" },
@@ -144,11 +150,9 @@ filterwarnings = [
[tool.uv]
reinstall-package = ["cogent3"]
-
-[tool.uv.pip]
exclude-newer = "7 days"
-[tool.uv.pip.exclude-newer-package]
+[tool.uv.exclude-newer-package]
# but we allow other cogent3 packages to be more recent
scinexus = false
cogent3-h5seqs = false
=====================================
src/cogent3/align/pairwise.py
=====================================
@@ -823,8 +823,7 @@ class PairEmissionProbs:
else:
cells.append((state, (i, j)))
probs = self.dp(TM, dp_options, cells=cells, backward=backward)
- probs = numpy.array(probs)
- probs.shape = (len(p_rows), N - 1, len(T) - 1)
+ probs = numpy.array(probs).reshape(len(p_rows), N - 1, len(T) - 1)
return numpy.array([probs[p_rows.index(i)] for i in last_row])
def dp(self, TM, dp_options, cells=None, backward=False):
=====================================
src/cogent3/core/alignment.py
=====================================
@@ -1225,7 +1225,7 @@ class CollectionBase(AnnotatableMixin, ABC, Generic[TSequenceOrAligned]):
result[name] = ambig
array = numpy.array(self.seqs[name])
for i in numpy.where(array > cast("int", alpha.gap_index))[0]:
- ambig[i] = alpha[array[i]]
+ ambig[int(i)] = alpha[array[i]]
return result
def counts(
@@ -3946,8 +3946,7 @@ class Alignment(CollectionBase[Aligned]):
gap_cols = gap_array.sum(axis=0) > 0
gap_array = gap_array[:, gap_cols]
- result = gap_array.sum(axis=1)
- result = darr.wrap(result)
+ result = darr.wrap(gap_array.sum(axis=1))
if drawable:
drawable = drawable.lower()
trace_name = pathlib.Path(self.source).name if self.source else None
=====================================
src/cogent3/core/alphabet.py
=====================================
@@ -1036,7 +1036,7 @@ def seq_to_kmer_indices_overlapping(
if gained_char >= num_states:
val = gap_index if gained_char == gap_char_index else missing_index
elif idx < 0:
- idx = (seq[i : i + k] * coeffs).sum()
+ idx = int((seq[i : i + k] * coeffs).sum())
val = idx
skip_until = -1
else:
=====================================
src/cogent3/core/moltype.py
=====================================
@@ -1277,7 +1277,9 @@ class MolType(Generic[TStrOrBytes]):
if isinstance(seq, str):
assert self.ambiguities is not None
rng = numpy.random.default_rng()
- return "".join(rng.choice(list(self.ambiguities.get(n, n))) for n in seq)
+ return "".join(
+ str(rng.choice(list(self.ambiguities.get(n, n)))) for n in seq
+ )
if isinstance(seq, bytes):
return self.random_disambiguate(seq.decode("utf8")).encode("utf8")
=====================================
src/cogent3/core/sequence.py
=====================================
@@ -146,7 +146,7 @@ def count_kmers(
continue
if idx < 0:
- idx = (seq[i : i + k] * coeffs).sum()
+ idx = int((seq[i : i + k] * coeffs).sum())
kfreqs[idx] += 1
continue
=====================================
src/cogent3/util/union_dict.py
=====================================
@@ -1,31 +1,48 @@
"""UnionDict extension of dict."""
+from typing import Any, Self
+
+
+def _convert_children(root: "UnionDict", source: Any) -> None:
+ on_path = {id(source)}
+ frames = [(root, iter(root.items()), id(source))]
+ while frames:
+ node, items, source_id = frames[-1]
+ for key, value in items:
+ if not isinstance(value, dict):
+ continue
+ if id(value) in on_path:
+ msg = f"cannot copy {key!r}, it refers back to a dict containing it"
+ raise ValueError(msg)
+ child = UnionDict()
+ dict.update(child, value)
+ dict.__setitem__(node, key, child)
+ on_path.add(id(value))
+ frames.append((child, iter(child.items()), id(value)))
+ break
+ else:
+ frames.pop()
+ on_path.discard(source_id)
+
class UnionDict(dict):
"""dictionary class that can be updated as a union with another dict
entries are also accessible directly as attributes on the object"""
- def __init__(self, *args, **kwargs) -> None:
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Parameters
----------
- data : dict or None
+ data : dict
+ nested dicts are copied, never shared with data
"""
assert not (args and kwargs)
if args:
assert len(args) == 1
kwargs = args[0]
super().__init__(kwargs)
-
- # force all children to to UnionDicts
- for key in self:
- value = self._getsubattr_([], key)
- if not isinstance(value, dict):
- continue
-
- if not isinstance(value, UnionDict):
- self.update({key: UnionDict(value)})
+ _convert_children(self, kwargs)
def __getattr__(self, item):
if item in self:
@@ -37,74 +54,50 @@ class UnionDict(dict):
msg = f"'{item}' not a key or attribute"
raise AttributeError(msg)
- def __setattr__(self, key, value) -> None:
- if isinstance(value, dict):
- value = UnionDict(value)
-
- self.update({key: value})
+ def __setattr__(self, key: str, value: Any) -> None:
+ self[key] = value
- def __setitem__(self, key, value) -> None:
- if isinstance(value, dict):
+ def __setitem__(self, key: Any, value: Any) -> None:
+ # a |= on an attribute assigns the same object back, which must not copy it
+ stored_as_is = value is self.get(key) and isinstance(value, UnionDict)
+ if isinstance(value, dict) and not stored_as_is:
value = UnionDict(value)
+ super().__setitem__(key, value)
- self.update({key: value})
+ def setdefault(self, key: Any, default: Any = None) -> Any:
+ if key not in self:
+ self[key] = default
+ return self[key]
- def __or__(self, other):
+ def __or__(self, other: Any) -> Self:
result = self.__class__(self)
result.union(other)
return result
- def __ior__(self, other):
+ def __ior__(self, other: Any) -> Self:
self.union(other)
return self
- def union(self, other) -> None:
- """returns the union of self with other
+ def union(self, other: Any) -> None:
+ """merge other into self in place
keys unique to other are introduced, keys in self shared with other
- are updated with the value from other"""
-
- if not isinstance(other, UnionDict):
- other = UnionDict(other)
-
- for a in list(other):
- if self.get(a) is None or not (
- isinstance(other.get(a), dict) and isinstance(self.get(a), dict)
- ):
- self.update({a: other.get(a)})
- continue
-
- path = [a]
- while len(path) > 0:
- name = path.pop()
- cs = self._getsubattr_(path, name)
- cd = other._getsubattr_(path, name)
- path.append(name)
-
- for attr in list(cd):
- if isinstance(cd.get(attr), dict) and isinstance(
- cs.get(attr),
- dict,
- ):
- # go into dict
- path.append(attr)
- else:
- cs.update({attr: cd.get(attr)})
- path.pop()
-
- def _getsubattr_(self, path, name):
- """returns nested values"""
- d = self
- for node in path:
- d = d.get(node)
- return d.get(name)
-
- def update(self, *args, **kwargs) -> None:
+ are updated with the value from other, except where both values are
+ dicts, which are merged in the same way"""
+ stack: list[tuple[dict[Any, Any], dict[Any, Any]]] = [(self, UnionDict(other))]
+ while stack:
+ target, source = stack.pop()
+ for key, value in source.items():
+ current = target.get(key)
+ if isinstance(value, dict) and isinstance(current, dict):
+ stack.append((current, value))
+ else:
+ dict.__setitem__(target, key, value)
+
+ def update(self, *args: Any, **kwargs: Any) -> None:
"""Converts to UnionDict."""
assert not (args and kwargs)
if args:
assert len(args) == 1
- kwargs = UnionDict(args[0])
- if not isinstance(kwargs, UnionDict):
- kwargs = UnionDict(kwargs)
- super().update(kwargs)
+ kwargs = args[0]
+ super().update(UnionDict(kwargs))
=====================================
tests/test_app/test_evo.py
=====================================
@@ -883,6 +883,7 @@ class TestBootstrap(TestCase):
out_dstore = open_data_store(dirname / "delme.sqlitedb", mode="w")
writer = io.write_db(out_dstore)
_ = io.load_db() + evo_app.bootstrap(hyp, num_reps=2) + writer
+ out_dstore.close()
def test_bstrap_parallel():
=====================================
tests/test_app/test_init.py
=====================================
@@ -17,6 +17,7 @@ except ImportError:
def _get_all_composables(tmp_dir_name):
tmp_dir_name = Path(tmp_dir_name)
+ tmp_dir_name.mkdir(parents=True, exist_ok=True)
test_model1 = get_app("model", "HKY85")
test_model2 = get_app("model", "GN")
test_hyp = get_app("hypothesis", test_model1, test_model2)
=====================================
tests/test_app/test_io.py
=====================================
@@ -11,7 +11,6 @@ import pytest
from numpy.testing import assert_allclose
from scinexus.composable import (
NotCompleted,
- NotCompletedType,
propagate_source,
source_proxy,
)
@@ -475,6 +474,8 @@ def test_write_db_load_db(fasta_dir, tmp_dir):
assert orig == read
assert data_store.record_type == get_object_provenance(orig)
+ orig_dstore.close()
+ data_store.close()
def test_load_db_prefer_source_attr(tmp_dir):
@@ -497,6 +498,7 @@ def test_load_db_prefer_source_attr(tmp_dir):
loader = io_app.load_db()
got = loader(data_store[0]) # pylint: disable=not-callable
assert "source" not in got.info
+ data_store.close()
def test_write_read_db_not_completed(tmp_dir):
@@ -511,6 +513,7 @@ def test_write_read_db_not_completed(tmp_dir):
reader = io_app.load_db()
got = reader(data_store.not_completed[0])
assert got.to_rich_dict() == nc.to_rich_dict()
+ data_store.close()
def test_write_read_db_summary_not_completed(tmp_dir):
@@ -522,6 +525,7 @@ def test_write_read_db_summary_not_completed(tmp_dir):
writer = get_app("write_db", data_store=data_store)
writer.main(nc, identifier="blah")
assert isinstance(writer.data_store.summary_not_completed, Table)
+ data_store.close()
def test_write_db_parallel(tmp_dir, fasta_dir):
@@ -542,6 +546,8 @@ def test_write_db_parallel(tmp_dir, fasta_dir):
got = [str(pathlib.Path(m.data_store.source) / m.unique_id) for m in result]
assert got != []
assert got == expect
+ dstore.close()
+ out_dstore.close()
def test_define_data_store(fasta_dir):
@@ -611,13 +617,15 @@ def test_writer_unique_id_arg(tmp_dir, writer, data, dstore):
name = pathlib.Path(get_data_source(source)).name
return name.split(".")[0]
- writer = get_app(writer, data_store=dstore(tmp_dir), id_from_source=uniqid)
+ out_dstore = dstore(tmp_dir)
+ writer = get_app(writer, data_store=out_dstore, id_from_source=uniqid)
m = writer(data)
# directory data stores have a suffix, so we create expected name using that
suffix = getattr(writer.data_store, "suffix", "")
suffix = f".{suffix}" if suffix else suffix
expect = f"blah{suffix}"
assert m.unique_id == expect
+ out_dstore.close()
src_attr = "source"
@@ -633,13 +641,15 @@ src_attr = "source"
],
)
def test_writer_no_unique_id(tmp_dir, writer, data, attr, dstore):
- writer = get_app(writer, data_store=dstore(tmp_dir))
+ out_dstore = dstore(tmp_dir)
+ writer = get_app(writer, data_store=out_dstore)
# no unique id possible, so m will be NotCompleted error
value = {} if attr == "info" else None
setattr(data, attr, value)
- m = writer(data)
- assert isinstance(m, NotCompleted)
- assert m.type is NotCompletedType.ERROR
+ with pytest.raises(ValueError, match="cannot name a record"):
+ _ = writer(data)
+
+ out_dstore.close()
@pytest.mark.parametrize(
=====================================
tests/test_app/test_sqlite_data_store.py
=====================================
@@ -1,3 +1,4 @@
+from contextlib import closing
from datetime import UTC, datetime
from pathlib import Path
@@ -91,16 +92,18 @@ def test_open_existing(dstore_on_disk):
def test_open_to_append(dstore_on_disk):
- DataStoreSqlite(dstore_on_disk, mode=APPEND)
+ with closing(DataStoreSqlite(dstore_on_disk, mode=APPEND)) as _:
+ pass
def test_open_to_write(dstore_on_disk):
- DataStoreSqlite(dstore_on_disk, mode=OVERWRITE)
+ with closing(DataStoreSqlite(dstore_on_disk, mode=OVERWRITE)) as _:
+ pass
def test_db_creation():
- db = DataStoreSqlite(":memory:", mode=OVERWRITE)
- db = db.db
+ store = DataStoreSqlite(":memory:", mode=OVERWRITE)
+ db = store.db
result = db.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
assert len(result) == 4
created_names = {r["name"] for r in result}
@@ -200,7 +203,7 @@ def test_limit_datastore(full_dstore_sqlite):
def test_validate(full_dstore_sqlite):
r = full_dstore_sqlite.validate()
- assert r.shape == (4, 2)
+ assert r.shape == (5, 2)
def test_set_record_type(full_dstore_sqlite):
@@ -268,6 +271,7 @@ def test_append_makes_logs(tmp_dir, ro_dir_dstore, name, suffix):
out_path = tmp_dir / name
got1 = _make_and_run_proc(out_path, suffix, ro_dir_dstore[:num])
assert len(got1.logs) == 1
+ got1.close()
# creating a separate instance should result in a
# new log file
@@ -276,6 +280,7 @@ def test_append_makes_logs(tmp_dir, ro_dir_dstore, name, suffix):
# should be a row for each log
summary = got2.summary_logs
assert summary.shape[0] == 2
+ got2.close()
def test_summary_not_completed(nc_objects):
=====================================
tests/test_core/test_annotation_db.py
=====================================
@@ -365,7 +365,7 @@ def test_load_annotations_multi(DATA_DIR):
def compressed_flat_file(DATA_DIR, tmp_path, request):
src = DATA_DIR / request.param
out_path = tmp_path / f"{request.param}.gz"
- with cogent3.open_(out_path, mode="w") as out:
+ with cogent3.open_(out_path, mode="wb") as out:
out.write(src.read_bytes())
return out_path
=====================================
tests/test_evolve/test_scale_rules.py
=====================================
@@ -42,10 +42,9 @@ class ScaleRuleTests(unittest.TestCase):
"""Scale rule requiring matrix entries to have all pars specified"""
model = _make_model_factory({"k": trans}, {"ts": trans, "tv": ~trans})
- assert self._get_scaled_lengths(model, {"k": 6.0, "length": 4.0}) == {
- "ts": 3.0,
- "tv": 1.0,
- }
+ assert self._get_scaled_lengths(
+ model, {"k": 6.0, "length": 4.0}
+ ) == pytest.approx({"ts": 3.0, "tv": 1.0})
def test_scaled_or(self):
"""Scale rule where matrix entries can have any of the pars specified"""
@@ -57,7 +56,7 @@ class ScaleRuleTests(unittest.TestCase):
assert self._get_scaled_lengths(
model,
{"k": 6.0, "length": 6.0, "ac": 3.0},
- ) == {"or": 5.0, "not": 1.0}
+ ) == pytest.approx({"or": 5.0, "not": 1.0})
def test_scaling(self):
"""Testing scaling calculations using Dn and Ds as an example."""
=====================================
tests/test_util/test_union_dict.py
=====================================
@@ -1,5 +1,7 @@
"""Unit tests for union_dict."""
+import pytest
+
from cogent3.util.union_dict import UnionDict
@@ -97,14 +99,6 @@ def test_sub_dicts_are_union():
assert isinstance(d.e.g, UnionDict)
-def test_get_subattr():
- """_getsubattr_ returns nested values via key"""
- d = UnionDict({"a": 1, "b": 2, "c": 3, "d": {"e": 5, "f": 6}})
- assert d._getsubattr_([], "a") == 1
- assert d._getsubattr_([], "d") == UnionDict({"e": 5, "f": 6})
- assert d._getsubattr_(["d"], "e") == 5
-
-
def test_setitem():
"""should work via property or key"""
d = UnionDict()
@@ -116,3 +110,168 @@ def test_setitem():
assert d.b.c == 42
assert d["b"] == {"c": 42}
assert isinstance(d.b, UnionDict)
+
+
+def test_or_with_plain_dict():
+ d = UnionDict({"a": 1, "d": {"e": 5}})
+ f = d | {"d": {"g": {"h": 8}}}
+ assert isinstance(f, UnionDict)
+ assert f.d.e == 5
+ assert f.d.g.h == 8
+ assert "g" not in d.d
+
+
+def test_two_level_shared_merge():
+ d = UnionDict({"d": {"e": {"k": 1}}})
+ f = d | {"d": {"e": {"m": 2}}}
+ assert f.d.e == {"k": 1, "m": 2}
+ assert d.d.e == {"k": 1}
+
+
+def test_or_leaves_right_operand_unaliased():
+ d = UnionDict({"a": 1})
+ e = UnionDict({"b": {"c": 1}, "p": {"q": {"r": 1}}})
+ f = d | e
+ f.b.c = 99
+ f.p.q.r = 99
+ assert e.b.c == 1
+ assert e.p.q.r == 1
+
+
+def test_union_none_replaces_dict():
+ d = UnionDict({"a": {"b": 1}})
+ d |= {"a": None}
+ assert d.a is None
+
+
+def test_union_dict_replaces_nested_value():
+ d = UnionDict({"a": {"b": 1}})
+ d |= {"a": {"b": {"c": 2}}}
+ assert isinstance(d.a.b, UnionDict)
+ assert d.a.b.c == 2
+
+
+def test_construction_leaves_input_unchanged():
+ data = {"a": {"b": {"c": 1}}}
+ d = UnionDict(data)
+ d.a.b.c = 2
+ assert type(data["a"]) is dict
+ assert type(data["a"]["b"]) is dict
+ assert data["a"]["b"]["c"] == 1
+
+
+def test_nested_ior_keeps_held_references():
+ layout = UnionDict({"xaxis": {"title": {"text": "a"}}})
+ xaxis = layout.xaxis
+ title = layout.xaxis.title
+ layout.xaxis |= {"range": [0, 1]}
+ title.text = "z"
+ assert layout.xaxis is xaxis
+ assert layout.xaxis.title.text == "z"
+
+
+def test_assigning_union_dict_copies_it():
+ d = UnionDict()
+ inner = UnionDict({"c": {"e": 1}})
+ d.b = inner
+ assert d.b == inner
+ assert d.b is not inner
+ assert d.b.c is not inner.c
+
+
+def test_construction_copies_shared_children():
+ shared = {"c": {"e": 1}}
+ d = UnionDict({"a": shared, "b": shared})
+ assert d.a == d.b
+ assert d.a is not d.b
+ assert d.a.c is not d.b.c
+
+
+def test_union_into_children_built_from_one_dict():
+ shared = {"x": 0}
+ d = UnionDict({"a": shared, "b": shared})
+ d |= {"a": {"x": 1}, "b": {"x": 2}}
+ assert d.a.x == 1
+ assert d.b.x == 2
+
+
+def test_setdefault_copies_dict():
+ shared = {"x": 0}
+ d = UnionDict()
+ d.setdefault("a", shared)
+ d.setdefault("b", shared)
+ assert isinstance(d.a, UnionDict)
+ assert d.a is not d.b
+ d |= {"a": {"x": 1}, "b": {"x": 2}}
+ assert d.a.x == 1
+ assert d.b.x == 2
+ assert shared == {"x": 0}
+
+
+def test_setdefault_returns_existing_value():
+ d = UnionDict({"a": {"x": 1}})
+ existing = d.a
+ assert d.setdefault("a", {"x": 2}) is existing
+ assert d.setdefault("b") is None
+
+
+def test_construction_from_self_referencing_dict_raises():
+ data: dict[str, object] = {"x": 1}
+ data["me"] = data
+ with pytest.raises(ValueError):
+ UnionDict(data)
+
+
+def test_union_with_self_referencing_dict_raises():
+ data: dict[str, object] = {"x": 1}
+ data["me"] = data
+ d = UnionDict({"x": 0})
+ with pytest.raises(ValueError):
+ d |= data
+
+
+def test_update_kwargs_converts_children():
+ d = UnionDict()
+ d.update(b={"c": {"e": 2}})
+ assert d.b.c.e == 2
+
+
+def test_ior_with_plain_dict_converts_children():
+ d = UnionDict({"a": 1})
+ d |= {"b": {"c": {"e": 2}}}
+ assert isinstance(d.b, UnionDict)
+ assert isinstance(d.b.c, UnionDict)
+ assert d.b.c.e == 2
+
+
+def test_union_method():
+ d = UnionDict({"d": {"e": 5, "f": 6}})
+ d.union({"d": {"f": 0}})
+ assert d.d == {"e": 5, "f": 0}
+
+
+def _make_nested(depth: int, leaf: dict[str, object]) -> dict[str, object]:
+ data = leaf
+ for _ in range(depth):
+ data = {"x": data}
+ return data
+
+
+def test_deeply_nested_construction():
+ depth = 2000
+ d = UnionDict(_make_nested(depth, {"leaf": 1}))
+ node = d
+ for _ in range(depth):
+ node = node.x
+ assert isinstance(node, UnionDict)
+ assert node.leaf == 1
+
+
+def test_deeply_nested_union():
+ depth = 2000
+ d = UnionDict(_make_nested(depth, {"a": 1}))
+ d |= _make_nested(depth, {"b": 2})
+ node = d
+ for _ in range(depth):
+ node = node.x
+ assert node == {"a": 1, "b": 2}
=====================================
uv.lock
=====================================
The diff for this file was not included because it is too large.
View it on GitLab: https://salsa.debian.org/med-team/python-cogent/-/commit/3606e8d9c96aa7a5c5ec798888a5cae5c44ebea9
--
View it on GitLab: https://salsa.debian.org/med-team/python-cogent/-/commit/3606e8d9c96aa7a5c5ec798888a5cae5c44ebea9
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20261019/995ef717/attachment-0001.htm>
More information about the debian-med-commit
mailing list