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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ tests/test_nested_cmd
tests/test_help
tests/test_argument
tests/test_parsecmdarg
tests/test_obsolete
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,34 @@ The `defaultValue` won't be set.

-----------------

```nim
template obsolete*(v: string = "") {.pragma.}
```

Apply this to a field to emit a deprecation warning when the option
is set by the user through the CLI, an env-var, or a configuration file.

The warning logger can be customized by overloading
`proc obsoleteCmdOpt*(T: type, opt, msg: string)`.
Where `T` is the config type, `opt` is the option name, and `msg`
is the value of the `obsolete` pragma (which may be empty).

If the logger requires initialization, it can be set through
the `loggerSetup` parameter, for example:

```nim
type MyConf = object
# options

proc myLogger(config: MyConf) {.gcsafe, raises: [ConfigurationError].} =
# set up logger
discard

let c = MyConf.load(loggerSetup = myLogger)
```

-----------------

```nim
template implicitlySelectable* {.pragma.}
```
Expand Down
53 changes: 46 additions & 7 deletions confutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type
OptFlag = enum
optHidden
optDebug
optObsolete

OptInfo = ref object
name, abbr, desc, typename: string
Expand All @@ -74,6 +75,7 @@ type
flags: set[OptFlag]
hasDefault: bool
defaultInHelpText: string
obsoleteMsg: string
case kind: OptKind
of Discriminator:
isCommand: bool
Expand Down Expand Up @@ -270,8 +272,7 @@ func hasAbbrs(cmds: openArray[CmdInfo], excl: set[OptFlag]): bool =
return true
false

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

let cmd = activeCmds[^1]

var excl = {optHidden}
var excl = {optHidden, optObsolete}
if hlpDebug notin appInfo.flags:
excl.incl optDebug

appInfo.maxNameLen = maxNameLen(activeCmds, excl)
appInfo.hasAbbrs = hasAbbrs(activeCmds, excl)
appInfo.hasDebugOpts = hasDebugOpts(activeCmds)
appInfo.hasDebugOpts = hasDebugOpts(activeCmds, excl - {optDebug})
let termWidth =
try:
terminalWidth()
Expand Down Expand Up @@ -782,6 +783,24 @@ func requiresInput*(T: type): bool =
func acceptsMultipleValues*(T: type): bool =
T is seq

when defined(nimscript):
template warnOutput(args: varargs[string]) =
writeLine(stderr, "Warning: " & join(@args))
else:
template warnOutput(args: varargs[untyped]) =
errorOutput(styleBright, fgYellow, "Warning: ", resetStyle, args)
errorOutput("\p")

proc obsoleteCmdOpt(T: type, opt, msg: string) =
if msg.len > 0:
warnOutput(msg, "; ", opt, " is deprecated")
else:
warnOutput(opt, " is deprecated")

proc obsoleteCmdOptAux(T: type, opt, msg: string) =
mixin obsoleteCmdOpt
obsoleteCmdOpt(T, opt, msg)

template debugMacroResult(macroName: string) {.dirty.} =
when defined(debugMacros) or defined(debugConfutils):
echo "\n-------- ", macroName, " ----------------------"
Expand Down Expand Up @@ -890,6 +909,8 @@ func readPragmaFlags(field: FieldDescription): set[OptFlag] =
result.incl optHidden
if field.readPragma("debug") != nil:
result.incl optDebug
if field.readPragma"obsolete" != nil:
result.incl optObsolete

proc cmdInfoFromType(T: NimNode): CmdInfo =
result = CmdInfo()
Expand All @@ -907,6 +928,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
defaultInHelp = if defaultValueDesc != nil: defaultValueDesc
else: defaultValue
defaultInHelpText = toText(defaultInHelp)
obsoleteMsg = field.readPragma"obsolete"
separator = field.readPragma"separator"
longDesc = field.readPragma"longDesc"
envVarValueSep = field.readPragma"envVarValueSep"
Expand All @@ -932,6 +954,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
if separator != nil: opt.separator = separator.strVal
if longDesc != nil: opt.longDesc = longDesc.strVal
if envVarValueSep != nil: opt.envVarValueSep = envVarValueSep.strVal
if obsoleteMsg != nil: opt.obsoleteMsg = obsoleteMsg.strVal

inc fieldIdx

Expand Down Expand Up @@ -1090,7 +1113,10 @@ proc loadImpl[C, SecondarySources](
config: Configuration, sources: ref SecondarySources
) {.gcsafe, raises: [ConfigurationError].} = nil,
envVarsPrefix = appInvocation(),
termWidth = 0
termWidth = 0,
loggerSetup: proc (
config: Configuration
) {.gcsafe, raises: [ConfigurationError].} = nil,
): Configuration {.raises: [ConfigurationError].} =
## Loads a program configuration by parsing command-line arguments
## and a standard set of config files that can specify:
Expand Down Expand Up @@ -1369,6 +1395,17 @@ proc loadImpl[C, SecondarySources](
for cmd in activeCmds:
result.processMissingOpts(cmd)

if not isNil(loggerSetup):
try:
loggerSetup(result)
except ConfigurationError as err:
fail "Failed to setup the logger: '" & err.msg & "'"

for cmd in activeCmds:
for opt in cmd.opts:
if optObsolete in opt.flags and fieldCounters[opt.idx] != 0:
obsoleteCmdOptAux(typeof(Configuration), opt.humaneName(), opt.obsoleteMsg)

template load*(
Configuration: type,
cmdLine = commandLineParams(),
Expand All @@ -1379,12 +1416,14 @@ template load*(
ignoreUnknown = false,
secondarySources: untyped = nil,
envVarsPrefix = appInvocation(),
termWidth = 0): untyped =
termWidth = 0,
loggerSetup: untyped = nil
): untyped =
block:
let secondarySourcesRef = generateSecondarySources(Configuration)
loadImpl(Configuration, cmdLine, version,
copyrightBanner, printUsage, quitOnFailure, ignoreUnknown,
secondarySourcesRef, secondarySources, envVarsPrefix, termWidth)
secondarySourcesRef, secondarySources, envVarsPrefix, termWidth, loggerSetup)

func defaults*(Configuration: type): Configuration =
load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false)
Expand Down
1 change: 1 addition & 0 deletions confutils/defs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ template separator*(v: string) {.pragma.}
template defaultValue*(v: untyped) {.pragma.}
template defaultValueDesc*(v: string) {.pragma.}
template envVarValueSep*(v = "") {.pragma.}
template obsolete*(v = "") {.pragma.}
template required* {.pragma.}
template command* {.pragma.}
template argument* {.pragma.}
Expand Down
2 changes: 2 additions & 0 deletions tests/test_all.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import
test_ignore,
test_multi_case_values,
test_nested_cmd,
test_obsolete_overload,
test_obsolete,
test_parsecmdarg,
test_pragma,
test_qualified_ident,
Expand Down
107 changes: 107 additions & 0 deletions tests/test_obsolete.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import
unittest2,
../confutils

type
TestConf = object
opt1 {.
obsolete
defaultValue: "opt1 default"
name: "opt1"}: string

#let conf = TestConf.load()
#echo conf.opt1

suite "test obsolete option":
test "obsolete option default":
let conf = TestConf.load()
check conf.opt1 == "opt1 default"

test "obsolete option set":
let conf = TestConf.load(cmdLine = @[
"--opt1=foo"
])
check conf.opt1 == "foo"

type
OverloadConf = object
opt1 {.
obsolete
defaultValue: "opt1 default"
name: "opt1"}: string
opt2 {.
obsolete
defaultValue: "opt2 default"
name: "opt2"}: string
opt3 {.
obsolete: "opt3 obsolete msg"
defaultValue: "opt3 default"
name: "opt3"}: string
opt4 {.
defaultValue: "opt4 default"
name: "opt4"}: string

var registry {.threadvar.}: seq[string]

proc obsoleteCmdOpt(T: type OverloadConf, opt, msg: string) =
registry.add opt
if msg.len > 0:
registry.add msg

suite "test obsolete option overload":
test "the overload is called if opt is set":
registry.setLen 0
let conf = OverloadConf.load(cmdLine = @[
"--opt1=foo"
])
check conf.opt1 == "foo"
check registry == @["opt1"]

test "the overload is not called if opt not set":
registry.setLen 0
let conf = OverloadConf.load()
check conf.opt1 == "opt1 default"
check registry.len == 0

test "the overload is called for all obsolete opts set":
registry.setLen 0
let conf = OverloadConf.load(cmdLine = @[
"--opt1=foo",
"--opt2=bar"
])
check conf.opt1 == "foo"
check conf.opt2 == "bar"
check registry == @["opt1", "opt2"]

test "the overload is called with obsolete msg":
registry.setLen 0
let conf = OverloadConf.load(cmdLine = @[
"--opt3=foo"
])
check conf.opt3 == "foo"
check registry == @["opt3", "opt3 obsolete msg"]

test "the logger setup is called":
proc loggerSetup(c: OverloadConf) =
doAssert c.opt1 == "opt1 default"
registry.add "logger"

registry.setLen 0
let conf = OverloadConf.load(loggerSetup = loggerSetup)
check conf.opt1 == "opt1 default"
check registry == @["logger"]

test "the logger setup is called before the overload":
proc loggerSetup(c: OverloadConf) =
doAssert c.opt1 == "foo"
registry.add "logger"

registry.setLen 0
let conf = OverloadConf.load(
cmdLine = @[
"--opt1=foo"
],
loggerSetup = loggerSetup
)
check conf.opt1 == "foo"
check registry == @["logger", "opt1"]
26 changes: 26 additions & 0 deletions tests/test_obsolete_overload.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import
unittest2,
../confutils,
./test_obsolete_overload_def

type
TestConf = object
opt1 {.
obsolete
defaultValue: "opt1 default"
name: "opt1"}: string

suite "test obsolete overload for type":
test "obsolete option default":
registry.setLen 0
let conf = TestConf.load()
check conf.opt1 == "opt1 default"
check registry.len == 0

test "obsolete option set":
registry.setLen 0
let conf = TestConf.load(cmdLine = @[
"--opt1=foo"
])
check conf.opt1 == "foo"
check registry == @["opt1"]
8 changes: 8 additions & 0 deletions tests/test_obsolete_overload_def.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

# this is in its own module to test there
# is no ambiguous (import) call error for obsoleteCmdOpt

var registry* {.threadvar.}: seq[string]

proc obsoleteCmdOpt*(T: type[object], opt, msg: string) =
registry.add opt