• 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

35.17
/src/python/pants/engine/internals/parser.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 inspect
1✔
7
import itertools
1✔
8
import logging
1✔
9
import re
1✔
10
import threading
1✔
11
import tokenize
1✔
12
import traceback
1✔
13
import typing
1✔
14
from collections.abc import Callable, Iterable, Mapping
1✔
15
from dataclasses import InitVar, dataclass, field
1✔
16
from difflib import get_close_matches
1✔
17
from io import StringIO
1✔
18
from pathlib import PurePath
1✔
19
from typing import Annotated, Any, TypeVar
1✔
20

21
import typing_extensions
1✔
22

23
from pants.base.deprecated import warn_or_error
1✔
24
from pants.base.exceptions import MappingError
1✔
25
from pants.base.parse_context import ParseContext
1✔
26
from pants.build_graph.build_file_aliases import BuildFileAliases
1✔
27
from pants.engine.env_vars import EnvironmentVars
1✔
28
from pants.engine.internals.defaults import BuildFileDefaultsParserState, SetDefaultsT
1✔
29
from pants.engine.internals.dep_rules import BuildFileDependencyRulesParserState
1✔
30
from pants.engine.internals.target_adaptor import TargetAdaptor
1✔
31
from pants.engine.target import Field, ImmutableValue, RegisteredTargetTypes
1✔
32
from pants.engine.unions import UnionMembership
1✔
33
from pants.util.docutil import doc_url
1✔
34
from pants.util.frozendict import FrozenDict
1✔
35
from pants.util.memo import memoized_property
1✔
36
from pants.util.strutil import docstring, softwrap, strval
1✔
37

38
logger = logging.getLogger(__name__)
1✔
39
T = TypeVar("T")
1✔
40

41

42
@dataclass(frozen=True)
1✔
43
class BuildFileSymbolsInfo:
1✔
44
    info: FrozenDict[str, BuildFileSymbolInfo]
1✔
45

46
    @classmethod
1✔
47
    def from_info(cls, *infos: Iterable[BuildFileSymbolInfo]) -> BuildFileSymbolsInfo:
1✔
UNCOV
48
        info = {}
×
UNCOV
49
        for symbol in itertools.chain.from_iterable(infos):
×
UNCOV
50
            if symbol.name not in info:
×
UNCOV
51
                info[symbol.name] = symbol
×
52
            elif symbol != info[symbol.name]:
×
53
                logger.warning(f"BUILD file symbol `{symbol.name}` conflict. Name already defined.")
×
54
                logger.debug(
×
55
                    f"BUILD file symbol conflict between:\n{info[symbol.name]} and {symbol}"
56
                )
UNCOV
57
        return cls(info=FrozenDict(info))
×
58

59
    @memoized_property
1✔
60
    def symbols(self) -> FrozenDict[str, Any]:
1✔
UNCOV
61
        return FrozenDict({name: symbol.value for name, symbol in self.info.items()})
×
62

63

64
@dataclass(frozen=True)
1✔
65
class BuildFilePreludeSymbols(BuildFileSymbolsInfo):
1✔
66
    referenced_env_vars: tuple[str, ...]
1✔
67

68
    @classmethod
1✔
69
    def create(cls, ns: Mapping[str, Any], env_vars: Iterable[str]) -> BuildFilePreludeSymbols:
1✔
UNCOV
70
        info = {}
×
UNCOV
71
        annotations_name = "__annotations__"
×
UNCOV
72
        annotations = ns.get(annotations_name, {})
×
UNCOV
73
        for name, symb in ns.items():
×
UNCOV
74
            if name == annotations_name:
×
75
                # don't include the annotations themselves as a symbol
UNCOV
76
                continue
×
77

UNCOV
78
            info[name] = BuildFileSymbolInfo(
×
79
                name,
80
                symb,
81
                # We only need type hints via `annotations` for top-level values which doesn't work with `inspect`.
82
                type_hints=annotations.get(name),
83
                # If the user has defined a _ symbol, we assume they don't want it in `pants help` output.
84
                hide_from_help=name.startswith("_"),
85
            )
86

UNCOV
87
        return cls(info=FrozenDict(info), referenced_env_vars=tuple(sorted(env_vars)))
×
88

89

90
@dataclass(frozen=True)
1✔
91
class BuildFileSymbolInfo:
1✔
92
    name: str
1✔
93
    value: Any
1✔
94
    help: str | None = field(default=None, compare=False)
1✔
95
    signature: str | None = field(default=None, compare=False, init=False)
1✔
96
    type_hints: InitVar[Any] = None
1✔
97

98
    hide_from_help: bool = False
1✔
99

100
    def __post_init__(self, type_hints: Any) -> None:
1✔
UNCOV
101
        annotated_type: type = type(self.value)
×
UNCOV
102
        help: str | None = self.help
×
UNCOV
103
        signature: str | None = None
×
104

UNCOV
105
        if type_hints is not None:
×
UNCOV
106
            if typing.get_origin(type_hints) is Annotated:
×
UNCOV
107
                annotated_type, *metadata = typing.get_args(type_hints)
×
UNCOV
108
                for meta in metadata:
×
UNCOV
109
                    if isinstance(meta, typing_extensions.Doc):
×
UNCOV
110
                        help = meta.documentation
×
UNCOV
111
                        break
×
112
            else:
UNCOV
113
                annotated_type = type_hints
×
114

UNCOV
115
        if help is None:
×
UNCOV
116
            if hasattr(self.value, "__name__"):
×
UNCOV
117
                help = inspect.getdoc(self.value)
×
118

UNCOV
119
        if self.help is None and isinstance(help, str):
×
UNCOV
120
            object.__setattr__(self, "help", softwrap(help))
×
121

UNCOV
122
        if callable(self.value):
×
UNCOV
123
            try:
×
UNCOV
124
                signature = str(inspect.signature(self.value))
×
125
            except ValueError:
×
126
                signature = None
×
127
        else:
UNCOV
128
            signature = f": {annotated_type.__name__}"
×
UNCOV
129
        object.__setattr__(self, "signature", signature)
×
130

131

132
class ParseError(Exception):
1✔
133
    """Indicates an error parsing BUILD configuration."""
134

135

136
class UnaddressableObjectError(MappingError):
1✔
137
    """Indicates an un-addressable object was found at the top level."""
138

139

140
class ParseState(threading.local):
1✔
141
    def __init__(self) -> None:
1✔
UNCOV
142
        self._defaults: BuildFileDefaultsParserState | None = None
×
UNCOV
143
        self._dependents_rules: BuildFileDependencyRulesParserState | None = None
×
UNCOV
144
        self._dependencies_rules: BuildFileDependencyRulesParserState | None = None
×
UNCOV
145
        self._filepath: str | None = None
×
UNCOV
146
        self._target_adaptors: list[TargetAdaptor] = []
×
UNCOV
147
        self._is_bootstrap: bool | None = None
×
UNCOV
148
        self._env_vars: EnvironmentVars | None = None
×
UNCOV
149
        self._symbols = tuple(
×
150
            BuildFileSymbolInfo(name, value)
151
            for name, value in (
152
                ("build_file_dir", self.build_file_dir),
153
                ("env", self.get_env),
154
                ("__defaults__", self.set_defaults),
155
                ("__dependents_rules__", self.set_dependents_rules),
156
                ("__dependencies_rules__", self.set_dependencies_rules),
157
            )
158
        )
159

160
    def reset(
1✔
161
        self,
162
        filepath: str,
163
        is_bootstrap: bool,
164
        defaults: BuildFileDefaultsParserState,
165
        dependents_rules: BuildFileDependencyRulesParserState | None,
166
        dependencies_rules: BuildFileDependencyRulesParserState | None,
167
        env_vars: EnvironmentVars,
168
    ) -> None:
UNCOV
169
        self._is_bootstrap = is_bootstrap
×
UNCOV
170
        self._defaults = defaults
×
UNCOV
171
        self._dependents_rules = dependents_rules
×
UNCOV
172
        self._dependencies_rules = dependencies_rules
×
UNCOV
173
        self._env_vars = env_vars
×
UNCOV
174
        self._filepath = filepath
×
UNCOV
175
        self._target_adaptors.clear()
×
176

177
    def add(self, target_adaptor: TargetAdaptor) -> None:
1✔
UNCOV
178
        self._target_adaptors.append(target_adaptor)
×
179

180
    def parsed_targets(self) -> list[TargetAdaptor]:
1✔
UNCOV
181
        return list(self._target_adaptors)
×
182

183
    def _prelude_check(self, name: str, value: T | None, closure_supported: bool = True) -> T:
1✔
UNCOV
184
        if value is not None:
×
UNCOV
185
            return value
×
UNCOV
186
        note = (
×
187
            (
188
                " If used in a prelude file, it must be within a function that is called from a BUILD"
189
                " file."
190
            )
191
            if closure_supported
192
            else ""
193
        )
UNCOV
194
        raise NameError(
×
195
            softwrap(
196
                f"""
197
                The BUILD file symbol `{name}` may only be used in BUILD files.{note}
198
                """
199
            )
200
        )
201

202
    def filepath(self) -> str:
1✔
UNCOV
203
        return self._prelude_check("build_file_dir", self._filepath)
×
204

205
    def build_file_dir(self) -> PurePath:
1✔
206
        """Returns the path to the directory of the current BUILD file.
207

208
        The returned value is an instance of `PurePath` to make path name manipulations easy.
209

210
        See: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath
211
        """
UNCOV
212
        return PurePath(self.filepath()).parent
×
213

214
    @property
1✔
215
    def defaults(self) -> BuildFileDefaultsParserState:
1✔
UNCOV
216
        return self._prelude_check("__defaults__", self._defaults)
×
217

218
    @property
1✔
219
    def env_vars(self) -> EnvironmentVars:
1✔
UNCOV
220
        return self._prelude_check("env", self._env_vars)
×
221

222
    @property
1✔
223
    def is_bootstrap(self) -> bool:
1✔
UNCOV
224
        if self._is_bootstrap is None:
×
225
            raise AssertionError(
×
226
                "Internal error in Pants. Please file a bug report at "
227
                "https://github.com/pantsbuild/pants/issues/new"
228
            )
UNCOV
229
        return self._is_bootstrap
×
230

231
    def get_env(self, name: str, *args, **kwargs) -> Any:
1✔
232
        """Reference environment variable."""
UNCOV
233
        return self.env_vars.get(name, *args, **kwargs)
×
234

235
    @docstring(
1✔
236
        f"""Provide default field values.
237

238
        Learn more {doc_url("docs/using-pants/key-concepts/targets-and-build-files#field-default-values")}
239
        """
240
    )
241
    def set_defaults(
1✔
242
        self,
243
        *args: SetDefaultsT,
244
        ignore_unknown_fields: bool = False,
245
        ignore_unknown_targets: bool = False,
246
        **kwargs,
247
    ) -> None:
UNCOV
248
        self.defaults.set_defaults(
×
249
            *args,
250
            ignore_unknown_fields=self.is_bootstrap or ignore_unknown_fields,
251
            ignore_unknown_targets=self.is_bootstrap or ignore_unknown_targets,
252
            **kwargs,
253
        )
254

255
    def set_dependents_rules(self, *args, **kwargs) -> None:
1✔
256
        """Declare dependents rules."""
257
        if self._dependents_rules is not None:
×
258
            self._dependents_rules.set_dependency_rules(self.filepath(), *args, **kwargs)
×
259

260
    def set_dependencies_rules(self, *args, **kwargs) -> None:
1✔
261
        """Declare dependencies rules."""
262
        if self._dependencies_rules is not None:
×
263
            self._dependencies_rules.set_dependency_rules(self.filepath(), *args, **kwargs)
×
264

265

266
class RegistrarField:
1✔
267
    __slots__ = ("_field_type", "_default")
1✔
268

269
    def __init__(self, field_type: type[Field], default: Callable[[], Any]) -> None:
1✔
UNCOV
270
        self._field_type = field_type
×
UNCOV
271
        self._default = default
×
272

273
    @property
1✔
274
    def default(self) -> ImmutableValue:
1✔
275
        return self._default()
×
276

277

278
class Registrar:
1✔
279
    def __init__(
1✔
280
        self,
281
        parse_state: ParseState,
282
        type_alias: str,
283
        field_types: tuple[type[Field], ...],
284
        help: str,
285
    ) -> None:
UNCOV
286
        self.__doc__ = help
×
UNCOV
287
        self._parse_state = parse_state
×
UNCOV
288
        self._type_alias = type_alias
×
UNCOV
289
        for field_type in field_types:
×
UNCOV
290
            registrar_field = RegistrarField(field_type, self._field_default_factory(field_type))
×
UNCOV
291
            setattr(self, field_type.alias, registrar_field)
×
UNCOV
292
            if field_type.deprecated_alias:
×
293

294
                def deprecated_field(self):
×
295
                    # TODO(17720) Support fixing automatically with `build-file` deprecation
296
                    # fixer.
297
                    warn_or_error(
×
298
                        removal_version=field_type.deprecated_alias_removal_version,
299
                        entity=f"the field name {field_type.deprecated_alias}",
300
                        hint=(
301
                            f"Instead, use `{type_alias}.{field_type.alias}`, which "
302
                            "behaves the same."
303
                        ),
304
                    )
305
                    return registrar_field
×
306

307
                setattr(self, field_type.deprecated_alias, property(deprecated_field))
×
308

309
    def __str__(self) -> str:
1✔
310
        """The BuildFileDefaultsParserState.set_defaults() rely on string inputs.
311

312
        This allows the use of the BUILD file symbols for the target types to be used un- quoted for
313
        the defaults dictionary.
314
        """
315
        return self._type_alias
×
316

317
    def __call__(self, **kwargs: Any) -> TargetAdaptor:
1✔
UNCOV
318
        if self._parse_state.is_bootstrap and any(
×
319
            isinstance(v, _UnrecognizedSymbol) for v in kwargs.values()
320
        ):
321
            # Remove any field values that are not recognized during the bootstrap phase.
322
            kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, _UnrecognizedSymbol)}
×
323

324
        # Target names default to the name of the directory their BUILD file is in
325
        # (as long as it's not the root directory).
UNCOV
326
        if "name" not in kwargs:
×
UNCOV
327
            if not self._parse_state.filepath():
×
328
                raise UnaddressableObjectError(
×
329
                    "Targets in root-level BUILD files must be named explicitly."
330
                )
UNCOV
331
            kwargs["name"] = None
×
332

UNCOV
333
        frame = inspect.currentframe()
×
UNCOV
334
        source_line = frame.f_back.f_lineno if frame and frame.f_back else "??"
×
UNCOV
335
        kwargs["__description_of_origin__"] = f"{self._parse_state.filepath()}:{source_line}"
×
UNCOV
336
        raw_values = dict(self._parse_state.defaults.get(self._type_alias))
×
UNCOV
337
        raw_values.update(kwargs)
×
UNCOV
338
        target_adaptor = TargetAdaptor(self._type_alias, **raw_values)
×
UNCOV
339
        self._parse_state.add(target_adaptor)
×
UNCOV
340
        return target_adaptor
×
341

342
    def _field_default_factory(self, field_type: type[Field]) -> Callable[[], Any]:
1✔
UNCOV
343
        def resolve_field_default() -> Any:
×
344
            target_defaults = self._parse_state.defaults.get(self._type_alias)
×
345
            if target_defaults:
×
346
                for field_alias in (field_type.alias, field_type.deprecated_alias):
×
347
                    if field_alias and field_alias in target_defaults:
×
348
                        return target_defaults[field_alias]
×
349
            return field_type.default
×
350

UNCOV
351
        return resolve_field_default
×
352

353

354
class Parser:
1✔
355
    def __init__(
1✔
356
        self,
357
        *,
358
        build_root: str,
359
        registered_target_types: RegisteredTargetTypes,
360
        union_membership: UnionMembership,
361
        object_aliases: BuildFileAliases,
362
        ignore_unrecognized_symbols: bool,
363
    ) -> None:
UNCOV
364
        self._symbols_info, self._parse_state = self._generate_symbols(
×
365
            build_root,
366
            object_aliases,
367
            registered_target_types,
368
            union_membership,
369
        )
UNCOV
370
        self.ignore_unrecognized_symbols = ignore_unrecognized_symbols
×
371

372
    @staticmethod
1✔
373
    def _generate_symbols(
1✔
374
        build_root: str,
375
        object_aliases: BuildFileAliases,
376
        registered_target_types: RegisteredTargetTypes,
377
        union_membership: UnionMembership,
378
    ) -> tuple[BuildFileSymbolsInfo, ParseState]:
379
        # N.B.: We re-use the thread local ParseState across symbols for performance reasons.
380
        # This allows a single construction of all symbols here that can be re-used for each BUILD
381
        # file parse with a reset of the ParseState for the calling thread.
UNCOV
382
        parse_state = ParseState()
×
383

UNCOV
384
        def create_registrar_for_target(alias: str) -> tuple[str, Registrar]:
×
UNCOV
385
            target_cls = registered_target_types.aliases_to_types[alias]
×
UNCOV
386
            return alias, Registrar(
×
387
                parse_state,
388
                alias,
389
                tuple(target_cls.class_field_types(union_membership)),
390
                strval(getattr(target_cls, "help", "")),
391
            )
392

UNCOV
393
        type_aliases = dict(map(create_registrar_for_target, registered_target_types.aliases))
×
UNCOV
394
        parse_context = ParseContext(
×
395
            build_root=build_root,
396
            type_aliases=type_aliases,
397
            filepath_oracle=parse_state,
398
        )
399

UNCOV
400
        symbols_info = BuildFileSymbolsInfo.from_info(
×
401
            parse_state._symbols,
402
            (
403
                BuildFileSymbolInfo(alias, registrar, help=registrar.__doc__)
404
                for alias, registrar in type_aliases.items()
405
            ),
406
            (BuildFileSymbolInfo(alias, value) for alias, value in object_aliases.objects.items()),
407
            (
408
                BuildFileSymbolInfo(alias, object_factory(parse_context))
409
                for alias, object_factory in object_aliases.context_aware_object_factories.items()
410
            ),
411
        )
412

UNCOV
413
        return symbols_info, parse_state
×
414

415
    @property
1✔
416
    def symbols_info(self) -> BuildFileSymbolsInfo:
1✔
417
        return self._symbols_info
×
418

419
    @property
1✔
420
    def symbols(self) -> FrozenDict[str, Any]:
1✔
UNCOV
421
        return self._symbols_info.symbols
×
422

423
    def parse(
1✔
424
        self,
425
        filepath: str,
426
        build_file_content: str,
427
        extra_symbols: BuildFilePreludeSymbols,
428
        env_vars: EnvironmentVars,
429
        is_bootstrap: bool,
430
        defaults: BuildFileDefaultsParserState,
431
        dependents_rules: BuildFileDependencyRulesParserState | None,
432
        dependencies_rules: BuildFileDependencyRulesParserState | None,
433
    ) -> list[TargetAdaptor]:
UNCOV
434
        self._parse_state.reset(
×
435
            filepath=filepath,
436
            is_bootstrap=is_bootstrap,
437
            defaults=defaults,
438
            dependents_rules=dependents_rules,
439
            dependencies_rules=dependencies_rules,
440
            env_vars=env_vars,
441
        )
442

UNCOV
443
        global_symbols: dict[str, Any] = {
×
444
            **self.symbols,
445
            **extra_symbols.symbols,
446
        }
447

UNCOV
448
        if self.ignore_unrecognized_symbols:
×
UNCOV
449
            defined_symbols = set()
×
UNCOV
450
            while True:
×
UNCOV
451
                try:
×
UNCOV
452
                    code = compile(build_file_content, filepath, "exec", dont_inherit=True)
×
UNCOV
453
                    exec(code, global_symbols)
×
UNCOV
454
                except NameError as e:
×
UNCOV
455
                    bad_symbol = _extract_symbol_from_name_error(e)
×
UNCOV
456
                    if bad_symbol in defined_symbols:
×
457
                        # We have previously attempted to define this symbol, but have received
458
                        # another error for it. This can indicate that the symbol is being used
459
                        # from code which has already been compiled, such as builtin functions.
460
                        raise
×
UNCOV
461
                    defined_symbols.add(bad_symbol)
×
462

UNCOV
463
                    global_symbols[bad_symbol] = _UnrecognizedSymbol(bad_symbol)
×
UNCOV
464
                    self._parse_state.reset(
×
465
                        filepath=filepath,
466
                        is_bootstrap=is_bootstrap,
467
                        defaults=defaults,
468
                        dependents_rules=dependents_rules,
469
                        dependencies_rules=dependencies_rules,
470
                        env_vars=env_vars,
471
                    )
UNCOV
472
                    continue
×
UNCOV
473
                break
×
474

UNCOV
475
            error_on_imports(build_file_content, filepath)
×
UNCOV
476
            return self._parse_state.parsed_targets()
×
477

UNCOV
478
        try:
×
UNCOV
479
            code = compile(build_file_content, filepath, "exec", dont_inherit=True)
×
UNCOV
480
            exec(code, global_symbols)
×
UNCOV
481
        except NameError as e:
×
UNCOV
482
            frame = traceback.extract_tb(e.__traceback__, limit=-1)[0]
×
UNCOV
483
            msg = (  # Capitalise first letter of NameError message.
×
484
                e.args[0][0].upper() + e.args[0][1:]
485
            )
UNCOV
486
            location = f":{frame.name}" if frame.name != "<module>" else ""
×
UNCOV
487
            original = f"{frame.filename}:{frame.lineno}{location}: {msg}"
×
UNCOV
488
            help_str = softwrap(
×
489
                f"""
490
                If you expect to see more symbols activated in the below list, refer to
491
                {doc_url("docs/using-pants/key-concepts/backends")} for all available backends to activate.
492
                """
493
            )
UNCOV
494
            valid_symbols = sorted(s for s in global_symbols.keys() if s != "__builtins__")
×
UNCOV
495
            candidates = get_close_matches(build_file_content, valid_symbols)
×
UNCOV
496
            if candidates:
×
UNCOV
497
                if len(candidates) == 1:
×
UNCOV
498
                    formatted_candidates = candidates[0]
×
UNCOV
499
                elif len(candidates) == 2:
×
UNCOV
500
                    formatted_candidates = " or ".join(candidates)
×
501
                else:
UNCOV
502
                    formatted_candidates = f"{', '.join(candidates[:-1])}, or {candidates[-1]}"
×
UNCOV
503
                help_str = f"Did you mean {formatted_candidates}?\n\n" + help_str
×
UNCOV
504
            raise ParseError(
×
505
                f"{original}.\n\n{help_str}\n\nAll registered symbols: {valid_symbols}"
506
            )
507

UNCOV
508
        error_on_imports(build_file_content, filepath)
×
UNCOV
509
        return self._parse_state.parsed_targets()
×
510

511

512
def error_on_imports(build_file_content: str, filepath: str) -> None:
1✔
513
    # This is poor sandboxing; there are many ways to get around this. But it's sufficient to tell
514
    # users who aren't malicious that they're doing something wrong, and it has a low performance
515
    # overhead.
UNCOV
516
    if "import" not in build_file_content:
×
UNCOV
517
        return
×
UNCOV
518
    io_wrapped_python = StringIO(build_file_content)
×
UNCOV
519
    for token in tokenize.generate_tokens(io_wrapped_python.readline):
×
UNCOV
520
        token_str = token[1]
×
UNCOV
521
        lineno, _ = token[2]
×
UNCOV
522
        if token_str != "import":
×
UNCOV
523
            continue
×
UNCOV
524
        raise ParseError(
×
525
            f"Import used in {filepath} at line {lineno}. Import statements are banned in "
526
            "BUILD files and macros (that act like a normal BUILD file) because they can easily "
527
            "break Pants caching and lead to stale results. "
528
            f"\n\nInstead, consider writing a plugin ({doc_url('docs/writing-plugins/overview')})."
529
        )
530

531

532
def _extract_symbol_from_name_error(err: NameError) -> str:
1✔
UNCOV
533
    result = re.match(r"^name '(\w*)'", err.args[0])
×
UNCOV
534
    if result is None:
×
535
        raise AssertionError(
×
536
            softwrap(
537
                f"""
538
                Failed to extract symbol from NameError: {err}
539

540
                Please open a bug at https://github.com/pantsbuild/pants/issues/new/choose
541
                """
542
            )
543
        )
UNCOV
544
    return result.group(1)
×
545

546

547
class _UnrecognizedSymbol:
1✔
548
    """Allows us to not choke on unrecognized symbols, including when they're called as functions.
549

550
    During bootstrap macros are not loaded and if used in field values to environment targets (which
551
    are parsed during the bootstrap phase) those fields will get instances of this class as field
552
    values.
553
    """
554

555
    def __init__(self, name: str) -> None:
1✔
UNCOV
556
        self.name = name
×
UNCOV
557
        self.args: tuple[Any, ...] = ()
×
UNCOV
558
        self.kwargs: dict[str, Any] = {}
×
559

560
    def __hash__(self) -> int:
1✔
UNCOV
561
        return hash(self.name)
×
562

563
    def __call__(self, *args, **kwargs) -> _UnrecognizedSymbol:
1✔
UNCOV
564
        self.args = args
×
UNCOV
565
        self.kwargs = kwargs
×
UNCOV
566
        return self
×
567

568
    def __eq__(self, other) -> bool:
1✔
UNCOV
569
        return (
×
570
            isinstance(other, _UnrecognizedSymbol)
571
            and other.name == self.name
572
            and other.args == self.args
573
            and other.kwargs == self.kwargs
574
        )
575

576
    def __repr__(self) -> str:
1✔
UNCOV
577
        args = ", ".join(map(repr, self.args))
×
UNCOV
578
        kwargs = ", ".join(f"{k}={v!r}" for k, v in self.kwargs.items())
×
UNCOV
579
        signature = ", ".join(s for s in (args, kwargs) if s)
×
UNCOV
580
        return f"{self.name}({signature})"
×
581

582

583
# Customize the type name presented by the InvalidFieldTypeException.
584
_UnrecognizedSymbol.__name__ = "<unrecognized symbol>"
1✔
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