diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f99801a..d2be379 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,7 +10,7 @@ jobs: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: version: - '1.0' @@ -33,8 +33,3 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 - with: - file: lcov.info - fail_ci_if_error: true diff --git a/Project.toml b/Project.toml index 65cdbad..2b835d7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ConstructionBase" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" authors = ["Takafumi Arakaki", "Rafael Schouten", "Jan Weidner"] -version = "1.3.1" +version = "1.4.0" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/README.md b/README.md index 3ad6f61..f624da6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaObjects.github.io/ConstructionBase.jl/stable) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaObjects.github.io/ConstructionBase.jl/dev) [![Build Status](https://github.com/JuliaObjects/ConstructionBase.jl/workflows/CI/badge.svg)](https://github.com/JuliaObjects/ConstructionBase.jl/actions?query=workflow%3ACI) -[![Codecov](https://codecov.io/gh/JuliaObjects/ConstructionBase.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaObjects/ConstructionBase.jl) [![GitHub stars](https://img.shields.io/github/stars/JuliaObjects/ConstructionBase.jl?style=social)](https://github.com/JuliaObjects/ConstructionBase.jl) ConstructionBase is a very lightwight package, that provides primitive functions for construction of objects: diff --git a/docs/Manifest.toml b/docs/Manifest.toml index fe71747..ae8bbc4 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,89 +1,149 @@ # This file is machine-generated - editing it directly is not advised -[[Base64]] +julia_version = "1.7.0" +manifest_format = "2.0" + +[[deps.ANSIColoredPrinters]] +git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" +uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" +version = "0.0.1" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + +[[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -[[Dates]] +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" + +[[deps.ConstructionBase]] +deps = ["LinearAlgebra"] +path = ".." +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.3.0" + +[[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -[[Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[DocStringExtensions]] -deps = ["LibGit2", "Markdown", "Pkg", "Test"] -git-tree-sha1 = "0513f1a8991e9d83255e0140aace0d0fc4486600" +[[deps.DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.8.0" +version = "0.8.6" -[[Documenter]] -deps = ["Base64", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] -git-tree-sha1 = "1b6ae3796f60311e39cd1770566140d2c056e87f" +[[deps.Documenter]] +deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] +git-tree-sha1 = "7d9a46421aef53cbd6b8ecc40c3dcbacbceaf40e" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.23.3" +version = "0.27.15" + +[[deps.Future]] +deps = ["Random"] +uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" -[[InteractiveUtils]] +[[deps.IOCapture]] +deps = ["Logging", "Random"] +git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a" +uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" +version = "0.2.2" + +[[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -[[JSON]] +[[deps.JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.0" +version = "0.21.3" -[[LibGit2]] +[[deps.LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -[[Logging]] +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -[[Markdown]] +[[deps.MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.9" + +[[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -[[Mmap]] +[[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" -[[Parsers]] -deps = ["Dates", "Test"] -git-tree-sha1 = "ef0af6c8601db18c282d092ccbd2f01f3f0cd70b" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "0.3.7" +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -[[Pkg]] -deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +[[deps.Parsers]] +deps = ["Dates"] +git-tree-sha1 = "85b5da0fa43588c75bb1ff986493443f821c70b7" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.2.3" -[[Printf]] +[[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -[[REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets"] +[[deps.REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -[[Random]] -deps = ["Serialization"] +[[deps.Random]] +deps = ["SHA", "Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -[[SHA]] +[[deps.Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.3.0" + +[[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -[[Serialization]] +[[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -[[Sockets]] +[[deps.Setfield]] +deps = ["ConstructionBase", "Future", "MacroTools", "Requires"] +git-tree-sha1 = "38d88503f695eb0301479bc9b0d4320b378bafe5" +uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" +version = "0.8.2" + +[[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -[[Test]] -deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -[[UUIDs]] +[[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -[[Unicode]] +[[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" diff --git a/docs/Project.toml b/docs/Project.toml index 13764a2..33447c3 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,4 @@ [deps] +ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" diff --git a/docs/src/index.md b/docs/src/index.md index 0411784..8eb1022 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,13 +1,25 @@ # ConstructionBase.jl -```@index -``` +[`ConstructionBase`](@ref) allows flexible construction and destructuring of objects. +There are two levels of under which this can be done: +### [The raw level](@id the-raw-level) +This is where `Base.fieldnames`, `Base.getfield`, `Base.setfield!` live. +This level is what an object is ultimately composed of including all private details. +At the raw level [`ConstructionBase`](@ref) adds [`constructorof`](@ref) and [`getfields`](@ref). +### [The semantic level](@id the-semantic-level) +This is where `Base.propertynames`, `Base.getproperty` and `Base.setproperty!` live. This level is typically the public interface of a type, it may hide private details and do magic tricks. +At the semantic level [`ConstructionBase`](@ref) adds [`setproperties`](@ref) and [`getproperties`](@ref). + ## Interface +```@index +``` + ```@docs ConstructionBase ConstructionBase.constructorof +ConstructionBase.getfields ConstructionBase.getproperties ConstructionBase.setproperties ``` diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 15fde7d..86a7be4 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -3,11 +3,14 @@ module ConstructionBase export getproperties export setproperties export constructorof +export getfields + # Use markdown files as docstring: for (name, path) in [ :ConstructionBase => joinpath(dirname(@__DIR__), "README.md"), :constructorof => joinpath(@__DIR__, "constructorof.md"), + :getfields => joinpath(@__DIR__, "getfields.md"), :getproperties => joinpath(@__DIR__, "getproperties.md"), :setproperties => joinpath(@__DIR__, "setproperties.md"), ] @@ -38,33 +41,58 @@ struct NamedTupleConstructor{names} end NamedTuple{names}(args) end +################################################################################ +#### getfields +################################################################################ +getfields(x::Tuple) = x +getfields(x::NamedTuple) = x getproperties(o::NamedTuple) = o getproperties(o::Tuple) = o + +@generated function check_properties_are_fields(obj) + if is_propertynames_overloaded(obj) + return quote + T = typeof(obj) + msg = """ + The function `Base.propertynames` was overloaded for type `$T`. + Please make sure the following methods are also overloaded for this type: + ```julia + ConstructionBase.setproperties + ConstructionBase.getproperties # optional in VERSION >= julia v1.7 + ``` + """ + error(msg) + end + else + :(nothing) + end +end + +function is_propertynames_overloaded(T::Type)::Bool + which(propertynames, Tuple{T}).sig !== Tuple{typeof(propertynames), Any} +end + if VERSION >= v"1.7" function getproperties(obj) fnames = propertynames(obj) NamedTuple{fnames}(getproperty.(Ref(obj), fnames)) end + function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) + end else - @generated function getproperties(obj) - if which(propertynames, Tuple{obj}).sig != Tuple{typeof(propertynames), Any} - # custom propertynames defined for this type - return quote - msg = """ - Different fieldnames and propertynames are only supported on Julia v1.7+. - For older julia versions, consider overloading - `ConstructionBase.getproperties(obj::$(typeof(obj))`. - See also https://github.com/JuliaObjects/ConstructionBase.jl/pull/60. - """ - error(msg) - end - end + @generated function getfields(obj) fnames = fieldnames(obj) fvals = map(fnames) do fname - :(obj.$fname) + Expr(:call, :getfield, :obj, QuoteNode(fname)) end :(NamedTuple{$fnames}(($(fvals...),))) end + function getproperties(obj) + check_properties_are_fields(obj) + getfields(obj) + end end ################################################################################ @@ -92,20 +120,17 @@ setproperties_namedtuple(obj, patch::Tuple{}) = obj end function setproperties_namedtuple(obj, patch) res = merge(obj, patch) - validate_setproperties_result(res, obj, obj, patch) + check_patch_properties_exist(res, obj, obj, patch) res end -function validate_setproperties_result( +function check_patch_properties_exist( nt_new::NamedTuple{fields}, nt_old::NamedTuple{fields}, obj, patch) where {fields} nothing end -@noinline function validate_setproperties_result(nt_new, nt_old, obj, patch) +@noinline function check_patch_properties_exist(nt_new, nt_old, obj, patch) O = typeof(obj) msg = """ Failed to assign properties $(propertynames(patch)) to object with properties $(propertynames(obj)). - You may want to overload - ConstructionBase.setproperties(obj::$O, patch::NamedTuple) - ConstructionBase.getproperties(obj::$O) """ throw(ArgumentError(msg)) end @@ -157,11 +182,14 @@ setproperties_object(obj, patch::Tuple{}) = obj throw(ArgumentError(msg)) end setproperties_object(obj, patch::NamedTuple{()}) = obj + function setproperties_object(obj, patch) + check_properties_are_fields(obj) nt = getproperties(obj) nt_new = merge(nt, patch) - validate_setproperties_result(nt_new, nt, obj, patch) - constructorof(typeof(obj))(Tuple(nt_new)...) + check_patch_properties_exist(nt_new, nt, obj, patch) + args = Tuple(nt_new) # old julia inference prefers if we wrap in Tuple + constructorof(typeof(obj))(args...) end include("nonstandard.jl") diff --git a/src/constructorof.md b/src/constructorof.md index 4c88f81..42fef1e 100644 --- a/src/constructorof.md +++ b/src/constructorof.md @@ -32,17 +32,16 @@ julia> constructorof(S)(1,2,4) ERROR: AssertionError: a + b == checksum ``` Instead `constructor` can be any object that satisfies the following properties: -* It must be possible to reconstruct an object from the `NamedTuple` returned by -`getproperties`: +* It must be possible to reconstruct an object from the elements of [`getfields`](@ref): ```julia ctor = constructorof(typeof(obj)) -@assert obj == ctor(getproperties(obj)...) -@assert typeof(obj) == typeof(ctor(getproperties(obj)...)) +@assert obj == ctor(getfields(obj)...) +@assert typeof(obj) == typeof(ctor(getfields(obj)...)) ``` * The other direction should hold for as many values of `args` as possible: ```julia ctor = constructorof(T) -getproperties(ctor(args...)) == args +getfields(ctor(args...)) == args ``` For instance given a suitable parametric type it should be possible to change the type of its fields: @@ -61,7 +60,7 @@ T{Float64, Int64}(1.0, 2) julia> constructorof(typeof(t))(10, 2) T{Int64, Int64}(10, 2) ``` - +`constructorof` belongs to [the raw level](@ref the-raw-level). `constructorof` is generated for all anonymous `Function`s lacking constructors, identified as having `gensym` `#` in their names. A custom struct `<: Function` with a `gensym` name may need to define `constructorof` manually. diff --git a/src/getfields.md b/src/getfields.md new file mode 100644 index 0000000..d4f0d89 --- /dev/null +++ b/src/getfields.md @@ -0,0 +1,53 @@ + getfields(obj) -> NamedTuple + getfields(obj::Tuple) -> Tuple + +Return a `NamedTuple` containing the fields of `obj`. On `Tuples` `getfields` is +the identity function instead, since `Tuple` fields have no symbolic names. + +# Examples +```jldoctest +julia> using ConstructionBase + +julia> struct S{A,B} + a::A + b::B + end + +julia> getfields(S(1,2)) +(a = 1, b = 2) + +julia> getfields((a=10,b=20)) +(a = 10, b = 20) + +julia> getfields((4,5,6)) +(4, 5, 6) +``` + +# Specification + +`getfields` belongs to the [the raw level](@ref the-raw-level). +Semantically `getfields` boils down to `getfield` and `fieldnames`: +```julia +function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) +end +``` +However the actual implementation can be more optimized. For builtin types, there can also be deviations from this semantics: +* `getfields(::Tuple)::Tuple` since `Tuples` don't have symbolic fieldnames +* There are some types in `Base` that have `undef` fields. Since accessing these results in an error, `getfields` instead just omits these. + +# Implementation + +The semantics of `getfields` should not be changed for user defined types. It should +return the raw fields as a `NamedTuple` in the struct order. In other words it should be +equivalent to +```julia +function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) +end +``` +even if that includes private fields of `obj`. +If a change of semantics is desired, consider overloading [`getproperties`](@ref) instead. +See also [`getproperties`](@ref), [`constructorof`](@ref) diff --git a/src/getproperties.md b/src/getproperties.md index b70d009..61a6501 100644 --- a/src/getproperties.md +++ b/src/getproperties.md @@ -1,6 +1,8 @@ - getproperties(obj) + getproperties(obj)::NamedTuple + getproperties(obj::Tuple)::Tuple -Return the fields of `obj` as a `NamedTuple`. +Return the properties of `obj` as a `NamedTuple`. Since `Tuple` don't have symbolic properties, +`getproperties` is the identity function on tuples. # Examples ```jldoctest @@ -17,26 +19,31 @@ S(1, 2, 3) julia> getproperties(s) (a = 1, b = 2, c = 3) -``` - -# Implementation -`getproperties` is defined by default for all objects. However for a custom type `MyType`, -`getproperties(obj::MyType)` may be defined when objects may have undefined fields, -when it has calculated fields that should not be accessed or set manually, or -other conditions that do not meet the specification with the default implementation. +julia> getproperties((10,20)) +(10, 20) +``` ## Specification +`getproperties` belongs to [the semantic level](@ref the-semantic-level). `getproperties` guarantees a couple of invariants. When overloading it, the user is responsible for ensuring them: -1. Relation to `propertynames` and `fieldnames`: `getproperties` relates to `propertynames` and `getproperty`, not to `fieldnames` and `getfield`. - This means that any series `p₁, p₂, ..., pₙ` of `propertynames(obj)` that is not undefined should be returned by `getproperties`. -2. `getproperties` is defined in relation to `constructorof` so that: - ```julia - obj == constructorof(obj)(getproperties(obj)...) - ``` +1. `getproperties` should be consistent with `Base.propertynames`, `Base.getproperty`, `Base.setproperty!`. + Semantically it should be equivalent to: + ```julia + function getproperties(obj) + fnames = propertynames(obj) + NamedTuple{fnames}(getproperty.(Ref(obj), fnames)) + end + ``` 2. `getproperties` is defined in relation to `setproperties` so that: ```julia obj == setproperties(obj, getproperties(obj)) ``` + The only exception from this semantics is that undefined properties may be avoided + in the return value of `getproperties`. + +# Implementation + +`getproperties` is defined by default for all objects. It should be very rare that a custom type `MyType`, has to implement `getproperties(obj::MyType)`. Reasons to do so are undefined fields or performance considerations. diff --git a/src/nonstandard.jl b/src/nonstandard.jl index 8ca9104..640306e 100644 --- a/src/nonstandard.jl +++ b/src/nonstandard.jl @@ -36,8 +36,8 @@ function tridiagonal_constructor(dl::V, d::V, du::V, du2::V) where {V<:AbstractV Tridiagonal{T,V}(dl, d, du, du2) end -# `du2` may be undefined, so we need a custom `getproperties` that checks `isdefined` -function getproperties(o::Tridiagonal) +# `du2` may be undefined, so we need a custom `getfields` that checks `isdefined` +function getfields(o::Tridiagonal) if isdefined(o, :du2) (dl=o.dl, d=o.d, du=o.du, du2=o.du2) else diff --git a/src/setproperties.md b/src/setproperties.md index e40ac4d..7e44f13 100644 --- a/src/setproperties.md +++ b/src/setproperties.md @@ -1,6 +1,6 @@ setproperties(obj, patch::NamedTuple) -Return a copy of `obj` with attributes updates accoring to `patch`. +Return a copy of `obj` with properties updates according to `patch`. # Examples ```jldoctest @@ -44,23 +44,9 @@ julia> setproperties(o, a="A", c="cc") S("A", 2, "cc") ``` -# Implementation - -For a custom type `MyType`, a method `setproperties(obj::MyType, patch::NamedTuple)` -may be defined. - -* Prefer to overload [`constructorof`](@ref) whenever makes sense (e.g., no `getproperty` - method is defined). Default `setproperties` is defined in terms of `constructorof`. - -* If `getproperty` is customized, it may be a good idea to define `setproperties`. - -!!! warning - The signature `setproperties(obj::MyType; kw...)` should never be overloaded. - Instead `setproperties(obj::MyType, patch::NamedTuple)` should be overloaded. - ## Specification -`setproperties` guarantees a couple of invariants. When overloading it, the user is responsible for ensuring them: +`setproperties` belongs to [the semantic level](@ref the-semantic-level). If satisfies the following invariants: 1. Purity: `setproperties` is supposed to have no side effects. In particular `setproperties(obj, patch::NamedTuple)` may not mutate `obj`. 2. Relation to `propertynames` and `fieldnames`: `setproperties` relates to `propertynames` and `getproperty`, not to `fieldnames` and `getfield`. @@ -100,3 +86,18 @@ let obj′ = setproperties(obj, ($p₁=v₁, $p₂=v₂, ..., $pₙ=vₙ)), @assert obj′′.$pₙ == wₙ end ``` + +# Implementation + +For a custom type `MyType`, a method `setproperties(obj::MyType, patch::NamedTuple)` +may be defined. When doing so it is important to ensure compliance with the specification. + +* Prefer to overload [`constructorof`](@ref) whenever makes sense (e.g., no `getproperty` + method is defined). Default `setproperties` is defined in terms of `constructorof` and `getproperties`. + +* If `getproperty` is customized, it may be a good idea to define `setproperties`. + +!!! warning + The signature `setproperties(obj::MyType; kw...)` should never be overloaded. + Instead `setproperties(obj::MyType, patch::NamedTuple)` should be overloaded. + diff --git a/test/runtests.jl b/test/runtests.jl index 2bfa23f..0cfb291 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,6 +19,30 @@ end @test constructorof(Tuple{Nothing, Missing})(1.0, 2) === (1.0, 2) end +@testset "getfields" begin + @test getfields(()) === () + @test getfields([]) === NamedTuple() + @test getfields(Empty()) === NamedTuple() + @test getfields(NamedTuple()) === NamedTuple() + @test getfields((10,20,30)) === (10,20,30) + @test getfields((a=10,b=20f0,c=true)) === (a=10,b=20f0,c=true) + @test getfields(AB(1, 10)) === (a=1, b=10) + adder(a) = x -> x + a + @test getfields(adder(1)) === (a=1,) +end + +struct DontTouchProperties + a + b +end +Base.propertynames(::DontTouchProperties) = error() +Base.getproperty(::DontTouchProperties, ::Symbol) = error() +ConstructionBase.getproperties(::DontTouchProperties) = error() +@testset "getfields does not depend on properties" begin + @test getfields(DontTouchProperties(1,2)) === (a=1, b=2) + @test constructorof(DontTouchProperties) === DontTouchProperties +end + @testset "getproperties" begin o = AB(1, 2) @test getproperties(o) === (a=1, b=2) @@ -55,14 +79,10 @@ end res = @test_throws ArgumentError setproperties(AB(1,2), (a=2, this_field_does_not_exist=3.0)) msg = sprint(showerror, res.value) @test occursin("this_field_does_not_exist", msg) - @test occursin("overload", msg) - @test occursin("ConstructionBase.setproperties", msg) res = @test_throws ArgumentError setproperties(AB(1,2), a=2, this_field_does_not_exist=3.0) msg = sprint(showerror, res.value) @test occursin("this_field_does_not_exist", msg) - @test occursin("overload", msg) - @test occursin("ConstructionBase.setproperties", msg) @test setproperties(42, NamedTuple()) === 42 @test setproperties(42) === 42 @@ -121,29 +141,29 @@ end @testset "SubArray" begin subarray = view(A1, 1:2, 3:4) - @test constructorof(typeof(subarray))(getproperties(subarray)...) === subarray + @test constructorof(typeof(subarray))(getfields(subarray)...) === subarray @test all(constructorof(typeof(subarray))(A2, (Base.OneTo(2), 3:4), 0, 0) .== Float32[1 1; 1 1]) - @inferred constructorof(typeof(subarray))(getproperties(subarray)...) + @inferred constructorof(typeof(subarray))(getfields(subarray)...) @inferred constructorof(typeof(subarray))(A2, (Base.OneTo(2), 3:4), 0, 0) end @testset "ReinterpretArray" begin ra1 = reinterpret(Float16, A1) @test constructorof(typeof(ra1))(A1) === ra1 - @test constructorof(typeof(ra1))(getproperties(ra1)...) === ra1 + @test constructorof(typeof(ra1))(getfields(ra1)...) === ra1 ra2 = constructorof(typeof(ra1))(A2) @test size(ra2) == (10, 6) @test eltype(ra2) == Float16 - @inferred constructorof(typeof(ra1))(getproperties(ra1)...) + @inferred constructorof(typeof(ra1))(getfields(ra1)...) @inferred constructorof(typeof(ra1))(A2) end @testset "PermutedDimsArray" begin pda1 = PermutedDimsArray(A1, (2, 1)) @test constructorof(typeof(pda1))(A1) === pda1 - @test constructorof(typeof(pda1))(getproperties(pda1)...) === pda1 + @test constructorof(typeof(pda1))(getfields(pda1)...) === pda1 @test eltype(constructorof(typeof(pda1))(A2)) == Float32 - @inferred constructorof(typeof(pda1))(getproperties(pda1)...) + @inferred constructorof(typeof(pda1))(getfields(pda1)...) @inferred constructorof(typeof(pda1))(A2) end @@ -154,24 +174,24 @@ end tda = Tridiagonal(dl, d, du) @test isdefined(tda, :du2) == false @test constructorof(typeof(tda))(dl, d, du) === tda - @test constructorof(typeof(tda))(getproperties(tda)...) === tda + @test constructorof(typeof(tda))(getfields(tda)...) === tda # lu factorization defines du2 tda_lu = lu!(tda).factors @test isdefined(tda_lu, :du2) == true - @test constructorof(typeof(tda_lu))(getproperties(tda_lu)...) === tda_lu - @test constructorof(typeof(tda_lu))(getproperties(tda)...) !== tda_lu - @test constructorof(typeof(tda_lu))(getproperties(tda)...) === tda - @inferred constructorof(typeof(tda))(getproperties(tda)...) - @inferred constructorof(typeof(tda))(getproperties(tda_lu)...) + @test constructorof(typeof(tda_lu))(getfields(tda_lu)...) === tda_lu + @test constructorof(typeof(tda_lu))(getfields(tda)...) !== tda_lu + @test constructorof(typeof(tda_lu))(getfields(tda)...) === tda + @inferred constructorof(typeof(tda))(getfields(tda)...) + @inferred constructorof(typeof(tda))(getfields(tda_lu)...) end @testset "LinRange" begin lr1 = LinRange(1, 7, 10) lr2 = LinRange(1.0f0, 7.0f0, 10) @test constructorof(typeof(lr1))(1, 7, 10, nothing) === lr1 - @test constructorof(typeof(lr1))(getproperties(lr2)...) === lr2 - @inferred constructorof(typeof(lr1))(getproperties(lr1)...) - @inferred constructorof(typeof(lr1))(getproperties(lr2)...) + @test constructorof(typeof(lr1))(getfields(lr2)...) === lr2 + @inferred constructorof(typeof(lr1))(getfields(lr1)...) + @inferred constructorof(typeof(lr1))(getfields(lr2)...) end end @@ -244,22 +264,37 @@ struct FieldProps{NT <: NamedTuple{(:a, :b)}} components::NT end -Base.propertynames(obj::FieldProps) = (:a, :b) +Base.propertynames(::FieldProps) = (:a, :b) Base.getproperty(obj::FieldProps, name::Symbol) = getproperty(getfield(obj, :components), name) -ConstructionBase.constructorof(::Type{<:FieldProps}) = (a, b) -> FieldProps((a=a, b=b)) @testset "use properties, not fields" begin x = FieldProps((a=1, b=:b)) + @test constructorof(typeof(x)) === FieldProps + @test getfields(x) === (components=(a=1, b=:b),) + res = @test_throws ErrorException setproperties(x, c=0) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + @test_throws ErrorException setproperties(x, components=(a=1,b=:b)) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + @test_throws ErrorException setproperties(x, a="aaa") + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + # == FieldProps((a="aaa", b=:b) if VERSION >= v"1.7" @test getproperties(x) == (a=1, b=:b) - @test setproperties(x, a="aaa") == FieldProps((a="aaa", b=:b)) - VERSION >= v"1.8-dev" ? - (@test_throws "Failed to assign properties (:c,) to object with properties (:a, :b)" setproperties(x, c=0)) : - (@test_throws ArgumentError setproperties(x, c=0)) else - @test_throws ErrorException getproperties(x) - @test_throws ErrorException setproperties(x, a="aaa") - @test_throws ErrorException setproperties(x, c=0) + res = @test_throws ErrorException getproperties(x) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("getproperties", msg) + @test occursin("FieldProps", msg) end end @@ -343,6 +378,7 @@ end @test 0 == hot_loop_allocs(constructorof, typeof(obj)) @test 0 == hot_loop_allocs(reconstruct, obj, new_content) @test 0 == hot_loop_allocs(getproperties, obj) + @test 0 == hot_loop_allocs(getfields, obj) patch_sizes = [0,1,n÷3,n÷2,n] patch_sizes = min.(patch_sizes, n) patch_sizes = unique(patch_sizes) @@ -363,6 +399,8 @@ end @test length(t) == n @test getproperties(t) === t @inferred getproperties(t) + @test getfields(t) === t + @inferred getfields(t) @inferred constructorof(typeof(t)) content = funny_numbers(Tuple,n) @inferred reconstruct(t, content) @@ -384,6 +422,8 @@ end @test length(nt) == n @test getproperties(nt) === nt @inferred getproperties(nt) + @test getfields(nt) === nt + @inferred getfields(nt) @inferred constructorof(typeof(nt)) if VERSION >= v"1.3" @@ -418,6 +458,10 @@ end @inferred reconstruct(funny_numbers(S,40), funny_numbers(Tuple,40)) end + @inferred getfields(funny_numbers(S,0)) + @inferred getfields(funny_numbers(S,1)) + @inferred getfields(funny_numbers(S,20)) + @inferred getfields(funny_numbers(S,40)) @inferred getproperties(funny_numbers(S,0)) @inferred getproperties(funny_numbers(S,1)) @inferred getproperties(funny_numbers(S,20))