psdi-data-conversion 0.0.37__py3-none-any.whl → 0.0.39__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 (30) hide show
  1. psdi_data_conversion/app.py +64 -14
  2. psdi_data_conversion/constants.py +6 -5
  3. psdi_data_conversion/converter.py +20 -13
  4. psdi_data_conversion/converters/base.py +75 -68
  5. psdi_data_conversion/converters/c2x.py +14 -0
  6. psdi_data_conversion/converters/openbabel.py +12 -11
  7. psdi_data_conversion/database.py +361 -115
  8. psdi_data_conversion/dist.py +2 -1
  9. psdi_data_conversion/file_io.py +1 -2
  10. psdi_data_conversion/log_utility.py +1 -1
  11. psdi_data_conversion/main.py +152 -70
  12. psdi_data_conversion/static/content/index-versions/psdi-common-footer.html +12 -8
  13. psdi_data_conversion/static/content/psdi-common-footer.html +12 -8
  14. psdi_data_conversion/static/data/data.json +617 -3
  15. psdi_data_conversion/static/javascript/convert.js +54 -6
  16. psdi_data_conversion/static/javascript/convert_common.js +16 -2
  17. psdi_data_conversion/static/javascript/data.js +36 -4
  18. psdi_data_conversion/static/javascript/format.js +22 -9
  19. psdi_data_conversion/static/styles/format.css +7 -0
  20. psdi_data_conversion/templates/index.htm +57 -48
  21. psdi_data_conversion/testing/constants.py +3 -0
  22. psdi_data_conversion/testing/conversion_callbacks.py +4 -3
  23. psdi_data_conversion/testing/conversion_test_specs.py +44 -20
  24. psdi_data_conversion/testing/gui.py +362 -294
  25. psdi_data_conversion/testing/utils.py +38 -19
  26. {psdi_data_conversion-0.0.37.dist-info → psdi_data_conversion-0.0.39.dist-info}/METADATA +88 -4
  27. {psdi_data_conversion-0.0.37.dist-info → psdi_data_conversion-0.0.39.dist-info}/RECORD +30 -30
  28. {psdi_data_conversion-0.0.37.dist-info → psdi_data_conversion-0.0.39.dist-info}/WHEEL +0 -0
  29. {psdi_data_conversion-0.0.37.dist-info → psdi_data_conversion-0.0.39.dist-info}/entry_points.txt +0 -0
  30. {psdi_data_conversion-0.0.37.dist-info → psdi_data_conversion-0.0.39.dist-info}/licenses/LICENSE +0 -0
@@ -7,9 +7,10 @@ Functions and utilities related to handling multiple user OSes and distributions
7
7
 
8
8
  import os
9
9
  import shutil
10
- import psdi_data_conversion
11
10
  import sys
12
11
 
12
+ import psdi_data_conversion
13
+
13
14
  # Labels for each platform (which we use for the folder in this project), and the head of the name each platform will
14
15
  # have in `sys.platform`
15
16
 
@@ -149,8 +149,7 @@ def pack_zip_or_tar(archive_filename: str,
149
149
  archive_format = _format
150
150
  archive_root_filename = split_archive_ext(archive_filename)[0]
151
151
  break
152
- # Check that the format was found
153
- if archive_format is None:
152
+ else:
154
153
  raise AssertionError("Invalid execution path entered - filename wasn't found with a valid archive "
155
154
  "extension, but it did pass the `is_supported_archive` check")
156
155
  else:
@@ -5,11 +5,11 @@ Created 2024-12-09 by Bryan Gillis.
5
5
  Functions and classes related to logging and other messaging for the user
6
6
  """
7
7
 
8
- from datetime import datetime
9
8
  import logging
10
9
  import os
11
10
  import re
12
11
  import sys
12
+ from datetime import datetime
13
13
 
14
14
  from psdi_data_conversion import constants as const
15
15
 
@@ -8,19 +8,20 @@ Entry-point file for the command-line interface for data conversion.
8
8
  """
9
9
 
10
10
  import logging
11
- from argparse import ArgumentParser
12
11
  import os
13
12
  import sys
14
13
  import textwrap
14
+ from argparse import ArgumentParser
15
+ from itertools import product
15
16
 
16
17
  from psdi_data_conversion import constants as const
17
18
  from psdi_data_conversion.constants import CL_SCRIPT_NAME, CONVERTER_DEFAULT, TERM_WIDTH
18
19
  from psdi_data_conversion.converter import (D_CONVERTER_ARGS, D_SUPPORTED_CONVERTERS, L_REGISTERED_CONVERTERS,
19
20
  L_SUPPORTED_CONVERTERS, run_converter)
20
- from psdi_data_conversion.converters.base import (FileConverterAbortException, FileConverterInputException,
21
- FileConverterHelpException)
22
- from psdi_data_conversion.database import (get_conversion_quality, get_converter_info, get_format_info,
23
- get_in_format_args, get_out_format_args, get_possible_converters,
21
+ from psdi_data_conversion.converters.base import (FileConverterAbortException, FileConverterException,
22
+ FileConverterInputException)
23
+ from psdi_data_conversion.database import (FormatInfo, get_conversion_quality, get_converter_info, get_format_info,
24
+ get_in_format_args, get_out_format_args, get_possible_conversions,
24
25
  get_possible_formats)
25
26
  from psdi_data_conversion.file_io import split_archive_ext
26
27
  from psdi_data_conversion.log_utility import get_log_level_from_str
@@ -84,9 +85,21 @@ class ConvertArgs:
84
85
  try:
85
86
  self.log_level = get_log_level_from_str(args.log_level)
86
87
  except ValueError as e:
87
- # A ValueError indicates an unrecognised logging level, so we reraise this as a help exception to
88
+ # A ValueError indicates an unrecognised logging level, so we reraise this with the help flag to
88
89
  # indicate we want to provide this as feedback to the user so they can correct their command
89
- raise FileConverterHelpException(str(e))
90
+ raise FileConverterInputException(str(e), help=True)
91
+
92
+ # If formats were provided as ints, convert them to the int type now
93
+ try:
94
+ if self.from_format:
95
+ self.from_format = int(self.from_format)
96
+ except ValueError:
97
+ pass
98
+ try:
99
+ if self.to_format:
100
+ self.to_format = int(self.to_format)
101
+ except ValueError:
102
+ pass
90
103
 
91
104
  # Special handling for listing converters
92
105
  if self.list:
@@ -114,40 +127,40 @@ class ConvertArgs:
114
127
  # Check validity of input
115
128
 
116
129
  if len(self.l_args) == 0:
117
- raise FileConverterHelpException("One or more names of files to convert must be provided")
130
+ raise FileConverterInputException("One or more names of files to convert must be provided", help=True)
118
131
 
119
132
  if self._input_dir is not None and not os.path.isdir(self._input_dir):
120
- raise FileConverterHelpException(f"The provided input directory '{self._input_dir}' does not exist as a "
121
- "directory")
133
+ raise FileConverterInputException(f"The provided input directory '{self._input_dir}' does not exist as a "
134
+ "directory", help=True)
122
135
 
123
136
  if self.to_format is None:
124
137
  msg = textwrap.fill("ERROR Output format (-t or --to) must be provided. For information on supported "
125
138
  "formats and converters, call:\n")
126
139
  msg += f"{CL_SCRIPT_NAME} -l"
127
- raise FileConverterHelpException(msg, msg_preformatted=True)
140
+ raise FileConverterInputException(msg, msg_preformatted=True, help=True)
128
141
 
129
142
  # If the output directory doesn't exist, silently create it
130
143
  if self._output_dir is not None and not os.path.isdir(self._output_dir):
131
144
  if os.path.exists(self._output_dir):
132
- raise FileConverterHelpException(f"Output directory '{self._output_dir}' exists but is not a "
133
- "directory")
145
+ raise FileConverterInputException(f"Output directory '{self._output_dir}' exists but is not a "
146
+ "directory", help=True)
134
147
  os.makedirs(self._output_dir, exist_ok=True)
135
148
 
136
149
  # Check the converter is recognized
137
150
  if self.name not in L_SUPPORTED_CONVERTERS:
138
151
  msg = textwrap.fill(f"ERROR: Converter '{self.name}' not recognised", width=TERM_WIDTH)
139
152
  msg += f"\n\n{get_supported_converters()}"
140
- raise FileConverterHelpException(msg, msg_preformatted=True)
153
+ raise FileConverterInputException(msg, help=True, msg_preformatted=True)
141
154
  elif self.name not in L_REGISTERED_CONVERTERS:
142
155
  msg = textwrap.fill(f"ERROR: Converter '{self.name}' is not registered. It may be possible to register "
143
156
  "it by installing an appropriate binary for your platform.", width=TERM_WIDTH)
144
157
  msg += f"\n\n{get_supported_converters()}"
145
- raise FileConverterHelpException(msg, msg_preformatted=True)
158
+ raise FileConverterInputException(msg, help=True, msg_preformatted=True)
146
159
 
147
160
  # Logging mode is valid
148
161
  if self.log_mode not in const.L_ALLOWED_LOG_MODES:
149
- raise FileConverterHelpException(f"Unrecognised logging mode: {self.log_mode}. Allowed "
150
- f"modes are: {const.L_ALLOWED_LOG_MODES}")
162
+ raise FileConverterInputException(f"Unrecognised logging mode: {self.log_mode}. Allowed "
163
+ f"modes are: {const.L_ALLOWED_LOG_MODES}", help=True)
151
164
 
152
165
  # Arguments specific to this converter
153
166
  self.d_converter_args = {}
@@ -194,10 +207,10 @@ class ConvertArgs:
194
207
  if os.path.isfile(test_filename):
195
208
  first_filename = test_filename
196
209
  else:
197
- raise FileConverterHelpException(f"Input file {first_filename} cannot be found. Also "
198
- f"checked for {test_filename}.")
210
+ raise FileConverterInputException(f"Input file {first_filename} cannot be found. Also "
211
+ f"checked for {test_filename}.", help=True)
199
212
  else:
200
- raise FileConverterHelpException(f"Input file {first_filename} cannot be found.")
213
+ raise FileConverterInputException(f"Input file {first_filename} cannot be found.", help=True)
201
214
 
202
215
  filename_base = os.path.split(split_archive_ext(first_filename)[0])[1]
203
216
  if self.log_mode == const.LOG_FULL:
@@ -345,7 +358,7 @@ def detail_converter_use(args: ConvertArgs):
345
358
  print_wrap(converter_class.info, break_long_words=False, break_on_hyphens=False, newline=True)
346
359
 
347
360
  # If both an input and output format are specified, provide the degree of success for this conversion. Otherwise
348
- # list possible input output formats
361
+ # list possible input/output formats
349
362
  if args.from_format is not None and args.to_format is not None:
350
363
  qual = get_conversion_quality(args.name, args.from_format, args.to_format)
351
364
  if qual is None:
@@ -375,19 +388,19 @@ def detail_converter_use(args: ConvertArgs):
375
388
  print_wrap(f"Conversion {to_or_from} {format_name} is {optional_not}supported by {args.name}.\n")
376
389
 
377
390
  # List all possible formats, and which can be used for input and which for output
378
- s_all_formats: set[str] = set(l_input_formats)
391
+ s_all_formats: set[FormatInfo] = set(l_input_formats)
379
392
  s_all_formats.update(l_output_formats)
380
- l_all_formats: list[str] = list(s_all_formats)
381
- l_all_formats.sort(key=lambda s: s.lower())
393
+ l_all_formats: list[FormatInfo] = list(s_all_formats)
394
+ l_all_formats.sort(key=lambda x: x.disambiguated_name.lower())
382
395
 
383
396
  print_wrap(f"File formats supported by {args.name}:", newline=True)
384
- max_format_length = max([len(x) for x in l_all_formats])
397
+ max_format_length = max([len(x.disambiguated_name) for x in l_all_formats])
385
398
  print(" "*(max_format_length+4) + " INPUT OUTPUT")
386
399
  print(" "*(max_format_length+4) + " ----- ------")
387
400
  for file_format in l_all_formats:
388
401
  in_yes_or_no = "yes" if file_format in l_input_formats else "no"
389
402
  out_yes_or_no = "yes" if file_format in l_output_formats else "no"
390
- print(f" {file_format:>{max_format_length}}{in_yes_or_no:>8}{out_yes_or_no:>8}")
403
+ print(f" {file_format.disambiguated_name:>{max_format_length}}{in_yes_or_no:>8}{out_yes_or_no:>8}")
391
404
  print("")
392
405
 
393
406
  if converter_class.allowed_flags is None:
@@ -472,10 +485,16 @@ def list_supported_formats(err=False):
472
485
  """Prints a list of all formats recognised by at least one registered converter
473
486
  """
474
487
  # Make a list of all formats recognised by at least one registered converter
475
- s_all_formats: set[str] = set()
476
- s_registered_formats: set[str] = set()
488
+ s_all_formats: set[FormatInfo] = set()
489
+ s_registered_formats: set[FormatInfo] = set()
477
490
  for converter_name in L_SUPPORTED_CONVERTERS:
478
491
  l_in_formats, l_out_formats = get_possible_formats(converter_name)
492
+
493
+ # To make sure we don't see any unexpected duplicates in the set due to cached/uncached values, get the
494
+ # disambiguated name of each format first
495
+ [x.disambiguated_name for x in l_in_formats]
496
+ [x.disambiguated_name for x in l_out_formats]
497
+
479
498
  s_all_formats.update(l_in_formats)
480
499
  s_all_formats.update(l_out_formats)
481
500
  if converter_name in L_REGISTERED_CONVERTERS:
@@ -486,14 +505,14 @@ def list_supported_formats(err=False):
486
505
 
487
506
  # Convert the sets to lists and alphabetise them
488
507
  l_registered_formats = list(s_registered_formats)
489
- l_registered_formats.sort(key=lambda s: s.lower())
508
+ l_registered_formats.sort(key=lambda x: x.disambiguated_name.lower())
490
509
  l_unregistered_formats = list(s_unregistered_formats)
491
- l_unregistered_formats.sort(key=lambda s: s.lower())
510
+ l_unregistered_formats.sort(key=lambda x: x.disambiguated_name.lower())
492
511
 
493
512
  # Pad the format strings to all be the same length. To keep columns aligned, all padding is done with non-
494
513
  # breaking spaces (\xa0), and each format is followed by a single normal space
495
- longest_format_len = max([len(x) for x in l_registered_formats])
496
- l_padded_formats = [f"{x:\xa0<{longest_format_len}} " for x in l_registered_formats]
514
+ longest_format_len = max([len(x.disambiguated_name) for x in l_registered_formats])
515
+ l_padded_formats = [f"{x.disambiguated_name:\xa0<{longest_format_len}} " for x in l_registered_formats]
497
516
 
498
517
  print_wrap("Formats supported by registered converters: ", err=err, newline=True)
499
518
  print_wrap("".join(l_padded_formats), err=err, initial_indent=" ", subsequent_indent=" ", newline=True)
@@ -508,9 +527,33 @@ def list_supported_formats(err=False):
508
527
  initial_indent=" ", subsequent_indent=" ", newline=True)
509
528
 
510
529
  print_wrap("Note that not all formats are supported with all converters, or both as input and as output.")
530
+ if err:
531
+ print("")
532
+ print_wrap("For more details on a format, call:")
533
+ print(f"{CL_SCRIPT_NAME} -l -f <format>")
511
534
 
512
535
 
513
- def detail_possible_converters(from_format: str, to_format: str):
536
+ def detail_format(format_name: str):
537
+ """Prints details on a format
538
+ """
539
+
540
+ l_format_info = get_format_info(format_name, which="all")
541
+
542
+ if len(l_format_info) == 0:
543
+ print_wrap(f"ERROR: Format '{format_name}' not recognised", err=True, newline=True)
544
+ list_supported_formats(err=True)
545
+ exit(1)
546
+
547
+ if len(l_format_info) > 1:
548
+ print_wrap(f"WARNING: Format '{format_name}' is ambiguous and could refer to multiple formats. It may be "
549
+ "necessary to explicitly specify which you want to use when calling this script, e.g. with "
550
+ f"'-f {format_name}-0' - see the disambiguated names in the list below:", newline=True)
551
+
552
+ for format_info in l_format_info:
553
+ print_wrap(f"{format_info.id}: {format_info.disambiguated_name} ({format_info.note})")
554
+
555
+
556
+ def detail_formats_and_possible_converters(from_format: str, to_format: str):
514
557
  """Prints details on converters that can perform a conversion from one format to another
515
558
  """
516
559
 
@@ -518,13 +561,13 @@ def detail_possible_converters(from_format: str, to_format: str):
518
561
  either_format_failed = False
519
562
 
520
563
  try:
521
- get_format_info(from_format)
564
+ get_format_info(from_format, which=0)
522
565
  except KeyError:
523
566
  either_format_failed = True
524
567
  print_wrap(f"ERROR: Input format '{from_format}' not recognised", newline=True, err=True)
525
568
 
526
569
  try:
527
- get_format_info(to_format)
570
+ get_format_info(to_format, which=0)
528
571
  except KeyError:
529
572
  either_format_failed = True
530
573
  print_wrap(f"ERROR: Output format '{from_format}' not recognised", newline=True, err=True)
@@ -534,32 +577,58 @@ def detail_possible_converters(from_format: str, to_format: str):
534
577
  list_supported_formats(err=True)
535
578
  exit(1)
536
579
 
537
- l_possible_converters = get_possible_converters(from_format, to_format)
580
+ # Provide details on both the input and output formats
581
+ detail_format(from_format)
582
+ print()
583
+ detail_format(to_format)
538
584
 
539
- l_possible_registered_converters = [x for x in l_possible_converters if x in L_REGISTERED_CONVERTERS]
540
- l_possible_unregistered_converters = [x for x in l_possible_converters if
541
- x in L_SUPPORTED_CONVERTERS and x not in L_REGISTERED_CONVERTERS]
585
+ l_possible_conversions = get_possible_conversions(from_format, to_format)
542
586
 
543
- if len(l_possible_registered_converters)+len(l_possible_unregistered_converters) == 0:
544
- print_wrap(f"No converters are available which can perform a conversion from {from_format} to {to_format}")
545
- return
546
- elif len(l_possible_registered_converters) == 0:
547
- print_wrap(f"No registered converters can perform a conversion from {from_format} to {to_format}, however "
548
- "the following converters are supported by this package on other platforms and can perform this "
549
- "conversion:", newline=True)
550
- print("\n ".join(l_possible_unregistered_converters))
551
- return
587
+ # Get a list of all different formats which share the provided name, cutting out duplicates
588
+ l_from_formats = list(set([x[1] for x in l_possible_conversions]))
589
+ l_from_formats.sort(key=lambda x: x.disambiguated_name)
590
+ l_to_formats = list(set([x[2] for x in l_possible_conversions]))
591
+ l_to_formats.sort(key=lambda x: x.disambiguated_name)
552
592
 
553
- print_wrap(f"The following registered converters can convert from {from_format} to {to_format}:", newline=True)
554
- print(" " + "\n ".join(l_possible_registered_converters) + "\n")
555
- if l_possible_unregistered_converters:
556
- print("")
557
- print_wrap("Additionally, the following converters are supported by this package on other platforms and can "
558
- "perform this conversion:", newline=True)
559
- print(" " + "\n ".join(l_possible_unregistered_converters) + "\n")
593
+ # Loop over all possible combinations of formats
594
+
595
+ for possible_from_format, possible_to_format in product(l_from_formats, l_to_formats):
596
+
597
+ from_name = possible_from_format.disambiguated_name
598
+ to_name = possible_to_format.disambiguated_name
560
599
 
561
- print_wrap("For details on input/output flags and options allowed by a converter for this conversion, call:")
562
- print(f"{CL_SCRIPT_NAME} -l <converter name> -f {from_format} -t {to_format}")
600
+ l_conversions_matching_formats = [x for x in l_possible_conversions
601
+ if x[1] == possible_from_format and x[2] == possible_to_format]
602
+
603
+ l_possible_registered_converters = [x[0] for x in l_conversions_matching_formats
604
+ if x[0] in L_REGISTERED_CONVERTERS]
605
+ l_possible_unregistered_converters = [x[0] for x in l_conversions_matching_formats
606
+ if x[0] in L_SUPPORTED_CONVERTERS and x[0] not in L_REGISTERED_CONVERTERS]
607
+
608
+ print()
609
+
610
+ if len(l_possible_registered_converters)+len(l_possible_unregistered_converters) == 0:
611
+ print_wrap(f"No converters are available which can perform a conversion from {from_name} to "
612
+ f"{to_name}")
613
+ continue
614
+ elif len(l_possible_registered_converters) == 0:
615
+ print_wrap(f"No registered converters can perform a conversion from {from_name} to "
616
+ f"{to_name}, however the following converters are supported by this package on other "
617
+ "platforms and can perform this conversion:", newline=True)
618
+ print("\n ".join(l_possible_unregistered_converters))
619
+ continue
620
+
621
+ print_wrap(f"The following registered converters can convert from {from_name} to "
622
+ f"{to_name}:", newline=True)
623
+ print(" " + "\n ".join(l_possible_registered_converters) + "\n")
624
+ if l_possible_unregistered_converters:
625
+ print("")
626
+ print_wrap("Additionally, the following converters are supported by this package on other platforms and "
627
+ "can perform this conversion:", newline=True)
628
+ print(" " + "\n ".join(l_possible_unregistered_converters) + "\n")
629
+
630
+ print_wrap("For details on input/output flags and options allowed by a converter for this conversion, call:")
631
+ print(f"{CL_SCRIPT_NAME} -l <converter name> -f {from_name} -t {to_name}")
563
632
 
564
633
 
565
634
  def get_supported_converters():
@@ -612,7 +681,13 @@ def detail_converters_and_formats(args: ConvertArgs):
612
681
  list_supported_converters(err=True)
613
682
  exit(1)
614
683
  elif args.from_format and args.to_format:
615
- detail_possible_converters(args.from_format, args.to_format)
684
+ detail_formats_and_possible_converters(args.from_format, args.to_format)
685
+ return
686
+ elif args.from_format:
687
+ detail_format(args.from_format)
688
+ return
689
+ elif args.to_format:
690
+ detail_format(args.to_format)
616
691
  return
617
692
 
618
693
  list_supported_converters()
@@ -623,6 +698,9 @@ def detail_converters_and_formats(args: ConvertArgs):
623
698
  print_wrap("For more details on a converter, call:")
624
699
  print(f"{CL_SCRIPT_NAME} -l <converter name>\n")
625
700
 
701
+ print_wrap("For more details on a format, call:")
702
+ print(f"{CL_SCRIPT_NAME} -l -f <format>\n")
703
+
626
704
  print_wrap("For a list of converters that can perform a desired conversion, call:")
627
705
  print(f"{CL_SCRIPT_NAME} -l -f <input format> -t <output format>\n")
628
706
 
@@ -693,12 +771,6 @@ def run_from_args(args: ConvertArgs):
693
771
  log_level=args.log_level,
694
772
  delete_input=args.delete_input,
695
773
  refresh_local_log=False)
696
- except FileConverterHelpException as e:
697
- if not e.logged:
698
- print_wrap(f"ERROR: {e}", err=True)
699
- e.logged = True
700
- success = False
701
- continue
702
774
  except FileConverterAbortException as e:
703
775
  if not e.logged:
704
776
  print_wrap(f"ERROR: Attempt to convert file {filename} aborted with status code {e.status_code} and "
@@ -706,11 +778,19 @@ def run_from_args(args: ConvertArgs):
706
778
  e.logged = True
707
779
  success = False
708
780
  continue
709
- except FileConverterInputException as e:
710
- if "Conversion from" in str(e) and "is not supported" in str(e):
781
+ except FileConverterException as e:
782
+ if e.help and not e.logged:
783
+ print_wrap(f"ERROR: {e}", err=True)
784
+ e.logged = True
785
+ elif "Conversion from" in str(e) and "is not supported" in str(e):
711
786
  if not e.logged:
712
787
  print_wrap(f"ERROR: {e}", err=True, newline=True)
713
- detail_possible_converters(args.from_format, args.to_format)
788
+ detail_formats_and_possible_converters(args.from_format, args.to_format)
789
+ elif e.help and not e.logged:
790
+ if e.msg_preformatted:
791
+ print(e, file=sys.stderr)
792
+ else:
793
+ print_wrap(f"ERROR: {e}", err=True)
714
794
  elif not e.logged:
715
795
  print_wrap(f"ERROR: Attempt to convert file {filename} failed at converter initialization with "
716
796
  f"exception type {type(e)} and message: \n{e}\n", err=True)
@@ -748,9 +828,11 @@ def main():
748
828
 
749
829
  try:
750
830
  args = parse_args()
751
- except FileConverterHelpException as e:
752
- # If we get a Help exception, it's likely due to user error, so don't bother them with a traceback and simply
753
- # print the message to stderr
831
+ except FileConverterInputException as e:
832
+ if not e.help:
833
+ raise
834
+ # If we get an exception with the help flag set, it's likely due to user error, so don't bother them with a
835
+ # traceback and simply print the message to stderr
754
836
  if e.msg_preformatted:
755
837
  print(e, file=sys.stderr)
756
838
  else:
@@ -17,16 +17,20 @@
17
17
  </ul>
18
18
  <ul class="footer__col footer__items clean-list">
19
19
  <li>
20
- <img class="lm-only" src="static/img/ukri-logo-lighttext.png"
21
- alt="UKRI logo">
22
- <img class="dm-only" src="static/img/ukri-logo-darktext.png"
23
- alt="UKRI logo">
20
+ <a href="https://www.ukri.org/">
21
+ <img class="lm-only" src="static/img/ukri-logo-lighttext.png"
22
+ alt="UKRI logo">
23
+ <img class="dm-only" src="static/img/ukri-logo-darktext.png"
24
+ alt="UKRI logo">
25
+ </a>
24
26
  </li>
25
27
  <li>
26
- <img class="lm-only" src="static/img/ukri-epsr-logo-lighttext.png"
27
- alt="UKRI EPSR logo">
28
- <img class="dm-only" src="static/img/ukri-epsr-logo-darktext.png"
29
- alt="UKRI EPSR logo">
28
+ <a href="https://www.ukri.org/councils/epsrc/">
29
+ <img class="lm-only" src="static/img/ukri-epsr-logo-lighttext.png"
30
+ alt="UKRI EPSR logo">
31
+ <img class="dm-only" src="static/img/ukri-epsr-logo-darktext.png"
32
+ alt="UKRI EPSR logo">
33
+ </a>
30
34
  </li>
31
35
  </ul>
32
36
  <div class="footer__hline small-screen-only">
@@ -17,16 +17,20 @@
17
17
  </ul>
18
18
  <ul class="footer__col footer__items clean-list">
19
19
  <li>
20
- <img class="lm-only" src="../img/ukri-logo-lighttext.png"
21
- alt="UKRI logo">
22
- <img class="dm-only" src="../img/ukri-logo-darktext.png"
23
- alt="UKRI logo">
20
+ <a href="https://www.ukri.org/">
21
+ <img class="lm-only" src="../img/ukri-logo-lighttext.png"
22
+ alt="UKRI logo">
23
+ <img class="dm-only" src="../img/ukri-logo-darktext.png"
24
+ alt="UKRI logo">
25
+ </a>
24
26
  </li>
25
27
  <li>
26
- <img class="lm-only" src="../img/ukri-epsr-logo-lighttext.png"
27
- alt="UKRI EPSR logo">
28
- <img class="dm-only" src="../img/ukri-epsr-logo-darktext.png"
29
- alt="UKRI EPSR logo">
28
+ <a href="https://www.ukri.org/councils/epsrc/">
29
+ <img class="lm-only" src="../img/ukri-epsr-logo-lighttext.png"
30
+ alt="UKRI EPSR logo">
31
+ <img class="dm-only" src="../img/ukri-epsr-logo-darktext.png"
32
+ alt="UKRI EPSR logo">
33
+ </a>
30
34
  </li>
31
35
  </ul>
32
36
  <div class="footer__hline small-screen-only">