Skip to content

Keep a copied PropertyBitmaps' labels apart from the original's - #212

Open
ulfarsson wants to merge 2 commits into
developfrom
property-bitmaps-copy
Open

ulfarsson wants to merge 2 commits into
developfrom
property-bitmaps-copy

Conversation

@ulfarsson

Copy link
Copy Markdown
Member

Adds PropertyBitmaps.__copy__, in permuta/bisc/bisc.py, so that copy.copy of a PropertyBitmaps gives one whose lengths are its own. Until now the copy shared the original's dictionaries, and its views, good, bad and bitmaps, were the original's objects, so a length added to the copy was added to the original too:

>>> import copy
>>> increasing = PropertyBitmaps(Perm.is_increasing)
>>> increasing.add_length(3)
>>> copied = copy.copy(increasing)
>>> copied.add_length(4)
>>> sorted(increasing.good_counts), sorted(copied.good_counts)
([3], [3, 4])
>>> 4 in copied.good, 4 in increasing.good
(True, False)

On develop the last two lines give ([3, 4], [3, 4]) and (True, True). copy.deepcopy and pickle already kept the two apart, and are unchanged. PropertyBitmaps is new in Unreleased (#209), so no release has the old behavior.

Details

Why

copy.copy used Python's default shallow copy, which copies the instance's attributes as they are: the dictionaries of labels and of good counts, and the three views, each bound to the original. Copying a PropertyBitmaps to try a length or another property on it is a natural use, and it changed the original without a word.

What changes

  • PropertyBitmaps.__copy__ makes a new PropertyBitmaps with the same property and gives it copies of the two dictionaries, so its views read the copy.
    • A length's labels are shared: they are bytes, or an array or a list that is replaced when the length is labeled again, never changed in place.
    • So a copy costs two small dictionaries, whatever the lengths hold.
    • The copy is a PropertyBitmaps, whatever the class of self, as its docstring says. Nothing in Permuta, Tilings or comb_spec_searcher subclasses the class.
    • Its docstring has the example above as a doctest.
  • tests/bisc/test_property_bitmaps.py gets test_a_copy_keeps_its_lengths_apart_from_the_original, for copy.copy and copy.deepcopy, on lengths kept in both forms. It checks that:
    • the copy reads as the original does;
    • a length added to either is not added to the other;
    • each view of each reads its own lengths;
    • a length labeled again with another property changes only in the copy.
  • tests/bisc/test_sparse_storage.py gets two tests of what the copy shares:
    • test_relabeling_a_length_kept_as_indices_changes_only_one: lengths kept as an array, labeled again in the copy or in the original, change only in the one relabeled. The sharing rests on that.
    • test_a_copy_of_lengths_kept_as_indices_sizes_no_bitmap: a copy of lengths kept as indices, in an array and, past length 20, in a list, sizes no bitmap, where building one at length 14 would take 10.9 GB.
  • No CHANGELOG.md entry, as Unreleased already names PropertyBitmaps.

Checks

  • tox: flake8, mypy, pylint and black pass. On CPython 3.15 all 702 tests pass, the doctests of bisc.py among them. On PyPy 3.12, 700 pass and 2 are skipped, which need CPython's reference counting and tracemalloc.

  • Against develop: every new test fails, on both Pythons, but for the copy.deepcopy case, which develop already got right.

  • Downstream: Tilings' 3,722 tests pass at its develop with this Permuta, on both Pythons.

  • Faults made by hand, each run against the tests. Each of these fails at least one test:

    • a dictionary shared, or either one left out;
    • develop's shallow copy;
    • any one view bound to the original;
    • _keep overwriting a kept array in place;
    • a copy made through the constructor from dict(self.bitmaps).

    mutmut's six mutants of __copy__ are killed too.

  • Speed: copy.copy takes 0.93 µs against develop's 0.98 under CPython, and 0.04 µs against 0.64 under PyPy. That is for a PropertyBitmaps of Perm.is_increasing with lengths 0 to 8. copy.deepcopy and pickle are level.


AI assistance: written with Claude Code (Opus 5.5) as the Permuta developer, and reviewed before submission by Claude Code (Opus 5.5) as an automated reviewer; Henning Ulfarsson checked and opened it.

ulfarsson and others added 2 commits November 6, 2026 19:59
copy.copy of a PropertyBitmaps used Python's default shallow copy: the
copy shared the original's dictionaries of labels and good counts, and
its views, good, bad and bitmaps, were the original's objects, bound to
it. A length added to the copy was added to the original too.

__copy__ now makes a new PropertyBitmaps with the same property and
copies of the two dictionaries, so its views read the copy. A length's
labels are shared, as they are replaced when the length is labeled
again, never changed. copy.deepcopy and pickle already kept the two
apart. The new test fails on develop with copy.copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g and cost

The docstring of __copy__ now says exactly what the copy owns, its
dictionaries of lengths, while the labels inside them are shared, and
that the copy is a PropertyBitmaps whatever the class of self. Its
example also shows a view of the copy reading the copy.

The tests now check that the bad view of a copy reads the copy too,
that labeling a length kept as indices again, in the copy or in the
original, changes only the one relabeled, which the sharing rests on,
and that copying lengths kept as indices, in an array and in a list,
sizes no bitmap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant