More nested meta help page issues
I tried the latest release and found:
- The help page for
metacommands don't show the parameters frommeta.meta.default. -
meta.metacommands show the parameters frommeta.default. - Non-meta commands except
app.defaultdon't show the parameters frommeta.meta.default.
Hey @JuneStepp!
Can you explain your use-case a little more? I'm sure there are some meta-meta consistency/bugs, but I'm also a bit hesitant if the fixes make the code significantly more complicated. Is there a way to restructure your app to avoid these structures?
The help page for meta commands don't show the parameters from meta.meta.default.
If i'm interpreting this correctly, this is intended. The relationship is that meta calls it's parent, so the parent doesn't care/concern itself about the meta. By extension, app.meta doesn't do anything with app.meta.meta.
Can you explain your use-case a little more? I'm sure there are some meta-meta consistency/bugs, but I'm also a bit hesitant if the fixes make the code significantly more complicated. Is there a way to restructure your app to avoid these structures?
I explained this trouble in https://github.com/BrianPugh/cyclopts/issues/641#issuecomment-3489180169. Every level of a parameter's validation depending on another parameter requires another meta app. In my application that has various games registered to it, I first need to get where the program config directory is in meta.meta.default, then handle program options and selecting a game in meta.default, then finally handle game specific options in app.default. I attach commands that don't depend on the program config to meta.meta and ones that don't depend on a game being selected to meta.
The help page for meta commands don't show the parameters from meta.meta.default.
If i'm interpreting this correctly, this is intended. The relationship is that meta calls it's parent, so the parent doesn't care/concern itself about the meta. By extension, app.meta doesn't do anything with app.meta.meta.
When an app.meta command is called, app.meta.meta.default runs first, so the parameters for it should be in the help page, no?
First things first, I merged in #689 as I ran into an ergonomics issue when typing up the example below. I'll release this soon, but as of right now please use main for playing around with the example below.
Every level of a parameter's validation depending on another parameter requires another meta app.
Have you looked into Groups? I suspect what you are trying to achieve is best accomplished through Group validators. I admit, the documentation there is not very great (and I'd appreciate some help there once we resolve this issue!).
To make the conversation a little more concrete, here's a relatively small minimum-working-example that probably has strong resemblance to what you want:
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated
import cyclopts
from cyclopts import App, Group, Parameter
from cyclopts.validators._group import MutuallyExclusive
app = App(
name="launcher",
default_parameter=Parameter(negative=""),
)
def _credential_validator(argument_collection):
# Filter to only arguments that have values (were provided by the user)
populated = argument_collection.filter_by(value_set=True)
# Check if both token and username/password were provided
if "--token" in populated and ("--username" in populated or "--password" in populated):
raise ValueError("Cannot supply both TOKEN and USERNAME/PASSWORD.")
_credential_group = Group(name="Credentials", validator=_credential_validator)
cheats = Group(name="Cheats") # Just for organizing
mods = Group(name="Mods") # Just for organizing
@Parameter(name="*", show_default=False)
@dataclass(kw_only=True)
class Credentials:
username: Annotated[str, Parameter(alias="-u", group=_credential_group)] = ""
"""Username to login."""
password: Annotated[str, Parameter(alias="-p", group=_credential_group)] = ""
"""Password for username."""
token: Annotated[str, Parameter(alias="-t", group=_credential_group)] = ""
"""Authentication token (instead of username/password)."""
@app.command(group="Games")
def mario(
*,
credentials: Credentials | None = None,
infinite_lives: Annotated[bool, Parameter(group=cheats)] = False,
):
"""An Italian plumber stomping on some Koopas.
Parameters
----------
infinite_lives: bool
Give mario an infinite number of lives.
"""
print(f"{credentials=}")
print(f"{infinite_lives=}")
_mutually_exclusive_health = Group(validator=MutuallyExclusive())
@app.command(group="Games")
def zelda(
*,
credentials: Credentials | None = None,
infinite_health: Annotated[bool, Parameter(group=(cheats, _mutually_exclusive_health))] = False,
one_hit_ko: Annotated[bool, Parameter(group=(mods, _mutually_exclusive_health))],
):
"""The adventure awaits our hero of Hyrule.
Parameters
----------
infinite_health: bool
Give the player infinite health.
one_hit_ko: bool
Taking damage causes instant death.
"""
print(f"{credentials=}")
print(f"{infinite_health=}")
@app.meta.default
def meta(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
config: Path = Path("config.yaml"),
):
"""Customized game launcher.
Parameters
----------
config: Path
Path to YAML configuration file.
"""
app.config = cyclopts.config.Yaml(config, search_parents=True)
app(tokens)
if __name__ == "__main__":
app.meta()
Please play around with this and let me know what you think! We can use this example as a concrete way to experiment with logic that we want to implement.