Skip to content

Set up the package skeleton, packaging and shared test fixtures — Closes #2 - #7

Draft
conradbzura wants to merge 15 commits into
mainfrom
quadrat-2-package-skeleton
Draft

conradbzura wants to merge 15 commits into
mainfrom
quadrat-2-package-skeleton

Conversation

@conradbzura

@conradbzura conradbzura commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stand up the package so the ported data path has somewhere to land: src layout, hatchling, ruff, pytest config, CI, and a shared test fixture layer. No ported logic, so a later failure is attributable to the port rather than to the scaffolding under it.

Follow abdenlab/giql — the nearest sibling and quadrat's own dependency — for the src layout, hatchling, ruff at line-length 89 and PEP 735 dependency groups. Set the Python floor at 3.11 rather than cherimoya's 3.10, because polars-bio, polars and giql are all gated there and the pileup is not an optional part of what this package is for.

Three trade-offs worth noting. The ruff lint select is pinned to ["E4", "E7", "E9", "F", "I"] rather than left to the default: ruff enables UP, SIM, FURB and RUF by default, and UP032 alone fires on the str.format style used throughout the code this repository is about to receive, so the default would demand a style migration inside the change that moves the code — I is in anyway, because import order is mechanical even in ported code and is the one style defect a reviewer reliably misses. giql resolves from a pinned public git revision rather than a version specifier, because the datafusion-bio target is not on PyPI; that pin is the one thing blocking publication, and it is why the pileup extra resolves with uv and not yet with pip. And the cut-site table builder and window-geometry arguments that acceptance criterion #4 originally named are relocated to issue #4, the first slice whose tests take either as input, because the on-disk cut-site schema such a builder would have to target is not fixed until #3 lands.

Closes #2

Proposed changes

Packaging

Shear the runtime dependencies from cherimoya's sixteen to six and put polars-bio, polars, giql and pyfaidx behind a pileup extra. Every one of those imports is already lazy inside a function body upstream, so a caller who streams windows over tables they already have never pays for them. State that guarantee over the relationship rather than over the members — nothing the extra installs is imported at package import time — so the extra in pyproject.toml stays the single enumeration and adding a fifth dependency does not falsify the docstring.

Correct two defects in cherimoya's manifest rather than carrying them: pyfaidx is imported by production code but declared only in a dev group, and cherimoya.workers is missing from the explicit package list so it never ships in a built distribution. Hatchling's src discovery makes the second unrepeatable.

Declare the licence once, as a PEP 639 expression with no accompanying classifier, since PyPI rejects a distribution carrying both. Declare the sdist contents where hatchling reads them, with ** on both sides of the checkpoint exclusion: the pattern is gitignore-style, so a single * spans one path segment and would ship every checkpoint directory deeper than one level.

Make the pytest configuration enforce itself — --strict-markers so a misspelt marker is a collection error rather than a test silently neither selected nor deselected, and a deprecation filter scoped to first-party code so a stale API surfaces instead of being silenced along with the third-party noise that motivated the filter.

Shared test fixtures

Add tests/builders.py — plain functions, no fixtures and no state, each rendering one synthetic artifact — and a corpus_factory fixture over them. Cherimoya has no data-path fixtures in conftest: every one is module-local, and the FASTA, BAM and BED quintet is duplicated five ways differing only in chromosome sizes. Take the geometry as an argument so the five sets collapse onto one factory.

State the builders' observable behaviour where a caller reads it rather than where a test happens to assert it. write_fasta applies its N runs after its soft mask, so a position covered by both reads as an upper-case N; aligned_read carries the CIGAR it is given whether or not the record is placed, falls back to twenty bases only for a CIGAR consuming no query, and produces a half-placed record for a contig the header does not declare. Each of those was already load-bearing somewhere in the suite before it was written down.

Import pysam under a guard so the module imports without it, and give the skip two owners that between them cover every path: the suite's pysam fixture for a test that touches a builder directly, and corpus_factory on its own behalf when a call asks for an alignment or a FASTA index. Reject header contigs that do not cover the reference at its declared length, since reads are placed against the reference and a narrower header would otherwise yield records it cannot resolve, inside a corpus a later suite would assert against.

Give the cache directory two tiers and document which is in force when. A session directory is assigned at import time, before anything imports the package, because the function that will read it reads it per call: set later, the first reader still sees the user's own cache. That directory is removed at exit rather than on a fixture teardown, which the subprocess tests reach and a fixture would not. During a test body an autouse fixture supersedes it with a directory that test alone can see, so a stale entry from an earlier test cannot satisfy one written to exercise a cold build.

Put what the test modules share in tests/__init__.py rather than per module — the geometries, immutably, so a corpus built from one cannot alter the geometry a later corpus is built from; the project root; the BAM reader the assertions compare placements through; the child-process runner that reports the stderr a failed subprocess would otherwise swallow; and the Hypothesis settings every file-writing property test runs under. Those settings carry no deadline, because such a test spends about a second in pysam's sort and index on a cold first call and Hypothesis would report that timing as a falsifying example; the properties that stay in memory keep theirs.

Pin the builders' invariants with property tests rather than examples where the domain is wide: that a FASTA holds its declared lengths and masks exactly the intervals it is given, that intervals tile their range, that a header declares any mapping, that a query and its qualities match any CIGAR, that a BAM round-trips any reads in sorted order, that every random read starts far enough from its contig's end to finish inside it, that every contig receives one, and that the surplus skews toward the first.

Repository

Add a README carrying the install shapes, the import guarantee and the commands the acceptance criteria are written in terms of, a .gitignore, and a test workflow modelled on giql's — permissions: {}, persist-credentials: false, a concurrency group, and both actions pinned to full commit SHAs so a re-tagged release cannot execute in CI without a repository change. Sync with --locked, so a green run says the committed lock resolves rather than that some resolution uv wrote and discarded did, and run the bare install as a job of its own, because that is the install the import guarantee is about and syncing without extras would strip the venv the test job needs.

Ignore .sdlc/, which holds local review roles and guides, untracked here as in the sibling projects.

Test cases

# Test Suite Given When Then Coverage Target
1 test_builders A mapping of two contig names to lengths The builder renders it Write each contig at its declared length over the DNA alphabet FASTA rendering
2 test_builders A soft-masked interval and an N run on a known contig, with unmasked sequence on both sides of each The builder renders it Lowercase only the first interval, fill only the second with N, and leave every base outside them an uppercase ACGT FASTA rendering
3 test_builders Two renders of one geometry under the same seed The bases are compared Produce identical bases, so every later assertion about this data is reproducible FASTA rendering
4 test_builders Two renders of one geometry under different seeds The bases are compared Produce different bases, so a geometry can be rendered more than one way FASTA rendering
5 test_builders A contig longer than the requested line width The builder renders it Wrap every sequence line but the last at that width, which is what makes the file indexable FASTA rendering
6 test_builders An N run whose stop lies past the end of its contig The builder renders it Still write the contig at its declared length, with the run clipped to the end, because a contig longer than its own header would make the suite agree with itself and nothing else FASTA rendering
7 test_builders A soft mask whose start lies before the beginning of its contig The builder renders it Lowercase from the contig's first base to the mask's stop, because a negative start truncates to an empty slice and would otherwise mask nothing at all FASTA rendering
8 test_builders A two-contig geometry with indexing requested The builder renders it Write an index beside the file from which each contig's full sequence can be fetched, which is the path the sequence stores read through FASTA rendering
9 test_builders Any mapping of contig names to positive lengths, any soft masks and any N runs over them including intervals past a contig's end, at any line width The builder renders it and the file is parsed back Hold every contig at exactly its declared length, within the masked and unmasked DNA alphabet, with no line longer than the width FASTA rendering
10 test_builders Any contig mapping with any soft masks and N runs over it, including intervals reaching past a contig's end The builder renders it and the file is parsed back Leave no position inside a requested interval unmasked, i.e., N where a run covers it and lower case where only a soft mask does FASTA rendering
11 test_builders One row and a path whose suffix is plain or gzipped The builder writes it and the matching reader reads it back Produce the same tab-separated line either way, so a caller reads back what it wrote whichever suffix it chose BED writing
12 test_builders Several rows, the first carrying a name, score and strand beyond the leading three fields The builder writes them Write one line per row in order, keeping only the contig, start and stop BED writing
13 test_builders Any list of contig, start and stop rows, written to both a plain and a gzipped path Each file is read back with its matching reader Return the original rows in order from both BED writing
14 test_builders A range of 400 to 600 at a step of 75 and a width of 20 The builder builds the rows Return every start in the range, each paired with a stop one width along Interval tiling
15 test_builders Any start, stop, positive step and positive width The builder tiles the range Return one row per step across the range, each spanning exactly one width, and nothing at all once the start has caught the stop Interval tiling
16 test_builders A mapping of two contig names to lengths The builder builds a header Mark the order coordinate-sorted and declare each contig at its length in insertion order, each resolving to its insertion index Alignment header
17 test_builders Any mapping of contig names to positive lengths The builder builds a header from it Declare every contig at its length in the mapping's own order and resolve each name to that position, so a read placed by name lands on the contig the caller named Alignment header
18 test_builders A read with five soft-clipped and fifteen matched bases The builder builds it Give the record a twenty-base query, so the CIGAR and the sequence agree Record construction
19 test_builders A read placed at a known position on a known contig The builder builds it Resolve the contig to its header id and start at that position Record construction
20 test_builders A read whose contig is None The builder builds it Carry no reference and no start while keeping the CIGAR it was given, so a record with no coordinates is something a caller can construct rather than only encounter, and the query length still follows the CIGAR there Record construction
21 test_builders A read given the reverse flag and an explicit mapping quality The builder builds it Mark the record reverse and carry that mapping quality Record construction
22 test_builders A hard-clipped CIGAR over a fifteen-base match The builder builds it Size the query from the matched span alone, because a hard clip names bases the record does not carry Record construction
23 test_builders A contig name the header does not declare The builder builds it Leave the record without a reference while keeping its start and CIGAR -- a half-placed record, distinct from the unplaced one a None contig produces Record construction
24 test_builders Any CIGAR assembled from match, soft-clip, insert and delete operations, consuming at least one query base The builder builds the record Give the sequence and the qualities equal length, both matching the length the CIGAR consumes -- an invariant pysam raises on rather than tolerates Record construction
25 test_builders Three reads supplied in descending coordinate order The builder writes them Emit them in ascending coordinate order beside an index, and return the path BAM sorting and indexing
26 test_builders Reads written through the staging file the builder sorts from The builder finishes Leave only the BAM and its index, so nothing later reaching into the directory can pick up a file whose order disagrees with that index BAM sorting and indexing
27 test_builders Four reads across two contigs in scrambled order The builder writes them Order them by contig as the header declares them, then by position within each BAM sorting and indexing
28 test_builders Any reads over any contig mapping, handed over in any order, including unplaced ones The builder writes them and every record is read back Hold exactly those reads with the placed ones in contig then position order, so no input can produce a file whose order disagrees with its index BAM sorting and indexing
29 test_builders One placed read and one unplaced one The builder writes them and every record is read back Hold both with the unplaced one last, so a count taken over the file counts records written rather than records the index can find BAM sorting and indexing
30 test_builders A geometry whose only contig is shorter than the read length The builder is asked to place reads on it Raise ValueError naming that contig, because a random offset on it would put the record past the end the header declares Random read placement
31 test_builders A contig exactly as long as the read length The builder places one read on it Write that read across the whole contig, because a contig that can hold a read is one the builder has no reason to turn down Random read placement
32 test_builders A contig twice the read length, and a seed that draws the latest start the contig admits The builder places one read on it End at the contig's last base, so a suite expecting signal near a boundary is not handed a region no read can ever occupy Random read placement
33 test_builders A request for twelve randomly placed reads The builder writes them Write exactly that many records and return the path Random read placement
34 test_builders A request for an even number of randomly placed reads The builder writes them Put half on each strand, so a suite reading strand from the data sees both without arranging for either Random read placement
35 test_builders Two renders of one geometry under the same seed The placements are compared Place every read identically, so a suite that depends on where a read landed can depend on the seed instead Random read placement
36 test_builders Two renders of one geometry under different seeds The placements are compared Place them differently, so the seed is a real knob rather than a parameter the builder ignores Random read placement
37 test_builders A three-contig geometry and forty reads The builder places them Put at least one read on every contig and more on the first than on any other, so a per-contig count is neither zero anywhere nor the same number everywhere Random read placement
38 test_builders Any contig mapping the builder accepts The builder places reads across it Start every read far enough from the end that a record of the read length still finishes inside the contig, so the bound the refusal protects holds across geometries rather than only at the two sizes an example can pin Random read placement
39 test_builders Any contig mapping, and any read count at or above the contig count, i.e., the condition under which the builder promises it The builder places them Put at least one read on every contig, so no caller asking for more reads than contigs is handed an empty one Random read placement
40 test_builders Any contig mapping, and any read count above the contig count, i.e., the condition under which the builder promises a skew The builder places them Put more reads on the first contig than on any other, so a suite that splits work per contig sees an uneven split rather than one it only assumes is uneven Random read placement
41 test_builders An explicit read length The builder writes the reads Give every record a CIGAR and a query of that length Random read placement
42 TestCorpus A root directory and a chromosome mapping A corpus is instantiated Expose that root and mapping with no alignments and no locus files, so a test cannot read a file nothing built Corpus attribute bag
43 TestCorpus A caller's chromosome mapping A corpus is built from it Hold an equal but separate mapping, so a test that mutates its own geometry cannot corrupt a constant every later corpus in the session is built from Corpus attribute bag
44 TestCorpus A corpus rooted at a known directory A name is resolved through it Return that name inside the corpus directory as a string, which is what the builders take Corpus attribute bag
45 TestCorpus A corpus An attribute outside its declared slots is assigned Raise AttributeError, which is the whole reason the class is an attribute bag rather than a dict Corpus attribute bag
46 test_conftest A corpus requested with peaks and nothing else The factory builds it Populate the peaks and leave the rest unset, beside the FASTA it writes unconditionally, so a test cannot read a file it never asked to exist Fixture factory
47 test_conftest A corpus requested with peaks, negatives and an exclusion list The factory builds it Write all three, with only the exclusion list gzipped, as the suffix the factory gives each one dictates Fixture factory
48 test_conftest A corpus requested with an empty peaks iterable The factory builds it Leave the peaks attribute unset, so asking for no loci is today indistinguishable from not asking -- a limitation worth pinning, because the underlying writer does accept an empty row list Fixture factory
49 test_conftest Two corpora requested under one name and identical chromosome sizes Both are built Give each its own directory, so neither can observe the other's writes even though the factory itself lives for the whole session Fixture factory
50 test_conftest A corpus built from a caller's chromosome mapping That mapping is compared to the corpus's own Hold an equal but separate mapping, so a test cannot corrupt a shared constant for the rest of the session Fixture factory
51 test_conftest A corpus requested with a seed, a soft mask, an N run and FASTA indexing The factory builds it Produce a FASTA matching a direct render of the same request, with an index beside it Fixture factory
52 test_conftest A corpus requested with two alignments The factory builds it Write two separately named BAMs with an index beside each, so a later suite can open either by name and seek in it Fixture factory
53 test_conftest A corpus requested with two alignments of a given read count The factory builds it Write that many records to each of them, so the keyword reaches the builder rather than stopping here Fixture factory
54 test_conftest A corpus requested with two alignments Their placements are compared Have placed them differently, so a suite reading two samples sees two Fixture factory
55 test_conftest Two corpora of one geometry requested under the same seed Their alignments are compared Place every read identically, so a suite that rebuilds a corpus reasons about the same data twice Fixture factory
56 test_conftest Two corpora of one geometry requested under different seeds Their alignments are compared Place the reads differently, so the seed is a real knob on the alignment path rather than one the factory accepts and drops Fixture factory
57 test_conftest Header contigs that leave out a contig the chromosome sizes declare, or declare one of them shorter than it is The factory is asked to build an alignment over them Raise ValueError naming the uncovered contig, because reads are placed against the reference and would otherwise be written half-placed or past the end the header declares Fixture factory
58 test_conftest A corpus whose header contigs include one absent from the chromosome sizes the FASTA was built from The factory builds an alignment over it Write the BAM with that contig in its header, so a header-versus-reference mismatch is something a suite can construct rather than only hope to encounter Fixture factory
59 test_conftest A corpus requested with one alignment and no header contigs The factory builds it Give the BAM a header whose contigs and lengths equal the chromosome sizes, so the symmetric case needs no argument Fixture factory
60 test_conftest An arbitrary contig mapping and a request for peaks and an exclusion list over it The factory builds a corpus Report that geometry and write a sequence identical to a direct render of it, with both locus files present, so the one parameter the factory exists to take is not assumed at a single value Fixture factory
61 test_conftest A fresh interpreter with no cache variable set The suite's conftest is imported and nothing else Set the variable while leaving the package unimported, so any later import of it sees the variable already there, for the reason the conftest module docstring gives Cache isolation
62 test_conftest A fresh interpreter with the cache variable already exported to a path of the caller's own The suite's conftest is imported Replace that value with a fresh temporary directory, for the reason the conftest module docstring gives Cache isolation
63 test_conftest A fresh interpreter with progress bars explicitly enabled The suite's conftest is imported Leave that value in place, because a progress bar changes nothing a test can observe and watching one is how a hang gets diagnosed Progress-bar setting
64 TestDistribution An installed distribution of the package The declared version and the distribution metadata version are compared Report the same version from both, so the attribute the build backend reads is the one that versions the artifact Package surface
65 TestDistribution The package's declared export list The package is star-imported into a fresh namespace Bind exactly the declared names, so __all__ is enforced rather than documentary Package surface
66 TestDistribution Every module of the installed package The top-level declarations carrying a # public marker are collected across all of them Name exactly the package's export list, so a symbol marked public but never re-exported -- or re-exported without the marker -- fails here rather than drifting until someone reads both files Package surface
67 TestDistribution A fresh interpreter in an environment where the optional dependencies are installed The package is imported and the loaded modules inspected Have loaded none of polars, polars-bio, giql or pyfaidx, so a caller who already has cut-site tables never pays for the engine that builds them Lazy pileup imports
68 TestDistribution A wheel built from the repository Its metadata is inspected Carry a licence expression and no licence classifier, because PEP 639 directs tools not to combine the two and PyPI rejects a distribution that does Built metadata
69 TestDistribution A source distribution built from the repository Its members are inspected Carry the test package including conftest, so the suite runs from an sdist exactly as it does from the repository Source distribution
70 TestDistribution A staged tree holding exactly what the sdist declares it includes, with checkpoint directories planted one and two levels inside those roots. The copy carries no .gitignore, whose own checkpoint entry would otherwise keep them out of the archive and leave this unable to fail A source distribution is built from that tree Ship none of them, so the exclusion rests on the build configuration the distribution declares rather than on an ignore file that is no part of it Source distribution

@conradbzura conradbzura self-assigned this Oct 23, 2026
@conradbzura
conradbzura force-pushed the quadrat-2-package-skeleton branch 6 times, most recently from 118d766 to 93206c2 Compare October 28, 2026 14:25
Somewhere for the ported data path to land. No logic yet, so
that a later failure is attributable to the port rather than to the
scaffolding it lands on.

Follows abdenlab/giql, the nearest sibling and quadrat's own
dependency: src layout, hatchling, ruff at line-length 89, four-space
indentation, PEP 735 dependency groups. Python floor is 3.11 rather
than cherimoya's 3.10, because polars-bio, polars and giql are all
gated there and the pileup is not an optional part of what this
package is for.

Runtime dependencies shear from cherimoya's sixteen to six, with the
pileup engine behind a `pileup` extra. Every one of those imports is
already lazy inside a function body upstream, so a caller that streams
windows over tables it already has never pays for them. Two defects in
cherimoya's manifest are corrected rather than carried: pyfaidx is
imported by production code but declared only in a dev group, and
cherimoya.workers is missing from the explicit package list, so it
never ships in a built distribution. Hatchling's src discovery makes
the second unrepeatable.

The ruff select is pinned rather than left to the default. Ruff 0.17
enables UP, SIM, FURB and RUF by default, and UP032 alone fires on
cherimoya's `str.format` style throughout -- so the default would
demand a style migration inside the change that moves the code, which
is the one thing that makes a port unreviewable.

giql resolves from the public commit at the tip of its cherimoya-mvp
branch. The datafusion-bio target is not on PyPI and cherimoya pins a
path outside its own tree, which a published package cannot inherit.
That commit's library code is identical to what cherimoya builds
against; the only delta is benchmark material outside src.

The fixture layer is the substantive half. Cherimoya has no data-path
fixtures in conftest -- every one is module-local, and the FASTA, BAM
and BED quintet is duplicated five ways differing only in chromosome
sizes and window geometry. Porting that duplication would multiply it,
so the builders take the geometry as arguments and the five sets
collapse onto one factory. Cut-site builders arrive with the writer
they need, in the pileup slice.

The cache directory is pointed at a fresh temporary directory at import
time, before anything imports the package, because default_cache_dir
reads it per call: set later, the first reader would still see the
user's own cache, and a surviving entry would satisfy a test written to
exercise a cold build.
Review roles, authoring guides and accumulated review documents are
local working state, untracked here as they are in the sibling
projects. The review history is versioned by its own repository under
.sdlc; the guides are config rather than a project artifact.
Hatchling does not read MANIFEST.in -- that is a setuptools file, so
its graft and excludes were inert. An sdist built with the file and one
built without it came out byte-identical: the test tree was shipping
only by way of hatchling's gitignore-based default, never by the
deliberate graft the file appeared to express.

Naming the contents under the sdist target makes it deliberate, and
drops .github and uv.lock from the distribution on the way.
The run was written with a list-slice assignment, so a right-hand side
longer than the slice grew the contig rather than being truncated: a
100 bp contig masked over 90 to 120 came out 120 bases long, under a
header still declaring 100. Soft masking is immune, since lowercasing
preserves the count.

A FASTA whose bases and declared lengths disagree is the worst shape
this builder can produce. Every later suite's reference comes from
here, so the suite would have agreed with itself and with nothing else.
The round-robin branch ran only when the index was divisible by three
and then indexed by that same index, so it selected the first contig
again whenever the contig count also divided three. Measured over 60
reads: three contigs put all 60 on the first and left two empty, six
contigs reached only the first and the fourth. Dividing the index by
three before the modulo walks the contigs as intended.

The docstring claimed a two-to-one skew toward the first contig. The
real ratio is 2k+1 for k contigs, which the corrected text now states
along with the short-contig overrun it has always had.
Setting the variable with setdefault honoured an exported value, which
is exactly the surviving cache the comment above it rules out: a run
by a developer who exports QUADRAT_CACHE_DIR read and wrote their own
cache, and the existing cache test failed outright for them. Plain
assignment is what the comment already claimed and what the issue asks
for.

TQDM_DISABLE stays a setdefault. Turning progress bars back on to
watch a hang changes nothing a test can observe.
The corpus held the caller's own mapping, and the factory that builds
one is session-scoped, so a corpus built from a module constant was
that constant. One test mutating what looks like its own attribute
would have corrupted the constant for every test after it, in file
order, for the rest of the session.
Every later assertion about the pileup, the window cache or the sampler
is an assertion about data these builders produced, so a builder that
silently writes the wrong thing makes the suite agree with itself and
with nothing else. The suite now covers all seven builders -- three had
none -- every corpus_factory keyword, Corpus, the environment block and
the distribution surface, and a named test fails for each of the five
defects fixed in the commits before this one.

Split three ways by module under test: the builders, the conftest and
the package. The builders file is ordered foundational-first, so a run
that stops at the first failure stops at the deepest one rather than at
whichever caller happened to come first alphabetically.

A pysam fixture replaces the module-level importorskip. That skip
aborted collection of all thirteen tests when four needed pysam, and
reported it as one line, so a reader could not tell thirteen tests had
gone. Without pysam the suite is now 25 passed and 19 skipped, one line
each, and the FASTA, BED and interval builders still run -- which is
what importing pysam lazily was for.
The fixture layer's prose was written as commentary on the migration --
what the upstream repository carried, which half of the work was
larger, what the arithmetic used to do, what the ported suites would do
later -- rather than as the contract each member offers a caller. That
one habit produced the changelog archaeology, the claims with three and
four homes apiece, the counts that were wrong on the commit that
introduced them, the enumerations of consumers that do not exist yet,
and the implementation mechanics sitting in contract prose. The same
orientation is why the guarantees that should have been documented were
absent: the N-run clamp, the chromosome-sizes copy, the unbuilt-versus-
empty convention and the whole keyword surface of the corpus factory
were each pinned by a test and stated in no docstring.

One rule applied throughout. Each docstring states what its member does
and guarantees; the migration story stays in version control; a claim
has exactly one home and every other site points at it. Mechanism that
was worth keeping moved under an Implementation notes rubric rather
than being deleted.

Nothing executable changed. With docstrings stripped, the abstract
syntax tree of every touched module is identical to the one before.
@conradbzura
conradbzura force-pushed the quadrat-2-package-skeleton branch from 93206c2 to 142c822 Compare October 28, 2026 15:41
write_fasta clamped only its N runs, so a soft mask starting before a
contig truncated to an empty slice and masked nothing, while the
docstring promised both parameters were clipped to the contig.

write_random_bam reserved twice the read length before placing a read,
so the last twenty bases of every contig could never be covered and the
last forty could never hold a read start. Its own error message
justifies one read length, not two, and a later suite expecting signal
near a contig boundary would have found a region no read can occupy.

aligned_read set the CIGAR only on the placed branch, so an unplaced
record discarded the argument without a word: the inferred query length
came back empty and the query fell through to the twenty-base fallback.
The docstring claimed the opposite, and that claim is now true rather
than qualified away. pysam keeps a CIGAR on a record with no reference
id and it survives a write, sort, index and read back, and the default
20M infers twenty, so no existing caller sees a change. The docstring
also gains the two claims the suite was already relying on: the
fallback, which now fires only for a CIGAR consuming no query, and the
half-placed record an undeclared contig produces.

corpus_factory now rejects header contigs that do not cover the
reference at its declared length, which previously produced records the
header could not resolve inside a corpus a later suite would assert
against, and skips rather than failing with an AttributeError when a
caller asks for an alignment without pysam installed.

The session cache directory is removed at exit, which the subprocess
tests reach and a fixture teardown would not, and each test now reads
and writes a cache directory of its own.
Adds the tests the corrected behaviour needs and the ones the suite was
missing: a soft mask that starts before its contig, the shortest contig
the builder accepts, a read reaching a contig's last base, the contig
skew under the condition the docstring states, the header-coverage
refusal, the seed on the alignment path, the licence declared once in
the built metadata, and the progress-bar setting the conftest docstring
contrasts with the cache directory.

The unplaced-record test now passes a CIGAR of its own rather than
relying on the default, which the twenty-base fallback satisfied either
way, so it can tell a CIGAR that was applied from one that was
discarded.

Collapses the two-contig header into a fixture the sixteen tests that
built it inline now request, folds two uv build invocations into one
helper that reports the stderr a failed build would otherwise discard,
moves the pileup import guard inside the Arrange phase, and renames the
tests whose subject is an environment variable or a dunder rather than
a method, so each name matches what it exercises. The short-contig
refusal is among them: its name said two reads where its body, its
docstring, its match string and the builder's guard all say one, and
its sibling accepted a contig that is shorter than two reads.

Corrects the docstrings that went with them: several described
consumers this repository does not contain yet, and several restated a
claim their own builder already owns, in one case with the wording
already drifted.
The install block presented streaming as current behaviour for a
package whose whole surface is a version, and omitted that the pileup
extra resolves only with uv until its git-pinned dependency ships to
PyPI. The commands the acceptance criteria are written in terms of
lived only in the test workflow, which is not where a contributor looks
for them.
Four near-duplicates inside the layer whose purpose is de-duplication, all
introduced by this branch: bam_placements was record_placements with the until_eof
knob dropped, PROJECT_ROOT was written out twice, a module-level strategy named
chrom_sizes was defined twice over different domains, and the run-a-child-process
block three times. tests/__init__.py already declared itself the home for what the
modules share, so that is where they go; the two strategies are renamed for what
each generates.

FILE_BACKED carries the settings every file-writing property test needs. It also
drops the Hypothesis deadline, which those tests were already failing
intermittently: a cold first call spends about a second in pysam's sort and index
against a 200ms default, and Hypothesis reports that timing as a falsifying
example. Four runs at the parent commit failed two; six runs here failed none. The
two properties that stay in memory keep the deadline.
write_random_bam documents that a read may start anywhere a record of read_length
still ends inside its contig, and both existing properties discarded the position:
one asserts the contig set, the other a Counter over contigs. Replacing the span
with chrom_sizes[chrom] was caught only by two seed-pinned single-contig examples,
so nothing covered an out-of-bounds placement on the multi-contig geometries every
consumer of this layer will use. The new property catches that mutation.
write_fasta applies n_runs after soft_mask, so a position covered by
both reads as an upper-case N rather than a lower-case base. The suite
already depended on it, subtracting the N runs from the soft mask for
exactly that reason, so the precedence was pinned in a test while
absent from the contract it belongs to.

The conftest module docstring rested the cold-build guarantee on the
session directory, which no test body ever sees: the per-test fixture
is autouse and supersedes it before every test. The session value is
what import time and the two re-importing subprocesses observe, and
that is now what the module docstring claims, with the per-test
guarantee moved to the fixture that delivers it.
@conradbzura
conradbzura force-pushed the quadrat-2-package-skeleton branch from 142c822 to f00a79e Compare October 28, 2026 18:25
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.

Set up the package skeleton, packaging and shared test fixtures

1 participant