From e3216b0132ea25e5ff11d40eafaa716d7fa82956 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Andersen Date: Fri, 20 Feb 2026 14:12:25 +0100 Subject: [PATCH 1/2] Bump MemoryViews to 0.4.0 --- Project.toml | 4 ++-- docs/Project.toml | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 99f14a5..d64435c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "PairwiseMappingFormat" uuid = "30ee2946-b067-41c0-bed2-0900b78dbe59" -version = "0.1.1" +version = "0.1.2" authors = ["Jakob Nybo Nissen "] [deps] @@ -12,7 +12,7 @@ XAMAuxData = "e99d641e-1821-45d7-9150-ecb7bf333fe1" [compat] Aqua = "0.8.7" FormatSpecimens = "1.3" -MemoryViews = "0.2, 0.3" +MemoryViews = "0.2, 0.3, 0.4" PrecompileTools = "1.2.1" StringViews = "1.3.3" Test = "1.11" diff --git a/docs/Project.toml b/docs/Project.toml index 388614e..799c782 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -4,3 +4,6 @@ MemoryViews = "a791c907-b98b-4e44-8f4d-e4c2362c6b2f" PairwiseMappingFormat = "30ee2946-b067-41c0-bed2-0900b78dbe59" StringViews = "354b36f9-a18e-4713-926e-db85100087ba" XAMAuxData = "e99d641e-1821-45d7-9150-ecb7bf333fe1" + +[sources] +PairwiseMappingFormat = {path = ".."} From b50264aa6d8c90221d5418ab22bc05e58bdd9ffd Mon Sep 17 00:00:00 2001 From: Jakob Nybo Andersen Date: Fri, 20 Feb 2026 14:17:48 +0100 Subject: [PATCH 2/2] Switch formatter to Runic --- .JuliaFormatter.toml | 12 --- .github/workflows/formatting.yml | 18 +++++ docs/make.jl | 22 +++--- src/PairwiseMappingFormat.jl | 132 +++++++++++++++---------------- test/runtests.jl | 79 +++++++++--------- 5 files changed, 135 insertions(+), 128 deletions(-) delete mode 100644 .JuliaFormatter.toml create mode 100644 .github/workflows/formatting.yml diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml deleted file mode 100644 index a86de4d..0000000 --- a/.JuliaFormatter.toml +++ /dev/null @@ -1,12 +0,0 @@ -always_for_in = true -whitespace_typedefs = true -whitespace_ops_in_indices = true -remove_extra_newlines = true -import_to_using = true -normalize_line_endings = "unix" -separate_kwargs_with_semicolon = true -whitespace_in_kwargs = false -short_to_long_function_def = true -long_to_short_function_def = false -conditional_to_if = true -trailing_comma = true diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 0000000..286f68c --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,18 @@ +name: Runic formatting +on: + push: + branches: + - 'master' + - 'release-' + tags: + - '*' + pull_request: +jobs: + runic: + name: Runic + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: fredrikekre/runic-action@v1 + with: + version: '1' diff --git a/docs/make.jl b/docs/make.jl index 292a3c9..ef6f1de 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -8,20 +8,20 @@ meta = quote using PairwiseMappingFormat: try_parse end -DocMeta.setdocmeta!(PairwiseMappingFormat, :DocTestSetup, meta; recursive=true) +DocMeta.setdocmeta!(PairwiseMappingFormat, :DocTestSetup, meta; recursive = true) makedocs(; - sitename="PairwiseMappingFormat.jl", - modules=[PairwiseMappingFormat], - pages=["Home" => "index.md"], - authors="Jakob Nybo Nissen", - checkdocs=:public, - remotes=nothing, + sitename = "PairwiseMappingFormat.jl", + modules = [PairwiseMappingFormat], + pages = ["Home" => "index.md"], + authors = "Jakob Nybo Nissen", + checkdocs = :public, + remotes = nothing, ) deploydocs(; - repo="github.com/BioJulia/PairwiseMappingFormat.jl.git", - push_preview=true, - deps=nothing, - make=nothing, + repo = "github.com/BioJulia/PairwiseMappingFormat.jl.git", + push_preview = true, + deps = nothing, + make = nothing, ) diff --git a/src/PairwiseMappingFormat.jl b/src/PairwiseMappingFormat.jl index 5bf1182..b2980d5 100644 --- a/src/PairwiseMappingFormat.jl +++ b/src/PairwiseMappingFormat.jl @@ -91,7 +91,7 @@ end # Since the interface of the records are defined in terms of its # properties, we need to be able to abstract over them function Base.getproperty(record::PAFRecord, sym::Symbol) - if sym === :qname + return if sym === :qname qname(record) elseif sym == :tname tname(record) @@ -128,12 +128,12 @@ is_mapped(x::PAFRecord) = !iszero(getfield(x, :strand)) function is_rc(record::PAFRecord)::Union{Bool, Nothing} x = getfield(record, :strand) - iszero(x) ? nothing : x == 0x02 + return iszero(x) ? nothing : x == 0x02 end # For tab completion function Base.propertynames(::PAFRecord) - ( + return ( :qname, :tname, :mapq, @@ -149,9 +149,9 @@ function Base.propertynames(::PAFRecord) ) end -function PAFRecord(size::Int=0) +function PAFRecord(size::Int = 0) data = Vector{UInt8}(undef, max(size, 0)) - PAFRecord(data, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0xff, 0x00) + return PAFRecord(data, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0xff, 0x00) end function Base.show(io::IO, ::MIME"text/plain", record::PAFRecord) @@ -161,16 +161,16 @@ function Base.show(io::IO, ::MIME"text/plain", record::PAFRecord) println(buf, " Query: \"", qname(record), '"') if is_mapped(record) println(buf, " Target: \"", tname(record), '"') - println(buf, " Q cov: ", round(query_coverage(record); digits=4)) - println(buf, " T cov: ", round(target_coverage(record); digits=4)) - println(buf, " Identity: ", round(aln_identity(record); digits=4)) + println(buf, " Q cov: ", round(query_coverage(record); digits = 4)) + println(buf, " T cov: ", round(target_coverage(record); digits = 4)) + println(buf, " Identity: ", round(aln_identity(record); digits = 4)) qual = mapq(record) qual = qual === nothing ? "255 (missing)" : string(qual) println(buf, " Quality: ", qual) end print(buf, " Aux data: ") write(buf, repr_aux(record)) - print(io, String(take!(buf))) + return print(io, String(take!(buf))) end # Print the AUX fields indented @@ -179,22 +179,22 @@ function repr_aux(record::PAFRecord)::Vector{UInt8} out = IOBuffer() show(buf, MIME"text/plain"(), aux_data(record)) seekstart(buf) - for line in eachline(buf; keep=true) + for line in eachline(buf; keep = true) write(out, " ", line) end - take!(out)[3:end] + return take!(out)[3:end] end # Return StringView to not allocate function qname(record::PAFRecord) - StringView( + return StringView( ImmutableMemoryView(getfield(record, :data))[1:(getfield(record, :qname_len))], ) end # Return StringView to not allocate function tname(record::PAFRecord)::Union{StringView, Nothing} - if is_mapped(record) + return if is_mapped(record) ql = getfield(record, :qname_len) span = (ql + 1):(ql + getfield(record, :tname_len)) StringView(ImmutableMemoryView(getfield(record, :data))[span]) @@ -206,7 +206,7 @@ end # Note: We return nothing instead of missing because fuck missing function mapq(record::PAFRecord)::Union{Int, Nothing} x = getfield(record, :mapq) - x == 0xff ? nothing : Int(x) + return x == 0xff ? nothing : Int(x) end """ @@ -225,7 +225,7 @@ julia> aln_identity(record) See also: [`query_coverage`](@ref), [`target_coverage`](@ref) """ function aln_identity(record::PAFRecord)::Float64 - record.matches / record.alnlen + return record.matches / record.alnlen end """ @@ -242,7 +242,7 @@ julia> query_coverage(record) See also: [`target_coverage`](@ref), [`aln_identity`](@ref) """ function query_coverage(record::PAFRecord)::Float64 - (record.qend - record.qstart + Int32(1)) / record.qlen + return (record.qend - record.qstart + Int32(1)) / record.qlen end """ @@ -259,7 +259,7 @@ julia> target_coverage(record) See also: [`query_coverage`](@ref), [`aln_identity`](@ref) """ function target_coverage(record::PAFRecord)::Float64 - (record.tend - record.tstart + Int32(1)) / record.tlen + return (record.tend - record.tstart + Int32(1)) / record.tlen end """ @@ -287,7 +287,7 @@ true ``` """ function aux_data(record::PAFRecord) - Auxiliary( + return Auxiliary( getfield(record, :data), getfield(record, :qname_len) + getfield(record, :tname_len) + 1, ) @@ -308,19 +308,19 @@ InvalidZero See also: [`PairwiseMappingFormat.ParserException`](@ref) """ module Errors -public Err -# See descriptions in the `showerror` method below. -@enum Err::Int32 begin - TooFewFields - IntegerOverflow - BadInteger - InvalidStrand - EmptyNonTailingLine - BackwardsIndices - EmptyInteger - InvalidZero - PositionOutOfBounds -end + public Err + # See descriptions in the `showerror` method below. + @enum Err::Int32 begin + TooFewFields + IntegerOverflow + BadInteger + InvalidStrand + EmptyNonTailingLine + BackwardsIndices + EmptyInteger + InvalidZero + PositionOutOfBounds + end end # module Errors using .Errors: Err @@ -356,7 +356,7 @@ ParserException(index::Integer, kind::Errors.Err) = ParserException(kind, index, Base.propertynames(x::ParserException) = (:kind, :line) function Base.getproperty(x::ParserException, s::Symbol) - if s ∈ (:kind, :line) + return if s ∈ (:kind, :line) getfield(x, s) else throw(ArgumentError(lazy"No such property in ParserException: $s")) @@ -391,13 +391,13 @@ function Base.showerror(io::IO, err::ParserException) "Query or target start or end position is longer than query/target length" end print(buf, s) - print(io, String(take!(buf))) + return print(io, String(take!(buf))) end # Use this macro to return early and reduce code duplication. # It's similar to `a = @something b return nothing` macro var"?"(expr) - quote + return quote local res = $(esc(expr)) res isa ParserException ? (return res) : res end @@ -429,13 +429,13 @@ try_parse(x::ImmutableMemoryView{UInt8}) = parse_line!(PAFRecord(length(x) - 19) function Base.parse(::Type{PAFRecord}, s::Union{String, SubString{String}}) y = try_parse(s) - y isa PAFRecord ? y : throw(y) + return y isa PAFRecord ? y : throw(y) end function parse_line!( - record::PAFRecord, - mem::ImmutableMemoryView{UInt8}, -)::Union{PAFRecord, ParserException} + record::PAFRecord, + mem::ImmutableMemoryView{UInt8}, + )::Union{PAFRecord, ParserException} if lastindex(mem) > typemax(Int32) error("PairwiseMappingFormat.jl can't handle lines longer than 2147483647 bytes") end @@ -463,7 +463,7 @@ function parse_line!( return ParserException(i % Int32, Errors.InvalidStrand) end @inbounds mem[i + 1] == UInt8('\t') || - return ParserException((i + 1) % Int32, Errors.TooFewFields) + return ParserException((i + 1) % Int32, Errors.TooFewFields) # Load rest of the fields (tname, i) = @? parse_str(mem, i + 2) # i + 2 because strand plus tab took two bytes @@ -512,19 +512,19 @@ function parse_line!( record.mapq = mapq record.strand = strand - record + return record end # If the record is unmapped we don't even bother reading # the rest of the fields, so take this fast path # i is the byte index of the strand plus two function finish_unmapped!( - record::PAFRecord, - mem::ImmutableMemoryView{UInt8}, - qname::UnitRange{Int}, - qlen::Int32, - i::Int, -) + record::PAFRecord, + mem::ImmutableMemoryView{UInt8}, + qname::UnitRange{Int}, + qlen::Int32, + i::Int, + ) # Determine the position of aux data for _ in 1:6 i = findnext(==(UInt8('\t')), mem, i) @@ -561,16 +561,16 @@ function finish_unmapped!( record.mapq = 0xff record.strand = 0x00 - record + return record end # Just get the index of the next \t function parse_str( - v::ImmutableMemoryView{UInt8}, - from::Int, -)::Union{Tuple{UnitRange{Int}, Int}, ParserException} + v::ImmutableMemoryView{UInt8}, + from::Int, + )::Union{Tuple{UnitRange{Int}, Int}, ParserException} i = findnext(==(UInt8('\t')), v, from) - if isnothing(i) + return if isnothing(i) ParserException(lastindex(v) % Int32, Errors.TooFewFields) else (from:(i - 1), i + 1) @@ -579,11 +579,11 @@ end # TODO: 40% of runtime is spent in this function. Microoptimize it function parse_int( - v::ImmutableMemoryView{UInt8}, - from::Int, - allow_zero::Bool, - at_end::Bool=false, -)::Union{Tuple{Int32, Int}, ParserException} + v::ImmutableMemoryView{UInt8}, + from::Int, + allow_zero::Bool, + at_end::Bool = false, + )::Union{Tuple{Int32, Int}, ParserException} n = Int32(0) i = from for outer i in from:lastindex(v) @@ -604,7 +604,7 @@ function parse_int( # if they do not end with a \t and thus reach this line, a TooFewFields # error will be returned. # at_end is if this is the mapq field, which does not need to end with a tab - if !at_end + return if !at_end ParserException(lastindex(v) % Int32, Errors.TooFewFields) elseif from > lastindex(v) ParserException(i % Int32, Errors.EmptyInteger) @@ -667,10 +667,10 @@ mutable struct PAFReader{I <: IO} copy::Bool end -function PAFReader(io::IO; buf_size::Int=2^16, copy::Bool=true) +function PAFReader(io::IO; buf_size::Int = 2^16, copy::Bool = true) mem = Memory{UInt8}(undef, max(buf_size, 16)) rec = PAFRecord(512) - PAFReader{typeof(io)}(io, rec, mem, 1, 0, 1, copy) + return PAFReader{typeof(io)}(io, rec, mem, 1, 0, 1, copy) end Base.IteratorSize(::Type{<:PAFReader}) = Base.SizeUnknown() @@ -679,7 +679,7 @@ Base.close(reader::PAFReader) = close(reader.io) # TODO: Docstring function PAFReader(f::Function, io::IO; kwargs...) reader = PAFReader(io; kwargs...) - try + return try f(reader) finally close(reader) @@ -687,7 +687,7 @@ function PAFReader(f::Function, io::IO; kwargs...) end function Base.copy(record::PAFRecord) - PAFRecord( + return PAFRecord( copy(getfield(record, :data)), getfield(record, :qname_len), getfield(record, :tname_len), @@ -704,9 +704,9 @@ function Base.copy(record::PAFRecord) ) end -function Base.iterate(reader::PAFReader, (::Nothing)=nothing) +function Base.iterate(reader::PAFReader, (::Nothing) = nothing) res = try_next!(reader) - if res isa ParserException + return if res isa ParserException throw(res) elseif res === nothing nothing @@ -727,7 +727,7 @@ function reallocate!(reader::PAFReader) new = Memory{UInt8}(undef, newlen) unsafe_copyto!(new, 1, mem, 1, reader.filled) reader.mem = new - reader + return reader end # Shift the data inside the buffer such that the usable @@ -740,7 +740,7 @@ function shift_buffer!(reader::PAFReader) copyto!(mem, 1, mem, reader.index, len) reader.index = 1 reader.filled = len - reader + return reader end # Read data into the buffer. Returns zero if and only if underlying @@ -752,7 +752,7 @@ function read_buffer!(reader::PAFReader)::Int if iszero(remaining) shift_buffer!(reader) remaining = length(mem) - reader.filled - # If still no space, the buffer is truly full, and we need to + # If still no space, the buffer is truly full, and we need to # resize it if iszero(remaining) reallocate!(reader) diff --git a/test/runtests.jl b/test/runtests.jl index 7320979..6d1b5bb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,8 +10,8 @@ using Aqua using XAMAuxData.SAM: AuxTag function cmp_aux(record::PAFRecord, other) - aux = sort!(collect(aux_data(record)); by=first) - other = sort!(collect(other); by=first) + aux = sort!(collect(aux_data(record)); by = first) + other = sort!(collect(other); by = first) @test length(aux) == length(other) for ((kaux, vaux), (ko, vo)) in zip(aux, other) @test kaux == AuxTag(ko) @@ -25,35 +25,36 @@ function cmp_aux(record::PAFRecord, other) @test false end end + return end @testset "Normal records" begin - (rec1, rec2, rec3) = PAFReader(collect, open(joinpath(path_of_format("PAF"), "good1.paf"))) - - @testset "Field access" begin - @test rec1.qname == "NODE_1_length_301156_cov_79.148382" - @test rec1.qlen == 301156 - @test rec1.qstart == 4 # one-based indexing - @test rec1.is_rc == false - @test rec1.tname == "CP004047.1" - @test rec1.tlen == 6701780 - @test rec1.tstart == 2764861 # one-based indexing - @test rec1.tend == 3066002 - @test rec1.matches == 301142 - @test rec1.alnlen == 301142 - @test rec1.mapq == 0 - - @test_throws Exception rec1.foobar - @test_throws Exception rec1.strand - @test_throws Exception rec1.data - @test_throws Exception rec1.qname_len - @test_throws Exception rec1.tname_len - end - - @testset "Aux data" begin - cmp_aux(rec1, ["tp" => 'P', "cm" => 29990, "s1" => 301142, "s2" => 301142, "dv" => 0, "rl" => 0]) - cmp_aux(rec2, ["tp" => 'P', "cm" => 29957, "s1" => 299269, "s2" => 299269, "dv" => 0, "rl" => 38]) - cmp_aux(rec3, ["tp" => 'P', "cm" => 28871, "s1" => 288618, "s2" => 288618, "dv" => 0, "rl" => 57]) + (rec1, rec2, rec3) = PAFReader(collect, open(joinpath(path_of_format("PAF"), "good1.paf"))) + + @testset "Field access" begin + @test rec1.qname == "NODE_1_length_301156_cov_79.148382" + @test rec1.qlen == 301156 + @test rec1.qstart == 4 # one-based indexing + @test rec1.is_rc == false + @test rec1.tname == "CP004047.1" + @test rec1.tlen == 6701780 + @test rec1.tstart == 2764861 # one-based indexing + @test rec1.tend == 3066002 + @test rec1.matches == 301142 + @test rec1.alnlen == 301142 + @test rec1.mapq == 0 + + @test_throws Exception rec1.foobar + @test_throws Exception rec1.strand + @test_throws Exception rec1.data + @test_throws Exception rec1.qname_len + @test_throws Exception rec1.tname_len + end + + @testset "Aux data" begin + cmp_aux(rec1, ["tp" => 'P', "cm" => 29990, "s1" => 301142, "s2" => 301142, "dv" => 0, "rl" => 0]) + cmp_aux(rec2, ["tp" => 'P', "cm" => 29957, "s1" => 299269, "s2" => 299269, "dv" => 0, "rl" => 38]) + cmp_aux(rec3, ["tp" => 'P', "cm" => 28871, "s1" => 288618, "s2" => 288618, "dv" => 0, "rl" => 57]) end end @@ -92,7 +93,7 @@ end @test rec1.qlen == typemax(Int32) cmp_aux(rec1, ["tp" => 'S', "cm" => 29990, "s1" => 301142, "dv" => -13.2442f-18, "rl" => 0, "xX" => [0xea, 0x19, 0xfe, 0x1b], "K1" => Int8[15, 9, 22, 127, -12, -128, 0]]) cmp_aux(rec2, ["dv" => 1.243f-4, "rl" => 38, "kk" => "!=[]! And then~~ "]) - cmp_aux(rec3, ["dv" => +17f21, "rl" => 38]) + cmp_aux(rec3, ["dv" => +17.0f21, "rl" => 38]) # Empty record rec = PAFReader(first, open(joinpath(path_of_format("PAF"), "good3.paf"))) @@ -117,13 +118,13 @@ function with_replaced(s::String, field::Integer, new::String) fields = split(s, '\t') fields[field] = new y = try_parse(join(fields, "\t")) - y isa PairwiseMappingFormat.ParserException ? y.kind : y + return y isa PairwiseMappingFormat.ParserException ? y.kind : y end @testset "Errors" begin good = "my_qname\t301156\t3\t301145\t+\tmy_tname\t6701780\t2764860\t3066002\t301142\t301142\t0" @test with_replaced(good, 1, "my other name") isa PAFRecord - + # Negative numbers, zero numbers @test with_replaced(good, 2, "-1") == Errors.BadInteger @@ -150,7 +151,7 @@ end # Char not valid @test with_replaced(good, 5, "/") == Errors.InvalidStrand - + # Too few columns for i in 0:11 s = join(split(good, '\t')[1:i], '\t') @@ -162,7 +163,7 @@ end for int_field in [2, 3, 4, 7, 8, 9, 10, 11, 12] @test with_replaced(good, int_field, "") == Errors.EmptyInteger end - + # Mapq too high @test with_replaced(good, 12, "256") == Errors.IntegerOverflow end @@ -172,7 +173,7 @@ end good = "my_qname\t301156\t3\t301145\t+\tmy_tname\t6701780\t2764860\t3066002\t301142\t301142\t0" @test only(PAFReader(collect, IOBuffer(good))) isa PAFRecord - bad = good[1:end-2] + bad = good[1:(end - 2)] @test_throws PairwiseMappingFormat.ParserException only(PAFReader(collect, IOBuffer(bad))) end @@ -180,8 +181,8 @@ end # Large data where buffer needs to be shifted good = "my_qname\t301156\t3\t301145\t+\tmy_tname\t6701780\t2764860\t3066002\t301142\t301142\t0" large = join([good for _ in 1:1000], '\n') - PAFReader(IOBuffer(large); buf_size=16) do reader - (allgood, n) = foldl(reader; init=(true, 0)) do (isgood, n), record + PAFReader(IOBuffer(large); buf_size = 16) do reader + (allgood, n) = foldl(reader; init = (true, 0)) do (isgood, n), record isgood &= record.qlen == 301156 isgood &= record.mapq == 0 isgood &= record.tstart == 2764861 @@ -196,7 +197,7 @@ end # that the aux data has the right span and byte content. function test_aux_content(record_data::String, target_aux::String) rec = parse(PAFRecord, record_data) - @test collect(MemoryView(aux_data(rec))) == codeunits(target_aux) + return @test collect(MemoryView(aux_data(rec))) == codeunits(target_aux) end @testset "Auxiliary data" begin @@ -204,7 +205,7 @@ end goodaux = "tp:A:P kL:Z:325 1 s1:i:301142 x3:H:a40140 dv:f:0.0000 rl:i:0" test_aux_content(good, goodaux) - stringaux_with_whitespace = "\tHA:Z:lsjd " + stringaux_with_whitespace = "\tHA:Z:lsjd " test_aux_content(good * stringaux_with_whitespace, goodaux * stringaux_with_whitespace) end @@ -233,7 +234,7 @@ end end @testset "Aqua" begin - Aqua.test_all(PairwiseMappingFormat; ambiguities=false) + Aqua.test_all(PairwiseMappingFormat; ambiguities = false) end end # module