idf-build-apps 2.5.0rc1__py3-none-any.whl → 2.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
idf_build_apps/args.py CHANGED
@@ -1,45 +1,41 @@
1
1
  # SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
- """
4
- Arguments used in the CLI, and functions.
5
-
6
- The reason that does not use pydantic models, but dataclasses
7
-
8
- - poor autocomplete in IDE when using pydantic custom Fields with extra metadata
9
- - pydantic Field alias is nice, but hard to customize, when
10
- - the deprecated field has a different nargs
11
- """
12
3
 
13
4
  import argparse
5
+ import enum
14
6
  import inspect
15
7
  import logging
16
8
  import os
17
9
  import re
10
+ import sys
18
11
  import typing as t
19
12
  from copy import deepcopy
20
- from dataclasses import InitVar, asdict, dataclass, field, fields
21
- from enum import Enum
13
+ from dataclasses import dataclass
14
+ from io import TextIOWrapper
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from pydantic import AliasChoices, Field, computed_field, field_validator
19
+ from pydantic.fields import FieldInfo
20
+ from pydantic_core.core_schema import ValidationInfo
21
+ from pydantic_settings import (
22
+ BaseSettings,
23
+ PydanticBaseSettingsSource,
24
+ SettingsConfigDict,
25
+ )
26
+ from typing_extensions import Concatenate, ParamSpec
22
27
 
23
- from . import SESSION_ARGS, setup_logging
24
- from .app import App
25
- from .config import get_valid_config
28
+ from . import SESSION_ARGS, App, setup_logging
26
29
  from .constants import ALL_TARGETS
27
30
  from .manifest.manifest import FolderRule, Manifest
28
- from .utils import (
29
- InvalidCommand,
30
- Self,
31
- drop_none_kwargs,
32
- files_matches_patterns,
33
- semicolon_separated_str_to_list,
34
- to_absolute_path,
35
- to_list,
36
- )
31
+ from .utils import InvalidCommand, files_matches_patterns, semicolon_separated_str_to_list, to_absolute_path, to_list
32
+ from .vendors.pydantic_sources import PyprojectTomlConfigSettingsSource, TomlConfigSettingsSource
37
33
 
38
34
  LOGGER = logging.getLogger(__name__)
39
35
 
40
36
 
41
- class _Field(Enum):
42
- UNSET = 'UNSET'
37
+ class ValidateMethod(str, enum.Enum):
38
+ TO_LIST = 'to_list'
43
39
 
44
40
 
45
41
  @dataclass
@@ -48,7 +44,7 @@ class FieldMetadata:
48
44
  dataclass field metadata. All fields are optional.
49
45
  Some fields are used in argparse while running :func:`add_args_to_parser`.
50
46
 
51
- :param description: description of the field
47
+ :param validate_method: validate method for the field
52
48
  :param deprecates: deprecates field names, used in argparse
53
49
  :param shorthand: shorthand for the argument, used in argparse
54
50
  :param action: action for the argument, used in argparse
@@ -56,10 +52,12 @@ class FieldMetadata:
56
52
  :param choices: choices for the argument, used in argparse
57
53
  :param type: type for the argument, used in argparse
58
54
  :param required: whether the argument is required, used in argparse
59
- :param default: default value, used in argparse
55
+ :param default: default value for the argument, used in argparse
56
+ :param hidden: whether the argument is hidden, used in argparse
60
57
  """
61
58
 
62
- description: t.Optional[str] = None
59
+ # validate method
60
+ validate_method: t.Optional[t.List[str]] = None
63
61
  # the field description will be copied from the deprecates field if not specified
64
62
  deprecates: t.Optional[t.Dict[str, t.Dict[str, t.Any]]] = None
65
63
  shorthand: t.Optional[str] = None
@@ -71,245 +69,214 @@ class FieldMetadata:
71
69
  required: bool = False
72
70
  # usually default is not needed. only set it when different from the default value of the field
73
71
  default: t.Any = None
72
+ # hidden field, use deprecated instead, or hide it in the argparse
73
+ hidden: bool = False
74
74
 
75
75
 
76
- @dataclass
77
- class GlobalArguments:
78
- """
79
- Global arguments used in all commands
80
- """
76
+ P = ParamSpec('P')
77
+ T = t.TypeVar('T')
81
78
 
82
- config_file: t.Optional[str] = field(
83
- default=None,
84
- metadata=asdict(
85
- FieldMetadata(
86
- description='Path to the configuration file',
87
- shorthand='-c',
88
- )
89
- ),
90
- )
91
- verbose: int = field(
92
- default=0,
93
- metadata=asdict(
94
- FieldMetadata(
95
- description='Verbosity level. By default set to WARNING. Specify -v for INFO, -vv for DEBUG',
96
- shorthand='-v',
97
- action='count',
98
- )
99
- ),
100
- )
101
- log_file: t.Optional[str] = field(
102
- default=None,
103
- metadata=asdict(
104
- FieldMetadata(
105
- description='Path to the log file, if not specified logs will be printed to stderr',
106
- )
107
- ),
108
- )
109
- no_color: bool = field(
110
- default=False,
111
- metadata=asdict(
112
- FieldMetadata(
113
- description='Disable colored output',
114
- action='store_true',
115
- )
116
- ),
117
- )
118
79
 
119
- _depr_name_to_new_name_dict: t.ClassVar[t.Dict[str, str]] = {} # record deprecated field <-> new field
80
+ def _wrap_with_metadata(
81
+ _: t.Callable[P, t.Any],
82
+ ) -> t.Callable[[t.Callable[..., T]], t.Callable[Concatenate[t.Optional[FieldMetadata], P], T]]:
83
+ """Patch the function signature with metadata args"""
120
84
 
121
- def __new__(cls, *args, **kwargs): # noqa: ARG003
122
- for f in fields(cls):
123
- _metadata = FieldMetadata(**f.metadata)
124
- if _metadata.deprecates:
125
- for depr_name in _metadata.deprecates:
126
- cls._depr_name_to_new_name_dict[depr_name] = f.name
85
+ def return_func(func: t.Callable[..., T]) -> t.Callable[Concatenate[t.Optional[FieldMetadata], P], T]:
86
+ return t.cast(t.Callable[Concatenate[t.Optional[FieldMetadata], P], T], func)
127
87
 
128
- return super().__new__(cls)
88
+ return return_func
129
89
 
130
- @classmethod
131
- def from_dict(cls, d: t.Dict[str, t.Any]) -> Self:
132
- """
133
- Create an instance from a dictionary. Ignore unknown keys.
134
90
 
135
- :param d: dictionary
136
- :return: instance
137
- """
138
- return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
91
+ @_wrap_with_metadata(Field)
92
+ def field(meta: t.Optional[FieldMetadata], *args, **kwargs):
93
+ """field with metadata"""
94
+ f = Field(*args, **kwargs)
95
+ f.metadata.append(meta)
96
+ return f
139
97
 
140
- def __post_init__(self):
141
- self.apply_config()
142
98
 
143
- def apply_config(self) -> None:
144
- """
145
- Apply the configuration file to the arguments
146
- """
147
- config_dict = get_valid_config(custom_path=self.config_file) or {}
99
+ def get_meta(f: FieldInfo) -> t.Optional[FieldMetadata]:
100
+ """
101
+ Get the metadata of the field
148
102
 
149
- # set log fields first
150
- self.verbose = config_dict.pop('verbose', self.verbose)
151
- self.log_file = config_dict.pop('log_file', self.log_file)
152
- self.no_color = config_dict.pop('no_color', self.no_color)
153
- setup_logging(self.verbose, self.log_file, not self.no_color)
103
+ :param f: field
104
+ :return: metadata of the field if exists, None otherwise
105
+ """
106
+ for m in f.metadata:
107
+ if isinstance(m, FieldMetadata):
108
+ return m
109
+
110
+ return None
154
111
 
155
- if config_dict:
156
- for name, value in config_dict.items():
157
- if hasattr(self, name):
158
- setattr(self, name, value)
159
112
 
160
- if name in self._depr_name_to_new_name_dict:
161
- self.set_deprecated_field(self._depr_name_to_new_name_dict[name], name, value)
113
+ class BaseArguments(BaseSettings):
114
+ """Base settings class for all settings classes"""
162
115
 
163
- def set_deprecated_field(self, new_k: str, depr_k: str, depr_v: t.Any) -> None:
164
- if depr_v == _Field.UNSET:
165
- return
116
+ model_config = SettingsConfigDict(
117
+ toml_file='.idf_build_apps.toml',
118
+ pyproject_toml_table_header=('tool', 'idf-build-apps'),
119
+ pyproject_toml_depth=sys.maxsize,
120
+ )
166
121
 
167
- LOGGER.warning(
168
- f'Field `{depr_k}` is deprecated. Will be removed in the next major release. '
169
- f'Use field `{new_k}` instead.'
122
+ @classmethod
123
+ def settings_customise_sources(
124
+ cls,
125
+ settings_cls: t.Type[BaseSettings],
126
+ init_settings: PydanticBaseSettingsSource,
127
+ env_settings: PydanticBaseSettingsSource, # noqa: ARG003
128
+ dotenv_settings: PydanticBaseSettingsSource, # noqa: ARG003
129
+ file_secret_settings: PydanticBaseSettingsSource, # noqa: ARG003
130
+ ) -> t.Tuple[PydanticBaseSettingsSource, ...]:
131
+ return (
132
+ init_settings,
133
+ TomlConfigSettingsSource(settings_cls),
134
+ PyprojectTomlConfigSettingsSource(settings_cls),
170
135
  )
171
- if getattr(self, new_k) is not None:
172
- LOGGER.warning(f'Field `{new_k}` is already set. Ignoring deprecated field `{depr_k}`')
173
- return
174
136
 
175
- setattr(self, new_k, depr_v)
137
+ @field_validator('*', mode='before')
138
+ @classmethod
139
+ def validate_by_validate_methods(cls, v: t.Any, info: ValidationInfo):
140
+ if info.field_name and info.field_name in cls.model_fields:
141
+ f = cls.model_fields[info.field_name]
142
+ meta = get_meta(f)
143
+ if meta and meta.validate_method and ValidateMethod.TO_LIST in meta.validate_method:
144
+ return to_list(v)
176
145
 
146
+ return v
177
147
 
178
- @dataclass
179
- class DependencyDrivenBuildArguments(GlobalArguments):
180
- """
181
- Arguments used in the dependency-driven build feature.
182
- """
183
148
 
184
- manifest_file: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
185
- manifest_files: t.Optional[t.List[str]] = field(
149
+ class GlobalArguments(BaseArguments):
150
+ verbose: int = field(
151
+ FieldMetadata(
152
+ shorthand='-v',
153
+ action='count',
154
+ ),
155
+ description='Verbosity level. By default set to WARNING. Specify -v for INFO, -vv for DEBUG',
156
+ default=0,
157
+ )
158
+ log_file: t.Optional[str] = field(
159
+ None,
160
+ description='Path to the log file, if not specified logs will be printed to stderr',
186
161
  default=None,
187
- metadata=asdict(
188
- FieldMetadata(
189
- deprecates={
190
- 'manifest_file': {
191
- 'nargs': '+',
192
- },
162
+ )
163
+ no_color: bool = field(
164
+ FieldMetadata(
165
+ action='store_true',
166
+ ),
167
+ description='Disable colored output',
168
+ default=False,
169
+ )
170
+
171
+ def model_post_init(self, __context: Any) -> None:
172
+ super().model_post_init(__context)
173
+
174
+ setup_logging(self.verbose, self.log_file, not self.no_color)
175
+
176
+
177
+ class DependencyDrivenBuildArguments(GlobalArguments):
178
+ manifest_files: t.Optional[t.List[t.Union[Path, str]]] = field(
179
+ FieldMetadata(
180
+ validate_method=[ValidateMethod.TO_LIST],
181
+ deprecates={
182
+ 'manifest_file': {
183
+ 'nargs': '+',
193
184
  },
194
- description='Path to the manifest files which contains the build test rules of the apps',
195
- nargs='+',
196
- )
185
+ },
186
+ nargs='+',
197
187
  ),
188
+ description='Path to the manifest files which contains the build test rules of the apps',
189
+ validation_alias=AliasChoices('manifest_files', 'manifest_file'),
190
+ default=None,
198
191
  )
199
192
  manifest_rootpath: str = field(
193
+ None,
194
+ description='Root path to resolve the relative paths defined in the manifest files. '
195
+ 'By default set to the current directory',
200
196
  default=os.curdir,
201
- metadata=asdict(
202
- FieldMetadata(
203
- description='Root path to resolve the relative paths defined in the manifest files. '
204
- 'By default set to the current directory',
205
- )
206
- ),
207
197
  )
208
198
  modified_components: t.Optional[t.List[str]] = field(
209
- default=None,
210
- metadata=asdict(
211
- FieldMetadata(
212
- description='semicolon-separated list of modified components',
213
- type=semicolon_separated_str_to_list,
214
- )
199
+ FieldMetadata(
200
+ validate_method=[ValidateMethod.TO_LIST],
201
+ type=semicolon_separated_str_to_list,
215
202
  ),
203
+ description='semicolon-separated list of modified components. '
204
+ 'If set to "", the value would be considered as None. '
205
+ 'If set to ";", the value would be considered as an empty list.',
206
+ default=None,
216
207
  )
217
208
  modified_files: t.Optional[t.List[str]] = field(
218
- default=None,
219
- metadata=asdict(
220
- FieldMetadata(
221
- description='semicolon-separated list of modified files',
222
- type=semicolon_separated_str_to_list,
223
- )
209
+ FieldMetadata(
210
+ validate_method=[ValidateMethod.TO_LIST],
211
+ type=semicolon_separated_str_to_list,
224
212
  ),
213
+ description='semicolon-separated list of modified files. '
214
+ 'If set to "", the value would be considered as None. '
215
+ 'If set to ";", the value would be considered as an empty list.',
216
+ default=None,
225
217
  )
226
- ignore_app_dependencies_components: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
227
218
  deactivate_dependency_driven_build_by_components: t.Optional[t.List[str]] = field(
228
- default=None,
229
- metadata=asdict(
230
- FieldMetadata(
231
- deprecates={
232
- 'ignore_app_dependencies_components': {
233
- 'type': semicolon_separated_str_to_list,
234
- 'shorthand': '-ic',
235
- }
236
- },
237
- description='semicolon-separated list of components. '
238
- 'dependency-driven build feature will be deactivated when any of these components are modified',
239
- type=semicolon_separated_str_to_list,
240
- shorthand='-dc',
241
- )
219
+ FieldMetadata(
220
+ validate_method=[ValidateMethod.TO_LIST],
221
+ deprecates={
222
+ 'ignore_app_dependencies_components': {
223
+ 'type': semicolon_separated_str_to_list,
224
+ 'shorthand': '-ic',
225
+ }
226
+ },
227
+ type=semicolon_separated_str_to_list,
228
+ shorthand='-dc',
229
+ ),
230
+ description='semicolon-separated list of components. '
231
+ 'dependency-driven build feature will be deactivated when any of these components are modified. '
232
+ 'Must be specified together with --modified-components. '
233
+ 'If set to "", the value would be considered as None. '
234
+ 'If set to ";", the value would be considered as an empty list.',
235
+ validation_alias=AliasChoices(
236
+ 'deactivate_dependency_driven_build_by_components', 'ignore_app_dependencies_components'
242
237
  ),
238
+ default=None,
243
239
  )
244
- ignore_app_dependencies_filepatterns: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
245
240
  deactivate_dependency_driven_build_by_filepatterns: t.Optional[t.List[str]] = field(
246
- default=None,
247
- metadata=asdict(
248
- FieldMetadata(
249
- deprecates={
250
- 'ignore_app_dependencies_filepatterns': {
251
- 'type': semicolon_separated_str_to_list,
252
- 'shorthand': '-if',
253
- }
254
- },
255
- description='semicolon-separated list of file patterns. '
256
- 'dependency-driven build feature will be deactivated when any of matched files are modified',
257
- type=semicolon_separated_str_to_list,
258
- shorthand='-df',
259
- )
241
+ FieldMetadata(
242
+ validate_method=[ValidateMethod.TO_LIST],
243
+ deprecates={
244
+ 'ignore_app_dependencies_filepatterns': {
245
+ 'type': semicolon_separated_str_to_list,
246
+ 'shorthand': '-if',
247
+ }
248
+ },
249
+ type=semicolon_separated_str_to_list,
250
+ shorthand='-df',
251
+ ),
252
+ description='semicolon-separated list of file patterns. '
253
+ 'dependency-driven build feature will be deactivated when any of matched files are modified. '
254
+ 'Must be specified together with --modified-files. '
255
+ 'If set to "", the value would be considered as None. '
256
+ 'If set to ";", the value would be considered as an empty list.',
257
+ validation_alias=AliasChoices(
258
+ 'deactivate_dependency_driven_build_by_filepatterns', 'ignore_app_dependencies_filepatterns'
260
259
  ),
260
+ default=None,
261
261
  )
262
262
  check_manifest_rules: bool = field(
263
- default=False,
264
- metadata=asdict(
265
- FieldMetadata(
266
- description='Check if all folders defined in the manifest files exist. Fail if not',
267
- action='store_true',
268
- )
263
+ FieldMetadata(
264
+ action='store_true',
269
265
  ),
266
+ description='Check if all folders defined in the manifest files exist. Fail if not',
267
+ default=False,
270
268
  )
271
269
  compare_manifest_sha_filepath: t.Optional[str] = field(
270
+ None,
271
+ description='Path to the file containing the sha256 hash of the manifest rules. '
272
+ 'Compare the hash with the current manifest rules. '
273
+ 'All matched apps will be built if the corresponding manifest rule is modified',
272
274
  default=None,
273
- metadata=asdict(
274
- FieldMetadata(
275
- description='Path to the file containing the sha256 hash of the manifest rules. '
276
- 'Compare the hash with the current manifest rules. '
277
- 'All matched apps will be built if the cooresponding manifest rule is modified',
278
- )
279
- ),
280
275
  )
281
276
 
282
- def __post_init__(
283
- self,
284
- manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
285
- ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
286
- ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
287
- ):
288
- super().__post_init__()
277
+ def model_post_init(self, __context: Any) -> None:
278
+ super().model_post_init(__context)
289
279
 
290
- self.set_deprecated_field('manifest_files', 'manifest_file', manifest_file)
291
- self.set_deprecated_field(
292
- 'deactivate_dependency_driven_build_by_components',
293
- 'ignore_app_dependencies_components',
294
- ignore_app_dependencies_components,
295
- )
296
- self.set_deprecated_field(
297
- 'deactivate_dependency_driven_build_by_filepatterns',
298
- 'ignore_app_dependencies_filepatterns',
299
- ignore_app_dependencies_filepatterns,
300
- )
301
-
302
- self.manifest_files = to_list(self.manifest_files)
303
- self.modified_components = to_list(self.modified_components)
304
- self.modified_files = to_list(self.modified_files)
305
- self.deactivate_dependency_driven_build_by_components = to_list(
306
- self.deactivate_dependency_driven_build_by_components
307
- )
308
- self.deactivate_dependency_driven_build_by_filepatterns = to_list(
309
- self.deactivate_dependency_driven_build_by_filepatterns
310
- )
311
-
312
- # Validation
313
280
  Manifest.CHECK_MANIFEST_RULES = self.check_manifest_rules
314
281
  if self.manifest_files:
315
282
  App.MANIFEST = Manifest.from_files(
@@ -379,234 +346,159 @@ class DependencyDrivenBuildArguments(GlobalArguments):
379
346
  return None
380
347
 
381
348
 
382
- def _os_curdir_as_list() -> t.List[str]:
383
- return [os.curdir]
384
-
385
-
386
- @dataclass
387
349
  class FindBuildArguments(DependencyDrivenBuildArguments):
388
- """
389
- Arguments used in both find and build commands
390
- """
391
-
392
350
  paths: t.List[str] = field(
393
- default_factory=_os_curdir_as_list,
394
- metadata=asdict(
395
- FieldMetadata(
396
- default=os.curdir,
397
- description='Paths to the directories containing the apps. By default set to the current directory',
398
- shorthand='-p',
399
- nargs='*',
400
- )
351
+ FieldMetadata(
352
+ validate_method=[ValidateMethod.TO_LIST],
353
+ shorthand='-p',
354
+ nargs='*',
401
355
  ),
356
+ description='Paths to the directories containing the apps. By default set to the current directory',
357
+ default=os.curdir,
402
358
  )
403
359
  target: str = field(
404
- default='all',
405
- metadata=asdict(
406
- FieldMetadata(
407
- description='Filter the apps by target. By default set to "all"',
408
- shorthand='-t',
409
- )
360
+ FieldMetadata(
361
+ shorthand='-t',
410
362
  ),
363
+ description='Filter the apps by target. By default set to "all"',
364
+ default='all',
411
365
  )
412
366
  build_system: t.Union[str, t.Type[App]] = field(
413
- default='cmake',
414
- metadata=asdict(
415
- FieldMetadata(
416
- description='Filter the apps by build system. By default set to "cmake"',
417
- choices=['cmake', 'make'],
418
- )
367
+ FieldMetadata(
368
+ choices=['cmake', 'make'],
419
369
  ),
370
+ description='Filter the apps by build system. By default set to "cmake"',
371
+ default='cmake',
420
372
  )
421
373
  recursive: bool = field(
422
- default=False,
423
- metadata=asdict(
424
- FieldMetadata(
425
- description='Search for apps recursively under the specified paths',
426
- action='store_true',
427
- )
374
+ FieldMetadata(
375
+ action='store_true',
428
376
  ),
377
+ description='Search for apps recursively under the specified paths',
378
+ default=False,
429
379
  )
430
- exclude_list: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
431
380
  exclude: t.Optional[t.List[str]] = field(
432
- default=None,
433
- metadata=asdict(
434
- FieldMetadata(
435
- description='Ignore the specified directories while searching recursively',
436
- nargs='+',
437
- )
381
+ FieldMetadata(
382
+ validate_method=[ValidateMethod.TO_LIST],
383
+ nargs='+',
438
384
  ),
385
+ description='Ignore the specified directories while searching recursively',
386
+ validation_alias=AliasChoices('exclude', 'exclude_list'),
387
+ default=None,
439
388
  )
440
389
  work_dir: t.Optional[str] = field(
390
+ None,
391
+ description='Copy the app to this directory before building. '
392
+ 'By default set to the app directory. Can expand placeholders',
441
393
  default=None,
442
- metadata=asdict(
443
- FieldMetadata(
444
- description='Copy the app to this directory before building. '
445
- 'By default set to the app directory. Can expand placeholders',
446
- )
447
- ),
448
394
  )
449
395
  build_dir: str = field(
396
+ None,
397
+ description='Build directory for the app. By default set to "build". '
398
+ 'When set to relative path, it will be treated as relative to the app directory. '
399
+ 'Can expand placeholders',
450
400
  default='build',
451
- metadata=asdict(
452
- FieldMetadata(
453
- description='Build directory for the app. By default set to "build". '
454
- 'When set to relative path, it will be treated as relative to the app directory. '
455
- 'Can expand placeholders',
456
- )
457
- ),
458
401
  )
459
- build_log: InitVar[t.Optional[str]] = _Field.UNSET
460
402
  build_log_filename: t.Optional[str] = field(
461
- default=None,
462
- metadata=asdict(
463
- FieldMetadata(
464
- deprecates={'build_log': {}},
465
- description='Log filename under the build directory instead of stdout. Can expand placeholders',
466
- )
403
+ FieldMetadata(
404
+ deprecates={'build_log': {}},
467
405
  ),
406
+ description='Log filename under the build directory instead of stdout. Can expand placeholders',
407
+ validation_alias=AliasChoices('build_log_filename', 'build_log'),
408
+ default=None,
468
409
  )
469
- size_file: InitVar[t.Optional[str]] = _Field.UNSET
470
410
  size_json_filename: t.Optional[str] = field(
471
- default=None,
472
- metadata=asdict(
473
- FieldMetadata(
474
- deprecates={'size_file': {}},
475
- description='`idf.py size` output file under the build directory when specified. '
476
- 'Can expand placeholders',
477
- )
411
+ FieldMetadata(
412
+ deprecates={'size_file': {}},
478
413
  ),
414
+ description='`idf.py size` output file under the build directory when specified. ' 'Can expand placeholders',
415
+ validation_alias=AliasChoices('size_json_filename', 'size_file'),
416
+ default=None,
479
417
  )
480
- config: InitVar[t.Union[t.List[str], str, None]] = _Field.UNSET # cli # type: ignore
481
- config_rules_str: InitVar[t.Union[t.List[str], str, None]] = _Field.UNSET # func # type: ignore
482
418
  config_rules: t.Optional[t.List[str]] = field(
419
+ FieldMetadata(
420
+ validate_method=[ValidateMethod.TO_LIST],
421
+ deprecates={
422
+ 'config': {'nargs': '+'},
423
+ },
424
+ nargs='+',
425
+ ),
426
+ description='Defines the rules of building the project with pre-set sdkconfig files. '
427
+ 'Supports FILENAME[=NAME] or FILEPATTERN format. '
428
+ 'FILENAME is the filename of the sdkconfig file, relative to the app directory. '
429
+ 'Optional NAME is the name of the configuration. '
430
+ 'if not specified, the filename is used as the name. '
431
+ 'FILEPATTERN is the filename of the sdkconfig file with a single wildcard character (*). '
432
+ 'The NAME is the value matched by the wildcard',
433
+ validation_alias=AliasChoices('config_rules', 'config_rules_str', 'config'),
483
434
  default=None,
484
- metadata=asdict(
485
- FieldMetadata(
486
- deprecates={
487
- 'config': {'nargs': '+'},
488
- },
489
- description='Defines the rules of building the project with pre-set sdkconfig files. '
490
- 'Supports FILENAME[=NAME] or FILEPATTERN format. '
491
- 'FILENAME is the filename of the sdkconfig file, relative to the app directory. '
492
- 'Optional NAME is the name of the configuration. '
493
- 'if not specified, the filename is used as the name. '
494
- 'FILEPATTERN is the filename of the sdkconfig file with a single wildcard character (*). '
495
- 'The NAME is the value matched by the wildcard',
496
- nargs='+',
497
- )
498
- ),
499
435
  )
500
436
  override_sdkconfig_items: t.Optional[str] = field(
437
+ None,
438
+ description='A comma-separated list of key=value pairs to override the sdkconfig items',
501
439
  default=None,
502
- metadata=asdict(
503
- FieldMetadata(
504
- description='A comma-separated list of key=value pairs to override the sdkconfig items',
505
- )
506
- ),
507
440
  )
508
441
  override_sdkconfig_files: t.Optional[str] = field(
442
+ None,
443
+ description='A comma-separated list of sdkconfig files to override the sdkconfig items. '
444
+ 'When set to relative path, it will be treated as relative to the current directory',
509
445
  default=None,
510
- metadata=asdict(
511
- FieldMetadata(
512
- description='A comma-separated list of sdkconfig files to override the sdkconfig items. '
513
- 'When set to relative path, it will be treated as relative to the current directory',
514
- )
515
- ),
516
446
  )
517
447
  sdkconfig_defaults: t.Optional[str] = field(
448
+ None,
449
+ description='A semicolon-separated list of sdkconfig files passed to `idf.py -DSDKCONFIG_DEFAULTS`. '
450
+ 'SDKCONFIG_DEFAULTS environment variable is used when not specified',
518
451
  default=os.getenv('SDKCONFIG_DEFAULTS', None),
519
- metadata=asdict(
520
- FieldMetadata(
521
- description='A semicolon-separated list of sdkconfig files passed to `idf.py -DSDKCONFIG_DEFAULTS`. '
522
- 'SDKCONFIG_DEFAULTS environment variable is used when not specified',
523
- )
524
- ),
525
452
  )
526
453
  check_warnings: bool = field(
527
- default=False,
528
- metadata=asdict(
529
- FieldMetadata(
530
- description='Check for warnings in the build output. Fail if any warnings are found',
531
- action='store_true',
532
- )
454
+ FieldMetadata(
455
+ action='store_true',
533
456
  ),
457
+ description='Check for warnings in the build output. Fail if any warnings are found',
458
+ default=False,
534
459
  )
535
460
  default_build_targets: t.Optional[t.List[str]] = field(
536
- default=None,
537
- metadata=asdict(
538
- FieldMetadata(
539
- description='space-separated list of the default enabled build targets for the apps. '
540
- 'When not specified, the default value is the targets listed by `idf.py --list-targets`',
541
- )
461
+ FieldMetadata(
462
+ validate_method=[ValidateMethod.TO_LIST],
463
+ nargs='+',
542
464
  ),
465
+ description='space-separated list of the default enabled build targets for the apps. '
466
+ 'When not specified, the default value is the targets listed by `idf.py --list-targets`',
467
+ default=None,
543
468
  )
544
469
  enable_preview_targets: bool = field(
545
- default=False,
546
- metadata=asdict(
547
- FieldMetadata(
548
- description='When enabled, the default build targets will be set to all apps, '
549
- 'including the preview targets. As the targets defined in `idf.py --list-targets --preview`',
550
- action='store_true',
551
- )
470
+ FieldMetadata(
471
+ action='store_true',
552
472
  ),
473
+ description='When enabled, the default build targets will be set to all apps, '
474
+ 'including the preview targets. As the targets defined in `idf.py --list-targets --preview`',
475
+ default=False,
553
476
  )
554
477
  include_skipped_apps: bool = field(
555
- default=False,
556
- metadata=asdict(
557
- FieldMetadata(
558
- description='Include the skipped apps in the output, together with the enabled ones',
559
- action='store_true',
560
- )
478
+ FieldMetadata(
479
+ action='store_true',
561
480
  ),
481
+ description='Include the skipped apps in the output, together with the enabled ones',
482
+ default=False,
562
483
  )
563
484
  include_disabled_apps: bool = field(
564
- default=False,
565
- metadata=asdict(
566
- FieldMetadata(
567
- description='Include the disabled apps in the output, together with the enabled ones',
568
- action='store_true',
569
- )
485
+ FieldMetadata(
486
+ action='store_true',
570
487
  ),
488
+ description='Include the disabled apps in the output, together with the enabled ones',
489
+ default=False,
571
490
  )
572
491
  include_all_apps: bool = field(
573
- default=False,
574
- metadata=asdict(
575
- FieldMetadata(
576
- description='Include skipped, and disabled apps in the output, together with the enabled ones',
577
- action='store_true',
578
- )
492
+ FieldMetadata(
493
+ action='store_true',
579
494
  ),
495
+ description='Include skipped, and disabled apps in the output, together with the enabled ones',
496
+ default=False,
580
497
  )
581
498
 
582
- def __post_init__( # type: ignore
583
- self,
584
- manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
585
- ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
586
- ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
587
- exclude_list: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
588
- build_log: t.Optional[str] = _Field.UNSET, # type: ignore
589
- size_file: t.Optional[str] = _Field.UNSET, # type: ignore
590
- config: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
591
- config_rules_str: t.Union[t.List[str], str, None] = _Field.UNSET, # type: ignore
592
- ):
593
- super().__post_init__(
594
- manifest_file=manifest_file,
595
- ignore_app_dependencies_components=ignore_app_dependencies_components,
596
- ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
597
- )
598
-
599
- self.set_deprecated_field('exclude', 'exclude_list', exclude_list)
600
- self.set_deprecated_field('build_log_filename', 'build_log', build_log)
601
- self.set_deprecated_field('size_json_filename', 'size_file', size_file)
602
- self.set_deprecated_field('config_rules', 'config', config)
603
- self.set_deprecated_field('config_rules', 'config_rules_str', config_rules_str)
499
+ def model_post_init(self, __context: Any) -> None:
500
+ super().model_post_init(__context)
604
501
 
605
- self.paths = to_list(self.paths)
606
- self.config_rules = to_list(self.config_rules)
607
- self.exclude = to_list(self.exclude)
608
-
609
- # Validation
610
502
  if not self.paths:
611
503
  raise InvalidCommand('At least one path must be specified')
612
504
 
@@ -636,54 +528,26 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
636
528
  SESSION_ARGS.set(self)
637
529
 
638
530
 
639
- @dataclass
640
531
  class FindArguments(FindBuildArguments):
641
- """
642
- Arguments used in the find command
643
- """
644
-
645
532
  output: t.Optional[str] = field(
646
- default=None,
647
- metadata=asdict(
648
- FieldMetadata(
649
- description='Record the found apps to the specified file instead of stdout',
650
- shorthand='-o',
651
- )
533
+ FieldMetadata(
534
+ shorthand='-o',
652
535
  ),
536
+ description='Record the found apps to the specified file instead of stdout',
537
+ default=None,
653
538
  )
654
539
  output_format: str = field(
655
- default='raw',
656
- metadata=asdict(
657
- FieldMetadata(
658
- description='Output format of the found apps. '
659
- 'In "raw" format, each line is a json string serialized from the app model. '
660
- 'In "json" format, the output is a json list of the serialized app models',
661
- choices=['raw', 'json'],
662
- )
540
+ FieldMetadata(
541
+ choices=['raw', 'json'],
663
542
  ),
543
+ description='Output format of the found apps. '
544
+ 'In "raw" format, each line is a json string serialized from the app model. '
545
+ 'In "json" format, the output is a json list of the serialized app models',
546
+ default='raw',
664
547
  )
665
548
 
666
- def __post_init__( # type: ignore
667
- self,
668
- manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
669
- ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
670
- ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
671
- exclude_list: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
672
- build_log: t.Optional[str] = _Field.UNSET, # type: ignore
673
- size_file: t.Optional[str] = _Field.UNSET, # type: ignore
674
- config: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
675
- config_rules_str: t.Union[t.List[str], str, None] = _Field.UNSET, # type: ignore
676
- ):
677
- super().__post_init__(
678
- manifest_file=manifest_file,
679
- ignore_app_dependencies_components=ignore_app_dependencies_components,
680
- ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
681
- exclude_list=exclude_list,
682
- build_log=build_log,
683
- size_file=size_file,
684
- config=config,
685
- config_rules_str=config_rules_str,
686
- )
549
+ def model_post_init(self, __context: Any) -> None:
550
+ super().model_post_init(__context)
687
551
 
688
552
  if self.include_all_apps:
689
553
  self.include_skipped_apps = True
@@ -694,264 +558,204 @@ class FindArguments(FindBuildArguments):
694
558
  self.output_format = 'json'
695
559
 
696
560
 
697
- @dataclass
698
561
  class BuildArguments(FindBuildArguments):
699
562
  build_verbose: bool = field(
700
- default=False,
701
- metadata=asdict(
702
- FieldMetadata(
703
- description='Enable verbose output of the build system',
704
- action='store_true',
705
- )
563
+ FieldMetadata(
564
+ action='store_true',
706
565
  ),
566
+ description='Enable verbose output of the build system',
567
+ default=False,
707
568
  )
708
569
  parallel_count: int = field(
709
- default=1,
710
- metadata=asdict(
711
- FieldMetadata(
712
- description='Number of parallel build jobs in total. '
713
- 'Specified together with --parallel-index. '
714
- 'The given apps will be divided into parallel_count parts, '
715
- 'and the current run will build the parallel_index-th part',
716
- type=int,
717
- )
570
+ FieldMetadata(
571
+ type=int,
718
572
  ),
573
+ description='Number of parallel build jobs in total. '
574
+ 'Specified together with --parallel-index. '
575
+ 'The given apps will be divided into parallel_count parts, '
576
+ 'and the current run will build the parallel_index-th part',
577
+ default=1,
719
578
  )
720
579
  parallel_index: int = field(
721
- default=1,
722
- metadata=asdict(
723
- FieldMetadata(
724
- description='Index (1-based) of the parallel build job. '
725
- 'Specified together with --parallel-count. '
726
- 'The given apps will be divided into parallel_count parts, '
727
- 'and the current run will build the parallel_index-th part',
728
- type=int,
729
- )
580
+ FieldMetadata(
581
+ type=int,
730
582
  ),
583
+ description='Index (1-based) of the parallel build job. '
584
+ 'Specified together with --parallel-count. '
585
+ 'The given apps will be divided into parallel_count parts, '
586
+ 'and the current run will build the parallel_index-th part',
587
+ default=1,
731
588
  )
732
589
  dry_run: bool = field(
733
- default=False,
734
- metadata=asdict(
735
- FieldMetadata(
736
- description='Skip the actual build, only print the build process',
737
- action='store_true',
738
- )
590
+ FieldMetadata(
591
+ action='store_true',
739
592
  ),
593
+ description='Skip the actual build, only print the build process',
594
+ default=False,
740
595
  )
741
596
  keep_going: bool = field(
742
- default=False,
743
- metadata=asdict(
744
- FieldMetadata(
745
- description='Continue building the next app when the current build fails',
746
- action='store_true',
747
- )
597
+ FieldMetadata(
598
+ action='store_true',
748
599
  ),
749
- )
750
- no_preserve: bool = field(
600
+ description='Continue building the next app when the current build fails',
751
601
  default=False,
752
- metadata=asdict(
753
- FieldMetadata(
754
- description='Do not preserve the build directory after a successful build',
755
- action='store_true',
756
- )
757
- ),
758
602
  )
759
- collect_size_info: t.Optional[str] = field(
760
- default=None,
761
- metadata=asdict(
762
- FieldMetadata(
763
- description='Record size json filepath of the built apps to the specified file. '
764
- 'Each line is a json string. Can expand placeholders @p',
765
- )
766
- ),
767
- )
768
- _collect_size_info: t.Optional[str] = field(init=False, repr=False, default=None)
769
- collect_app_info: t.Optional[str] = field(
770
- default=None,
771
- metadata=asdict(
772
- FieldMetadata(
773
- description='Record serialized app model of the built apps to the specified file. '
774
- 'Each line is a json string. Can expand placeholders @p',
775
- )
603
+ no_preserve: bool = field(
604
+ FieldMetadata(
605
+ action='store_true',
776
606
  ),
607
+ description='Do not preserve the build directory after a successful build',
608
+ default=False,
777
609
  )
778
- _collect_app_info: t.Optional[str] = field(init=False, repr=False, default=None)
779
- ignore_warning_str: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
780
610
  ignore_warning_strs: t.Optional[t.List[str]] = field(
611
+ FieldMetadata(
612
+ validate_method=[ValidateMethod.TO_LIST],
613
+ deprecates={
614
+ 'ignore_warning_str': {'nargs': '+'},
615
+ },
616
+ nargs='+',
617
+ ),
618
+ description='space-separated list of patterns. '
619
+ 'Ignore the warnings in the build output that match the patterns',
620
+ validation_alias=AliasChoices('ignore_warning_strs', 'ignore_warning_str'),
781
621
  default=None,
782
- metadata=asdict(
783
- FieldMetadata(
784
- deprecates={
785
- 'ignore_warning_str': {'nargs': '+'},
786
- },
787
- description='space-separated list of patterns. '
788
- 'Ignore the warnings in the build output that match the patterns',
789
- nargs='+',
790
- )
791
- ),
792
622
  )
793
- ignore_warning_file: InitVar[t.Optional[str]] = _Field.UNSET
794
- ignore_warning_files: t.Optional[t.List[str]] = field(
623
+ ignore_warning_files: t.Optional[t.List[t.Union[str, TextIOWrapper]]] = field(
624
+ FieldMetadata(
625
+ validate_method=[ValidateMethod.TO_LIST],
626
+ deprecates={
627
+ 'ignore_warning_file': {
628
+ 'type': argparse.FileType('r'),
629
+ }
630
+ },
631
+ nargs='+',
632
+ type=argparse.FileType('r'),
633
+ ),
634
+ description='Path to the files containing the patterns to ignore the warnings in the build output',
635
+ validation_alias=AliasChoices('ignore_warning_files', 'ignore_warning_file'),
795
636
  default=None,
796
- metadata=asdict(
797
- FieldMetadata(
798
- deprecates={'ignore_warning_file': {}},
799
- description='Path to the files containing the patterns to ignore the warnings in the build output',
800
- nargs='+',
801
- )
802
- ),
803
637
  )
804
638
  copy_sdkconfig: bool = field(
639
+ FieldMetadata(
640
+ action='store_true',
641
+ ),
642
+ description='Copy the sdkconfig file to the build directory',
805
643
  default=False,
806
- metadata=asdict(
807
- FieldMetadata(
808
- description='Copy the sdkconfig file to the build directory',
809
- action='store_true',
810
- )
644
+ )
645
+
646
+ # Attrs that support placeholders
647
+ collect_size_info_filename: t.Optional[str] = field(
648
+ FieldMetadata(
649
+ deprecates={'collect_size_info': {}},
650
+ hidden=True,
811
651
  ),
652
+ description='Record size json filepath of the built apps to the specified file. '
653
+ 'Each line is a json string. Can expand placeholders @p',
654
+ validation_alias=AliasChoices('collect_size_info_filename', 'collect_size_info'),
655
+ default=None,
656
+ exclude=True, # computed field is used
812
657
  )
813
- junitxml: t.Optional[str] = field(
658
+ collect_app_info_filename: t.Optional[str] = field(
659
+ FieldMetadata(
660
+ deprecates={'collect_app_info': {}},
661
+ hidden=True,
662
+ ),
663
+ description='Record serialized app model of the built apps to the specified file. '
664
+ 'Each line is a json string. Can expand placeholders @p',
665
+ validation_alias=AliasChoices('collect_app_info_filename', 'collect_app_info'),
814
666
  default=None,
815
- metadata=asdict(
816
- FieldMetadata(
817
- description='Path to the junitxml file to record the build results. Can expand placeholder @p',
818
- )
667
+ exclude=True, # computed field is used
668
+ )
669
+ junitxml_filename: t.Optional[str] = field(
670
+ FieldMetadata(
671
+ deprecates={'junitxml': {}},
672
+ hidden=True,
819
673
  ),
674
+ description='Path to the junitxml file to record the build results. Can expand placeholder @p',
675
+ validation_alias=AliasChoices('junitxml_filename', 'junitxml'),
676
+ default=None,
677
+ exclude=True, # computed field is used
820
678
  )
821
- _junitxml: t.Optional[str] = field(init=False, repr=False, default=None)
822
-
823
679
  # used for expanding placeholders
824
680
  PARALLEL_INDEX_PLACEHOLDER: t.ClassVar[str] = '@p' # replace it with the parallel index
825
681
 
826
- def __post_init__( # type: ignore
827
- self,
828
- manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
829
- ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
830
- ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
831
- exclude_list: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
832
- build_log: t.Optional[str] = _Field.UNSET, # type: ignore
833
- size_file: t.Optional[str] = _Field.UNSET, # type: ignore
834
- config: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
835
- config_rules_str: t.Union[t.List[str], str, None] = _Field.UNSET, # type: ignore
836
- ignore_warning_str: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
837
- ignore_warning_file: t.Optional[str] = _Field.UNSET, # type: ignore
838
- ):
839
- super().__post_init__(
840
- manifest_file=manifest_file,
841
- ignore_app_dependencies_components=ignore_app_dependencies_components,
842
- ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
843
- exclude_list=exclude_list,
844
- build_log=build_log,
845
- size_file=size_file,
846
- config=config,
847
- config_rules_str=config_rules_str,
848
- )
849
-
850
- self.set_deprecated_field('ignore_warning_strs', 'ignore_warning_str', ignore_warning_str)
851
- self.set_deprecated_field('ignore_warning_files', 'ignore_warning_file', ignore_warning_file)
852
-
853
- self.ignore_warning_strs = to_list(self.ignore_warning_strs) or []
682
+ def model_post_init(self, __context: Any) -> None:
683
+ super().model_post_init(__context)
854
684
 
855
685
  ignore_warnings_regexes = []
856
686
  if self.ignore_warning_strs:
857
687
  for s in self.ignore_warning_strs:
858
688
  ignore_warnings_regexes.append(re.compile(s))
859
689
  if self.ignore_warning_files:
860
- for s in self.ignore_warning_files:
861
- ignore_warnings_regexes.append(re.compile(s.strip()))
690
+ for f in self.ignore_warning_files:
691
+ if isinstance(f, str):
692
+ with open(f) as fr:
693
+ for s in fr:
694
+ ignore_warnings_regexes.append(re.compile(s.strip()))
695
+ else:
696
+ for s in f:
697
+ ignore_warnings_regexes.append(re.compile(s.strip()))
862
698
  App.IGNORE_WARNS_REGEXES = ignore_warnings_regexes
863
699
 
864
- if not isinstance(BuildArguments.collect_size_info, property):
865
- self._collect_size_info = self.collect_size_info
866
- BuildArguments.collect_size_info = property( # type: ignore
867
- BuildArguments._get_collect_size_info,
868
- BuildArguments._set_collect_size_info,
869
- )
870
-
871
- if not isinstance(BuildArguments.collect_app_info, property):
872
- self._collect_app_info = self.collect_app_info
873
- BuildArguments.collect_app_info = property( # type: ignore
874
- BuildArguments._get_collect_app_info,
875
- BuildArguments._set_collect_app_info,
876
- )
877
-
878
- if not isinstance(BuildArguments.junitxml, property):
879
- self._junitxml = self.junitxml
880
- BuildArguments.junitxml = property( # type: ignore
881
- BuildArguments._get_junitxml,
882
- BuildArguments._set_junitxml,
883
- )
884
-
885
- def _get_collect_size_info(self) -> t.Optional[str]:
886
- return (
887
- self._collect_size_info.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
888
- if self._collect_size_info
889
- else None
890
- )
700
+ @computed_field # type: ignore
701
+ @property
702
+ def collect_size_info(self) -> t.Optional[str]:
703
+ if self.collect_size_info_filename:
704
+ return self.collect_size_info_filename.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
891
705
 
892
- def _set_collect_size_info(self, k: str) -> None:
893
- self._collect_size_info = k
706
+ return None
894
707
 
895
- def _get_collect_app_info(self) -> t.Optional[str]:
896
- return (
897
- self._collect_app_info.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
898
- if self._collect_app_info
899
- else None
900
- )
708
+ @computed_field # type: ignore
709
+ @property
710
+ def collect_app_info(self) -> t.Optional[str]:
711
+ if self.collect_app_info_filename:
712
+ return self.collect_app_info_filename.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
901
713
 
902
- def _set_collect_app_info(self, k: str) -> None:
903
- self._collect_app_info = k
714
+ return None
904
715
 
905
- def _get_junitxml(self) -> t.Optional[str]:
906
- return (
907
- self._junitxml.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
908
- if self._junitxml
909
- else None
910
- )
716
+ @computed_field # type: ignore
717
+ @property
718
+ def junitxml(self) -> t.Optional[str]:
719
+ if self.junitxml_filename:
720
+ return self.junitxml_filename.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
911
721
 
912
- def _set_junitxml(self, k: str) -> None:
913
- self._junitxml = k
722
+ return None
914
723
 
915
724
 
916
- @dataclass
917
725
  class DumpManifestShaArguments(GlobalArguments):
918
- """
919
- Arguments used in the dump-manifest-sha command
920
- """
921
-
922
726
  manifest_files: t.Optional[t.List[str]] = field(
923
- default=None,
924
- metadata=asdict(
925
- FieldMetadata(
926
- description='Path to the manifest files which contains the build test rules of the apps',
927
- nargs='+',
928
- required=True,
929
- )
727
+ FieldMetadata(
728
+ validate_method=[ValidateMethod.TO_LIST],
729
+ nargs='+',
730
+ required=True,
930
731
  ),
732
+ description='Path to the manifest files which contains the build test rules of the apps',
733
+ default=None,
931
734
  )
735
+
932
736
  output: t.Optional[str] = field(
933
- default=None,
934
- metadata=asdict(
935
- FieldMetadata(
936
- description='Record the sha256 hash of the manifest rules to the specified file',
937
- shorthand='-o',
938
- required=True,
939
- )
737
+ FieldMetadata(
738
+ shorthand='-o',
739
+ required=True,
940
740
  ),
741
+ description='Path to the output file to record the sha256 hash of the manifest rules',
742
+ default=None,
941
743
  )
942
744
 
943
- def __post_init__(self):
944
- super().__post_init__()
745
+ def model_post_init(self, __context: Any) -> None:
746
+ super().model_post_init(__context)
945
747
 
946
- # Validation
947
- self.manifest_files = to_list(self.manifest_files)
948
748
  if not self.manifest_files:
949
749
  raise InvalidCommand('Manifest files are required to dump the SHA values.')
950
750
  if not self.output:
951
751
  raise InvalidCommand('Output file is required to dump the SHA values.')
952
752
 
953
753
 
954
- def add_arguments_to_parser(argument_cls: t.Type[GlobalArguments], parser: argparse.ArgumentParser) -> None:
754
+ def _snake_case_to_cli_arg_name(s: str) -> str:
755
+ return f'--{s.replace("_", "-")}'
756
+
757
+
758
+ def add_args_to_parser(argument_cls: t.Type[BaseArguments], parser: argparse.ArgumentParser) -> None:
955
759
  """
956
760
  Add arguments to the parser from the argument class.
957
761
 
@@ -960,48 +764,58 @@ def add_arguments_to_parser(argument_cls: t.Type[GlobalArguments], parser: argpa
960
764
  :param argument_cls: argument class
961
765
  :param parser: argparse parser
962
766
  """
963
- name_fields_dict = {f.name: f for f in fields(argument_cls)}
964
-
965
- def _snake_case_to_cli_arg_name(s: str) -> str:
966
- return f'--{s.replace("_", "-")}'
967
-
968
- for name, f in name_fields_dict.items():
969
- _meta = FieldMetadata(**f.metadata)
970
-
971
- desp = _meta.description
972
- # add deprecated fields
973
- if _meta.deprecates:
974
- for depr_k, depr_kwargs in _meta.deprecates.items():
975
- depr_kwargs['help'] = f'[DEPRECATED by {_snake_case_to_cli_arg_name(name)}] {desp}'
976
- short_name = depr_kwargs.pop('shorthand', None)
977
- _names = [_snake_case_to_cli_arg_name(depr_k)]
978
- if short_name:
979
- _names.append(short_name)
980
- parser.add_argument(*_names, **depr_kwargs)
981
-
982
- # args
983
- args = [_snake_case_to_cli_arg_name(name)]
984
- if _meta.shorthand:
985
- args.append(_meta.shorthand)
986
-
987
- # kwargs passed to add_argument
988
- kwargs = drop_none_kwargs(
989
- {
990
- 'help': desp,
991
- 'action': _meta.action,
992
- 'nargs': _meta.nargs,
993
- 'choices': _meta.choices,
994
- 'type': _meta.type,
995
- 'required': _meta.required,
996
- }
997
- )
998
- # default None is important for argparse
999
- kwargs['default'] = _meta.default or getattr(f, 'default', None)
767
+ for f_name, f in argument_cls.model_fields.items():
768
+ f_meta = get_meta(f)
769
+ if f_meta and f_meta.deprecates:
770
+ for dep_f_name, dep_f_kwargs in f_meta.deprecates.items():
771
+ _names = [_snake_case_to_cli_arg_name(dep_f_name)]
772
+ _shorthand = dep_f_kwargs.pop('shorthand', None)
773
+ if _shorthand:
774
+ _names.append(_shorthand)
775
+
776
+ if f_meta.hidden: # f is hidden, use deprecated field instead
777
+ help_msg = f.description
778
+ else:
779
+ help_msg = f'[Deprecated] Use {_snake_case_to_cli_arg_name(f_name)} instead'
780
+
781
+ parser.add_argument(
782
+ *_names,
783
+ **dep_f_kwargs,
784
+ help=help_msg,
785
+ )
1000
786
 
1001
- parser.add_argument(*args, **kwargs)
787
+ if f_meta and f_meta.hidden:
788
+ continue
789
+
790
+ names = [_snake_case_to_cli_arg_name(f_name)]
791
+ if f_meta and f_meta.shorthand:
792
+ names.append(f_meta.shorthand)
793
+
794
+ kwargs: t.Dict[str, t.Any] = {}
795
+ if f_meta:
796
+ if f_meta.type:
797
+ kwargs['type'] = f_meta.type
798
+ if f_meta.required:
799
+ kwargs['required'] = True
800
+ if f_meta.action:
801
+ kwargs['action'] = f_meta.action
802
+ if f_meta.nargs:
803
+ kwargs['nargs'] = f_meta.nargs
804
+ if f_meta.choices:
805
+ kwargs['choices'] = f_meta.choices
806
+ if f_meta.default:
807
+ kwargs['default'] = f_meta.default
808
+ if 'default' not in kwargs:
809
+ kwargs['default'] = f.default
810
+
811
+ parser.add_argument(
812
+ *names,
813
+ **kwargs,
814
+ help=f.description,
815
+ )
1002
816
 
1003
817
 
1004
- def add_arguments_to_obj_doc_as_params(argument_cls: t.Type[GlobalArguments], obj: t.Any = None) -> None:
818
+ def add_args_to_obj_doc_as_params(argument_cls: t.Type[GlobalArguments], obj: t.Any = None) -> None:
1005
819
  """
1006
820
  Add arguments to the function as parameters.
1007
821
 
@@ -1009,14 +823,24 @@ def add_arguments_to_obj_doc_as_params(argument_cls: t.Type[GlobalArguments], ob
1009
823
  :param obj: object to add the docstring to
1010
824
  """
1011
825
  _obj = obj or argument_cls
1012
- _docs_s = _obj.__doc__ or ''
1013
- _docs_s += '\n'
826
+ _doc_str = _obj.__doc__ or ''
827
+ _doc_str += '\n'
1014
828
 
1015
- for f in fields(argument_cls):
829
+ for f_name, f in argument_cls.model_fields.items():
1016
830
  # typing generic alias is not a class
1017
- _annotation = f.type.__name__ if inspect.isclass(f.type) else f.type
831
+ _annotation = f.annotation.__name__ if inspect.isclass(f.annotation) else f.annotation
832
+ _doc_str += f' :param {f_name}: {f.description}\n'
833
+ _doc_str += f' :type {f_name}: {_annotation}\n'
834
+
835
+ _obj.__doc__ = _doc_str
836
+
1018
837
 
1019
- _docs_s += f' :param {f.name}: {f.metadata.get("description", "")}\n'
1020
- _docs_s += f' :type {f.name}: {_annotation}\n'
838
+ def apply_config_file(config_file: t.Optional[str]) -> None:
839
+ def _subclasses(klass: t.Type[T]) -> t.Set[t.Type[T]]:
840
+ return set(klass.__subclasses__()).union([s for c in klass.__subclasses__() for s in _subclasses(c)])
1021
841
 
1022
- _obj.__doc__ = _docs_s
842
+ if config_file:
843
+ BaseArguments.model_config['toml_file'] = str(config_file)
844
+ # modify all subclasses
845
+ for cls in _subclasses(BaseArguments):
846
+ cls.model_config['toml_file'] = str(config_file)