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