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

pantsbuild / pants / 19068377358

04 Nov 2025 12:18PM UTC coverage: 92.46% (+12.2%) from 80.3%
19068377358

Pull #22816

github

web-flow
Merge a242f1805 into 89462b7ef
Pull Request #22816: Update Pants internal Python to 3.14

13 of 14 new or added lines in 12 files covered. (92.86%)

244 existing lines in 13 files now uncovered.

89544 of 96846 relevant lines covered (92.46%)

3.72 hits per line

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

93.79
/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
11✔
5

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

21
import typing_extensions
11✔
22

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

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

41

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

46
    @classmethod
11✔
47
    def from_info(cls, *infos: Iterable[BuildFileSymbolInfo]) -> BuildFileSymbolsInfo:
11✔
48
        info = {}
11✔
49
        for symbol in itertools.chain.from_iterable(infos):
11✔
50
            if symbol.name not in info:
11✔
51
                info[symbol.name] = symbol
11✔
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
                )
57
        return cls(info=FrozenDict(info))
11✔
58

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

63

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

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

78
            info[name] = BuildFileSymbolInfo(
2✔
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

87
        return cls(info=FrozenDict(info), referenced_env_vars=tuple(sorted(env_vars)))
11✔
88

89

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

98
    hide_from_help: bool = False
11✔
99

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

105
        if type_hints is not None:
11✔
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

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

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

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

131

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

135

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

139

140
class ParseState(threading.local):
11✔
141
    def __init__(self) -> None:
11✔
142
        self._defaults: BuildFileDefaultsParserState | None = None
11✔
143
        self._dependents_rules: BuildFileDependencyRulesParserState | None = None
11✔
144
        self._dependencies_rules: BuildFileDependencyRulesParserState | None = None
11✔
145
        self._filepath: str | None = None
11✔
146
        self._target_adaptors: list[TargetAdaptor] = []
11✔
147
        self._is_bootstrap: bool | None = None
11✔
148
        self._env_vars: EnvironmentVars | None = None
11✔
149
        self._symbols = tuple(
11✔
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(
11✔
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:
169
        self._is_bootstrap = is_bootstrap
11✔
170
        self._defaults = defaults
11✔
171
        self._dependents_rules = dependents_rules
11✔
172
        self._dependencies_rules = dependencies_rules
11✔
173
        self._env_vars = env_vars
11✔
174
        self._filepath = filepath
11✔
175
        self._target_adaptors.clear()
11✔
176

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

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

183
    def _prelude_check(self, name: str, value: T | None, closure_supported: bool = True) -> T:
11✔
184
        if value is not None:
11✔
185
            return value
11✔
186
        note = (
1✔
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
        )
194
        raise NameError(
1✔
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:
11✔
203
        return self._prelude_check("build_file_dir", self._filepath)
11✔
204

205
    def build_file_dir(self) -> PurePath:
11✔
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
        """
212
        return PurePath(self.filepath()).parent
1✔
213

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

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

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

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

235
    @docstring(
11✔
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(
11✔
242
        self,
243
        *args: SetDefaultsT,
244
        ignore_unknown_fields: bool = False,
245
        ignore_unknown_targets: bool = False,
246
        **kwargs,
247
    ) -> None:
248
        self.defaults.set_defaults(
3✔
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:
11✔
256
        """Declare dependents rules."""
257
        if self._dependents_rules is not None:
2✔
258
            self._dependents_rules.set_dependency_rules(self.filepath(), *args, **kwargs)
2✔
259

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

265

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

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

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

277

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

294
                def deprecated_field(self):
1✔
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))
1✔
308

309
    def __str__(self) -> str:
11✔
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
2✔
316

317
    def __call__(self, **kwargs: Any) -> TargetAdaptor:
11✔
318
        if self._parse_state.is_bootstrap and any(
11✔
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)}
1✔
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).
326
        if "name" not in kwargs:
11✔
327
            if not self._parse_state.filepath():
11✔
328
                raise UnaddressableObjectError(
×
329
                    "Targets in root-level BUILD files must be named explicitly."
330
                )
331
            kwargs["name"] = None
11✔
332

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

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

351
        return resolve_field_default
11✔
352

353

354
class Parser:
11✔
355
    def __init__(
11✔
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:
364
        self._symbols_info, self._parse_state = self._generate_symbols(
11✔
365
            build_root,
366
            object_aliases,
367
            registered_target_types,
368
            union_membership,
369
        )
370
        self.ignore_unrecognized_symbols = ignore_unrecognized_symbols
11✔
371

372
    @staticmethod
11✔
373
    def _generate_symbols(
11✔
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.
382
        parse_state = ParseState()
11✔
383

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

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

400
        symbols_info = BuildFileSymbolsInfo.from_info(
11✔
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

413
        return symbols_info, parse_state
11✔
414

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

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

423
    def parse(
11✔
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]:
434
        self._parse_state.reset(
11✔
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

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

448
        if self.ignore_unrecognized_symbols:
11✔
449
            defined_symbols = set()
2✔
450
            while True:
2✔
451
                try:
2✔
452
                    code = compile(build_file_content, filepath, "exec", dont_inherit=True)
2✔
453
                    exec(code, global_symbols)
2✔
454
                except NameError as e:
2✔
455
                    bad_symbol = _extract_symbol_from_name_error(e)
2✔
456
                    if bad_symbol in defined_symbols:
2✔
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
×
461
                    defined_symbols.add(bad_symbol)
2✔
462

463
                    global_symbols[bad_symbol] = _UnrecognizedSymbol(bad_symbol)
2✔
464
                    self._parse_state.reset(
2✔
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
                    )
472
                    continue
2✔
473
                break
2✔
474

475
            error_on_imports(build_file_content, filepath)
2✔
476
            return self._parse_state.parsed_targets()
2✔
477

478
        try:
11✔
479
            code = compile(build_file_content, filepath, "exec", dont_inherit=True)
11✔
480
            exec(code, global_symbols)
11✔
481
        except NameError as e:
3✔
482
            frame = traceback.extract_tb(e.__traceback__, limit=-1)[0]
2✔
483
            msg = (  # Capitalise first letter of NameError message.
2✔
484
                e.args[0][0].upper() + e.args[0][1:]
485
            )
486
            location = f":{frame.name}" if frame.name != "<module>" else ""
2✔
487
            original = f"{frame.filename}:{frame.lineno}{location}: {msg}"
2✔
488
            help_str = softwrap(
2✔
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
            )
494
            valid_symbols = sorted(s for s in global_symbols.keys() if s != "__builtins__")
2✔
495
            candidates = get_close_matches(build_file_content, valid_symbols)
2✔
496
            if candidates:
2✔
497
                if len(candidates) == 1:
1✔
498
                    formatted_candidates = candidates[0]
1✔
499
                elif len(candidates) == 2:
1✔
500
                    formatted_candidates = " or ".join(candidates)
1✔
501
                else:
502
                    formatted_candidates = f"{', '.join(candidates[:-1])}, or {candidates[-1]}"
1✔
503
                help_str = f"Did you mean {formatted_candidates}?\n\n" + help_str
1✔
504
            raise ParseError(
2✔
505
                f"{original}.\n\n{help_str}\n\nAll registered symbols: {valid_symbols}"
506
            )
507

508
        error_on_imports(build_file_content, filepath)
11✔
509
        return self._parse_state.parsed_targets()
11✔
510

511

512
def error_on_imports(build_file_content: str, filepath: str) -> None:
11✔
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.
516
    if "import" not in build_file_content:
11✔
517
        return
11✔
518
    io_wrapped_python = StringIO(build_file_content)
3✔
519
    for token in tokenize.generate_tokens(io_wrapped_python.readline):
3✔
520
        token_str = token[1]
3✔
521
        lineno, _ = token[2]
3✔
522
        if token_str != "import":
3✔
523
            continue
2✔
524
        raise ParseError(
2✔
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:
11✔
533
    result = re.match(r"^name '(\w*)'", err.args[0])
2✔
534
    if result is None:
2✔
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
        )
544
    return result.group(1)
2✔
545

546

547
class _UnrecognizedSymbol:
11✔
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:
11✔
556
        self.name = name
2✔
557
        self.args: tuple[Any, ...] = ()
2✔
558
        self.kwargs: dict[str, Any] = {}
2✔
559

560
    def __hash__(self) -> int:
11✔
561
        return hash(self.name)
2✔
562

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

568
    def __eq__(self, other) -> bool:
11✔
569
        return (
1✔
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:
11✔
577
        args = ", ".join(map(repr, self.args))
1✔
578
        kwargs = ", ".join(f"{k}={v!r}" for k, v in self.kwargs.items())
1✔
579
        signature = ", ".join(s for s in (args, kwargs) if s)
1✔
580
        return f"{self.name}({signature})"
1✔
581

582

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