fprime-gds 3.6.2a1__py3-none-any.whl → 4.0.0__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.
Files changed (61) hide show
  1. fprime_gds/common/communication/adapters/ip.py +14 -9
  2. fprime_gds/common/communication/adapters/uart.py +34 -25
  3. fprime_gds/common/communication/ccsds/__init__.py +0 -0
  4. fprime_gds/common/communication/ccsds/apid.py +19 -0
  5. fprime_gds/common/communication/ccsds/chain.py +106 -0
  6. fprime_gds/common/communication/ccsds/space_data_link.py +196 -0
  7. fprime_gds/common/communication/ccsds/space_packet.py +129 -0
  8. fprime_gds/common/communication/framing.py +27 -32
  9. fprime_gds/common/decoders/ch_decoder.py +1 -1
  10. fprime_gds/common/decoders/event_decoder.py +9 -2
  11. fprime_gds/common/decoders/pkt_decoder.py +1 -1
  12. fprime_gds/common/distributor/distributor.py +6 -3
  13. fprime_gds/common/encoders/ch_encoder.py +2 -2
  14. fprime_gds/common/encoders/cmd_encoder.py +2 -2
  15. fprime_gds/common/encoders/event_encoder.py +2 -2
  16. fprime_gds/common/encoders/pkt_encoder.py +2 -2
  17. fprime_gds/common/encoders/seq_writer.py +2 -2
  18. fprime_gds/common/fpy/README.md +56 -0
  19. fprime_gds/common/fpy/SPEC.md +69 -0
  20. fprime_gds/common/fpy/__init__.py +0 -0
  21. fprime_gds/common/fpy/bytecode/__init__.py +0 -0
  22. fprime_gds/common/fpy/bytecode/directives.py +490 -0
  23. fprime_gds/common/fpy/codegen.py +1687 -0
  24. fprime_gds/common/fpy/grammar.lark +88 -0
  25. fprime_gds/common/fpy/main.py +40 -0
  26. fprime_gds/common/fpy/parser.py +239 -0
  27. fprime_gds/common/gds_cli/base_commands.py +1 -1
  28. fprime_gds/common/handlers.py +39 -0
  29. fprime_gds/common/loaders/fw_type_json_loader.py +54 -0
  30. fprime_gds/common/loaders/pkt_json_loader.py +125 -0
  31. fprime_gds/common/loaders/prm_json_loader.py +85 -0
  32. fprime_gds/common/logger/__init__.py +2 -2
  33. fprime_gds/common/pipeline/dictionaries.py +28 -2
  34. fprime_gds/common/pipeline/encoding.py +19 -0
  35. fprime_gds/common/pipeline/histories.py +4 -0
  36. fprime_gds/common/pipeline/standard.py +16 -2
  37. fprime_gds/common/templates/cmd_template.py +8 -0
  38. fprime_gds/common/templates/prm_template.py +81 -0
  39. fprime_gds/common/testing_fw/api.py +148 -1
  40. fprime_gds/common/testing_fw/pytest_integration.py +37 -3
  41. fprime_gds/common/tools/README.md +34 -0
  42. fprime_gds/common/tools/params.py +246 -0
  43. fprime_gds/common/utils/config_manager.py +6 -6
  44. fprime_gds/common/utils/data_desc_type.py +6 -1
  45. fprime_gds/executables/apps.py +189 -11
  46. fprime_gds/executables/cli.py +468 -127
  47. fprime_gds/executables/comm.py +5 -2
  48. fprime_gds/executables/data_product_writer.py +164 -165
  49. fprime_gds/executables/fprime_cli.py +3 -3
  50. fprime_gds/executables/run_deployment.py +13 -5
  51. fprime_gds/flask/static/js/vue-support/channel.js +1 -1
  52. fprime_gds/flask/static/js/vue-support/event.js +1 -1
  53. fprime_gds/plugin/definitions.py +86 -8
  54. fprime_gds/plugin/system.py +172 -58
  55. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0.dist-info}/METADATA +23 -21
  56. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0.dist-info}/RECORD +61 -41
  57. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0.dist-info}/WHEEL +1 -1
  58. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0.dist-info}/entry_points.txt +2 -0
  59. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0.dist-info/licenses}/LICENSE.txt +0 -0
  60. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0.dist-info/licenses}/NOTICE.txt +0 -0
  61. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0.dist-info}/top_level.txt +0 -0
@@ -11,12 +11,17 @@ code that they are importing.
11
11
  import argparse
12
12
  import datetime
13
13
  import errno
14
+ import functools
14
15
  import getpass
16
+ import inspect
15
17
  import itertools
16
18
  import os
17
19
  import platform
18
20
  import re
19
21
  import sys
22
+
23
+ import yaml
24
+
20
25
  from abc import ABC, abstractmethod
21
26
  from pathlib import Path
22
27
  from typing import Any, Dict, List, Tuple
@@ -30,7 +35,7 @@ from fprime_gds.common.transport import ThreadedTCPSocketClient
30
35
  from fprime_gds.common.utils.config_manager import ConfigManager
31
36
  from fprime_gds.executables.utils import find_app, find_dict, get_artifacts_root
32
37
  from fprime_gds.plugin.definitions import PluginType
33
- from fprime_gds.plugin.system import Plugins
38
+ from fprime_gds.plugin.system import Plugins, PluginsNotLoadedException
34
39
  from fprime_gds.common.zmq_transport import ZmqClient
35
40
 
36
41
 
@@ -81,22 +86,51 @@ class ParserBase(ABC):
81
86
  self.fill_parser(parser)
82
87
  return parser
83
88
 
89
+ @staticmethod
90
+ def safe_add_argument(parser, *flags, **keywords):
91
+ """Add an argument allowing duplicates
92
+
93
+ Add arguments to the parser (passes through *flags and **keywords) to the supplied parser. This method traps
94
+ errors to prevent duplicates from crashing the system when two plugins use the same flags.
95
+
96
+ Args:
97
+ parser: parser or argument group to add arguments to
98
+ *flags: positional arguments passed to `add_argument`
99
+ **keywords: key word arguments passed to `add_argument`
100
+ """
101
+ try:
102
+ parser.add_argument(*flags, **keywords)
103
+ except argparse.ArgumentError:
104
+ # flag has already been added, pass
105
+ pass
106
+
107
+ @classmethod
108
+ def add_arguments_from_specification(cls, parser, arguments):
109
+ """Safely add arguments to parser
110
+
111
+ In parsers and plugins, arguments are represented as a map of flag tuples to argparse keyword arguments. This
112
+ function will add arguments of that representation supplied as `arguments` to the supplied parser in a safe
113
+ collision-avoidant manner.
114
+
115
+ Args:
116
+ parser: argparse Parser or ArgumentGroup, or anything with an `add_argument` function
117
+ arguments: arguments specification
118
+
119
+ """
120
+ for flags, keywords in arguments.items():
121
+ cls.safe_add_argument(parser, *flags, **keywords)
122
+
84
123
  def fill_parser(self, parser):
85
- """ Fill supplied parser with arguments
124
+ """Fill supplied parser with arguments
86
125
 
87
126
  Fills the supplied parser with the arguments returned via the `get_arguments` method invocation. This
88
- implementation add the arguments directly to the parser.
127
+ implementation adds the arguments directly to the parser.
89
128
 
90
129
  Args:
91
130
  parser: parser to fill with arguments
92
131
 
93
132
  """
94
- for flags, keywords in self.get_arguments().items():
95
- try:
96
- parser.add_argument(*flags, **keywords)
97
- except argparse.ArgumentError:
98
- # flag has already been added, pass
99
- pass
133
+ self.add_arguments_from_specification(parser, self.get_arguments())
100
134
 
101
135
  def reproduce_cli_args(self, args_ns):
102
136
  """Reproduce the list of arguments needed on the command line"""
@@ -154,8 +188,34 @@ class ParserBase(ABC):
154
188
  Returns: namespace with processed results of arguments.
155
189
  """
156
190
 
157
- @staticmethod
191
+ @classmethod
192
+ def parse_known_args(
193
+ cls,
194
+ parser_classes,
195
+ description="No tool description provided",
196
+ arguments=None,
197
+ **kwargs,
198
+ ):
199
+ """Parse and post-process arguments
200
+
201
+ Create a parser for the given application using the description provided. This will then add all specified
202
+ ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
203
+ arguments methods will be called, and the final namespace will be returned. This will allow unknown arguments
204
+ which are returned as the last tuple result.
205
+
206
+ Args:
207
+ parser_classes: a list of ParserBase subclasses that will be used to
208
+ description: description passed ot the argument parser
209
+ arguments: arguments to process, None to use command line input
210
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
211
+ """
212
+ return cls._parse_args(
213
+ parser_classes, description, arguments, use_parse_known=True, **kwargs
214
+ )
215
+
216
+ @classmethod
158
217
  def parse_args(
218
+ cls,
159
219
  parser_classes,
160
220
  description="No tool description provided",
161
221
  arguments=None,
@@ -163,20 +223,52 @@ class ParserBase(ABC):
163
223
  ):
164
224
  """Parse and post-process arguments
165
225
 
226
+ Create a parser for the given application using the description provided. This will then add all specified
227
+ ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
228
+ arguments methods will be called, and the final namespace will be returned. This does not allow unknown
229
+ arguments.
230
+
231
+ Args:
232
+ parser_classes: a list of ParserBase subclasses that will be used to
233
+ description: description passed ot the argument parser
234
+ arguments: arguments to process, None to use command line input
235
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
236
+ """
237
+ return cls._parse_args(parser_classes, description, arguments, **kwargs)
238
+
239
+ @staticmethod
240
+ def _parse_args(
241
+ parser_classes,
242
+ description="No tool description provided",
243
+ arguments=None,
244
+ use_parse_known=False,
245
+ **kwargs,
246
+ ):
247
+ """Parse and post-process arguments helper
248
+
166
249
  Create a parser for the given application using the description provided. This will then add all specified
167
250
  ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
168
251
  arguments methods will be called, and the final namespace will be returned.
169
252
 
253
+ This takes a function that will take in a parser and return the parsing function to call on arguments.
254
+
170
255
  Args:
256
+ parse_function_processor: takes a parser, returns the parse function to call
171
257
  parser_classes: a list of ParserBase subclasses that will be used to
172
258
  description: description passed ot the argument parser
173
259
  arguments: arguments to process, None to use command line input
260
+ use_parse_known: use parse_known_arguments from argparse
261
+
174
262
  Returns: namespace with all parsed arguments from all provided ParserBase subclasses
175
263
  """
176
264
  composition = CompositeParser(parser_classes, description)
177
265
  parser = composition.get_parser()
178
266
  try:
179
- args_ns = parser.parse_args(arguments)
267
+ if use_parse_known:
268
+ args_ns, *unknowns = parser.parse_known_args(arguments)
269
+ else:
270
+ args_ns = parser.parse_args(arguments)
271
+ unknowns = []
180
272
  args_ns = composition.handle_arguments(args_ns, **kwargs)
181
273
  except ValueError as ver:
182
274
  print(f"[ERROR] Failed to parse arguments: {ver}", file=sys.stderr)
@@ -185,7 +277,7 @@ class ParserBase(ABC):
185
277
  except Exception as exc:
186
278
  print(f"[ERROR] {exc}", file=sys.stderr)
187
279
  sys.exit(-1)
188
- return args_ns, parser
280
+ return args_ns, parser, *unknowns
189
281
 
190
282
  @staticmethod
191
283
  def find_in(token, deploy, is_file=True):
@@ -205,6 +297,119 @@ class ParserBase(ABC):
205
297
  return None
206
298
 
207
299
 
300
+ class ConfigDrivenParser(ParserBase):
301
+ """Parser that allows options from configuration and command line
302
+
303
+ This parser reads a configuration file (if supplied) and uses the values to drive the inputs to arguments. Command
304
+ line arguments will still take precedence over the configured values.
305
+ """
306
+
307
+ DEFAULT_CONFIGURATION_PATH = Path("fprime-gds.yml")
308
+
309
+ @classmethod
310
+ def set_default_configuration(cls, path: Path):
311
+ """Set path for (global) default configuration file
312
+
313
+ Set the path for default configuration file. If unset, will use 'fprime-gds.yml'. Set to None to disable default
314
+ configuration.
315
+ """
316
+ cls.DEFAULT_CONFIGURATION_PATH = path
317
+
318
+ @classmethod
319
+ def parse_args(
320
+ cls,
321
+ parser_classes,
322
+ description="No tool description provided",
323
+ arguments=None,
324
+ **kwargs,
325
+ ):
326
+ """Parse and post-process arguments using inputs and config
327
+
328
+ Parse the arguments in two stages: first parse the configuration data, ignoring unknown inputs, then parse the
329
+ full argument set with the supplied configuration to fill in additional options.
330
+
331
+ Args:
332
+ parser_classes: a list of ParserBase subclasses that will be used to
333
+ description: description passed ot the argument parser
334
+ arguments: arguments to process, None to use command line input
335
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
336
+ """
337
+ arguments = sys.argv[1:] if arguments is None else arguments
338
+
339
+ # Help should spill all the arguments, so delegate to the normal parsing flow including
340
+ # this and supplied parsers
341
+ if "-h" in arguments or "--help" in arguments:
342
+ parsers = [ConfigDrivenParser] + parser_classes
343
+ ParserBase.parse_args(parsers, description, arguments, **kwargs)
344
+ sys.exit(0)
345
+
346
+ # Custom flow involving parsing the arguments of this parser first, then passing the configured values
347
+ # as part of the argument source
348
+ ns_config, _, remaining = ParserBase.parse_known_args(
349
+ [ConfigDrivenParser], description, arguments, **kwargs
350
+ )
351
+ config_options = ns_config.config_values.get("command-line-options", {})
352
+ config_args = cls.flatten_options(config_options)
353
+ # Argparse allows repeated (overridden) arguments, thus the CLI override is accomplished by providing
354
+ # remaining arguments after the configured ones
355
+ ns_full, parser = ParserBase.parse_args(
356
+ parser_classes, description, config_args + remaining, **kwargs
357
+ )
358
+ ns_final = argparse.Namespace(**vars(ns_config), **vars(ns_full))
359
+ return ns_final, parser
360
+
361
+ @staticmethod
362
+ def flatten_options(configured_options):
363
+ """Flatten options down to arguments"""
364
+ flattened = []
365
+ for option, value in configured_options.items():
366
+ flattened.append(f"--{option}")
367
+ if value is not None:
368
+ flattened.extend(
369
+ value if isinstance(value, (list, tuple)) else [f"{value}"]
370
+ )
371
+ return flattened
372
+
373
+ def get_arguments(self) -> Dict[Tuple[str, ...], Dict[str, Any]]:
374
+ """Arguments needed for config processing"""
375
+ return {
376
+ ("-c", "--config"): {
377
+ "dest": "config",
378
+ "required": False,
379
+ "default": self.DEFAULT_CONFIGURATION_PATH,
380
+ "type": Path,
381
+ "help": f"Argument configuration file path.",
382
+ }
383
+ }
384
+
385
+ def handle_arguments(self, args, **kwargs):
386
+ """Handle the arguments
387
+
388
+ Loads the configuration file specified and fills in the `config_values` attribute of the namespace with the
389
+ loaded configuration dictionary.
390
+ """
391
+ args.config_values = {}
392
+ # Specified but non-existent config file is a hard error
393
+ if (
394
+ "-c" in sys.argv[1:] or "--config" in sys.argv[1:]
395
+ ) and not args.config.exists():
396
+ raise ValueError(
397
+ f"Specified configuration file '{args.config}' does not exist"
398
+ )
399
+ # Read configuration if the file was set and exists
400
+ if args.config is not None and args.config.exists():
401
+ print(f"[INFO] Reading command-line configuration from: {args.config}")
402
+ with open(args.config, "r") as file_handle:
403
+ try:
404
+ loaded = yaml.safe_load(file_handle)
405
+ args.config_values = loaded if loaded is not None else {}
406
+ except Exception as exc:
407
+ raise ValueError(
408
+ f"Malformed configuration {args.config}: {exc}", exc
409
+ )
410
+ return args
411
+
412
+
208
413
  class DetectionParser(ParserBase):
209
414
  """Parser that detects items from a root/directory or deployment"""
210
415
 
@@ -253,71 +458,207 @@ class DetectionParser(ParserBase):
253
458
  return args
254
459
 
255
460
 
461
+ class BareArgumentParser(ParserBase):
462
+ """Takes in the argument specification (used in plugins and get_arguments) to parse args
463
+
464
+ This parser takes in and uses a raw specification of arguments as seen in plugins and arguments to perform argument
465
+ parsing. The spec is a map of flag tuples to argparse kwargs.
466
+
467
+ Argument handling only validates using the checking_function which is a function taking in keyword arguments for
468
+ each cli argument specified. This function will be called as such: `checking_function(**args)`. Use None to skip
469
+ argument checking. checking_function should raise ValueError to indicate an error with an argument.
470
+ """
471
+
472
+ def __init__(self, specification, checking_function=None):
473
+ """Initialize this parser with the provided specification"""
474
+ self.specification = specification
475
+ self.checking_function = checking_function
476
+
477
+ def get_arguments(self):
478
+ """Raw specification is returned immediately"""
479
+ return self.specification
480
+
481
+ def handle_arguments(self, args, **kwargs):
482
+ """Handle argument calls checking function to validate"""
483
+ if self.checking_function is not None:
484
+ self.checking_function(**self.extract_arguments(args))
485
+ return args
486
+
487
+ def extract_arguments(self, args) -> Dict[str, Any]:
488
+ """Extract argument values from the args namespace into a map matching the original specification
489
+
490
+ This function extracts arguments matching the original specification and returns them as a dictionary of key-
491
+ value pairs.
492
+
493
+ Return:
494
+ filled arguments dictionary
495
+ """
496
+ expected_args = self.specification
497
+ argument_destinations = [
498
+ (
499
+ value["dest"]
500
+ if "dest" in value
501
+ else key[0].replace("--", "").replace("-", "_")
502
+ )
503
+ for key, value in expected_args.items()
504
+ ]
505
+ filled_arguments = {
506
+ destination: getattr(args, destination)
507
+ for destination in argument_destinations
508
+ }
509
+ return filled_arguments
510
+
511
+
512
+ class IndividualPluginParser(BareArgumentParser):
513
+ """Parser for an individual plugin's command line
514
+
515
+ A CLI parser for an individual plugin. This handles all the functions and arguments that apply to the parsing of a
516
+ single plugin's arguments. It also handles FEATURE plugin disable flags.
517
+ """
518
+
519
+ def __init__(self, plugin_system: Plugins, plugin_class: type):
520
+ """Initialize the plugin parser
521
+
522
+ Args:
523
+ plugin_system: Plugins object used to work with the plugin system
524
+ plugin_class: plugin class used for this specific parser
525
+ """
526
+ # Add disable flags for feature type plugins
527
+ super().__init__(plugin_class.get_arguments(), plugin_class.check_arguments)
528
+ self.disable_flag_destination = (
529
+ f"disable-{plugin_class.get_name()}".lower().replace("-", "_")
530
+ )
531
+ self.plugin_class = plugin_class
532
+ self.plugin_system = plugin_system
533
+
534
+ def get_arguments(self):
535
+ """Get the arguments for this plugin
536
+
537
+ The individual plugin parser will read the arguments from the supplied plugin class. Additionally, if the
538
+ plugin_class's plugin_type is FEATURE then this parser will add an disable flag to allow users to turn disable
539
+ the plugin feature.
540
+ """
541
+ arguments = {}
542
+ if self.plugin_class.type == PluginType.FEATURE:
543
+ arguments.update(
544
+ {
545
+ (f"--disable-{self.plugin_class.get_name()}",): {
546
+ "action": "store_true",
547
+ "default": False,
548
+ "dest": self.disable_flag_destination,
549
+ "help": f"Disable the {self.plugin_class.category} plugin '{self.plugin_class.get_name()}'",
550
+ }
551
+ }
552
+ )
553
+ arguments.update(super().get_arguments())
554
+ return arguments
555
+
556
+ def handle_arguments(self, args, **kwargs):
557
+ """Handle the given arguments for a plugin
558
+
559
+ This will process the arguments for a given plugin. Additionally, it will construct the plugin object and
560
+ supply the constructed object to the plugin system if the plugin is a selection or is enabled.
561
+
562
+ Args:
563
+ args: argparse namespace
564
+ """
565
+ arguments = super().handle_arguments(
566
+ args, **kwargs
567
+ ) # Perform argument checking first
568
+ if not getattr(args, self.disable_flag_destination, False):
569
+ # Remove the disable flag from the arguments
570
+ plugin_arguments = {
571
+ key: value
572
+ for key, value in self.extract_arguments(arguments).items()
573
+ if key != self.disable_flag_destination
574
+ }
575
+
576
+ plugin_zero_argument_class = functools.partial(
577
+ self.plugin_class.get_implementor(), **plugin_arguments
578
+ )
579
+ self.plugin_system.add_bound_class(
580
+ self.plugin_class.category, plugin_zero_argument_class
581
+ )
582
+ return arguments
583
+
584
+ def get_plugin_class(self):
585
+ """Plugin class accessor"""
586
+ return self.plugin_class
587
+
588
+
256
589
  class PluginArgumentParser(ParserBase):
257
590
  """Parser for arguments coming from plugins"""
258
591
 
259
592
  DESCRIPTION = "Plugin options"
593
+ # Defaults:
260
594
  FPRIME_CHOICES = {
261
- "framing": "fprime",
595
+ "framing": "space-packet-space-data-link",
262
596
  "communication": "ip",
263
597
  }
264
598
 
265
- def __init__(self):
266
- """Initialize the plugin information for this parser"""
599
+ def __init__(self, plugin_system: Plugins = None):
600
+ """Initialize the plugin information for this parser
601
+
602
+ This will initialize this plugin argument parser with the supplied plugin system. If not supplied this will use
603
+ the system plugin singleton, which is configured elsewhere.
604
+ """
605
+ # Accept the supplied plugin system defaulting to the global singleton
606
+ self.plugin_system = plugin_system if plugin_system else Plugins.system()
267
607
  self._plugin_map = {
268
- category: Plugins.system().get_plugins(category)
269
- for category in Plugins.system().get_categories()
608
+ category: [
609
+ IndividualPluginParser(self.plugin_system, plugin)
610
+ for plugin in self.plugin_system.get_plugins(category)
611
+ ]
612
+ for category in self.plugin_system.get_categories()
270
613
  }
271
614
 
272
- @staticmethod
273
- def safe_add_argument(parser, *flags, **keywords):
274
- """ Add an argument allowing duplicates
615
+ def fill_parser(self, parser):
616
+ """Fill supplied parser with grouped arguments
275
617
 
276
- Add arguments to the parser (passes through *flags and **keywords) to the supplied parser. This method traps
277
- errors to prevent duplicates.
618
+ Fill the supplied parser with arguments from the `get_arguments` method invocation. This implementation groups
619
+ arguments based on the constituent parser that the argument comes from. Category specific arguments are also
620
+ added (i.e. SELECTION type selection arguments).
278
621
 
279
622
  Args:
280
- parser: parser or argument group to add arguments to
281
- *flags: positional arguments passed to `add_argument`
282
- **keywords: key word arguments passed to `add_argument`
623
+ parser: parser to fill
283
624
  """
284
- try:
285
- parser.add_argument(*flags, **keywords)
286
- except argparse.ArgumentError:
287
- # flag has already been added, pass
288
- pass
625
+ for category, plugin_parsers in self._plugin_map.items():
626
+ # Add category specific flags (selection flags, etc)
627
+ argument_group = parser.add_argument_group(
628
+ title=f"{category.title()} Plugin Options"
629
+ )
630
+ self.add_arguments_from_specification(
631
+ argument_group, self.get_category_arguments(category)
632
+ )
289
633
 
290
- def fill_parser(self, parser):
291
- """ File supplied parser with grouped arguments
634
+ # Handle the individual plugin parsers
635
+ for plugin_parser in plugin_parsers:
636
+ plugin = plugin_parser.get_plugin_class()
637
+ argument_group = parser.add_argument_group(
638
+ title=f"{category.title()} Plugin '{plugin.get_name()}' Options"
639
+ )
640
+ plugin_parser.fill_parser(argument_group)
292
641
 
293
- Fill the supplied parser with arguments from the `get_arguments` method invocation. This implementation groups
294
- arguments based on the constituent that sources the argument.
642
+ def get_category_arguments(self, category):
643
+ """Get category arguments for a given plugin category
644
+
645
+ This function will generate category arguments for the supplied category. These arguments will follow the
646
+ standard argument specification of a dictionary of flag tuples to argparse keyword arguments.
647
+
648
+ Currently category specific arguments are just selection flags for SELECTION type plugins.
295
649
 
296
650
  Args:
297
- parser: parser to fill
651
+ category: category arguments
298
652
  """
299
- for category, plugins in self._plugin_map.items():
300
- argument_group = parser.add_argument_group(title=f"{category.title()} Plugin Options")
301
- for flags, keywords in self.get_category_arguments(category).items():
302
- self.safe_add_argument(argument_group, *flags, **keywords)
303
-
304
- for plugin in plugins:
305
- argument_group = parser.add_argument_group(title=f"{category.title()} Plugin '{plugin.get_name()}' Options")
306
- if plugin.type == PluginType.FEATURE:
307
- self.safe_add_argument(argument_group,
308
- f"--disable-{plugin.get_name()}",
309
- action="store_true",
310
- default=False,
311
- help=f"Disable the {category} plugin '{plugin.get_name()}'")
312
- for flags, keywords in plugin.get_arguments().items():
313
- self.safe_add_argument(argument_group, *flags, **keywords)
653
+ plugin_type = self.plugin_system.get_category_plugin_type(category)
654
+ plugins = [
655
+ plugin_parser.get_plugin_class()
656
+ for plugin_parser in self._plugin_map[category]
657
+ ]
314
658
 
315
- def get_category_arguments(self, category):
316
- """ Get arguments for a plugin category """
317
659
  arguments: Dict[Tuple[str, ...], Dict[str, Any]] = {}
318
- plugins = self._plugin_map[category]
660
+
319
661
  # Add category options: SELECTION plugins add a selection flag
320
- plugin_type = Plugins.get_category_plugin_type(category)
321
662
  if plugin_type == PluginType.SELECTION:
322
663
  arguments.update(
323
664
  {
@@ -333,81 +674,54 @@ class PluginArgumentParser(ParserBase):
333
674
  return arguments
334
675
 
335
676
  def get_arguments(self) -> Dict[Tuple[str, ...], Dict[str, Any]]:
336
- """Return arguments to used in plugins"""
677
+ """Return arguments to used in plugin system
678
+
679
+ This will return the command line arguments all the plugins contained within the supplied plugin system. This
680
+ will recursively return plugins from all of the IndividualPluginParser objects composing this plugin argument
681
+ parser. Arguments are returned in the standard specification form of tuple of flags mapped to a dictionary of
682
+ argparse kwarg inputs.
683
+ """
337
684
  arguments: Dict[Tuple[str, ...], Dict[str, Any]] = {}
338
- for category, plugins in self._plugin_map.items():
685
+ for category, plugin_parsers in self._plugin_map.items():
339
686
  arguments.update(self.get_category_arguments(category))
340
- for plugin in plugins:
341
- # Add disable flags for feature type plugins
342
- if plugin.type == PluginType.FEATURE:
343
- arguments.update({
344
- (f"--disable-{plugin.get_name()}", ): {
345
- "action": "store_true",
346
- "default": False,
347
- "help": f"Disable the {category} plugin '{plugin.get_name()}'"
348
- }
349
- })
350
- arguments.update(plugin.get_arguments())
687
+ [
688
+ arguments.update(plugin_parser.get_arguments())
689
+ for plugin_parser in plugin_parsers
690
+ ]
351
691
  return arguments
352
692
 
353
693
  def handle_arguments(self, args, **kwargs):
354
- """Handles the arguments"""
355
- for category, plugins in self._plugin_map.items():
356
- plugin_type = Plugins.get_category_plugin_type(category)
694
+ """Handle the plugin arguments
357
695
 
696
+ This will handle the plugin arguments delegating each to the IndividualPluginParser. For SELECTION plugins this
697
+ will bind a single instance of the selected plugin to its arguments. For FEATURE plugins it will bind arguments
698
+ to every enabled plugin. Bound plugins are registered with the plugin system.
699
+ """
700
+ for category, plugin_parsers in self._plugin_map.items():
701
+ plugin_type = self.plugin_system.get_category_plugin_type(category)
702
+ self.plugin_system.start_loading(category)
358
703
  # Selection plugins choose one plugin and instantiate it
359
704
  if plugin_type == PluginType.SELECTION:
360
- selection_string = getattr(args, f"{category}_selection")
361
- matching_plugins = [plugin for plugin in plugins if plugin.get_name() == selection_string]
362
- assert len(matching_plugins) == 1, "Plugin selection system failed"
363
- selection_class = matching_plugins[0].plugin_class
364
- filled_arguments = self.extract_plugin_arguments(args, selection_class)
365
- selection_instance = selection_class(**filled_arguments)
366
- setattr(args, f"{category}_selection_instance", selection_instance)
705
+ try:
706
+ self.plugin_system.get_selected_class(category)
707
+ except PluginsNotLoadedException:
708
+ selection_string = getattr(args, f"{category}_selection")
709
+ matching_plugin_parsers = [
710
+ plugin_parser
711
+ for plugin_parser in plugin_parsers
712
+ if plugin_parser.get_plugin_class().get_name()
713
+ == selection_string
714
+ ]
715
+ assert (
716
+ len(matching_plugin_parsers) == 1
717
+ ), "Plugin selection system failed"
718
+ args = matching_plugin_parsers[0].handle_arguments(args, **kwargs)
367
719
  # Feature plugins instantiate all enabled plugins
368
720
  elif plugin_type == PluginType.FEATURE:
369
- enabled_plugins = [
370
- plugin for plugin in plugins
371
- if not getattr(args, f"disable_{plugin.get_name().replace('-', '_')}", False)
372
- ]
373
- plugin_instantiations = [
374
- plugin.plugin_class(**self.extract_plugin_arguments(args, plugin))
375
- for plugin in enabled_plugins
376
- ]
377
- setattr(args, f"{category}_enabled_instances", plugin_instantiations)
721
+ for plugin_parser in plugin_parsers:
722
+ args = plugin_parser.handle_arguments(args, **kwargs)
378
723
  return args
379
724
 
380
- @staticmethod
381
- def extract_plugin_arguments(args, plugin) -> Dict[str, Any]:
382
- """Extract plugin argument values from the args namespace into a map
383
-
384
- Plugin arguments will be supplied to the `__init__` function of the plugin via a keyword argument dictionary.
385
- This function maps from the argument namespace from parsing back into that dictionary.
386
-
387
- Args:
388
- args: argument namespace from argparse
389
- plugin: plugin to extract arguments for
390
- Return:
391
- filled arguments dictionary
392
- """
393
- expected_args = plugin.get_arguments()
394
- argument_destinations = [
395
- (
396
- value["dest"]
397
- if "dest" in value
398
- else key[0].replace("--", "").replace("-", "_")
399
- )
400
- for key, value in expected_args.items()
401
- ]
402
- filled_arguments = {
403
- destination: getattr(args, destination)
404
- for destination in argument_destinations
405
- }
406
- # Check arguments or yield a Value error
407
- if hasattr(plugin, "check_arguments"):
408
- plugin.check_arguments(**filled_arguments)
409
- return filled_arguments
410
-
411
725
 
412
726
  class CompositeParser(ParserBase):
413
727
  """Composite parser handles parsing as a composition of multiple other parsers"""
@@ -415,7 +729,15 @@ class CompositeParser(ParserBase):
415
729
  def __init__(self, constituents, description=None):
416
730
  """Construct this parser by instantiating the sub-parsers"""
417
731
  self.given = description
418
- constructed = [constituent() for constituent in constituents]
732
+ constructed = [
733
+ constituent() if callable(constituent) else constituent
734
+ for constituent in constituents
735
+ ]
736
+ # Check to ensure everything passed in became a ParserBase after construction
737
+ for i, construct in enumerate(constructed):
738
+ assert isinstance(
739
+ construct, ParserBase
740
+ ), f"{construct.__class__.__name__} ({i}) not a ParserBase child"
419
741
  flattened = [
420
742
  item.constituents if isinstance(item, CompositeParser) else [item]
421
743
  for item in constructed
@@ -423,7 +745,7 @@ class CompositeParser(ParserBase):
423
745
  self.constituent_parsers = {*itertools.chain.from_iterable(flattened)}
424
746
 
425
747
  def fill_parser(self, parser):
426
- """ File supplied parser with grouped arguments
748
+ """File supplied parser with grouped arguments
427
749
 
428
750
  Fill the supplied parser with arguments from the `get_arguments` method invocation. This implementation groups
429
751
  arguments based on the constituent that sources the argument.
@@ -435,7 +757,9 @@ class CompositeParser(ParserBase):
435
757
  if isinstance(constituent, (PluginArgumentParser, CompositeParser)):
436
758
  constituent.fill_parser(parser)
437
759
  else:
438
- argument_group = parser.add_argument_group(title=constituent.description)
760
+ argument_group = parser.add_argument_group(
761
+ title=constituent.description
762
+ )
439
763
  constituent.fill_parser(argument_group)
440
764
 
441
765
  @property
@@ -520,6 +844,13 @@ class LogDeployParser(ParserBase):
520
844
  "default": False,
521
845
  "help": "Log to standard out along with log output files",
522
846
  },
847
+ ("--log-level-gds",): {
848
+ "action": "store",
849
+ "dest": "log_level_gds",
850
+ "choices": ["DEBUG", "INFO", "WARNING", "ERROR"],
851
+ "default": "INFO",
852
+ "help": "Set the logging level of GDS processes [default: %(default)s]",
853
+ },
523
854
  }
524
855
 
525
856
  def handle_arguments(self, args, **kwargs):
@@ -547,7 +878,7 @@ class LogDeployParser(ParserBase):
547
878
  raise
548
879
  # Setup the basic python logging
549
880
  fprime_gds.common.logger.configure_py_log(
550
- args.logs, mirror_to_stdout=args.log_to_stdout
881
+ args.logs, mirror_to_stdout=args.log_to_stdout, log_level=args.log_level_gds
551
882
  )
552
883
  return args
553
884
 
@@ -649,7 +980,15 @@ class DictionaryParser(DetectionParser):
649
980
  "default": None,
650
981
  "required": False,
651
982
  "type": str,
652
- "help": "Path to packet specification.",
983
+ "help": "Path to packet XML specification (should not be used if JSON packet definitions are used).",
984
+ },
985
+ ("--packet-set-name",): {
986
+ "dest": "packet_set_name",
987
+ "action": "store",
988
+ "default": None,
989
+ "required": False,
990
+ "type": str,
991
+ "help": "Name of packet set defined in the JSON dictionary.",
653
992
  },
654
993
  },
655
994
  }
@@ -730,6 +1069,7 @@ class StandardPipelineParser(CompositeParser):
730
1069
  "dictionary": args_ns.dictionary,
731
1070
  "file_store": args_ns.files_storage_directory,
732
1071
  "packet_spec": args_ns.packet_spec,
1072
+ "packet_set_name": args_ns.packet_set_name,
733
1073
  "logging_prefix": args_ns.logs,
734
1074
  }
735
1075
  pipeline = pipeline if pipeline else StandardPipeline()
@@ -753,13 +1093,16 @@ class CommParser(CompositeParser):
753
1093
  CommExtraParser,
754
1094
  MiddleWareParser,
755
1095
  LogDeployParser,
756
- PluginArgumentParser,
757
1096
  ]
758
1097
 
759
1098
  def __init__(self):
760
1099
  """Initialization"""
1100
+ # Added here to ensure the call to Plugins does not interfere with the full plugin system
1101
+ comm_plugin_parser_instance = PluginArgumentParser(
1102
+ Plugins(["communication", "framing"])
1103
+ )
761
1104
  super().__init__(
762
- constituents=self.CONSTITUENTS,
1105
+ constituents=self.CONSTITUENTS + [comm_plugin_parser_instance],
763
1106
  description="Communications bridge application",
764
1107
  )
765
1108
 
@@ -867,9 +1210,7 @@ class BinaryDeployment(DetectionParser):
867
1210
  class SearchArgumentsParser(ParserBase):
868
1211
  """Parser for search arguments"""
869
1212
 
870
- DESCRIPTION = (
871
- "Searching and filtering options"
872
- )
1213
+ DESCRIPTION = "Searching and filtering options"
873
1214
 
874
1215
  def __init__(self, command_name: str) -> None:
875
1216
  self.command_name = command_name