Skip to content

fix: several small bugs found in a static-analysis sweep#234

Merged
mmore500 merged 13 commits into
jeetsukumaran:mainfrom
steps-re:fix/static-analysis-sweep
Jul 13, 2026
Merged

fix: several small bugs found in a static-analysis sweep#234
mmore500 merged 13 commits into
jeetsukumaran:mainfrom
steps-re:fix/static-analysis-sweep

Conversation

@steps-re

Copy link
Copy Markdown
Contributor

Following up on your note in #233 (and thanks for the quick v5.0.10 release). I ran a static-analysis sweep over DendroPy and verified a handful of small, independent bugs. Each is its own commit with a regression test that fails before the change and passes after.

  • treemodel/_node.py (Node._set_edge): reassigning a node's edge silently dropped it from its parent's child list, corrupting the tree
  • calculate/treesum.py (summarize_node_ages_on_tree): is_bipartitions_updated=True triggered a re-encode instead of skipping one, the opposite of every sibling method in the module
  • calculate/treesum.py (summarize_node_ages_on_tree): collapse_negative_edges=True crashed with a TypeError on essentially any tree (compared ages in the wrong traversal order)
  • calculate/phylogeneticdistance.py (_calculate_standardized_effect_size): unweighted (edge-count) SES statistics never randomized the null model at all, due to a copy-pasted variable name
  • model/discrete.py (simulate_discrete_chars): the root_states argument was silently discarded in favor of a random root sequence
  • datamodel/charmatrixmodel.py (CharacterMatrix.concatenate): the unequal-length-sequences error message showed a literal %d instead of the matrix index
  • datamodel/charmatrixmodel.py (CharacterMatrix.concatenate): concatenating matrices with a shared label hung in an infinite loop
  • datamodel/charmatrixmodel.py (DiscreteCharacterMatrix.append_taxon_sequence): raised AttributeError on any non-empty call (referenced a nonexistent attribute)
  • utility/container.py (NormalizedBitmaskDict): mutating the dict silently broke all iteration (items()/keys()/values()) while len() stayed correct
  • datamodel/taxonmodel.py (Taxon.__init__): constructing a Taxon from another Taxon (or copying/cloning one) dropped its comments
  • treemodel/_node.py (Node.distance_from_root): crashed with a TypeError on a node with no edge length whose parent also had no edge length

Up front, so there are no surprises: I found these with AI assistance (a static-analysis pass), and I reviewed, reproduced, and tested each one myself before sending. I dropped a fair number of false positives from the same sweep along the way.

If you would rather I split these into separate PRs, or open issues instead, just say the word. I also have a longer list of candidates from the sweep that I have not fully verified yet. Happy to work through and send fixes for any areas that would be useful to you, or to hold off. Whatever fits how you like to receive contributions.

steps-re added 11 commits July 12, 2026 12:29
Node._set_edge() removed the node from its parent's _child_nodes list
as a side effect of swapping in a new Edge object, without re-adding it
anywhere or clearing _parent_node. Since a node's tail_node is derived
from head_node._parent_node (not stored independently), this detachment
had no bookkeeping purpose and only corrupted the tree: after
'node.edge = new_edge', the node vanished from its parent's children
while node.parent_node still pointed at the old parent.

Signed-off-by: Mike German <mike@stepsventures.com>
TreeSummarizer.summarize_node_ages_on_tree() checked
'if is_bipartitions_updated:' before re-encoding bipartitions, the
opposite of every sibling summarization method in this module (which
all skip the (deprecated) re-encode when the caller has already
updated bipartitions). Passing is_bipartitions_updated=True therefore
triggered an unwanted call to the deprecated Tree.encode_splits()
instead of skipping it.

Signed-off-by: Mike German <mike@stepsventures.com>
…_on_tree

The 'force parent at least as old as its oldest child' step ran inside
the same preorder pass used to assign ages, so it compared a node's
age against children that the preorder traversal had not visited yet
(still None), raising a TypeError on essentially any tree. Move the
adjustment to its own postorder pass, run after all ages are assigned.

Signed-off-by: Mike German <mike@stepsventures.com>
_calculate_standardized_effect_size() passed
is_shuffle_phylogenetic_distances instead of
is_shuffle_phylogenetic_path_steps to shuffle_taxa(). For
is_weighted_edge_distances=False, this meant both shuffle flags were
False, so the null-model replicates were never randomized at all,
silently producing a null_model_sd of 0 for standardized effect size
calculations on unweighted (edge-count) distances.

Signed-off-by: Mike German <mike@stepsventures.com>
simulate_discrete_chars() accepts a 'root_states' parameter but passed
'root_states=None' on to DiscreteCharacterEvolver.evolve_states(),
silently discarding it and always simulating a random root sequence
from the model's stationary distribution instead of the caller's
requested states.

Signed-off-by: Mike German <mike@stepsventures.com>
…ndex

The unequal-length-sequences error used a '%d' printf-style
placeholder but called '.format()' on the string (which only
substitutes '{}' placeholders), so the raised ValueError always showed
the literal, unhelpful '%d' instead of the offending matrix's index.

Signed-off-by: Mike German <mike@stepsventures.com>
The character-subset label de-duplication loop computed a new
candidate into 'label' but checked/looped on 'cs_label', which was
never reassigned. Any label collision (e.g. concatenating two
matrices with the same or default label) caused an infinite loop.

Signed-off-by: Mike German <mike@stepsventures.com>
append_taxon_sequence() referenced 'self.default_symbol_state_map',
an attribute that does not exist anywhere on CharacterMatrix or its
subclasses, so any call with a non-empty 'state_symbols' argument
raised an AttributeError. Use the existing
'default_state_alphabet.full_symbol_state_map' lookup instead.

Signed-off-by: Mike German <mike@stepsventures.com>
__setitem__() and __delitem__() called dict.__setitem__()/
dict.__delitem__() directly, bypassing OrderedDict's internal
bookkeeping. Reads (__getitem__, __contains__, get) were unaffected,
but iteration (items()/keys()/values()/__iter__(), and thus len()
staying correct while iteration went empty) was silently broken after
any mutation.

Signed-off-by: Mike German <mike@stepsventures.com>
Taxon.__init__() unconditionally set 'self.comments = []' after the
if/else branch, even for the Taxon-from-Taxon copy path, which had
just deep-copied 'comments' (along with the rest of __dict__) from the
source Taxon a few lines earlier. The unconditional reset silently
discarded it, so Taxon(other_taxon), copy.deepcopy(), and .clone() all
dropped comments on the copy.

Signed-off-by: Mike German <mike@stepsventures.com>
…length

When a node's own edge length is None but it has a parent, the method
fell back to float(self._parent_node.edge.length); if the parent's
edge length was also None, this raised a TypeError instead of
degrading to 0.0 the way the sibling no-parent/no-length branch
already does.

Signed-off-by: Mike German <mike@stepsventures.com>
@codecov-commenter

codecov-commenter commented Jul 12, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.81%. Comparing base (6245993) to head (ae2c0f4).
⚠️ Report is 2 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #234      +/-   ##
==========================================
+ Coverage   88.71%   88.81%   +0.09%     
==========================================
  Files         114      114              
  Lines       12853    12963     +110     
==========================================
+ Hits        11403    11513     +110     
  Misses       1450     1450              
Flag Coverage Δ
unittests 88.81% <100.00%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mmore500 mmore500 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks for your work on this @steps-re , think this is a good use of the new capabilities for static analysis that are coming online --- some good catches in here. There is one change I flagged I want to verify further, so if we can split that out into its own PR, I think that will be helpful.

Otherwise, a few very minor suggestions then I think we'll be ready to merge.

Comment thread src/dendropy/datamodel/taxonmodel.py Outdated
Comment thread src/dendropy/model/discrete.py Outdated
Comment thread src/dendropy/datamodel/taxonmodel.py
Comment thread src/dendropy/datamodel/charmatrixmodel.py
Comment thread src/dendropy/datamodel/charmatrixmodel.py
Comment thread tests/unittests/test_phylogenetic_distance_matrix.py Outdated
Comment thread tests/unittests/test_tree_summarization_and_consensus.py Outdated
Comment thread src/dendropy/datamodel/treemodel/_node.py
Comment thread tests/unittests/test_tree_summarization_and_consensus.py
steps-re added a commit to steps-re/DendroPy that referenced this pull request Jul 13, 2026
…y collapse behavior

Applies @mmore500's review on jeetsukumaran#234:
- taxonmodel/discrete: accept docstring suggestion edits
- drop the SES-shuffle and is_bipartitions_updated deprecation tests
  (complicated tests for simple fixes, per request); the underlying
  source fixes stay
- replace the collapse_negative_edges crash test with a behavioral test
  that asserts a negative edge (child older than parent) is actually
  collapsed by raising the parent to its oldest child's age

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
steps-re added a commit to steps-re/DendroPy that referenced this pull request Jul 13, 2026
Per @mmore500's review on jeetsukumaran#234, the change that stopped Node._set_edge()
from removing the node from its parent's _child_nodes list needs its own
discussion (whether _parent_node should also be reset). Moved to branch
fix/node-parent-reindex; removed here along with its regression test.
The unrelated distance_from_root() None-parent fix stays in this sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@mmore500 mmore500 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm! thanks @steps-re

steps-re added 2 commits July 13, 2026 18:46
…y collapse behavior

Applies @mmore500's review on jeetsukumaran#234:
- taxonmodel/discrete: accept docstring suggestion edits
- drop the SES-shuffle and is_bipartitions_updated deprecation tests
  (complicated tests for simple fixes, per request); the underlying
  source fixes stay
- replace the collapse_negative_edges crash test with a behavioral test
  that asserts a negative edge (child older than parent) is actually
  collapsed by raising the parent to its oldest child's age
Per @mmore500's review on jeetsukumaran#234, the change that stopped Node._set_edge()
from removing the node from its parent's _child_nodes list needs its own
discussion (whether _parent_node should also be reset). Moved to branch
fix/node-parent-reindex; removed here along with its regression test.
The unrelated distance_from_root() None-parent fix stays in this sweep.
@mmore500
mmore500 force-pushed the fix/static-analysis-sweep branch from 4e4fe7b to ae2c0f4 Compare July 13, 2026 22:46
@mmore500

Copy link
Copy Markdown
Collaborator

Stripped @claude authorship because we haven't yet set a policy for the project (for my own projects, I tend to use tool authorship, but this should probably be decided in a more formal way on a shared project like this).

@mmore500
mmore500 merged commit 05ef717 into jeetsukumaran:main Jul 13, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants