Skip to content

Commit aa7f92b

Browse files
authored
Fix #62; Add obsolete pragma (#136)
1 parent c303782 commit aa7f92b

8 files changed

Lines changed: 219 additions & 7 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ tests/test_nested_cmd
99
tests/test_help
1010
tests/test_argument
1111
tests/test_parsecmdarg
12+
tests/test_obsolete

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,34 @@ The `defaultValue` won't be set.
361361

362362
-----------------
363363

364+
```nim
365+
template obsolete*(v: string = "") {.pragma.}
366+
```
367+
368+
Apply this to a field to emit a deprecation warning when the option
369+
is set by the user through the CLI, an env-var, or a configuration file.
370+
371+
The warning logger can be customized by overloading
372+
`proc obsoleteCmdOpt*(T: type, opt, msg: string)`.
373+
Where `T` is the config type, `opt` is the option name, and `msg`
374+
is the value of the `obsolete` pragma (which may be empty).
375+
376+
If the logger requires initialization, it can be set through
377+
the `loggerSetup` parameter, for example:
378+
379+
```nim
380+
type MyConf = object
381+
# options
382+
383+
proc myLogger(config: MyConf) {.gcsafe, raises: [ConfigurationError].} =
384+
# set up logger
385+
discard
386+
387+
let c = MyConf.load(loggerSetup = myLogger)
388+
```
389+
390+
-----------------
391+
364392
```nim
365393
template implicitlySelectable* {.pragma.}
366394
```

confutils.nim

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ type
6464
OptFlag = enum
6565
optHidden
6666
optDebug
67+
optObsolete
6768

6869
OptInfo = ref object
6970
name, abbr, desc, typename: string
@@ -74,6 +75,7 @@ type
7475
flags: set[OptFlag]
7576
hasDefault: bool
7677
defaultInHelpText: string
78+
obsoleteMsg: string
7779
case kind: OptKind
7880
of Discriminator:
7981
isCommand: bool
@@ -270,8 +272,7 @@ func hasAbbrs(cmds: openArray[CmdInfo], excl: set[OptFlag]): bool =
270272
return true
271273
false
272274

273-
func hasDebugOpts(cmds: openArray[CmdInfo]): bool =
274-
let excl = {optHidden}
275+
func hasDebugOpts(cmds: openArray[CmdInfo], excl: set[OptFlag]): bool =
275276
for opt in helpOptsIt(cmds, excl):
276277
if optDebug in opt.flags:
277278
return true
@@ -492,13 +493,13 @@ proc showHelp(help: var string,
492493

493494
let cmd = activeCmds[^1]
494495

495-
var excl = {optHidden}
496+
var excl = {optHidden, optObsolete}
496497
if hlpDebug notin appInfo.flags:
497498
excl.incl optDebug
498499

499500
appInfo.maxNameLen = maxNameLen(activeCmds, excl)
500501
appInfo.hasAbbrs = hasAbbrs(activeCmds, excl)
501-
appInfo.hasDebugOpts = hasDebugOpts(activeCmds)
502+
appInfo.hasDebugOpts = hasDebugOpts(activeCmds, excl - {optDebug})
502503
let termWidth =
503504
try:
504505
terminalWidth()
@@ -782,6 +783,24 @@ func requiresInput*(T: type): bool =
782783
func acceptsMultipleValues*(T: type): bool =
783784
T is seq
784785

786+
when defined(nimscript):
787+
template warnOutput(args: varargs[string]) =
788+
writeLine(stderr, "Warning: " & join(@args))
789+
else:
790+
template warnOutput(args: varargs[untyped]) =
791+
errorOutput(styleBright, fgYellow, "Warning: ", resetStyle, args)
792+
errorOutput("\p")
793+
794+
proc obsoleteCmdOpt(T: type, opt, msg: string) =
795+
if msg.len > 0:
796+
warnOutput(msg, "; ", opt, " is deprecated")
797+
else:
798+
warnOutput(opt, " is deprecated")
799+
800+
proc obsoleteCmdOptAux(T: type, opt, msg: string) =
801+
mixin obsoleteCmdOpt
802+
obsoleteCmdOpt(T, opt, msg)
803+
785804
template debugMacroResult(macroName: string) {.dirty.} =
786805
when defined(debugMacros) or defined(debugConfutils):
787806
echo "\n-------- ", macroName, " ----------------------"
@@ -890,6 +909,8 @@ func readPragmaFlags(field: FieldDescription): set[OptFlag] =
890909
result.incl optHidden
891910
if field.readPragma("debug") != nil:
892911
result.incl optDebug
912+
if field.readPragma"obsolete" != nil:
913+
result.incl optObsolete
893914

894915
proc cmdInfoFromType(T: NimNode): CmdInfo =
895916
result = CmdInfo()
@@ -907,6 +928,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
907928
defaultInHelp = if defaultValueDesc != nil: defaultValueDesc
908929
else: defaultValue
909930
defaultInHelpText = toText(defaultInHelp)
931+
obsoleteMsg = field.readPragma"obsolete"
910932
separator = field.readPragma"separator"
911933
longDesc = field.readPragma"longDesc"
912934
envVarValueSep = field.readPragma"envVarValueSep"
@@ -932,6 +954,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
932954
if separator != nil: opt.separator = separator.strVal
933955
if longDesc != nil: opt.longDesc = longDesc.strVal
934956
if envVarValueSep != nil: opt.envVarValueSep = envVarValueSep.strVal
957+
if obsoleteMsg != nil: opt.obsoleteMsg = obsoleteMsg.strVal
935958

936959
inc fieldIdx
937960

@@ -1090,7 +1113,10 @@ proc loadImpl[C, SecondarySources](
10901113
config: Configuration, sources: ref SecondarySources
10911114
) {.gcsafe, raises: [ConfigurationError].} = nil,
10921115
envVarsPrefix = appInvocation(),
1093-
termWidth = 0
1116+
termWidth = 0,
1117+
loggerSetup: proc (
1118+
config: Configuration
1119+
) {.gcsafe, raises: [ConfigurationError].} = nil,
10941120
): Configuration {.raises: [ConfigurationError].} =
10951121
## Loads a program configuration by parsing command-line arguments
10961122
## and a standard set of config files that can specify:
@@ -1369,6 +1395,17 @@ proc loadImpl[C, SecondarySources](
13691395
for cmd in activeCmds:
13701396
result.processMissingOpts(cmd)
13711397

1398+
if not isNil(loggerSetup):
1399+
try:
1400+
loggerSetup(result)
1401+
except ConfigurationError as err:
1402+
fail "Failed to setup the logger: '" & err.msg & "'"
1403+
1404+
for cmd in activeCmds:
1405+
for opt in cmd.opts:
1406+
if optObsolete in opt.flags and fieldCounters[opt.idx] != 0:
1407+
obsoleteCmdOptAux(typeof(Configuration), opt.humaneName(), opt.obsoleteMsg)
1408+
13721409
template load*(
13731410
Configuration: type,
13741411
cmdLine = commandLineParams(),
@@ -1379,12 +1416,14 @@ template load*(
13791416
ignoreUnknown = false,
13801417
secondarySources: untyped = nil,
13811418
envVarsPrefix = appInvocation(),
1382-
termWidth = 0): untyped =
1419+
termWidth = 0,
1420+
loggerSetup: untyped = nil
1421+
): untyped =
13831422
block:
13841423
let secondarySourcesRef = generateSecondarySources(Configuration)
13851424
loadImpl(Configuration, cmdLine, version,
13861425
copyrightBanner, printUsage, quitOnFailure, ignoreUnknown,
1387-
secondarySourcesRef, secondarySources, envVarsPrefix, termWidth)
1426+
secondarySourcesRef, secondarySources, envVarsPrefix, termWidth, loggerSetup)
13881427

13891428
func defaults*(Configuration: type): Configuration =
13901429
load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false)

confutils/defs.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ template separator*(v: string) {.pragma.}
6161
template defaultValue*(v: untyped) {.pragma.}
6262
template defaultValueDesc*(v: string) {.pragma.}
6363
template envVarValueSep*(v = "") {.pragma.}
64+
template obsolete*(v = "") {.pragma.}
6465
template required* {.pragma.}
6566
template command* {.pragma.}
6667
template argument* {.pragma.}

tests/test_all.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import
1616
test_ignore,
1717
test_multi_case_values,
1818
test_nested_cmd,
19+
test_obsolete_overload,
20+
test_obsolete,
1921
test_parsecmdarg,
2022
test_pragma,
2123
test_qualified_ident,

tests/test_obsolete.nim

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import
2+
unittest2,
3+
../confutils
4+
5+
type
6+
TestConf = object
7+
opt1 {.
8+
obsolete
9+
defaultValue: "opt1 default"
10+
name: "opt1"}: string
11+
12+
#let conf = TestConf.load()
13+
#echo conf.opt1
14+
15+
suite "test obsolete option":
16+
test "obsolete option default":
17+
let conf = TestConf.load()
18+
check conf.opt1 == "opt1 default"
19+
20+
test "obsolete option set":
21+
let conf = TestConf.load(cmdLine = @[
22+
"--opt1=foo"
23+
])
24+
check conf.opt1 == "foo"
25+
26+
type
27+
OverloadConf = object
28+
opt1 {.
29+
obsolete
30+
defaultValue: "opt1 default"
31+
name: "opt1"}: string
32+
opt2 {.
33+
obsolete
34+
defaultValue: "opt2 default"
35+
name: "opt2"}: string
36+
opt3 {.
37+
obsolete: "opt3 obsolete msg"
38+
defaultValue: "opt3 default"
39+
name: "opt3"}: string
40+
opt4 {.
41+
defaultValue: "opt4 default"
42+
name: "opt4"}: string
43+
44+
var registry {.threadvar.}: seq[string]
45+
46+
proc obsoleteCmdOpt(T: type OverloadConf, opt, msg: string) =
47+
registry.add opt
48+
if msg.len > 0:
49+
registry.add msg
50+
51+
suite "test obsolete option overload":
52+
test "the overload is called if opt is set":
53+
registry.setLen 0
54+
let conf = OverloadConf.load(cmdLine = @[
55+
"--opt1=foo"
56+
])
57+
check conf.opt1 == "foo"
58+
check registry == @["opt1"]
59+
60+
test "the overload is not called if opt not set":
61+
registry.setLen 0
62+
let conf = OverloadConf.load()
63+
check conf.opt1 == "opt1 default"
64+
check registry.len == 0
65+
66+
test "the overload is called for all obsolete opts set":
67+
registry.setLen 0
68+
let conf = OverloadConf.load(cmdLine = @[
69+
"--opt1=foo",
70+
"--opt2=bar"
71+
])
72+
check conf.opt1 == "foo"
73+
check conf.opt2 == "bar"
74+
check registry == @["opt1", "opt2"]
75+
76+
test "the overload is called with obsolete msg":
77+
registry.setLen 0
78+
let conf = OverloadConf.load(cmdLine = @[
79+
"--opt3=foo"
80+
])
81+
check conf.opt3 == "foo"
82+
check registry == @["opt3", "opt3 obsolete msg"]
83+
84+
test "the logger setup is called":
85+
proc loggerSetup(c: OverloadConf) =
86+
doAssert c.opt1 == "opt1 default"
87+
registry.add "logger"
88+
89+
registry.setLen 0
90+
let conf = OverloadConf.load(loggerSetup = loggerSetup)
91+
check conf.opt1 == "opt1 default"
92+
check registry == @["logger"]
93+
94+
test "the logger setup is called before the overload":
95+
proc loggerSetup(c: OverloadConf) =
96+
doAssert c.opt1 == "foo"
97+
registry.add "logger"
98+
99+
registry.setLen 0
100+
let conf = OverloadConf.load(
101+
cmdLine = @[
102+
"--opt1=foo"
103+
],
104+
loggerSetup = loggerSetup
105+
)
106+
check conf.opt1 == "foo"
107+
check registry == @["logger", "opt1"]

tests/test_obsolete_overload.nim

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import
2+
unittest2,
3+
../confutils,
4+
./test_obsolete_overload_def
5+
6+
type
7+
TestConf = object
8+
opt1 {.
9+
obsolete
10+
defaultValue: "opt1 default"
11+
name: "opt1"}: string
12+
13+
suite "test obsolete overload for type":
14+
test "obsolete option default":
15+
registry.setLen 0
16+
let conf = TestConf.load()
17+
check conf.opt1 == "opt1 default"
18+
check registry.len == 0
19+
20+
test "obsolete option set":
21+
registry.setLen 0
22+
let conf = TestConf.load(cmdLine = @[
23+
"--opt1=foo"
24+
])
25+
check conf.opt1 == "foo"
26+
check registry == @["opt1"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
# this is in its own module to test there
3+
# is no ambiguous (import) call error for obsoleteCmdOpt
4+
5+
var registry* {.threadvar.}: seq[string]
6+
7+
proc obsoleteCmdOpt*(T: type[object], opt, msg: string) =
8+
registry.add opt

0 commit comments

Comments
 (0)