Skip to content

Commit 8d6e1be

Browse files
authored
Handle non-bijective data-type conversions more robustly (#240)
* Enrich docstring of `@convert` macro * Add some control on `@convert` macro arguments * Add CEnum.jl dependency for `@convert` macro arguments checks * Move import CEnum in utils.jl to using CEnum in ArchGDAL.jl * Removed in `@convert`macro a duplicate `@assert` on subtypes 2 when type 2 is an `UnionAll` * Added tests on default types for non bijective conversion mapsets * Formatted with JuliaFormatter * Moved from Dict to ImmutableDict in `@convert` macro and modified relevant test cases accordingly * Formatted with JuliaFormatter * Suppressed ImmutableDict helper function in utils.jl. It works without * For Julia version < 1.5, add an `ImmutableDict `constructor based on a list of `Pair` arguments (available in Base since Julia 1.5) * ... add Base prefix for ImmutableDict constructor based on a list of Pair arguments * ... one more Base prefix added * ... fallback to an `ImmutableDict` constructor based on a list of `Pair` arguments, based on a loop
1 parent 18578bb commit 8d6e1be

File tree

4 files changed

+126
-57
lines changed

4 files changed

+126
-57
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ desc = "A high level API for GDAL - Geospatial Data Abstraction Library"
66
version = "0.7.4"
77

88
[deps]
9+
CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82"
910
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
1011
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
1112
DiskArrays = "3c3547ce-8d99-4f5e-a174-61eb10b00ae3"
@@ -16,6 +17,7 @@ ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
1617
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
1718

1819
[compat]
20+
CEnum = "0.4"
1921
ColorTypes = "0.10, 0.11"
2022
DiskArrays = "0.2.4"
2123
GDAL = "1.1.3"

src/ArchGDAL.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ using GeoInterface: GeoInterface
77
using Tables: Tables
88
using ImageCore: Normed, N0f8, N0f16, N0f32, ImageCore
99
using ColorTypes: ColorTypes
10+
using CEnum
1011

1112
const GFT = GeoFormatTypes
1213

src/utils.jl

Lines changed: 97 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,87 @@
1+
# An ImmutableDict constructor based on a list of Pair arguments has been introduced in Julia 1.6.0 and backported to Julia 1.5
2+
if VERSION < v"1.5"
3+
function Base.ImmutableDict(KV::Pair{K,V}, KVs::Pair{K,V}...) where {K,V}
4+
d = Base.ImmutableDict(KV)
5+
for p in KVs
6+
d = Base.ImmutableDict(d, p)
7+
end
8+
return d
9+
end
10+
end
11+
112
"""
2-
@convert(args...)
13+
@convert(<T1>::<T2>,
14+
<conversions>
15+
)
16+
17+
Generate `convert` functions both ways between ArchGDAL Enum of typeids (e.g. `ArchGDAL.OGRFieldType`)
18+
and other types or typeids.
19+
20+
ArchGDAL uses Enum types, listing typeids of various data container used in GDAL/OGR object model.
21+
Some of these types are used to implement concrete types in julia through parametric composite types
22+
based on those Enum of typeids (e.g. `Geometry` and `IGeometry` types with `OGRwkbGeometryType`)
23+
24+
Other types or typeids can be:
25+
- GDAL CEnum.Cenum typeids (e.g. `GDAL.OGRFieldType`),
26+
- Base primitive DataType types (e.g. `Bool`),
27+
- other parametric composite types (e.g. `ImageCore.Normed`)
28+
29+
# Arguments
30+
- `(<T1>::<T2>)::Expr`: source and target supertypes, where `T1<:Enum` and `T2<:CEnum.Cenum || T2::Type{DataType} || T2::UnionAll}``
31+
- `(<stype1>::<stype2>)::Expr`: source and target subtypes or type ids with `stype1::T1` and
32+
- `stype2::T2 where T2<:CEnum.Cenum` or
33+
- `stype2::T2 where T2::Type{DataType}` or
34+
- `stype2<:T2`where T2<:UnionAll
35+
- ...
36+
37+
**Note:** In the case where the mapping is not bijective, the last declared typeid of subtype is used.
38+
Example:
39+
```
40+
@convert(
41+
OGRFieldType::DataType,
42+
OFTInteger::Bool,
43+
OFTInteger::Int16,
44+
OFTInteger::Int32,
45+
)
46+
```
47+
will generate a `convert` functions giving:
48+
- `Int32` type for `OFTInteger` and not `Ìnt16`
49+
- `OFTInteger` OGRFieldType typeid for both `Int16` and `Int32`
350
4-
# General case:
51+
# Usage
52+
### General case:
553
```
6-
eval(@convert(GDALRWFlag::GDAL.GDALRWFlag,
54+
@convert(GDALRWFlag::GDAL.GDALRWFlag,
755
GF_Read::GDAL.GF_Read,
856
GF_Write::GDAL.GF_Write,
9-
))
57+
)
1058
```
1159
does the equivalent of
1260
```
13-
const GDALRWFlag_to_GDALRWFlag_map = Dict(
61+
const GDALRWFlag_to_GDALRWFlag_map = ImmutableDict(
1462
GF_Read => GDAL.GF_Read,
1563
GF_Write => GDAL.GF_Write
1664
)
1765
Base.convert(::Type{GDAL.GDALRWFlag}, ft::GDALRWFlag) =
1866
GDALRWFlag_to_GDALRWFlag_map[ft]
1967
20-
const GDALRWFlag_to_GDALRWFlag_map = Dict(
68+
const GDALRWFlag_to_GDALRWFlag_map = ImmutableDict(
2169
GDAL.GF_Read => GF_Read,
2270
GDAL.GF_Write => GF_Write
2371
)
2472
Base.convert(::Type{GDALRWFlag}, ft::GDAL.GDALRWFlag) =
2573
GDALRWFlag_to_GDALRWFlag_map[ft]
2674
```
27-
# Case where 1st type `<: Enum` and 2nd type `== DataType` or ìsa UnionAll`:
75+
### Case where 1st type `<: Enum` and 2nd type `== DataType` or `ìsa UnionAll`:
2876
```
29-
eval(@convert(OGRFieldType::DataType,
77+
@convert(OGRFieldType::DataType,
3078
OFTInteger::Bool,
3179
OFTInteger::Int16,
32-
))
80+
)
3381
```
3482
does the equivalent of
3583
```
36-
const OGRFieldType_to_DataType_map = Dict(
84+
const OGRFieldType_to_DataType_map = ImmutableDict(
3785
OFTInteger => Bool,
3886
OFTInteger => Int16,
3987
)
@@ -46,46 +94,48 @@ Base.convert(::Type{OGRFieldType}, ft::Type{Int16}) = OFTInteger
4694
4795
"""
4896
macro convert(args...)
49-
@assert length(args) > 0
97+
@assert length(args) > 1
5098
@assert args[1].head == :(::)
51-
type1_symbol = args[1].args[1]
52-
type2_symbol = args[1].args[2]
53-
type1_string = replace(string(type1_symbol), "." => "_")
54-
type2_string = replace(string(type2_symbol), "." => "_")
55-
type1_isenum_and_type2_equaldatatype_or_isaunionall =
56-
(eval(type1_symbol) <: Enum) &&
57-
((eval(type2_symbol) == DataType) || (eval(type2_symbol) isa UnionAll))
58-
type1 = esc(type1_symbol)
59-
type2 = esc(type2_symbol)
60-
fwd_map = Expr[Expr(:tuple, esc.(a.args)...) for a in args[2:end]]
61-
if type1_isenum_and_type2_equaldatatype_or_isaunionall
62-
rev_to_enum_map = [Tuple(esc.(reverse(a.args))) for a in args[2:end]]
63-
else
64-
rev_map =
65-
Expr[Expr(:tuple, esc.(reverse(a.args))...) for a in args[2:end]]
66-
end
99+
(T1, T2) = args[1].args
100+
101+
# Types and type ids / subtypes checks
102+
stypes1, stypes2 = zip((eval.(a.args) for a in args[2:end])...)
103+
@assert(eval(T1) <: Enum && all(isa.(stypes1, eval(T1))))
104+
@assert(
105+
((eval(T2) <: CEnum.Cenum) && all(isa.(stypes2, eval(T2)))) ||
106+
((eval(T2) isa Type{DataType}) && all(isa.(stypes2, eval(T2)))) ||
107+
((eval(T2) isa UnionAll) && all((<:).(stypes2, eval(T2))))
108+
)
109+
110+
# Types other representations
111+
(T1_string, T2_string) = replace.(string.((T1, T2)), "." => "_")
112+
(type1, type2) = esc.((T1, T2))
113+
114+
# Subtypes forward and backward mapping
115+
fwd_map = [Expr(:call, esc(:Pair), esc.(a.args)...) for a in args[2:end]]
116+
rev_map =
117+
[Expr(:call, esc(:Pair), esc.(reverse(a.args))...) for a in args[2:end]]
67118

119+
#! Convert functions generation
68120
result_expr = Expr(:block)
69-
# Forward conversion (no case of 1st type == DataType and 2nd type <: Enum handled)
70-
type1_to_type2_map_name =
71-
esc(Symbol(type1_string * "_to_" * type2_string * "_map"))
121+
122+
#* Forward conversions from ArchGDAL typeids
123+
T1_to_T2_map_name = esc(Symbol(T1_string * "_to_" * T2_string * "_map"))
72124
push!(
73125
result_expr.args,
74-
:(const $type1_to_type2_map_name = Dict([$(fwd_map...)])),
126+
:(const $T1_to_T2_map_name = Base.ImmutableDict([$(fwd_map...)]...)),
75127
)
76128
push!(
77129
result_expr.args,
78130
:(function Base.convert(::Type{$type2}, ft::$type1)
79-
return get($type1_to_type2_map_name, ft) do
80-
return error("Unknown type: $ft")
81-
end
131+
return $T1_to_T2_map_name[ft]
82132
end),
83133
)
84-
# Reverse conversion
85-
if type1_isenum_and_type2_equaldatatype_or_isaunionall
86-
for stypes in rev_to_enum_map
87-
eval(type2_symbol) isa UnionAll &&
88-
@assert eval(stypes[1].args[1]) <: eval(type2_symbol)
134+
135+
#* Reverse conversions to ArchGDAL typeids
136+
# Optimization for conversion from types
137+
if !(eval(T2) <: CEnum.Cenum)
138+
for stypes in [Tuple(esc.(reverse(a.args))) for a in args[2:end]]
89139
push!(
90140
result_expr.args,
91141
:(
@@ -98,22 +148,24 @@ macro convert(args...)
98148
),
99149
)
100150
end
151+
# Conversion from typeids
101152
else
102-
type2_to_type1_map_name =
103-
esc(Symbol(type2_string * "_to_" * type1_string * "_map"))
153+
T2_to_T1_map_name = esc(Symbol(T2_string * "_to_" * T1_string * "_map"))
104154
push!(
105155
result_expr.args,
106-
:(const $type2_to_type1_map_name = Dict([$(rev_map...)])),
156+
:(
157+
const $T2_to_T1_map_name =
158+
Base.ImmutableDict([$(rev_map...)]...)
159+
),
107160
)
108161
push!(
109162
result_expr.args,
110163
:(function Base.convert(::Type{$type1}, ft::$type2)
111-
return get($type2_to_type1_map_name, ft) do
112-
return error("Unknown type: $ft")
113-
end
164+
return $T2_to_T1_map_name[ft]
114165
end),
115166
)
116167
end
168+
117169
return result_expr
118170
end
119171

test/test_types.jl

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ const AG = ArchGDAL;
44
import ImageCore
55

66
@testset "test_types.jl" begin
7+
@testset "Testing convert macro" begin
8+
@testset "Basic conversions" begin
9+
@test Base.convert(AG.GDALDataType, UInt8) == AG.GDT_Byte
10+
end
11+
12+
@testset "Error on conversion non implemented" begin
13+
@test_throws MethodError Base.convert(AG.GDALDataType, Int64)
14+
@test_throws KeyError Base.convert(DataType, AG.GDT_TypeCount)
15+
@test_throws KeyError Base.convert(ImageCore.Normed, AG.GDT_Float32)
16+
@test_throws KeyError Base.convert(DataType, AG.OFSTMaxSubType)
17+
end
18+
19+
@testset "Default types for non bijective conversion mapset" begin
20+
# Default type for AG.OFTInteger is Int32
21+
@test Base.convert(AG.OGRFieldType, Bool) == AG.OFTInteger
22+
@test Base.convert(AG.OGRFieldType, Int16) == AG.OFTInteger
23+
@test Base.convert(AG.OGRFieldType, Int32) == AG.OFTInteger
24+
@test Base.convert(DataType, AG.OFTInteger) == Int32
25+
26+
# Default type for AG.OFTReal is Float64
27+
@test Base.convert(AG.OGRFieldType, Float32) == AG.OFTReal
28+
@test Base.convert(AG.OGRFieldType, Float64) == AG.OFTReal
29+
@test Base.convert(DataType, AG.OFTReal) == Float64
30+
end
31+
end
32+
733
@testset "Testing GDAL Type Methods" begin
834
@testset "GDAL Open Flags" begin
935
@test AG.OF_READONLY | 0x04 == 0x04
@@ -21,18 +47,6 @@ import ImageCore
2147

2248
@test AG.iscomplex(AG.GDT_CFloat32) == true
2349
@test AG.iscomplex(AG.GDT_CFloat64) == true
24-
25-
@test Base.convert(AG.GDALDataType, UInt8) == AG.GDT_Byte
26-
@test_throws MethodError Base.convert(AG.GDALDataType, Int64)
27-
@test_throws ErrorException Base.convert(DataType, AG.GDT_TypeCount)
28-
@test_throws ErrorException Base.convert(
29-
ImageCore.Normed,
30-
AG.GDT_Float32,
31-
)
32-
@test_throws ErrorException Base.convert(
33-
DataType,
34-
AG.OFSTMaxSubType,
35-
)
3650
end
3751

3852
@testset "GDAL Colors and Palettes" begin

0 commit comments

Comments
 (0)