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

peleiden / pelutils / 23945768026

02 Apr 2026 10:56AM UTC coverage: 97.581% (+0.09%) from 97.495%
23945768026

push

github

asgerius
Make code compliant with linter and type checker

250 of 250 new or added lines in 16 files covered. (100.0%)

32 existing lines in 8 files now uncovered.

1452 of 1488 relevant lines covered (97.58%)

0.98 hits per line

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

96.54
/pelutils/parser.py
1
from __future__ import annotations
1✔
2

3
import io
1✔
4
import os
1✔
5
import re
1✔
6
import shlex
1✔
7
import sys
1✔
8
from abc import ABC
1✔
9
from argparse import SUPPRESS, ArgumentParser, Namespace
1✔
10
from ast import literal_eval
1✔
11
from configparser import ConfigParser, MissingSectionHeaderError
1✔
12
from copy import deepcopy
1✔
13
from datetime import datetime
1✔
14
from pprint import pformat
1✔
15
from shutil import rmtree
1✔
16
from typing import Any, Callable, TypeVar, Union
1✔
17

18
from typing_extensions import override
1✔
19

20
from pelutils import except_keys, get_timestamp_for_files
1✔
21

22
_T = TypeVar("_T")
1✔
23
_type = type  # Save `type` under different name to prevent name collisions
1✔
24

25
# Support for nargs is limited to 0 (any number of args) and set number
26
# As such, not all modes supported by argparse (see documentation) are supported here
27
# https://docs.python.org/3/library/argparse.html#nargs
28
_NargsTypes = Union[int, None]
1✔
29

30

31
def _fixdash(argname: str) -> str:
1✔
32
    """Replace dashes in argument names with underscores."""
33
    return argname.replace("-", "_")
1✔
34

35

36
class ParserError(Exception):
1✔
37
    """Raised when unable to parse arguments."""
38

39

40
class ConfigError(ParserError):
1✔
41
    """Config file related errors."""
42

43

44
class _AbstractArgument(ABC):  # noqa: B024
1✔
45
    """Contains description of an argument.
46

47
    '--' is automatically prepended to `name` when given from the command line.
48
    """
49

50
    def __init__(self, name: str, abbrv: str | None, help: str | None, **kwargs: Any):  # pyright: ignore[reportExplicitAny]
1✔
51
        self._validate(name, abbrv)
1✔
52

53
        self.name = name
1✔
54
        self.abbrv = abbrv
1✔
55
        self.help = help
1✔
56
        self.kwargs = kwargs
1✔
57

58
    @staticmethod
1✔
59
    def _validate(name: str, abbrv: str | None):
1✔
60
        if not name:
1✔
61
            raise ValueError(f"`name` ('{name}') must not be an empty string")
1✔
62
        if name.startswith("-"):
1✔
63
            raise ValueError(f"Double dashes are automatically prepended and should not be given by user: '{name}'")
1✔
64
        if isinstance(abbrv, str) and (len(abbrv) != 1 or not abbrv.isalpha()):
1✔
65
            raise ValueError(f"`abbrv` ('{abbrv}') must be an alpha character and have length 1")
1✔
66
        if re.search(r"\s", name):
1✔
67
            raise ValueError(f"`name` ('{name}') cannot contain whitespace")
1✔
68

69
    def _name_or_flags(self) -> tuple[str, ...]:
1✔
70
        if self.abbrv:
1✔
71
            return ("-" + self.abbrv, "--" + self.name)
1✔
72
        else:
73
            return ("--" + self.name,)
1✔
74

75
    @staticmethod
1✔
76
    def _validate_nargs(nargs: _NargsTypes):
1✔
77
        if type(nargs) not in _NargsTypes.__args__:  # pyright: ignore[reportAttributeAccessIssue]
1✔
78
            raise TypeError(f"`nargs` type must be one of {_NargsTypes.__args__}, not {type(nargs)}")  # pyright: ignore[reportAttributeAccessIssue]
1✔
79
        if isinstance(nargs, int) and nargs < 0:
1✔
80
            raise ValueError("When expecting a set number of arguments, the number must be at least 0")
1✔
81

82
    @override
1✔
83
    def __str__(self) -> str:
1✔
84
        vars_str = ", ".join(f"{name}={value}" for name, value in vars(self).items())
1✔
85
        return f"{self.__class__.__name__}({vars_str})"
1✔
86

87
    @override
1✔
88
    def __hash__(self) -> int:
1✔
89
        return hash(self.name)
1✔
90

91

92
class Argument(_AbstractArgument):
1✔
93
    """Argument that must be given a value."""
94

95
    def __init__(  # noqa: PLR0913
1✔
96
        self,
97
        name: str,
98
        *,
99
        type: Callable[[str], _T] = str,
100
        abbrv: str | None = None,
101
        help: str | None = None,
102
        metavar: str | tuple[str, ...] | None = None,
103
        nargs: _NargsTypes = None,
104
        **kwargs: Any,  # pyright: ignore[reportExplicitAny]
105
    ):
106
        super().__init__(name, abbrv, help=help, **kwargs)
1✔
107
        self._validate_nargs(nargs)
1✔
108
        if "default" in kwargs:
1✔
109
            raise TypeError(f"Class {self.__class__.__name__} does not accept keyword argument 'default'")
1✔
110
        self.type = type
1✔
111
        self.metavar = metavar
1✔
112
        self.nargs = nargs
1✔
113

114

115
class Option(_AbstractArgument):
1✔
116
    """Optional argument with a default value."""
117

118
    def __init__(  # noqa: PLR0913
1✔
119
        self,
120
        name: str,
121
        *,
122
        default: _T | None = None,
123
        type: Callable[[str], _T] | None = None,
124
        abbrv: str | None = None,
125
        help: str | None = None,
126
        metavar: str | tuple[str, ...] | None = None,
127
        nargs: _NargsTypes = None,
128
        **kwargs: Any,  # pyright: ignore[reportExplicitAny]
129
    ):
130
        super().__init__(name, abbrv, help, **kwargs)
1✔
131
        self._validate_nargs(nargs)
1✔
132

133
        self.default = default
1✔
134
        if type is not None:
1✔
135
            self.type = type
1✔
136
        elif self.default is not None:
1✔
137
            if nargs is not None:
1✔
138
                self.default = list(self.default)  # pyright: ignore[reportArgumentType]
1✔
139
            self.type = _type(self.default) if nargs is None else _type(self.default[0])  # pyright: ignore[reportIndexIssue]
1✔
140
            if nargs is not None and not all(isinstance(x, self.type) for x in self.default):  # pyright: ignore[reportGeneralTypeIssues]
1✔
UNCOV
141
                raise ValueError(f"All elements in default value of {name} must be of type {self.type}")
×
142
        else:
143
            self.type = str
1✔
144
        self.metavar = metavar
1✔
145
        self.nargs = nargs
1✔
146

147

148
class Flag(_AbstractArgument):
1✔
149
    """Boolean flag. Defaults to `False` when not given and `True` when given."""
150

151
    def __init__(
1✔
152
        self,
153
        name: str,
154
        *,
155
        abbrv: str | None = None,
156
        help: str | None = None,
157
        **kwargs: Any,  # pyright: ignore[reportExplicitAny]
158
    ):
159
        super().__init__(name, abbrv, help, **kwargs)
1✔
160

161
    @property
1✔
162
    def default(self) -> bool:
1✔
163
        """The default value for a Flag is always False."""
164
        return False
1✔
165

166

167
class JobDescription(Namespace):
1✔
168
    """Namespace containing the values of all defined parameters parsed from the command line and config file if given.
169

170
    Functionally, it is very similar to Namespace from argpase.
171
    """
172

173
    document_filename = "used-config.ini"
1✔
174

175
    def __init__(self, name: str, location: str, explicit_args: set[str], docfile_content: str, **kwargs: Any):  # pyright: ignore[reportExplicitAny]
1✔
176
        super().__init__(**kwargs)
1✔
177
        self.name = name
1✔
178
        self.location = location
1✔
179
        self.explicit_args = explicit_args
1✔
180
        self._docfile_content = docfile_content
1✔
181

182
    def todict(self) -> dict[str, Any]:  # pyright: ignore[reportExplicitAny]
1✔
183
        """Return a dictionary version of itself which contains solely the parsed values."""
184
        d = vars(self)
1✔
185
        d = {kw: v for kw, v in d.items() if not kw.startswith("_") and kw not in {"config", "explicit_args"}}
1✔
186
        return d
1✔
187

188
    def prepare_directory(self, encoding: str | None = None):
1✔
189
        """Clear the job directory and puts a documentation file in it."""
190
        rmtree(self.location, ignore_errors=True)
1✔
191
        os.makedirs(self.location)
1✔
192
        self.write_documentation(encoding)
1✔
193

194
    def write_documentation(self, encoding: str | None = None, *, append: bool = True):
1✔
195
        """Write, or append if one already exists, a documentation file in the location.
196

197
        The file has the CLI command user for running the program as a comment as well as the config file,
198
        if such a one was used.
199
        """
200
        os.makedirs(self.location, exist_ok=True)
1✔
201
        path = os.path.join(self.location, self.document_filename)
1✔
202
        with open(path, "a" if append else "w", encoding=encoding) as docfile:
1✔
203
            docfile.write(self._docfile_content)
1✔
204

205
    def __getitem__(self, key: str) -> Any:  # pyright: ignore[reportExplicitAny]
1✔
206
        """Get the argument value with the given key. -/_ disambiguities are resolved."""
207
        if key in self.__dict__:
1✔
208
            return self.__dict__[key]
1✔
209
        elif _fixdash(key) in self.__dict__:
1✔
210
            return self.__dict__[_fixdash(key)]
1✔
211
        else:
212
            raise KeyError(f"No such job argument '{key}'")
1✔
213

214
    @override
1✔
215
    def __str__(self) -> str:
1✔
216
        return pformat(self.todict())
1✔
217

218

219
ArgumentTypes = Union[Argument, Option, Flag]
1✔
220

221

222
class Parser:
1✔
223
    """Extension of built-in argparse.ArgumentParser which also supports reading from config files."""
224

225
    location: str | None = None  # Set in `parse` method
1✔
226

227
    _default_config_job = "DEFAULT"
1✔
228

229
    _location_arg = Argument("location")
1✔
230
    _location_arg._name_or_flags = lambda: ("location",)  # pyright: ignore[reportPrivateUsage]
1✔
231
    _name_arg = Option("name", default=None, help="Name of the job")
1✔
232
    _section_separator = ":"
1✔
233
    _encoding_separator = "::"
1✔
234
    _config_arg = Option(
1✔
235
        "config",
236
        default=None,
237
        abbrv="c",
238
        help=f"Path to config file. Encoding can be specified by giving <path>{_encoding_separator}<encoding>, "
239
        + f"e.g. --config path/to/config.ini{_encoding_separator}utf-8",
240
    )
241

242
    _reserved_arguments: tuple[ArgumentTypes, ...] = (_location_arg, _name_arg, _config_arg)
1✔
243
    _reserved_names = {arg.name for arg in _reserved_arguments}  # noqa: RUF012
1✔
244
    _reserved_names.add("help")  # Reserved by argparse
1✔
245
    _reserved_abbrvs = {arg.abbrv for arg in _reserved_arguments if arg.abbrv}  # noqa: RUF012
1✔
246

247
    @property
1✔
248
    def reserved_names(self) -> set[str]:
1✔
249
        """Argument names which are reserved."""
250
        return self._reserved_names
1✔
251

252
    @property
1✔
253
    def reserved_abbrvs(self) -> set[str]:
1✔
254
        """Argument abbreviations which are reserved."""
255
        return self._reserved_abbrvs
1✔
256

257
    @property
1✔
258
    def encoding_seperator(self) -> str:
1✔
259
        """Seperator used to specify the encoding (if given) of the config file."""
260
        return self._encoding_separator
1✔
261

262
    def __init__(
1✔
263
        self,
264
        *arguments: ArgumentTypes,
265
        description: str | None = None,
266
        multiple_jobs: bool = False,
267
    ):
268
        # Modifications are made to the argument objects, so make a deep copy
269
        arguments = tuple(deepcopy(arg) for arg in arguments)
1✔
270

271
        self._multiple_jobs = multiple_jobs
1✔
272

273
        self._argparser = ArgumentParser(description=description)
1✔
274
        self._configparser = ConfigParser(allow_no_value=True)
1✔
275
        self._configparser.optionxform = str  # pyright: ignore[reportAttributeAccessIssue]
1✔
276

277
        # Ensure that no conflicts exist with reserved arguments
278
        if any(arg.name in self._reserved_names for arg in arguments):
1✔
279
            raise ParserError(f"An argument conflicted with one of the reserved arguments: {self._reserved_names}")
1✔
280
        if any(arg.abbrv in self._reserved_abbrvs for arg in arguments):
1✔
UNCOV
281
            raise ParserError(f"An argument conflicted with one of the reserved abbreviations: {self._reserved_abbrvs}")
×
282

283
        # Map argument names to arguments with dashes replaced by underscores
284
        self._arguments = {_fixdash(arg.name): arg for arg in self._reserved_arguments + arguments}
1✔
285
        if len(self._arguments) != len(self._reserved_arguments) + len(arguments):
1✔
286
            raise ParserError(
1✔
287
                "Conflicting arguments found. Notice that '-' and '_' are counted the same,so e.g. 'a-b' and 'a_b' would cause a conflict"
288
            )
289

290
        self._location_arg.help = "Directory containing all job directories" if self._multiple_jobs else "Job directory"
1✔
291

292
        # Build abbrevations for arguments
293
        # Those with explicit abbreviations are handled first to prevent being overwritten
294
        _used_abbrvs = {"h"}  # Reserved by argparse
1✔
295
        _args_with_abbrvs_first = sorted(self._arguments, key=lambda arg: self._arguments[arg].abbrv is None)
1✔
296
        for argname in _args_with_abbrvs_first:
1✔
297
            argument = self._arguments[argname]
1✔
298
            if argument.abbrv and argument.abbrv not in _used_abbrvs:
1✔
299
                _used_abbrvs.add(argument.abbrv)
1✔
300
            elif argument.abbrv:
1✔
301
                raise ParserError(f"Abbreviation '{argument.abbrv}' was used multiple times")
1✔
302
            # Autogenerate abbreviation
303
            # First argname[0] is tried. If it exists, the other casing is used if it does not exist
304
            elif argname[0] not in _used_abbrvs:
1✔
305
                argument.abbrv = argname[0]
1✔
306
                _used_abbrvs.add(argument.abbrv)
1✔
307
            elif argname[0].swapcase() not in _used_abbrvs:
1✔
308
                argument.abbrv = argname[0].swapcase()
1✔
309
                _used_abbrvs.add(argument.abbrv)
1✔
310

311
        # Finally, add all arguments to argparser
312
        for argument in self._arguments.values():
1✔
313
            # nargs is given as "*" to argparser to prevent it from raising errors
314
            # Input validity is then checked later
315
            if isinstance(argument, Argument):
1✔
316
                self._argparser.add_argument(
1✔
317
                    *argument._name_or_flags(),  # pyright: ignore[reportPrivateUsage]
318
                    type=argument.type,
319
                    help=argument.help,
320
                    metavar=argument.metavar,
321
                    nargs="*" if argument.nargs is not None else None,
322
                    **argument.kwargs,
323
                )
324
            elif isinstance(argument, Option):
1✔
325
                self._argparser.add_argument(
1✔
326
                    *argument._name_or_flags(),  # pyright: ignore[reportPrivateUsage]
327
                    default=argument.default,
328
                    type=argument.type,
329
                    help=argument.help,
330
                    metavar=argument.metavar,
331
                    nargs="*" if argument.nargs is not None else None,
332
                    **argument.kwargs,
333
                )
334
            else:
335
                assert isinstance(argument, Flag)
1✔
336
                self._argparser.add_argument(
1✔
337
                    *argument._name_or_flags(),  # pyright: ignore[reportPrivateUsage]
338
                    action="store_true",
339
                    help=argument.help,
340
                    **argument.kwargs,
341
                )
342

343
    def _get_default_values(self) -> dict[str, Any]:  # pyright: ignore[reportExplicitAny]
1✔
344
        """Build a dictionary that maps argument names to their default values.
345

346
        Arguments without defaults values are not included.
347
        """
348
        return {
1✔
349
            argname: arg.default  # pyright: ignore[reportAttributeAccessIssue]
350
            for argname, arg in self._arguments.items()
351
            if hasattr(arg, "default") and arg.name not in self._reserved_names
352
        }
353

354
    def _parse_explicit_cli_args(self) -> set[str]:
1✔
355
        """Return a set of arguments explicitly given from the command line.
356

357
        No prepended dashes and in-word dashes have been changed to underscores.
358
        """
359
        # Create auxiliary parser to help determine if arguments are given explicitly from CLI
360
        # Heavily inspired by this answer: https://stackoverflow.com/a/45803037/13196863
361
        aux_parser = ArgumentParser(argument_default=SUPPRESS)
1✔
362
        args = self._argparser.parse_args()
1✔
363
        for argname in vars(args):
1✔
364
            arg = self._arguments[argname]
1✔
365
            if isinstance(arg, Flag):
1✔
366
                aux_parser.add_argument(*arg._name_or_flags(), action="store_true")  # pyright: ignore[reportPrivateUsage]
1✔
367
            else:
368
                aux_parser.add_argument(*arg._name_or_flags(), nargs="*" if arg.nargs is not None else None)  # pyright: ignore[reportPrivateUsage]
1✔
369
        explicit_cli_args = aux_parser.parse_args()
1✔
370

371
        return set(vars(explicit_cli_args))
1✔
372

373
    def _parse_config_file(self, config_path: str) -> dict[str, dict[str, Any]]:  # noqa: PLR0912  # pyright: ignore[reportExplicitAny]
1✔
374
        """Parse a given configuration file (.ini format).
375

376
        Return a dictionary where each section as a key pointing to corresponding argument/value pairs.
377
        """
378
        if self._encoding_separator in config_path:
1✔
UNCOV
379
            config_path, encoding = config_path.split(self._encoding_separator, maxsplit=1)
×
380
        else:
381
            encoding = None
1✔
382

383
        config_path, *sections = config_path.split(self._section_separator)
1✔
384
        sections = set(sections)
1✔
385

386
        try:
1✔
387
            if not self._configparser.read(config_path, encoding=encoding):
1✔
UNCOV
388
                raise FileNotFoundError(f"Configuration file not found at {config_path}")
×
UNCOV
389
        except MissingSectionHeaderError as e:
×
UNCOV
390
            raise ConfigError(
×
391
                "The provided config file contains no section headers. This is best resolved by adding `[DEFAULT]` at the very top."
392
            ) from e
393

394
        for section in sections:
1✔
395
            if section not in self._configparser:
1✔
396
                raise ParserError(f"Unable to parse unknown section '{section}'")
1✔
397

398
        # Save given values and convert to proper types
399
        config_dict = dict()
1✔
400
        for section, arguments in self._configparser.items():
1✔
401
            if sections and section not in sections and section != self._default_config_job:
1✔
402
                continue
1✔
403
            config_dict[section] = dict()
1✔
404
            for argname, value in arguments.items():
1✔
405
                argname = _fixdash(argname)  # noqa: PLW2901
1✔
406
                if argname not in self._arguments:
1✔
407
                    raise ParserError(f"Unknown argument '{argname}'")
1✔
408
                if isinstance(self._arguments[argname], Flag):
1✔
409
                    # If flag value is given in config file, parse True/False
410
                    if isinstance(value, str):  # pyright: ignore[reportUnnecessaryIsInstance]
1✔
411
                        config_dict[section][argname] = literal_eval(value)
1✔
412
                        # Check if valid value
413
                        if not isinstance(config_dict[section][argname], bool):
1✔
UNCOV
414
                            raise ValueError(f"Value {argname} in section {section} must be 'True' or 'False', not '{value}'")
×
415
                    else:
416
                        assert value is None
1✔
417
                        config_dict[section][argname] = True
1✔
418
                else:  # Arguments and options
419
                    # If multiple values, parse each as given type
420
                    # Otherwise, parse single argument as given type
421
                    try:
1✔
422
                        if self._arguments[argname].nargs is not None:  # pyright: ignore[reportAttributeAccessIssue]
1✔
423
                            config_dict[section][argname] = [self._arguments[argname].type(x) for x in shlex.split(value)]  # pyright: ignore[reportAttributeAccessIssue, reportCallIssue]
1✔
424
                        else:
425
                            config_dict[section][argname] = self._arguments[argname].type(value)  # pyright: ignore[reportCallIssue, reportAttributeAccessIssue]
1✔
UNCOV
426
                    except ValueError as e:
×
UNCOV
427
                        raise ValueError(f"Unable to parse value '{argname}' in section '{section}': {e.args[0]}") from e
×
428

429
        return config_dict
1✔
430

431
    def parse_args(self) -> JobDescription | list[JobDescription]:  # noqa: PLR0912
1✔
432
        """Parse command line arguments and optionally a configuration file if given.
433

434
        If multiple_jobs was set to True in __init__, a list of job descriptions is returned.
435
        Otherwise, a single job description is returned.
436
        """
437
        job_descriptions: list[JobDescription] = list()
1✔
438
        args = self._argparser.parse_args()
1✔
439
        explicit_cli_args = self._parse_explicit_cli_args()
1✔
440
        self.location = args.location
1✔
441
        assert self.location is not None
1✔
442

443
        if args.config is None:
1✔
444
            docfile_content = self._get_docfile_content()
1✔
445
            name = args.name or get_timestamp_for_files()
1✔
446
            if self._multiple_jobs:
1✔
447
                location = os.path.join(self.location, name)
1✔
448
            else:
449
                location = self.location
1✔
450
            arg_dict = vars(args)
1✔
451
            for argname, arg in self._arguments.items():
1✔
452
                if isinstance(arg, Argument) and arg_dict[argname] is None:
1✔
453
                    raise ParserError(f"Missing value for '{arg.name}'")
1✔
454

455
            job_descriptions.append(
1✔
456
                JobDescription(
457
                    name=name,
458
                    location=location,
459
                    explicit_args=explicit_cli_args,
460
                    docfile_content=docfile_content,
461
                    **except_keys(arg_dict, ("location", "name")),
462
                )
463
            )
464
        else:
465
            config_dict = self._parse_config_file(args.config)
1✔
466
            # Update documentation file docname
467
            docfile_content = self._get_docfile_content()
1✔
468
            # If any section other than DEFAULT is given, then the sections consist of DEFAULT and the others
469
            # In that case the DEFAULT is not used and is thus discarded
470
            if len(config_dict) > 1:
1✔
471
                del config_dict[self._default_config_job]
1✔
472
            if len(config_dict) > 1 and not self._multiple_jobs:
1✔
473
                raise ConfigError("Multiple sections found in config file, yet multiple_jobs has been set to `False`")
1✔
474

475
            # Create job descriptions section-wise
476
            for section, config_args in config_dict.items():
1✔
477
                if self._multiple_jobs:
1✔
478
                    name = section
1✔
479
                    if section == self._default_config_job:
1✔
480
                        name = section if self._name_arg.name not in explicit_cli_args else args.name
1✔
481
                    location = os.path.join(self.location, name)
1✔
482
                else:
483
                    name = section if self._name_arg.name not in explicit_cli_args else args.name
1✔
484
                    location = self.location
1✔
485

486
                # Final values of all arguments
487
                # No prepended dashes, but in-word dashes have been changed to underscores
488
                value_dict = {
1✔
489
                    **except_keys(
490
                        self._get_default_values(),
491
                        ("name", "config"),
492
                    ),
493
                    **config_args,
494
                    **{
495
                        argname: value
496
                        for argname, value in except_keys(vars(args), ("name", "location")).items()
497
                        if argname in explicit_cli_args
498
                    },
499
                }
500
                job_descriptions.append(
1✔
501
                    JobDescription(
502
                        name=name,
503
                        location=location,
504
                        explicit_args={*config_args.keys(), *explicit_cli_args},
505
                        docfile_content=docfile_content,
506
                        **value_dict,
507
                    )
508
                )
509

510
        # Check if any arguments are missing or are invalid
511
        for job in job_descriptions:
1✔
512
            for argname, arg in self._arguments.items():
1✔
513
                argument = self._arguments[argname]
1✔
514
                if argname not in job:
1✔
515
                    raise ParserError(f"Job '{job.name}' is missing value for '{arg.name}'")
1✔
516
                elif hasattr(argument, "nargs") and argument.nargs is not None:  # pyright: ignore[reportAttributeAccessIssue]
1✔
517
                    if job[argname] is None and isinstance(argument, Argument):
1✔
UNCOV
518
                        raise ParserError(f"Argument '{argname}' has not been given in job '{job.name}'")
×
519
                    assert isinstance(job[argname], list) or job[argname] is None
1✔
520
                    if job[argname] is not None:
1✔
521
                        assert all(isinstance(x, argument.type) for x in job[argname])  # pyright: ignore[reportArgumentType, reportAttributeAccessIssue]
1✔
522
                        if argument.nargs > 0 and len(job[argname]) != argument.nargs:  # pyright: ignore[reportAttributeAccessIssue]
1✔
523
                            raise ValueError(f"Argument '{argname}' expected {argument.nargs} values but received {len(job[argname])}")  # pyright: ignore[reportAttributeAccessIssue]
1✔
524

525
        return job_descriptions if self._multiple_jobs else job_descriptions[0]
1✔
526

527
    def _get_docfile_content(self) -> str:
1✔
528
        buffer = io.StringIO()
1✔
529
        buffer.write(f"# Running job at {datetime.now()}{os.linesep}")
1✔
530
        lines: list[str] = [
1✔
531
            "CLI command",
532
            " ".join(sys.argv),
533
            "Default values",
534
            *pformat(self._get_default_values(), width=120).splitlines(),
535
        ]
536
        buffer.write(f"{os.linesep}# " + f"{os.linesep}# ".join(lines) + os.linesep)
1✔
537
        cline = f"# Used config file{os.linesep}"
1✔
538
        buffer.write(cline)
1✔
539
        position = buffer.tell()
1✔
540
        self._configparser.write(buffer)
1✔
541
        if buffer.tell() == position:
1✔
542
            # Nothing was written to the buffer, so clear the config file section
543
            buffer.seek(position - len(cline))
1✔
544
            buffer.truncate()
1✔
545
            buffer.write(2 * os.linesep)
1✔
546
        content = buffer.getvalue()
1✔
547
        buffer.close()
1✔
548
        return content
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

© 2026 Coveralls, Inc