/
ConstructionBase.jl
225 lines (205 loc) · 7.66 KB
/
ConstructionBase.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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"),
]
# Don't fail when somehow importing docstrings doesn't work (we
# don't loose any functionalities for that). We use explicit
# `@docs` in `docs/src/index.md` to make sure importing docstrings
# succeeds in CI.
try
include_dependency(path)
str = read(path, String)
@eval @doc $str $name
catch err
@error "Failed to import docstring for $name" exception=(err, catch_backtrace())
end
end
@generated function constructorof(::Type{T}) where T
getfield(parentmodule(T), nameof(T))
end
constructorof(::Type{<:Tuple}) = tuple
constructorof(::Type{<:NamedTuple{names}}) where names =
NamedTupleConstructor{names}()
struct NamedTupleConstructor{names} end
@inline function (::NamedTupleConstructor{names})(args...) where names
NamedTuple{names}(args)
end
################################################################################
#### getfields
################################################################################
getfields(x::Tuple) = x
getfields(x::NamedTuple) = x
getproperties(o::NamedTuple) = o
getproperties(o::Tuple) = o
if VERSION >= v"1.7"
function check_properties_are_fields(obj)
# for ntuples of symbols `===` is semantically the same as `==`
# but triple equals is easier for the compiler to optimize, see #82
if propertynames(obj) !== fieldnames(typeof(obj))
error("""
The function `Base.propertynames` was overloaded for type `$(typeof(obj))`.
Please make sure `ConstructionBase.setproperties` is also overloaded for this type.
""")
end
end
else
function is_propertynames_overloaded(T::Type)::Bool
which(propertynames, Tuple{T}).sig !== Tuple{typeof(propertynames), Any}
end
@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
end
# dispatch on eltype(names) to select Tuple vs NamedTuple
@inline tuple_or_ntuple(names, vals) = tuple_or_ntuple(eltype(names), names, vals)
# if names are empty (object has no properties): return namedtuple, for backwards compat and generally makes more sense than tuple
@inline tuple_or_ntuple(names::Tuple{}, vals::Tuple) = NamedTuple{names}(vals)
# names are consecutive integers: return tuple
@inline function tuple_or_ntuple(::Type{Int}, names, vals)
@assert Tuple(names) == ntuple(identity, length(names))
Tuple(vals)
end
# names are symbols: return namedtuple
@inline tuple_or_ntuple(::Type{Symbol}, names, vals) = NamedTuple{Tuple(names)}(vals)
# otherwise: throw an error
tuple_or_ntuple(::Type, names, vals) = error("Only Int and Symbol propertynames are supported")
if VERSION >= v"1.7"
function getproperties(obj)
fnames = propertynames(obj)
tuple_or_ntuple(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 getfields(obj)
fnames = fieldnames(obj)
fvals = map(fnames) do fname
Expr(:call, :getfield, :obj, QuoteNode(fname))
end
:(NamedTuple{$fnames}(($(fvals...),)))
end
function getproperties(obj)
check_properties_are_fields(obj)
getfields(obj)
end
end
################################################################################
##### setproperties
################################################################################
function setproperties(obj; kw...)
setproperties(obj, (;kw...))
end
setproperties(obj , patch::Tuple ) = setproperties_object(obj , patch )
setproperties(obj , patch::NamedTuple ) = setproperties_object(obj , patch )
setproperties(obj::NamedTuple , patch::Tuple ) = setproperties_namedtuple(obj , patch )
setproperties(obj::NamedTuple , patch::NamedTuple ) = setproperties_namedtuple(obj , patch )
setproperties(obj::Tuple , patch::Tuple ) = setproperties_tuple(obj , patch )
setproperties(obj::Tuple , patch::NamedTuple ) = setproperties_tuple(obj , patch )
setproperties_namedtuple(obj, patch::Tuple{}) = obj
@noinline function setproperties_namedtuple(obj, patch::Tuple)
msg = """
setproperties(obj::NamedTuple, patch::Tuple) only allowed for empty Tuple. Got:
obj = $obj
patch = $patch
"""
throw(ArgumentError(msg))
end
function setproperties_namedtuple(obj, patch)
res = merge(obj, patch)
check_patch_properties_exist(res, obj, obj, patch)
res
end
function check_patch_properties_exist(
nt_new::NamedTuple{fields}, nt_old::NamedTuple{fields}, obj, patch) where {fields}
nothing
end
@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)).
"""
throw(ArgumentError(msg))
end
function setproperties_namedtuple(obj::NamedTuple{fields}, patch::NamedTuple{fields}) where {fields}
patch
end
setproperties_tuple(obj::Tuple, patch::NamedTuple{()}) = obj
@noinline function setproperties_tuple(obj::Tuple, patch::NamedTuple)
msg = """
setproperties(obj::Tuple, patch::NamedTuple) only allowed for empty NamedTuple. Got:
obj ::Tuple = $obj
patch::NamedTuple = $patch
"""
throw(ArgumentError(msg))
end
function setproperties_tuple(obj::NTuple{N,Any}, patch::NTuple{N,Any}) where {N}
patch
end
append(x,y) = (x..., y...)
@noinline function throw_setproperties_tuple_error(obj, patch)
msg = """
Cannot call `setproperties(obj::Tuple, patch::Tuple)` with `length(obj) < length(patch)`. Got:
obj = $obj
patch = $patch
"""
throw(ArgumentError(msg))
end
function setproperties_tuple(obj::NTuple{N,Any}, patch::NTuple{K,Any}) where {N,K}
if K > N
throw_setproperties_tuple_error(obj, patch)
end
append(patch, after(obj, Val{K}()))
end
function after(xs::Tuple, ::Val{N}) where {N}
after(Base.tail(xs), Val{N-1}())
end
function after(x::Tuple, ::Val{0})
x
end
setproperties_object(obj, patch::Tuple{}) = obj
@noinline function setproperties_object(obj, patch::Tuple)
msg = """
setproperties(obj, patch::Tuple) only allowed for empty Tuple. Got:
obj = $obj
patch = $patch
"""
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)
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")
include("functions.jl")
end # module