Skip to content

Add ability to retrieve list of possible options for Option property#647

Merged
natecook1000 merged 15 commits into
apple:mainfrom
bripeticca:enumerableoption
Sep 30, 2024
Merged

Add ability to retrieve list of possible options for Option property#647
natecook1000 merged 15 commits into
apple:mainfrom
bripeticca:enumerableoption

Conversation

@bripeticca
Copy link
Copy Markdown
Contributor

@bripeticca bripeticca commented Jun 5, 2024

Previously, information regarding a custom Option type's possible values was lost when generating subsequent structures in the pipeline to describe the argument. This would largely have to be defined by the user in the discussion string property in ArgumentHelp and would provide no mechanism to directly access the allowable values for a custom Option.

This adds an extra static property on ExpressibleByArgument labelled defaultValueDescriptions, which maps the value name to its description. It is up to the user to implement the defaultValueDescription for their custom CaseIterable type to fully support this behaviour. Further refactoring has been made downstream to the ArgumentDefinition and subsequent structures describing the argument to allow the ability to retrieve the value descriptions from the ExpressibleByArgument type, making information about the property more visible and accessible.

A new enumeration ArgumentDiscussion has been introduced to better encapsulate the discussion information for an argument, and includes cases for discussion sections that contain only static text, as well as cases that include a list of argument values that derive from a type that implements ExpressibleByArgument, CaseIterable and RawRepresentable.

Addresses #637

Checklist

  • I've added at least one test that validates that my change is working, if appropriate
  • I've followed the code style of the rest of the project
  • I've read the Contribution Guidelines
  • I've updated the documentation if necessary

An EnumerableOption protocol has been created to allow users to specify
help/discussion information on a custom Option type itself. This
information can be accessed further down the pipeline, which permits the
retrieval of all the option types and their descriptions for dump/help
generation.

Note that this has yet to be formatted and is currently WIP.
@bripeticca bripeticca marked this pull request as draft June 5, 2024 18:54
Comment thread Package.swift Outdated
Comment thread Sources/ArgumentParser/Parsable Properties/Option.swift Outdated
Comment thread Sources/ArgumentParser/Parsable Properties/Option.swift Outdated
Comment thread Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift Outdated
Comment thread Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift
Comment thread Sources/ArgumentParser/Parsing/ArgumentDefinition.swift Outdated
Comment thread Sources/ArgumentParser/Usage/DumpHelpGenerator.swift Outdated
Comment thread Sources/ArgumentParser/Usage/HelpGenerator.swift Outdated
Comment thread Sources/ArgumentParser/Usage/HelpGenerator.swift Outdated
Comment thread Sources/ArgumentParser/Usage/HelpGenerator.swift
Comment thread Sources/ArgumentParserToolInfo/ToolInfo.swift Outdated
Comment thread Sources/ArgumentParserToolInfo/ToolInfo.swift Outdated
Comment thread Sources/ArgumentParserToolInfo/ToolInfo.swift Outdated
Comment thread Tests/ArgumentParserUnitTests/DumpHelpGenerationTests.swift
Copy link
Copy Markdown
Contributor

@cmcgee1024 cmcgee1024 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks good to me. I've added thoughts and questions along the way. I don't feel comfortable enough with this repo to approve at this time.

* Pulled out redundant Discussion structs; to continue
* Make Discussion an optional property where applicable
* EnumerableOptionValue moved to its own file
* Added tests to appropriate files
* Refactored to fit repo code formatting; to continue

This commit largely addresses comments made to the
previous commit. Some aspsects are still in progress.
Created an extension to Option that handles optional
EnumerableOptionValue types within its initializers.
* ArgumentDiscussion struct represents a standardized way to
  define an argument's discussion block, handling both the
  static block of text as well as a list of EnumerableOptionValues
* Update Option initializers to better handle cases where the type
  implements `EnumerableOptionValue`.
* Allow users to specify a discussion block alongside their
  EnumerableOptionValue types, which will print out a preamble
  block of text before the list of option values and descriptions.
* Support the Discussion struct in the Single/Multi page
  manual generation
* Refactor the encoding method for Discussion; generates
  a list of objects containing 'value' and 'description'
  properties in the resulting JSON, keeping the order
  of the properties consistent across runs.
@bripeticca bripeticca changed the title [WIP] Add ability to retrieve list of possible options for Option property Add ability to retrieve list of possible options for Option property Jun 19, 2024
@bripeticca bripeticca marked this pull request as ready for review June 19, 2024 19:56
@bripeticca bripeticca requested a review from natecook1000 June 27, 2024 19:36
Copy link
Copy Markdown
Member

@natecook1000 natecook1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for looking at this, @bripeticca! The updated help screens look really clear, and it's great that adoption is all opt in. I think we can cut back on the new API a little bit – if you agree, we just have a protocol name to discuss. Thanks!

/// Can include specific abstracts about the argument's possible values (e.g.
/// for a custom `EnumerableOptionValue` type), or can describe
/// a static block of text that extends the description of the argument.
public var discussion: ArgumentDiscussion?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like the right place for a property this type, since a command generally contains a group of flags, options, and arguments. Can you say more about why CommandConfiguration should include an ArgumentDiscussion?

Separately, changing the type of discussion is source breaking – we'll need a different strategy if we do add this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point! I had changed this as I was making my way through the DumpHelpGenerator and updating the inits for CommandInfoV0, ArgumentInfoV0, etc. to consider ArgumentDiscussion. Along the way, I updated each struct that contained a discussion string to instead use ArgumentDiscussion - it seems like for CommandConfiguration this is unnecessary though! Since it's a breaking change, I can revert.

usage: String? = nil,
discussion: String = "",
discussion: String? = nil,
options: (any EnumerableOptionValue.Type)? = nil,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note as above, here.

///
/// In any case where the argument type is not `EnumerableOptionValue`, the default implementation
/// will use the `.staticText` case and will print a block of discussion text.
public enum ArgumentDiscussion {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't use this in CommandConfiguration, it also looks like it doesn't need to be public.

/// }
/// }
/// ```
public protocol EnumerableOptionValue: CaseIterable, ExpressibleByArgument, RawRepresentable where RawValue: ExpressibleByArgument, AllCases == [Self] {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tricky protocol to name – I'd like to make sure we leave room for adopting this named-value feature for arguments, as well. This command type should be able to have the nice value-based help text for the color argument:

struct Example: ParsableCommand {
    @Argument var color: Color
}

We don't need to land @Argument support for this, but we should be able to make room for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great point! The naming is definitely tricky :) Perhaps something a little more generic like EnumerableValue could be used instead?

parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where Value: EnumerableOptionValue {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Value.self ends up getting erased into (any EnumerableOptionValue)? anyway, I wonder if we can skip all these overloads and just try casting the type in the regular ExpressibleByArgument initializers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure - I just tried this out locally by adding the following to the beginning of each ExpressibleByArgument init:

var help = help
if let type = Value.self as? (any EnumerableOptionValue.Type) {
  help = ArgumentHelp.init(
  help?.abstract ?? "",
  discussion: help?.discussion,
  options: type, // retaining necessary type information for enumerable values here
  valueName: help?.valueName,
  visibility: help?.visibility ?? .default
  )
}

This way we can still retain the modified ArgumentHelp for types implementing EnumerableOptionValue (or whatever we decide to rename it to!).

Alternatively, I can modify the ArgumentHelp init to take the options parameter and have it be optional, and can remove some code so it comes down to this:

// ArgumentHelp init:
public init(
  _ abstract: String = "",
  discussion: String? = nil,
  valueName: String? = nil,
  options: (any EnumerableOptionValue.Type)? = nil, // made this optional
  visibility: ArgumentVisibility = .default)
{
  self.abstract = abstract
  self.discussion = discussion
  self.valueName = valueName
  self.options = options
  self.visibility = visibility
} 

and the resulting ExpressibleByArgument inits would have some additional code that looks something like this:

self.init(_parsedValue: .init { key in
  let arg = ArgumentDefinition(
    container: Bare<Value>.self,
    key: key,
    kind: .name(key: key, specification: name),
    help: .init(
      help?.abstract ?? "",
      discussion: help?.discussion,
      valueName: help?.valueName,
      options: Value.self as? (any EnumerableOptionValue.Type),
      visibility: help?.visibility ?? .default
    ),
    parsingStrategy: parsingStrategy.base,
    initial: wrappedValue,
    completion: completion)

  return ArgumentSet(arg)
})

If this seems good to you, I can go ahead and change it!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay here! I've been wrestling with whether we can avoid the EnumerableOptionValue protocol altogether, since it seems to be a refinement of the ExpressibleByArgument protocol that we're already handling for a couple slightly different uses. In particular, adding features like this one to the library is challenging in that we need to design a source-compatible addition that makes some kind of intuitive sense with the features that are already present.

The most related existing feature supported by the ExpressibleByArgument protocol is the ability to automatically include value names in the help description when a type is CaseIterable. Since the change in this PR kind of augments that to include descriptions as well as value names, I think we can piggy back on that existing design.

In particular, what would you think about adding another customization point to ExpressibleByArgument:

public protocol ExpressibleByArgument {
  // ...other members...
  
  // Existing static var for value strings:

  /// An array of all possible strings that can convert to a value of this
  /// type, for display in the help screen.
  ///
  /// The default implementation of this property returns an empty array. If the
  /// conforming type is also `CaseIterable`, the default implementation returns
  /// an array with a value for each case.
  static var allValueStrings: [String] { get }

  // New static var for value descriptions:
  
  static var allValueDescriptions: [String: String] { get }
}

We could provide a default implementation that returns an empty dictionary, and then any type that wants to provide a more expanded help screen could implement allValueDescriptions to provide corresponding descriptions for each element of the allValueStrings array. Internally, we can use similar machinery to capture those descriptions for use when displaying the help screen or generating the JSON help description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great idea! Thanks for the feedback - I tried to implement this locally and it seems to work well.

- Removes extra Option initializers by attempting a type cast
  to `EnumerableOptionValue` in ExpressibleByArgument inits
- Revert `CommandConfiguration` discussion property to String
- ArgumentDiscussion no longer public
Since `ExpressibleByArgument` already maintains a list of
enumerable values for an argument, we can extend this to serve
as an ordered list for a new dictionary property that maps the
value name to its description, if applicable. The new property
is a static variable on `ExpressibleByArgument` labelled
`allValueDescriptions`.

If the description string for a value is the same as the value
string, it's assumed that the description is not implemented.

This will replace the use of the EnumerableOption protocol.
@bripeticca
Copy link
Copy Markdown
Contributor Author

@swift-ci please test

Remove previous expectations for dump help test results.
@bripeticca
Copy link
Copy Markdown
Contributor Author

@swift-ci please test

@bripeticca
Copy link
Copy Markdown
Contributor Author

@swift-ci please test

Copy link
Copy Markdown
Member

@natecook1000 natecook1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking like the right API! Just a couple of nits and then a question about testing - should be good to go after that!

Comment thread Sources/ArgumentParser/Parsable Properties/ArgumentDiscussion.swift Outdated
Comment thread Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift Outdated
/// Can include specific abstracts about the argument's possible values (e.g.
/// for a custom `CaseIterable` type), or can describe
/// a static block of text that extends the description of the argument.
public var discussion: Discussion?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rauhul Do we need to update the tool info version for a change like this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this will need a version bump and will be source break. Thats not a deal breaker, but can be annoying.

Alternatively we can call this discussion2 and deprecate the old one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go ahead and rename the property in that case! Should this deprecation strategy also be applied to ArgumentInfoV0?

Comment thread Sources/ArgumentParser/Usage/HelpGenerator.swift
If an option case's label was too long for the description
to fit on the same line, it would omit the label entirely.

Append the label to the rendered help text with a newline if
the label is too long for the description to fit on the same
line.

Added test cases to address scenarios where the option cases
either have:
* a label that is too long for the description to fit on the
  same line
* a description that needs to be wrapped
* a label that is too long and a description that needs to be
  wrapped
* all of the above cases when an Option also has a discussion,
  which further indents the list of possible option values in
  the rendered help text.
@bripeticca
Copy link
Copy Markdown
Contributor Author

@swift-ci please test

--argument <argument> A collection of cases with varying lengths of
labels/descriptions.
This discussion should make the list of options wrap differently.
Values:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a real need for "Values:"? I dont think I like the difference in indentation with/without a "discussion"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No real need, I can change it!

Section(title: "description") {
if let discussion = command.discussion {
discussion
if case let .staticText(text) = discussion {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we factor this body out into a Discussion type and use it the 4 places changed here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a DiscussionText struct to the project that encapsulates this logic!

* Removed extra 'Values:' text in enumerated argument discussion
* DiscussionText struct MDocComponent created to wrap discussion
  component creation
* Apply deprecation strategy to CommandInfoV0.discussion and
  ArgumentInfoV0.discussion properties - rename new property
  to discussion2
* Updated tests
@bripeticca
Copy link
Copy Markdown
Contributor Author

@swift-ci please test

@bripeticca bripeticca requested a review from rauhul September 13, 2024 20:36
var b: OptionValues?
}

func testEnumerableValuesWithPreamble() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im a little confused by how this list view interacts with the change introduced in #594. Do you have a test that covers both features together?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add a test that demonstrates the differences between both of these features!

The idea is that if the type for the @Option property is an enum that implements ExpressibleByArgument and if the enum also implements the defaultValueDescription property for its cases, then the generated help text will enumerate each of the enum's values with the description defined in defaultValueDescription.

For example:

enum OptionEnumerated: String, CaseIterable, ExpressibleByArgument { 
    case one
    case two
    case three

    public var defaultValueDescription: String { 
        switch self { 
            case .one:
                return "The number one."
            case .two:
                return "The number two."
            case .three:
                return "The number three."
        }
    }
}

struct MyCommand: ParsableCommand { 
    @Option(help: "The values for this option should be enumerated with a description.")
    var list: OptionEnumerated
}

// Example generated help text:
...
--list <list>         The values for this option should be enumerated with a description.
      one              - The number one.
      two              - The number two.
      three            - The number three.
...

Otherwise if this property is not implemented in the enum, then it will generate the help text like so:

enum OptionWithoutEnumeratedHelpText: String, CaseIterable, ExpressibleByArgument { 
    case one = "1"
    case two = "2"
    case three = "3"
}

struct MyOtherCommand: ParsableCommand { 
    @Option(help: "This is an option without explicit enumeration in the help text.")
    var values: OptionWithoutEnumeratedHelpText
}

// Example generated help text:
...
 --values <values>       This is an option without explicit enumeration in the
                                       help text. (values: 1, 2, 3)
...

A new test has been implemented to showcase the difference
between the enumerated help descriptions feature and the
default generated help text for options with multiple
possible values.
@bripeticca
Copy link
Copy Markdown
Contributor Author

@swift-ci please test

@bripeticca
Copy link
Copy Markdown
Contributor Author

@swift-ci please test

@bripeticca bripeticca requested a review from rauhul September 17, 2024 20:55
Copy link
Copy Markdown
Collaborator

@rauhul rauhul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty happy with this change! If @natecook1000 is happy with it, we can merge :)

@natecook1000
Copy link
Copy Markdown
Member

@swift-ci Please test

@natecook1000
Copy link
Copy Markdown
Member

@swift-ci Please test

@natecook1000
Copy link
Copy Markdown
Member

Thanks so much for your patience on this, @bripeticca! 🎉🎉🎉

@natecook1000 natecook1000 merged commit 83c5134 into apple:main Sep 30, 2024
rauhul added a commit that referenced this pull request Feb 4, 2025
Cleans up some lingering odds and ends from #647. Specifically switches
CommandInfoV0 to use the old style description and updates
ArgumentInfov0 to emit value descriptions as a separate dictionary.
rauhul added a commit that referenced this pull request Feb 4, 2025
Cleans up some lingering odds and ends from #647. Specifically switches
CommandInfoV0 to use the old style description and updates
ArgumentInfov0 to emit value descriptions as a separate dictionary.
rauhul added a commit that referenced this pull request Feb 4, 2025
Cleans up some lingering odds and ends from #647. Specifically switches
CommandInfoV0 to use the old style description and updates
ArgumentInfov0 to emit value descriptions as a separate dictionary.
rauhul added a commit that referenced this pull request Feb 5, 2025
Cleans up some lingering odds and ends from #647. Specifically switches
CommandInfoV0 to use the old style description and updates
ArgumentInfoV0 to emit value descriptions as a separate dictionary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants