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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

71.53
/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
4✔
5

6
import inspect
4✔
7
import itertools
4✔
8
import logging
4✔
9
import re
4✔
10
import threading
4✔
11
import tokenize
4✔
12
import traceback
4✔
13
import typing
4✔
14
from collections.abc import Callable, Iterable, Mapping
4✔
15
from dataclasses import InitVar, dataclass, field
4✔
16
from difflib import get_close_matches
4✔
17
from io import StringIO
4✔
18
from pathlib import PurePath
4✔
19
from typing import Annotated, Any, TypeVar
4✔
20

21
import typing_extensions
4✔
22
from annotationlib import Format, ForwardRef, call_annotate_function
4✔
23

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

39
logger = logging.getLogger(__name__)
4✔
40
T = TypeVar("T")
4✔
41

42

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

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

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

64

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

69
    @classmethod
4✔
70
    def create(cls, ns: Mapping[str, Any], env_vars: Iterable[str]) -> BuildFilePreludeSymbols:
4✔
71
        annotations: dict[str, Any | ForwardRef] = {}
4✔
72
        if annotation_function := ns.get("__annotate__"):
4✔
UNCOV
73
            annotations = call_annotate_function(annotation_function, format=Format.FORWARDREF)
×
74

75
        info = {
4✔
76
            name: BuildFileSymbolInfo(
77
                name,
78
                symb,
79
                # We only need type hints via `annotations` for top-level values which doesn't work with `inspect`.
80
                type_hints=annotations.get(name),
81
                # If the user has defined a _ symbol, we assume they don't want it in `pants help` output.
82
                hide_from_help=name.startswith("_"),
83
            )
84
            for name, symb in ns.items()
85
            if name not in ["__annotate__", "__annotations__", "__conditional_annotations__"]
86
        }
87

88
        return cls(info=FrozenDict(info), referenced_env_vars=tuple(sorted(env_vars)))
4✔
89

90

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

99
    hide_from_help: bool = False
4✔
100

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

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

116
        if help is None:
4✔
117
            if hasattr(self.value, "__name__"):
4✔
118
                help = inspect.getdoc(self.value)
4✔
119

120
        if self.help is None and isinstance(help, str):
4✔
121
            object.__setattr__(self, "help", softwrap(help))
4✔
122

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

132

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

136

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

140

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

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

178
    def add(self, target_adaptor: TargetAdaptor) -> None:
4✔
179
        self._target_adaptors.append(target_adaptor)
4✔
180

181
    def parsed_targets(self) -> list[TargetAdaptor]:
4✔
182
        return list(self._target_adaptors)
4✔
183

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

203
    def filepath(self) -> str:
4✔
204
        return self._prelude_check("build_file_dir", self._filepath)
4✔
205

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

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

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

215
    @property
4✔
216
    def defaults(self) -> BuildFileDefaultsParserState:
4✔
217
        return self._prelude_check("__defaults__", self._defaults)
4✔
218

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

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

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

236
    @docstring(
4✔
237
        f"""Provide default field values.
238

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

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

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

266

267
class RegistrarField:
4✔
268
    __slots__ = ("_field_type", "_default")
4✔
269

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

274
    @property
4✔
275
    def default(self) -> ImmutableValue:
4✔
UNCOV
276
        return self._default()
×
277

278

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

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

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

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

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

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

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

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

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

352
        return resolve_field_default
4✔
353

354

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

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

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

394
        type_aliases = dict(map(create_registrar_for_target, registered_target_types.aliases))
4✔
395
        parse_context = ParseContext(
4✔
396
            build_root=build_root,
397
            type_aliases=type_aliases,
398
            filepath_oracle=parse_state,
399
        )
400

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

414
        return symbols_info, parse_state
4✔
415

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

420
    @property
4✔
421
    def symbols(self) -> FrozenDict[str, Any]:
4✔
422
        return self._symbols_info.symbols
4✔
423

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

444
        global_symbols: dict[str, Any] = {
4✔
445
            **self.symbols,
446
            **extra_symbols.symbols,
447
        }
448

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

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

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

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

509
        error_on_imports(build_file_content, filepath)
4✔
510
        return self._parse_state.parsed_targets()
4✔
511

512

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

532

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

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

547

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

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

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

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

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

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

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

583

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

© 2026 Coveralls, Inc