idf-build-apps 2.5.0rc1__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
104
+
105
+ return None
154
106
 
155
- if config_dict:
156
- for name, value in config_dict.items():
157
- if hasattr(self, name):
158
- setattr(self, name, value)
159
107
 
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)
108
+ class BaseArguments(BaseSettings):
109
+ """Base settings class for all settings classes"""
162
110
 
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
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
+ )
166
116
 
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.'
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__()
262
+ def model_post_init(self, __context: Any) -> None:
263
+ super().model_post_init(__context)
289
264
 
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
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
- )
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)
604
-
605
- self.paths = to_list(self.paths)
606
- self.config_rules = to_list(self.config_rules)
607
- self.exclude = to_list(self.exclude)
481
+ def model_post_init(self, __context: Any) -> None:
482
+ super().model_post_init(__context)
608
483
 
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,163 +540,114 @@ 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
  ),
749
- )
750
- no_preserve: bool = field(
582
+ description='Continue building the next app when the current build fails',
751
583
  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
- )
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
584
  )
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
- )
585
+ no_preserve: bool = field(
586
+ FieldMetadata(
587
+ action='store_true',
776
588
  ),
589
+ description='Do not preserve the build directory after a successful build',
590
+ default=False,
777
591
  )
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
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'),
781
603
  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
604
  )
793
- ignore_warning_file: InitVar[t.Optional[str]] = _Field.UNSET
794
605
  ignore_warning_files: t.Optional[t.List[str]] = field(
795
- 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
- )
606
+ FieldMetadata(
607
+ deprecates={'ignore_warning_file': {}},
608
+ nargs='+',
802
609
  ),
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'),
612
+ default=None,
803
613
  )
804
614
  copy_sdkconfig: bool = field(
805
- default=False,
806
- metadata=asdict(
807
- FieldMetadata(
808
- description='Copy the sdkconfig file to the build directory',
809
- action='store_true',
810
- )
615
+ FieldMetadata(
616
+ action='store_true',
811
617
  ),
618
+ description='Copy the sdkconfig file to the build directory',
619
+ default=False,
812
620
  )
813
- junitxml: t.Optional[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'),
814
628
  default=None,
815
- metadata=asdict(
816
- FieldMetadata(
817
- description='Path to the junitxml file to record the build results. Can expand placeholder @p',
818
- )
819
- ),
629
+ exclude=True, # computed field is used
630
+ )
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
638
+ )
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'),
643
+ default=None,
644
+ exclude=True, # computed field is used
820
645
  )
821
- _junitxml: t.Optional[str] = field(init=False, repr=False, default=None)
822
-
823
646
  # used for expanding placeholders
824
647
  PARALLEL_INDEX_PLACEHOLDER: t.ClassVar[str] = '@p' # replace it with the parallel index
825
648
 
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 []
649
+ def model_post_init(self, __context: Any) -> None:
650
+ super().model_post_init(__context)
854
651
 
855
652
  ignore_warnings_regexes = []
856
653
  if self.ignore_warning_strs:
@@ -861,97 +658,65 @@ class BuildArguments(FindBuildArguments):
861
658
  ignore_warnings_regexes.append(re.compile(s.strip()))
862
659
  App.IGNORE_WARNS_REGEXES = ignore_warnings_regexes
863
660
 
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
- )
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))
891
666
 
892
- def _set_collect_size_info(self, k: str) -> None:
893
- self._collect_size_info = k
667
+ return None
894
668
 
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
- )
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))
901
674
 
902
- def _set_collect_app_info(self, k: str) -> None:
903
- self._collect_app_info = k
675
+ return None
904
676
 
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
- )
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))
911
682
 
912
- def _set_junitxml(self, k: str) -> None:
913
- self._junitxml = k
683
+ return None
914
684
 
915
685
 
916
- @dataclass
917
686
  class DumpManifestShaArguments(GlobalArguments):
918
- """
919
- Arguments used in the dump-manifest-sha command
920
- """
921
-
922
687
  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
- )
688
+ FieldMetadata(
689
+ validate_method=[ValidateMethod.TO_LIST],
690
+ nargs='+',
691
+ required=True,
930
692
  ),
693
+ description='Path to the manifest files which contains the build test rules of the apps',
694
+ default=None,
931
695
  )
696
+
932
697
  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
- )
698
+ FieldMetadata(
699
+ shorthand='-o',
700
+ required=True,
940
701
  ),
702
+ description='Path to the output file to record the sha256 hash of the manifest rules',
703
+ default=None,
941
704
  )
942
705
 
943
- def __post_init__(self):
944
- super().__post_init__()
706
+ def model_post_init(self, __context: Any) -> None:
707
+ super().model_post_init(__context)
945
708
 
946
- # Validation
947
- self.manifest_files = to_list(self.manifest_files)
948
709
  if not self.manifest_files:
949
710
  raise InvalidCommand('Manifest files are required to dump the SHA values.')
950
711
  if not self.output:
951
712
  raise InvalidCommand('Output file is required to dump the SHA values.')
952
713
 
953
714
 
954
- 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:
955
720
  """
956
721
  Add arguments to the parser from the argument class.
957
722
 
@@ -960,48 +725,50 @@ def add_arguments_to_parser(argument_cls: t.Type[GlobalArguments], parser: argpa
960
725
  :param argument_cls: argument class
961
726
  :param parser: argparse parser
962
727
  """
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)
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
+ )
1000
742
 
1001
- 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
+ )
1002
769
 
1003
770
 
1004
- 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:
1005
772
  """
1006
773
  Add arguments to the function as parameters.
1007
774
 
@@ -1009,14 +776,24 @@ def add_arguments_to_obj_doc_as_params(argument_cls: t.Type[GlobalArguments], ob
1009
776
  :param obj: object to add the docstring to
1010
777
  """
1011
778
  _obj = obj or argument_cls
1012
- _docs_s = _obj.__doc__ or ''
1013
- _docs_s += '\n'
779
+ _doc_str = _obj.__doc__ or ''
780
+ _doc_str += '\n'
1014
781
 
1015
- for f in fields(argument_cls):
782
+ for f_name, f in argument_cls.model_fields.items():
1016
783
  # typing generic alias is not a class
1017
- _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
+
1018
790
 
1019
- _docs_s += f' :param {f.name}: {f.metadata.get("description", "")}\n'
1020
- _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)])
1021
794
 
1022
- _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)