Skip to content

Commit

Permalink
Add lazy help setters
Browse files Browse the repository at this point in the history
  • Loading branch information
ajalt committed Jul 4, 2023
1 parent 824391c commit 282d184
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 141 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
- Removed legacy JS publications. Now only the JS/IR artifacts are published.
- Removed `CliktHelpFormatter`. Use `MordantHelpFormatter` instead.

### Deprecated
- Deprecated `CliktCommand.commandHelp` and `commandHelpEpilog` properties in favor of the methods with the same name.

### Changed
- `prompt` and `confirm` are now implemented with mordant's prompt functionality, and the method parameters have changed to match mordant's
- When using `treatUnknownOptionsAsArgs`, grouped short options like `-abc` will be treated as an argument rather than reporting an error as long as they don't match any short options in the command. ([#340](https://github.com/ajalt/clikt/pull/340))
Expand All @@ -40,6 +43,8 @@
- `CliktError` now includes `statusCode` and `printError` properties.
- The constructor of `UsageError` and its subclasses no longer takes a `context` parameter. The context is now inferred automatically.
- `UsageError.formatUsage` now takes the localization and formatter as arguments
- `Option.optionHelp` and `Argument.argumentHelp`, `CliktCommand.commandHelp`, and `CliktCommand.commandHelpEpilog` are now methods that take the context as an argument, and the `help` parameter to `copy` is now a `helpGetter` lambda. `CliktCommand.shortHelp` now takes the context as an argument.
- The `message` method on `TransformContext` interfaces is now an extension.

### Fixed
- When parsing a command line with more than one error, Clikt will now always report the error that occurs earliest if it can't report them all ([#361](https://github.com/ajalt/clikt/issues/361))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ internal object FishCompletionGenerator {

append("-a $commandName ")

val help = command.commandHelp.replace("'", "\\'")
val help = command.commandHelp(command.currentContext).replace("'", "\\'")
if (help.isNotBlank()) {
append("-d '${help}'")
}
Expand Down Expand Up @@ -91,7 +91,7 @@ internal object FishCompletionGenerator {
}

appendParamCompletion(option.completionCandidates)
appendHelp(option.optionHelp)
appendHelp(option.optionHelp(command.currentContext))
appendLine()
}

Expand All @@ -102,7 +102,7 @@ internal object FishCompletionGenerator {
for (argument in arguments) {
appendCompleteCall(rootCommandName, isTopLevel, hasSubcommands, commandName)
appendParamCompletion(argument.completionCandidates)
appendHelp(argument.argumentHelp)
appendHelp(argument.getArgumentHelp(command.currentContext))
appendLine()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,43 @@ abstract class CliktCommand(
*/
val commandName: String = name ?: inferCommandName()

@Deprecated("Use commandHelp method instead")
open val commandHelp: String = help

/**
* The help text for this command.
*
* You can set this by passing `help` to the [CliktCommand] constructor, or by overriding this
* property.
*/
open val commandHelp: String = help
* method if you need to build the string lazily or access the terminal or context.
*
* ### Example:
*
* ```
* class Command: CliktCommand() {
* override fun commandHelp(context: Context): String {
* return context.theme.info("This is a command")
* }
* }
* ```
*/
open fun commandHelp(context: Context): String {
@Suppress("DEPRECATION")
return commandHelp
}

@Deprecated("Use commandHelpEpilog method instead")
open val commandHelpEpilog: String = epilog

/**
* Help text to display at the end of the help output, after any parameters.
*
* You can set this by passing `epilog` to the [CliktCommand] constructor, or by overriding this
* property.
* method.
*/
open val commandHelpEpilog: String = epilog
open fun commandHelpEpilog(context: Context): String {
@Suppress("DEPRECATION")
return commandHelpEpilog
}

internal var _subcommands: List<CliktCommand> = emptyList()
internal val _options: MutableList<Option> = mutableListOf()
Expand Down Expand Up @@ -151,11 +173,12 @@ abstract class CliktCommand(
_options.mapNotNull { it.parameterHelp(currentContext) },
_arguments.mapNotNull { it.parameterHelp(currentContext) },
_groups.mapNotNull { it.parameterHelp(currentContext) },
_subcommands.mapNotNull {
when {
it.hidden -> null
else -> ParameterHelp.Subcommand(it.commandName, it.shortHelp(), it.helpTags)
}
_subcommands.mapNotNull { c ->
ParameterHelp.Subcommand(
c.commandName,
c.shortHelp(currentContext),
c.helpTags
).takeUnless { c.hidden }
}
).flatten()
}
Expand Down Expand Up @@ -197,8 +220,8 @@ abstract class CliktCommand(
}

/** The help displayed in the commands list when this command is used as a subcommand. */
protected fun shortHelp(): String =
Regex("""\s*(?:```)?\s*(.+)""").find(commandHelp)?.groups?.get(1)?.value ?: ""
protected fun shortHelp(context: Context): String =
Regex("""\s*(?:```)?\s*(.+)""").find(commandHelp(context))?.groups?.get(1)?.value ?: ""

/** The names of all direct children of this command */
fun registeredSubcommandNames(): List<String> = _subcommands.map { it.commandName }
Expand Down Expand Up @@ -290,8 +313,8 @@ abstract class CliktCommand(
val programName = cmd.getCommandNameWithParents()
return ctx.helpFormatter(ctx).formatHelp(
error as? UsageError,
cmd.commandHelp,
cmd.commandHelpEpilog,
cmd.commandHelp(ctx),
cmd.commandHelpEpilog(ctx),
cmd.allHelpParams(),
programName
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.github.ajalt.clikt.output.MordantHelpFormatter
import com.github.ajalt.clikt.output.defaultLocalization
import com.github.ajalt.clikt.sources.ChainedValueSource
import com.github.ajalt.clikt.sources.ValueSource
import com.github.ajalt.mordant.rendering.Theme
import com.github.ajalt.mordant.terminal.Terminal
import kotlin.properties.ReadOnlyProperty

Expand Down Expand Up @@ -366,6 +367,9 @@ inline fun <reified T : Any> CliktCommand.findOrSetObject(crossinline default: (
return ReadOnlyProperty { thisRef, _ -> thisRef.currentContext.findOrSetObject(default) }
}

/** The current terminal's theme */
val Context.theme : Theme get() = terminal.theme

private val DEFAULT_CORRECTION_SUGGESTOR: TypoSuggestor = { enteredValue, possibleValues ->
possibleValues.map { it to jaroWinklerSimilarity(enteredValue, it) }
.filter { it.second > 0.8 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import com.github.ajalt.clikt.output.HelpFormatter
import com.github.ajalt.clikt.output.HelpFormatter.ParameterHelp
import com.github.ajalt.clikt.parameters.internal.NullableLateinit
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.transform.HelpTransformContext
import com.github.ajalt.clikt.parameters.transform.TransformContext
import com.github.ajalt.clikt.parameters.transform.message
import com.github.ajalt.clikt.parameters.types.int
import kotlin.jvm.JvmOverloads
import kotlin.properties.PropertyDelegateProvider
Expand Down Expand Up @@ -37,7 +39,7 @@ interface Argument {
*
* It's usually better to leave this null and describe options in the usage line of the command instead.
*/
val argumentHelp: String
fun getArgumentHelp(context: Context): String

/** Extra information about this argument to pass to the help formatter. */
val helpTags: Map<String, String>
Expand Down Expand Up @@ -87,7 +89,6 @@ class ArgumentTransformContext(
override val context: Context,
) : Argument by argument, TransformContext {
override fun fail(message: String): Nothing = throw BadParameterValue(message, argument)
override fun message(message: String) = context.command.issueMessage(message)

/** If [value] is false, call [fail] with the output of [lazyMessage] */
inline fun require(value: Boolean, lazyMessage: () -> String = { "" }) {
Expand All @@ -111,6 +112,9 @@ typealias ArgValidator<AllT> = ArgumentTransformContext.(AllT) -> Unit
* An [Argument] delegate implementation that transforms its values .
*/
interface ProcessedArgument<AllT, ValueT> : ArgumentDelegate<AllT> {
/** A block that will return the help text for this argument, or `null` if no getter has been specified */
val helpGetter: (HelpTransformContext.() -> String)?

/** Called in [finalize] to transform each value provided to the argument. */
val transformValue: ArgValueTransformer<ValueT>

Expand All @@ -131,7 +135,7 @@ interface ProcessedArgument<AllT, ValueT> : ArgumentDelegate<AllT> {
name: String = this.name,
nvalues: Int = this.nvalues,
required: Boolean = this.required,
help: String = this.argumentHelp,
helpGetter: (HelpTransformContext.() -> String)? = this.helpGetter,
helpTags: Map<String, String> = this.helpTags,
completionCandidates: CompletionCandidates? = explicitCompletionCandidates,
): ProcessedArgument<AllT, ValueT>
Expand All @@ -142,7 +146,7 @@ interface ProcessedArgument<AllT, ValueT> : ArgumentDelegate<AllT> {
name: String = this.name,
nvalues: Int = this.nvalues,
required: Boolean = this.required,
help: String = this.argumentHelp,
helpGetter: (HelpTransformContext.() -> String)? = this.helpGetter,
helpTags: Map<String, String> = this.helpTags,
completionCandidates: CompletionCandidates? = explicitCompletionCandidates,
): ProcessedArgument<AllT, ValueT>
Expand All @@ -152,7 +156,7 @@ class ProcessedArgumentImpl<AllT, ValueT> internal constructor(
override var name: String,
override val nvalues: Int,
override val required: Boolean,
override val argumentHelp: String,
override val helpGetter: (HelpTransformContext.() -> String)?,
override val helpTags: Map<String, String>,
override val explicitCompletionCandidates: CompletionCandidates?,
override val transformValue: ArgValueTransformer<ValueT>,
Expand All @@ -163,12 +167,23 @@ class ProcessedArgumentImpl<AllT, ValueT> internal constructor(
require(nvalues != 0) { "Arguments cannot have nvalues == 0" }
}

override fun getArgumentHelp(context: Context): String {
return helpGetter?.invoke(HelpTransformContext(context)) ?: ""
}

override var value: AllT by NullableLateinit("Cannot read from argument delegate before parsing command line")
override val completionCandidates: CompletionCandidates
get() = explicitCompletionCandidates ?: CompletionCandidates.None

override fun parameterHelp(context: Context) =
ParameterHelp.Argument(name, argumentHelp, required || nvalues > 1, nvalues < 0, helpTags)
override fun parameterHelp(context: Context): ParameterHelp.Argument {
return ParameterHelp.Argument(
name = name,
help = getArgumentHelp(context),
required = required || nvalues > 1,
repeatable = nvalues < 0,
tags = helpTags
)
}

override fun getValue(thisRef: CliktCommand, property: KProperty<*>): AllT = value

Expand Down Expand Up @@ -196,15 +211,15 @@ class ProcessedArgumentImpl<AllT, ValueT> internal constructor(
name: String,
nvalues: Int,
required: Boolean,
help: String,
helpGetter: (HelpTransformContext.() -> String)?,
helpTags: Map<String, String>,
completionCandidates: CompletionCandidates?,
): ProcessedArgument<AllT, ValueT> {
return ProcessedArgumentImpl(
name = name,
nvalues = nvalues,
required = required,
argumentHelp = help,
helpGetter = helpGetter,
helpTags = helpTags,
explicitCompletionCandidates = completionCandidates,
transformValue = transformValue,
Expand All @@ -219,15 +234,15 @@ class ProcessedArgumentImpl<AllT, ValueT> internal constructor(
name: String,
nvalues: Int,
required: Boolean,
help: String,
helpGetter: (HelpTransformContext.() -> String)?,
helpTags: Map<String, String>,
completionCandidates: CompletionCandidates?,
): ProcessedArgument<AllT, ValueT> {
return ProcessedArgumentImpl(
name = name,
nvalues = nvalues,
required = required,
argumentHelp = help,
helpGetter = helpGetter,
helpTags = helpTags,
explicitCompletionCandidates = completionCandidates,
transformValue = transformValue,
Expand Down Expand Up @@ -267,7 +282,7 @@ fun CliktCommand.argument(
name = name,
nvalues = 1,
required = true,
argumentHelp = help,
helpGetter = { help },
helpTags = helpTags,
explicitCompletionCandidates = completionCandidates,
transformValue = { it },
Expand All @@ -279,9 +294,12 @@ fun CliktCommand.argument(
/**
* Set the help for this argument.
*
* Although you would normally pass the help string as an argument to [argument], this function
* Although you can also pass the help string as an argument to [argument], this function
* can be more convenient for long help strings.
*
* If you want to control the help string lazily or based on the context, you can pass a lambda that
* returns a string.
*
* ### Example:
*
* ```
Expand All @@ -290,10 +308,32 @@ fun CliktCommand.argument(
* .help("This is an argument that takes a number")
* ```
*/
fun <AllT, ValueT> ProcessedArgument<AllT, ValueT>.help(help: String): ProcessedArgument<AllT, ValueT> {
return copy(help = help)
fun <AllT, ValueT> ProcessedArgument<AllT, ValueT>.help(
help: String,
): ProcessedArgument<AllT, ValueT> {
return help { help }
}

/**
* Set the help for this argument lazily.
*
* You have access to the current Context if you need the theme or other information.
*
* ### Example:
*
* ```
* val number by argument()
* .int()
* .help { theme.info("This is an argument that takes a number") }
* ```
*/
fun <AllT, ValueT> ProcessedArgument<AllT, ValueT>.help(
help: HelpTransformContext.() -> String,
): ProcessedArgument<AllT, ValueT> {
return copy(helpGetter = help)
}


/**
* Transform all values to the final argument type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.mpp.isLetterOrDigit
import com.github.ajalt.clikt.output.HelpFormatter
import com.github.ajalt.clikt.parameters.transform.message
import com.github.ajalt.clikt.parsers.Invocation
import com.github.ajalt.clikt.sources.ValueSource
import kotlin.properties.PropertyDelegateProvider
Expand All @@ -20,7 +21,7 @@ interface Option {
fun metavar(context: Context): String?

/** The description of this option, usually a single line. */
val optionHelp: String
fun optionHelp(context: Context): String

/** The names that can be used to invoke this option. They must start with a punctuation character. */
val names: Set<String>
Expand Down Expand Up @@ -59,7 +60,7 @@ interface Option {
names,
secondaryNames,
metavar(context),
optionHelp,
optionHelp(context),
nvalues,
helpTags,
acceptsNumberValueWithoutName,
Expand Down Expand Up @@ -135,26 +136,6 @@ internal fun splitOptionPrefix(name: String): Pair<String, String> =
else -> name.substring(0, 1) to name.substring(1)
}

internal fun <EachT, AllT> deprecationTransformer(
message: String? = "",
error: Boolean = false,
transformAll: AllTransformer<EachT, AllT>,
): AllTransformer<EachT, AllT> = {
if (it.isNotEmpty()) {
val msg = when (message) {
null -> ""
"" -> "${if (error) "ERROR" else "WARNING"}: option ${option.longestName()} is deprecated"
else -> message
}
if (error) {
throw CliktError(msg)
} else if (message != null) {
message(msg)
}
}
transformAll(it)
}

@PublishedApi
internal fun Option.longestName(): String? = names.maxByOrNull { it.length }

Expand Down

0 comments on commit 282d184

Please sign in to comment.