Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ jobs:
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- name: Drop JET deps on Julia versions JET does not support

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Yikes, this will also bite when running tests locally, right?
Maybe we should just have a workflow step separate from running tests that just runs JET?

@PatrickHaecker PatrickHaecker May 18, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Well, it is unfortunately a trade-off currently, but running the tests locally is exactly the reason why I went this route.

With the approach proposed in the current PR, you can simply run your tests locally on Julia 1.10 and 1.12 and you'll get the JET results. Note that Julia 1.10 uses an older JET version, but this is the most recent JET version which supports Julia 1.10.

You can also test on Julia 1.13 if you add the branch described in this comment to your default environment and start the test from there.
I don't know whether Julia 1.6 and JET can cooperate, as my default environment is not fully functional due to using custom registries and I didn't want to spend additional time to work around this. But I think it's unlikely that you'd learn something about useful code changes there. And CI is not worse than before the JET PR.
Julia nightly, is currently just not supported in JET.

With the separate workflow you'd get no local JET testing at all. You could surely always explicitly test it with JET, but we all know how easy it is to forget about this. With the approach from this PR all the most important Julia versions (1.10, 1.12, 1.13) can be tested locally.

It's unfortunate that the situation around JET is currently so tricky. You can follow the above link and the linked discussion therein to see that currently there does not seem to be a better solution. I don't know of a better tradeoff.

# JET requires 1.7 <= Julia <= 1.12, so Pkg cannot resolve
# JET on Julia 1.6 or on nightly. Strip JET from Project.toml
# (compat + extras + targets) and from runtests.jl on those
# rows so the rest of the test suite still runs green.
# The job using a registered JET-supported Julia version ('1')
# keeps full JET coverage.
if: matrix.version == 'nightly' || matrix.version == '1.6'
run: |
sed -i '/^JET = /d' Project.toml
sed -i 's/, "JET"//' Project.toml
sed -i '/^@testset "JET" begin$/,/^end$/d' test/runtests.jl
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
Expand Down
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
[compat]
Aqua = "0.8, 1"
ConstructionBase = "1.3"
JET = "0.9, 0.10, 0.11"
StaticArrays = "1"
StructTypes = "1"
# `Test` is a stdlib. On Julia < 1.11 it is reported as unversioned
# (matched by `<0.0.1`); on Julia >= 1.11 it has a real version (matched
Expand All @@ -19,8 +21,10 @@ julia = "1.6"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Aqua", "Test", "StructTypes"]
test = ["Aqua", "JET", "StaticArrays", "StructTypes", "Test"]
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ For all supported options and defaults, consult the docstring:
julia>?@batteries
```

## `kwshow` vs `showrepr`

Two options overload `Base.show`; pick at most one (passing both is an
error):

* `kwshow=true` always renders `T(f1 = v1, f2 = v2, …)` — every field,
named, in declaration order. The output shape is fixed and stable
across versions, which makes it well-suited to diagnostics and
golden-file tests. It round-trips through `eval` only when the type
has a keyword constructor (e.g. via `kwconstructor=true` or
`Base.@kwdef`).

* `showrepr=true` prints a heuristically short constructor call that
recreates the object. It probes every constructor of the type
(positional, keyword, hybrid), omits trailing fields that already
match a default, and substitutes shorter literals where the
constructor still accepts them (e.g. `0x2a` → `42`, `2//1` → `2`,
uniform vectors → `fill(v, n)`). Because the result depends on the
field values and on the package's heuristics, the exact output is
*not* guaranteed to be stable across minor releases — don't pin
golden files against it. If no constructor recreates the object,
`showrepr` falls back to a non-executable `T(field = value, …)`
rendering.

Rule of thumb: pick `kwshow` if you want a predictable, name-every-field
diagnostic; pick `showrepr` if you want `Base.show` to produce an
idiomatic, recreatable, as-short-as-reasonable form for end users.

# Alternatives

* [AutoHashEquals](https://github.com/andrewcooke/AutoHashEquals.jl) requires annotating the struct definition. This can be inconvenient if you want to annotate the definition with another macro as well.
Expand Down
25 changes: 22 additions & 3 deletions src/StructHelpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export @enumbattery

import ConstructionBase: getproperties, constructorof, setproperties

include("showrepr.jl")

# `$SH.foo` in macro emissions interpolates the StructHelpers module value
# directly into the generated AST. This resolves via `getproperty` at the
# call site without requiring `import StructHelpers` (or any caller-side
Expand Down Expand Up @@ -112,6 +114,16 @@ end
nt = Tuple(getproperties(o))
Base.hash(nt, h)
end
"""
kwshow(io::IO, o)

Show `o` as `T(f1 = v1, f2 = v2, ...)`, listing every field as a keyword
argument. The output is recreatable via the keyword constructor (see
[`@batteries`](@ref) with `kwconstructor=true`).

For a representation that picks the shortest constructor call and omits
default values, see [`showrepr`](@ref).
"""
function kwshow(io::IO, o)
print(io, typeof(o))
print(io, "(")
Expand Down Expand Up @@ -173,6 +185,7 @@ const BATTERIES_DEFAULTS = (
kwconstructor = false,
selfconstructor = true,
kwshow = false,
showrepr = false,
getproperties = true ,
constructorof = true ,
typesalt = nothing,
Expand All @@ -186,6 +199,7 @@ const BATTERIES_DOCSTRINGS = (
kwconstructor = "Add a keyword constructor. Defaults can be supplied by overloading [`default_keywords`](@ref).",
selfconstructor = "Add a constructor of the for `T(self::T) = self`",
kwshow = "Overload `Base.show` such that the names of each field are printed. Fields whose value is `isequal` to the value returned by [`default_keywords`](@ref) are omitted.",
showrepr = "Overload `Base.show` to print a heuristically short constructor call that recreates the object, omitting default values. Mutually exclusive with `kwshow`.",
getproperties = "Overload `ConstructionBase.getproperties`.",
constructorof = "Overload `ConstructionBase.constructorof`.",
typesalt = "Only used if `hash=true`. In this case the `hash` will be purely computed from `typesalt` and `hash_eq_as(obj)`. The type `T` will not be used otherwise. This makes the hash more likely to stay constant, when executing on a different machine or julia version",
Expand Down Expand Up @@ -350,9 +364,7 @@ function def_batteries(__module__, T, kw, base)
ret = quote end

need_fieldnames = nt.kwconstructor || nt.getproperties
if need_fieldnames
fieldnames = Base.fieldnames(Base.eval(__module__, T))
end
fieldnames = need_fieldnames ? Base.fieldnames(Base.eval(__module__, T)) : ()
need_StructTypes = nt.StructTypes
if need_StructTypes
push!(ret.args, :(import StructTypes as $ST))
Expand Down Expand Up @@ -380,10 +392,17 @@ function def_batteries(__module__, T, kw, base)
)
push!(ret.args, def)
end
if nt.kwshow && nt.showrepr
error("`kwshow` and `showrepr` are mutually exclusive; please pick at most one.")
end
if nt.kwshow
def = :($(Base).show(io::$IO, o::$T) = $(kwshow)(io, o))
push!(ret.args, def)
end
if nt.showrepr
def = :(Base.show(io::IO, o::$T) = $(showrepr)(io, o))
push!(ret.args, def)
end
if nt.getproperties
def = def_getproperties(T, fieldnames)
push!(ret.args, def)
Expand Down
Loading
Loading