• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

38.13
/src/python/pants/help/help_info_extracter.py
1
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
1✔
5

6
import ast
1✔
7
import dataclasses
1✔
8
import difflib
1✔
9
import importlib.metadata
1✔
10
import inspect
1✔
11
import itertools
1✔
12
import json
1✔
13
import re
1✔
14
from collections import defaultdict, namedtuple
1✔
15
from collections.abc import Callable, Iterator, Sequence
1✔
16
from dataclasses import dataclass
1✔
17
from enum import Enum
1✔
18
from functools import reduce
1✔
19
from itertools import chain
1✔
20
from pathlib import Path
1✔
21
from typing import Any, DefaultDict, TypeVar, Union, cast, get_type_hints
1✔
22

23
import pants.backend
1✔
24
from pants.base import deprecated
1✔
25
from pants.build_graph.build_configuration import BuildConfiguration
1✔
26
from pants.core.environments.rules import option_field_name_for
1✔
27
from pants.engine.goal import GoalSubsystem
1✔
28
from pants.engine.internals.parser import BuildFileSymbolInfo, BuildFileSymbolsInfo, Registrar
1✔
29
from pants.engine.rules import Rule, TaskRule
1✔
30
from pants.engine.target import Field, RegisteredTargetTypes, StringField, Target, TargetGenerator
1✔
31
from pants.engine.unions import UnionMembership, UnionRule, is_union
1✔
32
from pants.option.native_options import NativeOptionParser, parse_dest
1✔
33
from pants.option.option_types import OptionInfo
1✔
34
from pants.option.option_util import is_dict_option, is_list_option
1✔
35
from pants.option.options import Options
1✔
36
from pants.option.ranked_value import Rank, RankedValue
1✔
37
from pants.option.registrar import OptionRegistrar, OptionValueHistory
1✔
38
from pants.option.scope import GLOBAL_SCOPE, ScopeInfo
1✔
39
from pants.util.frozendict import LazyFrozenDict
1✔
40
from pants.util.strutil import first_paragraph, strval
1✔
41

42
T = TypeVar("T")
1✔
43

44
_ENV_SANITIZER_RE = re.compile(r"[.-]")
1✔
45

46

47
class HelpJSONEncoder(json.JSONEncoder):
1✔
48
    """Class for JSON-encoding help data (including option values).
49

50
    Note that JSON-encoded data is not intended to be decoded back. It exists purely for terminal
51
    and browser help display.
52
    """
53

54
    def default(self, o):
1✔
55
        if callable(o):
×
56
            return o.__name__
×
57
        if isinstance(o, type):
×
58
            return type.__name__
×
59
        if isinstance(o, Enum):
×
60
            return o.value
×
61
        return super().default(o)
×
62

63

64
def to_help_str(val) -> str:
1✔
UNCOV
65
    if isinstance(val, (list, dict)):
×
UNCOV
66
        return json.dumps(val, sort_keys=True, indent=2, cls=HelpJSONEncoder)
×
UNCOV
67
    if isinstance(val, Enum):
×
UNCOV
68
        return str(val.value)
×
69
    else:
UNCOV
70
        return str(val)
×
71

72

73
@dataclass(frozen=True)
1✔
74
class OptionHelpInfo:
1✔
75
    """A container for help information for a single option.
76

77
    display_args: Arg strings suitable for display in help text, including value examples
78
                  (e.g., [-f, --[no]-foo-bar, --baz=<metavar>].)
79
    comma_separated_display_args: Display args as a comma-delimited string, used in
80
                                  reference documentation.
81
    scoped_cmd_line_args: The explicitly scoped raw flag names allowed anywhere on the cmd line,
82
                          (e.g., [--scope-baz, --no-scope-baz, --scope-qux])
83
    unscoped_cmd_line_args: The unscoped raw flag names allowed on the cmd line in this option's
84
                            scope context (e.g., [--baz, --no-baz, --qux])
85
    env_var: The environment variable that set's the option.
86
    config_key: The config key for this option (in the section named for its scope).
87
    target_field_name: The name for the field that overrides this option in `local_environment`,
88
                       `remote_environment`, or `docker_environment`, if the option is environment-
89
                       aware, else `None`.
90
    typ: The type of the option.
91
    default: The value of this option if no flags are specified (derived from config and env vars).
92
    help: The help message registered for this option.
93
    deprecated_message: If deprecated: A message explaining that this option is deprecated at
94
                        removal_version.
95
    removal_version: If deprecated: The version at which this option is to be removed.
96
    removal_hint: If deprecated: The removal hint message registered for this option.
97
    choices: If this option has a constrained set of choices, a tuple of the stringified choices.
98
    fromfile: Supports reading the option value from a file when using `@filepath`.
99
    """
100

101
    display_args: tuple[str, ...]
1✔
102
    comma_separated_display_args: str
1✔
103
    scoped_cmd_line_args: tuple[str, ...]
1✔
104
    unscoped_cmd_line_args: tuple[str, ...]
1✔
105
    env_var: str
1✔
106
    config_key: str
1✔
107
    target_field_name: str | None
1✔
108
    typ: type
1✔
109
    default: Any
1✔
110
    help: str
1✔
111
    deprecation_active: bool
1✔
112
    deprecated_message: str | None
1✔
113
    removal_version: str | None
1✔
114
    removal_hint: str | None
1✔
115
    choices: tuple[str, ...] | None
1✔
116
    comma_separated_choices: str | None
1✔
117
    value_history: OptionValueHistory | None
1✔
118
    fromfile: bool
1✔
119

120

121
@dataclass(frozen=True)
1✔
122
class OptionScopeHelpInfo:
1✔
123
    """A container for help information for a scope of options.
124

125
    scope: The scope of the described options.
126
    provider: Which backend or plugin registered this scope.
127
    deprecated_scope: A deprecated scope name for this scope. A scope that has a deprecated scope
128
      will be represented by two objects - one of which will have scope==deprecated_scope.
129
    basic|advanced|deprecated: A list of OptionHelpInfo for the options in that group.
130
    """
131

132
    scope: str
1✔
133
    description: str
1✔
134
    provider: str
1✔
135
    is_goal: bool  # True iff the scope belongs to a GoalSubsystem.
1✔
136
    deprecated_scope: str | None
1✔
137
    basic: tuple[OptionHelpInfo, ...]
1✔
138
    advanced: tuple[OptionHelpInfo, ...]
1✔
139
    deprecated: tuple[OptionHelpInfo, ...]
1✔
140

141
    def is_deprecated_scope(self):
1✔
142
        """Returns True iff this scope is deprecated.
143

144
        We may choose not to show deprecated scopes when enumerating scopes, but still want to show
145
        help for individual deprecated scopes when explicitly requested.
146
        """
147
        return self.scope == self.deprecated_scope
×
148

149
    def collect_unscoped_flags(self) -> list[str]:
1✔
150
        flags: list[str] = []
×
151
        for options in (self.basic, self.advanced, self.deprecated):
×
152
            for ohi in options:
×
153
                flags.extend(ohi.unscoped_cmd_line_args)
×
154
        return flags
×
155

156
    def collect_scoped_flags(self) -> list[str]:
1✔
157
        flags: list[str] = []
×
158
        for options in (self.basic, self.advanced, self.deprecated):
×
159
            for ohi in options:
×
160
                flags.extend(ohi.scoped_cmd_line_args)
×
161
        return flags
×
162

163

164
@dataclass(frozen=True)
1✔
165
class GoalHelpInfo:
1✔
166
    """A container for help information for a goal."""
167

168
    name: str
1✔
169
    description: str
1✔
170
    provider: str
1✔
171
    is_implemented: bool  # True iff all unions required by the goal are implemented.
1✔
172
    consumed_scopes: tuple[str, ...]  # The scopes of subsystems consumed by this goal.
1✔
173

174

175
def pretty_print_type_hint(hint: Any) -> str:
1✔
UNCOV
176
    if getattr(hint, "__origin__", None) == Union:
×
UNCOV
177
        union_members = hint.__args__
×
UNCOV
178
        hint_str = " | ".join(pretty_print_type_hint(member) for member in union_members)
×
179
    # NB: Checking for GenericMeta is only for Python 3.6 because some `typing` classes like
180
    # `typing.Iterable` have its type, whereas Python 3.7+ removes it. Remove this check
181
    # once we drop support for Python 3.6.
UNCOV
182
    elif isinstance(hint, type) and not str(type(hint)) == "<class 'typing.GenericMeta'>":
×
UNCOV
183
        hint_str = hint.__name__
×
184
    else:
UNCOV
185
        hint_str = str(hint)
×
UNCOV
186
    return (
×
187
        hint_str.replace("collections.abc.", "").replace("typing.", "").replace("NoneType", "None")
188
    )
189

190

191
@dataclass(frozen=True)
1✔
192
class TargetFieldHelpInfo:
1✔
193
    """A container for help information for a field in a target type."""
194

195
    alias: str
1✔
196
    provider: str
1✔
197
    description: str
1✔
198
    type_hint: str
1✔
199
    required: bool
1✔
200
    default: str | None
1✔
201

202
    @classmethod
1✔
203
    def create(cls, field: type[Field], *, provider: str) -> TargetFieldHelpInfo:
1✔
UNCOV
204
        raw_value_type = get_type_hints(field.compute_value)["raw_value"]
×
UNCOV
205
        type_hint = pretty_print_type_hint(raw_value_type)
×
206

207
        # Check if the field only allows for certain choices.
UNCOV
208
        if issubclass(field, StringField) and field.valid_choices is not None:
×
209
            valid_choices = sorted(
×
210
                field.valid_choices
211
                if isinstance(field.valid_choices, tuple)
212
                else (choice.value for choice in field.valid_choices)
213
            )
214
            type_hint = " | ".join([*(repr(c) for c in valid_choices), "None"])
×
215

UNCOV
216
        if field.required:
×
217
            # We hackily remove `None` as a valid option for the field when it's required. This
218
            # greatly simplifies Field definitions because it means that they don't need to
219
            # override the type hints for `PrimitiveField.compute_value()` and
220
            # `AsyncField.sanitize_raw_value()` to indicate that `None` is an invalid type.
UNCOV
221
            type_hint = type_hint.replace(" | None", "")
×
222

UNCOV
223
        return cls(
×
224
            alias=field.alias,
225
            provider=provider,
226
            description=strval(field.help),
227
            type_hint=type_hint,
228
            required=field.required,
229
            default=(
230
                repr(field.default) if (not field.required and field.default is not None) else None
231
            ),
232
        )
233

234

235
@dataclass(frozen=True)
1✔
236
class TargetTypeHelpInfo:
1✔
237
    """A container for help information for a target type."""
238

239
    alias: str
1✔
240
    provider: str
1✔
241
    summary: str
1✔
242
    description: str
1✔
243
    fields: tuple[TargetFieldHelpInfo, ...]
1✔
244

245
    @classmethod
1✔
246
    def create(
1✔
247
        cls,
248
        target_type: type[Target],
249
        *,
250
        provider: str,
251
        union_membership: UnionMembership,
252
        get_field_type_provider: Callable[[type[Field]], str] | None,
253
    ) -> TargetTypeHelpInfo:
UNCOV
254
        fields = list(target_type.class_field_types(union_membership=union_membership))
×
UNCOV
255
        if issubclass(target_type, TargetGenerator):
×
256
            # NB: Even though the moved_fields will never be present on a constructed
257
            # TargetGenerator, they are legal arguments... and that is what most help consumers
258
            # are interested in.
259
            fields.extend(target_type.moved_fields)
×
UNCOV
260
        helpstr = strval(target_type.help)
×
UNCOV
261
        return cls(
×
262
            alias=target_type.alias,
263
            provider=provider,
264
            summary=first_paragraph(helpstr),
265
            description=helpstr,
266
            fields=tuple(
267
                TargetFieldHelpInfo.create(
268
                    field,
269
                    provider=(
270
                        "" if get_field_type_provider is None else get_field_type_provider(field)
271
                    ),
272
                )
273
                for field in fields
274
                if not field.alias.startswith("_") and field.removal_version is None
275
            ),
276
        )
277

278

279
def maybe_cleandoc(doc: str | None) -> str | None:
1✔
UNCOV
280
    return doc and inspect.cleandoc(doc)
×
281

282

283
@dataclass(frozen=True)
1✔
284
class RuleInfo:
1✔
285
    """A container for help information for a rule.
286

287
    The `description` is the `desc` provided to the `@rule` decorator, and `documentation` is the
288
    rule's doc string.
289
    """
290

291
    name: str
1✔
292
    description: str | None
1✔
293
    documentation: str | None
1✔
294
    provider: str
1✔
295
    output_type: str
1✔
296
    input_types: tuple[str, ...]
1✔
297
    awaitables: tuple[str, ...]
1✔
298

299
    @classmethod
1✔
300
    def create(cls, rule: TaskRule, provider: str) -> RuleInfo:
1✔
UNCOV
301
        return cls(
×
302
            name=rule.canonical_name,
303
            description=rule.desc,
304
            documentation=maybe_cleandoc(rule.func.__doc__),
305
            provider=provider,
306
            input_types=tuple(typ.__name__ for typ in rule.parameters.values()),
307
            awaitables=tuple(str(constraints) for constraints in rule.awaitables),
308
            output_type=rule.output_type.__name__,
309
        )
310

311

312
@dataclass(frozen=True)
1✔
313
class PluginAPITypeInfo:
1✔
314
    """A container for help information for a plugin API type.
315

316
    Plugin API types are used as input parameters and output results for rules.
317
    """
318

319
    name: str
1✔
320
    module: str
1✔
321
    documentation: str | None
1✔
322
    provider: tuple[str, ...]
1✔
323
    is_union: bool
1✔
324
    union_type: str | None
1✔
325
    union_members: tuple[str, ...]
1✔
326
    dependencies: tuple[str, ...]
1✔
327
    dependents: tuple[str, ...]
1✔
328
    returned_by_rules: tuple[str, ...]
1✔
329
    consumed_by_rules: tuple[str, ...]
1✔
330
    used_in_rules: tuple[str, ...]
1✔
331

332
    @property
1✔
333
    def fully_qualified_name(self) -> str:
1✔
334
        return f"{self.module}.{self.name}"
×
335

336
    @staticmethod
1✔
337
    def fully_qualified_name_from_type(t: type):
1✔
UNCOV
338
        return f"{t.__module__}.{t.__qualname__}"
×
339

340
    @classmethod
1✔
341
    def create(
1✔
342
        cls, api_type: type, rules: Sequence[Rule | UnionRule], **kwargs
343
    ) -> PluginAPITypeInfo:
UNCOV
344
        union_type: str | None = None
×
UNCOV
345
        for rule in filter(cls._member_type_for(api_type), rules):
×
346
            # All filtered rules out of `_member_type_for` will be `UnionRule`s.
347
            union_type = cast(UnionRule, rule).union_base.__name__
×
348
            break
×
349

UNCOV
350
        task_rules = [rule for rule in rules if isinstance(rule, TaskRule)]
×
351

UNCOV
352
        return cls(
×
353
            name=api_type.__qualname__,
354
            module=api_type.__module__,
355
            documentation=maybe_cleandoc(api_type.__doc__),
356
            is_union=is_union(api_type),
357
            union_type=union_type,
358
            consumed_by_rules=cls._all_rules(cls._rule_consumes(api_type), task_rules),
359
            returned_by_rules=cls._all_rules(cls._rule_returns(api_type), task_rules),
360
            used_in_rules=cls._all_rules(cls._rule_uses(api_type), task_rules),
361
            **kwargs,
362
        )
363

364
    @staticmethod
1✔
365
    def _all_rules(
1✔
366
        satisfies: Callable[[TaskRule], bool], rules: Sequence[TaskRule]
367
    ) -> tuple[str, ...]:
UNCOV
368
        return tuple(sorted(rule.canonical_name for rule in filter(satisfies, rules)))
×
369

370
    @staticmethod
1✔
371
    def _rule_consumes(api_type: type) -> Callable[[TaskRule], bool]:
1✔
UNCOV
372
        def satisfies(rule: TaskRule) -> bool:
×
UNCOV
373
            return api_type in rule.parameters.values()
×
374

UNCOV
375
        return satisfies
×
376

377
    @staticmethod
1✔
378
    def _rule_returns(api_type: type) -> Callable[[TaskRule], bool]:
1✔
UNCOV
379
        def satisfies(rule: TaskRule) -> bool:
×
UNCOV
380
            return rule.output_type is api_type
×
381

UNCOV
382
        return satisfies
×
383

384
    @staticmethod
1✔
385
    def _rule_uses(api_type: type) -> Callable[[TaskRule], bool]:
1✔
UNCOV
386
        def satisfies(rule: TaskRule) -> bool:
×
UNCOV
387
            return any(
×
388
                api_type in (*constraint.input_types, constraint.output_type)
389
                for constraint in rule.awaitables
390
            )
391

UNCOV
392
        return satisfies
×
393

394
    @staticmethod
1✔
395
    def _member_type_for(api_type: type) -> Callable[[Rule | UnionRule], bool]:
1✔
UNCOV
396
        def satisfies(rule: Rule | UnionRule) -> bool:
×
UNCOV
397
            return isinstance(rule, UnionRule) and rule.union_member is api_type
×
398

UNCOV
399
        return satisfies
×
400

401
    def merged_with(self, that: PluginAPITypeInfo) -> PluginAPITypeInfo:
1✔
402
        def merge_tuples(l, r):
×
403
            return tuple(sorted({*l, *r}))
×
404

405
        return PluginAPITypeInfo(
×
406
            self.name,
407
            self.module,
408
            self.documentation,
409
            merge_tuples(self.provider, that.provider),
410
            self.is_union,
411
            self.union_type,
412
            merge_tuples(self.union_members, that.union_members),
413
            merge_tuples(self.dependencies, that.dependencies),
414
            merge_tuples(self.dependents, that.dependents),
415
            merge_tuples(self.returned_by_rules, that.returned_by_rules),
416
            merge_tuples(self.consumed_by_rules, that.consumed_by_rules),
417
            merge_tuples(self.used_in_rules, that.used_in_rules),
418
        )
419

420

421
@dataclass(frozen=True)
1✔
422
class BackendHelpInfo:
1✔
423
    name: str
1✔
424
    description: str
1✔
425
    enabled: bool
1✔
426
    provider: str
1✔
427

428

429
@dataclass(frozen=True)
1✔
430
class BuildFileSymbolHelpInfo:
1✔
431
    name: str
1✔
432
    is_target: bool
1✔
433
    signature: str | None
1✔
434
    documentation: str | None
1✔
435

436

437
@dataclass(frozen=True)
1✔
438
class AllHelpInfo:
1✔
439
    """All available help info."""
440

441
    scope_to_help_info: LazyFrozenDict[str, OptionScopeHelpInfo]
1✔
442
    name_to_goal_info: LazyFrozenDict[str, GoalHelpInfo]
1✔
443
    name_to_target_type_info: LazyFrozenDict[str, TargetTypeHelpInfo]
1✔
444
    name_to_rule_info: LazyFrozenDict[str, RuleInfo]
1✔
445
    name_to_api_type_info: LazyFrozenDict[str, PluginAPITypeInfo]
1✔
446
    name_to_backend_help_info: LazyFrozenDict[str, BackendHelpInfo]
1✔
447
    name_to_build_file_info: LazyFrozenDict[str, BuildFileSymbolHelpInfo]
1✔
448
    env_var_to_help_info: LazyFrozenDict[str, OptionHelpInfo]
1✔
449

450
    def non_deprecated_option_scope_help_infos(self):
1✔
451
        for oshi in self.scope_to_help_info.values():
×
452
            if not oshi.is_deprecated_scope():
×
453
                yield oshi
×
454

455
    def asdict(self) -> dict[str, Any]:
1✔
UNCOV
456
        return {
×
457
            field: {thing: dataclasses.asdict(info) for thing, info in value.items()}
458
            for field, value in dataclasses.asdict(self).items()
459
        }
460

461

462
ConsumedScopesMapper = Callable[[str], tuple[str, ...]]
1✔
463

464

465
class HelpInfoExtracter:
1✔
466
    """Extracts information useful for displaying help from option registration args."""
467

468
    @classmethod
1✔
469
    def get_all_help_info(
1✔
470
        cls,
471
        options: Options,
472
        union_membership: UnionMembership,
473
        consumed_scopes_mapper: ConsumedScopesMapper,
474
        registered_target_types: RegisteredTargetTypes,
475
        build_symbols: BuildFileSymbolsInfo,
476
        build_configuration: BuildConfiguration | None = None,
477
    ) -> AllHelpInfo:
UNCOV
478
        def option_scope_help_info_loader_for(
×
479
            scope_info: ScopeInfo,
480
        ) -> Callable[[], OptionScopeHelpInfo]:
UNCOV
481
            def load() -> OptionScopeHelpInfo:
×
UNCOV
482
                subsystem_cls = scope_info.subsystem_cls
×
UNCOV
483
                if not scope_info.description:
×
484
                    cls_name = (
×
485
                        f"{subsystem_cls.__module__}.{subsystem_cls.__qualname__}"
486
                        if subsystem_cls
487
                        else ""
488
                    )
489
                    raise ValueError(
×
490
                        f"Subsystem {cls_name} with scope `{scope_info.scope}` has no description. "
491
                        f"Add a class property `help`."
492
                    )
UNCOV
493
                provider = ""
×
UNCOV
494
                if subsystem_cls is not None and build_configuration is not None:
×
UNCOV
495
                    provider = cls.get_provider(
×
496
                        build_configuration.subsystem_to_providers.get(subsystem_cls),
497
                        subsystem_cls.__module__,
498
                    )
UNCOV
499
                return HelpInfoExtracter(scope_info.scope).get_option_scope_help_info(
×
500
                    scope_info.description,
501
                    options.get_registrar(scope_info.scope),
502
                    options.native_parser.with_derivation(),
503
                    # `filter` should be treated as a subsystem for `help`, even though it still
504
                    # works as a goal for backwards compatibility.
505
                    scope_info.is_goal if scope_info.scope != "filter" else False,
506
                    provider,
507
                    scope_info.deprecated_scope,
508
                )
509

UNCOV
510
            return load
×
511

UNCOV
512
        def goal_help_info_loader_for(scope_info: ScopeInfo) -> Callable[[], GoalHelpInfo]:
×
UNCOV
513
            def load() -> GoalHelpInfo:
×
UNCOV
514
                subsystem_cls = scope_info.subsystem_cls
×
UNCOV
515
                assert subsystem_cls is not None
×
UNCOV
516
                if build_configuration is not None:
×
UNCOV
517
                    provider = cls.get_provider(
×
518
                        build_configuration.subsystem_to_providers.get(subsystem_cls),
519
                        subsystem_cls.__module__,
520
                    )
UNCOV
521
                goal_subsystem_cls = cast(type[GoalSubsystem], subsystem_cls)
×
UNCOV
522
                return GoalHelpInfo(
×
523
                    goal_subsystem_cls.name,
524
                    scope_info.description,
525
                    provider,
526
                    goal_subsystem_cls.activated(union_membership),
527
                    consumed_scopes_mapper(scope_info.scope),
528
                )
529

UNCOV
530
            return load
×
531

UNCOV
532
        def target_type_info_for(target_type: type[Target]) -> Callable[[], TargetTypeHelpInfo]:
×
UNCOV
533
            def load() -> TargetTypeHelpInfo:
×
UNCOV
534
                return TargetTypeHelpInfo.create(
×
535
                    target_type,
536
                    union_membership=union_membership,
537
                    provider=cls.get_provider(
538
                        build_configuration
539
                        and build_configuration.target_type_to_providers.get(target_type)
540
                        or None,
541
                        target_type.__module__,
542
                    ),
543
                    get_field_type_provider=lambda field_type: cls.get_provider(
544
                        (
545
                            build_configuration.union_rule_to_providers.get(
546
                                UnionRule(target_type.PluginField, field_type)
547
                            )
548
                            if build_configuration is not None
549
                            else None
550
                        ),
551
                        field_type.__module__,
552
                    ),
553
                )
554

UNCOV
555
            return load
×
556

UNCOV
557
        def lazily(value: T) -> Callable[[], T]:
×
UNCOV
558
            return lambda: value
×
559

UNCOV
560
        known_scope_infos = sorted(options.known_scope_to_info.values(), key=lambda x: x.scope)
×
UNCOV
561
        scope_to_help_info = LazyFrozenDict(
×
562
            {
563
                scope_info.scope: option_scope_help_info_loader_for(scope_info)
564
                for scope_info in known_scope_infos
565
                if not scope_info.scope.startswith("_")
566
            }
567
        )
568

UNCOV
569
        env_var_to_help_info = LazyFrozenDict(
×
570
            {
571
                ohi.env_var: lazily(ohi)
572
                for oshi in scope_to_help_info.values()
573
                for ohi in chain(oshi.basic, oshi.advanced, oshi.deprecated)
574
            }
575
        )
576

UNCOV
577
        name_to_goal_info = LazyFrozenDict(
×
578
            {
579
                scope_info.scope: goal_help_info_loader_for(scope_info)
580
                for scope_info in known_scope_infos
581
                if (
582
                    scope_info.is_goal
583
                    and not scope_info.scope.startswith("_")
584
                    # `filter` should be treated as a subsystem for `help`, even though it still
585
                    # works as a goal for backwards compatibility.
586
                    and scope_info.scope != "filter"
587
                )
588
            }
589
        )
590

UNCOV
591
        name_to_target_type_info = LazyFrozenDict(
×
592
            {
593
                alias: target_type_info_for(target_type)
594
                for alias, target_type in registered_target_types.aliases_to_types.items()
595
                if (target_type.removal_version is None and alias != target_type.deprecated_alias)
596
            }
597
        )
598

UNCOV
599
        return AllHelpInfo(
×
600
            scope_to_help_info=scope_to_help_info,
601
            name_to_goal_info=name_to_goal_info,
602
            name_to_target_type_info=name_to_target_type_info,
603
            name_to_rule_info=cls.get_rule_infos(build_configuration),
604
            name_to_api_type_info=cls.get_api_type_infos(build_configuration, union_membership),
605
            name_to_backend_help_info=cls.get_backend_help_info(options),
606
            name_to_build_file_info=cls.get_build_file_info(build_symbols),
607
            env_var_to_help_info=env_var_to_help_info,
608
        )
609

610
    @staticmethod
1✔
611
    def compute_default(**kwargs) -> Any:
1✔
612
        """Compute the default val for help display for an option registered with these kwargs."""
613
        # If the kwargs already determine a string representation of the default for use in help
614
        # messages, use that.
UNCOV
615
        default_help_repr = kwargs.get("default_help_repr")
×
UNCOV
616
        if default_help_repr is not None:
×
UNCOV
617
            return str(default_help_repr)  # Should already be a string, but might as well be safe.
×
618

UNCOV
619
        default = kwargs.get("default")
×
UNCOV
620
        if default is not None:
×
UNCOV
621
            return default
×
UNCOV
622
        if is_list_option(kwargs):
×
UNCOV
623
            return []
×
UNCOV
624
        elif is_dict_option(kwargs):
×
UNCOV
625
            return {}
×
UNCOV
626
        return None
×
627

628
    @staticmethod
1✔
629
    def stringify_type(t: type) -> str:
1✔
UNCOV
630
        if t == dict:
×
UNCOV
631
            return "{'key1': val1, 'key2': val2, ...}"
×
UNCOV
632
        return f"<{t.__name__}>"
×
633

634
    @staticmethod
1✔
635
    def compute_metavar(kwargs):
1✔
636
        """Compute the metavar to display in help for an option registered with these kwargs."""
637

UNCOV
638
        stringify = HelpInfoExtracter.stringify_type
×
639

UNCOV
640
        metavar = kwargs.get("metavar")
×
UNCOV
641
        if not metavar:
×
UNCOV
642
            if is_list_option(kwargs):
×
UNCOV
643
                member_typ = kwargs.get("member_type", str)
×
UNCOV
644
                metavar = stringify(member_typ)
×
645
                # In a cmd-line list literal, string members must be quoted.
UNCOV
646
                if member_typ == str:
×
UNCOV
647
                    metavar = f"'{metavar}'"
×
UNCOV
648
            elif is_dict_option(kwargs):
×
UNCOV
649
                metavar = f'"{stringify(dict)}"'
×
650
            else:
UNCOV
651
                metavar = stringify(kwargs.get("type", str))
×
UNCOV
652
        if is_list_option(kwargs):
×
653
            # For lists, the metavar (either explicit or deduced) is the representation
654
            # of a single list member, so we turn the help string into a list of those here.
UNCOV
655
            return f'"[{metavar}, {metavar}, ...]"'
×
UNCOV
656
        return metavar
×
657

658
    @staticmethod
1✔
659
    def compute_choices(kwargs) -> tuple[str, ...] | None:
1✔
660
        """Compute the option choices to display."""
UNCOV
661
        typ = kwargs.get("type", [])
×
UNCOV
662
        member_type = kwargs.get("member_type", str)
×
UNCOV
663
        if typ == list and inspect.isclass(member_type) and issubclass(member_type, Enum):
×
UNCOV
664
            return tuple(choice.value for choice in member_type)
×
UNCOV
665
        elif inspect.isclass(typ) and issubclass(typ, Enum):
×
UNCOV
666
            return tuple(choice.value for choice in typ)
×
UNCOV
667
        elif "choices" in kwargs:
×
UNCOV
668
            return tuple(str(choice) for choice in kwargs["choices"])
×
669
        else:
UNCOV
670
            return None
×
671

672
    @staticmethod
1✔
673
    def get_provider(providers: tuple[str, ...] | None, hint: str | None = None) -> str:
1✔
674
        """Get the best match for the provider.
675

676
        To take advantage of provider names being modules, `hint` should be something like the a
677
        subsystem's `__module__` or a rule's `canonical_name`.
678
        """
UNCOV
679
        if not providers:
×
UNCOV
680
            return ""
×
UNCOV
681
        if not hint:
×
682
            # Pick the shortest backend name.
683
            return sorted(providers, key=len)[0]
×
684

685
        # Pick the one closest to `hint`.
686
        # No cutoff and max 1 result guarantees a result with a single element.
UNCOV
687
        [provider] = difflib.get_close_matches(hint, providers, n=1, cutoff=0.0)
×
UNCOV
688
        return provider
×
689

690
    @staticmethod
1✔
691
    def maybe_cleandoc(doc: str | None) -> str | None:
1✔
692
        return doc and inspect.cleandoc(doc)
×
693

694
    @staticmethod
1✔
695
    def get_module_docstring(filename: str) -> str:
1✔
UNCOV
696
        with open(filename) as fd:
×
UNCOV
697
            return ast.get_docstring(ast.parse(fd.read(), filename)) or ""
×
698

699
    @classmethod
1✔
700
    def get_rule_infos(
1✔
701
        cls, build_configuration: BuildConfiguration | None
702
    ) -> LazyFrozenDict[str, RuleInfo]:
UNCOV
703
        if build_configuration is None:
×
704
            return LazyFrozenDict({})
×
705

UNCOV
706
        def rule_info_loader(rule: TaskRule, provider: str) -> Callable[[], RuleInfo]:
×
UNCOV
707
            def load() -> RuleInfo:
×
UNCOV
708
                return RuleInfo.create(rule, provider)
×
709

UNCOV
710
            return load
×
711

UNCOV
712
        return LazyFrozenDict(
×
713
            {
714
                rule.canonical_name: rule_info_loader(
715
                    rule, cls.get_provider(providers, rule.canonical_name)
716
                )
717
                for rule, providers in build_configuration.rule_to_providers.items()
718
                if isinstance(rule, TaskRule)
719
            }
720
        )
721

722
    @classmethod
1✔
723
    def get_api_type_infos(
1✔
724
        cls, build_configuration: BuildConfiguration | None, union_membership: UnionMembership
725
    ) -> LazyFrozenDict[str, PluginAPITypeInfo]:
UNCOV
726
        if build_configuration is None:
×
727
            return LazyFrozenDict({})
×
728

729
        # The type narrowing achieved with the above `if` does not extend to the created closures of
730
        # the nested functions in this body, so instead we capture it in a new variable.
UNCOV
731
        bc = build_configuration
×
732

UNCOV
733
        known_providers = cast(
×
734
            "set[str]",
735
            set(
736
                chain.from_iterable(
737
                    chain.from_iterable(cast(dict, providers).values())
738
                    for providers in (
739
                        bc.subsystem_to_providers,
740
                        bc.target_type_to_providers,
741
                        bc.rule_to_providers,
742
                        bc.union_rule_to_providers,
743
                    )
744
                )
745
            ),
746
        )
747

UNCOV
748
        def _find_provider(api_type: type) -> str:
×
UNCOV
749
            provider = api_type.__module__
×
UNCOV
750
            while provider:
×
UNCOV
751
                if provider in known_providers:
×
752
                    return provider
×
UNCOV
753
                if "." not in provider:
×
UNCOV
754
                    break
×
UNCOV
755
                provider = provider.rsplit(".", 1)[0]
×
756
            # Unknown provider, depend directly on the type's module.
UNCOV
757
            return api_type.__module__
×
758

UNCOV
759
        def _rule_dependencies(rule: TaskRule) -> Iterator[type]:
×
UNCOV
760
            yield from rule.parameters.values()
×
UNCOV
761
            for constraint in rule.awaitables:
×
UNCOV
762
                yield constraint.output_type
×
763

UNCOV
764
        def _extract_api_types() -> Iterator[tuple[type, str, tuple[type, ...]]]:
×
765
            """Return all possible types we encounter in all known rules with provider and
766
            dependencies."""
UNCOV
767
            for rule, providers in bc.rule_to_providers.items():
×
UNCOV
768
                if not isinstance(rule, TaskRule):
×
UNCOV
769
                    continue
×
UNCOV
770
                provider = cls.get_provider(providers, rule.canonical_name)
×
UNCOV
771
                yield rule.output_type, provider, tuple(_rule_dependencies(rule))
×
772

UNCOV
773
                for constraint in rule.awaitables:
×
774
                    # TODO: For call-by-name, constraint.input_types only lists the types
775
                    # provided explicitly in a type map passed to **implicitly. Positional
776
                    # arguments don't appear in input_types. So this is no longer a robust
777
                    # way to get all the types. However, if a type is also available as
778
                    # some other rule's output type, we will still see it. This covers
779
                    # the important case for help output, namely "which rule should I
780
                    # call by name to get an instance of this type?"
UNCOV
781
                    for input_type in constraint.input_types:
×
UNCOV
782
                        yield input_type, _find_provider(input_type), ()
×
783

UNCOV
784
            union_bases: set[type] = set()
×
UNCOV
785
            for union_rule, providers in bc.union_rule_to_providers.items():
×
UNCOV
786
                provider = cls.get_provider(providers, union_rule.union_member.__module__)
×
UNCOV
787
                union_bases.add(union_rule.union_base)
×
UNCOV
788
                yield union_rule.union_member, provider, (union_rule.union_base,)
×
789

UNCOV
790
            for union_base in union_bases:
×
UNCOV
791
                yield union_base, _find_provider(union_base), ()
×
792

UNCOV
793
        all_types_with_dependencies = list(_extract_api_types())
×
UNCOV
794
        all_types = {api_type for api_type, _, _ in all_types_with_dependencies}
×
UNCOV
795
        type_graph: DefaultDict[type, dict[str, tuple[str, ...]]] = defaultdict(dict)
×
796

797
        # Calculate type graph.
UNCOV
798
        for api_type in all_types:
×
799
            # Collect all providers first, as we need them up-front for the dependencies/dependents.
UNCOV
800
            type_graph[api_type]["providers"] = tuple(
×
801
                sorted(
802
                    {
803
                        provider
804
                        for a_type, provider, _ in all_types_with_dependencies
805
                        if provider and a_type is api_type
806
                    }
807
                )
808
            )
809

UNCOV
810
        for api_type in all_types:
×
811
            # Resolve type dependencies to providers.
UNCOV
812
            type_graph[api_type]["dependencies"] = tuple(
×
813
                sorted(
814
                    set(
815
                        chain.from_iterable(
816
                            type_graph[dependency].setdefault(
817
                                "providers", (_find_provider(dependency),)
818
                            )
819
                            for a_type, _, dependencies in all_types_with_dependencies
820
                            if a_type is api_type
821
                            for dependency in dependencies
822
                        )
823
                    )
824
                    - set(
825
                        # Exclude providers from list of dependencies.
826
                        type_graph[api_type]["providers"]
827
                    )
828
                )
829
            )
830

831
        # Add a dependent on the target type for each dependency.
UNCOV
832
        type_dependents: DefaultDict[type, set[str]] = defaultdict(set)
×
UNCOV
833
        for _, provider, dependencies in all_types_with_dependencies:
×
UNCOV
834
            if not provider:
×
835
                continue
×
UNCOV
836
            for target_type in dependencies:
×
UNCOV
837
                type_dependents[target_type].add(provider)
×
UNCOV
838
        for api_type, dependents in type_dependents.items():
×
UNCOV
839
            type_graph[api_type]["dependents"] = tuple(
×
840
                sorted(
841
                    dependents
842
                    - set(
843
                        # Exclude providers from list of dependents.
844
                        type_graph[api_type]["providers"][0]
845
                    )
846
                )
847
            )
848

UNCOV
849
        rules = cast(
×
850
            "tuple[Rule | UnionRule]",
851
            tuple(
852
                chain(
853
                    bc.rule_to_providers.keys(),
854
                    bc.union_rule_to_providers.keys(),
855
                )
856
            ),
857
        )
858

UNCOV
859
        def get_api_type_info(api_types: tuple[type, ...]):
×
860
            """Gather the info from each of the types and aggregate it.
861

862
            The gathering is the expensive operation, and we can only aggregate once we've gathered.
863
            """
864

UNCOV
865
            def load() -> PluginAPITypeInfo:
×
UNCOV
866
                gatherered_infos = [
×
867
                    PluginAPITypeInfo.create(
868
                        api_type,
869
                        rules,
870
                        provider=type_graph[api_type]["providers"],
871
                        dependencies=type_graph[api_type]["dependencies"],
872
                        dependents=type_graph[api_type].get("dependents", ()),
873
                        union_members=tuple(
874
                            sorted(member.__qualname__ for member in union_membership.get(api_type))
875
                        ),
876
                    )
877
                    for api_type in api_types
878
                ]
UNCOV
879
                return reduce(lambda x, y: x.merged_with(y), gatherered_infos)
×
880

UNCOV
881
            return load
×
882

883
        # We want to provide a lazy dict so we don't spend so long doing the info gathering.
884
        # We provide a list of the types here, and the lookup function performs the gather and the aggregation
UNCOV
885
        api_type_name = PluginAPITypeInfo.fully_qualified_name_from_type
×
UNCOV
886
        all_types_sorted = sorted(all_types, key=api_type_name)
×
UNCOV
887
        infos: dict[str, Callable[[], PluginAPITypeInfo]] = {
×
888
            k: get_api_type_info(tuple(v))
889
            for k, v in itertools.groupby(all_types_sorted, key=api_type_name)
890
        }
891

UNCOV
892
        return LazyFrozenDict(infos)
×
893

894
    @classmethod
1✔
895
    def get_backend_help_info(cls, options: Options) -> LazyFrozenDict[str, BackendHelpInfo]:
1✔
UNCOV
896
        DiscoveredBackend = namedtuple(
×
897
            "DiscoveredBackend", "provider, name, register_py, enabled", defaults=(False,)
898
        )
899

UNCOV
900
        def discover_source_backends(root: Path, is_source_root: bool) -> set[DiscoveredBackend]:
×
UNCOV
901
            provider = root.name
×
UNCOV
902
            source_root = f"{root}/" if is_source_root else f"{root.parent}/"
×
UNCOV
903
            register_pys = root.glob("**/register.py")
×
UNCOV
904
            backends = {
×
905
                DiscoveredBackend(
906
                    provider,
907
                    str(register_py.parent).replace(source_root, "").replace("/", "."),
908
                    str(register_py),
909
                )
910
                for register_py in register_pys
911
            }
UNCOV
912
            return backends
×
913

UNCOV
914
        def discover_plugin_backends(entry_point_name: str) -> frozenset[DiscoveredBackend]:
×
UNCOV
915
            def get_dist_file(dist: importlib.metadata.Distribution, file_path: str):
×
UNCOV
916
                try:
×
UNCOV
917
                    return dist.locate_file(file_path)
×
918
                except Exception as e:
×
919
                    raise ValueError(
×
920
                        f"Failed to locate file `{file_path}` in distribution `{dist}`: {e}"
921
                    )
922

UNCOV
923
            backends = frozenset(
×
924
                DiscoveredBackend(
925
                    entry_point.dist.name,
926
                    entry_point.module,
927
                    str(
928
                        get_dist_file(
929
                            entry_point.dist, entry_point.module.replace(".", "/") + ".py"
930
                        )
931
                    ),
932
                    True,
933
                )
934
                for entry_point in importlib.metadata.entry_points().select(group=entry_point_name)
935
                if entry_point.dist is not None
936
            )
UNCOV
937
            return backends
×
938

UNCOV
939
        global_options = options.for_global_scope()
×
UNCOV
940
        builtin_backends = discover_source_backends(
×
941
            Path(pants.backend.__file__).parent.parent, is_source_root=False
942
        )
UNCOV
943
        inrepo_backends = chain.from_iterable(
×
944
            discover_source_backends(Path(path_entry), is_source_root=True)
945
            for path_entry in global_options.pythonpath
946
        )
UNCOV
947
        plugin_backends = discover_plugin_backends("pantsbuild.plugin")
×
UNCOV
948
        enabled_backends = {
×
949
            "pants.core",
950
            "pants.backend.project_info",
951
            *global_options.backend_packages,
952
        }
953

UNCOV
954
        def get_backend_help_info_loader(
×
955
            discovered_backend: DiscoveredBackend,
956
        ) -> Callable[[], BackendHelpInfo]:
UNCOV
957
            def load() -> BackendHelpInfo:
×
UNCOV
958
                return BackendHelpInfo(
×
959
                    name=discovered_backend.name,
960
                    description=cls.get_module_docstring(discovered_backend.register_py),
961
                    enabled=(
962
                        discovered_backend.enabled or discovered_backend.name in enabled_backends
963
                    ),
964
                    provider=discovered_backend.provider,
965
                )
966

UNCOV
967
            return load
×
968

UNCOV
969
        return LazyFrozenDict(
×
970
            {
971
                discovered_backend.name: get_backend_help_info_loader(discovered_backend)
972
                for discovered_backend in sorted(
973
                    chain(builtin_backends, inrepo_backends, plugin_backends)
974
                )
975
            }
976
        )
977

978
    @staticmethod
1✔
979
    def get_build_file_info(
1✔
980
        build_symbols: BuildFileSymbolsInfo,
981
    ) -> LazyFrozenDict[str, BuildFileSymbolHelpInfo]:
UNCOV
982
        def get_build_file_symbol_help_info_loader(
×
983
            symbol: BuildFileSymbolInfo,
984
        ) -> Callable[[], BuildFileSymbolHelpInfo]:
UNCOV
985
            def load() -> BuildFileSymbolHelpInfo:
×
UNCOV
986
                return BuildFileSymbolHelpInfo(
×
987
                    name=symbol.name,
988
                    is_target=isinstance(symbol.value, Registrar),
989
                    signature=symbol.signature,
990
                    documentation=symbol.help,
991
                )
992

UNCOV
993
            return load
×
994

UNCOV
995
        return LazyFrozenDict(
×
996
            {
997
                symbol.name: get_build_file_symbol_help_info_loader(symbol)
998
                for symbol in build_symbols.info.values()
999
                # (NB. we don't just check name.startswith("_") because there's symbols like
1000
                # __default__ that should appear in help & docs)
1001
                if not symbol.hide_from_help
1002
            }
1003
        )
1004

1005
    def __init__(self, scope: str):
1✔
UNCOV
1006
        self._scope = scope
×
UNCOV
1007
        self._scope_prefix = scope.replace(".", "-")
×
1008

1009
    def get_option_scope_help_info(
1✔
1010
        self,
1011
        description: str,
1012
        registrar: OptionRegistrar,
1013
        native_parser: NativeOptionParser,
1014
        is_goal: bool,
1015
        provider: str = "",
1016
        deprecated_scope: str | None = None,
1017
    ) -> OptionScopeHelpInfo:
1018
        """Returns an OptionScopeHelpInfo for the options parsed by the given parser."""
1019

UNCOV
1020
        basic_options = []
×
UNCOV
1021
        advanced_options = []
×
UNCOV
1022
        deprecated_options = []
×
UNCOV
1023
        for option_info in registrar.option_registrations_iter():
×
UNCOV
1024
            derivation = native_parser.get_derivation(registrar.scope, option_info)
×
1025
            # Massage the derivation structure returned by the NativeOptionParser into an
1026
            # OptionValueHistory as returned by the legacy parser.
1027
            # TODO: Once we get rid of the legacy parser we can probably simplify by
1028
            #  using the native structure directly.
UNCOV
1029
            ranked_values = []
×
1030

1031
            # Adding this constant, empty history entry is silly, but it appears in the
1032
            # legacy parser's results as an implementation artifact, and we want to be
1033
            # consistent with its tests until we get rid of it.
UNCOV
1034
            is_list = option_info.kwargs.get("type") == list
×
UNCOV
1035
            is_dict = option_info.kwargs.get("type") == dict
×
UNCOV
1036
            empty_val: list | dict | None = [] if is_list else {} if is_dict else None
×
UNCOV
1037
            empty_details = "" if (is_list or is_dict) else None
×
UNCOV
1038
            ranked_values.append(RankedValue(Rank.NONE, empty_val, empty_details))
×
1039

UNCOV
1040
            for value, rank, details in derivation:
×
UNCOV
1041
                ranked_values.append(RankedValue(rank, value, details or empty_details))
×
UNCOV
1042
            history = OptionValueHistory(tuple(ranked_values))
×
UNCOV
1043
            ohi = self.get_option_help_info(option_info)
×
UNCOV
1044
            ohi = dataclasses.replace(ohi, value_history=history)
×
UNCOV
1045
            if ohi.deprecation_active:
×
UNCOV
1046
                deprecated_options.append(ohi)
×
UNCOV
1047
            elif option_info.kwargs.get("advanced"):
×
UNCOV
1048
                advanced_options.append(ohi)
×
1049
            else:
UNCOV
1050
                basic_options.append(ohi)
×
1051

UNCOV
1052
        return OptionScopeHelpInfo(
×
1053
            scope=self._scope,
1054
            description=description,
1055
            provider=provider,
1056
            is_goal=is_goal,
1057
            deprecated_scope=deprecated_scope,
1058
            basic=tuple(basic_options),
1059
            advanced=tuple(advanced_options),
1060
            deprecated=tuple(deprecated_options),
1061
        )
1062

1063
    def get_option_help_info(self, option_info: OptionInfo) -> OptionHelpInfo:
1✔
1064
        """Returns an OptionHelpInfo for the option registered with the given (args, kwargs)."""
UNCOV
1065
        display_args = []
×
UNCOV
1066
        scoped_cmd_line_args = []
×
UNCOV
1067
        unscoped_cmd_line_args = []
×
1068

UNCOV
1069
        for arg in option_info.args:
×
UNCOV
1070
            is_short_arg = len(arg) == 2
×
UNCOV
1071
            unscoped_cmd_line_args.append(arg)
×
UNCOV
1072
            if self._scope_prefix:
×
UNCOV
1073
                scoped_arg = f"--{self._scope_prefix}-{arg.lstrip('-')}"
×
1074
            else:
UNCOV
1075
                scoped_arg = arg
×
UNCOV
1076
            scoped_cmd_line_args.append(scoped_arg)
×
1077

UNCOV
1078
            if OptionRegistrar.is_bool(option_info.kwargs):
×
UNCOV
1079
                if is_short_arg:
×
UNCOV
1080
                    display_args.append(scoped_arg)
×
1081
                else:
UNCOV
1082
                    unscoped_cmd_line_args.append(f"--no-{arg[2:]}")
×
UNCOV
1083
                    sa_2 = scoped_arg[2:]
×
UNCOV
1084
                    scoped_cmd_line_args.append(f"--no-{sa_2}")
×
UNCOV
1085
                    display_args.append(f"--[no-]{sa_2}")
×
1086
            else:
UNCOV
1087
                metavar = self.compute_metavar(option_info.kwargs)
×
UNCOV
1088
                separator = "" if is_short_arg else "="
×
UNCOV
1089
                display_args.append(f"{scoped_arg}{separator}{metavar}")
×
UNCOV
1090
                if option_info.kwargs.get("passthrough"):
×
UNCOV
1091
                    type_str = self.stringify_type(option_info.kwargs.get("member_type", str))
×
UNCOV
1092
                    display_args.append(f"... -- [{type_str} [{type_str} [...]]]")
×
1093

UNCOV
1094
        typ = option_info.kwargs.get("type", str)
×
UNCOV
1095
        default = self.compute_default(**option_info.kwargs)
×
UNCOV
1096
        help_msg = option_info.kwargs.get("help", "No help available.")
×
UNCOV
1097
        deprecation_start_version = option_info.kwargs.get("deprecation_start_version")
×
UNCOV
1098
        removal_version = option_info.kwargs.get("removal_version")
×
UNCOV
1099
        deprecation_active = removal_version is not None and deprecated.is_deprecation_active(
×
1100
            deprecation_start_version
1101
        )
UNCOV
1102
        deprecated_message = None
×
UNCOV
1103
        if removal_version:
×
UNCOV
1104
            deprecated_tense = deprecated.get_deprecated_tense(removal_version)
×
UNCOV
1105
            message_start = (
×
1106
                "Deprecated"
1107
                if deprecation_active
1108
                else f"Upcoming deprecation in version: {deprecation_start_version}"
1109
            )
UNCOV
1110
            deprecated_message = (
×
1111
                f"{message_start}, {deprecated_tense} removed in version: {removal_version}."
1112
            )
UNCOV
1113
        removal_hint = option_info.kwargs.get("removal_hint")
×
UNCOV
1114
        choices = self.compute_choices(option_info.kwargs)
×
1115

UNCOV
1116
        dest = parse_dest(option_info)
×
UNCOV
1117
        udest = dest.upper()
×
UNCOV
1118
        if self._scope == GLOBAL_SCOPE:
×
1119
            # Global options have 2-3 env var variants, e.g., --pants-workdir can be
1120
            # set with PANTS_GLOBAL_PANTS_WORKDIR, PANTS_PANTS_WORKDIR, or PANTS_WORKDIR.
1121
            # The last one is the most human-friendly, so it's what we use in the help info.
UNCOV
1122
            if udest.startswith("PANTS_"):
×
UNCOV
1123
                env_var = udest
×
1124
            else:
UNCOV
1125
                env_var = f"PANTS_{udest}"
×
1126
        else:
UNCOV
1127
            env_var = f"PANTS_{_ENV_SANITIZER_RE.sub('_', self._scope.upper())}_{udest}"
×
1128

UNCOV
1129
        target_field_name = (
×
1130
            f"{self._scope_prefix}_{option_field_name_for(option_info.args)}".replace("-", "_")
1131
        )
UNCOV
1132
        environment_aware = option_info.kwargs.get("environment_aware") is True
×
UNCOV
1133
        fromfile = option_info.kwargs.get("fromfile", False)
×
1134

UNCOV
1135
        ret = OptionHelpInfo(
×
1136
            display_args=tuple(display_args),
1137
            comma_separated_display_args=", ".join(display_args),
1138
            scoped_cmd_line_args=tuple(scoped_cmd_line_args),
1139
            unscoped_cmd_line_args=tuple(unscoped_cmd_line_args),
1140
            env_var=env_var,
1141
            config_key=dest,
1142
            target_field_name=target_field_name if environment_aware else None,
1143
            typ=typ,
1144
            default=default,
1145
            help=help_msg,
1146
            deprecation_active=deprecation_active,
1147
            deprecated_message=deprecated_message,
1148
            removal_version=removal_version,
1149
            removal_hint=removal_hint,
1150
            choices=choices,
1151
            comma_separated_choices=None if choices is None else ", ".join(choices),
1152
            value_history=None,
1153
            fromfile=fromfile,
1154
        )
UNCOV
1155
        return ret
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc