tests package

Subpackages

Submodules

tests.conftest module

Fixtures, configuration and helpers for tests.

tests.conftest.httpserver_listen_address()[source]

Bind the local HTTP server to the loopback address, not to a name.

Overrides pytest-httpserver’s default of ("localhost", 0). Resolving that name is the one thing the tests/test_config.py cases serving a configuration file over HTTP need from the host, and a build sandbox is exactly where it is unavailable: the Nix one on macOS denies the lookup, so every one of them errors out with socket.gaierror: [Errno 8] nodename nor servname provided, or not known.

Binding the literal address asks nothing of the resolver. Whether a sandbox additionally gates the loopback socket is its own policy, and a separate question: this only removes the lookup that failed first.

tests.conftest.requires_process_table()[source]

Skip a test where the process table does not list the test’s own process.

The kill paths of run_cli find a command’s descendants in the process table. Linux reads it from /proc. Elsewhere it comes from ps, which a build sandbox can deny: the MacPorts test phase gets an empty listing, so the kill tests would fail on what the sandbox hides rather than on the code.

The probe runs ps itself instead of _posix_process_table(), so a regression that empties the table still fails those tests.

tests.conftest.skip_windows_colors = MarkDecorator(mark=Mark(name='skipif', args=(<extra_platforms.pytest._DeferredCondition object>,), kwargs={'reason': 'Click overstrip colors on Windows'}))

Skips color tests on Windows as click.testing.invoke overzealously strips colors.

See: - https://github.com/pallets/click/issues/2111 - https://github.com/pallets/click/issues/2110

tests.conftest.TRANSIENT_STATUS_CODES = frozenset({408, 425, 429, 500, 502, 503, 504})

Status codes a host answers with when it is refusing this request rather than reporting something about the resource: a rate limit, a proxy hiccup, or a service that is briefly down.

tests.conftest.fetch_or_skip(url, timeout=60)[source]

Fetch url, skipping the test when the failure says nothing about it.

A network-dependent test asserts something about what a host serves. It cannot assert it while the host is rate-limiting, timing out, or down, and a bare assert response.ok there reports assert False: a red run that looks like the finding the test exists to make, with nothing naming the cause. GitHub throttles anonymous archive downloads, so a full-suite run hits this on its own schedule and reads as an order-dependent flake.

A response that is about the resource still fails, loudly and with its status: a 404 means the URL these tests build no longer resolves, which is the finding, not the weather.

Return type:

Response

tests.conftest.walk_commands(command, ctx=None, path=())[source]

Yield every (path, command) pair under command, itself included.

path holds the subcommand names leading to the command, the root’s own name excluded, so joining it names the invocation a user would type.

Parameters:
  • command (Command) – the command to walk, a group or a leaf.

  • ctx (Context | None) – the context command is looked up in. Built from the command itself when omitted, which is what a walk starting at the root wants.

  • path (tuple[str, ...]) – the names already walked through, for the recursion.

Return type:

Iterator[tuple[tuple[str, ...], Command]]

class tests.conftest.TTYStringIO(initial_value='', newline='\n')[source]

Bases: StringIO

An in-memory text buffer that claims to be an interactive terminal.

isatty()[source]

Return whether this is an ‘interactive’ stream.

Return False if it can’t be determined.

Return type:

bool

class tests.conftest.StallingTTY[source]

Bases: TTYStringIO

A terminal whose writes block once stall is set, until release is.

Stands in for a terminal paused by flow control (Ctrl+S), or a full pipe.

write(text)[source]

Write string to file.

Returns the number of characters written, which is always equal to the length of the string.

Return type:

int

tests.conftest.wait_until(predicate, timeout=3.0)[source]

Poll predicate until it is true or timeout seconds elapse.

Lets thread-driven assertions wait for an outcome instead of sleeping a fixed (and racy) amount.

Return type:

bool

tests.test_accessibility module

tests.test_accessibility.report_cli()[source]

An extra command that echoes the resolved color flag and table format, then renders a table with a styled cell.

tests.test_accessibility.test_default_params_include_accessible()[source]
tests.test_accessibility.test_accessible_precedence(invoke, report_cli, extra_args, expected_color, expected_format)[source]
tests.test_accessibility.test_accessible_via_envvar(invoke, report_cli)[source]
tests.test_accessibility.test_cli_envvar_outranks_accessible_envvar(invoke, report_cli)[source]

The variable of one CLI is more specific than the machine-wide one.

tests.test_accessibility.test_accessible_envvar_outranks_configuration(invoke, report_cli, create_config, configured, envvar, expected_color, expected_format)[source]

Unlike the other machine-wide variables, ACCESSIBLE outranks a configuration file: the mode is settled before the file loads.

tests.test_accessibility.test_explicit_flag_overrides_accessible_envvar(invoke, report_cli)[source]
tests.test_accessibility.test_default_renders_box_drawing_and_ansi(invoke, report_cli)[source]
tests.test_accessibility.test_accessible_strips_box_drawing_and_ansi(invoke, report_cli)[source]
tests.test_accessibility.test_standalone_decorator_on_plain_click_command(invoke)[source]

--accessible lowers the defaults of the sibling color and table options, so they must be composed alongside it on a plain click.command.

tests.test_accessibility.test_accessible_flag_published_to_context(invoke, args, expected)[source]

set_accessible publishes the resolved intent at ctx.meta[ACCESSIBLE].

tests.test_accessibility.test_clear_is_noop_under_accessible(invoke, monkeypatch)[source]

clear() defers to click.clear normally, but no-ops under –accessible.

tests.test_accessibility.test_echo_via_pager_streams_plainly_under_accessible(invoke, monkeypatch)[source]

echo_via_pager pages normally, but writes straight to stdout under –accessible.

tests.test_accessibility.test_pager_and_clear_defer_without_accessible_option(invoke, monkeypatch)[source]

With no –accessible option wired, the wrappers defer to Click unchanged.

tests.test_ansi module

Tests for click_extra.ansi, the ANSI escape grammar and its decoders.

tests.test_ansi.test_open_ansi(text, expected)[source]
tests.test_ansi.test_open_ansi_resumes_from_an_opened_state(text, expected)[source]
tests.test_ansi.test_open_ansi_chunked_matches_one_shot()[source]

Reading a string in two chunks answers like reading it whole.

Every cut is tried except the ones splitting an escape in half, which no parser can carry across a chunk boundary.

tests.test_ansi.test_strip_ansi(text, expected)[source]
tests.test_ansi.test_strip_ansi_keeps_bytes_as_bytes()[source]
tests.test_ansi.test_nearest_256_quantization(r, g, b, expected)[source]

Verify RGB-to-256 quantization for representative values.

tests.test_blocks module

Test the dependency-light self-updating block toolkit.

tests.test_blocks.DEPRECATED_BLOCKS_NAMES = ('FenceSpan', 'fence_spans', 'iter_markdown_files', 'marker_res', 'region_markers', 'replace_region', 'rewrite_fenced_regions', 'skip_region', 'split_options', 'update_blocks')

Public names of the 9.x click_extra.blocks module that 10.x still defines.

tests.test_blocks.REGION = 'intro\n\n<!-- t -->\nOLD\n<!-- t-end -->\n\nend\n'

A minimal document carrying a single <!-- t --> / <!-- t-end --> region.

tests.test_blocks.test_blocks_module_needs_no_sphinx_extra()[source]

click_extra.markdown.blocks imports without pulling in the sphinx extra.

The whole point of hosting the toolkit outside click_extra.sphinx is that a release pipeline or standalone script can reuse it without installing Sphinx or docutils. Assert it in a fresh interpreter so an already-loaded Sphinx (from another test) cannot mask a regression.

tests.test_blocks.test_deprecated_blocks_alias_serves_9x_name(name)[source]

click_extra.blocks serves each name it kept from 9.x, with a warning.

tests.test_blocks.test_deprecated_blocks_alias_star_import()[source]

A star import binds each name the alias serves, with one warning per name.

tests.test_blocks.test_deprecated_blocks_alias_serves_no_other_name(name)[source]

A name the alias does not serve stays missing, and sets off no warning.

tests.test_blocks.test_marker_res_grammar()[source]

The open marker is bare; the close marker carries the -end suffix.

tests.test_blocks.test_replace_region(content, pad, expected)[source]

replace_region swaps the marked body under both padding modes.

tests.test_blocks.QUOTED_REGION = '<!-- t -->\n\nOLD\n\n```markdown\n<!-- t-end -->\n```\n\n<!-- t-end -->\n'

A region whose body quotes its own closing marker inside a code fence.

tests.test_blocks.test_skip_region_steps_over_a_quoted_end_marker()[source]

A closing marker quoted in a fence of the region does not end it.

tests.test_blocks.test_rewrite_fenced_regions_keeps_a_quoted_end_marker()[source]

A region holding a fence that quotes its closing marker round-trips.

A search for the closing marker that does not step over fences ends the region on the quoted one, and each refresh then leaves the rest of the stale region below the new one.

tests.test_blocks.test_replace_region_ignores_markers_quoted_in_fences()[source]

Markers shown in a code example are not the live region.

tests.test_blocks.test_replace_region_missing_markers_is_noop()[source]

A document without the markers is returned untouched.

tests.test_blocks.test_fence_spans_treats_nested_fences_as_opaque()[source]

A fence nested inside a longer fence never starts a span of its own.

tests.test_blocks.test_fence_spans_close(lines, close, end)[source]

A fence closes on the first bare backtick run that is long enough.

tests.test_blocks.test_fence_span_body(lines, body)[source]

The body of a span is everything between its fence lines.

tests.test_blocks.test_fence_spans_chars(chars, spans)[source]

Colon fences are fences only on request, and close on colons alone.

tests.test_blocks.test_walk_fences_steps_over_fences()[source]

Lines outside fences come one by one, each fence as one unit.

tests.test_blocks.test_rewrite_fences_keeps_what_visit_leaves()[source]

A step visit passes on is copied, an unterminated fence included.

tests.test_blocks.test_split_options(lines, options, content)[source]

Leading option lines are split from the content of a fence body.

tests.test_blocks.test_update_blocks_writes_and_check_mode(tmp_path)[source]

update_blocks rewrites changed files, and check reports without writing.

tests.test_blocks.test_iter_markdown_files_recurses_directories(tmp_path)[source]

Directories are recursed for *.md; explicit files pass through as-is.

tests.test_carapace module

class tests.test_carapace.FruitType[source]

Bases: ParamType

A param type with a custom, dynamic shell_complete override.

name: str = 'fruit'

the descriptive name of this type

shell_complete(ctx, param, incomplete)[source]

Return a list of CompletionItem objects for the incomplete value. Most types do not provide completions, but some do, and this allows custom types to provide custom completions as well.

Parameters:
  • ctx – Invocation context for this command.

  • param – The parameter that is requesting completion.

  • incomplete – Value being completed. May be empty.

Added in version 8.0.

tests.test_carapace.complete_city(ctx, param, incomplete)[source]

A param-level shell_complete callback.

tests.test_carapace.test_flag_key_grammar(opts, value, optarg, repeatable, expected)[source]
tests.test_carapace.test_hidden_flag_key_carries_the_modifier(opts, value, repeatable, expected)[source]

A hidden flag is exported marked, never silently.

Carapace completes what a spec lists, so a hidden option stays reachable like a hidden command does; & is what keeps a reader able to tell.

tests.test_carapace.test_required_flag_key_carries_the_modifier(opts, value, repeatable, expected)[source]
tests.test_carapace.test_required_and_hidden_modifiers_keep_upstream_order()[source]

! before &, the order carapace-spec writes them in.

tests.test_carapace.test_required_option_is_marked()[source]
tests.test_carapace.test_required_boolean_pair_is_left_unmarked()[source]

Either spelling satisfies Click, and the spec cannot say “one of these”.

Marking both would demand both; marking the positive alone would hide that the negative answers too.

tests.test_carapace.test_hidden_option_is_exported_and_marked()[source]
tests.test_carapace.test_flag_name(opts, expected)[source]
tests.test_carapace.test_root_description()[source]
tests.test_carapace.test_description_drops_the_no_rewrap_marker()[source]

Click’s  marker never reaches the YAML.

It is a control character telling Click’s own formatter not to rewrap, not prose: a description carrying it would hand Carapace a stray byte.

tests.test_carapace.test_command_description_loses_its_backticks_like_a_flag_does()[source]

A command’s description and its flags share one normalizer.

A reST inline literal reads the same in both: its text is kept, and the backticks that only HTML docs render are dropped.

tests.test_carapace.test_count_flag_is_repeatable()[source]
tests.test_carapace.test_operand_help_reaches_the_documentation_block()[source]

An operand’s help has one home in the spec, and it is not completion.

completion.positional holds actions, so the prose rides the sibling documentation block, indexed the same way.

tests.test_carapace.test_undocumented_operand_leaves_no_documentation_block()[source]

Nothing to say means no block at all, rather than a row of blanks.

tests.test_carapace.test_documentation_stays_aligned_with_its_actions()[source]

A documented operand behind an undocumented one keeps its index.

The two lists are read positionally, so a dropped blank would shift the prose onto the wrong operand.

tests.test_carapace.test_option_computing_its_help_still_describes_itself()[source]

An option leaving help at None still reaches the spec described.

Click Extra’s own -v and -q build their text from the resolved base verbosity, so reading the attribute hands back nothing.

tests.test_carapace.test_multiple_value_flag()[source]
tests.test_carapace.test_boolean_pair_splits_into_two_flags()[source]
tests.test_carapace.test_choice_inlined()[source]
tests.test_carapace.test_enum_choice_completes_by_member_name()[source]

An Enum choice completes by the member names Click accepts.

str() of a member reads Fruit.APPLE, which Click rejects as a value: the spec has to carry the spelling the shell can type.

tests.test_carapace.test_path_file_vs_directory()[source]
tests.test_carapace.test_positional_choice()[source]
tests.test_carapace.test_positionalany_variadic_path()[source]
tests.test_carapace.test_hidden_command()[source]
tests.test_carapace.test_aliases()[source]
tests.test_carapace.test_exclusive_flags_from_option_group()[source]
tests.test_carapace.test_dynamic_action_emitted(flag_name)[source]
tests.test_carapace.test_completion_variable_follows_clicks_derivation()[source]

The callback exports the variable Click reads for the program name.

Click turns both - and . into _ before upper-casing, so a script run as garden.py listens to _GARDEN_PY_COMPLETE. Any other spelling is a variable nothing reads, and the callback silently completes nothing.

tests.test_carapace.test_no_flag_is_persistent(spec)[source]

Click parses an option only at the level of its own command.

A persistentflags entry would offer an option after a subcommand, where Click rejects it, so no node of the spec carries one.

tests.test_carapace.test_root_default_options_stay_on_the_root()[source]
tests.test_carapace.test_subcommand_lists_only_its_own_options()[source]
tests.test_carapace.test_spec_agrees_with_click_on_flag_scope(invoke)[source]

The spec offers a root option after a subcommand exactly when Click takes it.

tests.test_carapace.test_plain_cli_keeps_own_lookalike_option()[source]
tests.test_carapace.test_spec_validates_against_schema(spec)[source]
tests.test_carapace.test_dump_has_header_and_name()[source]
tests.test_carapace.test_dump_records_invocation()[source]
tests.test_carapace.test_dump_requires_pyyaml(monkeypatch)[source]

A missing PyYAML surfaces as a pointed error at dump time, not at import.

tests.test_carapace.test_help_format_carapace_without_pyyaml(monkeypatch)[source]

--help-format carapace names the extra to install, with no traceback.

tests.test_carapace.test_carapace_complete_registered()[source]
tests.test_carapace.test_format_completion(value, help_text, expected)[source]
tests.test_carapace.test_get_completion_args_reads_comp_words(monkeypatch)[source]
tests.test_carapace.test_complete_reuses_click_machinery()[source]
tests.test_carapace.test_subcommand_dynamic_completion_resolves_end_to_end(monkeypatch)[source]

The COMP_WORDS a subcommand flag’s generated macro builds must let the backend resolve that option’s value, not the root command.

When completing a subcommand flag’s value, Carapace forwards an empty word list to the macro: it strips the parent command names, and keeps the flag being completed in its own state rather than passing it through. So the macro bakes both the command path and the flag spelling, and $* expands to nothing. This reconstructs the resulting COMP_WORDS from the generated macro and confirms the backend resolves the forecast callback. A root-only, flag-less macro would reconstruct bare weather and complete the subcommand list instead.

tests.test_carapace.runner()[source]
tests.test_carapace.greet_script(tmp_path)[source]
tests.test_carapace.test_wrap_carapace_emits_valid_spec(runner, greet_script)[source]
tests.test_carapace.test_wrap_carapace_install(runner, greet_script, tmp_path, monkeypatch)[source]
tests.test_carapace.test_xdg_dir_resolves_at_call_time(tmp_path, monkeypatch, environ, expected)[source]

The variable wins when set and non-empty, else the default under HOME.

tests.test_carapace.test_get_cache_dir(monkeypatch, platform_id, environ, expected)[source]

An absolute XDG_CACHE_HOME comes first, except on Windows.

tests.test_carapace.test_install_resolves_home_at_call_time(tmp_path, monkeypatch)[source]

The spec lands under the HOME in force at the call, not at import.

With no XDG_CONFIG_HOME set, the spec directory derives from the home directory. A test fixture or a packaging script pointing HOME elsewhere for one invocation is honored, which a path expanded at import cannot do.

tests.test_carapace.test_wrap_carapace_mutually_exclusive(runner, greet_script, extra_flags, message)[source]
tests.test_carapace.test_wrap_install_requires_an_installable_format(runner, greet_script)[source]

A destination only means something for a rendering someone installs.

tests.test_carapace.test_spec_drives_real_carapace_engine(tmp_path)[source]

Feed a generated spec to the real carapace-spec binary, end-to-end.

The tests above simulate Carapace at the CarapaceComplete level. This one downloads a pinned carapace-spec release and drives it through each dynamic shape: a root option, a subcommand option, and a subcommand argument. It guards the contract between the emitted macros and Carapace’s real traverse/exec/parse behavior, which the simulation cannot see: Carapace strips parent command names from the words it forwards, and withholds the flag being completed entirely, so the macros must bake both back in.

Attention

Downloads the carapace-spec binary from GitHub and executes it, hence the network mark (and once, to run on a single CI runner). The dynamic macro shells out, so the test only runs on POSIX platforms.

tests.test_cli_wrapper module

Tests for the CLI wrapper feature.

tests.test_cli_wrapper.GREET_SCRIPT = 'import click\n\n@click.command()\n@click.option("--name", default="World", help="Name to greet.")\ndef hello(name):\n    """Greet someone."""\n    click.echo(f"Hello, {name}")\n\nif __name__ == "__main__":\n    hello()\n'

Plain @click.command() script: patched via decorator defaults.

tests.test_cli_wrapper.CUSTOM_CLS_SCRIPT = 'import click\n\nclass RecipeGroup(click.Group):\n    """Custom group like Flask\'s FlaskGroup."""\n\n@click.command(cls=RecipeGroup)\ndef kitchen():\n    """Manage recipes and ingredients."""\n\n@kitchen.command()\n@click.option("--servings", default=4, help="Number of servings.")\ndef bake(servings):\n    """Bake a cake."""\n    click.echo(f"Baking for {servings}")\n\nif __name__ == "__main__":\n    kitchen()\n'

Script with explicit cls=RecipeGroup: patched via method patching.

tests.test_cli_wrapper.MULTI_OPTION_SCRIPT = 'import click\n\n@click.command()\n@click.option("--city", default="Paris", help="City name.")\n@click.option("--unit", default="celsius", help="Temperature unit.")\n@click.option("--verbose", is_flag=True, help="Show details.")\ndef weather(city, unit, verbose):\n    """Check the weather."""\n    msg = f"{city}: 22 {unit}"\n    if verbose:\n        msg += " (detailed)"\n    click.echo(msg)\n\nif __name__ == "__main__":\n    weather()\n'

Script with multiple options for config passthrough tests.

tests.test_cli_wrapper.PACKAGE_GROUP_SRC = 'import click\n\n@click.group()\ndef cli():\n    """Manage a produce stand."""\n\n@cli.command()\ndef restock():\n    """Restock the shelves."""\n    click.echo("Restocked")\n'

Module-level Click group, exposed by a package inside a project directory.

tests.test_cli_wrapper.ORCHARD_SCRIPT = 'import click\n\n@click.group(context_settings={"auto_envvar_prefix": "ORCHARD"})\ndef orchard():\n    """Tend the orchard."""\n\n@orchard.command(context_settings={"auto_envvar_prefix": "PRESS"})\n@click.option("--batch", type=int, default=3, help="Apples per batch.")\ndef press(batch):\n    """Press apples into cider."""\n    click.echo(f"Pressing {batch} apples")\n\nif __name__ == "__main__":\n    orchard()\n'

Group whose subcommand sets an environment variable prefix of its own.

tests.test_cli_wrapper.runner()[source]

CLI runner for wrapper tests.

tests.test_cli_wrapper.greet_script(tmp_path)[source]

A minimal Click CLI script for wrapping tests.

tests.test_cli_wrapper.custom_cls_script(tmp_path)[source]

A Click CLI with explicit cls=CustomGroup (like Flask’s FlaskGroup).

tests.test_cli_wrapper.weather_script(tmp_path)[source]

A Click CLI with multiple options for config tests.

tests.test_cli_wrapper.make_project(tmp_path)[source]

Build a local project directory exposing a Click group at module level.

The returned factory writes packaging metadata (pyproject.toml or setup.cfg) plus a package, in either the flat or src layout. On teardown it undoes the sys.path and sys.modules mutations that resolving a project directory triggers, keeping the tests isolated.

tests.test_cli_wrapper.create_config(tmp_path)[source]

Produce a temporary configuration file.

tests.test_cli_wrapper.test_patched_class_inherits_click(cls, base)[source]
tests.test_cli_wrapper.test_patched_class_has_mixin(cls)[source]
tests.test_cli_wrapper.test_patched_class_context(cls)[source]
tests.test_cli_wrapper.test_patched_command_no_extra_params()[source]

Patched commands carry no default_params.

tests.test_cli_wrapper.test_resolve_target(script, expected_module, expected_func)[source]
tests.test_cli_wrapper.test_resolve_py_file(tmp_path)[source]
tests.test_cli_wrapper.test_resolve_py_file_missing(tmp_path)[source]

A .py path that doesn’t exist falls through to module resolution.

tests.test_cli_wrapper.test_resolve_not_found(script)[source]
tests.test_cli_wrapper.test_resolve_directory_flat_layout(make_project)[source]

A project directory resolves via its pyproject.toml entry point.

tests.test_cli_wrapper.test_resolve_directory_src_layout(make_project)[source]

A src-layout project puts its src/ directory on sys.path.

tests.test_cli_wrapper.test_resolve_directory_setup_cfg(make_project)[source]

console_scripts in setup.cfg resolve when no pyproject.toml is present.

tests.test_cli_wrapper.test_resolve_directory_duplicate_scripts(make_project)[source]

Multiple script names pointing to one target resolve unambiguously.

tests.test_cli_wrapper.test_resolve_directory_run(runner, make_project)[source]

The default run mode imports and executes a project directory’s CLI.

tests.test_cli_wrapper.test_resolve_directory_introspect(make_project)[source]

Introspection discovers the command tree of a project directory.

tests.test_cli_wrapper.test_resolve_directory_no_scripts(tmp_path)[source]

A project without console scripts raises an actionable error.

tests.test_cli_wrapper.test_resolve_directory_multiple_scripts(make_project)[source]

Distinct entry-point targets cannot be disambiguated and raise.

tests.test_cli_wrapper.test_resolve_directory_package_not_found(tmp_path)[source]

An entry point whose package is absent on disk raises.

tests.test_cli_wrapper.test_wrap_self(runner, args, expected)[source]
tests.test_cli_wrapper.test_run_invokes_target(runner, script_fixture, target_args, expected_text, request)[source]

The run subcommand forwards arguments to the target CLI.

tests.test_cli_wrapper.test_run_colorizes(runner, script_fixture, target_args, request)[source]

Help output contains ANSI escape codes.

tests.test_cli_wrapper.test_run_highlights_keywords_with_custom_cls(runner, custom_cls_script)[source]

Options and subcommands are individually styled, not just headings.

tests.test_cli_wrapper.test_run_unresolvable_target(runner)[source]
tests.test_cli_wrapper.test_run_usage_line_ignores_the_launcher(runner, make_project, outer_package, monkeypatch)[source]

The wrapped target’s usage line names the script, not click-extra’s launcher.

A target whose Click command detects its own program name reads __main__.__package__, which states how click-extra itself was launched. Left alone, a documentation build running python -m sphinx paints a wrapped usage line as python -m sphinx.flask. Pinning the detection on the script name keeps it identical whatever the launcher.

tests.test_cli_wrapper.test_resolve_target_command_returns_command_and_context(greet_script)[source]

The shared resolver returns the target’s command object and a context.

tests.test_cli_wrapper.test_resolve_target_command_drills_subcommand(custom_cls_script)[source]

Extra subcommands navigate into nested groups.

tests.test_cli_wrapper.test_resolve_target_command_rejects_a_wrong_path(custom_cls_script, path, message)[source]
tests.test_cli_wrapper.test_module_scan_finds_the_one_command(tmp_path, source, expected)[source]

A module without an entry point is scanned for its one command.

tests.test_cli_wrapper.test_module_scan_refuses_an_ambiguous_module(tmp_path, source, message)[source]
tests.test_cli_wrapper.test_params_follow_a_subcommands_own_context_settings(runner, tmp_path)[source]

A subcommand’s own context_settings shape the context it is described in.

Click applies them when it invokes the subcommand, so the environment variable the parameter table names is the one the run reads.

tests.test_cli_wrapper.test_wrapper_group_runs_a_lazy_subcommand(runner, tmp_path)[source]

A lazy subcommand not imported yet runs, rather than being wrapped.

tests.test_cli_wrapper.test_params_follow_the_targets_own_context_settings(runner, tmp_path)[source]

Every context_settings key of the target reaches the parameter table.

A target answering to -h as well as --help lists both spellings, as its own help screen does.

tests.test_cli_wrapper.test_split_navigation(args, nav, rest)[source]

Leading words navigate, and everything from the first option is replayed.

tests.test_cli_wrapper.test_wrap_man_reads_manual(runner, greet_script)[source]

click-extra wrap --man -- SCRIPT reads the target’s manual and exits.

A CLI shipping no man page of its own still gets one to read, typeset from its own command tree.

tests.test_cli_wrapper.test_wrap_help_format_man_emits_source(runner, greet_script)[source]

The roff a packager installs comes from the format, not from –man.

tests.test_cli_wrapper.test_wrap_man_custom_class_group(runner, custom_cls_script)[source]

--man resolves a custom-class group target via the shared scanner.

tests.test_cli_wrapper.test_wrap_man_drills_into_subcommand(runner, custom_cls_script)[source]

Extra arguments after SCRIPT render the nested subcommand’s page.

tests.test_cli_wrapper.test_wrap_man_unresolvable_target(runner)[source]
tests.test_cli_wrapper.test_wrap_man_output_dir_writes_tree(runner, custom_cls_script, tmp_path)[source]

wrap --help-format man --output-dir writes one .1 per (sub)command.

--output-dir must appear before SCRIPT, because wrap runs with allow_interspersed_args=False so that anything after SCRIPT is treated as a sub-command path rather than a click-extra flag.

tests.test_cli_wrapper.test_wrap_man_output_dir_creates_missing_directory(runner, greet_script, tmp_path)[source]

--output-dir creates the target dir when it does not exist yet.

tests.test_cli_wrapper.test_wrap_man_pages_reject_subcommand(runner, custom_cls_script, tmp_path, monkeypatch, destination)[source]

Writing man pages always emits the full tree; mixing in a SUBCOMMAND arg is rejected so the user cannot accidentally produce a tree of pages named after a partial path.

tests.test_cli_wrapper.test_wrap_man_flag_refuses_a_destination(runner, greet_script, tmp_path)[source]

--man reads the manual: the roff to write somewhere is a format’s.

tests.test_cli_wrapper.test_group_dispatches_to_wrap(runner, greet_script, args, expected)[source]

All invocation forms reach the target CLI.

tests.test_cli_wrapper.test_group_options_work_with_wrap(runner, greet_script, group_opts)[source]

Default Group options are accepted alongside the wrap subcommand.

tests.test_cli_wrapper.test_wrap_honors_group_theme(runner, greet_script, theme, styled_heading)[source]

A group-level --theme reaches the wrapped CLI’s help screen.

ThemeOption records the pick on the shared context meta, but the wrapped target runs under its own fresh context; wrap must bridge the pick to the process default (through patch_click) for the target’s help to pick it up.

tests.test_cli_wrapper.test_wrap_honors_theme_envvar(runner, greet_script, monkeypatch)[source]

CLICK_EXTRA_THEME reaches a wrapped CLI’s help screen too.

The group resolves the machine-wide variable like any other source of a palette, and wrap bridges whatever it settled on to the process default the wrapped target renders under.

tests.test_cli_wrapper.test_wrap_pins_group_color_on_every_target_kind(runner, request, script_fixture, color_opt, colored)[source]

The group’s tri-state color decision reaches the wrapped target.

Both kinds of target must obey it: one built by the patched @click.command() decorator (so it is a _HelpColorsMixin) and one carrying an explicit cls= (so it stays plain Click). They travel different code paths inside patch_click, so each direction of the flag is checked on both.

The runner is left on its own auto-detection here: forcing colors at the stream level would paint the output whatever the flag decided.

tests.test_cli_wrapper.test_group_known_subcommands_not_wrapped(runner, subcommand)[source]

Known demo subcommands are dispatched directly, not to wrap.

tests.test_cli_wrapper.test_cli_startup_leaves_the_test_tooling_alone()[source]

Loading the CLI imports neither the test-suite engine nor click.testing.

Only the test-suite command needs them, and click.testing drags pdb and asyncio in behind it: every other command would pay for them at startup.

tests.test_cli_wrapper.test_unpatch_click_restores_the_default_theme(patch_themes)[source]

Unpatching hands back the process-wide theme patching replaced.

The default in place before the patch is not the built-in dark one, so a restore to the built-in default fails here. A second patch must not save the theme of the first as the one to restore.

tests.test_cli_wrapper.test_unpatch_click_keeps_a_default_theme_it_did_not_replace()[source]

A patch that sets no theme leaves the default theme alone on unpatching.

A default set while patched therefore outlives the patch.

tests.test_cli_wrapper.test_config_verbosity(runner, greet_script, create_config)[source]

verbosity = "DEBUG" in pyproject.toml activates debug logging.

tests.test_cli_wrapper.test_config_group_theme(runner, greet_script, create_config)[source]

A [tool.click-extra] theme key sets the help-screen theme.

tests.test_cli_wrapper.test_config_target_string(runner, greet_script, create_config)[source]

A string config value is forwarded as --key value.

tests.test_cli_wrapper.test_config_target_bool_true(runner, weather_script, create_config)[source]

A true config value is forwarded as --flag.

tests.test_cli_wrapper.test_config_target_bool_false_is_noop(runner, weather_script, create_config)[source]

A false config value is skipped: the flag is simply not passed.

tests.test_cli_wrapper.test_config_target_multiple_keys(runner, weather_script, create_config)[source]

Multiple config keys are all forwarded.

tests.test_cli_wrapper.test_config_target_cli_overrides(runner, greet_script, create_config)[source]

Explicit CLI args override config target defaults.

tests.test_cli_wrapper.test_config_target_wrong_section_ignored(runner, greet_script, create_config)[source]

Config for a different script name has no effect.

tests.test_cli_wrapper.test_config_target_empty_section(runner, greet_script, create_config)[source]

An empty target section produces no extra args.

tests.test_cli_wrapper.test_config_target_no_config(runner, greet_script)[source]

No config file at all: target runs with its own defaults.

tests.test_cli_wrapper.test_config_target_invalid_option(runner, greet_script, create_config)[source]

An invalid config key is caught by the target CLI.

tests.test_cli_wrapper.test_config_args_for_target(section, script, expected)[source]
tests.test_cli_wrapper.test_config_args_no_config()[source]

No config loaded: returns empty tuple.

tests.test_cli_wrapper.test_config_args_no_wrap_section()[source]

Config exists but has no wrap section.

tests.test_cli_wrapper.test_wrap_help_format_describes_a_foreign_cli(runner, greet_script, help_format, expected)[source]

A CLI that never heard of Click Extra is rendered from the outside.

Same posture as --params and --man: the target is loaded and walked, never asked to cooperate, so machine-readable help needs no opt-in from the author of the wrapped CLI.

tests.test_cli_wrapper.test_wrap_help_format_json_is_parseable(runner, greet_script)[source]

The JSON rendering of a foreign CLI parses, and carries its options.

tests.test_cli_wrapper.test_wrap_help_format_stays_colorless(runner, greet_script)[source]

Machine-readable output carries no ANSI, whatever the group asked for.

Every format here is meant to be piped into a parser, which has no use for escape codes: --color=always paints the help screen, not the export.

tests.test_cli_wrapper.test_wrap_install_writes_where_the_consumer_looks(runner, greet_script, tmp_path, monkeypatch, help_format, artifact)[source]

--install puts each rendering where the tool reading it looks.

One destination flag across both formats, each module resolving its own canonical directory: Carapace’s spec directory, and the user’s man directory. Both honor their XDG variable at call time.

tests.test_cli_wrapper.test_wrap_destination_refused_for_documents(runner, greet_script, help_format)[source]

A document has nowhere canonical to be installed, and says so.

tests.test_cli_wrapper.test_target_prog_name_is_what_a_user_would_type(script, expected)[source]

Every rendering is titled with the name the target runs under.

tests.test_cli_wrapper.test_every_rendering_agrees_on_the_target_name(runner, tmp_path)[source]

One rule, so the man page, spec, Markdown and tree cannot disagree.

The script file is named after neither its command object nor the directory holding it, so a renderer picking the wrong source shows it.

tests.test_color module

tests.test_color.test_standalone_color_option(invoke, option_decorator, param, expecting_colors, assert_output_regex)[source]

Check color option values, defaults and effects on all things colored, including verbosity option.

tests.test_color.test_no_color_env_convention(invoke, env, env_vote, tty, param, param_expect_colors)[source]

Each color variable is read by its own convention, on a terminal and in a pipe.

A vote of None must leave the decision to the output stream: colored on a terminal, plain in a pipe. The pipe case is what tells a variable casting no vote apart from one forcing color.

tests.test_color.test_resolve_color_env_term(monkeypatch, term, expected)[source]

A dumb/unknown TERM votes color-off, while any other value stays neutral.

tests.test_color.test_is_dumb_terminal(monkeypatch, term, expected)[source]

Case does not matter, and an unset TERM is no dumb terminal.

tests.test_color.test_renamed_color_option_reads_the_color_envvars(invoke)[source]

The color variables reach a color option under another name.

tests.test_color.test_resolve_color_env_force_color_beats_dumb_term(monkeypatch, term)[source]

An explicit FORCE_COLOR stays authoritative over a dumb/unknown TERM.

tests.test_color.test_color_synonym_cli_resolution(invoke, when, ctx_color)[source]

Every canonical value and GNU synonym resolves –color to the right state.

tests.test_color.test_color_synonym_invalid_value(invoke, when)[source]

Unknown values, including the git-style true/false, still error. The message lists only the canonical choices, never the hidden synonyms.

tests.test_color.test_color_synonym_hidden_in_help(invoke)[source]

The GNU synonyms are accepted but never advertised: –help shows only the canonical metavar.

tests.test_color.test_color_synonym_cli_beats_env(invoke, when, env_var, ctx_color)[source]

A synonym on the command line keeps the same env precedence as its canonical twin: a command-line choice outranks the color environment variables.

tests.test_color.test_color_synonym_config_string(invoke, create_config, when, ctx_color)[source]

A configuration file accepts the GNU synonyms as strings, normalized exactly like on the command line.

tests.test_color.test_color_synonym_config_boolean(invoke, create_config, filename, raw, ctx_color)[source]

A configuration boolean maps true -> always and false -> never, including YAML’s coercion of yes/no/on/off, so a value means the same across formats.

tests.test_color.test_color_synonym_config_beats_env(invoke, create_config, rhs, env_var, ctx_color)[source]

A config synonym or boolean beats the color environment variables, matching the documented precedence of canonical config values.

tests.test_color.test_integrated_color_option(invoke, param, expecting_colors, ctx_color, assert_output_regex)[source]

Check effect of color option on all things colored, including verbosity option.

Also checks the color option in subcommands is inherited from parent context.

tests.test_color.test_color_settles_before_eager_help_and_version(invoke, args, expecting_colors)[source]

–color / –no-color colorize the eager –help and –version screens whatever their position on the command line.

Click processes eager options in command-line order, so a –color sitting after –help or –version would otherwise pin ctx.color only once the screen had already printed and exited. Command.parse_args settles the color options in a pre-pass to close that gap. See Command._resolve_presentation_eagerly.

tests.test_color.test_cli_color_envvars_reach_eager_screens(invoke, screen, env, tty, expecting_colors)[source]

<CLI>_COLOR and <CLI>_NO_COLOR color the --help and --version screens.

Neither puts a flag on the command line, so the presentation pre-pass has to be triggered by the variables themselves, as it is for <CLI>_THEME.

tests.test_color.test_forced_color_sets_and_restores_env(monkeypatch)[source]

forced_color forces FORCE_COLOR and clears Click Extra’s disabling vars.

Inside the context the capture sees FORCE_COLOR=1 with every flag that would disable color (NO_COLOR, LLM, …) removed; on exit the prior environment, including any pre-existing values, is restored untouched.

tests.test_color.test_resolve_background(monkeypatch, env, expected)[source]

Environment-variable precedence for the dark/light background decision.

tests.test_color.test_terminal_background_names_the_theme_of_its_shade()[source]

A shade selects the built-in theme of the same name, as a plain string does.

tests.test_color.test_resolve_background_query_is_opt_in(monkeypatch)[source]

The OSC 11 query runs only when query_background is set, below env vars.

tests.test_color.test_resolve_background_query_below_explicit_env(monkeypatch)[source]

An explicit env signal outranks the live query even when querying is allowed.

tests.test_color.test_parse_osc_rgb(response, expected)[source]

An OSC 11 reply is parsed into an 8-bit RGB tuple, or None when malformed.

tests.test_color.test_is_dark_rgb(rgb, is_dark)[source]

Perceived-lightness classification of background colors.

tests.test_color.test_is_a_tty()[source]

The probe reads isatty, guarding streams that do not expose it.

Return type:

None

tests.test_color.test_query_osc_background_without_tty(monkeypatch)[source]

The OSC query is a no-op when stdin or stdout is not a terminal.

tests.test_color.test_query_osc_background_without_streams(monkeypatch)[source]

A detached process (no __stdin__/__stdout__) cannot query the terminal.

tests.test_color.test_query_osc_background_pty(monkeypatch)[source]

A full OSC 11 round-trip over a pseudo-terminal yields the parsed color.

tests.test_color.test_invocation_color_reaches_background_threads(invoke, args, expected)[source]

The resolved color tri-state is mirrored process-wide, so a background thread (which has no reachable Click context) can still honor it; the mirror resets once the invocation closes.

tests.test_color.test_resilient_context_does_not_publish_invocation_color(monkeypatch)[source]

Building an introspection context must leave the color mirror untouched.

A resilient context is never closed, so publishing from one would pin the process-wide mirror to the build environment’s NO_COLOR, stripping ANSI from the output of any later CLI carrying no color option of its own.

tests.test_color.test_invocation_color_never_outlives_its_invocation(invoke, args)[source]

However an invocation ends, it must leave the color mirror on auto.

publish_invocation_color() queues its reset as a context-close callback, so an option callback raising during parameter processing aborts before the context is entered and the reset never fires. The mirror then stays pinned to that run’s --no-color for the rest of the process, stripping ANSI from any later CLI carrying no color option of its own.

tests.test_color.test_invocation_color_expires_with_a_plain_click_command(invoke)[source]

A plain click.command borrowing the color options must not leak either.

It has no ExtraCommand.main() to reset the mirror in a finally, so the cleanup rides the context finalizer. Click’s UsageError holds that context inside a reference cycle, so the collection is forced here rather than waited on: in a running program ordinary allocation churn reaches it on its own.

tests.test_color.test_a_stale_finalizer_does_not_clobber_a_live_invocation()[source]

The finalizer of a dead context must leave a newer invocation’s value alone.

Tying the reset to the context’s lifetime introduces this hazard: a context collected after the next invocation published would otherwise wipe a value that is still in use. The token comparison is what prevents it.

tests.test_command_doc module

tests.test_command_doc.test_render_manpage_header_and_sections()[source]
tests.test_command_doc.test_option_names_are_escaped_and_bold()[source]
tests.test_command_doc.test_choice_metavar_rendered()[source]
tests.test_command_doc.test_optional_value_metavar_attached()[source]
tests.test_command_doc.test_count_option_has_no_metavar()[source]
tests.test_command_doc.test_no_rewrap_marker_becomes_no_fill()[source]

Click’s \b marker must produce a roff .nf / .fi block (click-man #9).

tests.test_command_doc.test_no_rewrap_marker_does_not_leak_into_surrounding_paragraphs()[source]

A \b paragraph must not switch the whole DESCRIPTION to preformatted mode: prose before and after stays filled, only the marked paragraph lands between .nf / .fi.

tests.test_command_doc.test_name_line_keeps_full_short_help()[source]

The NAME .SH line carries the canonical description without Click’s 45-char terminal truncation.

tests.test_command_doc.test_inline_literal_in_short_help_renders_as_bold()[source]

Inline reST literals (..) in the first docstring line land on the NAME .SH line as \fB..\fR, not as raw backticks (which mandoc renders as quote characters).

tests.test_command_doc.test_inline_literal_in_description_renders_as_bold()[source]

Inline reST literals in the description body become \fB..\fR so mandoc’s HTML output shows them in bold rather than wrapped in Unicode quote characters.

tests.test_command_doc.test_boolean_flag_renders_both_spellings()[source]

--ascii / --no-ascii must both appear (click-man #41).

tests.test_command_doc.test_hidden_option_skipped()[source]
tests.test_command_doc.test_option_groups_become_subsections()[source]

An explicit option group renders as a roff .SS subsection of OPTIONS, with the ungrouped remainder gathered under Other options.

tests.test_command_doc.test_default_option_groups_close_the_options_section(subject, own_section)[source]

The sections of DEFAULT_OPTION_GROUPS trail a command’s own options.

They land past the ungrouped remainder, in the order the taxonomy declares, matching what --help draws.

tests.test_command_doc.test_ungrouped_command_has_no_subsections()[source]

A command carrying no option group at all keeps a flat OPTIONS list.

tests.test_command_doc.test_operand_documented()[source]
tests.test_command_doc.test_render_manpages_tree_filenames()[source]
tests.test_command_doc.test_subcommand_page_uses_full_path()[source]
tests.test_command_doc.test_hidden_command_skipped()[source]
tests.test_command_doc.test_dynamic_subcommand_discovered()[source]

Dynamically-resolved subcommands must be generated (click-man #14 / #56).

tests.test_command_doc.test_environment_section_deduplicated()[source]

A shared env var (--config / --no-config) appears only once.

tests.test_command_doc.test_files_section_from_config_option()[source]
tests.test_command_doc.test_version_and_authors_overrides()[source]
tests.test_command_doc.test_authors_omitted_without_metadata()[source]

No distribution matches the command name, so AUTHORS is dropped rather than synthesized from a fallback.

tests.test_command_doc.test_source_date_epoch_is_honored(monkeypatch)[source]
tests.test_command_doc.test_write_manpages(tmp_path)[source]
tests.test_command_doc.test_man_option_reads_the_manual()[source]

--man typesets the page for reading, the way man itself does.

tests.test_command_doc.test_strip_overstrike(text, expected)[source]

Bold and underline pairs both collapse to their plain character.

tests.test_command_doc.test_find_typesetters_fills_each_installed_one(monkeypatch)[source]

Each installed program yields its resolved path and its filled arguments.

tests.test_command_doc.test_man_option_falls_back_to_the_source(monkeypatch)[source]

With no typesetter installed, the source beats an error.

tests.test_command_doc.test_accessible_manual_drops_overstrike()[source]

Accessible mode strips the emphasis a screen reader would voice as noise.

tests.test_command_doc.test_generated_roff_passes_groff_lint(cli, prog_name)[source]

Every generated page must parse cleanly under groff (no warnings).

forecast exercises the .SS option-group subsections.

tests.test_command_doc.test_render_help_rejects_unknown_format()[source]

An unknown format names the ones that exist instead of failing blankly.

tests.test_command_doc.test_help_formats_describe_every_format()[source]

Every HelpFormat has a description, and members run alphabetically.

tests.test_command_doc.test_render_help_takes_a_member_or_its_value(help_format)[source]

A HelpFormat member and its string value render the same document.

tests.test_command_doc.test_render_help_names_carapace_spec_after_prog_name(prog_name, spec_name)[source]

A passed prog_name names a Carapace spec, over the command’s own name.

tests.test_command_doc.test_json_carries_every_section()[source]

The JSON document covers what the man page covers, as native types.

tests.test_command_doc.test_json_carries_choices_a_short_metavar_hides()[source]

A consumer reads the accepted values off choices, never off the metavar.

An option overriding the metavar with a short placeholder leaves nothing to parse in metavar, which is the whole reason the key is separate.

tests.test_command_doc.test_json_reports_no_choices_for_an_open_type()[source]

choices is None for a type that enumerates nothing.

tests.test_command_doc.test_a_short_metavar_lists_its_values_in_the_render(help_format)[source]

Shortening the metavar moves the values below the help, never drops them.

tests.test_command_doc.test_a_default_choice_metavar_does_not_repeat_its_values(help_format)[source]

A [a|b] metavar already shows them, so nothing is appended below it.

tests.test_command_doc.test_every_enumerated_default_option_reaches_the_render(help_format)[source]

No default option hides its accepted values behind a short metavar.

--table-format and --export-config both trade the [a|b|c] metavar for a one-word placeholder to keep the help screen readable. That is a help-screen decision, and it must not reach the man page or the Markdown render, which are what a reader consults precisely to learn what a value may be.

tests.test_command_doc.test_json_lists_subcommands_by_name_only()[source]

Progressive disclosure: children are named, never recursively expanded.

tests.test_command_doc.test_subcommand_aliases_reach_every_backend()[source]

A generated page names the short spelling the help screen advertises.

An alias is invocable, so a reader who only ever sees the man page or the JSON export would otherwise never learn it exists.

tests.test_command_doc.test_json_full_walks_the_whole_tree()[source]

The -full variant is the one that expands every command of the tree.

tests.test_command_doc.test_markdown_renders_every_section()[source]

The Markdown document carries the same sections as headings.

tests.test_command_doc.test_markdown_full_walks_the_whole_tree()[source]

Every command of the tree gets its own title in one document.

tests.test_command_doc.test_no_rewrap_marker_keeps_its_shape_in_markdown()[source]

A \b region is fenced rather than reflowed into a paragraph.

tests.test_command_doc.test_markdown_escapes_html_characters_outside_code_spans()[source]

Help prose shows as written, where Markdown would read it as HTML.

A browser shows nothing for <guests>, which Markdown takes for a tag. A code span keeps its content as written, whether a reST literal made it or the author wrote it.

tests.test_command_doc.test_examples_render_in_every_backend()[source]

One examples= declaration feeds the help screen, roff, Markdown and JSON.

tests.test_command_doc.test_command_without_examples_grows_no_section()[source]

A command declaring no example renders no section for them.

tests.test_command_doc.test_malformed_examples_fail_at_construction(malformed)[source]

A bad pair surfaces on import, not on the first –help a user runs.

tests.test_command_doc.test_dynamic_help_is_extracted()[source]

Options computing their help from the context are not left blank.

-v / -q leave Option.help at None and build their sentence in get_help_record(). Every backend here reads the extracted model, so the text has to be resolved once, at extraction.

tests.test_command_doc.test_registered_name_names_the_page()[source]

A subcommand registered under another name is documented under that name.

The COMMANDS section lists predict, so the page a reader looks for is weather-predict.1, and its NAME line says weather predict: the name the parent registered wins over the command’s own.

class tests.test_command_doc.Fruit(*values)[source]

Bases: Enum

Choices an option can take as Enum members.

APPLE = 'apple'
BANANA = 'banana'
tests.test_command_doc.test_enum_choices_are_spelled_as_click_completes_them(help_format, expected)[source]

An Enum choice lists its member names, the spelling Click accepts.

str() of a member reads Fruit.APPLE, which no invocation can type. The short metavar hides the values, so the render adds them back and shows which spelling the document carries.

tests.test_command_doc.test_json_spells_enum_choices_as_click_completes_them()[source]
tests.test_command_doc.test_install_manpages_resolves_home_at_call_time(tmp_path, monkeypatch)[source]

The pages land under the HOME in force at the call, not at import.

With no XDG_DATA_HOME set, the default directory derives from the home directory. A test fixture or a packaging script pointing HOME elsewhere for one invocation is honored, which a path expanded at import cannot do.

tests.test_commands module

Test defaults of our custom commands, as well as their customizations and attached options, and how they interact with each others.

tests.test_commands.test_module_root_declarations()[source]

Verify click_extra.__all__ is a superset of click and cloup.

Sort order is enforced by ruff (RUF022).

tests.test_commands.test_deprecated_click_exports_are_forwarded(name)[source]

Click’s deprecated names resolve to Click’s objects, never bound eagerly.

A binding would bypass Click’s deprecation shim, and fetching it at import time would warn in every program importing click-extra. Whether Click warns on access depends on its version, so only the forwarding is checked.

tests.test_commands.test_import_raises_no_deprecation_warning(statement)[source]

Importing the package sets off no deprecation warning.

Cloup lists Click’s deprecated names in its __all__, so a plain star import of cloup fetches each of them, and Click warns once per name. A star import of click-extra would do the same for any such name its own __all__ declared.

Runs on every cell of the matrix, not as a once test: a deprecation shows first on the cells tracking the development branches of Click and Cloup, like the warning Click raises when a class is created with an override of a member it retired.

tests.test_commands.test_public_namespace_integrity()[source]

The package namespace and __all__ agree: nothing missing or extra.

click ships no __all__, so its star import would otherwise leak every submodule bound by click’s __init__ (click.core, click.globals, …) into the package namespace, handing out click’s un-enhanced classes and shadowing the globals builtin. Those bindings are scrubbed at the end of click_extra/__init__.py: verify the scrub held, that every public binding is declared in __all__, and that every declared name resolves (eagerly, lazily, or as one of our own submodules).

tests.test_commands.test_no_debugger_ballast_on_import()[source]

Importing the package must not load the test tooling nor the debugger stack.

click_extra.testing imports click.testing, whose module-level import pdb cascades into asyncio and the _pyrepl machinery on Python 3.13+. Loaded eagerly, that chain ends up bundled into every Nuitka-compiled CLI binary, so the test tooling is exported lazily: check from a pristine interpreter that none of it leaks at import time.

tests.test_commands.CLOUP_METADATA_PROBE_VERSION = (4, 0, 0)

Last cloup release importing importlib.metadata at module scope.

Up to this release, cloup reads Click’s version at import time, which loads importlib.metadata and the email tree behind it. janluke/cloup#242 removes that probe, so any later cloup keeps both off the startup path.

tests.test_commands.test_no_heavy_dependency_ballast_on_startup()[source]

Importing the package and rendering a --help must not load the dependencies the package defers.

Each module below is imported where it is used rather than at module scope, so that a CLI pays for it only on the code path that needs it: see the deferred-imports house rule in AGENTS.md. The probe runs a hello-world through decoration and --help in a pristine interpreter, since pytest itself has most of these loaded already.

importlib.metadata and the email tree it drags in join the list above CLOUP_METADATA_PROBE_VERSION only: up to that release, cloup imports them at module scope itself.

tests.test_commands.test_import_module_budget()[source]

import click_extra stays within a module-count budget.

A coarse net under the denylist above: a new eager import of a dependency with a deep tree shows up here as a jump of tens of modules, whichever package it comes from. Measured at 264 modules on Python 3.14 and cloup 4.0.0; the budget leaves room for the platform and version differences of the stdlib.

tests.test_commands.STARTUP_RATIO_TO_CLICK = 3.0

Most a hello-world --help of click-extra may take, as a multiple of Click’s.

Measured at 2.4 on Python 3.14 with Click 8.5.0, for the 10.0.0 release.

tests.test_commands.HELLO_CLICK = 'import click\n\n\n@click.command()\n@click.option("--name", default="World", help="The person to greet.")\ndef hello(name):\n    """Simple program that greets NAME."""\n    click.echo(f"Hello, {name}!")\n\n\nif __name__ == "__main__":\n    hello()\n'

The hello-world CLI the startup comparison times, written with Click.

tests.test_commands.test_startup_time_stays_within_a_ratio_of_click(tmp_path)[source]

A hello-world --help takes at most a few times as long as Click’s own.

Milliseconds depend on the runner, so the bound is relative: both CLIs run in alternation in fresh interpreters, and the best run of each is compared, as the one a loaded runner disturbs least. The children run without the color and coverage variables, which would add work to one side only.

tests.test_commands.test_lazy_test_tooling_exports()[source]

The lazy test-tooling names resolve, cache, and show up in dir().

tests.test_commands.test_lazy_converter_exports()[source]

The converter names resolve on first access, off the startup path.

Checked in a pristine interpreter, since another test may have imported the converters already.

tests.test_commands.LAZY_REGISTRIES = ('_LAZY_CAPTURES', '_LAZY_CONVERTERS', '_LAZY_DISPLAYS', '_LAZY_TEST_TOOLING')

The registries of names the package root serves through its __getattr__.

tests.test_commands.test_lazy_exports_are_declared_and_typed(registry)[source]

Each lazy name is in __all__, typed, and resolves to its source object.

A lazy name has no binding until its first access, so the TYPE_CHECKING import in click_extra/__init__.py is what shows it to type checkers.

tests.test_commands.test_lazy_export_modules_stay_off_startup()[source]

import click_extra loads none of the modules the lazy names live in.

Checked in a pristine interpreter, since other tests have imported them.

tests.test_commands.test_lazy_export_modules_are_visible_to_freezers()[source]

Each module a lazy name lives in has an import statement in a function.

Nuitka bundles what a live import statement names. It drops the TYPE_CHECKING block as dead code, and cannot follow a computed import_module() call, so a module missing here is missing from every binary built on click-extra, which then crashes on the lazy name.

tests.test_commands.all_command_cli()[source]

A CLI that is mixing all variations and flavors of subcommands.

tests.test_commands.test_unknown_option(invoke, all_command_cli)[source]
tests.test_commands.test_short_option_error_enhancement(invoke, cli_options, args, exit_code, expected_fragment)[source]

Command.parse_args improves error messages for single-dash multi-character tokens whose first character is not a registered short option. Vanilla Click would split -dbgwrong character by character and report “No such option: -d”; we re-raise with the full token and close-match suggestions instead.

The enhancement must not interfere with valid -abc-style combining or with the per-character diagnostic when a later character is unknown.

Upstream context: https://github.com/pallets/click/issues/2779

tests.test_commands.test_unknown_command(invoke, all_command_cli)[source]
tests.test_commands.test_required_command(invoke, all_command_cli, assert_output_regex)[source]
tests.test_commands.test_group_help(invoke, all_command_cli, param, exit_code, assert_output_regex)[source]
tests.test_commands.test_help_eagerness(invoke, all_command_cli, params, exit_code, expect_help, expect_empty_stderr, assert_output_regex)[source]

See: https://click.palletsprojects.com/en/stable/click-concepts/#callback-evaluation-order

tests.test_commands.test_help_custom_name(invoke)[source]

Removes the -h short option as we reserve it for a custom -h/--header option.

See: https://github.com/kdeldycke/mail-deduplicate/issues/762

tests.test_commands.test_subcommand_help(invoke, all_command_cli, cmd_id, param, assert_output_regex)[source]
tests.test_commands.test_subcommand_execution(invoke, all_command_cli, cmd_id)[source]
tests.test_commands.test_integrated_version_value(invoke, all_command_cli)[source]
tests.test_commands.test_colored_bare_help(invoke, cmd_decorator, param)[source]

Extra decorators are always colored.

Even when stripped of their default parameters, as reported in: https://github.com/kdeldycke/click-extra/issues/534 https://github.com/kdeldycke/click-extra/pull/543

tests.test_commands.test_duplicate_option(invoke)[source]

See: - https://kdeldycke.github.io/click-extra/commands.html#change-default-options - https://github.com/kdeldycke/click-extra/issues/232

tests.test_commands.test_no_option_leaks_between_subcommands(invoke, assert_output_regex)[source]

As reported in https://github.com/kdeldycke/click-extra/issues/489.

tests.test_commands.test_option_group_integration(invoke, assert_output_regex)[source]
tests.test_commands.test_show_envvar_parameter(invoke, cmd_decorator, ctx_settings, expected_help)[source]
tests.test_commands.test_show_choices_parameter(ctx_settings, expected)[source]

The show_choices context setting is forced on every option when set.

tests.test_commands.test_raw_args(invoke)[source]

Raw args are expected to be scoped in subcommands.

tests.test_commands.test_lazy_group(invoke, tmp_path, lazy_cmd_decorator, lazy_group_decorator)[source]

Test extends the snippet from Click documentation.

tests.test_commands.write_produce_modules(tmp_path)[source]

Write the command modules the sectioned lazy-group tests import.

tests.test_commands.test_lazy_group_sections(invoke, tmp_path)[source]

A LazySubcommand files its command under the section it declares.

tests.test_commands.test_lazy_group_lists_declared_help_without_importing(invoke, tmp_path)[source]

A lazy subcommand declaring its help is listed without being imported.

tests.test_commands.test_lazy_group_keeps_a_listed_subcommand_in_place(invoke, tmp_path)[source]

Importing a subcommand listed by its declared help keeps its place.

tests.test_commands.test_cli_help_imports_no_lazy_subcommand()[source]

The root help of the click-extra CLI lists its lazy subcommands unimported.

tests.test_commands.test_cli_lazy_subcommand_lists_as_its_command(name)[source]

The help a lazy subcommand declares lists it the way the command does.

The declared text and the command’s own help are two copies of one sentence: this fails the moment they drift apart.

tests.test_commands.test_lazy_group_section_shared_with_eager_subcommand(invoke, tmp_path)[source]

A Section can hold both eagerly and lazily registered subcommands.

tests.test_commands.test_lazy_group_no_default_section(invoke, tmp_path)[source]

fallback_to_default_section=False hides a subcommand but keeps it invocable.

tests.test_commands.test_lazy_subcommand_normalizes_bare_import_paths(tmp_path)[source]

A bare import path is normalized into a LazySubcommand.

tests.test_commands.test_decorator_overrides()[source]

Ensure our decorators are not just alias of Click and Cloup ones.

tests.test_commands.test_decorator_cls_parameter(klass, should_raise)[source]

Decorators accept custom cls parameters.

tests.test_commands.test_help_shows_group_help(invoke)[source]

mycli help produces the same output as mycli --help.

tests.test_commands.test_help_shows_subcommand_help(invoke)[source]

mycli help greet matches mycli greet --help.

tests.test_commands.test_help_nested_group(invoke)[source]

mycli help sub leaf resolves through nested groups.

tests.test_commands.test_help_nonexistent_subcommand(invoke)[source]

mycli help nosuch reports an error.

tests.test_commands.test_help_subcommand_of_non_group(invoke)[source]

mycli help leaf deeper errors when leaf is not a group.

tests.test_commands.test_help_disabled(invoke)[source]

help_command=False suppresses auto-injection.

tests.test_commands.test_help_user_override(invoke)[source]

User-defined help subcommand replaces the auto-injected one.

tests.test_commands.test_help_appears_in_listing(invoke)[source]

The help subcommand is visible in the group’s command list.

mycli help --search term finds matching subcommands.

tests.test_commands.test_help_search_skips_hidden_subcommands(invoke)[source]

A hidden subcommand stays out of the search, as it stays out of help.

tests.test_commands.test_help_search_no_match(invoke)[source]

mycli help --search term with no matches.

tests.test_commands.test_help_in_all_command_cli(invoke, all_command_cli)[source]

The help subcommand works on the fixture CLI.

tests.test_commands.test_help_for_subcommand_in_all_command_cli(invoke, all_command_cli)[source]

help default-subcommand works on the fixture CLI.

tests.test_commands.kitchen_group(**kwargs)[source]

A group whose subcommands are declared out of alphabetical order.

tests.test_commands.listed_subcommands(help_screen)[source]

Names listed under the Commands: heading of a help screen.

tests.test_commands.test_subcommand_order_in_help_screen(invoke, group_kwargs, expected)[source]

Cloup renders the help screen from sections, never from list_commands().

tests.test_commands.test_subcommand_order_in_list_commands(group_kwargs, expected)[source]

The flat listing feeding --tree, man pages, specs and completion.

tests.test_commands.test_subcommand_order_agrees_across_renderers(invoke)[source]

Every rendering of a command tree lists subcommands in the same order.

A renderer reaching for its own accessor would let a group’s help screen disagree with its man page or its completion spec. They all go through list_commands(), and this pins that down for the whole population rather than one renderer at a time.

tests.test_commands.test_iter_subcommand_contexts_builds_a_context_per_subcommand()[source]

Each subcommand comes with a context named after it, under its parent’s.

tests.test_commands.ghost_pantry()[source]

A lazy group also listing a name it cannot resolve, like a broken plugin.

tests.test_commands.test_tree_walks_agree_on_lazy_and_unresolvable_subcommands(invoke, tmp_path)[source]

Every walk of a command tree imports a lazy subcommand and skips a dead name.

A group can list a name it cannot resolve, like a plugin whose module broke. The walks all go through iter_subcommands, so none of them crashes on that name, and none drops a lazy subcommand it has not imported yet. Each walk gets a fresh group, so no walk finds the subcommand imported by another.

tests.test_commands.test_option_order_agrees_across_renderers(invoke)[source]

Every rendering of a command lists options in the same order.

Cloup draws the ungrouped section last, and Click Extra sends its own groups past it, so a renderer reaching for its own accessor would let a help screen disagree with its man page or its completion spec. They all resolve the section order through split_option_groups().

Pins the taxonomy too: each rendering is filtered down to the flags DEFAULT_OPTION_GROUPS declares, so the order they are declared in is the order a reader sees.

tests.test_commands.test_argument_help_agrees_across_renderers(invoke, argument_decorator)[source]

An argument’s help reaches every rendering, whatever its Argument class.

The help screen, the man page and the JSON export all resolve an operand’s description through the same helper, so a plain click.argument carries the same text everywhere.

tests.test_commands.test_deprecated_command_label_matches_click(invoke, deprecated, label)[source]

A deprecated command carries Click’s own marker, reason string included.

The marker is checked on the help screen and on full_short_help(), which feeds --tree, the man page, the JSON export and the completion specs.

tests.test_commands.test_full_short_help_drops_the_no_rewrap_marker()[source]

A first paragraph opening on Click’s \b loses the marker, as in Click.

full_short_help() feeds --tree, the man page, the completion specs and the click:tree summary table: a stray \x08 there moves the terminal cursor back one column, and turns the man page entry into a no-fill block.

tests.test_commands.test_every_default_option_lands_in_a_section()[source]

No option default_params() returns escapes DEFAULT_OPTION_GROUPS.

An option no section claims stays ungrouped, which draws it in the command’s own Options block where it reads as one the CLI author declared. Nothing else reports that: the help screen renders, and every cross-renderer check still agrees, because they all read the same wrong layout.

tests.test_commands.test_default_option_groups_name_no_stale_flag()[source]

Every flag DEFAULT_OPTION_GROUPS declares is one default_params() returns.

A renamed or dropped option otherwise leaves a dead entry behind. And a flag landing in two sections is silently taken by the later one, since _assign_option_groups writes them in order.

tests.test_commands.test_option_swapped_by_a_params_hook_keeps_its_section()[source]

An option a params hook replaces stays in the section of the original.

default_params files the instances it builds, so a replacement built after that call carries none, and cloup draws it in the command’s own Options block where it reads as one the CLI author declared. Each default option is swapped in turn, sections being declared one flag at a time: an option losing its own would pass unseen while its siblings still hold theirs.

tests.test_commands.test_params_hook_runs_once_per_decorated_command()[source]

A params hook builds one set of options per command, and never shares it.

A decorator built once and applied twice still gives each command its own option instances, the second application calling the hook again.

tests.test_commands.test_lazy_group_subcommand_order_is_stable_across_loading(tmp_path, monkeypatch)[source]

A lazy subcommand holds its slot before and after it is imported.

Importing appends the command to self.commands, so registration order read off that dictionary alone would reshuffle mid-run.

tests.test_commands.test_lazy_group_defaults_to_alphabetical_order()[source]

Sorting moved from __init__ to listing time, with the same result.

tests.test_commands.test_option_priorities_leave_processing_order_alone(invoke)[source]

The help screen reorders while params and the callbacks do not.

click.core.iter_params_for_processing breaks eager-option ties on declaration order, which is why --time measures everything and --accessible lowers the --color default before it resolves. Reordering the help screen must not disturb any of that.

tests.test_commands.test_option_priorities_match_flags_then_destination()[source]

A flag pair sharing one destination stays addressable one flag at a time.

tests.test_commands.test_option_priorities_never_reorder_positional_arguments()[source]

Argument order is part of the command’s grammar, not of its presentation.

tests.test_commands.test_command_listing_is_not_cut_by_an_abbreviation()[source]

A subcommand’s line in its parent’s list holds its whole first sentence.

Click ends the listing at the first word closing on a period, so an abbreviation mid-sentence (vs., e.g., etc.) cuts it into a fragment: gradient once read “Render 24-bit RGB gradients vs.”, which says nothing.

The guard is narrower than “the listing reads well”: it only catches a cut landing before the first sentence ends, which is the one failure a docstring can cause without anyone noticing.

tests.test_commands.test_no_help_screen_leaks_a_no_rewrap_marker()[source]

A \b marker never reaches the rendered help of any command.

Click strips the marker only where it opens a paragraph, and paragraphs are split on blank lines. An epilog writing one between two indented blocks without a blank line around it therefore ships the raw backspace character: a terminal hides it, an HTML page does not.

tests.test_config module

tests.test_config.DOCS_CONFIG_PAGE = PosixPath('/home/runner/work/click-extra/click-extra/docs/config-discovery.md')

The documentation page transcribing part of ConfigFormat.

tests.test_config.FULL_SEARCH_FLAGS = 285504

All search flags ConfigOption forces on, used as the baseline in tests.

tests.test_config.NO_DOTGLOB_FLAGS = 285440

FULL_SEARCH_FLAGS minus DOTGLOB, to exercise the dotfile warnings.

tests.test_config.SQLITE_DATA = {'config-cli1': {'default': {'int_param': 3, 'random_stuff': 'will be ignored'}, 'dummy_flag': True, 'my_list': ['pip', 'npm', 'gem']}}

The shared reference configuration, as SQLITE_CONFIG_TABLE rows.

Keys are dotted parameter paths, values are JSON-encoded. This mirrors TOML_DATA, minus the verbosity bump and the sections the other formats use to exercise their own quirks.

tests.test_config.flatten_sqlite_keys(data, prefix='')[source]

Flatten a nested mapping into dotted keys, the SQLite config layout.

Return type:

dict

tests.test_config.make_sqlite_config(path, data=None, *, create_table=True)[source]

Write a nested mapping into a SQLite configuration database.

Skips the calling test on a Python whose SQLite bindings are missing, the same interpreter on which ConfigFormat.SQLITE reports itself disabled.

Return type:

Path

tests.test_config.simple_config_cli()[source]
tests.test_config.test_unset_conf(invoke, simple_config_cli)[source]
tests.test_config.test_unset_conf_debug_message(invoke, simple_config_cli, assert_output_regex)[source]
tests.test_config.test_conf_default_path(invoke, simple_config_cli)[source]
tests.test_config.test_conf_chosen_formats_displayed(invoke, file_format_patterns, expected_pattern)[source]

A format set chosen by the developer is displayed in full.

Only an inherited set collapses to its folder, so the help screen keeps showing the effect of file_format_patterns, which is what docs/config-discovery.md demonstrates.

tests.test_config.test_conf_show_file_patterns(invoke, kwargs, shows_patterns)[source]

show_file_patterns overrides the display in both directions.

tests.test_config.test_conf_default_pathlib_type(invoke, create_config)[source]

Refs https://github.com/kdeldycke/click-extra/issues/1356

tests.test_config.test_conf_default_url_displayed_as_given(invoke, url)[source]

A URL default keeps the double slash after its scheme in the help screen.

tests.test_config.test_conf_not_found(invoke, simple_config_cli, conf_path)[source]
tests.test_config.test_conf_unparsable(invoke, simple_config_cli, create_config)[source]

Explicit –config pointing to a file with garbage content.

tests.test_config.test_conf_blank_source(invoke, simple_config_cli, create_config, filename, content)[source]

Explicit –config with nothing to read loads an empty configuration.

/dev/null (nul on Windows) is the Unix way to point a tool at no configuration. An empty file means the same, even where its format calls it invalid, like JSON, and where its parser reads the file from its path, like plist and SQLite. Every parameter then keeps its default.

tests.test_config.test_conf_parses_to_nothing(invoke, simple_config_cli, create_config, filename, content, label)[source]

Explicit –config parsing to nothing names its format, not a parse error.

tests.test_config.test_conf_blank_file_never_shadows_a_configuration(invoke, tmp_path, explicit)[source]

A blank file loses to a configuration the same pattern matches after it.

tests.test_config.test_conf_named_pipe(invoke, simple_config_cli, tmp_path)[source]

A named pipe is read like a file: its content is the configuration.

tests.test_config.test_no_config_option(invoke, simple_config_cli, create_config)[source]
tests.test_config.test_standalone_no_config_option(invoke)[source]

@no_config_option cannot work without @config_option.

tests.test_config.test_strict_conf(invoke, create_config, conf_text, expect_error)[source]

Strict mode rejects unknown params but accepts clean configs.

tests.test_config.test_kebab_case_keys(invoke, create_config)[source]

Kebab-case config keys reach the snake_case-named CLI parameters.

tests.test_config.test_kebab_case_spelling_collision(invoke, create_config)[source]

Both spellings of the same key: last one wins, a warning names both.

tests.test_config.test_strict_conf_ignores_foreign_sections(invoke, create_config)[source]

Other tools’ sections in a shared config file do not trip strict mode.

tests.test_config.test_command_forwards_config_strict(invoke, create_config)[source]

@command(config_strict=True) activates strict mode on the default option.

tests.test_config.test_command_forwards_config_cascade(cascade)[source]

@command(config_cascade=…) sets the cascade of the default option.

tests.test_config.test_command_excluded_params_additive(invoke, create_config)[source]

@command(excluded_params=…) extends the default blocklist.

The forwarded exclusion applies on top of the built-in ones, and a blocked parameter found in a config file is reported as blocked, not unknown.

tests.test_config.test_export_config_includes_unset_params(invoke)[source]

Parameters without a default are exported instead of silently dropped.

TOML has no null type so unset parameters are commented out; multi-value parameters read as empty lists; JSON renders unset parameters as null.

tests.test_config.test_export_config_includes_unset_params_of_a_plain_click_command(invoke)[source]

A plain Click command exports a parameter without a default as null too.

Such a command does not capture its raw arguments, so the export reads each parameter’s default instead, which Click leaves as its UNSET sentinel.

tests.test_config.test_serialize_toml_with_unset_layout(tree, expected)[source]

A commented-out key sits under the header of its own table.

A table with no key of its own keeps its header, else its commented-out keys read as keys of the table above.

tests.test_config.test_export_config_reads_a_repeatable_flag_as_a_toggle(invoke)[source]

A repeatable boolean flag exports as the boolean a configuration spells.

It collects occurrences of a toggle rather than values, so dumping an unset one as the empty list of a multi-value parameter advertises a shape no user writes. A repeatable option taking values still reads as a list.

tests.test_config.test_export_config_skips_an_opaque_subtree(invoke)[source]

A subcommand sharing a name with an opaque schema field is not exported.

The loader hands the whole sub-tree to the app’s own validator, so the subcommand’s options cannot be read back from there. Exporting them writes a file the same loader refuses.

tests.test_config.test_export_config_kebab_case_keys(invoke, tmp_path)[source]

Exported keys use the kebab-case spelling, the canonical form for files.

Either spelling loads back to the same parameter, so the kebab-cased export still round-trips through –config.

tests.test_config.test_introspection_flags_load_config_first(invoke, create_config)[source]

–params and –export-config reflect the config file regardless of the order in which Click processes the eager options.

Click processes eager parameters given on the command line ahead of eager parameters left at their defaults, so these flags used to render before the configuration file was discovered and loaded.

tests.test_config.test_conf_file_overrides_defaults(invoke, simple_config_cli, create_config, httpserver, conf_name, conf_text, conf_data, assert_output_regex)[source]
tests.test_config.test_auto_envvar_conf(invoke, simple_config_cli, create_config, httpserver, conf_name, conf_text, conf_data)[source]
tests.test_config.test_conf_file_overridden_by_cli_param(invoke, simple_config_cli, create_config, httpserver, conf_name, conf_text, conf_data)[source]
tests.test_config.test_config_status_line_follows_verbosity(invoke, create_config, before, after, shown)[source]

--config announces the file it loads, unless the command line asks for less than the default verbosity, whatever the position of the verbosity flags.

tests.test_config.test_no_config_status_line_follows_verbosity(invoke, args, shown)[source]

--no-config announces it skips loading, unless asked for quiet output.

tests.test_config.test_conf_metadata(invoke, create_config, httpserver, conf_name, conf_text, conf_data)[source]
tests.test_config.test_conf_metadata_no_config(invoke)[source]

ctx.meta entries are not set when –no-config skips loading.

tests.test_config.test_format_from_mime(media_type, expected)[source]
tests.test_config.test_format_from_mime_restricted_to_candidates()[source]

formats narrows the resolution, like format_from_path does.

tests.test_config.test_jwcc_resolves_to_the_json5_parser(tmp_path)[source]

A *.jwcc file is read by the JSON5 parser, which is a superset of it.

tests.test_config.test_jwcc_conf(invoke, simple_config_cli, tmp_path)[source]

A JWCC document loads: JSON plus comments and trailing commas.

tests.test_config.test_conf_key_reaches_a_case_preserving_param_name(invoke, create_config)[source]

A parameter whose name kept its case is still addressed by that case.

Click takes an identifier declaration verbatim, so this parameter is named Explicit_Name. No fold produces that spelling, so the template has to stay the authority on it.

tests.test_config.test_conf_key_case_folds_onto_the_param_name(invoke, create_config, spelling)[source]

Every spelling Click could have derived foo_bar from reaches it.

tests.test_config.test_conf_key_folding_onto_two_params_is_skipped(invoke, create_config, caplog)[source]

A key folding onto two parameter names picks neither, and warns.

Click allows foo_bar and Foo_Bar on one command, and nothing in the folded spelling says which was meant.

tests.test_config.test_strict_conf_accepts_a_folded_key(invoke, create_config)[source]

Strict mode no longer rejects a spelling the fold resolves.

tests.test_config.test_mime_types_are_unambiguous()[source]

No media type is claimed by two formats.

A duplicate would leave format_from_mime resolving on ConfigFormat declaration order alone, silently handing the media type to whichever format happens to be declared first.

tests.test_config.test_docs_media_types_table_matches_formats()[source]

The media-type table in the docs is ConfigFormat.mime_types, transcribed.

A format gaining or losing a media type otherwise leaves the table stale, and that table is the only place a user reads the mapping from.

tests.test_config.test_remote_conf_typed_by_content_type(invoke, simple_config_cli, httpserver, media_type, conf_text)[source]

An extension-less URL is typed by the media type its server advertises.

tests.test_config.test_remote_conf_falls_back_to_the_url_name(invoke, simple_config_cli, httpserver, media_type)[source]

A generic or plain wrong media type still leaves the URL name to match on.

tests.test_config.test_remote_conf_content_type_never_widens_the_format_set(invoke, httpserver, file_format_patterns, exit_code, stdout)[source]

A media type is resolved against file_format_patterns alone.

The same application/json download feeds the CLI when JSON is accepted, and is rejected outright when the option only declares TOML.

tests.test_config.test_unparsable_conf_message_enumerates_formats(invoke, create_config, caplog, file_format_patterns, expected)[source]

The “error parsing” message never dangles its conjunction.

tests.test_config.test_ini_subcommand_section_is_typed(invoke, create_config)[source]

An INI section addressing a subcommand reads its values as the options’ types.

A list is JSON-encoded in the file, like in the root section.

tests.test_config.test_argfile_conf_file_overrides_defaults(invoke, simple_config_cli, create_config, assert_output_regex)[source]

An argfile feeds CLI tokens into the same default_map pipeline.

tests.test_config.test_argfile_conf_metadata(invoke, create_config)[source]
tests.test_config.test_argfile_cli_overrides_conf(invoke, create_config)[source]

Command-line parameters take precedence over argfile values.

tests.test_config.test_argfile_secondary_flag_and_inline_value(invoke, create_config)[source]
tests.test_config.test_argfile_switches_sharing_a_destination(invoke, create_config, preparation)[source]

Each feature switch of one destination is read from an argfile.

The two options share a name, so the parameter structure lists both under one leaf. Keeping only the last one there leaves --peeled unmatched.

tests.test_config.test_ini_name_shared_by_two_types_rejected(invoke, create_config)[source]

An INI item a name of two option types addresses is refused, not guessed.

tests.test_config.test_argfile_strict_conf(invoke, create_config, conf_text, expected_key)[source]

Strict mode rejects unknown options with the standard error.

An unmatched declaration is named the way Click names a parameter it derives from one, case fold included, so --Unknown-Option is reported as unknown_option.

tests.test_config.test_argfile_unknown_option_ignored_when_not_strict(invoke, create_config)[source]
tests.test_config.test_argfile_unparsable_conf(invoke, create_config, conf_text, message)[source]

An argfile that produces no option is rejected, saying how it was read.

tests.test_config.test_argfile_positional_tokens_skipped(invoke, create_config)[source]
tests.test_config.test_argfile_export_config_rejected(invoke)[source]

Argfile has no serializer, so –export-config rejects it.

tests.test_config.test_sqlite_conf_file_overrides_defaults(invoke, simple_config_cli, tmp_path, ext)[source]
tests.test_config.test_sqlite_conf_metadata(invoke, tmp_path)[source]
tests.test_config.test_sqlite_read_and_parse_conf(tmp_path)[source]

The default format patterns discover SQLite databases by extension.

tests.test_config.test_sqlite_conf_unparsable(invoke, simple_config_cli, tmp_path, make_db, message)[source]

A SQLite file that cannot yield a configuration is rejected.

tests.test_config.SPLITTABLE_STDLIB_MODULES = frozenset({'curses', 'dbm', 'readline', 'sqlite3', 'tkinter'})

Standard library modules a distribution can ship apart from its base Python.

Each wraps a system library, so a packager can leave it out of the interpreter: FreeBSD serves sqlite3 as a separate pyXXX-sqlite3 package, and Debian serves tkinter as python3-tk. A module-level import of any of them kills every CLI built on click_extra at import time, on an interpreter that is otherwise complete, so each one is probed and imported at its point of use.

tests.test_config.eager_imports(tree)[source]

Top-level packages a module imports as soon as it is loaded.

Skips function bodies, which import at call time, and try blocks, which guard against a missing module. Those are the two shapes that survive an interpreter without the module.

Return type:

set[str]

tests.test_config.test_no_splittable_stdlib_module_imported_at_load_time()[source]

No module of the package imports a splittable module unconditionally.

tests.test_config.test_parser_probes_stay_off_the_import()[source]

Importing the package probes no parser: building a ConfigOption does.

tests.test_config.test_sqlite3_not_imported_by_the_package()[source]

Importing the package leaves sqlite3 out of sys.modules.

Covers the whole import graph, dependencies included, where test_no_splittable_stdlib_module_imported_at_load_time only reads the package’s own sources.

tests.test_config.test_sqlite_support_gates_the_format()[source]

SQLITE is enabled exactly when the standard library ships its bindings.

tests.test_config.test_plist_conf_file_overrides_defaults(invoke, simple_config_cli, assert_output_regex, tmp_path, plist_variant)[source]

Both the XML and the binary plist variants load through –config.

tests.test_config.test_plist_read_and_parse_conf(tmp_path)[source]

The default format patterns discover plist files by extension.

tests.test_config.test_validate_config_sqlite_valid(invoke, tmp_path)[source]

–validate-config accepts a valid SQLite configuration database.

tests.test_config.test_default_map_populated(invoke, create_config)[source]

Verify default_map structure when config values match CLI parameters.

Complements test_conf_metadata which only checks the empty default_map case (where no config values match the CLI’s parameter structure).

tests.test_config.test_merge_default_map_standalone(invoke)[source]

merge_default_map filters a config into default_map on its own.

load_conf bypasses this method by installing the merged config the validation pipeline already produced, so it is exercised here directly to cover the standalone entry point external callers rely on.

tests.test_config.test_merge_default_map_keeps_dotted_keys_in_opaque_subtree(invoke, caplog)[source]

merge_default_map leaves a dotted key inside an opaque sub-tree whole.

themes is the extension point every ConfigOption registers. A theme name that ends in a dot is data, so the merge logs no empty-segment warning.

tests.test_config.test_default_map_none_without_config(invoke)[source]

Verify default_map is left alone when –no-config is used.

tests.test_config.test_nested_subcommand_config(invoke, create_config)[source]

Config propagates through group -> subgroup -> leaf command.

tests.test_config.test_multiple_cli_shared_conf(invoke, create_config)[source]

Two CLIs sharing the same configuration file.

Refs: https://github.com/kdeldycke/click-extra/issues/1277

tests.test_config.test_params_template_not_mutated_across_invocations(invoke, create_config)[source]

Back-to-back invocations of the same CLI must not cross-contaminate via ConfigOption.params_template.

params_template is a @cached_property of the ConfigOption instance bound at decoration time, so it lives for the lifetime of the CLI object. _merge_into_template mutates its first argument in place; without a defensive copy, the cached template would accumulate values from earlier --config loads and leak them into default_map on subsequent invocations.

tests.test_config.test_lazy_group_config(invoke, create_config, tmp_path)[source]

Test that lazy groups work with config files.

Refs: https://github.com/kdeldycke/click-extra/issues/1332

tests.test_config.test_lazy_group_config_no_config_flag(invoke, create_config, tmp_path)[source]

Test that –no-config works with lazy groups.

tests.test_config.orchard_modules(tmp_path)[source]

Put the orchard_apple and orchard_banana command modules on sys.path.

Both modules leave sys.modules before and after the test, so the test sees which of them its CLI imports.

tests.test_config.test_lazy_group_included_params_imports(invoke, create_config, orchard_modules, included_params, expected_stdout)[source]

A configuration loads without importing the lazy subcommands the allowlist leaves out.

The walk building the parameter template skips each subcommand no allowed ID lies under. The section of a subcommand the allowlist leaves out does not reach it either, when the invocation imports it later.

tests.test_config.test_lazy_group_empty_allowlist_imports_no_subcommand(invoke, create_config, orchard_modules)[source]

included_params=() loads a configuration without importing any lazy subcommand.

The group runs on its own, so no dispatch imports a subcommand behind the walk’s back: a module in sys.modules is one the configuration load brought in.

tests.test_config.test_lazy_group_included_params_introspection(invoke, create_config, orchard_modules)[source]

--params, --export-config and --validate-config honor an allowlist for the subcommands its template walk skips.

The walk skips banana, so excluded_params lists none of its parameters.

tests.test_config.test_file_pattern(file_format_patterns, expected_pattern)[source]

Test the file_pattern property with different file format configurations.

tests.test_config.test_default_pattern_roaming_force_posix(roaming, force_posix, current_platform, expected_path, monkeypatch)[source]

Test that roaming and force_posix affect the default pattern generation.

tests.test_config.test_default_pattern_xdg_config_home(force_posix, tmp_path, monkeypatch)[source]

Test that default_pattern respects XDG_CONFIG_HOME on Linux.

tests.test_config.cascade_tree(tmp_path, monkeypatch)[source]

Point auto-discovery at a temporary app dir nested inside tmp_path.

Returns (tmp_path, app_dir): a config file dropped in app_dir is the most local source, one in tmp_path sits one level up the parent walk.

tests.test_config.test_cascade_merges_files_local_wins(invoke, cascade_tree)[source]

With cascade=True, local values win and parent values fill the gaps.

tests.test_config.test_cascade_list_local_wins(invoke, cascade_tree)[source]

A list two layers both set resolves to the most local one, like any value.

tests.test_config.test_cascade_merges_subcommand_sections(invoke, cascade_tree)[source]

A section two files both define resolves key by key, at every depth.

The local file wins each key it sets, and the keys only the parent file sets in the same section still apply. A list comes whole from the local file.

tests.test_config.test_no_cascade_first_file_wins(invoke, cascade_tree)[source]

Without cascade, the first parseable file wins entirely.

tests.test_config.test_cascade_single_layer_from_parent(invoke, cascade_tree)[source]

A lone file found up the walk is applied as-is.

tests.test_config.test_cascade_explicit_config_does_not_cascade(invoke, cascade_tree)[source]

An explicit –config pins a single source, even with cascade=True.

tests.test_config.test_cascade_conf_sources_metadata(invoke, cascade_tree)[source]

ctx.meta[CONF_SOURCES] lists every loaded file, highest precedence first.

tests.test_config.test_cascade_params_names_the_file_of_each_key(invoke, tmp_path, monkeypatch)[source]

The config_file column of --params names the file each value comes from.

With three files, the file furthest up the walk is named for the key only it sets: each default_map layer keeps the position of its file in CONF_SOURCES.

tests.test_config.test_cascade_conf_full_is_merged_view(invoke, cascade_tree)[source]

ctx.meta[CONF_FULL] exposes the deep-merged document.

tests.test_config.test_cascade_validation_error_names_file(invoke, cascade_tree, caplog)[source]

A strict-check failure in one layer names that file and exits 1.

tests.test_config.test_cascade_strict_check_skips_extension_points(invoke, cascade_tree)[source]

Each file of a cascade passes the strict check with an extension point in it.

The built-in themes table is one: its keys name themes, not parameters.

tests.test_config.test_read_and_parse_all_conf_orders_local_first(tmp_path)[source]

All parseable files are yielded, deepest first; unparsable ones skip.

tests.test_config.test_read_and_parse_conf_returns_first(tmp_path)[source]

read_and_parse_conf keeps its first-match contract on top of the generator.

tests.test_config.test_config_option_default_no_config(invoke, create_config)[source]

ConfigOption with default=NO_CONFIG disables autodiscovery.

tests.test_config.test_no_config_explicit_with_default_no_config(invoke)[source]

–no-config still prints the skip message even when NO_CONFIG is the default.

tests.test_config.test_excluded_params(invoke, create_config)[source]

Custom excluded_params prevents config values from being applied.

tests.test_config.test_included_params(invoke, create_config)[source]

Only parameters in included_params are loaded from config.

tests.test_config.test_included_params_empty(invoke, create_config)[source]

An empty included_params excludes all params from config.

tests.test_config.test_included_and_excluded_params_conflict()[source]

Providing both included_params and excluded_params raises ValueError.

tests.test_config.test_multiple_files_matching_glob(invoke, create_config, tmp_path)[source]

When multiple files match a glob, only the first parseable one is used.

tests.test_config.test_forced_flags_warnings(caplog)[source]

Warnings fire when SPLIT, BRACE or NODIR flags are missing.

The flags resolve on first use, not at construction, so reading them is what fires the warnings.

All format extensions are found in the search directory.

Regression test: before BRACE expansion, only the first format in the default pattern got the directory prefix: others were searched in CWD.

tests.test_config.test_root_dir_parent_search_finds_non_toml(invoke, tmp_path)[source]

Parent search with root_dir correctly finds non-TOML config in parents.

Before the root_dir refactoring, SPLIT patterns like *.toml|*.yaml only applied the directory prefix to the first sub-pattern. Now with root_dir, all sub-patterns are scoped to the correct directory.

tests.test_config.test_no_enabled_formats_raises()[source]

ValueError raised when all formats are disabled.

tests.test_config.test_pyproject_toml_in_defaults()[source]

ConfigOption() with default file_format_patterns includes PYPROJECT_TOML.

tests.test_config.test_pyproject_toml_tool_extraction(simple_config_cli)[source]

parse_conf with PYPROJECT_TOML returns the [tool] subsection.

tests.test_config.test_pyproject_toml_no_tool_section(simple_config_cli)[source]

pyproject.toml without [tool] returns empty dict.

tests.test_config.test_file_pattern_with_pyproject_toml()[source]

Explicit file_format_patterns with PYPROJECT_TOML works.

tests.test_config.test_pyproject_toml_overrides_defaults(invoke, create_config)[source]

End-to-end: a CLI with default formats reads from pyproject.toml.

tests.test_config.test_validate_config_valid(invoke, create_config)[source]

–validate-config with a valid config file exits 0.

tests.test_config.test_validate_config_accepts_a_glob(invoke, create_config)[source]

–validate-config takes every location --config takes, glob included.

tests.test_config.test_validate_config_accepts_a_url(invoke, httpserver)[source]

A configuration a CLI can load from a URL is one it can also validate.

tests.test_config.test_validate_config_accepts_the_null_device(invoke)[source]

/dev/null is a valid configuration, as --config loads it.

tests.test_config.test_validate_config_invalid_keys(invoke, create_config)[source]

–validate-config with unrecognized keys exits 1.

tests.test_config.test_extensionless_config(invoke, create_config, default_pattern, expected_help_default)[source]

Both broad and exact default patterns resolve the same .commandrc file.

The default parameter is printed as-is on the help screen, so an exact path is more informative than a broad glob, but both locate the same file.

tests.test_config.test_validate_config_unparsable(invoke, create_config)[source]

–validate-config with garbage content exits 2.

tests.test_config.test_validate_config_parses_to_nothing(invoke, create_config)[source]

–validate-config names the format a file parses to nothing with.

tests.test_config.test_validate_config_missing_file(invoke, tmp_path)[source]

–validate-config reports a nonexistent location from its own callback.

tests.test_config.test_validate_config_requires_config_option(invoke, tmp_path)[source]

–validate-config without @config_option raises RuntimeError.

tests.test_config.test_validate_config_pyproject_toml(invoke, create_config)[source]

–validate-config works with pyproject.toml [tool.*] sections.

tests.test_config.test_export_config_to_stdout(invoke)[source]

–export-config writes the resolved configuration and exits 0.

tests.test_config.test_export_config_captures_overrides(invoke)[source]

Command-line values are reflected in the dump.

tests.test_config.test_export_config_captures_environment(invoke)[source]

Values resolved from environment variables are reflected in the dump.

tests.test_config.test_export_config_numeric_values_keep_their_type(invoke)[source]

A command-line numeric scalar is dumped as a number, not a quoted string.

tests.test_config.test_export_config_round_trip(invoke, tmp_path, fmt)[source]

A dumped configuration reloads to the same values through –config.

tests.test_config.test_export_config_invalid_format(invoke)[source]

An unsupported format token is rejected by the Choice.

tests.test_config.test_export_config_without_its_writer(invoke, monkeypatch)[source]

A format whose writer is not installed fails with the install command.

tests.test_config.test_export_config_requires_config_option(invoke)[source]

–export-config without @config_option raises RuntimeError.

tests.test_config.test_export_config_standalone_falls_back_to_defaults(invoke)[source]

Without a captured command line (vanilla Command), defaults are dumped.

class tests.test_config.PlainGroupSubclass(name=None, commands=None, invoke_without_command=False, no_args_is_help=None, subcommand_metavar=None, chain=False, result_callback=None, **kwargs)[source]

Bases: Group

A bare click.Group subclass, like third-party plugin frameworks produce.

tests.test_config.SUBCOMMAND_GROUP_FACTORIES = (((<function group>,), (), 'click-group'), ((<function group>,), (), 'cloup-group'), ((<function decorator_factory.<locals>.decorator>,), (), 'click-extra-group'), ((functools.partial(<function group>, cls=<class 'tests.test_config.PlainGroupSubclass'>),), (), 'click-group-subclass'))

Every group flavor a --config option can be attached to.

The reserved subcommand keys are declared by config_option, so they must be honored by whichever group class carries that option, not only by click-extra’s own Group.

tests.test_config.make_subcommand_group(group_factory, *, chain, **kwargs)[source]

Build a subcmdcli group of group_factory, carrying --config.

Registers a debug and a sync subcommand, each echoing its own name. click-extra groups already ship --config, so the option is only added to the other flavors. Extra keyword arguments go to group_factory.

tests.test_config.test_default_subcommands_on_any_group_class(invoke, create_config, group_factory)[source]

_default_subcommands fires whatever group class carries --config.

tests.test_config.test_prepend_subcommands_on_any_group_class(invoke, create_config, group_factory)[source]

_prepend_subcommands fires whatever group class carries --config.

tests.test_config.SUBGROUP_CLASSES = (((<class 'click.core.Group'>,), (), 'click-subgroup'), ((<class 'cloup._commands.Group'>,), (), 'cloup-subgroup'), ((<class 'click_extra.commands.Group'>,), (), 'click-extra-subgroup'), ((<class 'tests.test_config.PlainGroupSubclass'>,), (), 'click-subgroup-subclass'))

Every group class a subgroup can be built on.

A subgroup reached through an ancestor’s --config holds no option of its own, so its class is all that could keep it from reading its own section.

tests.test_config.test_subcommands_on_a_subgroup(invoke, create_config, group_factory, subgroup_class, reserved_key, cli_args)[source]

A subgroup applies the reserved keys of its own configuration section.

Only the root group carries --config, so a subgroup is never visited as that option is processed. It reads [parent.subgroup] all the same, whatever classes the root and the subgroup are built on.

tests.test_config.app_dir_conf(tmp_path, monkeypatch)[source]

Point auto-discovery at a temporary app dir, and write a config into it.

Lets a test invoke a CLI with no argument at all, which is the only way to reach Click’s no_args_is_help short-circuit: passing --config would make the invocation non-empty.

tests.test_config.test_subcommands_beat_no_args_is_help(invoke, app_dir_conf, group_factory, reserved_key, chain)[source]

A configured subcommand outranks Click’s no-args help screen.

Click raises that screen before it runs any parameter, so --config alone never sees a bare invocation, whatever group class carries it.

tests.test_config.test_no_args_is_help_survives_a_silent_config(invoke, app_dir_conf, group_factory)[source]

The help screen stays when the configuration names no subcommand.

tests.test_config.test_bare_invocation_loads_the_configuration_once(invoke, create_config, group_factory)[source]

A bare invocation running a configured subcommand reads its document once.

--config is processed ahead of the no-args help screen, then again by Click’s parameter loop. A location set on purpose, here through the environment, makes each load print its status line.

tests.test_config.test_no_args_is_help_stays_without_a_config_option(invoke, app_dir_conf)[source]

A group carrying no --config never reads a configuration to dispatch on.

tests.test_config.test_group_parse_args_is_wrapped_once()[source]

Each new ConfigOption leaves the wrapper of click.Group as it found it.

tests.test_config.test_default_subcommand_selection(invoke, create_config, cli_subcmd, expected, unexpected)[source]

Config default is used when no subcommand given; CLI wins otherwise.

tests.test_config.test_default_subcommand_chained(invoke, create_config)[source]

chain=True group runs multiple config-listed subcommands in order.

tests.test_config.test_default_subcommand_config_errors(invoke, create_config, conf_value, error_fragment)[source]

Bad _default_subcommands values produce clear errors.

tests.test_config.test_default_subcommand_strict_mode_tolerance(invoke, create_config)[source]

strict=True config with _default_subcommands doesn’t raise.

tests.test_config.test_default_subcommand_validate_config_tolerance(invoke, create_config)[source]

–validate-config with _default_subcommands reports valid.

tests.test_config.test_default_subcommand_with_options(invoke, create_config)[source]

Default subcommand receives its config-provided options.

tests.test_config.test_default_subcommand_no_config(invoke)[source]

Normal behavior when no config file is loaded.

tests.test_config.test_default_subcommand_duplicates_warning(invoke, create_config)[source]

Duplicate entries in _default_subcommands are deduplicated with a warning.

tests.test_config.test_default_subcommand_cli_override_debug_log(invoke, create_config)[source]

Debug log emitted when CLI subcommands override config defaults.

tests.test_config.test_prepend_subcommand_selection(invoke, create_config, cli_subcmd, expected, unexpected)[source]

Prepend fires regardless of whether a CLI subcommand is given.

tests.test_config.test_prepend_subcommand_with_defaults(invoke, create_config, cli_subcmd, expect_backup)[source]

Prepend always applies; defaults only fire when no CLI subcommand given.

tests.test_config.test_prepend_subcommand_non_chained_error(invoke, create_config)[source]

Error on non-chained group.

tests.test_config.test_prepend_subcommand_config_errors(invoke, create_config, conf_value, error_fragment)[source]

Bad _prepend_subcommands values produce clear errors.

tests.test_config.test_prepend_subcommand_strict_mode_tolerance(invoke, create_config)[source]

strict=True config with _prepend_subcommands doesn’t raise.

tests.test_config.test_prepend_subcommand_validate_config_tolerance(invoke, create_config)[source]

–validate-config with _prepend_subcommands reports valid.

tests.test_config.test_prepend_subcommand_duplicates_warning(invoke, create_config)[source]

Duplicate entries in _prepend_subcommands are deduplicated with a warning.

tests.test_config.test_prepend_subcommand_info_log(invoke, create_config)[source]

INFO log emitted when _prepend_subcommands are injected.

tests.test_config.test_prepend_subcommand_multiple(invoke, create_config)[source]

Multiple prepend subcommands run in order.

tests.test_config.test_sanity_checks(caplog, default, file_format_patterns, flags, present, absent)[source]

_check_pattern_sanity emits (or suppresses) debug logs per pattern config.

tests.test_config.test_expand_dotted_keys(input_conf, expected)[source]
tests.test_config.test_dotted_keys_in_config(invoke, simple_config_cli, create_config, conf_name, conf_text)[source]

Dotted keys in config files are expanded into nested structures.

tests.test_config.test_expand_dotted_keys_conflict_warning(caplog, input_conf, warning_fragment)[source]

Scalar/dict conflicts on the same key emit a warning.

tests.test_config.test_expand_dotted_keys_empty_segments(caplog, input_conf)[source]

Dotted keys with empty segments are skipped with a warning.

tests.test_config.test_expand_dotted_keys_edge_cases(input_conf, expected)[source]
tests.test_config.test_expand_dotted_keys_strict_conflict(input_conf, error_fragment)[source]

Strict mode raises ValueError on type conflicts.

tests.test_config.test_expand_dotted_keys_strict_empty_segments(input_conf)[source]

Strict mode raises ValueError on dotted keys with empty segments.

tests.test_config.test_strict_conf_dotted_key_conflict(invoke, create_config, flag)[source]

Strict mode rejects a dotted-key type conflict with a validation error.

The error is reported like an unknown key, not raised as a traceback.

tests.test_config.test_expand_dotted_keys_keeps_a_key_that_is_not_a_string()[source]

YAML reads 1: as a number: such a key holds no dot to expand.

tests.test_config.test_conf_key_that_is_not_a_string(invoke, create_config, strict, flag, exit_code, message)[source]

A YAML key that reads as a number is a configuration error, not a traceback.

Strict mode and --validate-config reject it, and lax mode skips it.

tests.test_config.test_parse_jsonc(content, expected)[source]
tests.test_config.test_parse_jsonc_reports_the_line_of_an_error()[source]

A blanked block comment keeps its newlines, so the line numbers hold.

tests.test_config.test_parse_xml_conventions()[source]

Attributes, text, repeated tags and empty elements read as xmltodict read them.

tests.test_config.test_parse_xml_rejects_a_malformed_document()[source]
tests.test_config.test_serialize_xml_conventions()[source]
tests.test_config.test_xml_round_trip()[source]
tests.test_config.test_serialize_xml_respells_invalid_names()[source]

A key XML does not accept as a name is respelled, and reads back so.

A name starting with a digit gets a leading underscore, and a space becomes an underscore, in an element as in an attribute. A namespace prefix stays.

tests.test_config_discovery module

Test the search for configuration files, in click_extra.config.discovery.

tests.test_config_discovery.test_parent_patterns(tmp_path, search_parents, subdirs, create_file, expected_start)[source]
tests.test_config_discovery.test_parent_patterns_with_magic_pattern(tmp_path, pattern_factory, expected_factory)[source]

Test parent_patterns with glob patterns containing magic characters.

Magic pattern with search_parents=False yields only the original.

tests.test_config_discovery.test_parent_patterns_relative_path(tmp_path)[source]

Test parent_patterns resolves relative paths to absolute.

tests.test_config_discovery.test_parent_patterns_stop_at_path(tmp_path)[source]

stop_at as a path limits the parent directory walk.

tests.test_config_discovery.test_parent_patterns_stop_at_vcs(tmp_path, has_vcs, expected_bounded)[source]

stop_at=VCS stops at VCS root, or walks to filesystem root if none.

tests.test_config_discovery.test_parent_patterns_inaccessible_directory(tmp_path)[source]

Walk stops at an inaccessible directory.

tests.test_config_discovery.test_find_vcs_root(tmp_path, vcs_dir, expected)[source]

Test _find_vcs_root with .git, .hg, and no VCS markers.

tests.test_config_discovery.test_default_magic_characters_match_wcmatch()[source]

The characters _literal_base splits on are the ones wcmatch reads as magic.

Checked under the default search flags, over every printable character.

tests.test_config_discovery.test_literal_base(tmp_path, pattern_factory, expected_factory)[source]

_literal_base stops at the first segment holding glob syntax.

tests.test_config_discovery.test_literal_base_needs_the_default_search_flags()[source]

Custom search flags take wcmatch to read, so no folder is derived.

tests.test_config_discovery.test_search_skips_the_glob_of_a_missing_folder(tmp_path, monkeypatch)[source]

No file can match in a folder that does not exist, so it is never globbed.

tests.test_config_discovery.test_parent_search_goes_on_above_a_missing_folder(tmp_path)[source]

The parent walk still finds a file above a folder that does not exist.

tests.test_config_discovery.test_plain_run_loads_no_search_dependency(tmp_path)[source]

A plain run without a configuration file loads neither extra_platforms nor wcmatch.

Every invocation searches the default configuration folder, which rarely exists: a missing folder is ruled out before wcmatch is imported. The probe runs in a pristine interpreter, since pytest itself has both loaded already. The default folder is pointed at a place that does not exist, on every platform click.get_app_dir() knows, and the working directory holds no pyproject.toml.

tests.test_config_flags module

tests.test_config_flags.test_config_table_to_flags(table, expected)[source]
Return type:

None

tests.test_config_schema module

Tests for typed configuration schemas, validation, and extension points.

tests.test_config_schema.test_normalize_config_keys()[source]
tests.test_config_schema.test_normalize_config_keys_keeps_a_key_that_is_not_a_string()[source]

YAML reads 1: as a number: such a key has no hyphen to replace.

tests.test_config_schema.test_config_schema_dataclass(invoke, create_config)[source]

Dataclass schemas are auto-detected and instantiated with normalized keys.

tests.test_config_schema.test_config_schema_cascade(invoke, tmp_path, monkeypatch)[source]

With cascade=True, the schema is built from the merged view.

tests.test_config_schema.test_config_schema_callable(invoke, create_config)[source]

A plain callable can be used as config_schema.

tests.test_config_schema.test_config_schema_no_config_file(invoke)[source]

When no config file is found, schema defaults are used.

tests.test_config_schema.test_config_schema_dataclass_defaults(invoke, create_config)[source]

Dataclass defaults are used for fields not present in the config file.

tests.test_config_schema.test_fallback_sections(invoke, create_config)[source]

Legacy section names are recognized with a deprecation warning.

tests.test_config_schema.test_fallback_section_warns_once(invoke, create_config)[source]

A legacy section loaded with its themes is reported once, not per reader.

tests.test_config_schema.test_fallback_sections_prefers_current(invoke, create_config)[source]

When both current and legacy sections exist, current wins.

tests.test_config_schema.test_config_schema_multiple_formats(invoke, create_config, conf_name, conf_text)[source]

Config schema works with YAML and JSON, not just TOML.

tests.test_config_schema.test_config_schema_on_config_option_directly(invoke, create_config)[source]

Config schema can be set directly on ConfigOption via the decorator.

tests.test_config_schema.test_get_tool_config_defaults_to_current_context(invoke, create_config)[source]

get_tool_config() works without passing ctx explicitly.

tests.test_config_schema.test_flatten_config_keys()[source]
tests.test_config_schema.test_flatten_config_keys_with_normalize()[source]

flatten + normalize maps nested kebab-case config to flat snake_case fields.

tests.test_config_schema.test_config_schema_nested_toml(invoke, create_config)[source]

Nested TOML sub-tables map to flat dataclass fields via flattening.

tests.test_config_schema.test_config_schema_strict_rejects_unknown(invoke, create_config)[source]

schema_strict=True raises ValueError on unrecognized config keys.

tests.test_config_schema.test_config_schema_strict_passes_when_valid(invoke, create_config)[source]

schema_strict=True does not raise when all config keys are known.

tests.test_config_schema.test_config_schema_strict_with_nested(invoke, create_config)[source]

schema_strict=True validates flattened keys from nested sub-tables.

tests.test_config_schema.test_config_schema_lax_warns_unknown_when_schema_only(invoke, create_config)[source]

A schema-only section warns on unknown keys instead of dropping them silently.

included_params=() means no CLI parameter is merged from the app’s section, so any key the schema does not know can only be a typo: lax mode then logs a warning while still loading the known fields.

tests.test_config_schema.SCHEMA_ONLY_CONF = '[schema-only-cli]\nknown-field = "good"\ntypo-field = "oops"\n'

A schema-only section, holding one key its schema does not know.

class tests.test_config_schema.SchemaOnlyConfig(known_field='default')[source]

Bases: object

known_field: str = 'default'
tests.test_config_schema.test_schema_only_section_loads_in_strict_mode(invoke, create_config)[source]

The CLI-parameter strict check leaves a schema-only section to the schema.

No parameter is read from it, so the schema alone reports the key it does not know, which lax mode only warns about.

tests.test_config_schema.test_validate_config_warns_unknown_when_schema_only(invoke, create_config)[source]

--validate-config accepts a schema-only section, naming its unknown key.

The file stays valid, as it loads: lax mode only warns.

tests.test_config_schema.test_config_schema_lax_silent_when_params_merged(invoke, create_config)[source]

Without included_params=(), lax mode stays silent on unknown keys.

The section may legitimately mix CLI parameter keys with schema fields, so a key unknown to the schema is not necessarily a typo.

class tests.test_config_schema.MixedNestedConfig(depth=0)[source]

Bases: object

depth: int = 0
class tests.test_config_schema.MixedConfig(extra_stuff='', nested=MixedNestedConfig(), sub_section_key=0)[source]

Bases: object

extra_stuff: str = ''
nested: MixedNestedConfig
sub_section_key: int = 0
tests.test_config_schema.MIXED_CONF = '[mixed-cli]\nfruit = "plum"\nextra-stuff = "x"\n\n[mixed-cli.nested]\ndepth = 3\n\n[mixed-cli.sub-section]\nkey = 2\n'

A section mixing the --fruit parameter with the fields of MixedConfig.

Each field takes a shape a schema reads: a scalar, a nested dataclass table, and a table flattened into a field name.

tests.test_config_schema.test_strict_check_leaves_schema_keys_to_the_schema(invoke, create_config, kwargs, args, expected)[source]

The CLI-parameter strict check accepts the keys the dataclass schema reads.

--validate-config always runs the check, and config_strict=True runs it as the file loads.

tests.test_config_schema.test_strict_check_rejects_a_key_neither_reads(invoke, create_config)[source]

A key matching neither a parameter nor a schema field stays an error.

class tests.test_config_schema.PinnedConfig(extra_stuff='', nested=MixedNestedConfig(), sub_section_key=0, pinned=dict(), axes=dict())[source]

Bases: object

extra_stuff: str = ''
nested: MixedNestedConfig
sub_section_key: int = 0
pinned: dict
axes: dict[str, str]
tests.test_config_schema.test_schema_keys(section, expected)[source]

_schema_keys returns the keys of a section the dataclass reads in full.

tests.test_config_schema.test_schema_keys_of_a_callable_schema()[source]

A callable schema does not tell which keys it reads.

tests.test_config_schema.test_make_schema_callable_warn_unknown(caplog)[source]

warn_unknown=True logs unknown keys, recursing into nested dataclasses.

tests.test_config_schema.test_make_schema_callable_lax_default_is_silent(caplog)[source]

Without warn_unknown, lax mode keeps dropping unknown keys silently.

tests.test_config_schema.test_pyproject_toml_cwd_discovery(invoke, tmp_path, monkeypatch)[source]

pyproject.toml in CWD is discovered automatically without –config.

tests.test_config_schema.test_pyproject_toml_cwd_discovery_walks_up(invoke, tmp_path, monkeypatch)[source]

pyproject.toml discovery walks up from CWD to parent directories.

tests.test_config_schema.test_pyproject_toml_explicit_config_skips_cwd(invoke, create_config, tmp_path, monkeypatch)[source]

Explicit –config skips CWD pyproject.toml discovery.

tests.test_config_schema.test_pyproject_toml_cwd_skips_unrelated_tool_section(invoke, tmp_path, monkeypatch)[source]

A pyproject.toml without [tool.<cli_name>] is skipped.

Regression: a pyproject.toml carrying only unrelated [tool.X] sections (like a dotfiles repo’s [tool.ruff]) used to shadow the user’s app-dir config. It must now be ignored so the CLI falls back to its defaults instead of inheriting an unrelated project’s settings.

tests.test_config_schema.test_pyproject_toml_cwd_walks_past_unrelated_tool_section(invoke, tmp_path, monkeypatch)[source]

CWD walk continues past a pyproject.toml lacking [tool.<cli_name>].

A nearer pyproject.toml that only carries unrelated [tool.X] sections must not stop the upward walk: a parent pyproject.toml with a matching [tool.<cli_name>] section should still be discovered.

tests.test_config_schema.test_pyproject_toml_cwd_mixed_tool_sections(invoke, tmp_path, monkeypatch)[source]

[tool.<cli_name>] is picked from a pyproject.toml that also has others.

tests.test_config_schema.test_pyproject_toml_cwd_unrelated_does_not_shadow_app_dir(invoke, tmp_path, monkeypatch)[source]

An unrelated pyproject.toml falls through to the app-dir config.

Directly exercises the documented intent of the fix: when CWD contains a pyproject.toml whose only [tool.X] sections are unrelated to the CLI, the walk must give up so the standard app-dir search can find the user’s actual config and apply it.

tests.test_config_schema.test_flatten_config_keys_opaque()[source]

opaque_keys stops flattening at matching key boundaries.

tests.test_config_schema.test_flatten_config_keys_opaque_nested()[source]

opaque_keys works at deeper nesting levels.

tests.test_config_schema.test_schema_type_aware_flattening(invoke, create_config)[source]

dict-typed dataclass fields stop flattening automatically.

tests.test_config_schema.test_schema_field_metadata_config_path(invoke, create_config)[source]

click_extra.config_path extracts a value at a dotted TOML path.

tests.test_config_schema.test_schema_field_metadata_normalize_keys_true(invoke, create_config)[source]

click_extra.normalize_keys defaults to True: keys are normalized.

tests.test_config_schema.test_schema_nested_dataclass(invoke, create_config)[source]

Nested dataclass fields are recursively instantiated.

tests.test_config_schema.test_schema_nested_dataclass_with_opaque_fields(invoke, create_config)[source]

Nested dataclass with dict-typed fields preserves opaque keys.

tests.test_config_schema.test_schema_nested_dataclass_defaults(invoke, create_config)[source]

Nested dataclass uses defaults when config section is absent.

tests.test_config_schema.test_strict_skips_opaque_dict_field(invoke, create_config)[source]

Strict mode does not reject keys inside a dict[str, X] schema field.

A field typed as dict[str, dict] is user-controlled: the keys are data, not CLI flag names. Click-extra strips that sub-tree before running its unknown-key check so app extensions don’t trip strict mode.

tests.test_config_schema.test_strict_skips_opaque_metadata_field(invoke, create_config)[source]

Strict mode also honors the EXTENSION_METADATA_KEY marker on a field whose Python type is not a mapping.

tests.test_config_schema.test_validate_config_skips_opaque_field(invoke, create_config)[source]

–validate-config also skips opaque sub-trees, so a config that the runtime accepts also passes validation.

tests.test_config_schema.test_opaque_field_keeps_dotted_keys(invoke, create_config, caplog, strict)[source]

A dot in a key of an opaque sub-tree is part of the key, not a path.

Dotted-key expansion skips extension points, whose keys are data. A key with a trailing, inner or doubled dot reaches the schema whole, logs no empty-segment warning and passes strict mode.

tests.test_config_schema.test_config_validator_runs_and_fails_under_validate_config(invoke, create_config)[source]

A registered ConfigValidator runs during --validate-config and surfaces its ValidationError with a path rooted at the config file.

tests.test_config_schema.test_config_validator_runs_during_normal_load(invoke, create_config)[source]

A misconfigured opaque sub-tree fails fast during normal config loading, not only under --validate-config.

tests.test_config_schema.test_config_validator_extension_path_strips_strict_check(invoke, create_config)[source]

A ConfigValidator(extension_path=…) registration alone is enough to skip strict-check on that path, even when the schema doesn’t have the field.

tests.test_config_schema.test_config_validator_collects_all_errors(invoke, create_config)[source]

--validate-config reports every detected error in one pass.

A config with both an unknown CLI flag key and a validator-flagged field should surface both messages before the run exits non-zero, so the user sees the full punch list.

tests.test_config_schema.test_collect_opaque_paths_from_schema()[source]

Schema introspection picks up dict-typed fields, metadata-marked fields, and nested-dataclass opaque fields with dotted prefixes.

tests.test_config_schema.test_schema_strict_honors_extension_metadata_on_non_mapping_field(invoke, create_config)[source]

schema_strict must not descend into an EXTENSION_METADATA_KEY-marked field whose Python type is not a mapping.

Before the opaque-path unification, the outer strip honored the marker but the dataclass adapter’s own flatten boundary inspected only the type hint, so the marked sub-tree was flattened into dotted keys and rejected as unknown.

tests.test_config_schema.test_run_config_validation_valid_document()[source]

A clean document yields an ok report with the schema instance built and every opaque sub-tree extracted.

tests.test_config_schema.test_run_config_validation_exposes_merged_conf()[source]

A passing strict check carries the template-filtered config as merged_conf, with recognized values merged in and unknown keys dropped.

tests.test_config_schema.test_run_config_validation_collects_all_then_short_circuits()[source]

collect_all=True gathers errors from every stage in order; collect_all=False stops after the first.

tests.test_config_schema.test_run_config_validation_wraps_schema_errors()[source]

A schema_strict failure is recorded as a ValidationError with the schema_error code, and the message is preserved verbatim.

tests.test_config_schema.test_run_config_validation_no_schema_no_template()[source]

With neither a template nor a schema, the report is ok and carries no schema instance.

tests.test_config_schema.test_select_app_section_of_an_empty_app_name_is_the_document()[source]

An empty app name reads the whole document as the section.

_rooted_path and _scope_app_sections already read it that way.

tests.test_config_schema.test_run_config_validation_of_an_empty_app_name()[source]

An empty app name feeds the whole document to the schema and validators.

tests.test_config_schema.test_run_config_validation_reports_a_key_that_is_not_a_string()[source]

A key YAML reads as a number is an invalid key to the strict check, and an unknown option to a strict schema, never a crash.

tests.test_config_schema.test_make_schema_callable_coerces_dict_to_dataclass()[source]

The public make_schema_callable turns a raw config dict into a dataclass.

tests.test_config_schema.test_field_docstrings_returns_full_text()[source]

Attribute docstrings are recovered whole, paragraph breaks preserved.

tests.test_config_schema.test_field_docstrings_degrades_without_source()[source]

A class defined through exec has no source: the mapping is empty.

tests.test_config_schema.test_schema_field_infos_flat_schema()[source]

Keys are kebab-cased and sorted; defaults, types, and summaries surface.

tests.test_config_schema.test_schema_field_infos_nested_and_config_path()[source]

Nested dataclasses expand to dotted keys; config_path metadata wins.

tests.test_config_schema.test_schema_field_infos_sorts_segment_wise()[source]

A sub-table’s options stay contiguous when a sibling shares their prefix.

Plain string sort would interleave pear-cellar between pear.crates and pear.pickers (in ASCII - sorts before .); segment-wise sort keeps the pear table’s options together.

tests.test_config_schema.test_schema_field_infos_rejects_non_dataclass()[source]

A non-dataclass schema is refused with a clear error.

tests.test_config_schema.test_validation_codes_are_snake_case()[source]

Every code Click Extra raises a ValidationError with is in snake case.

Scans the source of the package for the code of each ValidationError it builds, so a new check cannot bring another spelling back.

tests.test_context module

Tests for click_extra.context, and the context class that carries it.

Covers four surfaces:

tests.test_context.KEY_CONSTANTS: tuple[tuple[str, str], ...] = (('RAW_ARGS', 'click_extra.raw_args'), ('INVOCATION_NAME', 'click_extra.invocation_name'), ('CONF_SOURCE', 'click_extra.conf_source'), ('CONF_FULL', 'click_extra.conf_full'), ('CONF_SOURCES', 'click_extra.conf_sources'), ('TOOL_CONFIG', 'click_extra.tool_config'), ('SUBCOMMANDS_APPLIED', 'click_extra.subcommands_applied'), ('VERBOSITY_LEVEL', 'click_extra.verbosity_level'), ('VERBOSITY', 'click_extra.verbosity'), ('DEBUG', 'click_extra.debug'), ('VERBOSE', 'click_extra.verbose'), ('QUIET', 'click_extra.quiet'), ('START_TIME', 'click_extra.start_time'), ('JOBS', 'click_extra.jobs'), ('TABLE_FORMAT', 'click_extra.table_format'), ('SORT_BY', 'click_extra.sort_by'), ('TABLE_SORT_KEY', 'click_extra.table_sort_key'), ('COLUMNS', 'click_extra.columns'), ('THEME', 'click_extra.theme.active'), ('THEME_OVERRIDES', 'click_extra.theme.overrides'), ('TELEMETRY', 'click_extra.telemetry'), ('PROGRESS', 'click_extra.progress'), ('ACCESSIBLE', 'click_extra.accessible'), ('ZERO_EXIT', 'click_extra.zero_exit'))

Pairs of (attribute name, raw string key) for every registered ctx.meta entry. Single source of truth used by every parametrized registry test below.

tests.test_context.test_key_constant_value(attr, expected)[source]

Each registered constant binds to its documented string value.

Pins the spelling of every key so a rename of the literal does not silently break downstream code that reads ctx.meta["click_extra.X"].

Return type:

None

tests.test_context.test_key_uses_namespace_prefix(key)[source]

Every registered key sits under META_NAMESPACE.

Return type:

None

tests.test_context.test_keys_are_unique()[source]

No two registry constants share a string value.

Return type:

None

tests.test_context.test_registry_covers_all_module_constants()[source]

KEY_CONSTANTS lists every public namespace-prefixed constant.

Drift detector: when a new ctx.meta key is added to click_extra.context but not to KEY_CONSTANTS, this test fails so the maintainer remembers to update both sides.

Return type:

None

tests.test_context.NON_META_KEYS = frozenset({'click_extra.config_docstring_heading', 'click_extra.config_path', 'click_extra.extension', 'click_extra.normalize_keys', 'click_extra.tabs_vertical', 'click_extra.todo_unrendered'})

Literals sharing META_NAMESPACE that name no ctx.meta key.

tests.test_context.test_meta_keys_are_spelled_in_context_only()[source]

No module but click_extra.context spells a ctx.meta key.

Every other module reads its keys from there: none builds one from META_NAMESPACE, or writes one as a literal.

Return type:

None

tests.test_context.test_internal_keys_are_private_and_unique()[source]

The keys only Click Extra reads sit under click_extra._, apart from the public ones.

Return type:

None

tests.test_context.test_get_returns_default_for_missing_key()[source]

get() mirrors dict.get semantics.

Return type:

None

tests.test_context.test_set_then_get_round_trip()[source]

set() is observable through get().

Return type:

None

tests.test_context.test_setdefault_stores_the_factory_value_once()[source]

setdefault() creates a missing value, then keeps it.

Return type:

None

tests.test_context.test_context_uses_help_formatter()[source]

Context installs Click Extra’s colorized formatter.

Return type:

None

tests.test_context.test_context_meta_kwarg_seeds_ctx_meta()[source]

The meta= kwarg populates ctx.meta at construction time.

Return type:

None

tests.test_context.test_context_meta_kwarg_omitted_leaves_meta_empty()[source]

Without meta=, ctx.meta is an empty dict, not None.

Return type:

None

tests.test_context.test_context_color(monkeypatch, parent_color, child_color, expected)[source]

Context color resolution covers every parent/child path.

Root contexts without an explicit color= resolve the GNU auto default: with no color environment variable they stay at None (TTY detection). Child contexts inherit the parent’s color unless they override it explicitly.

Return type:

None

tests.test_context.test_context_posixly_correct(monkeypatch, env_present, expected)[source]

POSIXLY_CORRECT flips allow_interspersed_args off when present.

A plain click.Command defaults the flag to True, so the unset case leaves interspersing enabled and the set case disables it.

Return type:

None

tests.test_context.test_posixly_correct_presence_overrides_explicit_true(monkeypatch)[source]

An empty POSIXLY_CORRECT still wins over an explicit True.

Presence alone triggers POSIX parsing (matching GNU getopt), regardless of value, and it takes precedence over a developer-supplied allow_interspersed_args=True.

Return type:

None

tests.test_context.test_posixly_correct_stops_option_parsing_at_first_argument()[source]

End-to-end: parsing stops at the first positional under POSIXLY_CORRECT.

Without the variable, the option interleaves with arguments (GNU style). With it set, the first positional ends option parsing, so the option keeps its default and the remaining tokens fall into the variadic argument.

Return type:

None

tests.test_context.test_lazy_meta_dict_resolves_on_first_access()[source]

Reading a lazy key triggers exactly one source attribute access.

Return type:

None

tests.test_context.test_lazy_meta_dict_caches_after_resolution()[source]

Subsequent reads of the same lazy key do not re-hit the source.

Return type:

None

tests.test_context.test_lazy_meta_dict_get_resolves_lazy_key()[source]

.get(lazy_key) triggers resolution like __getitem__.

Return type:

None

tests.test_context.test_lazy_meta_dict_get_returns_default_for_unknown_key()[source]

.get(unknown, default) returns the default without touching source.

Return type:

None

tests.test_context.test_lazy_meta_dict_contains_includes_lazy_keys_without_resolving()[source]

in returns True for declared lazy keys before any resolution.

Return type:

None

tests.test_context.test_lazy_meta_dict_preserves_base_entries()[source]

Entries from the wrapped base dict remain accessible.

Return type:

None

tests.test_context.test_lazy_meta_dict_independent_keys_resolve_independently()[source]

Resolving one lazy key does not resolve sibling lazy keys.

Return type:

None

tests.test_context.LAZY_ENTRIES = {'click_extra.alpha': 'alpha-value', 'click_extra.beta': 'beta-value', 'existing.key': 'existing-value'}

Every entry of the lazy dicts below once resolved, in iteration order.

tests.test_context.test_lazy_meta_dict_lists_pending_keys_without_resolving()[source]

len(), iteration and keys() count the keys in claims.

Listing a key does not read its value, so nothing resolves.

Return type:

None

tests.test_context.test_lazy_meta_dict_reads_pending_values(read)[source]

Whatever reads every value resolves the pending keys, and sees them all.

Return type:

None

tests.test_context.test_lazy_meta_dict_equality_reads_pending_values()[source]

Equality compares every entry, pending ones included, on both sides.

Return type:

None

tests.test_context.test_lazy_meta_dict_copy_keeps_pending_keys_pending()[source]

A copy lists the pending keys, and resolves them only when read.

Return type:

None

tests.test_context.test_lazy_meta_dict_removed_key_stays_removed(remove, removed)[source]

A pending key removed from the mapping does not come back.

Return type:

None

tests.test_context.test_lazy_meta_dict_pop_returns_pending_values()[source]

pop() and popitem() resolve the pending key they remove.

Return type:

None

tests.test_context.test_lazy_meta_dict_setdefault_resolves_pending_key()[source]

setdefault() keeps the value of a pending key rather than the default.

Return type:

None

tests.test_context.test_lazy_meta_dict_wraps_another_without_resolving()[source]

Wrapping a lazy dict, as each --version of a command tree does, keeps its pending keys pending.

Return type:

None

tests.test_context.test_pass_context_typed_for_enhanced_context(invoke)[source]

@pass_context accepts a handler typed with the enhanced Context.

Click’s own pass_context is typed for the base click.Context, so annotating the handler with click-extra’s Context (to reach its extra helpers) would fail static type checks by parameter contravariance. This must both type-check (mypy covers this file) and forward the active enhanced Context at runtime.

tests.test_docs_anchors module

Every Markdown fragment link resolves in a raw-Markdown reader.

Three slug algorithms compete over the same headings. docutils.nodes.make_id derives the ids Sphinx renders, and myst_heading_slug_func in docs/conf.py points MyST at it, so a link written parameters.md#params-option is correct on the published site. GitHub, and the lychee checker that models it, slug the raw Markdown instead. They keep the leading -- and the underscores that make_id strips or folds, drop a dot where it writes a hyphen, and write one hyphen per space where it writes one per run. The same link thus finds nothing when the page is read on GitHub.

Neither reader can be dropped, so a heading whose two slugs disagree carries an <a name="…"> companion holding the make_id spelling. That HTML anchor is invisible to Sphinx (it renders no id, so the page keeps one id per section) and authoritative for GitHub and lychee.

Docstrings take a third path to the site. click_extra.sphinx.myst_docstrings passes their link targets to reST unchanged. A page.md#anchor target, which myst-parser resolves in a page, thus ships as written, and the site serves no .md file. So a docstring links a page with :doc: and a section with :ref:.

The check runs on text alone: no build, no network, no platform floor. Its counterpart on the built HTML is test_sphinx_crossrefs.py, which is skipped off Linux, and the check-broken-links CI job, which files an issue instead of failing a run. Both let a broken anchor sit for a full cycle.

tests.test_docs_anchors.lychee_excludes()[source]

Read the link patterns [tool.lychee] waives, so both checkers agree.

The palette anchors of docs/theme.md are the ones that matter here: a {python:render} loop emits their headings at build time, so no reader of the raw Markdown can see them.

Return type:

tuple[Pattern[str], ...]

tests.test_docs_anchors.closes_fence(marker, opening)[source]

Tell whether a line’s fence marker closes the block opening started.

Return type:

bool

tests.test_docs_anchors.uncoded_lines(content, keep_directive_bodies=False)[source]

Number and return the lines sitting outside a code fence.

lychee ignores fenced content, so a link in an example is not a link. The docstring converter renders the body of a directive fence like :note:, so keep_directive_bodies returns those lines, without the fence markers.

Return type:

list[tuple[int, str]]

tests.test_docs_anchors.uncoded_spans(line)[source]

Drop a line’s code spans, so a link or anchor quoted in prose is not read.

Headings are slugged from the raw line instead: GitHub keeps the text of a code span in a heading’s slug.

Return type:

str

tests.test_docs_anchors.github_slug(heading)[source]

Slug a heading the way GitHub does, which is what lychee models.

Lowercase, drop every character that is neither a word character, a space nor a hyphen, then replace each space with a hyphen. Backticks vanish with the rest of the punctuation, so a leading -- and an underscore survive, and a dot goes with no separator. Each space gives its own hyphen: a / dropped from between two spaces leaves --, where make_id writes one -.

Return type:

str

tests.test_docs_anchors.readable_anchors(path)[source]

Collect every fragment a raw-Markdown reader can reach on a page.

Duplicate heading slugs are numbered the way GitHub numbers them, so a heading repeated three times answers to slug, slug-1 and slug-2.

Return type:

set[str]

tests.test_docs_anchors.unresolved_fragments(paths=(PosixPath('/home/runner/work/click-extra/click-extra/AGENTS.md'), PosixPath('/home/runner/work/click-extra/click-extra/changelog.md'), PosixPath('/home/runner/work/click-extra/click-extra/readme.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/benchmark.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/binaries.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/carapace.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/changelog.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/cli.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/click_extra.capture.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/click_extra.config.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/click_extra.markdown.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/click_extra.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/click_extra.sphinx.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/code-of-conduct.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/colorize.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/commands.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/config-discovery.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/config-formats.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/config-schema.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/config-validation.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/config.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/context.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/contributing.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/decorators.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/envvar.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/execution.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/index.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/install.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/layout.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/license.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/logging.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/machine-readable.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/man-page.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/mkdocs.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/multicall.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/myst-docstrings.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/packaging.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/parameters.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/pygments.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/pytest.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/python-directives.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/screenshots.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/snippets.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/sphinx.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/spinner.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/styling.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/table.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/telemetry.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/test-suite.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/testing.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/tests.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/tests.mkdocs.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/tests.sphinx.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/theme.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/todolist.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/tree.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/tutorial.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/typer.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/types.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/upstream.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/version.md'), PosixPath('/home/runner/work/click-extra/click-extra/docs/wrap.md')))[source]

Report every fragment link no raw-Markdown reader can follow.

Each entry names the source location, the target and the fragment, so the failure says where to add the missing <a name="…">.

Return type:

list[str]

tests.test_docs_anchors.rendered_prose(path)[source]

Collect the prose autodoc renders from a module, with the line it starts on.

Every bare string statement counts, so attribute docstrings join the module, class and function ones, and so does every #: comment. A docstring loses its indentation after the first line, so a fence opens at the margin as it does in a page.

Return type:

list[tuple[int, str]]

Report every docstring link that names a page by its .md file.

Return type:

list[tuple[Path, int, str]]

tests.test_docs_anchors.test_markdown_files_are_collected()[source]

The glob feeding the check still finds the documentation.

No fragment link dangles for a reader of the raw Markdown.

A failure means the target heading slugs differently on GitHub than make_id spells it. Add <a name="{fragment}"></a> above that heading, rather than rewording the link: the make_id spelling is what the built site renders.

tests.test_docs_anchors.test_github_slug(heading, expected)[source]

The slugger reproduces the spellings that diverge from make_id.

Every case is a heading of this documentation whose two slugs disagree, which is what makes its <a name="…"> companion necessary.

tests.test_docs_anchors.test_uncoded_spans(line, expected)[source]

A span goes whatever its backtick count, and a lone backtick stays.

A link or an anchor quoted in a code span is prose, as lychee reads it.

tests.test_docs_anchors.test_package_sources_are_collected()[source]

The glob feeding the docstring check still finds the package.

No docstring links a documentation page by its .md file.

A failure names a link that ships dead. Link the page with :doc:, or its section with :ref: after adding a (label)= target above the heading.

The scan reads every docstring autodoc renders, and only their prose.

tests.test_docs_api module

Every public module is indexed on the API page of its package.

sphinx-apidoc writes the API pages: docs/click_extra.md and one page per subpackage, plus the same set for tests. It writes each page once. A later run keeps the page already in place, as click_extra.markdown.myst.convert_rst_files_in_directory() does, so a module added afterwards never reaches its API page. Its only automodule block then tends to land on a guide page, and the build accepts that without a warning.

So the API page owns the index entry of each module, and every guide-page block carries :no-index:. This test reads the source trees and the API pages as text, and fails naming each public module that no API page indexes. The check imports nothing, so it needs none of the optional dependencies a module pulls in.

tests.test_docs_api.public_modules(package)[source]

Name every public module of package, read from its source tree.

A name with an underscore-prefixed part is private, and so is everything below it: click_extra._utils, click_extra.sphinx._base and click_extra.__main__ are all skipped. A package is named by its __init__.py.

Return type:

list[str]

tests.test_docs_api.api_pages(package)[source]

Return the API pages of package, in the documentation tree.

Return type:

list[Path]

tests.test_docs_api.indexed_modules(pages)[source]

Collect each module an automodule directive indexes on pages.

A block carrying :no-index: registers no index entry, so it does not count.

Return type:

set[str]

tests.test_docs_api.test_package_is_collected(package)[source]

The globs feeding the check still find the modules and their API pages.

tests.test_docs_api.test_deprecation_shims_still_exist()[source]

An exemption goes in the same change that deletes its shim.

tests.test_docs_api.test_every_module_is_indexed_on_an_api_page(package)[source]

No public module is left to a guide page, or to no page at all.

A failure names a module that no API page indexes. Add its section to the API page of its package, in alphabetical order with the others, and give any guide-page block of the same module :no-index:.

tests.test_docs_api.test_indexed_modules(tmp_path)[source]

A :no-index: block counts for nothing, wherever its option sits.

tests.test_docs_directives module

Every {click:run} block pins the outcome of the CLI it invokes.

The directive executes its body at build time through a bare exec() (click_extra.sphinx.click_runner), so any exception a block raises aborts sphinx-build. That makes the documentation build a real gate, but only over the blocks that raise: a block asserting nothing renders whatever the CLI printed, and a broken invocation prints a usage error into the published page while the build stays green. A stale option has shipped that way.

So the build cannot be the only check, and this test supplies the half it misses: it reads the Markdown as text and requires each block to carry at least one assertion. What that assertion pins is the author’s call. An exact result.exit_code is the strongest form, a containment check on result.stdout still fails when the CLI errors instead of running, and either one turns a silent page-level regression into a red build.

The check runs on text alone: no build, no network, no platform floor. It parses the fences through click_extra.markdown.blocks.fence_spans(), which consumes a fence as an opaque unit, so a {click:run} example quoted inside a longer code-block fence is never mistaken for a live directive.

tests.test_docs_directives.run_blocks(path)[source]

Return each {click:run} body in path, as (line number, source).

The leading :option: lines the directive consumes are dropped, so the remainder parses as plain Python. Line numbers are 1-based, naming the opening fence.

Return type:

list[tuple[int, str]]

tests.test_docs_directives.test_markdown_files_are_collected()[source]

The glob feeding the check still finds the documentation.

tests.test_docs_directives.test_run_directives_are_collected()[source]

The fence parser still recognizes the directives it is pointed at.

tests.test_docs_directives.test_run_directives_assert_their_outcome(path)[source]

No {click:run} block renders its CLI’s output unchecked.

tests.test_envvar module

tests.test_envvar.test_merge_envvar_ids(envvars, result)[source]
tests.test_envvar.test_parse_envvar_flag(value, expected)[source]
tests.test_envvar.test_machine_envvars_apply_to_the_built_in_default_only(invoke, args, envvar, default_map, expected)[source]

A machine-wide variable fills in only for a value nothing else set.

tests.test_envvar.test_clean_envvar_id(env_name, clean_name)[source]
tests.test_envvar.test_show_auto_envvar_help(invoke, cmd_decorator, option_help)[source]

Check that the auto-generated envvar appears in the help screen with the extra variants.

Checks that https://github.com/pallets/click/issues/2483 is addressed.

tests.test_envvar.envvars_test_cases()[source]
tests.test_envvar.test_auto_envvar_parsing(invoke, cmd_decorator, envvars, expected_flag)[source]

This test highlights the way Click recognize and parse envvars.

It shows that the default behavior is not ideal, and covers how command improves the situation by normalizing the envvar name.

tests.test_envvar.test_empty_envvar_falls_back_to_the_default(invoke, cmd_decorator, default)[source]

An empty variable is read as unset, so the flag keeps its own default.

Pins the rule the "" row of envvars_test_cases() relies on: Parameter.resolve_envvar_value guards its lookup with a bare if rv, so an empty value never reaches the flag’s type. The False that row expects is the option’s default showing through, which is why flipping the default flips the outcome with it.

tests.test_envvar.test_env_copy()[source]
tests.test_envvar.test_env_copy_removes_on_none(monkeypatch)[source]

A None value drops its variable, the way Click’s CliRunner reads it.

The only way to hide an inherited variable from a child: assigning the empty string leaves it set, which parse_envvar_flag() counts as activation.

tests.test_envvar.test_temporary_env_sets_unsets_and_restores(monkeypatch)[source]
tests.test_envvar.test_temporary_env_restores_after_a_rejected_value(monkeypatch)[source]

A change failing halfway through still restores the ones already made.

os.environ takes FRUIT, then rejects the integer: the same window an interrupt landing between two changes would hit.

tests.test_execution module

Tests for the execution-control options (–time, -0/–zero-exit) and the subprocess-execution primitives (run_cli and the interrupt machinery).

tests.test_execution.test_die_by_sigint_skips_a_stream_refusing_a_reentrant_flush(monkeypatch)[source]

The process still dies when a stream is mid-write in the interrupted code.

The handler of a second Ctrl+C can run inside a write to stderr, whose buffer then refuses the flush with a RuntimeError.

tests.test_execution.NAP_CLI = '\nimport time\n\nimport click_extra\n\n\n@click_extra.command\ndef nap():\n    print("ready", flush=True)\n    time.sleep(30)\n\n\nnap()\n'

A CLI that announces it is running, then sleeps until interrupted.

tests.test_execution.PARSE_NAP_CLI = '\nimport time\n\nimport click_extra\n\n\ndef soak(ctx, param, value):\n    print("ready", flush=True)\n    time.sleep(30)\n\n\n@click_extra.command\n@click_extra.option("--beans", expose_value=False, callback=soak)\ndef stew():\n    pass\n\n\nstew()\n'

A CLI that sleeps while it parses its options, before its context is entered.

tests.test_execution.interrupt_cli(tmp_path, script, presses=1, launcher=())[source]

Run script in a child, press Ctrl+C presses times once it is ready.

launcher holds the interpreter arguments that go before the script, to run it through another CLI.

A further press waits for the child to say what one does, so it lands once the child handles it. A child that never says so is killed.

Return type:

tuple[int, str]

Returns:

the child’s return code and its stderr.

tests.test_execution.test_ctrl_c_ends_the_process_by_sigint(tmp_path, script)[source]

A real Ctrl+C prints Click’s Aborted!, then ends the process by SIGINT, which a calling shell needs to stop its own loop: a status of 1, or even 130, tells the shell the program handled the interrupt itself.

tests.test_execution.PLAIN_NAP_CLI = '\nimport time\n\nimport click\n\n\n@click.command()\ndef nap():\n    print("ready", flush=True)\n    time.sleep(30)\n\n\nif __name__ == "__main__":\n    nap()\n'

A plain Click CLI that sleeps until interrupted, for the wrap command to run.

tests.test_execution.test_ctrl_c_in_a_wrapped_cli_ends_the_process_by_sigint(tmp_path)[source]

A CLI run by wrap aborts under Click’s own main(), and the wrapping process still ends by SIGINT.

tests.test_execution.SLOW_JOBS_CLI = '\nimport threading\nimport time\n\nimport click_extra\nfrom click_extra.jobs import run_jobs\n\nBOTH_IN = threading.Barrier(2)\n\n\ndef bake(tray):\n    # Reports once both trays are in, so the interrupt finds two running tasks.\n    if BOTH_IN.wait(timeout=10) == 0:\n        print("ready", flush=True)\n    time.sleep(30)\n    return tray\n\n\n@click_extra.command\ndef oven():\n    list(run_jobs(bake, [0, 1], jobs=2))\n\n\noven()\n'

A CLI whose two parallel tasks outlast any reasonable wait.

tests.test_execution.SLOW_THREAD_CLI = '\nimport threading\nimport time\n\nimport click_extra\n\n\n@click_extra.command\ndef kettle():\n    threading.Thread(target=time.sleep, args=(30,)).start()\n    print("ready", flush=True)\n    time.sleep(30)\n\n\nkettle()\n'

A CLI whose single background thread outlasts any reasonable wait.

tests.test_execution.test_second_ctrl_c_quits_while_tasks_finish(tmp_path, script, notice)[source]

After a Ctrl+C, the run says it waits for its running tasks, and what a second Ctrl+C does: quit at once, instead of waiting for the tasks.

tests.test_execution.LOCKED_PRESS = '\nimport signal\n\nimport click\n\nfrom click_extra import execution\n\n{setup}\nwith execution._LIVE_PROCESSES_LOCK:\n    signal.raise_signal(signal.SIGINT)\n'

A Ctrl+C that lands while the main thread holds the process registry lock.

tests.test_execution.test_ctrl_c_while_the_registry_lock_is_held(tmp_path, setup)[source]

A Ctrl+C ends the process even when the main thread holds the registry lock.

Both handlers stop the running subprocesses, which takes that lock. The main thread takes it too, as when the grace period of an interrupted run ends, and a plain lock would block the handler of a press landing in that moment for good.

tests.test_execution.STUBBORN_CHILDREN_CLI = '\nimport os\nimport sys\nimport threading\nimport time\n\nimport click_extra\nfrom click_extra import execution\nfrom click_extra.execution import run_cli\nfrom click_extra.jobs import run_jobs\n\nexecution._TERMINATE_GRACE = 0.5\n\nCHILD = (\n    "import os, signal, sys, time;"\n    "signal.signal(signal.SIGINT, signal.SIG_IGN);"\n    "signal.signal(signal.SIGTERM, signal.SIG_IGN);"\n    "open(sys.argv[1], \'w\', encoding=\'UTF-8\').write(str(os.getpid()));"\n    "time.sleep(30)"\n)\nPID_FILES = [os.path.join(FOLDER, f"child-{n}.pid") for n in range(2)]\n\n\ndef spawn(pid_file):\n    return run_cli((sys.executable, "-c", CHILD, pid_file))\n\n\ndef announce():\n    while not all(os.path.exists(path) for path in PID_FILES):\n        time.sleep(0.05)\n    print("ready", flush=True)\n\n\n@click_extra.command\ndef stubborn():\n    threading.Thread(target=announce, daemon=True).start()\n    list(run_jobs(spawn, PID_FILES, jobs=2))\n\n\nstubborn()\n'

A CLI fanning out two children that ignore both SIGINT and SIGTERM.

tests.test_execution.test_interrupted_run_kills_children_ignoring_sigterm(tmp_path)[source]

One Ctrl+C is enough: the aborting run sends SIGTERM to the children of run_cli, then SIGKILL to the ones still running after the grace.

tests.test_execution.STUBBORN_GRANDCHILDREN_CLI = '\nimport os\nimport sys\nimport threading\nimport time\n\nimport click_extra\nfrom click_extra import execution\nfrom click_extra.execution import run_cli\nfrom click_extra.jobs import run_jobs\n\nexecution._TERMINATE_GRACE = 0.5\n\nGRANDCHILD = (\n    "import os, signal, sys, time;"\n    "signal.signal(signal.SIGINT, signal.SIG_IGN);"\n    "signal.signal(signal.SIGTERM, signal.SIG_IGN);"\n    "open(sys.argv[1] + \'.tmp\', \'w\', encoding=\'UTF-8\').write(str(os.getpid()));"\n    "os.replace(sys.argv[1] + \'.tmp\', sys.argv[1]);"\n    "time.sleep(30)"\n)\nCHILD = (\n    "import signal, subprocess, sys, time;"\n    "signal.signal(signal.SIGINT, signal.SIG_IGN);"\n    "signal.signal(signal.SIGTERM, signal.CHILD_ON_SIGTERM);"\n    "subprocess.Popen((sys.executable, \'-c\', sys.argv[2], sys.argv[1]));"\n    "time.sleep(30)"\n)\nPID_FILES = [os.path.join(FOLDER, f"grandchild-{n}.pid") for n in range(2)]\n\n\ndef spawn(pid_file):\n    return run_cli(\n        (sys.executable, "-c", CHILD, pid_file, GRANDCHILD),\n        start_new_session=NEW_SESSION,\n    )\n\n\ndef announce():\n    while not all(os.path.exists(path) for path in PID_FILES):\n        time.sleep(0.05)\n    print("ready", flush=True)\n\n\n@click_extra.command\ndef stubborn():\n    threading.Thread(target=announce, daemon=True).start()\n    list(run_jobs(spawn, PID_FILES, jobs=2))\n\n\nstubborn()\n'

A CLI fanning out two children, each with a grandchild that ignores both SIGINT and SIGTERM and holds the output its child was given.

tests.test_execution.test_interrupted_run_kills_grandchildren_ignoring_sigterm(tmp_path, new_session, child_on_sigterm)[source]

The SIGKILL of an aborting run reaches what the children of run_cli started, so no grandchild holds the run open or outlives it.

tests.test_execution.SELF_INTERRUPT_CLI = '\nimport signal\n\nimport click_extra\n\n\n@click_extra.command\ndef nap():\n    signal.raise_signal(signal.SIGINT)\n\n\nnap()\n'

A CLI that interrupts itself, as a Ctrl+C does, on every platform.

tests.test_execution.SECOND_INTERRUPT_SCRIPT = '\nimport signal\n\nfrom click_extra import execution\n\nexecution._second_interrupt(signal.SIGINT, None)\n'

A process handling the Ctrl+C pressed while an interrupted run waits.

tests.test_execution.OUTLASTED_GRACE_SCRIPT = '\nimport threading\nimport time\n\nfrom click_extra import execution\n\nexecution._TERMINATE_GRACE = 0.1\nthreading.Thread(target=time.sleep, args=(1,)).start()\nexecution._exit_interrupted()\n'

An interrupted run whose only task outlasts the grace given to children.

tests.test_execution.test_interrupted_run_exits_like_an_unhandled_ctrl_c(tmp_path, script, notice)[source]

An interrupted run ends the way an unhandled Ctrl+C does, on Windows too.

The process dies by SIGINT on POSIX, and exits with the status Windows reports for a Ctrl+C. The tests sending a real Ctrl+C to a child are skipped on Windows, so each script here reaches one step of that exit on its own: the whole path of a command, the second Ctrl+C, and the kill of the children still running after the grace, for which Windows has no SIGKILL.

tests.test_execution.PORTABLE_SIGNALS = frozenset({'SIGABRT', 'SIGFPE', 'SIGILL', 'SIGINT', 'SIGSEGV', 'SIGTERM'})

The signal numbers Python defines on every platform, Windows included.

tests.test_execution.POSIX_BRANCHES = {'is_windows()': 'orelse', 'not is_windows()': 'body'}

The branch only POSIX takes, for each platform check an if can test.

tests.test_execution.unguarded_posix_signals(source)[source]

Lines of source that read a signal number Windows does not define.

signal.SIGKILL raises AttributeError on Windows, as every signal number outside PORTABLE_SIGNALS does. So a module may read one only where Windows never goes: in the else of an is_windows() check, or under a not is_windows() one, of an if statement or a conditional expression.

Return type:

list[int]

tests.test_execution.test_posix_only_signals_are_read_under_a_platform_check()[source]

No module of the package reads a POSIX-only signal number on Windows.

Windows runs none of the tests that send a real signal, so only the sources can tell. unguarded_posix_signals states the rule.

tests.test_execution.test_posix_only_signal_scan_finds_the_package_and_an_unguarded_read()[source]

The scan above reads the package, and tells a guarded read from a bare one.

tests.test_execution.test_simulated_interrupt_keeps_click_exit_status(invoke)[source]

A KeyboardInterrupt raised by code, as a test runner does, keeps Click’s Aborted! and status 1: only a real Ctrl+C ends the process by SIGINT.

tests.test_execution.test_abort_notice_precedes_close_callbacks(invoke, abort, notice)[source]

Aborted! prints as soon as the abort leaves the command: before the close callbacks of its context and of its parents, where Click prints it after them.

tests.test_execution.test_abort_caught_around_an_invoked_command_prints_no_notice(invoke)[source]

A command run through ctx.invoke() leaves its caller free to catch the abort: a run that goes on prints no Aborted!.

tests.test_execution.test_abort_without_standalone_mode_prints_no_notice(invoke)[source]

Without standalone mode, Click raises the abort to the caller, which reports it its own way: no Aborted! is printed.

tests.test_execution.test_integrated_time_option(invoke, subcommand_id, time_min)[source]
tests.test_execution.test_integrated_notime_option(invoke, subcommand_id)[source]
tests.test_execution.test_standalone_timer_option(invoke, cmd_decorator, option_decorator, assert_output_regex)[source]
tests.test_execution.test_nested_run_of_a_timed_command_keeps_the_outer_start(invoke)[source]

A run started from within another run of the same command times itself, and the outer run reports the time since its own start.

tests.test_execution.test_time_with_short_circuit_sibling_still_prints(invoke)[source]

--time --version still emits a duration.

--version is an eager option that calls ctx.exit() before the user command body runs, but --time is intentionally measured even on short-circuit paths so it can probe the cost of Click Extra’s own machinery (eager callbacks, config loading, option parsing).

tests.test_execution.test_standalone_zero_exit_option(invoke, cmd_decorator, option_decorator)[source]
tests.test_execution.test_zero_exit_auto_envvar(invoke)[source]
tests.test_execution.test_run_cli_returns_completed_process(caplog)[source]

run_cli mirrors subprocess.run’s result shape, with separate streams.

tests.test_execution.test_run_cli_cwd_moves_the_child(tmp_path)[source]

cwd runs the child elsewhere; omitted, it inherits the caller’s.

tests.test_execution.test_run_cli_flattens_nested_args(caplog)[source]

Nested iterables are flattened, None dropped, and elements stringified.

tests.test_execution.test_run_cli_discloses_command_at_info(caplog)[source]

The invocation is logged up front at INFO, with its forced env vars, and the output stays out of the INFO records.

tests.test_execution.test_run_cli_command_level_override(caplog)[source]

A caller can lower the disclosure line to DEBUG for internal probes.

tests.test_execution.test_run_cli_streams_output_at_debug_with_label(caplog)[source]

Every output line is forwarded to the logger, tagged with the label.

The tag rides the record’s label attribute, not the message text: the default click_extra.logging.Formatter renders it glued to the level name (debug:probe: line1). The prompt line carries it too, so the command and its output read as one call when several children interleave.

tests.test_execution.test_highlight_bin_name()[source]

Only the binary’s own name is styled; its directory stays plain, whichever separator convention the path uses.

tests.test_execution.test_format_cli_prompt_styles_token_families()[source]

Each token family gets the theme slot it holds elsewhere in a CLI’s output: dim sigil, envvar/default assignment pairs, the binary name as an invoked command (directory plain), option-styled flags, plain arguments.

tests.test_execution.test_format_cli_prompt_arguments_survive_a_shell_round_trip(argv)[source]

A drawn command line parses back to the arguments that produced it.

The line advertises itself as copy-pasteable, so every argument carrying a space or a shell metacharacter has to reach the shell as the single argument it started as, instead of spilling into the line as several.

tests.test_execution.test_format_cli_prompt_environment_values_survive_a_shell_round_trip()[source]

An assignment prefixing the command is quoted like an argument.

A value holding a space ends the assignment early otherwise, and the rest of it reads as the command to run.

tests.test_execution.test_format_cli_prompt_unsets_a_variable_mapped_to_none()[source]

A None value removes its variable from the child, which env -u says.

Drawn as an assignment, it read LANGUAGE=None, and a pasted copy then set the variable to the string None instead of removing it.

tests.test_execution.test_format_cli_prompt_honors_an_explicit_theme()[source]

A caller drawing the line onto a surface of its own picks the palette.

Every slot follows, the binary name included: that one is styled a level down, in highlight_bin_name(), which would otherwise read the active theme on its own and leave a light capture’s prompt in the dark theme’s near-white.

tests.test_execution.test_run_cli_merged_streams()[source]

merge_stderr interleaves stderr into stdout and nulls the stderr field.

tests.test_execution.test_run_cli_timeout_kills_child_and_attaches_partial_output()[source]

An overrun raises TimeoutExpired carrying what was captured so far, and leaves no zombie in the live registry.

tests.test_execution.test_run_cli_handles_an_interrupt_tripped_as_the_wait_began()[source]

A Ctrl+C the interpreter noted as the main thread blocked on the child is handled within the poll period, not when the child exits.

interrupt_main() leaves the interrupt pending without waking the main thread: the state a SIGINT leaves when it lands after the interpreter last checked for one and before the thread blocked in the C library. The child never receives it, like a child in a session of its own, or one with no console on Windows.

tests.test_execution.test_run_cli_kill_survives_a_second_interrupt(monkeypatch, code, stop_signals)[source]

A Ctrl+C landing while the timeout reads the process table waits for the signals to land, so the child never outlives the interrupt.

The timeout sends SIGTERM first, which a child may die of. A child ignoring it dies of the SIGKILL that the interrupt brings forward.

tests.test_execution.test_run_cli_default_shares_process_group()[source]

By default the child stays in the caller’s process group: it keeps the controlling terminal (an interactive sudo raised from inside the child must be able to prompt on /dev/tty) and receives the terminal’s signals with the rest of the foreground group.

tests.test_execution.test_run_cli_new_session_makes_child_group_leader()[source]

With start_new_session the child leads its own session and process group, whose ID is its own PID: the property every group-kill path relies on.

tests.test_execution.GRANDCHILD_SESSIONS = MarkDecorator(mark=Mark(name='parametrize', args=('grandchild_session', (ParameterSet(values=(False,), marks=(), id='same-group'), ParameterSet(values=(True,), marks=(), id='own-session'))), kwargs={}))

Run a group-kill test on a grandchild inside the child’s group and outside it.

tests.test_execution.test_run_cli_timeout_new_session_kills_grandchildren(grandchild_session)[source]

A timed-out start_new_session child takes its whole process tree down: the grandchild is reaped along with it instead of surviving as an orphan holding the inherited output pipe open.

tests.test_execution.test_run_cli_registers_live_process_then_discards_it()[source]

run_cli tracks its subprocess while it runs, and drops it once done.

A background call parks in a real subprocess. Once it is registered, terminate_live_processes() unblocks it, and run_cli’s finally clears the registry: this is the exact path the SIGINT handler drives on Ctrl+C.

tests.test_execution.test_terminate_live_processes_signals_whole_group(tmp_path, grandchild_session)[source]

Interrupting a start_new_session child reaps its grandchild too: the group never received the terminal’s SIGINT (it left the foreground group), so terminate_live_processes() is its only kill path and must cover the descendants.

The child reports its grandchild’s PID through a sentinel file, polled before tearing down, so the group is never signalled mid-spawn. The sentinel is published by rename: a bare open(..., "w") creates the file before the write is flushed, so polling for existence could tear the group down between creation and flush, leaving an empty file behind for int() to choke on.

tests.test_execution.test_kill_signal_reaches_a_grandchild_holding_the_output(tmp_path)[source]

The kill signal ends what a child started too, on every platform.

The grandchild holds the pipe run_cli reads, so the call returns only once the grandchild is gone: a prompt return is the evidence.

tests.test_execution.test_parse_proc_stat(stat, expected)[source]
tests.test_execution.test_posix_process_table_lists_this_process()[source]
tests.test_execution.test_run_cli_timeout_kills_grandchildren_without_a_session(grandchild_session)[source]

A timed-out child sharing the caller’s process group still takes its grandchild down, killed one PID at a time since no group can be signalled without hitting the test process too.

This is the shape of a shell script running commands of its own. An escalated call has it too, but the command sudo runs as root is out of reach of the per-PID kill: see test_run_cli_timeout_sends_sigterm_first.

tests.test_execution.test_run_cli_timeout_sends_sigterm_first(tmp_path)[source]

A timed-out child gets SIGTERM before any SIGKILL, so it can clean up.

This is what stops an escalated command: sudo forwards a SIGTERM to the command it runs as root, which a SIGKILL from the caller cannot reach.

tests.test_execution.test_run_cli_timeout_kills_a_child_ignoring_sigterm()[source]

A timed-out child ignoring SIGTERM is killed once the grace runs out.

tests.test_execution.test_descendant_pids_walks_the_whole_tree_deepest_first()[source]

Every descendant is found whatever group it moved to, and a parent never precedes its own child.

The IDs sit above the largest PID any platform allocates, so none of them can be a process the test runner owns.

tests.test_execution.test_escaped_process_groups()[source]

Only the groups of descendants outside the leader’s group are returned.

The IDs sit above the largest PID any platform allocates, so none of them can be the group of the running test process.

tests.test_execution.test_terminate_live_processes_ignores_already_reaped()[source]

A process gone between snapshot and signal is skipped, not raised on.

tests.test_execution.test_install_interrupt_handler_terminates_children_and_reraises()[source]

The installed SIGINT handler SIGTERMs live children, then raises to abort.

tests.test_execution.test_install_interrupt_handler_restored_on_context_close()[source]

Closing the context restores the handler in place before the install.

tests.test_execution.test_install_interrupt_handler_skips_off_main_thread()[source]

signal.signal() only works in the main thread: off-thread install is a no-op.

tests.test_highlight module

tests.test_highlight.test_theme_definition()[source]

Ensure we do not leave any property we would have inherited from cloup and logging primitives.

tests.test_highlight.test_extra_theme()[source]
tests.test_highlight.test_cloup_theme_lends_its_slots_to_the_active_theme(invoke)[source]

A plain cloup.HelpTheme in the formatter settings renders a help screen.

Cloup’s slots come from it, and the slots click-extra adds from the active theme.

class tests.test_highlight.HashType(*values)[source]

Bases: Enum

MD5 = 1
SHA1 = 2
BCRYPT = 3
class tests.test_highlight.Priority(*values)[source]

Bases: Enum

LOW = 'low-priority'
HIGH = 'high-priority'
class tests.test_highlight.Port(*values)[source]

Bases: IntEnum

HTTP = 80
HTTPS = 443
tests.test_highlight.test_option_highlight(opt, expected_outputs)[source]

Test highlighting of all option variations: types, defaults, ranges, envvars, choices, flags, metavars, deprecated messages.

tests.test_highlight.test_extra_deprecated_markers_are_painted()[source]

A CLI’s own marker takes the deprecated slot, beside Click’s spelling.

DEPRECATED_RE is the word deprecated and nothing else, so a project marking a parameter in its own vocabulary gets no color for it until it declares the marker.

tests.test_highlight.test_extra_deprecated_marker_overlapping_click_is_painted_once()[source]

A declared marker Click also writes is styled by one pass, not two.

tests.test_highlight.test_deprecation_marker_stays_painted_around_a_keyword(category, keyword)[source]

A keyword quoted in a deprecation reason keeps the marker painted.

Cross-reference passes run over text the deprecation pass already painted, and the style each one applies closes with a reset. Unless the surrounding style is re-opened after it, the marker renders plain from the keyword to its closing parenthesis. One case per keyword category: the passes run in sequence, so a hole one of them punches says nothing about the others.

tests.test_highlight.test_bracket_field_full_combination_styling()[source]

Document the runtime styling of a fully-loaded bracket field.

Covers the canonical [env var: NAME; default: VAL; required] block that combines all three trailing-field labels. The bracket slot styles only the structural tokens: the outer [ / ], the env var: `` / ``default: `` labels, and the ``; `` separators, while the value tokens (``NAME, VAL) and the required label get their own slot styling layered on top. This is the contract referenced in bracket’s docstring.

tests.test_highlight.test_range_field_does_not_leak_into_following_choice()[source]

A range field must not bleed its bracket styling into the next option.

Regression guard: the [x>=1] field emitted for an IntRange(min=1) option is immediately followed by a Choice option whose [apple|mango] metavar supplies a closing ]. When the range bound matched \S+ it absorbed the field’s own ], letting the bracket regex run past the field and dim everything up to the choice metavar’s ]. Assert the range field is self-contained and closed, and the following choice keeps its own styling.

tests.test_highlight.test_bracket_field_inner_slot_fallback_to_bracket()[source]

Inner bracket-field slots fall back to bracket when at identity.

Contract: a theme that styles only bracket and leaves envvar / default / required / range_label at identity should still render the whole bracket field with the bracket styling: value tokens inherit it rather than rendering plain. Specific inner slots override piecemeal.

tests.test_highlight.test_bracket_field_inner_slot_override_takes_precedence()[source]

When a theme sets one inner slot, only that token uses it.

Complements test_bracket_field_inner_slot_fallback_to_bracket(): a theme that styles bracket + only required (leaving envvar and default at identity) should render the required label with the dedicated style and fall back to bracket for the envvar and default values.

tests.test_highlight.test_skip_hidden_option()[source]

Ensure hidden options are not highlighted.

tests.test_highlight.test_cross_ref_highlight_disabled()[source]

When cross_ref_highlight is False, only structural elements are styled (bracket fields, deprecated messages, subcommands, choice metavars). Options, choices in free-form text, metavars, arguments, and CLI names are left plain.

tests.test_highlight.test_choice_does_not_override_default_style()[source]

Choice cross-ref highlighting must not restyle text inside bracket fields.

When a default value contains a substring that matches a choice keyword (like outline from rounded-outline), the choice style must not override the default value style. Regression test for the case where line-wrapping splits a hyphenated default so the second word starts a new line and passes the lookbehind.

tests.test_highlight.test_keyword_wrapped_at_its_dash_is_highlighted_whole()[source]

A keyword the renderer wraps after its dash is painted as one run.

tests.test_highlight.test_choice_collection_case(params, expected, forbidden)[source]

Choice keywords must use the original-case strings from the type definition, not the normalized (lowercased) forms produced by normalize_choice().

tests.test_highlight.test_argument_highlight(params, expected, forbidden)[source]

Argument metavars get the argument style, distinct from option metavars.

tests.test_highlight.test_no_false_positive_highlight(params, help_text, expected_present, expected_absent)[source]

Verify that highlighting does not leak into compound words, URLs, dotted names, already-styled regions, or partial-word matches.

tests.test_highlight.test_parent_keywords_highlighted_in_subcommand_help()[source]

Parent group names, options, and choices must be highlighted in subcommand help text.

tests.test_highlight.test_parent_choice_case_with_custom_metavar()[source]

Parent choices with custom metavar must use original-case strings in subcommand help, not normalized (lowercased) forms.

tests.test_highlight.test_command_aliases_collected()[source]

Command aliases are collected as keywords for highlighting.

tests.test_highlight.test_command_aliases_highlighted(invoke)[source]

Aliases inside parenthetical groups take the alias slot, and the parentheses and separators the alias_secondary slot.

tests.test_highlight.test_single_alias_highlighted(invoke)[source]

A command with exactly one alias still gets highlighted.

tests.test_highlight.test_alias_no_false_positive_in_description(invoke)[source]

An alias name appearing in a description must not be highlighted when it does not sit inside alias parentheses.

tests.test_highlight.test_alias_substring_not_highlighted(invoke)[source]

An alias that is a substring of the subcommand name must not cause double-highlighting or partial matches.

tests.test_highlight.test_help_keywords_merge(base_kwargs, other_kwargs, checks)[source]

HelpKeywords.merge() unions every field.

tests.test_highlight.test_help_keywords_subtract(base_kwargs, removals_kwargs, checks)[source]

HelpKeywords.subtract() removes matching entries per field.

tests.test_highlight.test_extra_keywords_merged()[source]

extra_keywords injects additional strings into the collected set.

tests.test_highlight.test_excluded_keywords_preserved_in_collection()[source]

excluded_keywords does not remove from collect_keywords().

Exclusion is deferred to highlight_extra_keywords() so that choice metavars can be styled with the full choices set before the excluded choices are removed for cross-ref passes.

tests.test_highlight.test_excluded_keywords_via_constructor()[source]

excluded_keywords can be passed through the Command constructor.

tests.test_highlight.test_excluded_keywords_suppresses_highlighting()[source]

Excluded keywords do not appear styled in the rendered help text.

tests.test_highlight.test_style_choice_metavar(metavar, choices, expected)[source]

style_choice_metavar styles known choices inside bracket-delimited metavar strings, styles non-choice parts (type placeholders) as metavars, and returns None for non-bracket strings.

tests.test_highlight.test_multiple_choice_options_metavar_styled()[source]

Each choice option gets its metavar individually styled.

tests.test_highlight.test_jobs_hybrid_metavar_highlights_keywords_and_placeholder()[source]

The hybrid --jobs [auto|max|INTEGER] metavar highlights every part.

Regression guard: JobCount is a custom ParamType, not a click.Choice, so the keyword collector learns its auto/max keywords only through the choices attribute it exposes. With no other option contributing these tokens (a bare JobsOption command pulls in no --color), auto/max can only be styled via JobCount, and the INTEGER placeholder renders as a metavar rather than staying plain.

tests.test_highlight.test_excluded_multiple_choices_styled_in_metavar_only()[source]

Multiple excluded choices appear styled in their own metavar but not in free-text descriptions.

tests.test_highlight.test_excluded_keywords_inheritance(parent_excluded, child_excluded, word, expect_styled)[source]

excluded_keywords propagate from parent groups to subcommands.

Parent choices are collected for subcommand help screens (cross-ref highlighting). The parent’s excluded_keywords must follow, otherwise excluded choices bleed into subcommand descriptions.

tests.test_highlight.test_excluded_keywords_grandparent_propagation()[source]

excluded_keywords propagate through multiple nesting levels.

tests.test_highlight.test_excluded_keywords_plain_click_group_parent()[source]

A plain click.Group parent without excluded_keywords does not crash.

tests.test_highlight.test_excluded_keywords_not_mutated()[source]

Calling format_help must not mutate the command’s excluded_keywords.

tests.test_highlight.test_keyword_collection(invoke, assert_output_regex)[source]
tests.test_highlight.test_substring_highlighting(content, patterns, expected, ignore_case)[source]
tests.test_highlight.test_standalone_help_option(invoke, cmd_decorator, cmd_type, option_decorator)[source]
tests.test_highlight.test_invoked_subcommand_is_highlighted_and_prose_is_not()[source]

An example line running the CLI paints the subcommands it names.

The same word in the prose describing that subcommand stays unpainted: a subcommand often carries the name of what it does, and reading a whole help screen for it colors plain English.

tests.test_highlight.test_subcommand_options_are_highlighted_on_the_group_screen()[source]

A group screen knows the options of the subcommands it lists.

An example invoking one names options the group itself never declares.

tests.test_highlight.test_end_of_options_separator_is_styled()[source]

A lone -- takes the separator slot; an option name keeps its own.

tests.test_highlight.test_bracket_field_closes_on_a_bracketed_default()[source]

A default value that is itself bracketed closes its own field.

A content pattern stopping at the first ] on screen would leave the field’s real closing bracket outside the styled run.

tests.test_highlight.test_enumerated_metavar_is_styled_part_by_part()[source]

A hand-written metavar that enumerates its parts is painted as one.

Click renders nothing structured for a hybrid type, so INTEGER|auto is a plain string: the value takes the choice slot and the type placeholder beside it the metavar slot.

tests.test_humanize module

tests.test_humanize.test_format_size_iec(size, expected)[source]

IEC binary units are the default.

Return type:

None

tests.test_humanize.test_format_size_si(size, expected)[source]
Return type:

None

tests.test_humanize.test_format_size_jedec(size, expected)[source]
Return type:

None

tests.test_humanize.test_format_size_precision(size, precision, expected)[source]
Return type:

None

tests.test_humanize.test_format_size_negative(size, expected)[source]
Return type:

None

tests.test_humanize.test_format_size_unknown_units()[source]
Return type:

None

tests.test_humanize.test_format_duration_seconds(seconds, expected)[source]
Return type:

None

tests.test_humanize.test_format_duration_timedelta(duration, expected)[source]

A timedelta renders the same as its total seconds.

Return type:

None

tests.test_hygiene module

Conventions the whole code base follows, checked file by file.

tests.test_hygiene.ROOT = PosixPath('/home/runner/work/click-extra/click-extra')

Root of the repository.

tests.test_hygiene.PYTHON_FILES = (PosixPath('/home/runner/work/click-extra/click-extra/click_extra/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/__main__.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/_utils.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/accessibility.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/ansi.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/blocks.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/capture/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/capture/html.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/capture/presets.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/capture/recording.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/capture/screenshot.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/capture/snippet.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/capture/svg.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/carapace.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/cli.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/cli_capture.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/cli_prebake.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/cli_wrapper.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/color.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/columns.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/command_doc.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/commands.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/builtin.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/discovery.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/export.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/flags.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/formats.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/option.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/schema.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/config/subcommands.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/context.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/decorators.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/envvar.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/execution.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/highlight.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/humanize.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/jobs.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/layout.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/logging.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/logo.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/markdown/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/markdown/blocks.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/markdown/myst.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/mkdocs.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/multicall.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/output.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/param_tools.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/parameters.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/prebake.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/progress.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/project.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/pygments.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/pytest.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/show_params.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/_base.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/announcement.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/changelog.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/click_reference.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/click_runner.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/crosshair.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/doctrees.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/fail_on_warnings.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/linkcheck.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/manpages.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/matrix.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/mermaid.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/myst_docstrings.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/python.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/tables.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/tabs.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/theme_docs.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/todos.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/sphinx/viewcode.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/spinner.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/spinner_presets.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/styling.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/table.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/table_options.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/telemetry.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/test_suite.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/testing.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/theme.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/trail.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/tree.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/types.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/version.py'), PosixPath('/home/runner/work/click-extra/click-extra/click_extra/version_facts.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/conftest.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/mkdocs/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/mkdocs/conftest.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/mkdocs/test_mkdocs.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/__init__.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/conftest.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_announcement.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_changelog.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_click.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_click_config.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_click_tree.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_crosshair.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_doctrees.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_fail_on_warnings.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_linkcheck.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_manpages.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_matrix.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_mermaid.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_myst_docstrings.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_python.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_tables.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_tabs.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_todos.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/sphinx/test_sphinx_viewcode.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_accessibility.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_ansi.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_blocks.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_carapace.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_cli_wrapper.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_color.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_command_doc.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_commands.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_config.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_config_discovery.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_config_flags.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_config_schema.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_context.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_docs_anchors.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_docs_api.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_docs_directives.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_envvar.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_execution.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_highlight.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_humanize.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_hygiene.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_jobs.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_layout.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_logging.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_logo.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_multicall.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_myst.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_output.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_parameters.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_prebake.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_progress.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_project.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_pygments.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_pyproject.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_pytest.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_readme.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_recording.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_screenshots.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_snippet.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_sphinx_crossrefs.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_spinner.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_styling.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_table.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_telemetry.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_test_suite.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_testing.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_theme.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_trail.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_tree.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_types.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_utils.py'), PosixPath('/home/runner/work/click-extra/click-extra/tests/test_version.py'))

Python sources of the package and its test suite.

tests.test_hygiene.codec_names(tree)[source]

Each codec name an encoding= argument, encode() or decode() receives.

Return type:

list[tuple[int, str]]

Returns:

the line and the literal of every codec spelled as a string constant, in the order of the source.

tests.test_hygiene.test_codec_names_detected()[source]

The scan sees every way the suite spells a codec name.

Return type:

None

tests.test_hygiene.test_utf8_spelled_uppercase()[source]

Every UTF-8 codec name reads UTF-8, the one spelling of this code base.

Python normalizes utf-8, utf8 and UTF-8 alike, so the difference carries no meaning, and a mixed code base only makes a reader stop to work that out.

Return type:

None

tests.test_jobs module

Tests for the –jobs option and the run_jobs and run_lanes fan-out primitives.

tests.test_jobs.test_standalone_jobs_option(invoke, cmd_decorator, option_decorator)[source]
tests.test_jobs.test_default_value(invoke)[source]

Default reserves one core, except on hosts with fewer than three CPUs.

tests.test_jobs.test_keyword_resolution(invoke, keyword, expected)[source]

‘auto’ resolves to the reserved-core default, ‘max’ to all logical CPUs.

On a host with at least two logical CPUs the resolution is silent; on a single-CPU host the keyword collapses to a single (sequential) job with a warning, so the assertion adapts to the host running the suite.

tests.test_jobs.test_parallel_keyword_collapses_to_sequential_warns(invoke, keyword, cpu_count, default_jobs, cpu_phrase)[source]

‘auto’/’max’ warn when too few logical CPUs force a single (sequential) job.

tests.test_jobs.test_explicit_single_job_is_silent(invoke)[source]

An explicit ‘–jobs 1’ is a deliberate sequential choice: no warning.

tests.test_jobs.test_default_collapse_to_sequential_is_quiet(invoke)[source]

The bare default (‘auto’) collapsing to a single job does not warn.

The user never asked for parallelism: warning on the option’s own default would fire on every bare invocation on a 1-CPU host, polluting captured runner streams and the CLI output rendered in Sphinx docs.

tests.test_jobs.test_default_collapse_to_sequential_logged_at_info(invoke)[source]

The default’s collapse to a single job stays discoverable at info level.

This is the silent trap on a single-CPU host: no flag is passed, yet execution runs sequentially. The trace lives at info level, next to the resolved-jobs line, instead of a default-verbosity warning.

tests.test_jobs.test_resolved_job_count_logged_at_info(invoke)[source]

The resolved job count and os.cpu_count() are logged at info level.

tests.test_jobs.test_run_jobs_preserves_order(jobs)[source]

Results come back in submission order, sequential or parallel.

tests.test_jobs.test_run_jobs_sequential_is_lazy()[source]

With one worker, items run lazily so a caller can stop early.

tests.test_jobs.test_run_jobs_preserves_order_past_its_window(jobs)[source]

Order holds when the stream is longer than the in-flight window.

tests.test_jobs.test_run_jobs_handles_empty_and_single_streams(jobs)[source]

The peek that sizes the run does not lose the items it looked at.

tests.test_jobs.test_run_jobs_parallel_reads_no_further_than_its_window()[source]

The parallel path pulls a bounded window instead of draining the stream.

tests.test_jobs.test_run_jobs_parallel_stops_scheduling_on_early_exit()[source]

Breaking out of a parallel run leaves the rest of the stream unread.

tests.test_jobs.test_run_jobs_reads_jobs_from_context(invoke)[source]

Without an explicit count, run_jobs reads the resolved –jobs value.

tests.test_jobs.test_run_jobs_without_context_runs_sequential()[source]

Outside any Click context and with no count, run_jobs falls back to 1.

tests.test_jobs.test_run_jobs_interrupt_aborts_without_blocking()[source]

A KeyboardInterrupt returns at once, without waiting on in-flight tasks.

Results yield in submission order, so the interrupting item (index 0) is pulled first: the abort fires while a second, still-running item is parked on an event that stays unset for the run. A plain with-block teardown would shutdown(wait=True) and hang on that parked task; the run drops queued work and returns at once.

tests.test_jobs.test_resolve_jobs_without_context_is_sequential()[source]

No context means nothing to read a job count from: stay sequential.

tests.test_jobs.test_resolve_jobs_single_item_is_sequential()[source]

A single item has nothing to parallelize.

tests.test_jobs.test_resolve_jobs_reads_context(jobs, count, expected)[source]

The resolved –jobs value drives the count, capped at the item count.

tests.test_jobs.test_resolve_jobs_serial_at_debug()[source]

serial_at_debug collapses to sequential only at DEBUG verbosity.

tests.test_jobs.test_run_lanes_preserves_order(jobs)[source]

Results come back in lane-submission order, items within a lane in order.

tests.test_jobs.test_run_lanes_preserves_order_past_its_window(jobs)[source]

Lane order holds when there are more lanes than the in-flight window.

tests.test_jobs.test_run_lanes_materializes_lanes_lazily()[source]

A lane becomes a list only when it is about to be scheduled.

tests.test_jobs.test_run_lanes_is_run_jobs_with_singleton_lanes()[source]

run_jobs is the degenerate case of run_lanes: one item per lane.

tests.test_jobs.test_run_lanes_serializes_within_a_lane()[source]

Within a lane, items run one at a time even when lanes run in parallel.

tests.test_jobs.test_run_lanes_runs_lanes_concurrently()[source]

Distinct lanes overlap: a barrier only releases if all lanes run at once.

tests.test_jobs.test_run_lanes_sequential_is_lazy()[source]

With one worker, items run lazily so a caller can stop early.

tests.test_jobs.test_run_lanes_reads_jobs_from_context(invoke)[source]

Without an explicit count, run_lanes reads the resolved –jobs value.

tests.test_jobs.test_run_lanes_without_context_runs_sequential()[source]

Outside any Click context and with no count, run_lanes falls back to 1.

tests.test_jobs.test_run_lanes_empty_yields_nothing()[source]

No lanes, or only empty lanes, yields nothing and raises nothing.

tests.test_jobs.test_lone_item_or_lane_runs_in_the_calling_thread(run)[source]

With nothing to overlap, the work runs in the calling thread, whatever the worker count.

tests.test_jobs.test_run_lanes_interrupt_aborts_without_blocking()[source]

A KeyboardInterrupt returns at once, without waiting on in-flight lanes.

Mirror of test_run_jobs_interrupt_aborts_without_blocking(): see it for the rationale.

tests.test_jobs.test_run_lanes_interrupt_stops_each_lane_after_its_current_item()[source]

An interrupt lets each running lane finish its current item, then stop.

tests.test_jobs.test_interrupt_tripped_as_the_wait_began_is_handled_promptly(run)[source]

A Ctrl+C the interpreter noted as the main thread blocked on a task is handled within the poll period, not when the task ends.

interrupt_main() leaves the interrupt pending without waking the main thread: the state a SIGINT leaves when it lands after the interpreter last checked for one and before the thread blocked in the C library. An unbounded wait on the task would run the handler only when the task returns.

tests.test_jobs.test_wait_result_raises_the_interrupt_a_lock_error_replaced(monkeypatch, context, expected)[source]

A RuntimeError the wait raises in place of a Ctrl+C becomes the Ctrl+C again.

A free-threaded CPython can raise the interrupt between the wait and the lock it takes back, then fail to release it, as python/cpython#118433 reports. Any other RuntimeError of the wait stays as it is.

tests.test_jobs.test_run_jobs_reports_a_task_timeout_error()[source]

A task raising TimeoutError ends the run on it, within the poll period.

concurrent.futures.TimeoutError is the builtin TimeoutError from Python 3.11 on, which a task raises itself when a socket times out: a wait reading the result with a timeout of its own could not tell the two apart.

tests.test_jobs.test_invalid_value(invoke)[source]

Values that are neither an integer nor a known keyword are rejected.

tests.test_jobs.test_jobs_shell_complete(incomplete, expected)[source]

–jobs completion suggests the auto/max keywords and never an integer.

tests.test_jobs.test_clamp_to_one(invoke, value, warning)[source]

0 disables parallelism and negatives clamp: both run 1 job with a warning.

tests.test_jobs.test_exceeds_cpu_count(invoke)[source]

A count above the core count is honored, with an I/O-bound caveat.

tests.test_jobs.test_no_warning_within_bounds(invoke)[source]

No warning when the value is within the valid range.

tests.test_jobs.test_default_jobs(cpu_count, expected)[source]

The default reserves one logical CPU, from three CPUs up.

tests.test_jobs.test_default_jobs_of_this_host()[source]

The published default follows the rule for the CPUs of this host.

tests.test_jobs.test_logical_cpu_count_prefers_process_count(process_count, fallback, expected)[source]

os.process_cpu_count() is preferred, os.cpu_count() is the fallback.

tests.test_jobs.test_logical_cpu_count_fallback_without_process_count()[source]

On runtimes lacking os.process_cpu_count(), os.cpu_count() answers.

tests.test_layout module

Tests for click_extra.layout: measuring a line and composing one.

tests.test_layout.test_center_in_rule_draws_an_unbroken_line_for_no_label(label)[source]

Naming nothing draws a divider, not a frame around an empty middle.

tests.test_layout.test_center_in_rule_measures_a_styled_label_unstyled()[source]

A label’s escapes occupy no cell, so they must not shorten the rule.

tests.test_layout.test_center_in_rule_paints_the_rule_and_leaves_the_label_alone()[source]

The rule recedes by default, and a caller’s own label styling survives.

tests.test_layout.test_fit_columns_floors_at_nothing_by_default()[source]

The floor belongs to whoever lays the text out, not to the measure.

tests.test_layout.test_cell_width_discounts_what_a_terminal_draws_nowhere(text, cells)[source]

Escapes and hyperlinks measure nothing, and a tab measures to its stop.

wcwidth.wcswidth refuses any string carrying a control character and answers -1: a measure built on it falls back to the character count of a styled string, 16 for a red apricot the terminal draws in 7.

tests.test_layout.test_cell_width_is_never_negative()[source]

A control character measures what a terminal advances by, never -1.

tests.test_layout.test_pad_to_measures_the_text_a_terminal_draws()[source]

str.ljust counts escapes it cannot see, so a styled line pads short.

tests.test_layout.test_pad_to_closes_a_style_before_the_blanks()[source]

A background left open would otherwise bleed across the gap.

tests.test_layout.test_pad_to_leaves_text_already_wide_enough_alone()[source]

Nothing to pad, nothing added, reset included.

tests.test_layout.test_number_lines_aligns_on_the_digits_it_is_given()[source]

digits widens the gutter, and never cuts the widest number short.

tests.test_layout.test_number_frames_gives_every_frame_the_gutter_of_the_tallest()[source]

Each frame counts its own rows, at the width its tallest sibling needs.

tests.test_logging module

tests.test_logging.CUSTOM_FORMAT = '{levelname} | {name} | {message}'

A format to tell a logger set up on purpose from the default of Click Extra.

tests.test_logging.run_handlers()[source]

The handlers of Click Extra attached to the root logger right now.

tests.test_logging.test_level_default_order()[source]
tests.test_logging.test_root_logger_defaults()[source]

Check our internal default is aligned to Python’s root logger.

tests.test_logging.test_integrated_verbosity_options(invoke, cmd_decorator, cmd_type, args, expected_level, assert_output_regex)[source]
tests.test_logging.test_custom_verbosity_option_name(invoke, args, assert_output_regex)[source]
tests.test_logging.test_custom_verbose_option_name(invoke, args, assert_output_regex)[source]
tests.test_logging.test_custom_quiet_option_name(invoke, args)[source]
tests.test_logging.test_counter_trace_agrees_with_its_count(invoke, args, trace)[source]

The -v/-q trace says one level in the singular, and more in the plural.

tests.test_logging.test_unrecognized_verbosity_level(invoke, cmd_decorator, cmd_type)[source]
tests.test_logging.test_standalone_option_default_logger(invoke, cmd_decorator, option_decorator, args, expected_level, assert_output_regex)[source]

Checks: - option affect log level - the default logger is root - the default logger message format - level names are colored - log level is propagated to all other loggers

tests.test_logging.test_default_logger_param(invoke, logger_param, params)[source]

Passing a logger instance or name to the default_logger parameter works.

tests.test_logging.test_new_logger_name_passing(invoke)[source]

Test extra logger with custom format, passed to the option by its name.

tests.test_logging.test_new_logger_object_passing(invoke)[source]

Test extra logger with custom format, passed as an object to the option.

tests.test_logging.restored_root_logger()[source]

Put the root logger back the way the test found it.

new_logger() naming no logger reconfigures the process-wide root one through basicConfig(force=True), which drops whatever handlers were already attached. Nothing in logging undoes that, so a test doing it once decides how every later test in the same worker renders: leaving the handler behind is what made test_logger_propagation fail whenever this module’s root-logger test ran before it.

A run of a plain Click command that failed while parsing keeps its hold on the handler of Click Extra until its context is collected. Collecting first keeps that handler out of the saved list, which would otherwise attach it again once the test is over.

class tests.test_logging.RecordingHandler[source]

Bases: Handler

Stand-in for the handler of an application: keeps the messages it receives.

Initializes the instance - basically setting the formatter to None and the filter list to empty.

emit(record)[source]

Do whatever it takes to actually log the specified logging record.

This version is intended to be implemented by subclasses and so raises a NotImplementedError.

close()[source]

Tidy up any resources used by the handler.

This version removes the handler from an internal map of handlers, _handlers, which is used for handler lookup by name. Subclasses should ensure that this gets called from overridden close() methods.

tests.test_logging.app_handler(restored_root_logger)[source]

A handler attached to the root logger before any command is defined.

This is the state an application leaves the root logger in before it imports a CLI, and the one pytest leaves it in with its own capture handlers.

tests.test_logging.test_new_logger_root_config(invoke, restored_root_logger)[source]

Modify the root logger via new_logger()

tests.test_logging.test_logger_propagation(invoke)[source]
tests.test_logging.test_decorating_a_command_leaves_the_root_logger_alone(cmd_decorator, app_handler)[source]

Defining a command changes neither the handlers nor the level of root.

A command is defined when its module is imported: the handlers found there belong to whoever imports it.

tests.test_logging.test_unregistered_logger_name_is_set_up_without_the_root_logger(app_handler)[source]

A name unknown to logging gets a logger of its own when the option is built. The root logger stays out of it.

tests.test_logging.extra_command(body)[source]

A Click Extra command, which carries the verbosity options by default.

tests.test_logging.extra_group(body)[source]

A Click Extra group: its subcommand carries the verbosity options too.

tests.test_logging.click_command(body)[source]

A plain Click command borrowing the verbosity option.

tests.test_logging.click_command_with_its_own_logger(body)[source]

A plain Click command whose verbosity option sets a logger of its own.

tests.test_logging.click_command_without_verbosity(body)[source]

A plain Click command borrowing an option other than the verbosity ones.

tests.test_logging.test_run_adds_one_handler_beside_the_others(invoke, app_handler, build)[source]

A run prints through one handler of Click Extra, added beside the handlers already on the root logger, and gone once the run is over.

The handler of the application stays attached all along, and receives the record too: a console handler there prints it a second time.

tests.test_logging.test_each_run_gets_the_handler(invoke, app_handler)[source]

The handler leaves with a run, and the next run gets one again.

tests.test_logging.test_record_of_an_earlier_option_is_printed(invoke, app_handler)[source]

The handler is attached before Click processes any option, so a record emitted by an option processed ahead of the verbosity ones is printed.

tests.test_logging.test_record_after_a_run_is_not_printed(invoke, app_handler, capsys)[source]

Once a run is over, Click Extra prints nothing of what the process logs.

tests.test_logging.test_caplog_captures_the_records_of_a_command_defined_in_a_test(invoke, caplog)[source]

pytest attaches its capture handlers to the root logger before the test body runs: a command defined in that body must leave them in place.

tests.test_logging.test_file_handler_of_the_root_logger_keeps_writing(invoke, restored_root_logger, tmp_path)[source]

No handler of the root logger is closed, at any point.

A file handler opened in w mode never opens its file again once closed, which is the mode of the one behind pytest’s --log-file.

tests.test_logging.test_handler_leaves_however_the_run_ends(invoke, app_handler, args, exit_code)[source]

A Click Extra command gives the handler back the moment its run ends, whatever the run ends on.

tests.test_logging.test_failed_run_of_a_click_command_gives_the_handler_back_when_collected(invoke, app_handler)[source]

A plain Click command has no hook on a run that fails while parsing: the handler leaves once Python collects the context of that run.

tests.test_logging.test_run_keeps_its_handler_when_a_failed_run_is_collected(invoke, app_handler)[source]

The context of a failed run may be collected in the middle of a later run, which then still needs the handler they share.

tests.test_logging.test_failed_run_collected_while_a_record_is_handled(invoke, restored_root_logger)[source]

The context of a failed run may be collected while the root logger hands a record to its handlers: the handlers after the one of that run still get it.

tests.test_logging.test_nested_runs_share_one_handler(invoke, app_handler, inner_args)[source]

A run started from within another one prints through the same handler, and leaves it to the run around it when it ends.

tests.test_logging.test_resilient_parsing_adds_no_handler(app_handler)[source]

Click parses without side effects to complete a command line: the root logger gets no handler then.

tests.test_logging.test_root_logger_set_up_on_purpose_is_left_as_it_is(invoke, restored_root_logger, set_up, build)[source]

A command left on the root logger keeps the format new_logger() or basicConfig() gave it, and adds no handler of its own.

tests.test_logging.test_handlers_given_to_new_logger_replace_the_run_handler(invoke, restored_root_logger)[source]

new_logger() given the handlers of the caller is how an application keeps the handler of Click Extra off the root logger.

tests.test_logging.test_new_logger_replaces_the_handlers_of_the_root_logger(restored_root_logger, force, replaced)[source]

Left on its default name, new_logger() removes and closes every handler of the root logger, unless told not to force.

That is its documented contract, and the reason not to call it at import time: the handlers of an application go with the others.

tests.test_logging.test_new_logger_with_a_name_leaves_the_root_logger_alone(app_handler)[source]

Given a name, new_logger() sets that logger up and no other.

tests.test_logging.application_levels(restored_root_logger)[source]

Give the loggers the levels of an application for the time of a test.

The root logger works at INFO, and the click_extra one follows it, as it does from the start. None of the two is at the level the verbosity options default to: a test can so tell a level given back from one reset.

tests.test_logging.levels()[source]

The levels a verbosity option sets: of root and of click_extra.

tests.test_logging.test_levels_are_given_back_however_the_run_ends(invoke, application_levels, args, exit_code, traced)[source]

A Click Extra command gives its loggers the levels they had before the run back the moment it ends, whatever it ends on.

tests.test_logging.test_logger_of_the_option_gets_its_own_level_back(invoke, application_levels)[source]

An option given a logger of its own leaves the level of root alone, and gives that logger the level it had.

tests.test_logging.test_reset_loggers_falls_back_on_the_default_level(application_levels)[source]

Called with no level to give back, reset_loggers() sets the default one.

tests.test_logging.test_levels_are_given_back_when_a_subcommand_fails_while_parsing(invoke, application_levels)[source]

The context of the group closes, the one of its subcommand never does.

The group carries no verbosity option of its own here: one that does queues the reset on its own context, which closes.

tests.test_logging.test_failed_run_of_a_click_command_gives_its_levels_back_when_collected(invoke, application_levels)[source]

A plain Click command has no hook on a run that fails while parsing: the levels come back once Python collects the context of that run.

tests.test_logging.failing_click_command(contexts, body=None)[source]

A plain Click command that keeps the context of each of its runs alive, and fails while parsing when given an invalid --baskets.

tests.test_logging.test_run_keeps_its_levels_when_a_failed_run_is_collected(invoke, application_levels)[source]

The context of a failed run may be collected in the middle of a later run: the levels are by then the ones of that later run, and stay.

tests.test_logging.test_levels_found_before_a_failed_run_are_the_ones_given_back(invoke, application_levels)[source]

A run that starts on the levels a failed run left behind gives back the ones found before that failed run, not the ones it left.

tests.test_logging.test_released_handler_put_back_on_the_root_logger_prints_nothing(invoke, app_handler, capsys)[source]

Code that saves the handlers of the root logger and puts them back later may attach again a handler its run let go of in between. That handler prints nothing, and the next run takes it off.

A fixture isolating a test does that around a plain Click command that failed while parsing: Python collects the context of that run, and so releases its handler, whenever it sees fit.

tests.test_logging.test_importing_a_cli_leaves_the_root_logger_alone()[source]

In a new interpreter, importing Click Extra and defining a command leave the root logger with the one handler the application gave it.

tests.test_logging.test_root_logger_prints_in_a_new_interpreter_beside_a_named_logger()[source]

A command whose verbosity option sets a named logger still prints the records of the root logger in the style of Click Extra.

Run in a new interpreter: no other command of the process may have given the root logger its handler.

tests.test_logging.test_stream_handler_honors_no_color_from_background_threads(capsys, monkeypatch)[source]

A record emitted with no reachable Click context (as from run_cli’s stream reader threads) still honors –no-color, through the process-wide mirror published by the color options.

tests.test_logging.test_formatter_renders_label_glued_to_level_name()[source]

A record tagged with a label attribute (as run_cli’s streamed output lines are) renders it inside the level prefix: debug:mas: message.

tests.test_logging.test_formatter_starts_a_block_below_its_prefix(label, prefix, message, expected)[source]

A message spanning several lines starts on the line below its prefix, so its first line keeps the columns of the lines under it.

tests.test_logging.test_stream_handler_routes_through_active_spinner(capsys)[source]

A record emitted while a spinner animates on the same stream is printed through Spinner.echo(), on its own line, instead of over the frame.

tests.test_multicall module

Tests multicall / argv[0] dispatch and the invocation-name hook.

tests.test_multicall.make_kitchen(personalities=None, **group_kwargs)[source]

A fresh multicall CLI per test, to keep process-global state from leaking.

The domain is deliberately a kitchen: the test suite asserts dispatch mechanics, not any real tool’s behavior.

tests.test_multicall.test_argv0_dispatch(runner, monkeypatch)[source]

With no explicit prog_name, the basename of argv[0] dispatches.

tests.test_multicall.test_env_prefix_follows_personality(runner)[source]

A personality reads its own <NAME>_* envvars, like a standalone binary.

tests.test_multicall.test_explicit_mapping_replaces_identity(runner)[source]

An explicit personalities mapping is exhaustive, not additive.

tests.test_multicall.test_explicit_prog_name_wins(runner, monkeypatch)[source]

An explicit prog_name takes precedence over argv[0].

tests.test_multicall.test_extra_tokens_prepended(runner)[source]

Personality tokens are prepended to the user’s arguments.

tests.test_multicall.test_flat_parse_any_order(runner)[source]

Group and subcommand options mix freely in a personality.

tests.test_multicall.test_group_callback_skipped_in_personality_mode(runner)[source]

A personality is standalone: the group’s callback does not run.

tests.test_multicall.test_group_mode_unchanged(runner)[source]

A non-matching invocation name leaves the regular group behavior intact.

tests.test_multicall.test_help_conformance(runner)[source]

A personality’s help lists every option the group’s help lists.

This is what catches the Cloup layout trap: patching params on a copy updates the parser but not the help formatter, which is computed from params at __init__ time.

tests.test_multicall.test_hidden_subcommand_is_not_a_default_personality(runner)[source]

The default identity mapping skips hidden and synthetic subcommands.

tests.test_multicall.test_invocation_name_meta_key(runner)[source]

ctx.meta exposes the invocation name on the root context.

tests.test_multicall.test_no_envvar_leak_into_group_mode(runner)[source]

Building a personality must not rewrite the original params’ envvars.

tests.test_multicall.test_resolve_invocation_name(runner, monkeypatch, argv0, expected)[source]

The detector uses the unresolved basename and strips .exe.

tests.test_multicall.test_unknown_subcommand_in_mapping(runner)[source]

A personality mapping an absent subcommand fails loudly at dispatch.

tests.test_multicall.test_version_names_each_personality(runner)[source]

Each personality’s --version names itself, in sequence.

Regression test: VersionOption used to cache prog_name on the instance, pinning the first name seen in a process forever. The warm-up invocation with debug logging matters: it resolves every version field without anyone calling --version at all.

tests.test_multicall.test_version_prog_name_varies_without_multicall(runner)[source]

--version follows an explicit prog_name even in plain group mode.

tests.test_multicall.test_personalities_subcommand(runner)[source]

The auto-injected personalities subcommand lists the names.

tests.test_multicall.test_personalities_command_can_be_disabled(runner)[source]

personalities_command=False suppresses the listing subcommand.

tests.test_multicall.test_personalities_command_can_be_overridden(runner)[source]

A personalities subcommand of the author’s replaces the injected one.

tests.test_multicall.test_injected_subcommands_listed_last_in_declaration_order()[source]

help and personalities trail the author’s own subcommands.

tests.test_multicall.test_plain_click_subcommand_is_a_personality(runner)[source]

A subcommand built by Click alone dispatches like any other.

tests.test_multicall.test_normalize_personality()[source]

Personality values normalize to non-empty token tuples.

tests.test_multicall.test_deepcopy_keeps_every_sentinel_identical()[source]

A copied parameter carries its enum members over, it does not rebuild them.

An enum member is a singleton, so a copy handing back an equal-but-distinct one is already wrong, and the object() behind a PEP 661 sentinel never survives a rebuild. Walking the whole default option set catches the next parameter to grow one.

tests.test_multicall.test_version_option_deepcopy_drops_cache()[source]

Deepcopying a warmed-up VersionOption yields fresh field resolution.

tests.test_multicall.test_lazy_subcommand_is_a_personality(runner, tmp_path, monkeypatch)[source]

A lazy multicall group answers to a subcommand it has not imported yet.

Personality dispatch runs before any context exists, so the name is resolved through the lazy registry rather than the subcommands already loaded. Invoked as bake, the group imports the subcommand and runs it, instead of falling through to its own usage screen.

tests.test_myst module

Tests for the reST-to-MyST converters and the convert-to-myst command.

tests.test_myst.test_convert_xrefs(rst, expected)[source]
tests.test_myst.test_convert_inline_code(rst, expected)[source]
tests.test_myst.test_convert_directives(rst, expected_fragments)[source]
tests.test_myst.test_convert_directives_keeps_fenced_body()[source]

A directive body already holding a fence cannot nest: it stays reST.

tests.test_myst.test_convert_directives_skips_fence_interiors()[source]

A reST directive inside a fence is prior nested-conversion output: keep it.

tests.test_myst.test_convert_source_nested_directives_idempotent()[source]

Nested directives convert the outer level only, and re-runs are no-ops.

tests.test_myst.test_convert_source_fenced_samples_untouched()[source]

Fence bodies are sample code: reST markup inside them is data.

tests.test_myst.test_convert_comment_blocks()[source]

#: blocks are unwrapped, converted, and re-wrapped.

tests.test_myst.test_convert_source_scopes_to_docstrings_and_comments()[source]
tests.test_myst.test_convert_source_attribute_docstrings()[source]
tests.test_myst.test_convert_source_sharp_colon_comment_blocks()[source]
tests.test_myst.test_convert_file_roundtrip_and_idempotence(tmp_path)[source]
tests.test_myst.test_convert_directory_reports_only_changed(tmp_path)[source]
tests.test_myst.test_convert_to_myst_cli_explicit_directory(tmp_path)[source]
tests.test_myst.test_convert_to_myst_cli_autodetect(tmp_path, monkeypatch)[source]
tests.test_myst.test_convert_to_myst_cli_autodetect_failure(tmp_path, monkeypatch)[source]
tests.test_myst.test_convert_to_myst_cli_not_a_directory(tmp_path)[source]
tests.test_myst.test_heading_conversion(rst, expected)[source]
tests.test_myst.test_automodule_block_wrapped_in_eval_rst()[source]
tests.test_myst.test_convert_rst_files_creates_md_and_deletes_rst(tmp_path)[source]
tests.test_myst.test_convert_rst_files_skips_non_apidoc(tmp_path)[source]
tests.test_myst.test_convert_rst_files_existing_md_wins(tmp_path)[source]

tests.test_output module

tests.test_output.UNENCODABLE_ON_CP1252 = ('✓', '✘', '╭', '─')

Glyphs Click Extra prints that a legacy Windows code page cannot encode: the two status marks, and two characters of a rounded table border.

class tests.test_output.FakeStream(encoding, *, refuse=False)[source]

Bases: object

A stand-in for a standard stream, recording what it was reconfigured to.

The real streams of a test run already speak UTF-8, so the legacy path is never taken through them.

reconfigured: dict[str, str]
reconfigure(**kwargs)[source]
Return type:

None

tests.test_output.test_stdout_sentinel()[source]
Return type:

None

tests.test_output.test_is_stdout()[source]
Return type:

None

tests.test_output.test_prep_path_creates_parents_and_writes_utf8(tmp_path)[source]
Return type:

None

tests.test_output.test_prep_path_stdout(capsys)[source]
Return type:

None

tests.test_output.test_prep_path_reopens_the_process_stdout_as_utf8(monkeypatch, tmp_path)[source]

The stdout of the process itself is opened again on its descriptor, as UTF-8.

Its own cp1252 encoding cannot carry the glyphs. Closing the stream returned leaves that stdout open.

Return type:

None

tests.test_output.test_prep_path_returns_a_process_stdout_without_descriptor(monkeypatch)[source]

A stdout of the process with no file descriptor is returned as-is.

Return type:

None

tests.test_output.test_prep_path_stdout_reaches_the_runner_capture(capture)[source]

A command writing to - prints into the capture of Click’s test runner.

Under capture="fd", sys.stdout.fileno() returns a copy of the original stdout descriptor, so a stream opened on it writes past the capture.

Return type:

None

tests.test_output.test_legacy_stream_is_switched_to_utf8(monkeypatch, encoding)[source]

A stream unable to carry the glyphs is reconfigured to UTF-8.

So is a stream reporting an encoding Python does not know.

Return type:

None

tests.test_output.test_utf8_stream_is_left_alone(monkeypatch, encoding)[source]

A stream already speaking UTF-8 is never reconfigured, whatever its spelling.

Return type:

None

tests.test_output.test_a_refusing_stream_never_raises(monkeypatch)[source]

A stream refusing reconfiguration is left as the platform made it.

Return type:

None

tests.test_output.test_a_stream_without_reconfigure_is_skipped(monkeypatch)[source]

A stream object without reconfigure(), like a capture, raises nothing.

Return type:

None

tests.test_output.test_command_prints_glyphs_to_a_legacy_stream(monkeypatch)[source]

A command prints a glyph to a cp1252 stream instead of crashing on it.

The streams keep \n as written: the default would turn it into \r\n on Windows.

Return type:

None

tests.test_parameters module

tests.test_parameters.CLICK_HELP_PARAM_NAME = '_click_default_help'

Click’s internal .name for its built-in --help option.

Detected at runtime instead of hard-coded: _click_default_help is an internal identifier of Click, which Click Extra does not control.

class tests.test_parameters.Custom[source]

Bases: ParamType

A dummy custom type.

name: str = 'Custom'

the descriptive name of this type

convert(value, param, ctx)[source]

Convert the value to the correct type. This is not called if the value is None (the missing value).

This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types.

The param and ctx arguments may be None in certain situations, such as when converting prompt input.

If the value cannot be converted, call fail() with a descriptive message.

Parameters:
  • value – The value to convert.

  • param – The parameter that is using this type to convert its value. May be None.

  • ctx – The current context that arrived at this value. May be None.

tests.test_parameters.test_canonical_param_name_matches_click(decl)[source]

The fold answers what Click names a parameter declared that way.

Click derives the name in Option._parse_decls, which splits the prefix then applies this same fold. Pinning the two together is what keeps the helper honest when Click moves.

tests.test_parameters.test_canonical_param_name_is_many_to_one()[source]

The fold identifies a name, and cannot reconstruct a spelling.

tests.test_parameters.test_canonical_param_name_never_answers_an_identifier_decl()[source]

Click takes an identifier declaration verbatim, so no fold produces it.

This is why a resolved spelling has to come back from the names a CLI declares, rather than from what the fold returns.

class tests.test_parameters.FruitOption(param_decls=None, show_default=None, prompt=False, confirmation_prompt=False, prompt_required=True, hide_input=False, is_flag=None, flag_value=Sentinel.UNSET, multiple=False, count=False, allow_from_autoenv=True, type=None, help=None, hidden=False, show_choices=True, show_envvar=False, deprecated=False, **attrs)[source]

Bases: Option

An option class to search for.

class tests.test_parameters.AppleOption(param_decls=None, show_default=None, prompt=False, confirmation_prompt=False, prompt_required=True, hide_input=False, is_flag=None, flag_value=Sentinel.UNSET, multiple=False, count=False, allow_from_autoenv=True, type=None, help=None, hidden=False, show_choices=True, show_envvar=False, deprecated=False, **attrs)[source]

Bases: FruitOption

A subclass of it, to tell subclass matching apart.

tests.test_parameters.test_search_params_returns_the_one_match()[source]

A single match comes back as itself, a subclass matching by default.

tests.test_parameters.test_last_param_keeps_the_last_exact_match()[source]

Duplicates resolve to the last one, and a subclass never matches.

tests.test_parameters.test_factory_decorators_expose_option_signature()[source]

Factory-built option decorators expose their option class’s real signature.

Guards the signature propagation in click_extra.decorators.decorator_factory(), which lets editors, help() and Sphinx see the actual parameters instead of an opaque (*args, **kwargs).

tests.test_parameters.test_params_auto_types(invoke, option_decorator)[source]

Check parameters types and structure are properly derived from CLI.

tests.test_parameters.assert_table_content(output, expected_table, table_format=None)[source]

Helper to assert the content of a rendered table in the output.

Return type:

None

tests.test_parameters.test_standalone_show_params_option(invoke, cmd_decorator, option_decorator, assert_output_regex)[source]
tests.test_parameters.test_integrated_show_params_option(invoke, create_config)[source]
tests.test_parameters.test_show_params_table_format_ordering(invoke, args_order)[source]

--params respects --table-format regardless of CLI order.

tests.test_parameters.test_show_params_native_types(invoke, table_format)[source]

Serialization formats emit native types instead of styled glyphs.

tests.test_parameters.test_show_params_no_default_renders_none(invoke)[source]

A parameter with no default renders as None, not the UNSET sentinel.

Regression guard for Click’s UNSET sentinel leaking into the value and default columns through the --params re-parse path. See the RAW_ARGS notes in click_extra.context.

tests.test_parameters.test_show_params_config_file_cascade(invoke, tmp_path, monkeypatch)[source]

The config_file column names the cascade layer a value resolved from.

The local file wins the root-level parameter; the parent file owns the [sub] section the local file does not define. A parameter overridden on the command line keeps an empty column: the file did not supply the effective value.

tests.test_parameters.test_show_params_config_file_cascade_shared_sections(invoke, tmp_path, monkeypatch)[source]

The config_file column names the file of each key of a shared section.

Both files define the [sub] section and its [sub.leaf] sub-section: at each depth, the local file is named for the key both set, and the parent file for the key only it sets.

tests.test_parameters.test_show_params_config_file_single_file(invoke, create_config)[source]

A single loaded file is attributed to every config-sourced parameter.

tests.test_parameters.test_show_params_config_file_url(invoke, httpserver, table_format, load_rows)[source]

A remote configuration is named by its exact URL, in text and JSON tables.

A text table shrinks a local path to ~, but a URL is not a path: normalized as one, it loses a slash after its scheme. The JSON table, which never shrinks, is the control.

tests.test_parameters.test_show_params_config_file_column_is_opt_in(invoke)[source]

config_file is addressable by ID but stays out of the default table.

tests.test_parameters.test_show_params_config_file_column_drawn_when_cascading(invoke)[source]

A cascading --config draws the config_file column without being asked.

Several files can then take part, so Source alone no longer tells which one a value came from.

tests.test_parameters.test_show_params_projection_overrides_the_cascade_column(invoke)[source]

An explicit --columns projection is honored as given, cascade or not.

tests.test_parameters.test_show_params_subclass_widens_the_default_columns(invoke)[source]

A subclass widening default_columns() draws the column it added.

That hook is the only way to reach an opt-in column on a CLI exposing no --columns option of its own, or one whose --columns belongs to its subcommands and carries a different vocabulary.

tests.test_parameters.test_column_registry_is_consistent()[source]

TABLE_HEADERS exposes parallel column_labels() / column_ids().

tests.test_parameters.test_find_column_known_and_unknown()[source]
tests.test_parameters.test_render_doc_table_emits_markdown()[source]

render_doc_table returns a 2-column Markdown table covering every column.

tests.test_parameters.test_columns_option_projects_and_orders(invoke)[source]

--columns keeps only selected columns and preserves the user order.

tests.test_parameters.test_columns_option_rejects_unknown_id(invoke)[source]

An unknown column ID raises a UsageError with available IDs listed.

tests.test_parameters.test_params_rows_keep_path_order_under_projection(invoke, columns)[source]

Rows follow their parameter path, whichever column comes first.

--zulu is declared under the alpha name: ordered by its spec, its row would follow the one of --beta.

tests.test_parameters.test_params_never_prompts(invoke)[source]

Introspecting an option that declares a prompt does not ask for its value.

The value of a parameter is replayed from the command line, which leaves the one a user would type at the prompt missing.

tests.test_parameters.test_params_confirmation_prompt_string_reads_as_set(invoke)[source]

A custom confirmation prompt asks for confirmation, like True does.

tests.test_parameters.test_recurse_subcommands(invoke)[source]
tests.test_parameters.test_subcommand_conflicts_with_parent_param(invoke)[source]

A subcommand whose name matches its direct parent’s param is skipped in the parameter tree (the config key would be ambiguous), but does not crash the CLI.

[root.alpha]
# "foo" is ambiguous: is it the --foo param or the [root.alpha.foo] subcommand?
foo = "apple"

See: https://github.com/kdeldycke/click-extra/pull/1286

tests.test_parameters.test_nested_subcommand_no_false_conflict_with_root_param(invoke)[source]

A nested subcommand can share a name with a root-level param without conflict.

The config paths are distinct (root.verbose vs root.alpha.verbose), so there is no ambiguity.

See: https://github.com/kdeldycke/click-extra/pull/1286

tests.test_parameters.test_standalone_table_rendering(invoke, opt1, opt2, table_format)[source]

Check all rendering styles of the table with standalone --params and --table-format option.

tests.test_parameters.test_standalone_no_color_rendering(invoke, opt1, opt2, opt3, table_format)[source]

Check that all rendering styles are responding to the --color option.

tests.test_parameters.test_option_value_kind(opt, expected)[source]
tests.test_parameters.test_iter_subcommands_hidden_handling()[source]
tests.test_parameters.test_iter_subcommands_empty_for_non_group()[source]
tests.test_parameters.test_iter_params_for_display_follows_the_help_screen()[source]

Presentation order, not the processing order carried by params.

tests.test_parameters.test_iter_params_for_display_keeps_option_groups_together()[source]

Options declared in a Cloup group are yielded under that group.

tests.test_parameters.test_iter_params_for_display_falls_back_to_get_params()[source]

A plain Click command holds a single order, and it is the one to yield.

tests.test_parameters.test_iter_params_for_display_yields_late_additions_last()[source]

A parameter attached after construction is missing from the cached groups.

tests.test_parameters.test_help_column_is_opt_in(invoke)[source]

The Help column exists, stays out of the way, and answers to its ID.

tests.test_parameters.test_help_column_paints_the_deprecation_marker(invoke)[source]

The marker takes the slot the help screen paints it with.

Every other column of the rendered table is themed, and the marker is the one part of this one Click Extra writes rather than the CLI author.

tests.test_parameters.PARAMETER_SPEC_CLICK_VERSION = (8, 6)

Click release naming a parameter by Parameter.spec in a deprecation notice.

From that release on, Click prints the flag with the longest prefix: --apt-cyg for --apt-cyg/--no-apt-cyg. Below it Click prints the apt_cyg identifier, which Click Extra replaces with every spelling.

tests.test_parameters.DEPRECATED_FLAG_NAME = '--apt-cyg / --no-apt-cyg'

How a notice names --apt-cyg/--no-apt-cyg on the installed Click.

tests.test_parameters.test_option_deprecation_notice_names_its_flags(invoke)[source]

The notice names a flag a user types, not the identifier Click derived.

Below PARAMETER_SPEC_CLICK_VERSION, Click reports human_readable_name, which for an option is self.name: the one spelling that appears nowhere on the command line.

tests.test_parameters.test_option_error_hint_names_each_environment_variable(invoke, envvar, hint)[source]

The hint of a failing option quotes each variable that feeds it.

Click Extra stores every envvar as a tuple, which the hint must not print as its repr.

tests.test_parameters.test_deprecation_notice_reaches_a_configuration_file(invoke, create_config)[source]

A file switching a deprecated parameter on gets the notice too.

Click stops one rank short of the configuration, so the one place a selection outlives the project it names said nothing.

tests.test_parameters.test_deprecation_notice_stays_silent_on_the_default(invoke)[source]

Nobody asked for the parameter, so nothing announces it.

tests.test_parameters.test_deprecation_notice_is_emitted_once(invoke, create_config)[source]

A parameter named on both the command line and in a file warns once.

tests.test_parameters.test_help_column_is_documented()[source]

The auto-generated column reference covers the opt-in column too.

tests.test_parameters.POSITIONAL_DEFINITIONS = frozenset({'SortByOption'})

Option classes taking positional definitions ahead of param_decls.

SortByOption reads its column definitions positionally, which is why its param_decls is keyword-only (see click_extra.decorators.sort_by_option).

tests.test_parameters.test_option_constructors_share_one_shape(option_class)[source]

param_decls first, then annotated keyword-only arguments, then **kwargs.

One shape across every option class: a caller never has to look up the positional order of a passthrough, and an unannotated one cannot slip in.

tests.test_parameters.test_option_constructors_leave_the_root_logger_alone(option_class)[source]

Building an option changes neither the handlers nor the level of root.

An option is built when its command is decorated, so when the module of the command is imported. The root logger belongs to the whole process by then: an application importing the CLI, or pytest collecting it, has its handlers there.

tests.test_parameters.test_options_lend_the_root_logger_a_handler_for_the_run(option_class)[source]

Each option gives root the handler of Click Extra when Click prepares to parse a command line, and takes it back when the context closes.

Any of these options may report through log records: none may count on another option of the command to bring the handler that prints them.

tests.test_parameters.test_params_lists_the_envvars_a_subcommand_reads(invoke)[source]

A subcommand setting its own auto_envvar_prefix is listed under it.

The walk behind --params builds each subcommand’s context the way Click does for a real invocation, context settings included. A prefix derived from the parent’s would list GARDEN_PLANT_CROP, a variable the CLI never reads.

tests.test_prebake module

Tests for the one-pass dunder baking.

The single-field prebake_dunder and prebake_version helpers are covered in test_version.py, beside the version option that reads what they write. The project metadata readers are covered in test_project.py.

tests.test_prebake.PYPROJECT_SCRIPTS = '[project]\nname = "orchard"\n\n[project.scripts]\norchard = "orchard.cli:main"\n'

A pyproject.toml declaring one console script.

tests.test_prebake.src_project(tmp_path)[source]

A src-layout project declaring one console script.

Its package carries two empty placeholders: a build field, which always resolves, and a git field, which cannot resolve outside a repository.

tests.test_prebake.test_prebake_dunders_fills_every_empty_placeholder_in_one_pass(tmp_path)[source]

Every empty placeholder is filled, and only the resolvers it needs run.

A placeholder already holding a value and a field with no placeholder at all never trigger their resolver, so a build does not shell out for nothing.

tests.test_prebake.test_prebake_dunders_leaves_a_placeholder_its_resolver_cannot_fill(tmp_path)[source]

A resolver answering nothing leaves its placeholder, and the file, alone.

tests.test_prebake.test_prebake_dunders_fills_two_placeholders_sharing_a_line(tmp_path)[source]

Two literals on one line both land, whatever the first one’s new length.

tests.test_prebake.test_prebake_dunders_writes_a_literal_holding_the_value(tmp_path, placeholder, value, expected)[source]

The baked literal parses back to the value, in the quotes it opened with.

A value can carry the quote character the placeholder is written in, like a branch named after a fruit’s owner: escaped, it cannot close the literal early, and the file still compiles.

tests.test_prebake.test_prebake_dunders_counts_columns_past_non_ascii_text(tmp_path)[source]

A placeholder after a non-ASCII literal on the same line lands in place.

The parser counts columns in UTF-8 bytes: read as characters, every multi-byte character ahead of the placeholder would shift the splice right.

tests.test_prebake.test_prebake_all_bakes_a_src_layout_project(invoke, src_project, monkeypatch)[source]

prebake all finds the package of a src-layout project on its own.

A build field always resolves, so its placeholder proves the file was found and written. The git field stays empty outside a repository.

tests.test_progress module

tests.test_progress.test_progressbar_follows_progress_flag(invoke, args, expected_hidden)[source]

click_extra.progressbar gates hidden on the resolved –progress flag.

tests.test_progress.test_progressbar_explicit_hidden_overrides_flag(invoke, forced)[source]

An explicit hidden= wins over –no-progress, like echo(color=…).

tests.test_progress.test_progressbar_shown_without_active_context()[source]

Outside a Click command the bar defaults to shown, like click.progressbar.

tests.test_progress.test_progressbar_label_emission_off_tty(invoke, args, label_shown)[source]

Off a TTY a shown bar still emits its label once; a hidden one emits nothing.

tests.test_progress.test_progressbar_shows_final_position_with_update_min_steps()[source]

With an update_min_steps that does not divide the length, the bar still lands on total/total instead of the last multiple (14/20 for a length of 20 and an update_min_steps of 7).

tests.test_progress.test_progressbar_prints_a_log_record_above_itself(monkeypatch)[source]

A record logged while a bar draws lands on a line of its own, above it.

Click draws a bar with a carriage return and no line feed until it finishes, so a record written in between continued the bar’s row, after its position.

tests.test_progress.LOGGING_UNDER_A_BAR_SCRIPT = '\nimport logging\nimport time\n\nfrom click_extra import command, progressbar\n\n\n@command\ndef sort_plums():\n    with progressbar(range(3), label="Sorting plums") as bar:\n        for _ in bar:\n            logging.warning("Opening the fruit basket")\n            time.sleep(0.05)\n\n\nsort_plums()\n'

A CLI logging from inside an open progress bar.

tests.test_progress.test_progressbar_logging_on_a_pseudo_terminal()[source]

On a real terminal, each record a CLI logs under a bar gets its own row.

tests.test_progress.test_progressbar_draws_on_stderr_by_default()[source]

The bar draws where log records and the other live lines go.

tests.test_progress.test_progressbar_draws_fast_items_once_per_interval()[source]

Items faster than the interval draw nothing between the first and the last position, however many there are.

tests.test_progress.test_progressbar_draws_every_slow_item()[source]

Items slower than the interval each draw their position, so a bar over slow items never looks frozen.

tests.test_progress.test_progressbar_keeps_an_explicit_update_min_steps()[source]

A caller stating update_min_steps keeps Click’s gate on a count of items.

tests.test_progress.test_progressbar_show_eta_follows_time_flag()[source]

The bar’s ETA follows –time by default; an explicit show_eta wins.

tests.test_project module

Tests for the project and entry-point discovery of click_extra.project.

tests.test_project.PYPROJECT_SCRIPTS = '[project]\nname = "orchard"\n\n[project.scripts]\norchard = "orchard.cli:main"\n'

A pyproject.toml declaring one console script.

tests.test_project.SETUP_CFG_SCRIPTS = '[options.entry_points]\nconsole_scripts =\n    vineyard = vineyard.cli:main\n'

A setup.cfg declaring one console script.

tests.test_project.src_project(tmp_path)[source]

A src-layout project declaring one console script.

Its package carries two empty placeholders: a build field, which always resolves, and a git field, which cannot resolve outside a repository.

tests.test_project.test_read_project_scripts_prefers_pyproject(tmp_path)[source]

[project.scripts] wins over a setup.cfg sitting beside it.

tests.test_project.test_read_project_scripts_falls_back_to_setup_cfg(tmp_path)[source]

console_scripts in setup.cfg is read when pyproject.toml has none.

tests.test_project.test_read_project_scripts_without_metadata(tmp_path)[source]
tests.test_project.test_entry_point_package(target, expected)[source]
tests.test_project.test_locate_package_root(tmp_path, layout, expected)[source]

The directory to import from follows the package through both layouts.

tests.test_project.test_locate_package_root_misses_an_absent_package(tmp_path)[source]
tests.test_project.test_discover_package_init_files_finds_a_src_layout(src_project)[source]

A package under src/ is discovered like one at the project root.

tests.test_project.test_discover_package_init_files_defaults_to_the_current_directory(src_project, monkeypatch)[source]

Without a directory, the paths are relative to the current one.

tests.test_project.test_discover_finds_init(tmp_path, monkeypatch)[source]

Discovers __init__.py from [project.scripts].

tests.test_project.test_discover_no_pyproject(tmp_path, monkeypatch)[source]

Returns empty list when pyproject.toml is missing.

tests.test_project.test_discover_no_scripts(tmp_path, monkeypatch)[source]

Returns empty list when [project.scripts] is absent.

tests.test_project.test_discover_deduplicates(tmp_path, monkeypatch)[source]

Multiple scripts from the same package yield one path.

tests.test_project.test_detect_source_package_single(tmp_path)[source]
tests.test_project.test_detect_source_package_src_layout(tmp_path)[source]
tests.test_project.test_detect_source_package_setup_cfg_only(tmp_path)[source]

A project declaring its scripts in setup.cfg alone is detected too.

tests.test_project.test_detect_source_package_no_entry_point(tmp_path)[source]
tests.test_project.test_detect_source_package_multiple(tmp_path)[source]
tests.test_project.test_detect_source_package_no_project(tmp_path)[source]

A directory holding no packaging metadata names the place it read.

tests.test_project.test_resolve_entry_point_ambiguity_rule(tmp_path, scripts, target)[source]

The scripts must name one target, however many names point to it.

Two targets are ambiguous even inside one package: nothing tells which function the project means.

tests.test_project.test_resolve_entry_point_src_layout(src_project)[source]

The entry point knows its module, function, package and import root.

tests.test_project.test_resolve_entry_point_package_not_on_disk(tmp_path)[source]

A package absent from disk leaves the root to each caller to settle.

tests.test_project.test_callers_share_the_ambiguity_rule(tmp_path)[source]

wrap and convert-to-myst refuse the same project, each its own way.

tests.test_pygments module

tests.test_pygments.lex(text, **options)[source]

Shorthand: lex text and return (token_type, value) pairs.

options go to the AnsiColorLexer, like true_color=False.

Return type:

list[tuple]

tests.test_pygments.collect_classes(klass, prefix='Ansi')[source]

Returns all classes defined in click_extra.pygments that are a subclass of klass, and whose name starts with the provided prefix.

tests.test_pygments.get_pyproject_section(*section_path)[source]

Descends into the TOML tree of pyproject.toml to reach the value specified by section_path.

Return type:

dict[str, str]

tests.test_pygments.check_entry_points(entry_points, *section_path)[source]
Return type:

None

tests.test_pygments.test_ansi_lexers_candidates(tmp_path)[source]

Look into Pygments test suite to find all ANSI lexers candidates.

Good candidates for ANSI colorization are lexers that are producing Generic.Output tokens, which are often used by REPL-like and scripting terminal to render text in a console.

The list is manually maintained in Click Extra code, and this test is here to detect new candidates from new releases of Pygments.

Attention

The Pygments source code is downloaded from GitHub in the form of an archive, and extracted in a temporary folder.

The version of Pygments used for this test is the one installed in the current environment.

Danger

Security check While extracting the archive, we double check we are not fed an archive exploiting relative .. or . path attacks.

tests.test_pygments.test_session_lexers_cover_every_shell_session_lexer()[source]

Every Pygments lexer built on ShellSessionBaseLexer has an ANSI variant.

collect_session_lexers names its lexers by hand rather than walking all of Pygments at import time, so this walk is what notices a new one.

tests.test_pygments.test_formatter_entry_points()[source]
tests.test_pygments.test_filter_entry_points()[source]
tests.test_pygments.test_lexer_entry_points()[source]
tests.test_pygments.test_registered_formatters()[source]
tests.test_pygments.test_registered_filters()[source]
tests.test_pygments.test_registered_lexers()[source]
tests.test_pygments.test_plain_text(text, expected)[source]

Plain text without escape sequences passes through unchanged.

tests.test_pygments.test_sgr_fg_standard(code, color)[source]

SGR 30-37 set standard foreground colors.

tests.test_pygments.test_sgr_bg_standard(code, color)[source]

SGR 40-47 set standard background colors.

tests.test_pygments.test_sgr_fg_bright(code, color)[source]

SGR 90-97 set bright foreground colors.

tests.test_pygments.test_sgr_bg_bright(code, color)[source]

SGR 100-107 set bright background colors.

tests.test_pygments.test_sgr_text_attribute(code, attr)[source]

SGR attribute codes set the corresponding text styling.

tests.test_pygments.test_sgr_attribute_reset(set_code, reset_code, attr)[source]

Each attribute can be individually reset by its specific SGR code.

tests.test_pygments.test_sgr22_resets_bold_and_faint()[source]

SGR 22 (normal intensity) resets both bold and faint simultaneously.

tests.test_pygments.test_sgr0_resets_all()[source]

SGR 0 resets all attributes and colors.

tests.test_pygments.test_empty_sgr_is_reset()[source]

An empty SGR sequence (ESC [ m) is equivalent to SGR 0.

tests.test_pygments.test_sgr39_resets_foreground()[source]

SGR 39 resets foreground color to default.

tests.test_pygments.test_sgr49_resets_background()[source]

SGR 49 resets background color to default.

tests.test_pygments.test_sgr39_keeps_other_attributes()[source]

SGR 39 only resets foreground; other attributes persist.

tests.test_pygments.test_sgr_combined(params, expected_token)[source]

Multiple SGR codes in a single escape sequence are applied together.

tests.test_pygments.test_256color_fg(index)[source]

SGR 38;5;n sets foreground to 256-color index.

tests.test_pygments.test_256color_bg(index)[source]

SGR 48;5;n sets background to 256-color index.

tests.test_pygments.test_24bit_rgb_fg_quantized(r, g, b, expected_idx)[source]

SGR 38;2;r;g;b quantizes to nearest 256-color index when true_color=False.

tests.test_pygments.test_24bit_rgb_bg_quantized(r, g, b, expected_idx)[source]

SGR 48;2;r;g;b quantizes background to nearest 256-color index when true_color=False.

tests.test_pygments.test_truecolor_fg_preserves_hex(r, g, b, expected_hex)[source]

With true_color=True, SGR 38;2;r;g;b emits Token.Ansi.FG_{hex}.

tests.test_pygments.test_truecolor_bg_preserves_hex(r, g, b, expected_hex)[source]

With true_color=True, SGR 48;2;r;g;b emits Token.Ansi.BG_{hex}.

tests.test_pygments.test_truecolor_combined_fg_and_bg()[source]

Foreground and background 24-bit RGB combine into a single compound token.

tests.test_pygments.test_truecolor_with_attribute()[source]

Bold + 24-bit RGB combines into a single compound token.

tests.test_pygments.test_truecolor_default_enabled()[source]

Default AnsiColorLexer() preserves 24-bit RGB as FG_{rrggbb} tokens.

tests.test_pygments.test_truecolor_explicit_disable_quantizes()[source]

AnsiColorLexer(true_color=False) quantizes RGB to the 256-color palette.

tests.test_pygments.test_truecolor_invalid_range_ignored()[source]

Out-of-range RGB values are skipped in true-color mode too.

tests.test_pygments.test_truecolor_truncated_params_ignored()[source]

Truncated RGB params set no color in true-color mode either.

The stray 2 reads as SGR 2 (faint), as in the quantized mode: see test_24bit_truncated_params.

tests.test_pygments.test_truecolor_filter_forwards_flag()[source]

AnsiFilter(true_color=True) forwards the flag to its inner lexer.

tests.test_pygments.test_truecolor_session_lexer_forwards_flag()[source]

Lexer kwarg true_color=True flows through _AnsiFilterMixin.

tests.test_pygments.test_true_color_lexer_option_reads_a_string(alias, value, expected)[source]

A lexer reads a true_color string as the boolean it spells.

tests.test_pygments.test_true_color_filter_option_reads_a_string(value, expected)[source]

The filter reads a true_color string as the boolean it spells.

tests.test_pygments.test_formatter_renders_truecolor_inline_style()[source]

AnsiHtmlFormatter emits inline style for FG_/BG_ tokens.

tests.test_pygments.test_formatter_renders_truecolor_background()[source]

Background 24-bit RGB renders as background-color inline style.

tests.test_pygments.test_formatter_truecolor_combined_with_class_styling()[source]

Bold + RGB renders both a CSS class for Bold and an inline style for the color.

tests.test_pygments.test_formatter_truecolor_fg_and_bg_nested_spans()[source]

Both fg and bg RGB on the same token produce two nested inline-style spans.

tests.test_pygments.test_formatter_quantize_path_no_inline_styles()[source]

When true_color=False is opted into, no inline styles are emitted.

tests.test_pygments.test_formatter_osc8_with_truecolor_coexist()[source]

OSC 8 hyperlink and 24-bit RGB tokens cooperate in the same span.

Both mechanisms inject Private Use Area markers into the token stream (_LINK_* for hyperlinks, _RGB_* for RGB colors) and rely on independent post-processing passes in format_unencoded. This test pins the contract that both rewrites happen and don’t interfere.

tests.test_pygments.test_extra_css_matches_sgr_attributes()[source]

EXTRA_ANSI_CSS keys match the attribute component names.

tests.test_pygments.test_extra_css_follows_the_style_table()[source]

Every attribute but blink renders with the declaration Style.to_css writes.

Faint in particular dims to the same opacity as a dim style. Blink has no CSS declaration of its own and takes the keyframe animation instead.

tests.test_pygments.test_non_sgr_csi_stripped()[source]

Non-SGR CSI sequences (cursor movement, etc.) are stripped.

tests.test_pygments.test_csi_ends_on_its_final_byte(text, expected)[source]

A CSI sequence ends on its ECMA-48 final byte, a letter or not.

A final byte like the ~ of the bracketed-paste markers ends the sequence: the text after it is kept whole.

tests.test_pygments.test_escape_ends_on_its_final_byte(text, expected)[source]

An escape carrying intermediate bytes is stripped up to its final byte.

ESC # 8 is the screen alignment test: the # is an intermediate byte and the 8 the final one. A grammar consuming one byte after the escape left that 8 in the text, where strip_ansi() removed it.

tests.test_pygments.test_vt100_charset_stripped()[source]

VT100 charset selection escapes are stripped.

tests.test_pygments.test_vt100_charset_g1_stripped()[source]

ESC ) designator for G1 charset is stripped.

tests.test_pygments.test_unknown_escape_stripped()[source]

Unknown single-byte escape sequences are stripped.

tests.test_pygments.test_bare_escape_at_end()[source]

A lone ESC at the end of input is consumed without error.

tests.test_pygments.test_osc_sequence_stripped()[source]

Non-hyperlink OSC sequences are fully consumed and stripped.

tests.test_pygments.test_osc_st_terminated_stripped()[source]

OSC sequences terminated by ST (ESC ) are fully stripped.

OSC 8 hyperlinks emit link start/end tokens around the visible text.

tests.test_pygments.test_osc8_with_sgr()[source]

OSC 8 hyperlink combined with SGR color preserves both.

tests.test_pygments.test_osc8_unsafe_scheme_stripped(url)[source]

OSC 8 with unsafe or missing URL scheme is stripped silently.

tests.test_pygments.test_osc8_implicit_close()[source]

A new OSC 8 link implicitly closes the previous one.

tests.test_pygments.test_osc8_unclosed()[source]

An unclosed OSC 8 link is automatically closed at end of input.

tests.test_pygments.test_osc8_close_without_open()[source]

An OSC 8 close without a preceding open is silently ignored.

tests.test_pygments.test_osc8_survives_a_full_reset()[source]

SGR 0 inside a link resets the styling alone: the link closes at its OSC 8.

tests.test_pygments.test_color_persists_until_changed()[source]

A foreground color set in one sequence persists until explicitly changed.

tests.test_pygments.test_multiple_color_changes()[source]

Color can be changed multiple times without resetting.

tests.test_pygments.test_attribute_stacking()[source]

Attributes accumulate: bold then italic produces bold+italic.

tests.test_pygments.test_independent_fg_bg()[source]

Foreground and background colors are independent of each other.

tests.test_pygments.test_sgr_with_trailing_semicolons()[source]

An empty trailing parameter is a 0, which resets the color set before it.

tests.test_pygments.test_sgr_leading_semicolons()[source]

An empty leading parameter is a 0: a reset, then the color after it.

The lexer shares the decoder of split_ansi, which reads \x1b[;31m as red, so a page renders what a terminal shows.

tests.test_pygments.test_sgr_double_semicolons()[source]

An empty parameter between two codes is a reset between them.

tests.test_pygments.test_sgr_unknown_codes_ignored()[source]

Unknown SGR codes are silently ignored; known codes still apply.

tests.test_pygments.test_256color_truncated_params()[source]

Truncated 256-color sequence (missing color index) leaves leftover codes.

38;5: code 38 triggers extended color handling but needs at least 2 more values (mode + index). Only 1 remains (5), so 38 skips. Then 5 is processed as SGR 5 (blink).

tests.test_pygments.test_256color_out_of_range()[source]

256-color index outside 0-255 is ignored.

tests.test_pygments.test_24bit_truncated_params()[source]

A truncated 24-bit RGB sequence sets no color.

38;2;255;128 misses the blue channel, so the decoder skips 38 without consuming anything after it: 2 is then read as SGR 2 (faint), and the two channel values are unknown codes. The same reading split_ansi gives the sequence.

tests.test_pygments.test_24bit_out_of_range()[source]

24-bit RGB values outside 0-255 are ignored.

tests.test_pygments.test_extended_color_unknown_mode()[source]

Extended color with unknown mode (not 5 or 2) sets no color.

38;3;100: code 38 is skipped on its own, since neither of the two color modes follows it. 3 is then read as SGR 3 (italic) and 100 as SGR 100 (bright black background), the reading split_ansi gives the sequence.

tests.test_pygments.test_non_numeric_sgr_params()[source]

Non-numeric characters in CSI params cause partial consumption.

ESC[abc;31m is not a valid SGR sequence. The regex consumes ESC[a as a CSI with a as the final byte, leaving bc;31mtext as plain text.

tests.test_pygments.test_consecutive_resets()[source]

Multiple consecutive resets are harmless.

tests.test_pygments.test_empty_text_between_sequences()[source]

Sequences with no text between them produce no empty tokens.

tests.test_pygments.test_newline_in_colored_text()[source]

Newlines within colored text are preserved in the token value.

tests.test_pygments.test_interleaved_text_and_escapes()[source]

Complex interleaving of plain text and escape sequences.

tests.test_pygments.test_lexer_resets_between_calls()[source]

Each call to get_tokens starts from a clean state.

tests.test_pygments.test_ansi_styles_has_all_named_colors()[source]

Style dict contains entries for all 16 named foreground and background colors.

tests.test_pygments.test_ansi_styles_has_256_palette()[source]

Style dict contains entries for all 256 foreground and background indices.

tests.test_pygments.test_ansi_styles_excludes_all_attributes()[source]

All text attribute tokens are absent from the style dict.

Furo’s dark-mode CSS generator adds color: #D0D0D0 to every token in the style dict. For attribute tokens, this overrides actual foreground colors on compound tokens when the attribute rule appears later in the CSS cascade. All attribute styling is handled by EXTRA_ANSI_CSS / custom.css instead.

tests.test_pygments.test_ansi_styles_count()[source]

Style dict has only color entries: 32 named + 512 indexed.

tests.test_pygments.test_formatter_no_color_on_attribute_css()[source]

CSS rules for attribute-only tokens must not set a color property.

When an attribute token (like -Ansi-Strikethrough) has a color property in its CSS rule, it can override the foreground color of a sibling color token (like -Ansi-Red) if the attribute rule appears later in the CSS cascade. This regression test catches the issue that Furo’s dark-mode generator exposed.

tests.test_pygments.test_palette_256_completeness()[source]

256-color palette has exactly 256 entries.

tests.test_pygments.test_palette_256_hex_format()[source]

All palette values are 7-character hex strings.

tests.test_pygments.test_lexer_map_completeness()[source]

LEXER_MAP has one entry per session lexer.

tests.test_pygments.test_ansi_variant_belongs_to_this_module(original, variant)[source]

Each variant lives in click_extra.pygments, under the name its entry point resolves, and derives from the lexer it varies.

tests.test_pygments.test_ansi_variant_subclass_keeps_its_names()[source]

A subclass of a variant inherits its names and aliases, prefixed once.

tests.test_pygments.test_ansi_filter_transforms_output_tokens()[source]

AnsiFilter converts Generic.Output tokens containing ANSI codes.

tests.test_pygments.test_ansi_filter_passes_through_other_tokens()[source]

AnsiFilter does not modify tokens that are not Generic.Output.

tests.test_pygments.test_formatter_css_classes_single_color()[source]

Single-color token gets the expected CSS classes.

tests.test_pygments.test_formatter_css_classes_compound()[source]

Compound token gets decomposed CSS classes.

tests.test_pygments.test_formatter_css_classes_256color()[source]

256-color tokens get the correct CSS class.

tests.test_pygments.test_formatter_style_defs_contain_ansi_colors()[source]

get_style_defs() includes CSS rules for ANSI color tokens.

tests.test_pygments.test_formatter_shares_the_augmented_style()[source]

Two formatters on the same base style run on one augmented style class.

tests.test_pygments.test_formatter_css_classes_cache_keyed_on_prefix()[source]

The shared class cache tells formatters with different prefixes apart.

OSC 8 hyperlink is rendered as an HTML <a> tag.

tests.test_pygments.test_formatter_osc8_with_color()[source]

OSC 8 hyperlink combined with SGR color renders both link and color.

tests.test_pygments.test_formatter_osc8_url_escaping()[source]

URLs with special HTML characters are properly escaped in href.

get_style_defs() includes CSS for hyperlink styling.

Unsafe URL schemes produce no <a> tag in formatted output.

tests.test_pygments.test_formatter_osc8_quotes_escaped_in_href()[source]

Quotes in a hyperlink URL cannot break out of the href attribute.

Pygments escapes quotes in token text only before its 2.21.0 release, so the formatter escapes them itself, with the entities older Pygments writes. Ampersands are left to Pygments, and are escaped once.

tests.test_pygments.test_real_world_ansi(text, expected_tokens)[source]

Real-world ANSI patterns from terminal tools and documentation references.

tests.test_pygments.test_ansi_session_decodes_a_styled_input_line()[source]

An input line arriving styled keeps its colors instead of its escape bytes.

A session lexer reads any line opening on a prompt as a typed command, and a --help epilog writing $ my-cli pick --ripe matches. A shell lexer reading its escapes would tokenize \x1b[36m as an operator between two runs of text, and leave the raw bytes in the page.

tests.test_pygments.test_ansi_session_leaves_a_plain_input_line_to_the_shell_lexer()[source]

Input carrying no escape still gets its shell highlighting.

tests.test_pygments.test_ansi_variant_keeps_the_text_of_its_original(original, variant)[source]

A variant renders a session free of escapes to the text of its original.

The blank lines opening and closing an output block included: each lexer reads its own prompts in _PLAIN_SESSIONS, so it splits the text into input and output blocks at different places.

tests.test_pygments.test_no_click_extra_help_screen_renders_raw_escapes()[source]

No help screen this package ships leaks an escape byte into its HTML.

The whole CLI tree is rendered the way click:run does it, then lexed and formatted. Whatever the lexer fails to decode surfaces as a literal \x1b in the page, so the assertion reads the formatted output rather than the token stream.

tests.test_pyproject module

Consistency checks tying the test matrix to the declared dependencies.

The click version axis of the test matrix is the one dependency whose whole supported range is exercised release-by-release (a patch can change behavior mid-stream). These tests keep that axis in sync with the click dependency specifier, so a drift, like a new Click release or a raised floor, fails CI instead of silently rotting.

tests.test_pyproject.PYPROJECT = PosixPath('/home/runner/work/click-extra/click-extra/pyproject.toml')

Path to the project’s pyproject.toml, relative to this test file.

tests.test_pyproject.SENTINELS = ('released', 'stable', 'main')

Moving-reference values of the click-version axis: the lockfile-resolved release, and Click’s stable and main development branches. Everything else in the axis is a pinned release number.

tests.test_pyproject.UV_LOCK = PosixPath('/home/runner/work/click-extra/click-extra/uv.lock')

Path to the project’s uv.lock, which holds the Click released cells run.

tests.test_pyproject.load_click_matrix()[source]

Read the Click setup from pyproject.toml.

The click-version axis is spread across the forward-looking test-matrix.full-include rows (the pinned releases and the stable / main sentinels) and the released default declared in test-matrix.include.

Return type:

tuple[SpecifierSet, list[str], set[str]]

Returns:

the click dependency specifier, the collected click-version axis values, and the subset that are pinned release numbers (sentinels start with a letter, pinned versions with a digit).

tests.test_pyproject.stable_pypi_versions(package)[source]

Return all non-yanked, non-prerelease versions of package on PyPI.

Return type:

set[Version]

tests.test_pyproject.locked_versions(package)[source]

Return the versions of package that uv.lock pins.

A released cell installs from the lock with uv run --frozen, so it runs this version, not the newest one on PyPI: the cooldown keeps a new release out of the lock for a week.

Return type:

set[str]

tests.test_pyproject.test_click_floor_is_pinned_and_sentinels_present()[source]

The matrix floor stays in sync with the dependency floor (hermetic).

The lowest pinned click-version must equal the lower bound of the click specifier, so raising or lowering the dependency floor without updating the matrix (or vice versa) fails here. Runs offline, unlike the PyPI cross-check below.

tests.test_pyproject.test_click_matrix_matches_authorized_releases()[source]

The pinned releases match exactly the Click releases the specifier allows.

Every release allowed by the click specifier must be referenced in the matrix: the locked one through the released sentinel, every other one by an explicit pin. This asserts the set equality in both directions:

  • a missing pin means a Click release that no cell runs, like a new release that the cooldown still keeps out of uv.lock;

  • a stale pin means a pinned version is no longer an authorized release (the floor was raised past it, or the release was yanked).

Either way, the matrix needs an edit, and this is the signal. There is no exemption list: a release the suite cannot run on gets its own answer at the moment it appears, whether that is a floor bump past it or a hole opened here on purpose.

tests.test_pyproject.test_nuitka_includes_every_lazy_subcommand_module()[source]

A Nuitka build bundles the module of each lazily mounted subcommand.

Nuitka follows import statements only, while a lazy subcommand is imported from a string: a module missing here ships a binary whose subcommand fails.

tests.test_pytest module

Test the Pytest helpers.

tests.test_pytest.test_aligned_colored_fixtures(uncolored, colored)[source]
tests.test_pytest.test_isolated_app_dir(invoke, isolated_app_dir)[source]

Config discovery is repointed at the isolated directory.

A CLI invoked in-process must not see the host’s real configuration folder, and a configuration file planted in the isolated directory must be picked up by the default --config search pattern.

tests.test_pytest.ambient_color_var(request, monkeypatch)[source]

One color variable set in the caller’s shell, before runner starts.

Requested ahead of runner in a signature, so the variable is in place by the time the fixture isolates the environment.

tests.test_pytest.test_runner_ignores_ambient_color_vars(ambient_color_var, runner)[source]

A color variable of the caller’s shell does not reach the invoked CLI.

Each one can switch the colors of the output a test matches as plain text.

tests.test_pytest.ambient_cache_home(monkeypatch, tmp_path)[source]

A cache folder set in the caller’s shell, before runner starts.

Requested ahead of runner in a signature, so the variable is in place by the time the fixture isolates the environment.

tests.test_pytest.test_runner_isolates_the_cache_dir(ambient_cache_home, runner)[source]

The cache directory of a CLI resolves inside the isolated filesystem.

The XDG_CACHE_HOME of the caller’s shell would otherwise send every cache a CLI writes to the folder of the user running the tests.

tests.test_pytest.test_runner_lets_a_test_ask_for_color(runner)[source]

A variable passed to one invocation still turns its colors on.

tests.test_readme module

Tests for readme.md, which ships to PyPI as the long description.

tests.test_readme.URL_RE = re.compile('\\]\\((?P<markdown>[^)\\s]+)|\\s(?:href|src)=["\'](?P<html>[^"\']+)["\']|^\\[[^\\]]+\\]:\\s*(?P<definition>\\S+)', re.MULTILINE)

Target of every link and image in a Markdown file.

Matches the inline Markdown forms, `text <url>`_ and ![alt](url), the href and src attributes of an HTML tag, and a reference definition, [label]: url.

Every link and image of readme.md points at an absolute URL.

pyproject.toml sets readme = "readme.md", so this file ships verbatim as the PyPI long description. GitHub resolves a relative target against the repository. PyPI has no repository to resolve it against, so the image breaks or the link goes nowhere, on the published project page alone.

Return type:

None

tests.test_recording module

Tests for click_extra.capture.recording, which rebuilds a terminal’s screen.

class tests.test_recording.TTYStringIO(initial_value='', newline='\n')[source]

Bases: StringIO

An in-memory text buffer that claims to be an interactive terminal.

isatty()[source]

Return whether this is an ‘interactive’ stream.

Return False if it can’t be determined.

Return type:

bool

tests.test_recording.snapshots(stream)[source]

Replay a stream, keeping the screen each redraw leaves behind.

Splitting on the erase-in-line is what a real sampler does with timestamps instead: every animation ends a frame with one.

Parameters:

stream (str) – what a command wrote, control sequences and all.

Return type:

list[str]

Returns:

the distinct screens, in the order they were drawn.

tests.test_recording.test_screen_replays_a_stream(stream, expected)[source]

The screen shows what a terminal would, for the vocabulary it follows.

tests.test_recording.test_screen_keeps_a_color_set_between_a_return_and_its_text()[source]

A color opened after the cursor comes back still wraps what follows.

An animation writes the return, then its color, then the text. The redraw is deferred until something lands on the row, and the color is the first thing to land: it must trigger the clear and survive it, or the frame draws in the terminal’s default ink.

tests.test_recording.test_screen_feeds_the_same_whether_split_or_whole()[source]

A stream arriving in pieces lands where the whole of it would.

A recorder reads whatever the terminal hands it, which is not aligned on anything: a frame routinely arrives across two reads.

tests.test_recording.test_screen_counts_a_wide_glyph_by_its_cells()[source]

A wide glyph covers the two cells it is drawn with.

The count is what says whether the cursor sits at the start of a row, which is what an erase-in-line acts on.

tests.test_recording.test_screen_recovers_every_frame_a_spinner_draws(monkeypatch)[source]

Replaying a real spinner’s stream gives back its frames, and only those.

The end of the loop this module exists to close: frame_lines() says what the spinner draws, the spinner writes it at a terminal, and the screen rebuilds those same lines out of the stream.

class tests.test_recording.StepClock(step=1.0)[source]

Bases: object

A clock advancing a fixed step per reading, so a test can time frames.

tests.test_recording.test_recorder_claims_to_be_a_terminal()[source]

A spinner animates for a recorder without being told to.

The whole question a spinner asks before drawing is whether its stream is a terminal. Answering it is what makes the in-process path need no pseudo-terminal, and therefore work on every platform.

tests.test_recording.test_recorder_frames_carry_what_the_screen_held()[source]

Each frame lasts until the next screen replaced it.

tests.test_recording.test_recorder_drops_a_write_that_changed_nothing()[source]

A write leaving the screen as it was is not a frame of its own.

tests.test_recording.test_recorder_drops_a_cursor_shape_change()[source]

A sequence the screen does not act on adds no frame, whatever its bytes.

ESC [ 2 SP q sets the cursor’s shape and carries an intermediate byte. A grammar stopping at the first letter missed it, so the sequence landed on the screen as text and split one picture into two identical frames.

tests.test_recording.test_recorder_stores_one_sample_per_distinct_screen()[source]

A write changing nothing stores nothing, so memory follows the screens.

A spinner redraws the same frame for as long as its label holds, and a long recording would otherwise keep a copy of the whole screen per redraw.

tests.test_recording.test_recorder_drops_a_screen_holding_only_blanks()[source]

An erasure, and the newline ending it, picture nothing.

tests.test_recording.test_recorder_records_a_spinner_without_a_terminal(monkeypatch)[source]

A spinner handed a recorder animates into it, on any platform.

No pseudo-terminal is involved, which is what a documentation build on Windows depends on.

tests.test_recording.BAR_SCRIPT = "import sys, time\nfor filled in range(4):\n    sys.stderr.write('\\r[' + '#' * filled + ']\\x1b[K')\n    sys.stderr.flush()\n    time.sleep(0.05)\n"

A progress bar drawn to stderr, redrawn in place, as a real one is.

tests.test_recording.test_record_command_recovers_a_foreign_animation()[source]

A command this process does not host still animates, under a terminal.

The case a pipe cannot reach: the program asks whether it is talking to a terminal, and only a pseudo-terminal makes it draw. Its frames go to stderr, where a spinner and a progress bar both write.

tests.test_recording.STUBBORN_SCRIPT = '\nimport signal, sys, time\nsignal.signal(signal.SIGTERM, signal.SIG_IGN)\nsys.stdout.write("ripe")\nsys.stdout.flush()\ntime.sleep(6)\n'

A command that ignores SIGTERM and outlives the recording.

tests.test_recording.test_record_command_kills_a_command_ignoring_sigterm(monkeypatch)[source]

A command ignoring SIGTERM is killed once the grace runs out, so the recording returns instead of waiting for the command to exit on its own.

tests.test_recording.SLEEPER_SCRIPT = '\nimport pathlib, sys, time\nsys.stdout.write("ripe")\nsys.stdout.flush()\npathlib.Path(sys.argv[1]).touch()\ntime.sleep(30)\n'

A command that draws once, touches the file it is given, then waits for a signal to end it.

tests.test_recording.test_record_command_registers_its_command_for_ctrl_c(tmp_path)[source]

A recording registers its command where the Ctrl+C handlers find it.

A worker thread records while the main thread calls terminate_live_processes, which is what a Ctrl+C runs: the command ends, and so does the recording, long before its duration. The interrupt waits for the command to draw, which a slow runner can delay past registration.

tests.test_recording.STUBBORN_PARENT_SCRIPT = '\nimport signal, subprocess, sys, time\nsignal.signal(signal.SIGTERM, signal.SIG_IGN)\nseed = subprocess.Popen((sys.executable, "-c", "import time; time.sleep(30)"))\nsys.stdout.write(f"seed={seed.pid}")\nsys.stdout.flush()\ntime.sleep(30)\n'

A command ignoring SIGTERM that starts a child of its own, which does too.

tests.test_recording.test_record_command_kills_what_its_command_started(monkeypatch)[source]

A command killed at the end of its recording takes its descendants along.

They would otherwise outlive the recording, holding the terminal it drew on.

tests.test_recording.SPLIT_GLYPH_SCRIPT = '\nimport sys, time\nraw = sys.stdout.buffer\nglyph = "╰".encode("UTF-8")\nraw.write(glyph[:1])\nraw.flush()\ntime.sleep(0.2)\nraw.write(glyph[1:] + b" ripe")\nraw.flush()\n'

A box-drawing corner flushed one byte at a time, as a busy pty read splits it.

tests.test_recording.test_record_command_reassembles_a_glyph_split_across_reads()[source]

A multi-byte glyph straddling two pty reads decodes whole.

A pseudo-terminal hands the stream back in kernel-buffer-sized chunks, so a heavy redraw regularly splits a box-drawing glyph or an emoji across two reads. Decoding each chunk on its own mangles both halves into U+FFFD; the recorder must hold the partial sequence until its tail arrives.

tests.test_recording.test_record_command_closes_its_terminal_when_the_command_cannot_start(tmp_path)[source]

A command that never started leaves no end of its terminal open.

The pseudo-terminal is opened before the command is spawned, so a spawn refused by the system is the one exit that has both ends to close.

tests.test_recording.test_record_and_render_draws_the_prompt_on_every_frame()[source]

The stated invocation leads each frame, styled as a shell prompt.

tests.test_recording.test_record_and_render_hides_an_empty_prompt()[source]

An empty –prompt draws no invocation at all.

tests.test_recording.test_record_and_render_reports_the_exit_code()[source]

The command’s own verdict travels beside the picture of it.

tests.test_recording.test_record_and_render_refuses_an_empty_recording()[source]

A command that drew nothing leaves nothing to animate.

tests.test_recording.text_columns(svg, text)[source]

Every column a capture starts a run reading text on, in any frame.

Return type:

set[int]

tests.test_recording.test_record_and_render_numbers_every_frame_in_one_gutter(monkeypatch)[source]

A screen reaching ten rows does not push its text a cell to the right.

Each frame counts its own rows, from one. Numbered on its own, a nine-row screen aligns its numbers on one digit and the ten-row screen after it on two, so every row moves between the two frames. The recording is stated rather than timed, so both screens are sure to be in it.

tests.test_recording.test_quantize_lands_jitter_on_one_grid(recorded, expected)[source]

Two runs of one unchanged command time their frames the same.

tests.test_recording.test_quantize_keeps_the_frames_it_times()[source]

Rounding a duration never rewrites the screen it belongs to.

tests.test_recording.test_quantize_rejects_a_grid_going_nowhere()[source]
tests.test_recording.test_quantized_recordings_of_one_command_render_the_same_bytes()[source]

The point of the exercise: a rerun rewrites the asset byte for byte.

Two recordings of one unchanged command, timed a few milliseconds apart the way two runs on a differently loaded machine are.

tests.test_recording.test_a_dropped_frame_does_not_move_the_recording()[source]

A frame the scheduler lost leaves the animation’s identity alone.

Quantizing cannot recover it, so the fingerprint is written not to care: it covers the frames a cycle holds and the beat, not how many were caught.

tests.test_recording.test_a_changed_command_moves_the_recording()[source]

What the fingerprint is for: real drift is still reported.

tests.test_recording.TYPED_PROMPT = '\x1b[90m$\x1b[0m \x1b[97mbasket\x1b[0m ripen --all'

A styled prompt line, as format_cli_prompt composes one.

tests.test_recording.test_ansi_prefix_keeps_the_styling_in_force(count, expected)[source]

An escape is drawn nowhere, so every one before the cut still applies.

tests.test_recording.test_ansi_prefix_never_cuts_an_escape(count)[source]

A half-written escape lands on the screen as the digits it is made of.

tests.test_recording.test_type_line_makes_one_frame_per_character()[source]

A keystroke is a screen, so a character is a frame.

tests.test_recording.test_type_line_holds_the_finished_line_for_the_submit_beat()[source]

The pause before the return key, and only on the frame that waits it.

tests.test_recording.test_type_line_types_nothing_for_an_empty_line()[source]

A recording carrying no prompt has no opening to play.

tests.test_recording.test_type_line_rejects_a_beat_going_nowhere(kwargs, message)[source]

Seconds run forwards, and a frame lasting none is never shown.

tests.test_recording.test_a_typed_opening_walks_its_cursor_one_cell_at_a_time()[source]

The caret is the cursor, and it follows the text with no code of its own.

What the design rests on: a cursor is drawn as part of the row it stands on, read off that row’s text, so an animation typing a line gets its caret for nothing.

tests.test_recording.test_a_recording_types_its_prompt_when_asked()[source]

The typed opening leads the recorded frames, under the prompt it types.

tests.test_recording.test_a_recording_types_nothing_unless_asked()[source]

By default, a recording opens on the finished prompt.

tests.test_recording.RIPENED_SCRIPT = "import sys, time\nfor filled in range(4):\n    sys.stderr.write('\\r[' + '#' * filled + ']\\x1b[K')\n    sys.stderr.flush()\n    time.sleep(0.05)\nsys.stderr.write('\\r\\x1b[Kripe\\n')\n"

The same bar, ending on a newline the way a finished command does.

tests.test_recording.last_drawn_row(svg)[source]

Index of the lowest row a capture draws a glyph on, counted from zero.

Matched on the styled runs alone: the caption and the credit line are <text> too, and both sit outside the grid this measures.

Return type:

int

tests.test_recording.window_height(svg)[source]

How tall the capture is, in pixels.

Return type:

float

tests.test_recording.recorded(script, **kwargs)[source]

Record one script, however the capture is asked to close.

Return type:

str

tests.test_recording.test_a_closing_prompt_fills_the_row_the_cursor_waited_on()[source]

A command closing on a newline already left the row the shell wants.

Which is what makes the pair free: the window is the same height either way, and the row holding nothing but a cursor holds a prompt instead.

tests.test_recording.test_a_closing_prompt_opens_a_row_under_a_screen_left_mid_line()[source]

A command that never ended its line is given one, as a shell gives it.

The free case above is the common one and not the only one: a progress bar redrawing in place leaves the cursor mid-line, so the shell coming back costs the row it prints its own newline onto.

tests.test_screenshots module

Tests for click_extra.capture and the captures committed under docs/.

A screenshot is CLI output that stopped re-running, so it rots the moment the command it pictures changes. A rendered capture is machine-readable, which is what makes both halves of this module possible: every run of same-styled characters is one <text> element carrying its column as an x offset and its width as a textLength, so the terminal text can be rebuilt from the glyph coordinates and compared to what a CLI prints today.

That comparison needs neither a network round-trip nor a capture tool, which keeps this an ordinary unit test. When the drift check fails, re-run the command documented alongside the image in docs/screenshots.md.

tests.test_screenshots.ASSETS = PosixPath('/home/runner/work/click-extra/click-extra/docs/assets')

Directory the committed captures live in.

tests.test_screenshots.COMMITTED_PROMPT = '$ '

Prompt sigil every committed capture was shot under.

PROMPT is the running platform’s, and Windows draws > there, so a reshoot has to be normalized back to this one before the two are compared. What the check is about is the command’s output, not which shell drew the line above it.

class tests.test_screenshots.Capture(filename, args, columns=80, head=None, background=TerminalBackground.DARK)[source]

Bases: NamedTuple

A terminal capture committed under docs/assets and embedded in the docs.

Carries what it takes to reshoot it, which is what makes the drift check below able to state the command each image is supposed to picture.

Create new instance of Capture(filename, args, columns, head, background)

filename: str

Name of the SVG under ASSETS.

args: tuple[str, ...]

Arguments the click-extra CLI was invoked with.

columns: int

Terminal width it was shot at.

A CLI wraps its output to the ambient terminal size, so a check running under a different width would compare a differently wrapped screen and fail on the wrapping alone.

head: int | None

Number of leading lines kept, or None when nothing was trimmed.

background: TerminalBackground

Chrome it was drawn on, which is also the terminal it was told it ran in.

property prompt: str

The COMMITTED_PROMPT line the capture draws above its output.

property command: tuple[str, ...]

The command line reproducing this capture through this interpreter.

docs/screenshots.md documents the same commands reached through uv run, which is what a human types from a checkout. Going straight at sys.executable keeps the check independent of uv, and of whether the environment happens to be synced.

tests.test_screenshots.COMMITTED_CAPTURES = (('auto-theme-dark-screen.svg', ('--theme', 'auto', 'themes', '--help'), 80, None, TerminalBackground.DARK), ('auto-theme-light-screen.svg', ('--theme', 'auto', 'themes', '--help'), 80, None, TerminalBackground.LIGHT), ('color-gradient-screen.svg', ('gradient',), 80, None, TerminalBackground.DARK), ('text-styles-screen.svg', ('styles',), 160, 14, TerminalBackground.DARK), ('theme-gallery-screen.svg', ('themes',), 80, 34, TerminalBackground.DARK))

Every capture shot by the screenshot command, in file-name order.

The hello-click* pair opening the readme is not here: those are written by the tutorial’s own click:run blocks at documentation-build time, and guarded by tests/test_sphinx_crossrefs.py instead.

tests.test_screenshots.SAMPLE_CAPTURE = '\x1b[1mFruit\x1b[0m   Colour\nbanana  yellow\nkiwi    green\n\n2 fruits\n'

A small styled capture, with padded columns and a blank line to preserve.

The second column is deliberately left unstyled: sharing a style with the padding that precedes it is what folds the two into a single run, which is the case that would otherwise place a column by the width of the spaces before it. Style the column and the padding becomes a run of its own.

tests.test_screenshots.svg_to_lines(svg)[source]

Rebuild the captured terminal lines from a rendered capture.

Groups the <text> runs into lines by their shared y baseline, then lays each one back on its column: the capture is monospaced, so dividing a run’s x offset by CELL_WIDTH gives the character column it starts at.

Parameters:

svg (str) – source of a rendered capture.

Return type:

list[str]

Returns:

the captured lines, top to bottom, with trailing whitespace removed.

tests.test_screenshots.svg_box(svg)[source]

The width and height of a rendered capture’s own box.

Return type:

tuple[float, float]

tests.test_screenshots.svg_window(svg)[source]

The width and height of the terminal window drawn in a capture.

Return type:

tuple[float, float]

tests.test_screenshots.html_to_text(markup)[source]

Strip a capture’s markup back off, leaving the terminal text.

Return type:

str

tests.test_screenshots.test_trim_lines(head, tail, expected)[source]

head and tail keep their ends and account for what they dropped.

tests.test_screenshots.test_trim_lines_rules_the_default_marker_across_the_picture()[source]

The default marker is a rule, spanning the width the picture is laid out at: the widest kept line when the width is left to the command.

tests.test_screenshots.test_trim_lines_caps_the_rule_at_the_terminal_width()[source]

A kept line wider than the capture draws a rule as wide as the capture.

The rule is measured on the kept lines, so a 150-cell log line among them asked for a 150-cell rule in an 80-column image. The renderer folds any line reaching past the width, so that rule came back as a row of dots plus its tail alone on a second row, started outside the text column.

tests.test_screenshots.test_rule_marker_tracks_the_drawn_width(columns, width_floor, expected)[source]

The auto rule spans the text column the picture is drawn at.

tests.test_screenshots.test_resolve_wrap_marker(wrap_marker, expected)[source]

Only auto is replaced: any other marker, empty included, is taken as is.

tests.test_screenshots.test_trim_lines_leaves_room_for_the_wrap_marker()[source]

A rule capped to the width gives up the cell a wrap marker takes.

Capped to columns alone, the rule filled the row the marker shortens, so it folded on its last dot and closed its row on the marker, reading as a rule that continues below.

tests.test_screenshots.test_capture_leaves_room_for_line_numbers_on_the_rule(prompt, head, tail)[source]

A numbered capture rules its cut inside the text column the gutter leaves.

The gutter takes cells in front of every row, the rule’s included, so a rule as wide as columns would fold on its last dots and put them alone on a row of their own.

tests.test_screenshots.test_render_draws_the_wrap_marker_at_each_fold()[source]

An SVG capture closes each folded row on the marker, and only those rows.

tests.test_screenshots.test_trim_lines_closes_a_style_the_cut_left_open(truncation)[source]

A cut between a style’s opening and its reset does not style the marker.

A heading printed as style("\n● Step") opens its style on the line above the text and resets it on the text’s own line. Cut between the two, the open underline ran into the marker, and into the lines tail kept after it.

tests.test_screenshots.test_trim_lines_adds_no_reset_after_a_closed_style()[source]

A head that closes every style it opens gets its marker untouched.

tests.test_screenshots.test_trim_lines_keeps_a_marker_too_wide_for_its_rule_bare()[source]

A capture narrower than the frame gets the label alone, not a broken rule.

tests.test_screenshots.test_trim_lines_leaves_an_explicit_marker_alone()[source]

Naming a marker takes it verbatim: only auto asks for a rule.

tests.test_screenshots.test_tile_runs_never_opens_a_piece_on_padding()[source]

A tiled column cut at a blank carries it into the piece before it.

A piece is placed by its first glyph, so one opening on a blank draws every tile behind it a cell late. The cut is made on character count, which lands on a blank whenever a label sits inside a rule.

tests.test_screenshots.test_tile_runs_keeps_every_piece_on_its_own_column()[source]

Cutting a tiled column apart never moves a glyph off its cell.

The cut counts characters and the offset counts cells, which part ways on a wide character.

tests.test_screenshots.test_tile_runs_cuts_every_tile_it_was_released_with(tile)[source]

tile_runs cuts Block Elements as well as Box Drawing, as 10.1.1 did.

A capture draws both as shapes, which leaves the deprecated helper no caller in the renderer: its answer stays the released one until it goes. The cut is spelled out, so TILE_RUN keeping its released 8 cells is checked too.

tests.test_screenshots.test_tile_helpers_warn_until_their_removal(name, private)[source]

The tile helpers a capture no longer uses still resolve, with a warning.

tests.test_screenshots.test_svg_module_serves_no_other_missing_name()[source]

A name the module never had stays missing, and sets off no warning.

tests.test_screenshots.named_eighths(char)[source]

The eighths of its cell a Block Elements character fills, read off its name.

A Unicode name states the shape outright, like LOWER THREE EIGHTHS BLOCK or QUADRANT UPPER LEFT AND LOWER RIGHT, which makes it a second source to hold BLOCK_ELEMENTS against.

Return type:

set[tuple[int, int]]

tests.test_screenshots.test_block_elements_cover_their_whole_unicode_block()[source]

Every Block Elements character has a shape, and is one cell wide.

tests.test_screenshots.test_block_element_fills_what_its_name_states(char)[source]

Each Block Elements character fills the part of its cell its name states.

tests.test_screenshots.test_shape_runs_cut_shapes_from_the_text_between(text, column, expected)[source]

Shapes are cut out on their own columns, and nothing else.

Box Drawing, Block Elements and Braille patterns are drawn as shapes. One following a zero width joiner belongs to the cluster before it, and stays in its text.

tests.test_screenshots.test_shape_runs_place_each_piece_where_cell_width_counts_it(text)[source]

Each piece starts on the column cell_width counts up to it.

cell_width counts the character after a zero width joiner in the cluster before it. A shape cut out of that cluster would push the text after it a cell past its column, onto the run the grid places next.

tests.test_screenshots.test_braille_dots_match_their_unicode_names(code)[source]

Each Braille pattern raises the dots its Unicode name lists.

tests.test_screenshots.test_braille_dots_keep_one_pitch_across_cells()[source]

Two stacked rows of full Braille cells plot one even grid of dots.

The dot positions are rounded to a tenth of a pixel, the precision every coordinate of a capture is written at.

tests.test_screenshots.named_arms(char)[source]

The arms of a Box Drawing line, read off its Unicode name.

A name states one weight for all its arms, as in LIGHT DOWN AND RIGHT, or one per group of arms, as in DOWN LIGHT AND RIGHT HEAVY.

Return type:

tuple[int, ...]

tests.test_screenshots.test_box_drawing_covers_its_whole_unicode_block()[source]

Every Box Drawing character is a line, a dash or a curve, one cell wide.

tests.test_screenshots.test_box_arms_match_their_unicode_names(char)[source]

Each Box Drawing line has the arms and weights its Unicode name lists.

tests.test_screenshots.test_box_dashes_match_their_unicode_names(char)[source]

Each dashed line runs the way, weighs and counts what its name says.

tests.test_screenshots.test_box_strokes_join_as_menlo_draws_them(char, expected)[source]

A line’s strokes meet where Menlo 22.0d2e1 joins them.

tests.test_screenshots.test_double_cross_leaves_its_gaps_open()[source]

No stroke of ╬ crosses into the gaps between its double lines.

tests.test_screenshots.test_box_rule_is_one_shape_and_its_lines_meet_across_rows()[source]

A rule merges across its cells, and a vertical line runs on into the next row.

tests.test_screenshots.test_blank_braille_draws_nothing_and_keeps_its_text()[source]

The blank pattern raises no dot, and still reads back as itself.

tests.test_screenshots.test_stacked_blocks_leave_no_gap()[source]

Two rows of full blocks paint one unbroken column, whatever the font.

A font draws █ shorter than a capture’s row, so the same picture set as glyphs leaves a band of background between the two rows.

tests.test_screenshots.test_half_block_splits_its_cell_in_the_middle()[source]

▀ inks the top half of its cell and leaves the bottom to the background.

tests.test_screenshots.test_block_run_is_one_shape()[source]

A bar of █ is one rectangle, not one per cell.

tests.test_screenshots.test_shade_mixes_its_ink_with_the_paper()[source]

▒ is half ink, mixed into the color behind it.

tests.test_screenshots.test_shapes_stay_in_the_text()[source]

A run drawn as shapes keeps its characters, as text that paints nothing.

tests.test_screenshots.test_no_committed_capture_draws_shapes_as_glyphs()[source]

No committed capture draws Box Drawing, Block Elements or Braille as glyphs.

A capture written before the renderer did still shows a seam at the joins of its box drawing, a band between its stacked rows of block art, or Braille wider than its column. Rewrite it the way its page documents: a recording, by deleting it and building again.

tests.test_screenshots.test_style_rules_writes_one_declaration_per_property(sgr, expected)[source]

Every decoration reaches the CSS, and the three share one declaration.

CSS keeps the last of a repeated property, so a run written as two text-decoration declarations loses all but one of them. overline had no branch at all, which left the column of click-extra styles naming it identical to the plain one in every viewer.

tests.test_screenshots.test_render_folds_an_unusable_unique_id()[source]

A file name that is not a CSS identifier cannot leak into a class name.

tests.test_screenshots.test_render_places_every_run_on_its_own_column()[source]

Rebuilding the terminal text from a capture gives the text back.

Each run is placed by its x offset alone, so laying the runs back on the columns that offset names has to reproduce what the terminal printed.

tests.test_screenshots.test_no_run_starts_on_padding()[source]

No visible run in a capture is positioned by the spaces preceding it.

The invariant the column arithmetic exists for: a renderer that ignores textLength or cannot resolve the font still lands every run where it belongs. Checked against every committed capture as well as a fresh render, so a regression cannot hide in the assets or in the renderer alone.

tests.test_screenshots.test_capture_clip_holds_every_descender()[source]

The terminal’s clip is never shorter than the text it contains.

A clip cropped to the text block’s height minus a pixel cuts the descenders off the last line, turning an underscore into a blank. Checked against every committed capture, which is where such a crop would go unnoticed.

tests.test_screenshots.test_no_capture_references_a_missing_clip()[source]

Every clip-path a capture uses resolves to a clipPath it defines.

A dangling reference is rendered inconsistently: a browser drops the clip, a strict SVG 1.1 renderer drops the whole element with it.

tests.test_screenshots.ANIMATION_PRESETS = ('aesthetic', 'arc', 'arrow', 'arrow2', 'arrow3', 'balloon', 'balloon2', 'beta-wave', 'binary', 'blue-pulse', 'bounce', 'bouncing-ball', 'bouncing-bar', 'box-bounce', 'box-bounce2', 'christmas', 'circle', 'circle-halves', 'circle-quarters', 'clock', 'dots', 'dots-8bit', 'dots-circle', 'dots10', 'dots11', 'dots12', 'dots13', 'dots14', 'dots2', 'dots3', 'dots4', 'dots5', 'dots6', 'dots7', 'dots8', 'dots9', 'dqpb', 'dwarf-fortress', 'earth', 'finger-dance', 'fish', 'fist-bump', 'flip', 'grenade', 'grow-horizontal', 'grow-vertical', 'hamburger', 'hearts', 'layer', 'line', 'line2', 'material', 'mindblown', 'monkey', 'moon', 'noise', 'orange-blue-pulse', 'orange-pulse', 'pipe', 'point', 'pong', 'rolling-line', 'runner', 'sand', 'shark', 'simple-dots', 'simple-dots-scrolling', 'smiley', 'soccer-header', 'speaker', 'square-corners', 'squish', 'star', 'star2', 'toggle', 'toggle10', 'toggle11', 'toggle12', 'toggle13', 'toggle2', 'toggle3', 'toggle4', 'toggle5', 'toggle6', 'toggle7', 'toggle8', 'toggle9', 'triangle', 'weather')

Every bundled spinner: the population an animated capture is checked over.

tests.test_screenshots.animated_capture(name)[source]

Draw one bundled spinner as an animated capture.

Return type:

str

tests.test_screenshots.stylesheet_of(svg)[source]

The <style> block of a rendered capture.

Return type:

str

tests.test_screenshots.claimed_names(svg)[source]

Every name a capture’s stylesheet claims in the scope of a whole page.

Class selectors and keyframe names both: inlined into one HTML document, the stylesheets of two captures are global and share that scope.

Return type:

set[str]

tests.test_screenshots.test_animated_capture_defines_every_class_it_uses(name)[source]

No frame names a class its capture’s stylesheet leaves undefined.

An undefined class does not blank its frame: the text falls back to the presentation attributes and draws in a default face in the window’s own ink. The animation then appears to reset its styling once a cycle, only the frames carrying a rule looking right.

tests.test_screenshots.test_animated_capture_draws_its_chrome_once(name)[source]

The window, its caption and its clip path are drawn once, not per frame.

tests.test_screenshots.test_animation_windows_tile_the_cycle(name)[source]

Each frame’s window abuts the next, covering the cycle exactly once.

A gap leaves the picture blank for part of every turn; an overlap draws two frames on top of each other.

tests.test_screenshots.test_animated_capture_draws_a_row_that_never_moves_once()[source]

A row identical in every frame is drawn outside them, not once per frame.

On a recording where one line moves under a screen of lines that do not, this is the difference between one copy of that screen and one per frame.

tests.test_screenshots.test_animated_capture_keeps_a_row_that_moves_in_every_frame()[source]

Nothing is shared when every row differs, which is the spinner’s case.

tests.test_screenshots.BAND_GROUP_RE = re.compile('<g clip-path="url\\(#[\\w-]+-window\\)">(.*?)</g>')

The group holding every band, clipped to the window rather than to the text.

tests.test_screenshots.bands_of(svg)[source]

The paint and baseline of each band an emphasized capture draws.

Return type:

list[tuple[str, float]]

tests.test_screenshots.test_emphasized_lines_are_banded_where_they_sit()[source]

A band lands on the row it names, counted from one, and spans the window.

Edge to edge rather than stopping where the padding does: the row is what is emphasized, not the column of text sitting in it.

tests.test_screenshots.test_emphasis_is_mixed_from_the_chrome_it_is_drawn_on()[source]

One ratio answers for both chromes: lighter on dark, darker on light.

tests.test_screenshots.test_emphasis_rejects_a_line_the_capture_does_not_have()[source]

Naming a line past the end is an authoring error, not a silent no-op.

tests.test_screenshots.test_screenshot_reports_a_line_the_capture_does_not_have(invoke, tmp_path)[source]

The command reports the authoring error as one, not as a traceback.

The height of a capture is only known once its command has run, so the renderer is the one to refuse a band past its end, and the command turns that refusal into its own error message.

tests.test_screenshots.FRAME_BAND_RE = re.compile('<g class="([\\w-]+)-f(\\d+)"[^>]*clip-path="url\\(#[\\w-]+-window\\)">(.*?)</g>')

A band group riding one frame of an animation.

tests.test_screenshots.banded_frames(svg)[source]

Which frames of an animated capture draw a band.

Return type:

set[int]

tests.test_screenshots.test_emphasis_arrives_with_the_row_it_marks()[source]

A band shows in the frame that first draws its row, and not before.

Waiting in empty space for the animation to reach it reads as a stray rectangle. Arriving with the row is also the moment a gutter would first number that row, so the two land together.

tests.test_screenshots.test_emphasis_leaves_a_closing_blank_alone()[source]

The empty beat closing a cycle draws no band, having drawn no row.

tests.test_screenshots.test_two_animations_share_no_selector()[source]

Two animated captures inlined in one page keep their own timing.

Both stylesheets are global there. A selector they share hands the shorter animation the longer one’s clock, and the frames it does not have go blank for that part of every cycle.

tests.test_screenshots.test_frame_animation_css_only_ever_animates()[source]

Every rule the timing emits sits behind the reduced-motion guard.

Nothing here hides a frame: that is the frames’ own visibility attribute, which is what a renderer reading no stylesheet still honors. A reader asking their system for less motion therefore lands on the same still as one whose viewer speaks no CSS animation at all.

tests.test_screenshots.test_animated_capture_hides_its_frames_outside_the_stylesheet(name)[source]

Every frame but the poster is hidden by an attribute, not by a rule.

A renderer free to ignore <style> would otherwise draw all of them at once, stacked on top of each other. The poster is the last frame, which on an animation that accumulates is the one that says the most.

tests.test_screenshots.test_animated_capture_renders_the_same_bytes_twice(name)[source]

An unchanged animation rewrites byte-identical bytes.

What lets a committed asset be regenerated on every build without dirtying the working tree.

tests.test_screenshots.test_animated_capture_needs_an_interval()[source]
tests.test_screenshots.test_animated_capture_counts_its_durations()[source]
tests.test_screenshots.test_animated_capture_rejects_a_frame_lasting_no_time()[source]
tests.test_screenshots.test_cursor_cell_reads_the_screen_it_is_given(picture, expected)[source]

A frame’s text already says where the terminal left its cursor.

tests.test_screenshots.test_cursor_cell_matches_the_screen_that_wrote_it()[source]

The derived column is the one the screen itself counted, not an estimate.

Locks the claim cursor_cell() rests on: a recorded frame carries its cursor position implicitly, so nothing has to travel beside the text to say where the cursor was.

tests.test_screenshots.cursor_rect(svg)[source]

The cursor a capture drew, as its x, y, width and height.

Found by its crispEdges rendering, which the text of these tests carries on nothing else: they draw unstyled words, so no run brings a background rectangle of its own. The count is asserted rather than assumed, so a test picture that grows a styled run fails here instead of measuring the wrong rectangle.

Return type:

tuple[float, float, float, float]

tests.test_screenshots.test_a_capture_draws_no_cursor_unless_asked()[source]

The default is no cursor, which is what every committed asset shows.

tests.test_screenshots.test_cursor_is_drawn_in_the_shape_it_names(shape, width, height)[source]

Each shape covers the part of its cell that shape is.

tests.test_screenshots.test_a_shapeless_cursor_takes_the_terminal_its_preset_names(name)[source]

--preset windows draws Windows Terminal’s bar without stating it.

tests.test_screenshots.test_a_cursor_under_the_output_grows_the_window_by_that_line()[source]

Output closing on a newline leaves the cursor on the row underneath.

tests.test_screenshots.test_a_cursor_does_not_move_what_emphasize_may_mark()[source]

A band picks out a line of output, and a cursor draws no line.

One rule and one keyframe set, however many frames carry a cursor.

A terminal has one cursor. Namespacing the blink per frame would instead let each copy light on its own beat, which reads as several cursors.

tests.test_screenshots.test_the_blank_closing_a_cycle_shows_no_cursor()[source]

An empty terminal has nothing to put a cursor on.

tests.test_screenshots.test_a_steady_cursor_names_no_class()[source]

A class the stylesheet never defines is what breaks a renderer.

tests.test_screenshots.test_a_blinking_cursor_sits_behind_the_reduced_motion_guard()[source]

Blinking is motion, and a reader may have asked for less of it.

Opacity multiplies into a hidden frame’s group; visibility would override it.

A frame is hidden by visibility and opacity together, and visibility is inherited: a cursor restoring it would show through every frame at once, which is the whole reason the blink dims opacity alone.

A cursor blinking in no time is a cursor that never lights.

tests.test_screenshots.test_a_capture_rejects_a_cursor_blinking_backwards()[source]

Seconds run forwards, here as everywhere else.

tests.test_screenshots.test_screenshot_cursor_options_need_their_partner(invoke, tmp_path, arguments, message)[source]

Each knob names the one it decorates, rather than passing unnoticed.

tests.test_screenshots.test_screenshot_draws_a_cursor_when_asked(invoke, tmp_path)[source]

--cursor puts one on a still capture, blinking on one clock.

tests.test_screenshots.test_screenshot_draws_no_cursor_unless_asked(invoke, tmp_path)[source]

The default is what every capture taken before the option showed.

tests.test_screenshots.test_screenshot_cursor_takes_the_shape_it_names(invoke, tmp_path, shape, width)[source]

A named shape overrides whatever the terminal preset would have drawn.

tests.test_screenshots.test_screenshot_bare_cursor_takes_the_preset_shape(invoke, tmp_path)[source]

--cursor alone draws Windows Terminal’s bar under --preset windows.

tests.test_screenshots.test_screenshot_record_types_its_prompt(invoke, tmp_path)[source]

--typing opens the recording on the command line being typed.

tests.test_screenshots.test_auto_columns_leaves_room_for_a_cursor()[source]

An auto width derives from the text, which always ends on its last cell.

Without the extra cell the cursor wraps onto a row of its own, which is a row holding nothing but a cursor.

tests.test_screenshots.test_auto_columns_with_a_cursor_on_a_blank_screen(picture)[source]

A blank screen shows no cursor, and leaves the width to its floor.

No frame placing a cursor left max() with the width alone to compare, which raised TypeError: 'int' object is not iterable for a command printing nothing under --prompt "".

tests.test_screenshots.test_a_closing_prompt_belongs_to_the_last_frame_alone()[source]

Closing every frame would say the command exited before it had.

Counted rather than located, the sigil landing on a different row in each frame of an animation that grows. A row drawn the same in every frame is drawn once, so an unclosed animation shows its opening sigil a single time and each closed frame adds one of its own: measured at 1, 2 and 5 here.

tests.test_screenshots.test_append_prompt_closes_a_finished_screen(text, expected)[source]

The shell comes back on the row under the output, and on no other.

tests.test_screenshots.test_append_prompt_takes_the_sigil_its_preset_draws()[source]

A capture of Windows Terminal closes on the shell that terminal runs.

tests.test_screenshots.test_a_closing_prompt_costs_no_height_beside_a_cursor()[source]

The cursor already claimed that row: the prompt only fills it.

What makes the pair worth having. A cursor alone leaves a row holding nothing, and the sigil turns it into the shell waiting.

tests.test_screenshots.test_screenshot_closes_a_still_with_a_prompt(invoke, tmp_path)[source]

--closing-prompt draws the shell coming back under the output.

tests.test_screenshots.test_auto_hold_scales_with_the_final_screen(text, expected)[source]

The pause follows the populated lines of the closing screen.

tests.test_screenshots.test_animated_capture_resolves_an_auto_hold()[source]

hold="auto" renders exactly as the resolved pause would.

tests.test_screenshots.test_animated_capture_rejects_an_unknown_hold_word()[source]

A word the annotation forbids still has to fail loudly at runtime.

tests.test_screenshots.test_animated_capture_rejects_a_negative_pause(pause)[source]

A pause runs forward or not at all, whichever end of the cycle it sits on.

tests.test_screenshots.test_parse_hold(value, expected)[source]

A hold is seconds, or the one word asking the final screen to decide.

tests.test_screenshots.test_parse_hold_rejects(value, message)[source]

A hold that is neither seconds nor auto says what it would take.

tests.test_screenshots.test_parse_pause(value, expected)[source]

A pause is seconds, zero included.

tests.test_screenshots.test_parse_pause_rejects_a_negative_one()[source]
tests.test_screenshots.test_html_capture_does_not_animate()[source]

An HTML capture is selectable text, which has no frame to hide.

The refusal names the format the way a reader writes it, not as the CaptureFormat.HTML member it is held in.

tests.test_screenshots.test_column_segments(text, expected)[source]

A run is cut at its gutters, and each column carries the one it starts on.

tests.test_screenshots.test_no_capture_places_a_column_by_its_padding()[source]

No column in a capture is positioned by the blanks in front of it.

The invariant that lets a capture survive a renderer ignoring textLength: librsvg (and through it rsvg-convert and ImageMagick) lays every run out at the font’s natural width, so a gutter paid for in glyphs collapses and the columns slide onto each other. Each column carrying its own x asks the renderer for nothing but coordinates.

tests.test_screenshots.test_no_capture_clips_through_a_transform()[source]

The element a capture’s clip is hung on carries no transform of its own.

A clipPath in userSpaceOnUse resolves against “the user coordinate system in place when it is referenced”, and renderers disagree over whether the referencing element’s own transform counts. Hung on a translated group, the two readings differ by the translation, and the one that ignores it crops the text partway down the window: what macOS Finder’s thumbnailer does.

tests.test_screenshots.test_no_capture_hangs_a_filter_on_its_window()[source]

The window’s own rectangle carries no filter.

An element whose filter a renderer cannot resolve is an element in error, which the spec answers by not rendering it at all. Hung on the window, a filter costs the background, the frame and the shadow together and leaves the text floating on the page: what macOS Finder’s thumbnailer and ImageMagick both do with a capture built that way.

tests.test_screenshots.test_capture_states_its_font_outside_the_stylesheet()[source]

The terminal text names its face, size and color as attributes too.

A renderer is free to ignore a <style> block, and several do. The text then falls back to a proportional face at a default size in default black, which is a terminal capture with neither its grid nor its colors.

tests.test_screenshots.test_every_capture_declares_its_encoding()[source]

A capture states UTF-8 outright rather than leaving it to be guessed.

A standalone SVG carries no HTTP header to say so. XML defaults to UTF-8 with no declaration, but WebKit applies its HTML fallback to the document encoding instead, which renders every multi-byte character as mojibake: a full block becomes â, and a capture of colored output becomes a wall of accented letters in macOS Quick Look.

tests.test_screenshots.test_no_capture_fetches_a_font()[source]

A capture resolves its fonts locally, or falls back down its own stack.

A committed image is read offline, on a page forbidding third-party requests, and by renderers that speak no @font-face.

tests.test_screenshots.test_wide_glyphs_are_sized_by_cell(text, characters, cells)[source]

A run is drawn as wide as the cells it occupies, not its character count.

A CJK ideograph takes two terminal cells, so sizing a run by len() squeezes it to half its width and stacks the glyphs on each other.

tests.test_screenshots.test_capture_output_forces_color_and_pins_width(monkeypatch)[source]

A capture overrides the environment’s color opt-out and terminal width.

tests.test_screenshots.test_capture_output_states_the_terminal_it_simulates(background, monkeypatch)[source]

The chrome reaches the command as the variables a terminal would set.

Which is what lets a CLI asking for --theme auto render for the window its capture lands in, instead of falling back to dark inside a light one.

tests.test_screenshots.test_capture_output_hides_the_terminal_it_runs_from(variable, monkeypatch)[source]

The terminal taking the capture never reaches the command captured.

Left through, a command tailoring its output to that terminal draws one picture on the machine that took the capture and another everywhere else, which no committed asset survives.

tests.test_screenshots.test_capture_output_keeps_stderr_out_unless_asked()[source]

Only stdout is captured by default, so a wrapper’s chatter stays out.

tests.test_screenshots.CAPTURE_CLICK_VERSION = (8, 5)

Click release whose help layout the committed captures picture.

Click 8.5.0 gave positional arguments a section of their own, reshaping every help screen it renders. A capture is a picture of one Click’s output, so the matrix cells pinning an older Click inside the supported range compare it against a screen that never carried that section. The Click released cells still check every committed capture, which is where the check belongs.

tests.test_screenshots.test_committed_capture_matches_cli(committed)[source]

Every committed capture still pictures what its command prints today.

The capture is reshot through capture(), the very pipeline that produced the committed file, and the two are compared as terminal text. Going through the whole pipeline rather than against the command’s raw output is what keeps the check honest on both counts the two differ:

  • a command wrapping to an 80-column terminal is handed min(columns, max_width) - 2, while Click’s own CliRunner pins FORCED_WIDTH to 80, so an in-process render sits two columns wider;

  • the renderer folds a line longer than the terminal, exactly as a terminal does, so a help screen carrying an unwrappable option list (--table-format and its 463-character choice list) occupies more rows in the image than the command printed.

Only the text is compared, so restyling a theme does not redden this.

tests.test_screenshots.test_capture_format_from_path(filename, expected)[source]

A capture’s format is read off the file name it is written under.

tests.test_screenshots.test_capture_format_from_path_rejects_an_unknown_extension(filename)[source]

An extension naming no format says which ones do.

tests.test_screenshots.test_capture_format_from_path_reads_a_file_named_dash_as_a_file(tmp_path)[source]

Only a bare - is the terminal: a file called - has an extension to lack.

--output ./- reaches here resolved to an absolute path, and the file it names is as nameable as any other.

tests.test_screenshots.test_deliver_capture_writes_a_file_named_dash(tmp_path, capsys)[source]

A resolved - is a file to write, not the terminal to print on.

tests.test_screenshots.test_screenshot_does_not_print_a_resolved_dash(invoke)[source]

--output ./- is a file in the working directory, which names no format.

Click resolves it to an absolute path whose last component is -, which is not the bare - naming the terminal.

tests.test_screenshots.test_render_draws_on_the_chrome_it_is_given(format, background, expected, unwanted)[source]

Both renderers answer to the chrome, so a light capture stays readable.

The colors are the contract: an SVG paints its window rect with them, and an HTML capture inlines them on its <pre>.

tests.test_screenshots.test_render_frames_with_a_border_its_chrome_can_show(background)[source]

Each chrome frames its window in something its own background shows.

The regression this guards is a light capture keeping the translucent white a dark renderer draws by default: a white window on a white page, whose shape is left for the reader to infer from the text inside it.

tests.test_screenshots.test_chrome_resolves_what_the_capture_draws(background)[source]

None fields take the background’s and the preset’s own; stated ones stay.

tests.test_screenshots.test_capture_functions_take_one_chrome(function)[source]

The window is one chrome argument everywhere, with one default.

tests.test_screenshots.test_renderers_take_one_resolved_chrome(renderer)[source]

The two renderers take the window as one chrome, bare unless stated.

A renderer draws the values it is given, so it refuses a chrome still carrying a None that only resolve() can fill.

tests.test_screenshots.test_bare_chrome_is_resolved(background)[source]

The renderers’ default draws as it stands: resolving it changes nothing.

tests.test_screenshots.test_render_rejects_an_unknown_argument()[source]

A typo is still refused, as a signature naming every argument would.

tests.test_screenshots.test_render_paints_what_it_is_given()[source]

A caller’s own frame and shadow reach the window.

tests.test_screenshots.test_render_states_the_whole_window()[source]

Corner radius, frame thickness, backdrop and title all reach the source.

tests.test_screenshots.test_gradient_svg(backdrop, expected)[source]

A CSS gradient becomes a paint server; anything else stays a color.

tests.test_screenshots.test_gradient_svg_places_the_line_in_user_space(value, expected)[source]

The gradient line runs through the center, at the angle asked for.

tests.test_screenshots.test_render_paints_a_gradient_backdrop()[source]

A gradient backdrop reaches the capture as a declared paint server.

tests.test_screenshots.test_number_lines(text, expected)[source]

Each line gets its number, right-aligned on the widest one.

tests.test_screenshots.test_preset_catalog(name)[source]

Every preset carries a full palette on both of its dresses.

tests.test_screenshots.test_palette_color(color, expected)[source]

A style’s color resolves against the palette only where it names a slot.

tests.test_screenshots.test_palette_color_rejects_a_value_naming_no_color(color)[source]

A value that is not a color cannot silently paint something.

A bool is an int, and names no palette slot for all that.

tests.test_screenshots.test_blend(ratio, expected)[source]

Dim text is mixed toward the background rather than made transparent.

tests.test_screenshots.test_grid_wraps_at_the_terminal_edge()[source]

A line reaching past the last column continues on the next row.

Cropping it would drop text the command printed, and a capture is evidence of what ran.

tests.test_screenshots.test_grid_marks_each_fold_with_its_wrap_marker()[source]

A marked fold closes its row on the marker, painted as a mark of the layout.

The marker takes the last cell of the row it closes, so each folded row holds one cell less of text, and the last row of the line carries none: nothing continues past it.

tests.test_screenshots.test_grid_without_a_wrap_marker_folds_a_full_row_silently()[source]

No marker means the terminal’s own fold: a line exactly as wide fits.

tests.test_screenshots.test_grid_never_splits_a_wide_glyph()[source]

A wide glyph straddling the last column moves down whole.

tests.test_screenshots.test_grid_keeps_a_glyph_wider_than_the_grid()[source]

A glyph too wide for the whole grid is drawn rather than wrapped forever.

tests.test_screenshots.test_window_buttons(name, expected, unwanted)[source]

Each preset draws the decorations its desktop does, or none at all.

tests.test_screenshots.test_render_draws_the_preset_it_is_given()[source]

A preset reaches the capture: its palette, its font, its decorations.

tests.test_screenshots.test_preset_paints_its_own_titlebar(name)[source]

A window drawn as a desktop’s carries that desktop’s chrome on top.

tests.test_screenshots.test_titlebar_collapses_on_a_window_wearing_nothing()[source]

An empty strip is dropped, and comes back as soon as it holds something.

tests.test_screenshots.test_render_thins_the_window_out(format, expected)[source]

Opacity below one lets whatever the capture sits on through its body.

tests.test_screenshots.DRAWN_FORMATS = (CaptureFormat.HTML, CaptureFormat.SVG)

The formats that draw a document around the captured text.

ANSI is the one that does not: it hands a terminal the stream it was already carrying, so there is no margin to credit in and nothing a window option could describe. Everything below asking about a window therefore asks it of these two.

tests.test_screenshots.test_render_credits_what_drew_it(format)[source]

Every capture carries the mark, in the space around the terminal.

The credit points a reader holding only the image back at the docs.

tests.test_screenshots.test_watermark_leaves_a_borrowed_credit_unlinked(format)[source]

A project crediting itself has no click-extra to point anywhere.

tests.test_screenshots.test_watermark_is_not_terminal_text()[source]

The mark is chrome, so reading a capture back does not collect it.

tests.test_screenshots.test_watermark_sits_where_the_margin_is()[source]

It is drawn against the image’s own corner, wherever the margin puts it.

tests.test_screenshots.test_render_draws_neither_when_asked_for_neither()[source]

none is the value that leaves the window bare.

tests.test_screenshots.test_render_geometry(margin, padding)[source]

Margin grows the image alone; padding grows the window inside it too.

Neither moves a glyph relative to the others: the capture is repositioned as a whole, so its text still reads back line by line.

tests.test_screenshots.test_render_html_escapes_the_captured_text()[source]

Markup a CLI prints is escaped, not handed to the browser as markup.

The load-bearing test of the HTML renderer: ansi_to_html() translates ANSI and copies everything else verbatim, so a caller skipping the escape turns click-extra’s own --export-config help, which says it writes to <stdout>, into an unclosed tag.

tests.test_screenshots.test_render_html_round_trips_the_terminal_text()[source]

Stripping the markup back off returns exactly what the CLI printed.

The HTML counterpart of svg_to_lines(): it catches a dropped character, a mangled escape and a stray tag in one assertion.

tests.test_screenshots.test_render_html_fragment_is_self_contained()[source]

A fragment carries its own styling, and no document scaffolding.

tests.test_screenshots.test_render_html_document_wraps_the_fragment()[source]

A full document is the fragment plus scaffolding, nothing else.

tests.test_screenshots.test_render_html_quotes_survive_the_style_attribute()[source]

The font stack cannot terminate the style attribute it sits in.

A double-quoted family name would close the attribute early and spill CSS into the markup, so the stack is single-quoted.

tests.test_screenshots.css_rects(declarations)[source]

The rectangles a block_css() box paints.

Read back off its background layers, in eighths of a cell, the unit of BLOCK_ELEMENTS.

Return type:

set[tuple[int, int, int, int]]

tests.test_screenshots.test_block_css_paints_the_shape_of_its_character(char)[source]

The CSS of a block element paints the rectangles its shape names.

tests.test_screenshots.test_render_html_draws_each_box_run_as_one_svg()[source]

A run of Box Drawing is one box holding the run’s lines as an SVG.

One SVG per run rather than a background per cell, so a rule merges into one line, and the characters stay in the text.

tests.test_screenshots.test_braille_css_draws_one_disc_per_dot(code)[source]

The CSS of a Braille pattern paints a disc for each dot it raises.

tests.test_screenshots.test_render_html_draws_shapes()[source]

Every shape sits in a box drawing it, whatever its run’s style.

Its glyphs stay in the text, so the capture still reads back as the terminal text, and the run keeps the colors it came with.

tests.test_screenshots.test_render_html_keeps_a_joined_shape_in_its_cluster()[source]

A shape after a zero width joiner stays in the text, as in an SVG capture.

tests.test_screenshots.test_fit_columns(text, expected)[source]

The width auto resolves to is the longest line, floored by the caller.

tests.test_screenshots.test_render_auto_columns_folds_nothing()[source]

auto lays the image out at what the text asks for.

A line the command does not wrap on its own (a prompt, a wide table) is folded by a pinned width and kept whole by this one.

tests.test_screenshots.test_screenshot_rejects_an_unusable_width(invoke, tmp_path, value, message)[source]

--columns takes a width or the one word standing for none of them.

tests.test_screenshots.test_parse_columns(value, expected)[source]

A width is an integer, or the one word leaving it to the text.

tests.test_screenshots.test_parse_columns_rejects(value, message)[source]

A width that is neither says what it would take.

tests.test_screenshots.test_screenshot_rejects_an_unusable_hold(invoke, tmp_path, value, message)[source]

A bad --hold is reported against the option, like a bad --columns.

The prefix is what tells a reader which of a dozen options to fix.

tests.test_screenshots.test_capture_environment(columns, pinned)[source]

The command is told the chrome, denied the terminal, and given its width.

tests.test_screenshots.test_screenshot_pacing_needs_record(invoke, tmp_path, arguments, message)[source]

The pacing knobs describe a recording, so a still refuses them.

tests.test_screenshots.test_screenshot_record_rejects_still_only_arrangements(invoke, tmp_path, arguments, output_name, message)[source]

A recording is an animated SVG of whole screens at a pinned width.

tests.test_screenshots.test_screenshot_record_writes_an_animation(invoke, tmp_path)[source]

--record lands an animated SVG named after its output stem.

tests.test_screenshots.test_screenshot_reports_a_command_that_cannot_start(invoke, monkeypatch, tmp_path, mode, program)[source]

A command the system refuses to start is an error line, not a traceback.

Only the start of the line is pinned: the reason closing it is the system’s own, and it differs between platforms.

tests.test_screenshots.test_screenshot_reports_a_command_outliving_its_timeout(invoke, tmp_path)[source]

A command still running when --timeout ends is an error line.

The command is killed, and nothing is written.

tests.test_screenshots.test_screenshot_wrap_needs_the_console_script(invoke, monkeypatch, tmp_path)[source]

--wrap refuses to re-enter through anything but the installed command.

python -m click_extra resolves a target differently, so falling back to it would capture a different CLI than the composition it stands for. Better to say so than to quietly picture the wrong thing.

tests.test_screenshots.test_screenshot_wrap_matches_the_composition(invoke, tmp_path)[source]

--wrap is the nested composition, spelled as a flag.

Captured both ways, the terminal text has to match: the flag is a shortcut, not a second way of rendering.

tests.test_screenshots.test_ansi_capture_is_the_text_it_was_given()[source]

The one format that renders nothing: a terminal reads the stream itself.

tests.test_screenshots.test_ansi_capture_draws_no_window()[source]

None of the window options reach a format that has no window.

tests.test_screenshots.test_ansi_capture_bands_an_emphasized_row()[source]

A terminal has no behind, so the band is the row’s own background.

tests.test_screenshots.test_ansi_band_survives_a_reset_inside_the_row()[source]

Restated after every escape, or the band stops at the first keyword.

Pygments closes a colored run with a full reset, which clears the background along with the ink it was closing.

tests.test_screenshots.test_ansi_capture_does_not_animate()[source]

There is no frame to hide in a stream a terminal paints as it arrives.

tests.test_screenshots.test_render_dresses_an_html_capture_in_the_preset_font_alone()[source]

A preset’s font reaches an HTML capture, and its title bar stays the SVG’s.

The buttons and the strip they sit in are drawn by the SVG alone: an HTML capture reflows with the page embedding it, and has no window to seat them.

tests.test_snippet module

Tests for click_extra.capture.snippet.

A snippet rides the screenshot renderer, so this module tests the step that is new: the colors a Pygments style hands over, and the shape of the text it hands them on. What the renderer then does with that text is covered by test_screenshots.py, and is deliberately not re-tested here.

tests.test_snippet.SAMPLE = 'def ripen(fruit, days=3):\n    """Wait for the mango."""\n    return fruit\n'

A few lines of Python exercising keywords, strings and punctuation.

tests.test_snippet.WINDOW_RE = re.compile('<rect fill="(?P<paint>#[0-9a-f]{6})" stroke=')

The window’s own rectangle, whose fill is the terminal’s background.

tests.test_snippet.window_paint(svg)[source]

The background color a rendered snippet’s window is filled with.

Return type:

str

tests.test_snippet.test_snippet_wears_the_style_background(background)[source]

The window is painted the background the syntax style was designed for.

A style states the surface its colors were chosen against, and drawn on any other one it is a picture of that style in a window it was never meant for.

tests.test_snippet.test_snippet_style_beats_the_preset_chrome()[source]

A preset dresses the window; the style still paints its body.

The two answer different questions, so a snippet wearing a terminal’s decorations keeps the colors its code was highlighted with.

tests.test_snippet.test_every_style_paints_a_readable_window(style)[source]

Every style Pygments ships renders, and states a background and an ink.

A conformance sweep rather than a spot check: the palette is built from two attributes a style is free to leave unset, and the fallback for each has to hold across the whole catalog.

tests.test_snippet.test_style_without_text_color_keeps_the_chrome_ink()[source]

A style naming no color for plain text falls back to the chrome’s own.

Twenty-five of the styles Pygments ships are in this case, all of them light ones whose code is meant to be black.

tests.test_snippet.test_snippet_carries_the_style_colors_into_the_picture()[source]

The colors a style names reach the rendered document.

The whole premise of the module: Pygments writes true-color ANSI, which is the interchange format the renderer already reads.

tests.test_snippet.test_highlight_expands_tabs()[source]

A tab becomes spaces before the snippet is laid out.

It has no width on a character grid, so one reaching the renderer takes the whole line’s measurement down with it.

tests.test_snippet.test_snippet_draws_the_lines_it_was_given()[source]

The code comes back out of the picture, character for character.

tests.test_snippet.test_snippet_numbers_its_lines()[source]

A numbered snippet counts from one, in a gutter of its own.

tests.test_snippet.test_snippet_leaves_room_for_line_numbers_on_the_rule()[source]

A numbered snippet cut short rules the cut inside its text column.

The gutter takes cells in front of every row, the rule’s included, so a rule as wide as columns would fold on its last dots and put them alone on a row of their own.

tests.test_snippet.test_snippet_renders_the_same_bytes_twice()[source]

Nothing in a snippet is timed, sampled or ordered by chance.

Which is what lets a documentation build rewrite a committed asset and leave the working tree clean.

tests.test_snippet.test_resolve_lexer(language, filename, expected)[source]

A stated language wins over a file name, which wins over a guess.

tests.test_snippet.test_unknown_language_is_an_error()[source]

A misspelled language fails loudly rather than drawing plain text.

A typo swallowed here would surface as an uncolored picture nobody re-reads.

tests.test_snippet.test_unknown_style_is_an_error()[source]

A misspelled style names the ones that exist.

tests.test_snippet.test_style_defaults_to_the_chrome(background)[source]

Naming no style takes the one the chrome is drawn for.

tests.test_snippet.test_default_style_sits_close_to_the_capture_chrome(background)[source]

The default style’s background is a near match for a capture’s own.

The reason these two were picked: a snippet and a terminal capture sharing a page must not show a step between their windows.

tests.test_snippet.test_snippet_renders_html()[source]

The HTML format carries the style’s colors too.

tests.test_snippet.shared_option_names()[source]

Every option name capture_options() attaches.

Read off a bare function the decorator is applied to, rather than restated here: a list written out by hand is one more place for the two commands to drift apart, which is the thing this is here to catch.

Return type:

tuple[str, ...]

tests.test_snippet.options_of(cmd)[source]

The command’s options, keyed by the name they bind to.

Return type:

dict[str, Option]

tests.test_snippet.test_capture_commands_share_one_vocabulary()[source]

Both capture commands spell every shared option the same way.

The drift this guards against is silent and lands on the reader rather than the maintainer: an option learned on one command that means something else, or nothing at all, on the other.

--columns is the deliberate exception. Its whole purpose is to differ: a command wraps its own output to it, where a file was never wrapped.

tests.test_snippet.test_columns_defaults_differ_by_command()[source]

A snippet sizes itself to its longest line; a capture pins a width.

tests.test_snippet.test_snippet_command_writes_a_capture(invoke, tmp_path)[source]

The command colors a file and writes the picture where it was told.

tests.test_snippet.test_snippet_command_reads_stdin(invoke, tmp_path)[source]

A dash reads the source from stdin, where no file name identifies it.

tests.test_snippet.test_snippet_command_rejects_a_fragment_image(invoke, tmp_path)[source]

--fragment is an HTML notion: an SVG has no page to be pasted into.

tests.test_snippet.test_snippet_command_reports_an_unknown_language(invoke, tmp_path)[source]

A misspelled language is a clean CLI error, not a traceback.

tests.test_snippet.LATIN1_SOURCE = b"p\xe2te = 'bris\xe9e'\n"

A line of Python saved in Latin-1, which UTF-8 cannot decode.

tests.test_snippet.test_snippet_command_refuses_a_source_that_is_not_utf8(invoke, tmp_path, from_stdin)[source]

A source in another encoding is a usage error on SOURCE, not a traceback.

tests.test_snippet.test_snippet_command_leaves_the_output_alone_on_undecodable_stdin(tmp_path)[source]

Undecodable stdin is refused before the output file is opened.

In UTF-8 mode, sys.stdin reads undecodable bytes as surrogates, and those only fail once the capture is written, over the previous capture. The test runner’s stdin decodes strictly and never takes that path, so the command runs as its own process, its stdin set to surrogateescape on every Python version.

tests.test_snippet.test_snippet_prints_ansi_to_stdout(invoke, tmp_path)[source]

A dash target prints the escape sequences a terminal paints.

The one target that needs no rendering: what a terminal reads is the stream the capture was carried in all along.

tests.test_snippet.test_snippet_stdout_strips_color_when_piped(invoke, tmp_path)[source]

Piped, the escapes go and the code stays.

The reason this routes through echo rather than a bare write: a redirect that captured raw escape sequences would produce a file nobody can read.

tests.test_snippet.test_snippet_stdout_closes_on_one_newline(invoke, tmp_path)[source]

Exactly one, so the next prompt does not land on the last row.

tests.test_snippet.test_snippet_writes_an_ansi_file(invoke, tmp_path)[source]

The .ansi extension names the same format, written out.

tests.test_snippet.EPILOG_EXAMPLES = (['--output', 'basket.svg', 'basket.py'], ['--output', 'basket.html', '--syntax-style', 'dracula', 'basket.py'], ['--output', 'basket.svg', '--line-numbers', '--emphasize-lines', '12', 'basket.py'])

The argument lists the snippet help screen closes on, one per example.

tests.test_snippet.test_snippet_epilog_examples_do_what_they_say(invoke, tmp_path, arguments)[source]

Every example of the help screen runs, and a style it names paints the window.

The --theme of every Click Extra command is the help screen’s own theme, so an example spelling the syntax style that way ran without complaint and drew the default style instead of the one it named.

tests.test_sphinx_crossrefs module

Render tests for Sphinx cross-references in the built documentation.

Build the docs once and assert against the real HTML that in-page and cross-page anchors resolve to the sections they name, and that intersphinx references to Click resolve to the upstream site. This catches drift the moment a heading rename shifts a make_id slug, a page link goes stale, or the intersphinx_mapping URL breaks, none of which a mock-based test would notice.

The same build shows what the click_extra.sphinx extension changes in its output: each changelog release anchored on its version, no viewcode page that nothing links to, and no pickled page holding a node of another document.

The docs are built in a subprocess through uv run --group docs, and so are the pickled pages read back, so this module needs no Sphinx dependency of its own and runs in the plain test environment.

tests.test_sphinx_crossrefs.UV_RUN_DOCS = ('uv', 'run', '--group', 'docs')

Prefix of a command run in the docs environment.

tests.test_sphinx_crossrefs.CHANGELOG_RELEASE = re.compile('^## \\[`(?P<version>\\d+\\.\\d+\\.\\d+)(?:\\.\\w+)?` \\(', re.MULTILINE)

Heading of a release in changelog.md, the .devN suffix left out.

tests.test_sphinx_crossrefs.FOREIGN_NODES_SCRIPT = 'import json\nimport pickle\nimport sys\nfrom pathlib import Path\n\ndoctrees = Path(sys.argv[1])\nforeign = {}\nfor path in sorted(doctrees.rglob("*.doctree")):\n    doctree = pickle.loads(path.read_bytes())\n    count = sum(1 for node in doctree.findall() if node.document is not doctree)\n    if count:\n        foreign[path.relative_to(doctrees).as_posix()] = count\nprint(json.dumps(foreign))\n'

Count, for each pickled page, the nodes that belong to another document.

Runs in the docs environment, which holds every class the pages pickle.

tests.test_sphinx_crossrefs.built_docs(tmp_path_factory, captures_before_build)[source]

Build the HTML documentation once and return its output directory.

Builds into a throwaway directory (rather than docs/_build) so the run is hermetic and never clobbers a developer’s local build. Reads in parallel, as docs.yaml does: a serial read hides most of the viewcode pages a parallel one publishes.

Requests captures_before_build() so the snapshot is taken before the build whichever test triggers it: every test of this module shares the one build, and pytest-randomly decides which of them runs first.

Return type:

Path

tests.test_sphinx_crossrefs.captures_before_build()[source]

Snapshot every committed capture before a build gets a chance to rewrite one.

built_docs() requests it, so the bytes are read while the tree is still as committed, whichever test of the module builds first.

Return type:

dict[Path, bytes]

tests.test_sphinx_crossrefs.test_committed_captures_survive_a_build(captures_before_build, built_docs)[source]

A build leaves every committed capture byte-identical.

A click:run block carrying :screenshot: rewrites its image from the CLI’s live output on every build. So an image that no longer matches the code surfaces here, instead of sitting stale in a readme nobody re-checked.

The bytes compare raw: the @generated stamp and the default watermark name no release, so a version bump moves neither. Regenerating is still the fix for a real difference: build and commit it.

tests.test_sphinx_crossrefs.read_html(built_docs, filename)[source]

Read a built HTML page.

Return type:

str

tests.test_sphinx_crossrefs.test_install_page_internal_anchor(built_docs)[source]

The install page’s in-page binaries link resolves to its section.

tests.test_sphinx_crossrefs.test_changelog_releases_are_anchored_on_their_version(built_docs)[source]

Each release of the changelog has an anchor made from its version.

docutils reduces a release heading to nothing and numbers its section from a per-page counter, so #id40 names another release after each new one. click_extra_release_anchors in docs/conf.py gives each section an anchor like #v1-4-2 instead. See click_extra.sphinx.changelog.

Each published module page links back to an object it documents.

sphinx.ext.viewcode keeps a module it read even when no documented object links to it, and a parallel read publishes a page for each such module, reachable from the _modules index alone. See click_extra.sphinx.viewcode.

tests.test_sphinx_crossrefs.test_pickled_pages_hold_no_node_of_another_document(built_docs)[source]

Every node of each pickled page belongs to that page.

A node that keeps a throwaway document pickles that document’s settings with the page, build environment included. Counting such nodes catches a leak of any size, where a ceiling on file sizes lets through a leak that only some of the pages carry. See click_extra.sphinx.doctrees.

tests.test_sphinx_crossrefs.test_cli_reference_anchors(built_docs, anchor)[source]

The CLI page’s tree and config summary tables deep-link their sections.

One anchor per directive: click-extra-wrap comes from {click:tree}, test-suite-file from {click:config}. Their presence is also the canary for the directives themselves staying on the page.

Cross-page links point at anchors that exist on the target page.

Every case names a page, so splitting one moves the anchor and leaves the case behind. Repoint them in the same commit that splits the page: this module is skipped off Linux, so the break surfaces only in CI, and a local pytest -m once reports a clean pass while skipping all of it.

tests.test_sphinx_crossrefs.test_intersphinx_click_resolves(built_docs)[source]

Click cross-references resolve to the upstream documentation site.

tests.test_spinner module

tests.test_spinner.test_spinner_exported_from_root()[source]
tests.test_spinner.test_default_stream_is_stderr()[source]
tests.test_spinner.test_explicit_stream_is_honored()[source]
tests.test_spinner.test_can_draw(live, stream, expected)[source]
tests.test_spinner.test_noop_on_non_tty_stream()[source]

A non-interactive stream produces no output and spawns no thread.

tests.test_spinner.test_delay_suppresses_quick_calls()[source]

A call shorter than the delay never draws anything.

tests.test_spinner.test_draws_and_cleans_up_when_enabled()[source]
tests.test_spinner.test_shown_false_when_not_drawn()[source]

shown stays False whenever no frame reaches the terminal.

tests.test_spinner.test_shown_true_after_drawing()[source]

shown flips to True once a frame is drawn, and stays True after stop.

tests.test_spinner.test_label_can_change_mid_spin()[source]
tests.test_spinner.test_hide_cursor_disabled()[source]
tests.test_spinner.test_ascii_frames()[source]
tests.test_spinner.test_stop_is_idempotent_and_safe_before_start()[source]
tests.test_spinner.test_suspend_and_resume()[source]

A spinner restarts cleanly after a stop, without re-using a dead thread.

tests.test_spinner.test_rotation_direction(reverse)[source]

Frames cycle forwards by default and backwards when reverse=True.

tests.test_spinner.test_beep_rings_bell_on_stop_when_enabled()[source]
tests.test_spinner.test_beep_silent_when_disabled()[source]

A disabled spinner never beeps, even with beep=True.

tests.test_spinner.test_echo_prints_above_running_spinner()[source]
tests.test_spinner.test_echo_degrades_to_plain_write_when_disabled()[source]

Off a TTY the message is still emitted, just without control codes.

tests.test_spinner.test_progress_option_is_a_default_option()[source]

ProgressOption ships in the default option set of every extra command.

tests.test_spinner.test_progress_option_resolution(invoke, args, expected)[source]

ctx.meta[PROGRESS] follows –progress and –accessible, never color.

tests.test_spinner.test_standalone_progress_option(invoke, cmd_decorator, option_decorator)[source]
tests.test_spinner.test_auto_spinner_follows_progress_flag(invoke, args, draws)[source]

A live="auto" spinner stays still under --no-progress and --accessible, even on a terminal, with no wiring in the command.

tests.test_spinner.test_spinner_stop_leaves_a_stalled_drawing_thread_behind(monkeypatch)[source]

stop() gives up on a drawing thread stuck in a write after the grace, instead of hanging the Ctrl+C that asked for the stop.

tests.test_spinner.SPINNING_SCRIPT = '\nimport sys\nimport time\n\nfrom click_extra import Spinner\n\nSpinner("Brewing tea", stream=sys.stdout, live="always", interval=0.02).start()\ntime.sleep(30)\n'

A process spinning until something kills it.

tests.test_spinner.test_sigterm_shows_the_cursor_again(tmp_path)[source]

A spinner killed by SIGTERM leaves its terminal with the cursor shown, and the process still dies by SIGTERM.

tests.test_spinner.test_dumb_terminal_disables_spinner(monkeypatch, term)[source]

A cursor-less terminal self-disables the spinner even on a TTY.

tests.test_spinner.test_live_always_overrides_dumb_terminal(monkeypatch)[source]

An explicit live="always" wins over the TERM=dumb auto-detection.

tests.test_spinner.test_decorator_runs_function_inside_spinner()[source]

@spinner animates while the wrapped function runs and returns its result.

tests.test_spinner.test_bare_decorator_without_parentheses()[source]

@Spinner with no parentheses wraps the function with default settings.

tests.test_spinner.test_resolve_color_enabled(monkeypatch, env, stream_factory, expected)[source]

Color follows FORCE_COLOR / dumb TERM / NO_COLOR then TTY, with no context.

tests.test_spinner.test_color_applied_on_tty(monkeypatch)[source]
tests.test_spinner.test_color_stripped_but_spinner_still_spins_when_disabled(monkeypatch)[source]

NO_COLOR strips the spinner’s color but never stops it spinning.

tests.test_spinner.test_style_applied_to_spinner(monkeypatch)[source]
tests.test_spinner.test_invalid_style_raises()[source]
tests.test_spinner.test_style_alone_keeps_painting_the_line_as_one_run()[source]

Without a part style, style still wraps the whole line, byte for byte.

tests.test_spinner.test_part_styles_paint_their_own_part(options, glyph, label, timer)[source]

Each part carries its leading space and takes its own style over style.

The timer’s parentheses belong to its part, which is what a timer callable formatting only the duration cannot reach.

tests.test_spinner.test_color_off_strips_escapes_embedded_in_the_label(options)[source]

A label styled by hand renders plain when color is off, like any other.

tests.test_spinner.test_kept_line_paints_label_and_timer_with_part_styles(monkeypatch)[source]

ok() paints its label and timer with the part styles, when color is on.

tests.test_spinner.test_kept_line_strips_embedded_escapes_with_color_off()[source]

Off a TTY, ok() writes the label plain, embedded escapes included.

tests.test_spinner.test_invalid_part_style_raises(options)[source]
tests.test_spinner.test_frame_lines_are_what_the_animation_draws(monkeypatch)[source]

A picture of a spinner shows the lines the spinner really writes.

Spins for real, recovers every frame written to the stream, and asserts each one is a line frame_lines() offers. Composing the picture separately from the animation is what lets the two drift the first time the glyph, the label or the timer change places; comparing them is what stops that.

tests.test_spinner.test_frame_lines_honors_reverse()[source]

A spinner cycling backwards is pictured spinning the way it animates.

tests.test_spinner.test_frame_lines_colors_on_request(color)[source]

A capture asks for color whatever stream the spinner would have drawn on.

A spinner that never started resolved no color for itself, so the picture has to state its own answer rather than inherit that one.

tests.test_spinner.test_outcome_leaves_persistent_line(monkeypatch, outcome, glyph, color)[source]
tests.test_spinner.test_ok_degrades_to_plain_line_when_disabled(monkeypatch)[source]

Off a TTY the outcome is still recorded, without symbol color.

tests.test_spinner.test_timer_appended_to_frames_and_final_line()[source]
tests.test_spinner.test_timer_accepts_custom_formatter()[source]

A callable timer formats the elapsed seconds itself (yaspin #236).

tests.test_spinner.test_enable_windows_ansi_is_a_safe_noop()[source]

The Windows VT-enable never raises: off Windows, or on a stream with no usable console handle (the path the spinner exercises on every platform).

tests.test_spinner.test_elapsed_time_freezes_after_stop()[source]
tests.test_spinner.test_catalog_is_complete()[source]

The cli-spinners / ora catalog is present and well-formed.

tests.test_spinner.test_catalog_frames_share_one_width(name)[source]

Every frame of a preset occupies the same number of terminal cells.

A ragged preset moves its label a cell in and out as the animation turns, and a capture laid out on the frames reserves a column for the widest one.

tests.test_spinner.test_catalog_frames_carry_no_upstream_padding(name)[source]

No preset pads every one of its frames with a trailing space.

Upstream writes its emoji frames that way. Kept, the space lands next to the one the spinner writes before its label and reads as a double gap.

tests.test_spinner.test_spinner_preset_supplies_frames_and_interval()[source]
tests.test_spinner.test_explicit_frames_and_interval_override_preset()[source]
tests.test_spinner.test_defaults_without_frames_or_preset()[source]
tests.test_spinner.test_multichar_preset_renders()[source]

A multi-character animation (which upstream \b renderers drop) draws.

tests.test_spinner.test_demo_spinner_table_lists_selection(invoke)[source]

--table prints the catalog table; the tour stays TTY-only.

tests.test_spinner.test_demo_spinner_without_table_flag_shows_no_table(invoke)[source]

Off a TTY and without –table, the command renders no table.

tests.test_spinner.test_demo_spinner_tour_column_shows_three_cycle_time(invoke)[source]

The Tour column reports 3 × frames × interval (dots = 2.4s).

tests.test_spinner.test_demo_spinner_all_lists_full_catalog(invoke)[source]
tests.test_spinner.test_demo_spinner_select_filters_by_name(invoke)[source]
tests.test_spinner.test_demo_spinner_select_rejects_unknown(invoke)[source]
tests.test_spinner.test_demo_spinner_random_limits_count(invoke)[source]
tests.test_spinner.test_demo_spinner_options_are_mutually_exclusive(invoke)[source]
tests.test_spinner.test_tour_duration_bounds_dwell()[source]

The tour dwell aims for three cycles, clamped to [_TOUR_MIN, _TOUR_CAP], and never trims a huge spinner below one full cycle.

tests.test_spinner.test_active_spinner_registry_lifecycle()[source]

A started spinner advertises itself, keyed by stream, until stopped.

tests.test_spinner.test_active_spinner_ignores_disabled_spinner()[source]

A disabled spinner never animates, so it never registers either.

tests.test_spinner.test_trail_glyph(args, kwargs, expected)[source]

The glyph takes a symbol and a style in place of the theme’s.

tests.test_spinner.test_color_resolution_reaches_worker_threads(monkeypatch)[source]

A spinner started on a worker thread sees the invocation’s --no-color.

The thread-local command context is absent there, so the published invocation color is what carries the decision, and it outranks the environment it was reconciled from.

tests.test_spinner.test_resolve_timer_follows_time_flag()[source]

timer=None follows the –time flag; explicit settings pass through.

tests.test_spinner.test_spinner_rejects_invalid_live()[source]

live must be one of WHEN.

tests.test_spinner.test_spinner_timer_follows_time_flag()[source]

A spinner’s default timer follows –time, as a trail’s does.

tests.test_spinner.CUSTOM_CSS = PosixPath('/home/runner/work/click-extra/click-extra/docs/_static/custom.css')

Stylesheet carrying the documentation’s unicode-range declaration.

tests.test_spinner.GRIDLESS_BLOCKS = ((9472, 9599), (9600, 9631), (9632, 9727), (10240, 10495))

Ranges no mainstream monospace font carries a glyph for.

A browser substitutes one that knows nothing of the character grid, so these come out narrow or wide and a captured table’s columns slide apart along the row. docs/_static/custom.css binds a subset font to exactly these.

tests.test_spinner.declared_unicode_ranges()[source]

Read the codepoint spans the documentation’s stylesheet claims.

Return type:

list[tuple[int, int]]

tests.test_spinner.test_catalog_gridless_glyphs_are_covered_by_the_docs_font(invoke)[source]

Every glyph a browser cannot size is one the documentation ships a font for.

The catalog table is laid out on a character grid, and a browser only reproduces that if every glyph advances by one cell. custom.css binds a subset font to the ranges no ordinary monospace font carries; this checks the two have not drifted apart, which they would the moment the declaration is narrowed or a spinner starts drawing from a range it omits.

Note

Narrower than the problem it guards: it checks the ranges already known to need a font, not that a newly used range has been noticed. A spinner drawn from some other gridless block would pass here and still misalign.

tests.test_spinner.SPINNER_ASSETS = PosixPath('/home/runner/work/click-extra/click-extra/docs/assets')

Directory the inventory gallery writes one animation per preset into.

tests.test_spinner.test_every_spinner_has_a_committed_animation()[source]

The inventory gallery covers the catalog exactly, one asset per preset.

The gallery is generated from SPINNERS at build time, so a new preset grows an asset on the next build and this only fails on a committed tree that has fallen behind. The stray half matters more: a renamed or dropped preset leaves its old animation behind, where nothing references it and nothing would notice.

tests.test_styling module

Tests for click_extra.styling.Style extras.

tests.test_styling.test_hex_fg_converts_to_rgb_tuple(hex_str, expected_rgb)[source]
tests.test_styling.test_hex_bg_converts_to_rgb_tuple()[source]
tests.test_styling.test_hex_invalid_raises(hex_str)[source]
tests.test_styling.test_named_color_string_passes_through()[source]

Plain named-color strings must not be touched by the hex shorthand.

tests.test_styling.test_or_right_operand_wins_on_conflicts()[source]
tests.test_styling.test_or_returns_subclass_instance()[source]
tests.test_styling.test_or_with_cloup_style_promotes_to_subclass()[source]
tests.test_styling.test_ror_with_cloup_left_operand()[source]

cloup_style | my_style is reached via __ror__.

tests.test_styling.test_or_with_non_style_returns_notimplemented()[source]

Style | int falls through to int.__ror__ and raises TypeError.

tests.test_styling.test_cascade_fills_unset_fields_from_base()[source]
tests.test_styling.test_cascade_keeps_subclass_identity()[source]
tests.test_styling.test_cascade_with_non_style_raises()[source]
tests.test_styling.test_to_dict_omits_unset_fields()[source]
tests.test_styling.test_to_dict_serializes_rgb_as_hex()[source]
tests.test_styling.test_to_dict_omits_none_only()[source]

False boolean attributes are kept; only None is filtered out.

tests.test_styling.test_from_dict_round_trip()[source]
tests.test_styling.test_from_dict_accepts_hex_or_rgb()[source]

Both hex strings and RGB tuples work as input.

tests.test_styling.test_str_returns_styled_sample()[source]
tests.test_styling.test_str_no_styling_has_no_color_codes()[source]

Style with no fields set produces only click’s bare reset suffix.

tests.test_styling.test_repr_compact_named_color()[source]
tests.test_styling.test_repr_compact_rgb_to_hex()[source]
tests.test_styling.test_repr_palette_index_zero()[source]

Index 0 is falsy but set: it must survive the is not None guards.

tests.test_styling.test_repr_empty_style()[source]
tests.test_styling.test_repr_multiple_attrs()[source]
tests.test_styling.test_to_css_basic()[source]
tests.test_styling.test_to_css_named_color_passes_through()[source]
tests.test_styling.test_to_css_palette_index_zero()[source]

Palette index 0 is falsy but set, and resolves to ANSI black.

tests.test_styling.test_to_css_bright_named_color_resolves_to_rgb()[source]

Bright ANSI colors aren’t valid CSS keywords: convert to RGB.

tests.test_styling.test_to_css_text_decorations_combine()[source]
tests.test_styling.test_to_css_dim_emits_opacity()[source]
tests.test_styling.test_to_css_empty_style_returns_empty_string()[source]
tests.test_styling.test_from_ansi_parses_codes(escape, expected)[source]
tests.test_styling.test_from_ansi_full_styled_string_round_trip()[source]

Parsing a style’s complete output (trailing reset included) recovers it.

tests.test_styling.test_from_ansi_round_trip_through_call()[source]

A Style can be parsed back from its own ANSI output.

tests.test_styling.test_from_ansi_invalid_raises()[source]
tests.test_styling.test_contrast_ratio_white_on_black_is_max()[source]
tests.test_styling.test_contrast_ratio_identical_colors_is_one()[source]
tests.test_styling.test_contrast_ratio_is_symmetric()[source]
tests.test_styling.test_contrast_ratio_meets_wcag_aa_for_dracula_default()[source]

Dracula’s default fg/bg pair clears WCAG AA (4.5).

tests.test_styling.test_contrast_ratio_requires_both_fgs()[source]
tests.test_styling.test_eq_survives_a_call()[source]

Calling a style leaves its equality and hash untouched.

tests.test_styling.test_eq_with_cloup_style(style, twin)[source]

A click-extra Style equals the cloup.Style carrying its fields, both ways.

tests.test_styling.test_eq_with_non_style()[source]

Comparing with anything else defers to the other operand.

tests.test_styling.test_supports_truecolor(monkeypatch, colorterm, term, expected)[source]
tests.test_styling.test_style_call_keeps_24bit_on_truecolor(monkeypatch)[source]

An RGB color emits a 24-bit sequence when the terminal supports truecolor.

tests.test_styling.test_style_call_quantizes_without_truecolor(monkeypatch)[source]

An RGB color downsamples to the nearest 256-index without truecolor.

tests.test_styling.test_style_call_leaves_named_and_indexed_colors(monkeypatch, colorterm)[source]

Named and palette-index colors never quantize, regardless of depth.

tests.test_styling.test_style_call_survives_depth_flip(monkeypatch)[source]

The same style renders correctly when truecolor flips between calls.

Quantizing works on a transient copy, so the shared (often singleton) theme style keeps its 24-bit colors for the next call.

tests.test_styling.test_style_call_palette_index_zero(style, expected)[source]

Rendering palette index 0 must emit its escape code, not drop it.

tests.test_styling.test_style_call_empty_string_color_rejected(style)[source]

Click validates colors at render time and rejects empty strings.

Click 8.5 raises TypeError, its development branch ValueError.

tests.test_styling.test_int_enum_color_round_trips_by_value()[source]

A palette index held by an IntEnum serializes as the index it is.

tests.test_styling.test_split_ansi(text, expected)[source]
tests.test_styling.test_split_ansi_empty_string()[source]
tests.test_styling.test_split_ansi_preserves_text()[source]

Concatenated run texts equal the ANSI-stripped input.

tests.test_styling.test_render_ansi_passthrough_unstyled()[source]
tests.test_styling.test_render_ansi_wraps_styled_runs()[source]
tests.test_styling.test_render_ansi_splits_runs_at_newlines()[source]

No markup wrapper ever crosses a line boundary.

tests.test_styling.test_wrap_ansi_matches_textwrap_on_plain_text()[source]

Unstyled input defers entirely to textwrap.

tests.test_styling.test_wrap_ansi_breaks_on_visible_width()[source]

A styled string breaks exactly where its plain counterpart does.

textwrap alone counts the escape bytes toward the line length, which would break a styled string several words early.

tests.test_styling.test_wrap_ansi_closes_styling_on_every_line()[source]

No escape sequence crosses a line boundary.

tests.test_styling.test_wrap_ansi_keeps_styling_on_its_own_words()[source]

Each run keeps its style once the text is split across lines.

tests.test_styling.test_wrap_ansi_edge_cases(text, width, expected)[source]
tests.test_styling.test_ansi_converters(converter, text, expected)[source]

tests.test_table module

tests.test_table.test_table_formats_definition()[source]

Check all table formats are accounted for and properly named.

tests.test_table.test_unrecognized_format(invoke, cmd_decorator, cmd_type)[source]
tests.test_table.test_corner_header()[source]
tests.test_table.test_all_table_formats_have_test_rendering()[source]

Check all table formats have a rendering test fixture defined.

tests.test_table.test_all_table_rendering(invoke, cmd_decorator, option_decorator, format_name, expected)[source]
tests.test_table.test_emoji_presentation_width_follows_the_terminal(monkeypatch, term_program, expected)[source]

An emoji-presentation sequence measures as its terminal advances it.

tests.test_table.test_emoji_presentation_gains_the_column_it_paints_into(monkeypatch)[source]

A terminal painting the glyph wider than it advances gets a column for it.

tests.test_table.test_emoji_presentation_padding_stays_out_of_markup(monkeypatch)[source]

A markup rendering outlives this terminal, so its cells keep their text.

tests.test_table.test_markup_pads_emoji_presentation_by_unicode_width(monkeypatch)[source]

A markup table pads an emoji-presentation sequence by the Unicode tables.

Whatever terminal renders it: a Markdown table is read by a formatter and a browser that never see that terminal, and mdformat pads it with wcswidth.

tests.test_table.test_print_table_pads_markup_by_unicode_width(monkeypatch, capsys)[source]

print_table measures a markup table like render_table does.

A narrow-emoji terminal must not leak into a Markdown table that a subcommand prints with ctx.print_table.

tests.test_table.test_table_rows_line_up_on_every_terminal_wcwidth_knows(monkeypatch, term_program)[source]

Rows advance the same width on each terminal, not just the default one.

Measured with wcwidth’s own per-terminal tables rather than with the measure the layout used, so a terminal whose correction Click Extra misses shows up as a ragged table instead of agreeing with itself.

tests.test_table.test_table_rows_are_uniform_under_a_narrow_emoji_terminal(monkeypatch)[source]

Every row of a rendered table advances the same width in that terminal.

Measured with the terminal’s own rule, which is the one deciding whether the table’s vertical rules line up on screen.

tests.test_table.test_markup_strips_ansi_by_default(invoke, format_id)[source]

Markup formats without native styling strip ANSI codes by default.

tests.test_table.test_markup_preserves_ansi_with_color_flag(invoke, format_id)[source]

--color overrides ANSI stripping for non-styled markup formats.

tests.test_table.test_color_flag_from_parent_group_preserves_ansi(invoke)[source]

A --color forced on a parent group reaches the subcommand’s table.

Mirrors the layout of the click-extra demo CLI: the color and table options live on the group, while the table is printed from a subcommand whose context does not own those parameters.

tests.test_table.STYLED_MARKUP_SAMPLES = {TableFormat.HTML: '<span style="color: red">hello</span>', TableFormat.JIRA: '{color:red}hello{color}', TableFormat.LATEX: '\\textcolor{red}{hello}', TableFormat.LATEX_BOOKTABS: '\\textcolor{red}{hello}', TableFormat.LATEX_LONGTABLE: '\\textcolor{red}{hello}', TableFormat.LATEX_RAW: '\\textcolor{red}{hello}', TableFormat.MEDIAWIKI: '<span style="color: red">hello</span>', TableFormat.TEXTILE: '%{color: red}hello%', TableFormat.UNSAFEHTML: '<span style="color: red">hello</span>'}

Expected native styling of a red hello cell, per styled format.

tests.test_table.test_styled_formats_all_have_samples()[source]
tests.test_table.test_styled_formats_translate_ansi(invoke, format_id, styled_cell, flags)[source]

Styled formats translate ANSI codes to native markup, by default and under a forced --color alike.

tests.test_table.test_styled_formats_strip_ansi_with_no_color(invoke, format_id)[source]

Disabling colors renders styled formats plain, with no translation.

tests.test_table.test_serialize_json_compatible(table_format, data)[source]
tests.test_table.test_serialize_roundtrip(table_format, data, loader)[source]
tests.test_table.test_serialize_toml_list_wrapping()[source]

Top-level lists are wrapped under a record key for TOML.

tests.test_table.test_serialize_strips_none(table_format)[source]

TOML and XML have no null type. None values are omitted.

tests.test_table.test_serialize_xml()[source]
tests.test_table.test_serialize_xml_list_wrapping()[source]

Top-level lists are wrapped under a record key for XML.

tests.test_table.test_serialize_xml_custom_root_element()[source]
tests.test_table.test_serialize_xml_respells_invalid_names()[source]

Keys XML does not accept as element names still give a well-formed document.

tests.test_table.test_serialize_default_callback()[source]

Custom types are converted via the default callback.

tests.test_table.test_serialize_unsupported_format_raises()[source]
tests.test_table.test_strip_none(data, expected)[source]
tests.test_table.test_apply_default_native_types_unchanged()[source]
tests.test_table.test_apply_default_custom_type_converted(data, expected)[source]
tests.test_table.test_missing_dependency_clean_error(monkeypatch, func, args, kwargs, message)[source]

Missing optional dependency produces a clean error, no traceback.

tests.test_table.test_render_table_sort(data, headers, sort_key, expected_fruits)[source]
tests.test_table.test_render_table_header_edge_cases(headers, data, expected)[source]

Edge cases for header handling in structured format rendering.

tests.test_table.test_render_table_xml_element_names(headers, expected)[source]

An XML table parses back with one element per cell, whatever its headers.

An element name cannot start with a digit, so a cell position or a numeric header gets a leading underscore and keeps its digits. A None cell is left out without shifting the position of the cells after it.

tests.test_table.test_wrappable_formats_definition()[source]

Wrapping is claimed by text layouts only, never by a data interchange.

tests.test_table.test_max_column_widths_never_breaks_a_format(format_id)[source]

Every format accepts a width: it either wraps on it or drops it.

Locks the invariant across the whole enum, so a format added later cannot silently raise or corrupt its output when handed a width.

tests.test_table.test_max_column_widths_keeps_data_formats_intact(format_id)[source]

A width never leaks a line break into a serialized cell.

tests.test_table.test_max_column_widths_scalar_applies_to_every_column()[source]

A single value stands in for a per-column list.

tests.test_table.test_max_column_widths_shorter_than_the_table()[source]

A list shorter than the table leaves the trailing columns unlimited.

tests.test_table.test_column_spec_max_width_is_the_default_source()[source]

A width declared on a ColumnSpec applies without any argument.

tests.test_table.test_max_column_widths_overrides_column_spec()[source]

An explicit argument wins over the ColumnSpec declaration.

tests.test_table.test_column_spec_max_width_follows_a_projection()[source]

A ColumnSpec width stays on its column when others are dropped.

tests.test_table.test_auto_width_fits_the_terminal(monkeypatch)[source]

An auto column absorbs the width left by the others.

tests.test_table.test_auto_width_shares_the_remainder(monkeypatch)[source]

Several auto columns split what is left evenly.

tests.test_table.test_auto_width_floors_at_min_column_width(monkeypatch)[source]

A terminal too narrow to fit the table still renders a usable column.

tests.test_table.test_auto_width_honors_context_width(invoke)[source]

auto reads the width a help screen would use, not the raw terminal.

tests.test_table.test_vertical_wrapping_keeps_the_label_gutter()[source]

Continuation lines of a wrapped cell align under the first one.

tests.test_table.test_vertical_labels_align_on_their_visible_width(headers)[source]

Labels are padded to the columns they occupy, not to their length.

Escape codes take no column and a double-width character takes two, so the separators and the continuation lines of a wrapped cell stay in one column.

tests.test_table.test_column_sort_key(header_defs, rows, sort_columns, cell_key, expected_first_col)[source]
tests.test_table.test_sort_by_option_choices_and_default(header_defs, expected_choices, expected_default)[source]

SortByOption choices and default are derived from column definitions.

tests.test_table.test_sort_by_option_wires_context(invoke)[source]

SortByOption publishes the sort key that ctx.print_table applies.

tests.test_table.test_print_table_from_subcommand_context(invoke)[source]

A group-level –table-format reaches ctx.print_table in subcommands.

ctx.meta is shared along the context chain, so the print_table context method works from a subcommand without reaching for the root context.

tests.test_table.test_print_table_without_table_option(invoke)[source]

ctx.print_table works with no –table-format in the chain.

Falls back to the default rendering format, like the module-level print_table() invoked without an explicit format.

tests.test_table.test_render_table_context_method_honors_sort_by(invoke)[source]

ctx.render_table applies the –sort-by selection, like ctx.print_table.

tests.test_table.test_sort_by_option_multi_column(invoke)[source]

Multiple –sort-by options define sort priority.

tests.test_table.test_sort_by_option_decorator(invoke)[source]

The sort_by_option decorator wires sorting like a direct SortByOption.

tests.test_table.test_sort_by_option_decorator_in_option_group(invoke)[source]

The sort_by_option decorator composes with @option_group.

tests.test_table.test_sort_by_option_columns_registry(invoke)[source]

A ColumnSpec registry passed via columns= drives choices and ordering.

tests.test_table.test_sort_by_option_accepts_column_spec_varargs()[source]

ColumnSpec instances also work positionally, normalized to (label, id).

tests.test_table.test_sort_by_option_rejects_positional_and_columns()[source]

Column definitions cannot be passed both positionally and via columns=.

tests.test_table.test_columns_type_matches_ids_exactly()[source]

Column IDs are identifiers: a comma list, matched with their case.

tests.test_table.test_sort_by_and_columns_share_registry()[source]

The same ColumnSpec registry configures both –columns and –sort-by.

tests.test_table.test_column_sort_key_field_mapping(header_defs, sort_columns, rows, expected_first_col)[source]

The public key builder maps requested fields onto the carried columns.

tests.test_table.test_column_sort_key_none_when_not_carried(sort_columns)[source]

No requested field carried by the table: rows must keep their order.

tests.test_table.test_render_table_rich_headers_render_labels()[source]

ColumnSpec and plain label header entries render their labels.

tests.test_table.test_unsortable_column_keeps_its_place()[source]

sortable=False keeps a column in the layout, out of the sort choices.

tests.test_table.test_sort_by_option_field_vocabulary()[source]

Bare column IDs declare a table-less field vocabulary.

tests.test_table.test_sort_by_option_labeled_defs_not_vocabulary()[source]

Labeled definitions keep the declaration-time baked-sort behavior.

tests.test_table.test_sort_by_option_rejects_mixed_defs()[source]

Bare IDs and labeled definitions cannot be mixed.

tests.test_table.test_sort_by_group_heterogeneous_tables(invoke)[source]

A group-level field vocabulary sorts each subcommand’s table independently.

Each table sorts by the selected fields it carries; a table carrying none of them keeps its original row order.

tests.test_table.test_print_table_explicit_sort_key_wins(invoke)[source]

An explicit sort_key bypasses the context --sort-by resolution.

tests.test_telemetry module

tests.test_telemetry.test_standalone_telemetry_option(invoke, cmd_decorator, telemetry_help, option_decorator)[source]
tests.test_telemetry.test_multiple_envvars(invoke, cmd_decorator, telemetry_help)[source]

tests.test_test_suite module

Tests for the declarative CLI test-suite engine and its test-suite command.

Covers three surfaces:

The host Python interpreter stands in for the command under test, so cases stay fast and platform-neutral.

tests.test_test_suite.test_parse_returns_cases()[source]

A well-formed YAML suite yields one CLITestCase per entry.

tests.test_test_suite.test_parse_returns_cases_per_format(suite_string, fmt)[source]

TOML (cases under [[cases]]) and JSON (bare array) yield the same cases.

tests.test_test_suite.test_parse_toml_requires_cases_key()[source]

A TOML mapping without a top-level ‘cases’ array of tables is rejected.

tests.test_test_suite.test_parse_rejects_non_suite_format()[source]

A format that cannot represent a list of cases is rejected.

tests.test_test_suite.test_parse_rejects_malformed(suite, exception)[source]

Empty, mapping-without-cases, and unknown-directive suites raise.

tests.test_test_suite.test_load_detects_format_from_extension(tmp_path, filename, content)[source]

The file extension selects the parser, so each format yields the case.

tests.test_test_suite.test_load_rejects_unknown_extension(tmp_path)[source]

An extension matching no suite format is rejected.

tests.test_test_suite.test_cases_from_data_builds_cases()[source]

A list of directive mappings becomes CLITestCase instances.

tests.test_test_suite.test_cases_from_data_rejects_invalid_directive(directive)[source]

An unknown directive is rejected, and so is the trace the runner fills.

tests.test_test_suite.test_case_execution_trace_stays_out_of_comparisons()[source]

A case that ran still compares equal to the same case, unrun.

tests.test_test_suite.test_split_args_honors_quotes(cli, expected)[source]

Quoting survives tokenization identically on POSIX and Windows.

shlex and CommandLineToArgvW are two different parsers, so the cases above are the subset of syntax on which they must agree.

Two corners stay out on purpose, because the parsers answer differently and each answer is right for its platform: a backslash escapes the next character for shlex but stands for itself on Windows (which is what keeps C:\Users intact), and a doubled quote inside a quoted run closes and reopens it for shlex while Windows folds it into one literal quote.

tests.test_test_suite.test_case_normalizes_scalars()[source]

String scalars are coerced: exit_code to int, cli_parameters to a tuple.

tests.test_test_suite.test_case_normalizes_timeout(value, expected)[source]

A timeout given as int, float, or numeric string becomes a float.

Plain integers are what every config format produces for a bare number, so they must be accepted (a timeout: 5 in any suite).

tests.test_test_suite.test_case_rejects_non_numeric_timeout(value)[source]

A boolean or non-scalar timeout is rejected as not-a-float.

tests.test_test_suite.test_case_accepts_compiled_regexes()[source]

A regex directive takes a compiled pattern as well as a string.

The field hints admit both: a compiled pattern is kept as is, a string is compiled with dots matching newlines, and the two mix in one directive.

tests.test_test_suite.test_case_survives_dataclass_replace()[source]

dataclasses.replace runs the normalization again over normalized values.

Every field family has to accept its own output for that: compiled regexes, resolved platform sets and split parameters all round-trip unchanged, and only the replaced field moves.

tests.test_test_suite.test_output_contains_sees_merged_stream()[source]

output_contains matches substrings from both stdout and stderr.

tests.test_test_suite.test_output_regex_preserves_cross_stream_order()[source]

output_regex_matches sees stdout and stderr interleaved in write order.

tests.test_test_suite.test_output_directive_mismatch_fails()[source]

A substring absent from the merged stream fails the case.

tests.test_test_suite.test_only_platforms_runs_on_member_platform()[source]

A case restricted to the current platform’s own id is not skipped.

tests.test_test_suite.test_only_platforms_skip_names_required_platforms(monkeypatch)[source]

The skip message names the case’s required platforms, not the current one.

Naming the current one would read “only runs on platform: macOS” for a case skipped on macOS.

tests.test_test_suite.test_non_utf8_output_does_not_crash_the_harness()[source]

Bytes that are not valid UTF-8 are escaped, not a reader-thread crash.

A binary emitting its platform’s legacy encoding (cp1252 on Windows) would otherwise kill the capture with a UnicodeDecodeError, failing the case with no hint of the cause. The escaped bytes flow into the captured stream, where assertions can see them.

tests.test_test_suite.test_child_inherits_utf8_io_encoding()[source]

The subprocess emits UTF-8 regardless of the platform’s default.

PYTHONIOENCODING is injected into the child environment so CPython-based binaries write UTF-8 on piped stdout, where Windows would pick cp1252 and desynchronize from the harness’s UTF-8 decoding.

tests.test_test_suite.test_output_and_stream_directives_are_mutually_exclusive()[source]

Combining output_* with stdout_*/stderr_* is rejected at construction.

tests.test_test_suite.ECHO_ENV = ('-c', "import os; print(os.environ.get('PROBE_VAR', '<absent>'))")

Command line printing one variable, or a marker when it is not set.

tests.test_test_suite.test_env_sets_a_variable_on_the_child()[source]

A variable a case declares reaches the command it runs.

tests.test_test_suite.test_unset_env_hides_an_inherited_variable(monkeypatch)[source]

A variable exported around the suite can be taken away for one case.

The half env cannot cover: assigning the empty string leaves the variable set, which a flag read by bare presence counts as activation.

tests.test_test_suite.test_env_directives_leave_the_runner_environment_alone(monkeypatch)[source]

Cases stay independent under --jobs: only the child is touched.

tests.test_test_suite.test_env_overrides_the_injected_io_encoding_default()[source]

A case pinning PYTHONIOENCODING wins over the harness’s own default.

The harness injects utf8 so a child’s piped stdout stays decodable (see test_child_inherits_utf8_io_encoding); a case declaring the variable itself must land after that. cp1252 is picked over latin-1 because Python reports the latter back under its iso8859-1 alias.

tests.test_test_suite.test_env_rejects_what_is_not_a_string_mapping(value, exception, match)[source]

An environment holds strings only, so anything else is refused loudly.

tests.test_test_suite.test_unset_env_normalizes_like_the_envvar_helpers()[source]

A single name is wrapped, duplicates collapse, blanks are dropped.

tests.test_test_suite.LIST_CWD = ('-c', "import os; print(sorted(os.listdir('.')))")

Command line printing the names the working directory holds.

tests.test_test_suite.test_work_directory_moves_the_command(tmp_path)[source]

The command runs where asked, not where the runner sits.

tests.test_test_suite.test_work_directory_defaults_to_the_runner_directory(tmp_path)[source]

Left unset, a case sees what it always saw.

tests.test_test_suite.test_work_directory_leaves_the_command_resolution_alone(tmp_path)[source]

The target is resolved before the move, so it is never looked for there.

run_cli_test resolves a PATH name (and .absolute()``s a path) against the runner's own directory first, which is what lets a relative target survive a ``work_directory pointing somewhere that does not hold it.

tests.test_test_suite.test_run_suite_applies_the_work_directory_to_every_case(tmp_path)[source]

The orchestrator hands it down, so a whole suite moves at once.

tests.test_test_suite.test_env_and_unset_env_are_valid_suite_directives()[source]

Both reach a case through a serialized suite, not just the Python API.

tests.test_test_suite.test_run_counts_pass_and_fail(jobs)[source]

Pass/fail tallies match regardless of the worker count.

tests.test_test_suite.test_run_select_test_skips_others()[source]

select_test runs only the chosen 1-based cases; the rest count as skipped.

tests.test_test_suite.test_run_exit_on_error_bails_sequentially()[source]

With one worker, exit_on_error stops before later cases run.

tests.test_test_suite.test_run_stats_echoes_summary(capsys)[source]

stats prints the worker line up front and the result tally at the end.

tests.test_test_suite.test_run_no_stats_is_quiet(capsys)[source]

Without stats, neither the worker line nor the tally is printed.

tests.test_test_suite.test_cli_runs_default_suite(invoke)[source]

With no suite source, the subcommand runs the built-in default suite.

tests.test_test_suite.test_cli_runs_suite_file(invoke, tmp_path)[source]

A –suite-file is parsed and run against the target command.

tests.test_test_suite.test_cli_runs_toml_suite_file(invoke, tmp_path)[source]

A TOML –suite-file runs without the yaml extra, since TOML is built in.

tests.test_test_suite.test_cli_reports_failure_exit_code(invoke, tmp_path)[source]

A failing case makes the subcommand exit non-zero.

tests.test_test_suite.test_cli_rejects_non_integer_jobs(invoke)[source]

–jobs is click-extra’s JobsOption, so a non-numeric value is refused.

tests.test_test_suite.test_cli_skip_platform_knows_every_platform_id(invoke)[source]

–skip-platform takes any extra_platforms ID and refuses anything else.

The IDs are loaded when the option is first parsed, so importing the CLI does not load the platform detection, and the check still happens.

tests.test_test_suite.test_cli_requires_command(invoke)[source]

Without –command/–binary, the subcommand errors with a usage message.

tests.test_test_suite.test_cli_resolves_suite_from_config(invoke, tmp_path, monkeypatch)[source]

With no –suite-file, the suite comes from [tool.click-extra.test-suite].

tests.test_test_suite.test_cli_resolves_native_cases_from_config(invoke, tmp_path, monkeypatch)[source]

Cases can be declared natively under [[tool.click-extra.test-suite.cases]].

tests.test_testing module

Test the testing utilities and the simulation of CLI execution.

tests.test_testing.test_real_fs()[source]

Check a simple test is not caught into the CLI runner fixture which is encapsulating all filesystem access into temporary directory structure.

tests.test_testing.test_temporary_fs(runner)[source]

Check the CLI runner fixture properly encapsulated the filesystem in temporary directory.

tests.test_testing.test_runner_output()[source]
tests.test_testing.check_default_colored_rendering(result)[source]
tests.test_testing.check_default_uncolored_rendering(result)[source]
tests.test_testing.check_forced_uncolored_rendering(result)[source]
tests.test_testing.test_invoke_optional_color(invoke)[source]
tests.test_testing.test_invoke_default_color(invoke)[source]
tests.test_testing.test_invoke_forced_color_stripping(invoke)[source]
tests.test_testing.test_invoke_color_keep(invoke)[source]
tests.test_testing.test_invoke_force_color(invoke)[source]

Test colors are preserved while invoking, and forced to be rendered on Windows.

tests.test_testing.test_invoke_force_color_contradicting_color_raises()[source]

Asking for forced colors and stripped ones at once is refused.

tests.test_testing.test_invoke_catch_exceptions_follows_the_runner(runner_catches, invoke_catches, propagates)[source]

invoke() falls back to the runner’s own catch_exceptions setting.

tests.test_testing.test_command_default_color()[source]

With @command and no color env var, Context and ColorOption resolve the GNU auto default (ctx.color=None), yet a forced runner still renders ANSI codes.

tests.test_testing.test_command_no_color_flag()[source]

Invoke with –no-color. Verify ctx.color=False and ANSI stripped from echo output.

tests.test_testing.test_force_color_attribute()[source]

CliRunner.force_color=True applies force_color to every run.

tests.test_testing.test_no_color_envvar()[source]

NO_COLOR=1 env var causes ctx.color=False via ColorOption.

tests.test_testing.test_force_color_envvar()[source]

FORCE_COLOR=1 env var keeps ctx.color=True via ColorOption.

tests.test_testing.test_should_strip_ansi_non_tty()[source]

In a test runner (non-TTY), should_strip_ansi behaves based on color arg.

tests.test_testing.test_resolve_color_default_no_context()[source]

Outside any Click context, resolve_color_default returns None or passed value.

tests.test_theme module

Tests for click_extra.theme: in-process isolation, built-in themes and [tool.<cli>.themes.<name>] config integration.

tests.test_theme.test_theme_does_not_leak_across_invocations()[source]

A --theme light invocation must not bleed into a later --help render.

Two back-to-back invocations of the same CLI in the same process:

  1. --theme light --help – selects the light palette for this call only.

  2. --help – no --theme argument, must fall back to the dark default.

The dark theme renders headings with \x1b[94m (bright blue); the light theme uses \x1b[35m (magenta, chosen to stay distinct from its blue options). If the second invocation picks up the first’s choice via process-wide state, it leaks the light palette and the assertion below fails.

tests.test_theme.test_theme_default_unchanged_after_invocation()[source]

An invocation with --theme light must not mutate the process-wide default.

tests.test_theme.test_demo_themes_renders_each_builtin_palette()[source]

click-extra themes renders the sample help once per built-in theme, each in its own palette.

Guards against the whole gallery collapsing onto the default palette: the themes command drives get_current_theme through the context THEME meta, so a regression there would render all seven screens identically.

tests.test_theme.test_demo_themes_renders_only_the_named_palettes()[source]

Named theme IDs narrow the gallery down, and keep the order they were given.

tests.test_theme.test_demo_themes_resolves_auto_to_a_palette_name()[source]

auto is a directive, so the gallery labels the palette it lands on.

Which palette that is depends on the background detected from the environment, so the expectation is read back from the same resolver rather than pinned to dark.

tests.test_theme.test_demo_themes_rejects_an_unknown_palette()[source]

An unknown ID fails, and the error lists every name that would have worked.

tests.test_theme.test_theme_meta_key_matches_registry()[source]

get_current_theme() reads from the same key ThemeOption writes.

tests.test_theme.test_font_role_slots_are_known_and_disjoint()[source]

LITERAL_STYLES / REPLACEABLE_STYLES must classify real, distinct slots.

The two frozensets encode the man-pages(7) bold/italic font roles by slot name and are maintained by hand, so guard against drift: every name must be a real HelpTheme field, the two roles must not overlap, and the representative slots must keep their expected role.

tests.test_theme.test_builtin_themes_alphabetical()[source]

Built-in themes are declared alphabetically (matters for --theme choices).

tests.test_theme.test_builtin_themes_build_on_first_lookup()[source]

Importing the package builds no theme, and a --help builds dark only.

Reading the theme names, which --theme does to list its choices, builds nothing either. Checked in a pristine interpreter, since the other tests have built every theme already.

tests.test_theme.test_builtin_themes_resolve_to_one_instance()[source]

A built-in theme is the same instance in every mapping that holds it.

tests.test_theme.test_theme_registry_takes_dict_mutations(monkeypatch)[source]

Registering, replacing and removing entries work on built-in names too.

tests.test_theme.test_builtin_themes_are_helptheme_instances()[source]

Every BUILTIN_THEMES entry is a HelpTheme instance.

tests.test_theme.test_cloup_constructors_keep_the_subclass(palette)[source]

dark() and light() return a click-extra theme carrying cloup’s palette.

Cloup builds both palettes from cloup.Style, which to_dict and cascade reject, so each style is widened to click-extra’s.

tests.test_theme.test_with_rejects_unknown_slots()[source]

Every argument naming no slot is listed, in a stable order.

tests.test_theme.test_with_takes_none_as_a_value()[source]

None resets a slot, where cloup’s version reads it as no change.

tests.test_theme.test_builtin_themes_follow_manpage_font_convention(theme_name)[source]

Every built-in theme bolds literal slots and italicizes replaceable ones.

Encodes the man-pages(7) typographic convention (LITERAL_STYLES bold, REPLACEABLE_STYLES italic) as an invariant across all palettes, so adding a theme or tweaking a slot can’t silently drop the literal/replaceable distinction. The manpage theme renders it with no color at all.

tests.test_theme.test_theme_round_trips_through_dict(theme_name)[source]

HelpTheme.to_dict/from_dict round-trips every built-in theme.

tests.test_theme.test_builtin_theme_definition_matches_to_dict(theme_name)[source]

Each built-in definition equals what to_dict emits for its theme.

So a definition sets no slot to its default value, which to_dict leaves out.

tests.test_theme.test_to_dict_omits_identity_slots()[source]

Slots left at the identity default do not appear in to_dict output.

tests.test_theme.test_to_dict_emits_cross_ref_highlight_only_when_overridden()[source]

cross_ref_highlight is emitted only when it differs from the default.

tests.test_theme.test_from_dict_rejects_unknown_keys()[source]

Typos like optoin raise TypeError instead of being silently dropped.

tests.test_theme.test_cascade_overrides_only_set_slots()[source]

cascade keeps base’s slots wherever the overlay leaves them at default.

tests.test_theme.test_cascade_returns_new_instance_when_overlay_changes_anything()[source]

Even a single-slot overlay produces a distinct theme instance.

tests.test_theme.test_cascade_round_trips_through_dict()[source]

self.to_dict() wins over base.to_dict() slot-by-slot.

tests.test_theme.test_cascade_rejects_non_theme_base()[source]

cascade rejects anything that is not a HelpTheme.

tests.test_theme.test_themechoice_choices_track_global_registry()[source]

Outside any context, choices reflects the module-level registry.

Plus the reserved auto directive, which every CLI takes and no registry holds.

tests.test_theme.test_themechoice_choices_pick_up_context_overrides()[source]

A theme stashed under THEME_OVERRIDES shows up in choices.

tests.test_theme.test_branded_themes_meet_wcag_aa_large(theme_name, slot)[source]

Branded themes’ readable-text slots clear WCAG AA Large (3.0+).

Branded palettes are deliberate 24-bit RGB choices, so we can hold them to a real WCAG threshold. AA Large is the realistic floor: full AA (4.5+) is unattainable for some published themes (like Solarized’s accent blue on its base03 background sits at ~4.08).

A regression that drops one of these slots below 3.0 means the theme is less readable than what currently ships and warrants a deliberate palette tweak rather than a silent slip.

tests.test_theme.test_themes_meet_legibility_floor(theme_name)[source]

Every styled slot in every theme stays above the legibility floor.

Subdued slots (debug, bracket, default, …) are allowed to fall below WCAG AA Large by design, but no slot should be effectively invisible against the theme’s assumed background. Catches accidental palette tweaks like setting an attribute to nearly the background color.

tests.test_theme.test_themechoice_inert_when_registry_empty(monkeypatch)[source]

With no themes available, ThemeChoice ignores any value instead of failing.

A program can empty the registry: then even the built-in dark default cannot resolve, so convert returns None rather than aborting the invocation.

tests.test_theme.test_cli_runs_with_empty_theme_registry(invoke, monkeypatch)[source]

A CLI still runs when no themes are available.

The built-in –theme option defaults to dark; with an empty registry that default must stay inert instead of crashing the whole command.

tests.test_theme.test_themes_from_config_overrides_existing_theme()[source]

Known theme names cascade on top of the matching built-in palette.

tests.test_theme.test_themes_from_config_creates_standalone_theme()[source]

Unknown theme names build a stand-alone theme with unset slots at default.

tests.test_theme.test_themes_from_config_does_not_mutate_global_registry()[source]

themes_from_config is pure: the module-level registry is untouched.

tests.test_theme.test_get_theme_registry_falls_back_to_global_without_ctx()[source]

get_theme_registry(None) returns a copy of the module-level registry.

tests.test_theme.test_config_loads_new_theme(invoke, create_config)[source]

A [tool.<cli>.themes.<name>] table registers a new theme for the invocation.

tests.test_theme.test_config_overrides_existing_theme_palette(invoke, create_config)[source]

A [tool.<cli>.themes.dark] table cascades onto the built-in dark palette.

tests.test_theme.test_config_theme_appears_in_help_metavar(invoke, create_config)[source]

--help lists config-defined themes alongside the built-ins.

tests.test_theme.test_config_theme_validation_error(invoke, create_config)[source]

A malformed [tool.<cli>.themes.<name>] table is rejected with a rooted path.

tests.test_theme.test_validate_themes_config_codes(themes, code)[source]

A malformed themes table is reported under a snake-case code, like every other configuration error.

tests.test_theme.test_command_validators_keep_the_theme_validator(invoke, create_config)[source]

Validators a command declares join the built-in themes validator.

@command(config_validators=...) forwards them to the default --config option, which keeps the built-in validator beside them: a malformed [tool.<cli>.themes.<name>] table is reported as a configuration validation error, not a TypeError.

tests.test_theme.test_config_theme_does_not_leak_across_invocations(invoke, create_config)[source]

Themes defined in invocation N are not visible in invocation N+1.

tests.test_theme.test_validate_config_catches_bad_theme(invoke, create_config)[source]

--validate-config surfaces the same ValidationError as the runtime path.

tests.test_theme.test_theme_auto_resolves_to_detected_palette(invoke, monkeypatch, env, expected_name)[source]

--theme=auto picks the built-in palette implied by the terminal background.

tests.test_theme.test_theme_auto_from_config(invoke, create_config, monkeypatch)[source]

theme = "auto" in the config file makes detection the effective default.

tests.test_theme.test_theme_auto_advertised_in_help_metavar(invoke)[source]

The reserved ‘auto’ directive is listed among the values –theme takes.

It resolves to a palette rather than being one, but it works on every Click Extra CLI, and a value named nowhere is a value nobody finds.

tests.test_theme.test_themechoice_accepts_auto_directive()[source]

‘auto’ converts to itself though it is not a registered palette.

tests.test_theme.test_theme_option_query_background_opt_in()[source]

The live OSC 11 query is off unless a CLI explicitly opts in.

tests.test_theme.test_resolve_auto_theme_forwards_query_flag(monkeypatch)[source]

query_background gates the live query that env-var detection ignores.

tests.test_theme.test_theme_envvar_picks_palette(invoke, monkeypatch, env_value, expected_name)[source]

CLICK_EXTRA_THEME themes a CLI invoked without --theme.

tests.test_theme.test_theme_envvar_accepts_auto_directive(invoke, monkeypatch)[source]

The reserved ‘auto’ directive resolves from the variable too.

tests.test_theme.test_theme_flag_outranks_envvar(invoke, monkeypatch)[source]

An explicit --theme beats the machine-wide variable.

tests.test_theme.test_cli_envvar_outranks_theme_envvar(invoke, monkeypatch)[source]

The <CLI>_THEME variable is more specific than the machine-wide one.

tests.test_theme.test_config_outranks_theme_envvar(invoke, create_config, monkeypatch)[source]

A palette pinned in the CLI’s own config file is more specific.

tests.test_theme.test_unknown_theme_envvar_warns_and_keeps_default(invoke, monkeypatch)[source]

A typo in a shell profile must not break every Click Extra CLI at once.

tests.test_theme.test_theme_from_env_reaches_the_help_screen(invoke, monkeypatch, envvar)[source]

A palette named by the environment paints the screen it exists to paint.

Click processes a typed --help before an untyped --theme, so the palette only reaches the help screen through the pre-pass in Command._resolve_presentation_eagerly.

tests.test_theme.test_theme_flag_after_help_still_paints_it(invoke)[source]

--help --theme nord renders under nord, like the reverse order.

Both are typed, so Click sorts --help first and would render before the palette settles: the same pre-pass covers it.

tests.test_trail module

tests.test_trail.test_bar_stop_leaves_a_stalled_ticker_behind(monkeypatch)[source]

The progress bar’s elapsed-clock ticker gets the same bounded stop.

tests.test_trail.test_bar_teardown_after_a_stall_returns_at_once(monkeypatch)[source]

A second teardown of a bar whose ticker stalled does not wait on it.

The stalled ticker holds the draw lock while it blocks in its write. A Ctrl+C inside a trail runs finish(), then stop(), and the second one must not take that lock and wait for the terminal to drain.

tests.test_trail.test_interrupted_trail_closes_with_a_finisher(progress_bar, jobs, stream_factory)[source]

A Ctrl+C inside the trail still ends it on a ✘ line saying how far it got, and an outcome landing afterwards prints below that line.

tests.test_trail.test_demo_trail_help_lists_renderings(invoke)[source]

trail --help documents its purpose and the rendering-selecting options.

tests.test_trail.test_demo_trail_rejects_unknown_spinner(invoke)[source]

An unknown –spinner name is rejected by the Choice type before any work.

tests.test_trail.test_demo_trail_prints_plain_lines_off_tty(invoke, monkeypatch, args)[source]

Every rendering runs the batch to completion and, off a TTY, prints each outcome and the finisher as plain lines, with no indicator drawn.

tests.test_trail.test_operation_trail_exported_from_root()[source]
tests.test_trail.test_trail_line_carries_themed_glyphs()[source]
tests.test_trail.test_sequential_trail_echoes_lines_and_finisher()[source]

A sequential batch on a TTY echoes each outcome, then a timed finisher.

tests.test_trail.test_sequential_trail_echoes_off_tty()[source]

A non-interactive stream gets the trail lines and finisher by default.

tests.test_trail.test_sequential_trail_invisible_stays_silent_off_tty()[source]

visible=False silences the trail on a non-interactive stream too.

tests.test_trail.test_trail_forced_spinner_draws_on_pipe()[source]

live="always" animates the aggregate spinner on a non-interactive stream.

tests.test_trail.test_sequential_trail_echo_opt_out()[source]

echo_sequential=False silences a sequential batch, even on a TTY.

tests.test_trail.test_concurrent_trail_buffers_until_spinner_draws()[source]

Outcomes marked before the aggregate spinner first draws are buffered, then flushed above it; the finisher becomes the spinner’s kept line.

The draw delay guarantees the first mark lands before the first frame, making the buffering deterministic instead of racing the animation thread.

tests.test_trail.test_concurrent_trail_invisible_stays_silent()[source]

visible=False keeps the concurrent spinner and its buffer off screen.

tests.test_trail.test_trail_echoes_a_batch_its_indicator_never_drew(rendering, finished)[source]

A batch finishing inside delay still leaves its whole record on screen.

Its indicator never drew, so the lines it buffered never reached the stream. A sequential batch of the same outcomes prints them, and so must this one, in order: before the finisher, or on its own when the batch is left without one.

tests.test_trail.test_trail_leaves_nothing_for_an_undrawn_batch_without_echo(options, stream_class, finished)[source]

The echo of an undrawn batch obeys the same gates as a sequential one.

tests.test_trail.test_trail_stays_plain_above_a_drawn_indicator_without_color(monkeypatch, rendering)[source]

With color off, a drawn indicator’s trail lines and finisher carry no SGR.

Those lines reach the stream through a raw write rather than click.echo, so the glyph’s paint and any escape embedded in the message have to be stripped before the write.

tests.test_trail.test_concurrent_trail_marks_are_thread_safe()[source]

Concurrent mark() calls from worker threads all land in the tally.

tests.test_trail.test_concurrent_trail_uses_chosen_spinner_preset()[source]

spinner= picks the concurrent aggregate spinner’s animation.

tests.test_trail.test_concurrent_spinner_eta_mode()[source]

clock=’eta’ drives the concurrent spinner’s estimate from a hidden Click bar (stepped per outcome), and the finisher still shows the elapsed total.

tests.test_trail.test_operation_trail_appends_per_operation_timing()[source]

With timer on (the default), a seconds value appends each operation’s own duration to its trail line, independent of the others.

tests.test_trail.test_operation_handle_times_from_its_creation()[source]

An operation() handle marks its outcome with the elapsed since it began.

tests.test_trail.test_operation_trail_timer_false_drops_all_timing()[source]

timer=False silences both the per-item and the finisher clock.

tests.test_trail.test_operation_trail_timer_callable_formats_durations()[source]

A callable timer formats the per-item durations it is handed.

tests.test_trail.test_progress_bar_trail_renders_bar_and_finisher()[source]

progress_bar=True drives a determinate bar with outcomes above it.

tests.test_trail.test_progress_bar_trail_shows_empty_bar_on_entry()[source]

The bar’s 0/total state draws on entry, not only once the first outcome advances it, matching the spinner indicator that animates its tally at once.

tests.test_trail.test_progress_bar_clock_defaults_to_elapsed()[source]

clock=’elapsed’ (the default) draws a stopwatch from the start via item_show_func, with Click’s ETA off and a ticker to keep it moving.

tests.test_trail.test_progress_bar_clock_eta_uses_click_eta()[source]

clock=’eta’ keeps Click’s estimated-time display and runs no ticker.

tests.test_trail.test_progress_bar_elapsed_clock_ticks_between_marks()[source]

The elapsed clock advances on its own between outcomes, with no mark.

tests.test_trail.test_operation_trail_rejects_invalid_clock()[source]

clock must be ‘elapsed’ or ‘eta’.

tests.test_trail.test_operation_trail_rejects_invalid_live()[source]

live must be ‘auto’, ‘always’ or ‘never’.

tests.test_trail.test_invisible_trail_draws_nothing_even_when_live_always()[source]

visible=False wins over live="always": no indicator, no line.

tests.test_trail.test_progress_bar_trail_works_concurrently()[source]

progress_bar=True also drives a concurrent batch from worker threads.

tests.test_trail.test_trail_echoes_live_where_no_indicator_draws(monkeypatch, rendering, stream_class, term, options)[source]

Where no indicator draws, each outcome prints as soon as it is marked.

Nothing owns the live line there, so no line waits for a first frame that never comes, and no cursor control reaches the stream.

tests.test_trail.test_progress_bar_requires_positive_total()[source]

A determinate bar needs a length, so total must be positive.

tests.test_trail.test_progress_bar_and_spinner_are_mutually_exclusive()[source]

progress_bar and spinner select different indicators; only one may win.

tests.test_trail.test_progress_bar_registers_as_active_line_not_spinner()[source]

A drawing bar owns the active line (so logs cooperate), but is no spinner.

tests.test_tree module

Tests for the command-tree rendering feature.

tests.test_tree.OBSERVATORY_TREE = 'observatory                     Weather observatory control center.\n├── help [COMMAND_PATH]...      Show help for a command.\n├── report CITY                 Print the forecast for a city.\n├── station (st)                Manage remote weather stations.\n│   ├── calibrate               Recalibrate the sensors.\n│   ├── help [COMMAND_PATH]...  Show help for a command.\n│   └── reboot                  Power-cycle the station. (DEPRECATED)\n└── status                      Report the current conditions.'

Expected unstyled tree of the observatory fixture CLI.

Descriptions are column-aligned on the longest rail-plus-label row, hidden commands are skipped, aliases are parenthesized, operand metavars follow the command name and deprecated commands carry their marker. The help entries are the implicit subcommand every click-extra group registers.

tests.test_tree.runner()[source]
tests.test_tree.test_render_tree_structure()[source]
tests.test_tree.test_render_tree_non_group()[source]
tests.test_tree.test_render_tree_hidden_skipped()[source]
tests.test_tree.test_render_tree_prog_name_override()[source]
tests.test_tree.test_render_tree_theme_styling()[source]

Nodes reuse the same theme slots and styling as help screens.

tests.test_tree.test_render_tree_paints_each_choice_of_an_operand()[source]

An operand enumerating its values draws them the way a help screen does.

Painted one value at a time rather than as a single metavar span, so a reader picks the accepted words out of the punctuation around them.

tests.test_tree.test_render_tree_paints_the_deprecation_label()[source]

The marker takes the slot the help screen paints it with.

A description reaches the tree already carrying Click’s label, and it is the only part of a node the name-and-metavar pass does not style.

tests.test_tree.test_render_tree_wraps_descriptions()[source]

Long descriptions wrap in the aligned column, rail running through.

tests.test_tree.test_tree_option_prints_and_exits(runner)[source]
tests.test_tree.test_tree_option_colored(runner)[source]
tests.test_tree.test_tree_option_accessible_ascii_rail(runner)[source]

Accessibility mode degrades the box-drawing rail to pure ASCII.

tests.test_tree.test_tree_option_in_help(runner)[source]
tests.test_tree.kitchen_script(tmp_path)[source]
tests.test_tree.test_wrap_tree_renders_tree(runner, kitchen_script)[source]
tests.test_tree.test_wrap_tree_drills_into_subcommand(runner, kitchen_script)[source]

Extra arguments after SCRIPT re-root the tree at the nested subcommand.

tests.test_tree.test_wrap_tree_accessible_ascii_rail(runner, kitchen_script)[source]

Accessibility mode carries over from the wrapping context.

tests.test_tree.test_wrap_tree_mutually_exclusive(runner, kitchen_script, conflicting)[source]
tests.test_tree.test_wrap_tree_unresolvable_target(runner)[source]

tests.test_types module

tests.test_types.test_click_choice_behavior()[source]

Lockdown the behavior of method inherited from Click’s Choice type.

Return type:

None

tests.test_types.test_enum_string_choices(enum_definition, choice_source, result)[source]
Return type:

None

tests.test_types.test_enum_choice_show_aliases(enum_definition, choice_source, show_aliases, result)[source]

Test that EnumChoice correctly handles Enum with aliases.

Return type:

None

tests.test_types.kebab_case(choice)[source]

Reshape a Python identifier into a CLI-friendly choice string.

Return type:

str

class tests.test_types.AliasedStrategy(*values)[source]

Bases: Enum

Aliases declared in the class body, which every supported Python handles.

Unlike _add_alias_(), this needs no Python 3.13.

SELECT_OLDER = 1
SELECT_NEWEST = 2
DISCARD_NEWEST = 1
DISCARD_OLDER = 2
class tests.test_types.TransformedFormat(*values)[source]

Bases: Enum

PLAIN_TEXT = 'plain_text'
RICH_HTML = 'rich_html'
tests.test_types.test_enum_choice_transform(choice_source, transform, expected)[source]

transform reshapes the choice string produced by any source.

Return type:

None

tests.test_types.test_enum_choice_transform_with_aliases(choice_source)[source]

transform is what makes show_aliases usable with non-identifier choices.

Return type:

None

tests.test_types.test_enum_choice_transform_collision(show_aliases)[source]

A transform collapsing two spellings into one is rejected.

Return type:

None

tests.test_types.test_enum_choice_transform_non_string()[source]

A transform returning a non-string blames itself, not the choice source.

Return type:

None

tests.test_types.test_enum_choice_transform_raising()[source]

A transform blowing up is reported with the string it choked on.

Return type:

None

class tests.test_types.MyEnum(*values)[source]

Bases: Enum

Produce different strings for keys/names, values and str().

FIRST_VALUE = 'first-value'
SECOND_VALUE = 'second-value'
tests.test_types.test_enum_choice_internals(source, expected_choices)[source]
Return type:

None

tests.test_types.test_enum_choice_case_sensitivity(case_sensitive)[source]
Return type:

None

tests.test_types.test_enum_choice_shell_complete(source, expected)[source]

Completion offers normalized choice strings, never the Enum.member form.

Completion routes through Choice.normalize_choice(), so an EnumChoice suggests parseable strings (case-folded, as it is case-insensitive by default) instead of MyEnum.FIRST_VALUE.

Return type:

None

tests.test_types.test_enum_choice_rejects_non_enum_choices(choices)[source]

Anything but an Enum class is refused with a TypeError.

Return type:

None

tests.test_types.test_enum_choice_rejects_unknown_choice_source()[source]

A string naming no ChoiceSource member is refused with a ValueError.

Return type:

None

tests.test_types.test_enum_choice_duplicate_string()[source]
Return type:

None

tests.test_types.test_enum_choice_command(invoke, cmd_decorator, opt_decorator, case_sensitive, valid_args, invalid_args)[source]

Test EnumChoice used within an option.

Return type:

None

tests.test_types.test_enum_choice_default_value(invoke, cmd_decorator, opt_decorator, opt_type, default_value)[source]

Test EnumChoice used within an option with a default value.

Return type:

None

tests.test_types.test_enum_choice_multiple_default_value(invoke, cmd_decorator, opt_decorator, default_value, expected)[source]

A multiple=True EnumChoice resolves each member of its tuple default.

Regression test for the get_default() override stringifying the whole default tuple (str((MyEnum.FOO,))) instead of mapping each member, which made the default trip Click’s Value must be an iterable check.

Return type:

None

tests.test_types.test_enum_choice_none_default(invoke, cmd_decorator, opt_decorator)[source]

A single-value EnumChoice option defaulting to None stays unset.

Regression test for the get_default() override turning a None default into the choice string "None", which then failed its own validation.

Return type:

None

tests.test_types.test_enum_choice_variadic_default_value(invoke, cmd_decorator, opt_decorator, default_value, expected)[source]

A variadic (nargs=-1) EnumChoice argument resolves each member of its default.

Companion to test_enum_choice_multiple_default_value covering the other branch of the get_default() override: the nargs == -1 path taken by arguments rather than the multiple path taken by options. Both map get_choice_string() over the members of a tuple default instead of stringifying the whole tuple.

Return type:

None

tests.test_types.test_enum_choice_callback(invoke, cmd_decorator, opt_decorator, choice_source, cli_value)[source]

Test that option callbacks receive properly converted Enum members.

Return type:

None

tests.test_types.test_multi_choice_parses_input(raw, expected)[source]

convert() splits on the separator, strips whitespace, drops empties.

Return type:

None

tests.test_types.test_multi_choice_validates_against_choices()[source]

Unknown tokens raise BadParameter via self.fail().

Return type:

None

tests.test_types.test_multi_choice_validates_an_already_parsed_sequence()[source]

A list is checked like a raw token, not waved through as pre-parsed.

A configuration file hands the option a list where the command line hands it one string, so skipping the check on a sequence lets an unknown value travel to whatever consumes the selection.

Return type:

None

tests.test_types.test_multi_choice_case_insensitive_normalizes()[source]

The default matches case-insensitively and returns the canonical case.

Return type:

None

tests.test_types.test_multi_choice_case_sensitive_matches_exactly()[source]

case_sensitive=True rejects a token differing only in case.

Return type:

None

tests.test_types.test_choice_types_default_to_case_insensitive(choice_type)[source]

Every choice type click-extra defines matches regardless of case by default.

Return type:

None

tests.test_types.test_multi_choice_metavar(choices, separator, expected)[source]
Return type:

None

tests.test_types.test_multi_choice_in_click_option()[source]

End-to-end: MultiChoice plugs into a Click option like Choice does.

Return type:

None

tests.test_types.test_duration_parsing(value, expected)[source]
Return type:

None

tests.test_types.test_duration_passthrough_timedelta()[source]

An already-parsed timedelta is returned unchanged (idempotent conversion).

Return type:

None

tests.test_types.test_duration_invalid(value)[source]
Return type:

None

tests.test_types.test_duration_rejects_calendar_units(value)[source]

Months and years are explicitly rejected because their length is ambiguous.

Return type:

None

tests.test_types.test_duration_absolute_timestamp(value)[source]

An RFC 3339 timestamp is converted to now - timestamp at parse time.

Return type:

None

tests.test_types.test_duration_future_timestamp_parses_to_none()[source]

A timestamp in the future parses to None, read as “no cutoff”.

Return type:

None

tests.test_types.test_duration_in_click_option()[source]

End-to-end: Duration plugs into a Click option.

Return type:

None

tests.test_types.test_parse_duration(value, expected)[source]
Return type:

None

tests.test_types.test_parse_duration_passthrough_timedelta()[source]

An existing timedelta is returned unchanged, like Duration.convert.

Return type:

None

tests.test_types.test_parse_duration_never_raises(value)[source]

The umbrella parser mirrors Duration but returns None instead of failing.

Return type:

None

tests.test_types.test_parse_friendly_duration(value, expected)[source]
Return type:

None

tests.test_types.test_parse_iso8601_duration(value, expected)[source]
Return type:

None

tests.test_utils module

Test the generic helpers of the private utility module.

tests.test_utils.test_exception_chain_ends_on_a_loop()[source]

Two exceptions naming each other are each yielded once.

tests.test_utils.test_exception_chain_follows_the_context_without_a_cause()[source]
tests.test_utils.test_exception_chain_of_nothing_is_empty()[source]
tests.test_utils.test_exception_chain_prefers_the_cause()[source]

The cause an exception was raised from wins over the one it replaced.

tests.test_utils.test_generator_tag()[source]
tests.test_utils.test_missing_extra_message()[source]
tests.test_utils.test_patch_attr_restores_on_exit()[source]
tests.test_utils.test_patch_attr_restores_on_error()[source]
tests.test_utils.test_patch_attr_requires_existing_attribute()[source]
tests.test_utils.test_flatten_collapses_nested_iterables()[source]

Every nesting level is walked, strings and bytes staying whole.

tests.test_utils.test_flatten_of_nothing_is_empty()[source]
tests.test_utils.test_shrinkuser_spells_the_home_directory_as_a_tilde()[source]
tests.test_utils.test_shrinkuser_takes_another_home_spelling()[source]
tests.test_utils.test_deep_merge_merges_dicts_and_takes_every_other_value_from_the_overlay()[source]
tests.test_utils.test_deep_merge_leaves_both_arguments_alone()[source]
tests.test_utils.SET_NESTED_PATHS: tuple[tuple[tuple[str, ...], object], ...] = ((('orchard', 'apple', 'count'), 3), (('orchard', 'apple', 'color'), 'red'), (('orchard', 'pear'), {'count': 1}), (('orchard', 'pear'), {'color': 'green'}), (('orchard', 'pear', 'count'), 2), (('garden',), 'tomato'), (('garden', 'basil'), True), (('orchard', 'apple'), ['fuji', 'gala']), (('orchard', 'apple', 'count'), 4), (('barn',), {'hay': {'bales': 10}}), (('barn', 'hay'), {'straw': 2}))

Paths and values covering every case of a merge: a fresh path, a shared prefix, two dictionaries merging, a dictionary replacing a value, and the reverse.

tests.test_utils.test_set_nested_matches_merging_one_path_trees()[source]

Setting each path in place builds the tree the one-path merges built.

tests.test_utils.test_set_nested_rejects_an_empty_path()[source]
tests.test_utils.test_walk_up_starts_at_the_start_and_ends_at_the_root(tmp_path)[source]
tests.test_utils.test_parse_bool(text, expected)[source]

The spellings read the same whatever their case and surrounding blanks.

tests.test_utils.test_boolean_states_match_configparser()[source]

The copy holds the spellings the standard library reads, no more.

tests.test_utils.BLANK_TREE: dict[str, object] = {'city': 'Lyon', 'forecast': {'rain': None, 'sky': '', 'wind': {}}, 'harbor': {'boats': 0, 'docks': [], 'flags': False}, 'lake': None, 'orchard': {}, 'river': ''}

A tree holding a blank value of each kind, at the top and one level down.

tests.test_utils.test_remove_blanks(kwargs, expected)[source]

Each kind of blank value goes, unless its remove_* argument keeps it.

A falsy value that is not blank, like 0, False or an empty list, stays.

tests.test_utils.test_remove_blanks_drops_a_dict_emptied_by_the_cleaning()[source]
tests.test_utils.test_remove_blanks_leaves_the_tree_alone()[source]

tests.test_version module

Test the --version option.

tests.test_version.test_standalone_version_option(invoke, cmd_decorator, option_decorator)[source]
tests.test_version.test_debug_output(invoke, cmd_decorator, option_decorator, assert_output_regex)[source]
tests.test_version.test_set_version(invoke)[source]
tests.test_version.test_set_version_positional(invoke)[source]

Click drop-in: an explicit version may be the first positional argument.

@version_option("1.2.3.4") is equivalent to the fields={"version": "1.2.3.4"} form used by test_set_version().

tests.test_version.test_version_option_dashed_positional_is_flag()[source]

A leading-dash positional names the option flag, not the version.

tests.test_version.test_version_option_conflicting_version()[source]

The version cannot be supplied both positionally and via fields=.

tests.test_version.test_custom_message(invoke, cmd_decorator, message, regex_stdout, assert_output_regex)[source]
tests.test_version.test_style_reset(invoke, cmd_decorator)[source]
tests.test_version.test_custom_message_style(invoke, cmd_decorator)[source]
tests.test_version.test_build_resolver_answers_and_is_a_template_field(field_id)[source]

Every build resolver answers on the host running it, and has a field.

A build fact is unlike a git one on both counts: nothing can fail to resolve, since the host is right here, and nothing can fall back later, so a resolver whose field is missing from the template would bake a value no message could ever print.

tests.test_version.test_build_time_honors_source_date_epoch(monkeypatch)[source]

A reproducible build pins the stamp instead of reading the clock.

tests.test_version.test_context_meta(invoke, cmd_decorator, assert_output_regex)[source]
tests.test_version.test_debug_version_run_resolves_no_host_name(invoke, monkeypatch)[source]

A --verbosity DEBUG run lists every version field without a DNS query.

A reverse DNS query blocks for as long as the resolver takes to answer, which has no bound on a host whose resolver never does.

Asserting on the calls rather than on a duration keeps the guard away from a timing threshold, which a loaded runner would flake on.

tests.test_version.test_debug_listing_reaches_a_level_below_debug(invoke, caplog)[source]

A logger set below DEBUG, like a custom trace level, lists the fields too.

tests.test_version.test_context_meta_laziness(invoke, cmd_decorator)[source]

Accessing a single field from ctx.meta must not evaluate unrelated fields.

Ensures that the _LazyVersionDict defers property evaluation: reading click_extra.version should not trigger expensive properties like the git fields.

tests.test_version.test_module_version_parent_package_fallback(monkeypatch)[source]

module_version falls back to parent package’s __version__.

Simulates the Nuitka use-case: a CLI whose module is myapp.__main__ (no __version__), with the parent package myapp providing it.

tests.test_version.test_is_main_module(module_name, expected)[source]
tests.test_version.test_main_entry_point_survives_absent_distribution_metadata(monkeypatch)[source]

A compiled binary reads its version off the package it started in.

A Nuitka standalone binary ships no distribution metadata, so distribution_of() answers None for the CLI’s own package just as it does for ecosystem plumbing, and the CLI module resolves to click_extra.__main__. That entry point must survive: traded for the root command’s callback module, it loses the __main__ exemption and --version renders nothing at all.

tests.test_version.test_prebaked_fields_read_from_the_package(monkeypatch, module_name)[source]

A field baked into the package __init__.py reaches the CLI module beside it.

click-extra prebake all writes its dunders into the __init__.py of each package a console script enters, while the CLI sits in a sibling module.

tests.test_version.test_prebaked_fields_skip_the_package_of_ecosystem_plumbing(monkeypatch)[source]

A walk ending on a CliRunner never reads Click Extra’s own baked fields.

tests.test_version.test_exec_name(module_name, package, expected)[source]

A __main__ entry point is named after its package, however it started.

python -m orchard runs the entry point under the __main__ name, while a console script or a compiled binary imports it as orchard.__main__.

tests.test_version.test_package_version_resolves_import_name_to_distribution(monkeypatch)[source]

When package_name is an import name that differs from its installed distribution name (PIL vs Pillow), package_version resolves it via packages_distributions() instead of returning None.

tests.test_version.test_package_version_ambiguous_import_name_returns_none(monkeypatch)[source]

When an import name maps to several installed distributions, package_version returns None rather than guessing.

tests.test_version.test_package_version_unknown_returns_none(monkeypatch)[source]

When the name resolves to no distribution, package_version returns None.

tests.test_version.test_cli_frame_fallback()[source]

cli_frame() falls back to the outermost frame when all frames are from the Click ecosystem.

tests.test_version.STANDALONE_SCRIPT = 'import click\nfrom click_extra import echo, version_option\n\n\n@click.command\n@version_option(\n    message=(\n        "{prog_name} | {exec_name} | {package_name}"\n        " | {module_version} | {version}"\n    )\n)\ndef weather():\n    echo("Sunny.")\n\n\nif __name__ == "__main__":\n    weather()\n'

A CLI run straight from a file as __main__, with no package around it.

Its version message renders every field the unpackaged case resolves on its own, so a single invocation pins them all.

tests.test_version.test_standalone_script(tmp_path, dunder, expected_version)[source]

A standalone script is named after its file, and versioned by __version__.

With no package to read metadata from, exec_name falls back to the script’s file name and package_name to None. The version is read from a __version__ variable defined alongside the CLI, and stays None when the script defines none.

Only a real interpreter reaches that code path: a CLI declared inside a test function belongs to the test module, and that module has a package.

tests.test_version.COMMAND_STRING_CLI = 'from click_extra import command, echo\n\n__version__ = "1.2.3"\n\n\n@command\ndef weather():\n    echo("Sunny.")\n\n\nweather()\n'

A Click Extra CLI for python -c, which runs it with no file behind it.

tests.test_version.test_command_string_cli(tmp_path, args, expected)[source]

A CLI run by python -c resolves its version fields without a file.

The version option of a Click Extra command looks for the CLI’s module on every run, not only on --version. The debug level renders every field, including the ones read off the module’s file: exec_name keeps the __main__ module name.

tests.test_version.NUITKA_RUNNER = 'import sys\nimport {module} as user_main\nsys.exit(user_main.{function}())\n'

What Nuitka compiles for --main-entry-point={{name}}={{module}}:{{function}}.

A __main__ with no package and no __version__, which binds the entry point module and calls its function: a compiled binary starts here.

tests.test_version.test_nuitka_runner_resolves_the_entry_point_version(tmp_path, module, function)[source]

The runner of a compiled binary prints the version python -m prints.

An entry point function stops the stack walk in its own module. A command object leaves no frame of its module on the stack, and the walk ends on the runner: the module of the command’s callback then names the CLI.

tests.test_version.test_package_version_without_a_cli_frame(tmp_path, runner_source)[source]

A CLI reads its package version when no frame of its module is on the stack.

No distribution provides the package, as in a compiled binary, so the version can only come from the __version__ of its __init__.py. The runner Nuitka compiles calls the command object from a packageless __main__. An atexit hook calls it once the main module has left the stack, like the target of _thread.start_new_thread() or a call from a C host: the walk then sees only the Click ecosystem, and falls back to the main() of the command. A thread and an executor call it from the standard library, where the walk stops.

tests.test_version.SUBCOMMAND_VERSION_CLI = 'from click_extra import echo, group, version_option\n\n\n@group\ndef orchard():\n    pass\n\n\n@orchard.command()\n@version_option(fields={"version": "1.2.3"})\ndef harvest():\n    echo("Apples picked.")\n\n\norchard()\n'

A group whose subcommand carries a version option of its own.

tests.test_version.test_subcommand_version_option_outside_a_test_runner(tmp_path, args, expected)[source]

The version option of a subcommand resolves where no test runner is loaded.

The stack walk of a subcommand crosses the invoke() of its group, which it checks against the test runner of Click. A process that never imported click.testing has no such runner to check against: a test process always has, so only a new interpreter can tell.

tests.test_version.test_integrated_version_option_precedence(invoke, params)[source]
tests.test_version.test_version_fields_forwarded_to_version_option(invoke)[source]

version_fields on @command forwards to VersionOption.

tests.test_version.test_version_fields_forwarded_on_group(invoke)[source]

version_fields works on @group too.

tests.test_version.test_version_fields_multiple(invoke)[source]

Multiple fields can be overridden at once.

tests.test_version.test_version_fields_rejects_unknown(invoke)[source]

Unknown field names raise TypeError.

tests.test_version.test_unknown_field_is_rejected_the_same_way_everywhere(build)[source]

Every way to name a template field rejects an unknown one alike.

tests.test_version.test_color_option_precedence(invoke)[source]

A plain click.command only decolors --version when --no-color precedes it on the command line.

Click evaluates eager parameters in the order the user typed them (callback evaluation order), so a --no-color placed after --version lands too late: the version screen has already rendered in color and exited.

click-extra’s own @command settles the color options in a pre-pass before any eager screen renders, so the color choice is honored whatever its position. See Command._resolve_presentation_eagerly and the order-independent test_color_settles_before_eager_help_and_version in test_color.py. This test pins the residual behavior of the plain-Click path, which that pre-pass does not reach.

tests.test_version.test_dev_version_appends_git_hash(invoke, cmd_decorator)[source]

A .dev version gets a +hash suffix appended (or not, if git is unavailable).

tests.test_version.test_prebaked_dev_version_not_double_suffixed(invoke, cmd_decorator)[source]

A version with an existing + is returned as-is: no second hash appended.

tests.test_version.test_release_version_unchanged(invoke, cmd_decorator)[source]

A non-dev version is never modified.

tests.test_version.init_file(tmp_path)[source]

Helper that creates a temporary __init__.py with the given content.

tests.test_version.test_prebake_cli_resolves_module_from_config(invoke, tmp_path, monkeypatch)[source]

click-extra prebake resolves its target from [tool.click-extra.prebake].

tests.test_version.test_prebake_dev_version(init_file)[source]

A .dev version gets +hash appended in the file.

tests.test_version.test_prebake_single_quotes(init_file)[source]

Single-quoted __version__ is also handled.

tests.test_version.test_prebake_already_baked_skipped(init_file)[source]

A version with existing + is left untouched.

tests.test_version.test_prebake_release_skipped(init_file)[source]

A release version (no .dev) is not modified.

tests.test_version.test_prebake_no_version_in_file(init_file)[source]

A file without __version__ returns None.

tests.test_version.test_prebake_missing_local_version_raises(init_file)[source]

Calling without local_version raises TypeError.

tests.test_version.test_prebake_idempotent(init_file)[source]

Running prebake twice does not double-suffix.

tests.test_version.test_prebake_preserves_surrounding_content(init_file)[source]

Content around __version__ is not disturbed.

tests.test_version.test_prebake_dunder_empty_replaced(init_file)[source]

An empty dunder variable gets replaced.

tests.test_version.test_prebake_dunder_single_quotes(init_file)[source]

Single-quoted empty dunder is also handled.

tests.test_version.test_prebake_dunder_nonempty_skipped(init_file)[source]

A dunder with an existing non-empty value is left untouched.

tests.test_version.test_prebake_dunder_not_found(init_file)[source]

A file without the target dunder returns None.

tests.test_version.test_prebake_dunder_idempotent(init_file)[source]

Running prebake_dunder twice does not overwrite.

tests.test_version.test_prebake_dunder_preserves_surrounding_content(init_file)[source]

Content around the target dunder is not disturbed.

tests.test_version.test_prebake_dunder_full_sha(init_file)[source]

A full 40-character SHA is handled correctly.

tests.test_version.test_prebaked_git_branch()[source]

A pre-baked __git_branch__ dunder is used over subprocess.

tests.test_version.test_prebaked_git_long_hash()[source]

A pre-baked __git_long_hash__ dunder is used over subprocess.

tests.test_version.test_prebaked_git_tag_sha()[source]

A pre-baked __git_tag_sha__ dunder is resolved.

tests.test_version.test_prebaked_empty_dunder_ignored()[source]

An empty dunder is not treated as a pre-baked value.

tests.test_version.test_prebaked_non_string_ignored()[source]

A non-string dunder is not treated as a pre-baked value.

tests.test_version.test_prebaked_git_distance()[source]

A pre-baked __git_distance__ dunder is used over subprocess.

tests.test_version.test_prebaked_git_dirty()[source]

A pre-baked __git_dirty__ dunder is used over subprocess.

tests.test_version.test_resolve_git_distance(monkeypatch, describe_output, expected)[source]
tests.test_version.test_resolve_git_dirty(monkeypatch, status_output, expected)[source]
tests.test_version.test_run_git_decodes_a_branch_name(tmp_path, branch, expected)[source]

A branch name decodes as UTF-8, and a byte that is not becomes a replacement character.

The branch goes in packed-refs: a loose ref is a file, and APFS refuses a file name that is no UTF-8.

tests.test_version.test_run_git_returns_nothing_or_raises_on_failure(tmp_path, monkeypatch)[source]

A failing command returns None, or raises when asked to.

tests.test_version.test_run_git_gives_up_after_its_timeout(tmp_path, monkeypatch)[source]

A command running past its timeout returns None, or raises when asked.

tests.test_version.test_run_git_keeps_an_empty_last_field(tmp_path)[source]

Only the final newline goes: the empty field ending a line stays.

A lightweight tag points at no tag object, so the date read through one is empty, which ends the line on a tab.

tests.test_version.test_find_git_root(tmp_path)[source]

The walk stops at the nearest .git entry, a file included.

tests.test_version.test_archival_field_substituted(field_id, expected)[source]
tests.test_version.test_archival_field_unsubstituted_ignored(field_id)[source]

A raw checkout keeps the $Format placeholders, which must be ignored.

tests.test_version.test_archival_field_exact_tag_distance_zero()[source]

A bare describe-name (no -N-g suffix) means distance zero.

tests.test_version.test_read_archival_roundtrip(tmp_path)[source]
tests.test_version.test_read_archival_invalid_json(tmp_path)[source]
tests.test_version.test_find_archival_file_walks_up(tmp_path)[source]
tests.test_version.test_find_archival_file_absent(tmp_path)[source]
tests.test_version.test_archival_resolves_git_fields(tmp_path)[source]

Git fields resolve from .git_archival.json when there is no live git.

tests.test_version.install_distribution(tmp_path, monkeypatch, direct_url)[source]

Install a fake apricot distribution, with a direct_url.json holding direct_url unless it is None.

Return type:

None

tests.test_version.test_direct_url_field(data, field_id, expected)[source]
tests.test_version.test_read_direct_url(tmp_path, monkeypatch, direct_url, expected)[source]
tests.test_version.test_read_direct_url_unknown_distribution(distribution)[source]
tests.test_version.test_direct_url_resolves_git_hashes(tmp_path, monkeypatch)[source]

A CLI installed from a Git URL names its commit, with no live git and no archive.

tests.test_version.test_version_dev_hash_assembly(module_version, expected)[source]

The git short hash is appended to dev releases only.

tests.test_version.terminal_width(monkeypatch)[source]

Pin the width VersionScreen.render measures itself against.

The function is patched, and COLUMNS deliberately left alone. shutil.get_terminal_size reads that variable before it measures the terminal, so exporting it would pin the width just as well. It would also resize pytest’s own reports for as long as a narrow pin is held.

The replacement accepts keyword arguments because pytest measures its progress line with shutil.get_terminal_size(fallback=(80, 24)). A positional-only stand-in raises TypeError there and aborts the run with an INTERNALERROR. See #1934.

A deliberately ragged mark: no caller should have to square its own artwork.

The same, carrying color, to prove the measurement discounts escape sequences.

The width comes from the artwork, never from the caller.

Every line is padded out to the widest, so the facts seat against a block.

Padding here rather than demanding it is what lets a caller hand over whatever its renderer produced. It cannot repair this afterwards even if it wanted to: str.ljust counts the escape sequences it cannot see, so on a styled line it silently does nothing.

tests.test_version.test_screen_padding_closes_an_open_style()[source]

Padding after an unterminated style cannot inherit it.

A mark whose last run is left open would otherwise paint its own trailing blanks, and the gutter with them, dragging a background across the facts.

tests.test_version.test_screen_facts_may_be_deferred()[source]

A callable is not called until the screen is drawn.

The point of accepting one: a CLI reporting something costly should pay for it when --version is asked for, not on every invocation that might have been.

tests.test_version.test_screen_draws_a_callable_logo_once_per_render(terminal_width)[source]

Measuring and padding the mark share one drawing of it.

tests.test_version.test_screen_sizes_its_label_column_to_the_longest_label()[source]

No declared width to outgrow: the column is measured, then gutter-separated.

tests.test_version.test_screen_facts_replace_a_default_row_in_place()[source]

| over the defaults swaps a row where it sits, and appends a new one.

tests.test_version.test_env_summary_joins_the_default_facts()[source]

The one-line summary says what the version screen’s default rows say.

tests.test_version.test_screen_declines_when_too_narrow(terminal_width)[source]
tests.test_version.test_screen_opens_on_a_blank_line_and_never_trails_whitespace(terminal_width)[source]
tests.test_version.test_screen_seats_every_fact_at_one_column(terminal_width)[source]

The facts start at the same column on every line that carries one.

tests.test_version.test_screen_uses_the_options_own_styles(terminal_width)[source]

The header is painted like the plain message, not like something new.

tests.test_version.screen_cli()[source]
tests.test_version.test_version_option_without_a_screen_is_unchanged(invoke)[source]

A CLI that never asks for a screen keeps the one-line message.

tests.test_version.test_screen_skipped_without_color(invoke, screen_cli)[source]
tests.test_version.test_screen_drawn_with_color(invoke, screen_cli, terminal_width)[source]
tests.test_version.test_screen_skipped_when_accessible(invoke, screen_cli, terminal_width)[source]
tests.test_version.test_version_message_follows_the_theme(invoke, theme, expected)[source]

The message is painted from the active palette, not from a frozen copy.

A palette captured at import would print the program name bright white under every theme, which on a light terminal is white on white.

tests.test_version.test_colors_reach_output(monkeypatch, color, stdout_is_a_tty, expected)[source]

auto reads sys.stdout, an explicit tri-state never looks at it.

The stream half is the part worth pinning: click.echo reaches stdout through a wrapper whose public accessor Click deprecates, and this resolution has to answer what echo answers without it.

tests.test_version.test_sphinx_runner_resets_version_resolution()[source]

Each documented invocation resolves --version for itself.

The resolution chain opens on a stack walk and memoizes, which suits a CLI: one invocation, one process. A documentation build renders many commands in one process, and a render carrying no CLI frame (writing roff for a command tree, say) resolves the chain from a stack the walk cannot read. That answer would stand for the rest of the build, and a later --version example would publish a screen with no version on it.

This guards the mechanism, not the build: it plants a stale resolution by hand rather than reproducing the frame shape a real sphinx-build has, so it holds ClickRunner.invoke to clearing the memo and says nothing about which renders would otherwise poison it.

tests.test_version.test_reset_resolution_clears_every_memoized_field()[source]

reset_resolution() drops the whole chain, not a chosen few fields.

tests.test_version.test_reset_resolution_keeps_a_pinned_field()[source]

A field pinned through fields= is not a resolution, so it survives.

An override lands in the instance dict under the name of the property it shadows, so a reset that goes by name alone erases it. That silently turns a pinned version into None on every documented invocation.

tests.test_version.test_deep_copy_keeps_pinned_fields_and_drops_resolved_ones()[source]

A copy carries the fields the caller pinned, and resolves the others anew.

click_extra.multicall deep-copies the options of a group to build each personality, which must report the version the group pins.