fprime-gds 3.6.2a1__py3-none-any.whl → 4.0.0a2__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 (44) hide show
  1. fprime_gds/common/communication/adapters/uart.py +34 -25
  2. fprime_gds/common/decoders/ch_decoder.py +1 -1
  3. fprime_gds/common/decoders/event_decoder.py +2 -1
  4. fprime_gds/common/decoders/pkt_decoder.py +1 -1
  5. fprime_gds/common/distributor/distributor.py +2 -2
  6. fprime_gds/common/encoders/ch_encoder.py +2 -2
  7. fprime_gds/common/encoders/cmd_encoder.py +2 -2
  8. fprime_gds/common/encoders/event_encoder.py +2 -2
  9. fprime_gds/common/encoders/pkt_encoder.py +2 -2
  10. fprime_gds/common/encoders/seq_writer.py +2 -2
  11. fprime_gds/common/fpy/__init__.py +0 -0
  12. fprime_gds/common/fpy/serialize_bytecode.py +229 -0
  13. fprime_gds/common/fpy/types.py +203 -0
  14. fprime_gds/common/gds_cli/base_commands.py +1 -1
  15. fprime_gds/common/handlers.py +39 -0
  16. fprime_gds/common/loaders/fw_type_json_loader.py +54 -0
  17. fprime_gds/common/loaders/pkt_json_loader.py +121 -0
  18. fprime_gds/common/loaders/prm_json_loader.py +85 -0
  19. fprime_gds/common/pipeline/dictionaries.py +21 -4
  20. fprime_gds/common/pipeline/encoding.py +19 -0
  21. fprime_gds/common/pipeline/histories.py +4 -0
  22. fprime_gds/common/pipeline/standard.py +16 -2
  23. fprime_gds/common/templates/prm_template.py +81 -0
  24. fprime_gds/common/testing_fw/api.py +42 -0
  25. fprime_gds/common/testing_fw/pytest_integration.py +25 -2
  26. fprime_gds/common/tools/README.md +34 -0
  27. fprime_gds/common/tools/params.py +246 -0
  28. fprime_gds/common/utils/config_manager.py +6 -6
  29. fprime_gds/executables/apps.py +184 -11
  30. fprime_gds/executables/cli.py +443 -125
  31. fprime_gds/executables/comm.py +5 -2
  32. fprime_gds/executables/fprime_cli.py +3 -3
  33. fprime_gds/executables/run_deployment.py +12 -4
  34. fprime_gds/flask/static/js/vue-support/channel.js +1 -1
  35. fprime_gds/flask/static/js/vue-support/event.js +1 -1
  36. fprime_gds/plugin/definitions.py +86 -8
  37. fprime_gds/plugin/system.py +171 -58
  38. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0a2.dist-info}/METADATA +18 -19
  39. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0a2.dist-info}/RECORD +44 -35
  40. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0a2.dist-info}/WHEEL +1 -1
  41. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0a2.dist-info}/entry_points.txt +2 -0
  42. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0a2.dist-info/licenses}/LICENSE.txt +0 -0
  43. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0a2.dist-info/licenses}/NOTICE.txt +0 -0
  44. {fprime_gds-3.6.2a1.dist-info → fprime_gds-4.0.0a2.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,32 @@ 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(parser_classes, description, arguments, use_parse_known=True, **kwargs)
213
+
214
+ @classmethod
158
215
  def parse_args(
216
+ cls,
159
217
  parser_classes,
160
218
  description="No tool description provided",
161
219
  arguments=None,
@@ -163,20 +221,53 @@ class ParserBase(ABC):
163
221
  ):
164
222
  """Parse and post-process arguments
165
223
 
224
+ Create a parser for the given application using the description provided. This will then add all specified
225
+ ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
226
+ arguments methods will be called, and the final namespace will be returned. This does not allow unknown
227
+ arguments.
228
+
229
+ Args:
230
+ parser_classes: a list of ParserBase subclasses that will be used to
231
+ description: description passed ot the argument parser
232
+ arguments: arguments to process, None to use command line input
233
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
234
+ """
235
+ return cls._parse_args(parser_classes, description, arguments, **kwargs)
236
+
237
+
238
+ @staticmethod
239
+ def _parse_args(
240
+ parser_classes,
241
+ description="No tool description provided",
242
+ arguments=None,
243
+ use_parse_known=False,
244
+ **kwargs,
245
+ ):
246
+ """Parse and post-process arguments helper
247
+
166
248
  Create a parser for the given application using the description provided. This will then add all specified
167
249
  ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
168
250
  arguments methods will be called, and the final namespace will be returned.
169
251
 
252
+ This takes a function that will take in a parser and return the parsing function to call on arguments.
253
+
170
254
  Args:
255
+ parse_function_processor: takes a parser, returns the parse function to call
171
256
  parser_classes: a list of ParserBase subclasses that will be used to
172
257
  description: description passed ot the argument parser
173
258
  arguments: arguments to process, None to use command line input
259
+ use_parse_known: use parse_known_arguments from argparse
260
+
174
261
  Returns: namespace with all parsed arguments from all provided ParserBase subclasses
175
262
  """
176
263
  composition = CompositeParser(parser_classes, description)
177
264
  parser = composition.get_parser()
178
265
  try:
179
- args_ns = parser.parse_args(arguments)
266
+ if use_parse_known:
267
+ args_ns, *unknowns = parser.parse_known_args(arguments)
268
+ else:
269
+ args_ns = parser.parse_args(arguments)
270
+ unknowns = []
180
271
  args_ns = composition.handle_arguments(args_ns, **kwargs)
181
272
  except ValueError as ver:
182
273
  print(f"[ERROR] Failed to parse arguments: {ver}", file=sys.stderr)
@@ -185,7 +276,7 @@ class ParserBase(ABC):
185
276
  except Exception as exc:
186
277
  print(f"[ERROR] {exc}", file=sys.stderr)
187
278
  sys.exit(-1)
188
- return args_ns, parser
279
+ return args_ns, parser, *unknowns
189
280
 
190
281
  @staticmethod
191
282
  def find_in(token, deploy, is_file=True):
@@ -205,6 +296,106 @@ class ParserBase(ABC):
205
296
  return None
206
297
 
207
298
 
299
+ class ConfigDrivenParser(ParserBase):
300
+ """ Parser that allows options from configuration and command line
301
+
302
+ This parser reads a configuration file (if supplied) and uses the values to drive the inputs to arguments. Command
303
+ line arguments will still take precedence over the configured values.
304
+ """
305
+ DEFAULT_CONFIGURATION_PATH = Path("fprime-gds.yml")
306
+
307
+ @classmethod
308
+ def set_default_configuration(cls, path: Path):
309
+ """ Set path for (global) default configuration file
310
+
311
+ Set the path for default configuration file. If unset, will use 'fprime-gds.yml'. Set to None to disable default
312
+ configuration.
313
+ """
314
+ cls.DEFAULT_CONFIGURATION_PATH = path
315
+
316
+ @classmethod
317
+ def parse_args(
318
+ cls,
319
+ parser_classes,
320
+ description="No tool description provided",
321
+ arguments=None,
322
+ **kwargs,
323
+ ):
324
+ """ Parse and post-process arguments using inputs and config
325
+
326
+ Parse the arguments in two stages: first parse the configuration data, ignoring unknown inputs, then parse the
327
+ full argument set with the supplied configuration to fill in additional options.
328
+
329
+ Args:
330
+ parser_classes: a list of ParserBase subclasses that will be used to
331
+ description: description passed ot the argument parser
332
+ arguments: arguments to process, None to use command line input
333
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
334
+ """
335
+ arguments = sys.argv[1:] if arguments is None else arguments
336
+
337
+ # Help should spill all the arguments, so delegate to the normal parsing flow including
338
+ # this and supplied parsers
339
+ if "-h" in arguments or "--help" in arguments:
340
+ parsers = [ConfigDrivenParser] + parser_classes
341
+ ParserBase.parse_args(parsers, description, arguments, **kwargs)
342
+ sys.exit(0)
343
+
344
+ # Custom flow involving parsing the arguments of this parser first, then passing the configured values
345
+ # as part of the argument source
346
+ ns_config, _, remaining = ParserBase.parse_known_args([ConfigDrivenParser], description, arguments, **kwargs)
347
+ config_options = ns_config.config_values.get("command-line-options", {})
348
+ config_args = cls.flatten_options(config_options)
349
+ # Argparse allows repeated (overridden) arguments, thus the CLI override is accomplished by providing
350
+ # remaining arguments after the configured ones
351
+ ns_full, parser = ParserBase.parse_args(parser_classes, description, config_args + remaining, **kwargs)
352
+ ns_final = argparse.Namespace(**vars(ns_config), **vars(ns_full))
353
+ return ns_final, parser
354
+
355
+ @staticmethod
356
+ def flatten_options(configured_options):
357
+ """ Flatten options down to arguments """
358
+ flattened = []
359
+ for option, value in configured_options.items():
360
+ flattened.append(f"--{option}")
361
+ if value is not None:
362
+ flattened.extend(value if isinstance(value, (list, tuple)) else [f"{value}"])
363
+ return flattened
364
+
365
+ def get_arguments(self) -> Dict[Tuple[str, ...], Dict[str, Any]]:
366
+ """Arguments needed for config processing"""
367
+ return {
368
+ ("-c", "--config"): {
369
+ "dest": "config",
370
+ "required": False,
371
+ "default": self.DEFAULT_CONFIGURATION_PATH,
372
+ "type": Path,
373
+ "help": f"Argument configuration file path.",
374
+ }
375
+ }
376
+
377
+ def handle_arguments(self, args, **kwargs):
378
+ """ Handle the arguments
379
+
380
+ Loads the configuration file specified and fills in the `config_values` attribute of the namespace with the
381
+ loaded configuration dictionary.
382
+ """
383
+ args.config_values = {}
384
+ # Specified but non-existent config file is a hard error
385
+ if ("-c" in sys.argv[1:] or "--config" in sys.argv[1:]) and not args.config.exists():
386
+ raise ValueError(f"Specified configuration file '{args.config}' does not exist")
387
+ # Read configuration if the file was set and exists
388
+ if args.config is not None and args.config.exists():
389
+ print(f"[INFO] Reading command-line configuration from: {args.config}")
390
+ with open(args.config, "r") as file_handle:
391
+ try:
392
+ loaded = yaml.safe_load(file_handle)
393
+ args.config_values = loaded if loaded is not None else {}
394
+ except Exception as exc:
395
+ raise ValueError(f"Malformed configuration {args.config}: {exc}", exc)
396
+ return args
397
+
398
+
208
399
  class DetectionParser(ParserBase):
209
400
  """Parser that detects items from a root/directory or deployment"""
210
401
 
@@ -253,6 +444,133 @@ class DetectionParser(ParserBase):
253
444
  return args
254
445
 
255
446
 
447
+ class BareArgumentParser(ParserBase):
448
+ """Takes in the argument specification (used in plugins and get_arguments) to parse args
449
+
450
+ This parser takes in and uses a raw specification of arguments as seen in plugins and arguments to perform argument
451
+ parsing. The spec is a map of flag tuples to argparse kwargs.
452
+
453
+ Argument handling only validates using the checking_function which is a function taking in keyword arguments for
454
+ each cli argument specified. This function will be called as such: `checking_function(**args)`. Use None to skip
455
+ argument checking. checking_function should raise ValueError to indicate an error with an argument.
456
+ """
457
+
458
+ def __init__(self, specification, checking_function=None):
459
+ """Initialize this parser with the provided specification"""
460
+ self.specification = specification
461
+ self.checking_function = checking_function
462
+
463
+ def get_arguments(self):
464
+ """Raw specification is returned immediately"""
465
+ return self.specification
466
+
467
+ def handle_arguments(self, args, **kwargs):
468
+ """Handle argument calls checking function to validate"""
469
+ if self.checking_function is not None:
470
+ self.checking_function(**self.extract_arguments(args))
471
+ return args
472
+
473
+ def extract_arguments(self, args) -> Dict[str, Any]:
474
+ """Extract argument values from the args namespace into a map matching the original specification
475
+
476
+ This function extracts arguments matching the original specification and returns them as a dictionary of key-
477
+ value pairs.
478
+
479
+ Return:
480
+ filled arguments dictionary
481
+ """
482
+ expected_args = self.specification
483
+ argument_destinations = [
484
+ (
485
+ value["dest"]
486
+ if "dest" in value
487
+ else key[0].replace("--", "").replace("-", "_")
488
+ )
489
+ for key, value in expected_args.items()
490
+ ]
491
+ filled_arguments = {
492
+ destination: getattr(args, destination)
493
+ for destination in argument_destinations
494
+ }
495
+ return filled_arguments
496
+
497
+
498
+ class IndividualPluginParser(BareArgumentParser):
499
+ """Parser for an individual plugin's command line
500
+
501
+ A CLI parser for an individual plugin. This handles all the functions and arguments that apply to the parsing of a
502
+ single plugin's arguments. It also handles FEATURE plugin disable flags.
503
+ """
504
+
505
+ def __init__(self, plugin_system: Plugins, plugin_class: type):
506
+ """Initialize the plugin parser
507
+
508
+ Args:
509
+ plugin_system: Plugins object used to work with the plugin system
510
+ plugin_class: plugin class used for this specific parser
511
+ """
512
+ # Add disable flags for feature type plugins
513
+ super().__init__(plugin_class.get_arguments(), plugin_class.check_arguments)
514
+ self.disable_flag_destination = (
515
+ f"disable-{plugin_class.get_name()}".lower().replace("-", "_")
516
+ )
517
+ self.plugin_class = plugin_class
518
+ self.plugin_system = plugin_system
519
+
520
+ def get_arguments(self):
521
+ """Get the arguments for this plugin
522
+
523
+ The individual plugin parser will read the arguments from the supplied plugin class. Additionally, if the
524
+ plugin_class's plugin_type is FEATURE then this parser will add an disable flag to allow users to turn disable
525
+ the plugin feature.
526
+ """
527
+ arguments = {}
528
+ if self.plugin_class.type == PluginType.FEATURE:
529
+ arguments.update(
530
+ {
531
+ (f"--disable-{self.plugin_class.get_name()}",): {
532
+ "action": "store_true",
533
+ "default": False,
534
+ "dest": self.disable_flag_destination,
535
+ "help": f"Disable the {self.plugin_class.category} plugin '{self.plugin_class.get_name()}'",
536
+ }
537
+ }
538
+ )
539
+ arguments.update(super().get_arguments())
540
+ return arguments
541
+
542
+ def handle_arguments(self, args, **kwargs):
543
+ """Handle the given arguments for a plugin
544
+
545
+ This will process the arguments for a given plugin. Additionally, it will construct the plugin object and
546
+ supply the constructed object to the plugin system if the plugin is a selection or is enabled.
547
+
548
+ Args:
549
+ args: argparse namespace
550
+ """
551
+ arguments = super().handle_arguments(
552
+ args, **kwargs
553
+ ) # Perform argument checking first
554
+ if not getattr(args, self.disable_flag_destination, False):
555
+ # Remove the disable flag from the arguments
556
+ plugin_arguments = {
557
+ key: value
558
+ for key, value in self.extract_arguments(arguments).items()
559
+ if key != self.disable_flag_destination
560
+ }
561
+ plugin_zero_argument_class = functools.partial(
562
+ self.plugin_class.get_implementor(), **plugin_arguments
563
+ )
564
+ self.plugin_system.add_bound_class(
565
+ self.plugin_class.category, plugin_zero_argument_class
566
+ )
567
+ return arguments
568
+
569
+ def get_plugin_class(self):
570
+ """Plugin class accessor"""
571
+ return self.plugin_class
572
+
573
+
256
574
  class PluginArgumentParser(ParserBase):
257
575
  """Parser for arguments coming from plugins"""
258
576
 
@@ -262,62 +580,69 @@ class PluginArgumentParser(ParserBase):
262
580
  "communication": "ip",
263
581
  }
264
582
 
265
- def __init__(self):
266
- """Initialize the plugin information for this parser"""
583
+ def __init__(self, plugin_system: Plugins = None):
584
+ """Initialize the plugin information for this parser
585
+
586
+ This will initialize this plugin argument parser with the supplied plugin system. If not supplied this will use
587
+ the system plugin singleton, which is configured elsewhere.
588
+ """
589
+ # Accept the supplied plugin system defaulting to the global singleton
590
+ self.plugin_system = plugin_system if plugin_system else Plugins.system()
267
591
  self._plugin_map = {
268
- category: Plugins.system().get_plugins(category)
269
- for category in Plugins.system().get_categories()
592
+ category: [
593
+ IndividualPluginParser(self.plugin_system, plugin)
594
+ for plugin in self.plugin_system.get_plugins(category)
595
+ ]
596
+ for category in self.plugin_system.get_categories()
270
597
  }
271
598
 
272
- @staticmethod
273
- def safe_add_argument(parser, *flags, **keywords):
274
- """ Add an argument allowing duplicates
599
+ def fill_parser(self, parser):
600
+ """Fill supplied parser with grouped arguments
275
601
 
276
- Add arguments to the parser (passes through *flags and **keywords) to the supplied parser. This method traps
277
- errors to prevent duplicates.
602
+ Fill the supplied parser with arguments from the `get_arguments` method invocation. This implementation groups
603
+ arguments based on the constituent parser that the argument comes from. Category specific arguments are also
604
+ added (i.e. SELECTION type selection arguments).
278
605
 
279
606
  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`
607
+ parser: parser to fill
283
608
  """
284
- try:
285
- parser.add_argument(*flags, **keywords)
286
- except argparse.ArgumentError:
287
- # flag has already been added, pass
288
- pass
609
+ for category, plugin_parsers in self._plugin_map.items():
610
+ # Add category specific flags (selection flags, etc)
611
+ argument_group = parser.add_argument_group(
612
+ title=f"{category.title()} Plugin Options"
613
+ )
614
+ self.add_arguments_from_specification(
615
+ argument_group, self.get_category_arguments(category)
616
+ )
289
617
 
290
- def fill_parser(self, parser):
291
- """ File supplied parser with grouped arguments
618
+ # Handle the individual plugin parsers
619
+ for plugin_parser in plugin_parsers:
620
+ plugin = plugin_parser.get_plugin_class()
621
+ argument_group = parser.add_argument_group(
622
+ title=f"{category.title()} Plugin '{plugin.get_name()}' Options"
623
+ )
624
+ plugin_parser.fill_parser(argument_group)
292
625
 
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.
626
+ def get_category_arguments(self, category):
627
+ """Get category arguments for a given plugin category
628
+
629
+ This function will generate category arguments for the supplied category. These arguments will follow the
630
+ standard argument specification of a dictionary of flag tuples to argparse keyword arguments.
631
+
632
+ Currently category specific arguments are just selection flags for SELECTION type plugins.
295
633
 
296
634
  Args:
297
- parser: parser to fill
635
+ category: category arguments
298
636
  """
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)
637
+ plugin_type = self.plugin_system.get_category_plugin_type(category)
638
+ plugins = [
639
+ plugin_parser.get_plugin_class()
640
+ for plugin_parser in self._plugin_map[category]
641
+ ]
314
642
 
315
- def get_category_arguments(self, category):
316
- """ Get arguments for a plugin category """
317
643
  arguments: Dict[Tuple[str, ...], Dict[str, Any]] = {}
318
- plugins = self._plugin_map[category]
644
+
319
645
  # Add category options: SELECTION plugins add a selection flag
320
- plugin_type = Plugins.get_category_plugin_type(category)
321
646
  if plugin_type == PluginType.SELECTION:
322
647
  arguments.update(
323
648
  {
@@ -333,81 +658,54 @@ class PluginArgumentParser(ParserBase):
333
658
  return arguments
334
659
 
335
660
  def get_arguments(self) -> Dict[Tuple[str, ...], Dict[str, Any]]:
336
- """Return arguments to used in plugins"""
661
+ """Return arguments to used in plugin system
662
+
663
+ This will return the command line arguments all the plugins contained within the supplied plugin system. This
664
+ will recursively return plugins from all of the IndividualPluginParser objects composing this plugin argument
665
+ parser. Arguments are returned in the standard specification form of tuple of flags mapped to a dictionary of
666
+ argparse kwarg inputs.
667
+ """
337
668
  arguments: Dict[Tuple[str, ...], Dict[str, Any]] = {}
338
- for category, plugins in self._plugin_map.items():
669
+ for category, plugin_parsers in self._plugin_map.items():
339
670
  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())
671
+ [
672
+ arguments.update(plugin_parser.get_arguments())
673
+ for plugin_parser in plugin_parsers
674
+ ]
351
675
  return arguments
352
676
 
353
677
  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)
678
+ """Handle the plugin arguments
357
679
 
680
+ This will handle the plugin arguments delegating each to the IndividualPluginParser. For SELECTION plugins this
681
+ will bind a single instance of the selected plugin to its arguments. For FEATURE plugins it will bind arguments
682
+ to every enabled plugin. Bound plugins are registered with the plugin system.
683
+ """
684
+ for category, plugin_parsers in self._plugin_map.items():
685
+ plugin_type = self.plugin_system.get_category_plugin_type(category)
686
+ self.plugin_system.start_loading(category)
358
687
  # Selection plugins choose one plugin and instantiate it
359
688
  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)
689
+ try:
690
+ self.plugin_system.get_selected_class(category)
691
+ except PluginsNotLoadedException:
692
+ selection_string = getattr(args, f"{category}_selection")
693
+ matching_plugin_parsers = [
694
+ plugin_parser
695
+ for plugin_parser in plugin_parsers
696
+ if plugin_parser.get_plugin_class().get_name()
697
+ == selection_string
698
+ ]
699
+ assert (
700
+ len(matching_plugin_parsers) == 1
701
+ ), "Plugin selection system failed"
702
+ args = matching_plugin_parsers[0].handle_arguments(args, **kwargs)
367
703
  # Feature plugins instantiate all enabled plugins
368
704
  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)
705
+ for plugin_parser in plugin_parsers:
706
+ args = plugin_parser.handle_arguments(args, **kwargs)
378
707
  return args
379
708
 
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
709
 
412
710
  class CompositeParser(ParserBase):
413
711
  """Composite parser handles parsing as a composition of multiple other parsers"""
@@ -415,7 +713,15 @@ class CompositeParser(ParserBase):
415
713
  def __init__(self, constituents, description=None):
416
714
  """Construct this parser by instantiating the sub-parsers"""
417
715
  self.given = description
418
- constructed = [constituent() for constituent in constituents]
716
+ constructed = [
717
+ constituent() if callable(constituent) else constituent
718
+ for constituent in constituents
719
+ ]
720
+ # Check to ensure everything passed in became a ParserBase after construction
721
+ for i, construct in enumerate(constructed):
722
+ assert isinstance(
723
+ construct, ParserBase
724
+ ), f"{construct.__class__.__name__} ({i}) not a ParserBase child"
419
725
  flattened = [
420
726
  item.constituents if isinstance(item, CompositeParser) else [item]
421
727
  for item in constructed
@@ -423,7 +729,7 @@ class CompositeParser(ParserBase):
423
729
  self.constituent_parsers = {*itertools.chain.from_iterable(flattened)}
424
730
 
425
731
  def fill_parser(self, parser):
426
- """ File supplied parser with grouped arguments
732
+ """File supplied parser with grouped arguments
427
733
 
428
734
  Fill the supplied parser with arguments from the `get_arguments` method invocation. This implementation groups
429
735
  arguments based on the constituent that sources the argument.
@@ -435,7 +741,9 @@ class CompositeParser(ParserBase):
435
741
  if isinstance(constituent, (PluginArgumentParser, CompositeParser)):
436
742
  constituent.fill_parser(parser)
437
743
  else:
438
- argument_group = parser.add_argument_group(title=constituent.description)
744
+ argument_group = parser.add_argument_group(
745
+ title=constituent.description
746
+ )
439
747
  constituent.fill_parser(argument_group)
440
748
 
441
749
  @property
@@ -649,7 +957,15 @@ class DictionaryParser(DetectionParser):
649
957
  "default": None,
650
958
  "required": False,
651
959
  "type": str,
652
- "help": "Path to packet specification.",
960
+ "help": "Path to packet XML specification (should not be used if JSON packet definitions are used).",
961
+ },
962
+ ("--packet-set-name",): {
963
+ "dest": "packet_set_name",
964
+ "action": "store",
965
+ "default": None,
966
+ "required": False,
967
+ "type": str,
968
+ "help": "Name of packet set defined in the JSON dictionary.",
653
969
  },
654
970
  },
655
971
  }
@@ -730,6 +1046,7 @@ class StandardPipelineParser(CompositeParser):
730
1046
  "dictionary": args_ns.dictionary,
731
1047
  "file_store": args_ns.files_storage_directory,
732
1048
  "packet_spec": args_ns.packet_spec,
1049
+ "packet_set_name": args_ns.packet_set_name,
733
1050
  "logging_prefix": args_ns.logs,
734
1051
  }
735
1052
  pipeline = pipeline if pipeline else StandardPipeline()
@@ -753,13 +1070,16 @@ class CommParser(CompositeParser):
753
1070
  CommExtraParser,
754
1071
  MiddleWareParser,
755
1072
  LogDeployParser,
756
- PluginArgumentParser,
757
1073
  ]
758
1074
 
759
1075
  def __init__(self):
760
1076
  """Initialization"""
1077
+ # Added here to ensure the call to Plugins does not interfere with the full plugin system
1078
+ comm_plugin_parser_instance = PluginArgumentParser(
1079
+ Plugins(["communication", "framing"])
1080
+ )
761
1081
  super().__init__(
762
- constituents=self.CONSTITUENTS,
1082
+ constituents=self.CONSTITUENTS + [comm_plugin_parser_instance],
763
1083
  description="Communications bridge application",
764
1084
  )
765
1085
 
@@ -867,9 +1187,7 @@ class BinaryDeployment(DetectionParser):
867
1187
  class SearchArgumentsParser(ParserBase):
868
1188
  """Parser for search arguments"""
869
1189
 
870
- DESCRIPTION = (
871
- "Searching and filtering options"
872
- )
1190
+ DESCRIPTION = "Searching and filtering options"
873
1191
 
874
1192
  def __init__(self, command_name: str) -> None:
875
1193
  self.command_name = command_name