02.11.03 — Auto CLI store + flags + validation#

Fichier source : src/ocarina/opinionated/cli/selenium/create_cli_store.py

Flags#

FlagDefaultTypeValidation
--driver-path""stris_file (sauf --browser safari)
--profile-pathNonestris_none OR is_dir
--browserNone (requis)strchoices argparse (par OS)
--not-headlessFalse (= headless par défaut)bool (store_true)phantom
--workers5intis_positive + is_not_zero
--logger"terminal+file"strchoices=LOGGERS_CHOICES
--wait-timeout10int≤ 60, > 0
--dont-force-delete-tmp-dirsFalsebool (store_true)phantom
--only[]list[str] (nargs="+")phantom + mutex
--exclude[]list[str] (nargs="+")phantom + mutex
_DEFAULT_WORKERS_AMOUNT = 5
_DEFAULT_LOGGER: SupportedLogger = "terminal+file"
_DEFAULT_BROWSER_AUTOMATION_TIMEOUT = 10
_MAX_BROWSER_AUTOMATION_TIMEOUT = 60

Literal[...]#

type SeleniumCliStoreKeys = Literal[
    "driver_path", "profile_path", "browser", "headless", "workers",
    "logger", "wait_timeout", "force_delete_tmp_dirs", "only", "exclude",
]

Ces clés sont l’unique vocabulaire du store. Les call-sites les utilisent : store.get("workers"), store.get("browser"), etc.

Note : "headless" (positif) vs --not-headless (flag négatif) ; idem "force_delete_tmp_dirs" (positif) vs --dont-force-delete-tmp-dirs (flag négatif). C’est volontaire : le store contient la valeur positive (« est-on headless ? oui/non »), le flag CLI utilise la formulation négative (parce que les défauts sont « oui »).

Effets post-parse#

Le effects_factory=lambda ns: (...) produit une collection Effects (tuple d’Effect).

1. Set#

lambda: store.set("driver_path", ns.driver_path),
lambda: store.set("profile_path", ns.profile_path),
lambda: store.set("browser", ns.browser),
lambda: store.set("headless", not ns.not_headless),         # ← inversion
lambda: store.set("workers", ns.workers),
lambda: store.set("logger", ns.logger),
lambda: store.set("wait_timeout", ns.wait_timeout),
lambda: store.set("force_delete_tmp_dirs", not ns.dont_force_delete_tmp_dirs),  # ← inversion
lambda: store.set("only", tuple(ns.only)),
lambda: store.set("exclude", tuple(ns.exclude)),

Notes :

  • Inversions sur headless et force_delete_tmp_dirs : on stocke la valeur positive.
  • tuple(...) sur only/exclude : on convertit en tuple immuable.

2. “create validate only exclude mutex effect”#

def _create_validate_only_exclude_mutex_effect(ns: Namespace) -> Effect:
    def _validate() -> None:
        if ns.only and ns.exclude:
            raise ValueError("--only and --exclude cannot be used together")
    return _validate

Validation inter-args. Pas exprimable par argparse natif ; on le fait en effet post-parse.

3. “create validate dependent args effect”#

def _validate_non_safari() -> None:
    is_unset: dict[str, bool] = {
        "--driver-path": ns.driver_path is None and ns.driver_path != "",
        "--browser": ns.browser is None,
    }
    all_explicit = all(not v for v in is_unset.values())
    if not all_explicit:
        raise ValueError("These parameters must all be specified:\n" + ...)


def _validate_safari() -> None:
    forbidden: dict[str, bool] = {
        "--driver-path": ns.driver_path is not None and ns.driver_path != "",
        "--profile-path": ns.profile_path is not None,
    }
    specified = [k for k, v in forbidden.items() if v]
    if specified:
        raise ValueError(
            "Safari uses the native macOS safaridriver"
            " — these arguments are not supported:\n"
            + "\n".join(f"  • {p}" for p in specified)
        )


def _validate() -> None:
    if ns.browser == "safari":
        _validate_safari()
    else:
        _validate_non_safari()

Validation conditionnelle :

  • Si safari : refus de --driver-path et --profile-path.
  • Sinon : exige --driver-path et --browser.

4. “create validate driver path”#

def _validate_driver_path() -> None:
    if ns.browser != "safari":
        validate(ns.driver_path).assert_that(
            is_file, msg="--driver-path should be a path to a file"
        ).execute().raise_if_invalid()

Si non-Safari : --driver-path doit être un fichier existant.

5. “create dont force delete tmp dirs effect”#

def unwrapped() -> None:
    if not dont_force_delete_tmp_dirs and platform.system() == "Windows":
        atexit.register(_clean_all_webdriver_tmp_dirs)

Si on est sur Windows et qu’on n’a pas désactivé : enregistre un atexit qui nettoie tmp*\webdriver-py-profilecopy à la sortie du process.

Dispatch#

def create_selenium_auto_cli_store() -> CliStore[SeleniumCliStoreKeys]:
    match platform.system():
        case "Windows":
            return create_selenium_win_cli_store()
        case "Darwin":
            return create_selenium_macos_cli_store()
        case "Linux":
            return create_selenium_linux_cli_store()
        case other:
            raise RuntimeError(f"Unsupported platform: {other}")

*_cli_store per-OS#

_WIN_BROWSER_CHOICES: list[SupportedSeleniumBrowser] = ["chrome", "firefox", "edge"]
_MACOS_BROWSER_CHOICES: list[SupportedSeleniumBrowser] = ["chrome", "firefox", "safari"]
_LINUX_BROWSER_CHOICES: list[SupportedSeleniumBrowser] = ["chrome", "firefox"]
def create_selenium_win_cli_store() -> CliStore[SeleniumCliStoreKeys]:
    return _create_selenium_cli_store(_WIN_BROWSER_CHOICES)


def create_selenium_macos_cli_store() -> CliStore[SeleniumCliStoreKeys]:
    return _create_selenium_cli_store(_MACOS_BROWSER_CHOICES)


def create_selenium_linux_cli_store() -> CliStore[SeleniumCliStoreKeys]:
    return _create_selenium_cli_store(_LINUX_BROWSER_CHOICES)
OSBrowsers proposés
Windowschrome, firefox, edge
macOSchrome, firefox, safari
Linuxchrome, firefox

edge n’est pas proposé sur macOS / Linux. safari n’est proposé que sur macOS.

“create selenium cli store”#

def _create_selenium_cli_store(browser_choices: list[SupportedSeleniumBrowser]) -> CliStore[SeleniumCliStoreKeys]:
    store = _create_store()
    cli = _create_cli(store, browser_choices)
    cli.parse()
    return store
  1. _create_store() : crée le CliStore[SeleniumCliStoreKeys] avec ses _CliField et leurs validators.
  2. _create_cli(store, browser_choices) : crée le CliBuilder avec ses CliArg et son effects_factory.
  3. cli.parse() : déclenche le parse + validation + effets. Si invalide, le process quitte avec code 2.
  4. Retourne le store, désormais rempli.

Cram Tests associés#

tests/cram/ :

Fichier .tVérifie
cli_help.t--help affiche les flags
cli_happy_path.tCombinaison valide produit la bonne sortie
cli_defaults.tDefaults parsés correctement
cli_invalid_browser.t--browser=banana lève proprement
cli_invalid_wait_timeout.t--wait-timeout=0 lève (is_not_zero)
cli_missing_driver_path.t--browser=chrome sans --driver-path lève
cli_not_headless_flag.t--not-headless inverse le default
cli_only_flag.t--only id1 id2 parse une liste
cli_exclude_flag.tid.
cli_only_exclude_mutex.tLes deux ensemble lève

Cf. ../../04-internal-tests/02-cram-prysk.md