python-pdffiller 1.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.
pdffiller/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from pdffiller import cli, const, typing
2
+ from pdffiller._version import __author__, __copyright__, __version__
3
+
4
+ __all__ = ["__version__", "__copyright__", "__author__", "cli", "typing", "const"]
pdffiller/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from pdffiller.cli import cli
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(cli.main(sys.argv[1:]))
pdffiller/_version.py ADDED
@@ -0,0 +1,6 @@
1
+ __author__ = "Jacques Raphanel"
2
+ __copyright__ = "Copyright 2025 SISMIC"
3
+ __email__ = "jraphanel@sismic.fr"
4
+ __license__ = "MIT"
5
+ __title__ = "pdffiller"
6
+ __version__ = "1.0.0"
File without changes
pdffiller/cli/args.py ADDED
@@ -0,0 +1,28 @@
1
+ import argparse
2
+
3
+ from pdffiller.cli.command import BaseCommand
4
+ from pdffiller.typing import Any, Optional, Union
5
+
6
+
7
+ def add_global_arguments(
8
+ parser: Union[argparse.ArgumentParser, Any],
9
+ add_help: bool = True,
10
+ main_parser: Optional[argparse.ArgumentParser] = None,
11
+ ) -> None:
12
+ """Add global parsers command-line options."""
13
+ BaseCommand.init_log_file(parser)
14
+ BaseCommand.init_log_levels(parser)
15
+
16
+ if hasattr(parser, "_command"):
17
+ getattr(parser, "_command").init_formatters(parser)
18
+ elif main_parser and hasattr(main_parser, "_command"):
19
+ getattr(main_parser, "_command").init_formatters(parser)
20
+ if add_help:
21
+ parser.add_argument(
22
+ "-h",
23
+ "--help",
24
+ dest="help",
25
+ action="store_true",
26
+ default=False,
27
+ help="show this help message and exit",
28
+ )
@@ -0,0 +1,35 @@
1
+ import argparse
2
+
3
+ from pdffiller.typing import Any, List, Optional, Sequence, Union
4
+
5
+
6
+ class BooleanAction(argparse.Action):
7
+ """Handle --no-xxx --xxx command-line options"""
8
+
9
+ def __init__(self, option_strings: Sequence[str], dest: str, **kwargs: Any) -> None:
10
+ _option_strings: List[str] = []
11
+ for option_string in option_strings:
12
+ _option_strings.append(option_string)
13
+
14
+ if option_string.startswith("--"):
15
+ option_string = "--no-" + option_string[2:]
16
+ _option_strings.append(option_string)
17
+
18
+ if kwargs.get("help") is not None and kwargs.get("default") is not None:
19
+ kwargs["help"] += f" (default: {kwargs['default']})"
20
+
21
+ super().__init__(option_strings=_option_strings, dest=dest, nargs=0, **kwargs)
22
+
23
+ def __call__(
24
+ self,
25
+ parser: argparse.ArgumentParser,
26
+ namespace: argparse.Namespace,
27
+ values: Optional[Union[str, Sequence[str]]] = None,
28
+ option_string: Optional[str] = None,
29
+ ) -> None:
30
+ if option_string and option_string in self.option_strings:
31
+ setattr(namespace, self.dest, not option_string.startswith("--no-"))
32
+
33
+ def format_usage(self) -> str:
34
+ """Get usage message"""
35
+ return " | ".join(self.option_strings)
pdffiller/cli/cli.py ADDED
@@ -0,0 +1,260 @@
1
+ import importlib
2
+ import os
3
+ import pkgutil
4
+ import signal
5
+ import sys
6
+ import textwrap
7
+ import traceback
8
+ from collections import defaultdict
9
+ from difflib import get_close_matches
10
+ from inspect import getmembers
11
+
12
+ from pdffiller import __version__ as pdffiller_version
13
+ from pdffiller.cli.command import PdfFillerSubCommand
14
+ from pdffiller.cli.exit_codes import (
15
+ ERROR_COMMAND_NAME,
16
+ ERROR_ENCOUNTERED,
17
+ ERROR_GENERAL,
18
+ ERROR_SIGTERM,
19
+ ERROR_SUBCOMMAND_NAME,
20
+ ERROR_UNEXPECTED,
21
+ SUCCESS,
22
+ USER_CTRL_BREAK,
23
+ USER_CTRL_C,
24
+ )
25
+ from pdffiller.exceptions import (
26
+ AbortExecution,
27
+ CommandLineError,
28
+ InvalidCommandNameException,
29
+ InvalidSubCommandNameException,
30
+ PdfFillerException,
31
+ )
32
+ from pdffiller.io.colors import init_colorama
33
+ from pdffiller.io.output import cli_out_write, Color, LEVEL_TRACE, PdfFillerOutput
34
+ from pdffiller.typing import Any, Dict, ExitCode, List, Optional
35
+
36
+
37
+ class Cli:
38
+ """A single command of the PdfFiller application, with all the first level commands.
39
+ Manages the parsing of parameters and delegates functionality to the appropriate
40
+ command. It can also show the help of the tool.
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ self._groups: Dict[str, List[str]] = defaultdict(list)
45
+ self._commands: Dict[str, Any] = {}
46
+ init_colorama(sys.stderr)
47
+
48
+ def _add_commands(self) -> None:
49
+ pdffiller_commands_path = os.path.join(
50
+ os.path.dirname(os.path.abspath(__file__)), "commands"
51
+ )
52
+ for module in pkgutil.iter_modules([pdffiller_commands_path]):
53
+ module_name = module[1]
54
+ self._add_command(f"pdffiller.cli.commands.{module_name}", module_name)
55
+
56
+ def _add_command(
57
+ self, import_path: str, method_name: str, package: Optional[str] = None
58
+ ) -> None:
59
+ try:
60
+ imported_module = importlib.import_module(import_path)
61
+ cmd_mapping = getattr(imported_module, "__MAPPING__", {})
62
+ cb_name = cmd_mapping.get(method_name, method_name)
63
+ command_wrapper = getattr(imported_module, cb_name)
64
+ if command_wrapper.doc:
65
+ name = f"{package}:{method_name}" if package else method_name
66
+ self._commands[name] = command_wrapper
67
+ self._groups[command_wrapper.group].append(name)
68
+ for name, value in getmembers(imported_module):
69
+ if isinstance(value, PdfFillerSubCommand):
70
+ if name.startswith(f"{cb_name}_"):
71
+ command_wrapper.add_subcommand(value)
72
+ elif name[0] != "_":
73
+ raise PdfFillerException(
74
+ "The name for the subcommand method should "
75
+ "begin with the main command name + '_'. "
76
+ f"i.e. {cb_name}_<subcommand_name>"
77
+ )
78
+ except AttributeError as exc:
79
+ raise PdfFillerException(
80
+ f"There is no {method_name} method defined in {import_path}"
81
+ ) from exc
82
+
83
+ def _print_similar(self, command: str) -> None:
84
+ """Looks for similar commands and prints them if found."""
85
+ output = PdfFillerOutput()
86
+ matches = get_close_matches(
87
+ word=command, possibilities=self._commands.keys(), n=5, cutoff=0.75
88
+ )
89
+
90
+ if len(matches) == 0:
91
+ return
92
+
93
+ if len(matches) > 1:
94
+ output.info("The most similar commands are")
95
+ else:
96
+ output.info("The most similar command is")
97
+
98
+ for match in matches:
99
+ output.info(f" {match}")
100
+
101
+ output.writeln("")
102
+
103
+ def _output_help_cli(self) -> None:
104
+ """
105
+ Prints a summary of all commands.
106
+ """
107
+ max_len = max((len(c) for c in self._commands)) + 4
108
+ line_format = f"{{: <{max_len}}}"
109
+
110
+ nb_groups = len(self._groups.items())
111
+ for group_name, comm_names in sorted(self._groups.items()):
112
+ if nb_groups != 1 or group_name:
113
+ cli_out_write("\n" + group_name + " commands", Color.YELLOW)
114
+ else:
115
+ cli_out_write("\n")
116
+ for name in comm_names:
117
+ # future-proof way to ensure tabular formatting
118
+ cli_out_write(line_format.format(name), Color.GREEN, endline="")
119
+
120
+ # Help will be all the lines up to the first empty one
121
+ docstring_lines = self._commands[name].doc.split("\n")
122
+ start = False
123
+ data: List[str] = []
124
+ for line in docstring_lines:
125
+ line = line.strip()
126
+ if not line:
127
+ if start:
128
+ break
129
+ start = True
130
+ continue
131
+ data.append(line)
132
+
133
+ txt = textwrap.fill(" ".join(data), 80, subsequent_indent=" " * (max_len + 2))
134
+ cli_out_write(txt)
135
+
136
+ cli_out_write("")
137
+ cli_out_write('Type "pdffiller <command> -h" for help', Color.BLUE)
138
+
139
+ def run(self, *args: Any) -> ExitCode:
140
+ """Entry point for executing commands, dispatcher to class
141
+ methods
142
+ """
143
+ output = PdfFillerOutput()
144
+ self._add_commands()
145
+ try:
146
+ command_argument = args[0][0]
147
+ except IndexError: # No parameters
148
+ self._output_help_cli()
149
+ raise InvalidCommandNameException(None) # pylint: disable=raise-missing-from
150
+
151
+ try:
152
+ command = self._commands[command_argument]
153
+ except KeyError: # No parameters
154
+ if command_argument in ["--version"]:
155
+ cli_out_write(
156
+ f"pdffiller {pdffiller_version}",
157
+ fore=Color.BRIGHT_GREEN,
158
+ )
159
+ raise AbortExecution(0) # pylint: disable=raise-missing-from
160
+
161
+ if command_argument in ["-h", "--help"]:
162
+ self._output_help_cli()
163
+ raise AbortExecution(0) # pylint: disable=raise-missing-from
164
+
165
+ output.info(
166
+ f"'{command_argument}' is not a pdffiller command. " "See 'pdffiller --help'."
167
+ )
168
+ output.info("")
169
+ self._print_similar(command_argument)
170
+ raise InvalidCommandNameException( # pylint: disable=raise-missing-from
171
+ command_argument
172
+ )
173
+
174
+ try:
175
+ command.run(args[0][1:])
176
+ except Exception as exc:
177
+ if not isinstance(exc, AbortExecution) and PdfFillerOutput.level_allowed(LEVEL_TRACE):
178
+ output.trace("\n".join(traceback.format_exception(*sys.exc_info())))
179
+ raise exc
180
+ return 0
181
+
182
+ @staticmethod
183
+ def exception_exit_error(exception: Optional[BaseException]) -> ExitCode:
184
+ """Convert exception into an exit code"""
185
+ output = PdfFillerOutput()
186
+ if exception is None:
187
+ return SUCCESS
188
+ if isinstance(exception, AbortExecution):
189
+ return SUCCESS if exception.exitcode == 0 else ERROR_ENCOUNTERED
190
+ if isinstance(exception, InvalidCommandNameException):
191
+ return ERROR_COMMAND_NAME
192
+ if isinstance(exception, InvalidSubCommandNameException):
193
+ return ERROR_SUBCOMMAND_NAME
194
+ if isinstance(exception, CommandLineError):
195
+ output.error(exception.message)
196
+ return ERROR_UNEXPECTED
197
+ if isinstance(exception, PdfFillerException):
198
+ output.error(exception.message)
199
+ return ERROR_GENERAL
200
+ if isinstance(exception, SystemExit):
201
+ if exception.code != 0:
202
+ output.error(f"Exiting with code: {exception.code}")
203
+ return ERROR_ENCOUNTERED
204
+
205
+ assert isinstance(exception, Exception)
206
+ output.error(traceback.format_exc())
207
+ try:
208
+ msg = str(exception)
209
+ except Exception: # pylint: disable=broad-except
210
+ msg = repr(exception)
211
+ output.error(msg)
212
+ return ERROR_UNEXPECTED
213
+
214
+
215
+ def main(args: Optional[List[str]] = None) -> ExitCode:
216
+ """main entry point of the PdfFiller application, using a Command to
217
+ parse parameters.
218
+
219
+ :parameters:
220
+ :param args: Optional command-line arguments, else `sys.argv` will
221
+ be used by default
222
+
223
+ :return: One of the following exit code:
224
+
225
+ * 0: Success (done)
226
+ * 1: General PdfFillerException error (done)
227
+ * 2: Ctrl+C
228
+ * 3: Ctrl+Break
229
+ * 4: SIGTERM
230
+ * 5: Invalid configuration (done)
231
+ * 6: Unexpected error
232
+ * 7: Invalid command name
233
+ * 8: Invalid sub-command name
234
+ """
235
+
236
+ def ctrl_c_handler(_: Any, __: Any) -> None:
237
+ print("You pressed Ctrl+C!")
238
+ sys.exit(USER_CTRL_C)
239
+
240
+ def sigterm_handler(_: Any, __: Any) -> None:
241
+ print("Received SIGTERM!")
242
+ sys.exit(ERROR_SIGTERM)
243
+
244
+ def ctrl_break_handler(_: Any, __: Any) -> None:
245
+ print("You pressed Ctrl+Break!")
246
+ sys.exit(USER_CTRL_BREAK)
247
+
248
+ signal.signal(signal.SIGINT, ctrl_c_handler)
249
+ signal.signal(signal.SIGTERM, sigterm_handler)
250
+
251
+ if sys.platform == "win32":
252
+ signal.signal(signal.SIGBREAK, ctrl_break_handler)
253
+
254
+ cli = Cli()
255
+ error: ExitCode = SUCCESS
256
+ try:
257
+ cli.run(args if args is not None else sys.argv[1:])
258
+ except BaseException as exc: # pylint: disable=broad-except
259
+ error = cli.exception_exit_error(exc)
260
+ return error
@@ -0,0 +1,291 @@
1
+ import argparse
2
+
3
+ from pdffiller.cli.once_argument import OnceArgument
4
+ from pdffiller.cli.smart_formatter import SmartFormatter
5
+ from pdffiller.exceptions import (
6
+ AbortExecution,
7
+ InvalidSubCommandNameException,
8
+ PdfFillerException,
9
+ )
10
+ from pdffiller.io.output import PdfFillerOutput
11
+ from pdffiller.typing import (
12
+ Any,
13
+ Callable,
14
+ Dict,
15
+ List,
16
+ Optional,
17
+ Sequence,
18
+ SubParserType,
19
+ Union,
20
+ )
21
+
22
+
23
+ class PdfFillerArgumentParser(argparse.ArgumentParser):
24
+ """PdfFiller argument parser to support configuration file"""
25
+
26
+ _command: "PdfFillerCommand"
27
+
28
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
29
+ super().__init__(*args, **kwargs)
30
+
31
+ def parse_args( # type: ignore[override] # pylint: disable=arguments-differ
32
+ self, args: Optional[Sequence[str]] = None
33
+ ) -> argparse.Namespace:
34
+ options = super().parse_args(args)
35
+
36
+ PdfFillerOutput.define_log_level(options.verbosity)
37
+ PdfFillerOutput.define_log_output(options.log_file)
38
+ output = PdfFillerOutput()
39
+
40
+ if "help" in options and options.help:
41
+ self.print_help(output.stream)
42
+ raise AbortExecution(0)
43
+
44
+ return options
45
+
46
+
47
+ CommandCallback = Callable[[PdfFillerArgumentParser, Any], Any]
48
+ """Command callback function.
49
+ :param PdfFillerArgumentParser: Current parser object
50
+ :param Any: Command-line arguments as a list
51
+ :return: Depend on each command.
52
+ """
53
+
54
+ SubCommandCallback = Callable[[PdfFillerArgumentParser, argparse.ArgumentParser, Any], Any]
55
+ """Sub-command callback function.
56
+ :param PdfFillerArgumentParser: Current parser object
57
+ :param argparse.ArgumentParser: The command sub-parser object
58
+ :param Any: Command-line arguments as a list
59
+ :return: Depend on each command.
60
+ """
61
+
62
+ FormatterCallback = Callable[[Any], None]
63
+ """Formatter callback function.
64
+ :param Dict: Output format keyword with its callback function function
65
+ """
66
+
67
+
68
+ class BaseCommand:
69
+ """Base PdfFiller command"""
70
+
71
+ def __init__(
72
+ self,
73
+ name: str,
74
+ callback: Union[CommandCallback, SubCommandCallback],
75
+ formatters: Optional[Dict[str, FormatterCallback]] = None,
76
+ ) -> None:
77
+ self.formatters: Dict[str, FormatterCallback] = {"text": lambda x: None}
78
+ self.callback = callback
79
+ self.callback_name: str = name
80
+ if formatters:
81
+ for kind, action in formatters.items():
82
+ if callable(action):
83
+ self.formatters[kind] = action
84
+ else:
85
+ raise PdfFillerException(
86
+ f"Invalid formatter for {kind}. The formatter must be" "a valid function"
87
+ )
88
+ if callback.__doc__:
89
+ self.callback_doc = callback.__doc__
90
+ else:
91
+ raise PdfFillerException(
92
+ f"No documentation string defined for command: '{self.callback_name}'."
93
+ " PdfFiller commands should provide a documentation string explaining "
94
+ "its use briefly."
95
+ )
96
+
97
+ @staticmethod
98
+ def init_log_levels(parser: argparse.ArgumentParser) -> None:
99
+ """Add verbosity command-line option"""
100
+ parser.add_argument(
101
+ "-V",
102
+ "--verbosity",
103
+ default="status",
104
+ metavar="LEVEL",
105
+ nargs="?",
106
+ type=str,
107
+ help="Level of detail of the output. Valid options from less verbose "
108
+ "to more verbose: -Vquiet, -Verror, -Vwarning, -Vnotice, -Vstatus, "
109
+ "-V or -Vverbose, -VV or -Vdebug, -VVV or -vtrace",
110
+ )
111
+
112
+ @staticmethod
113
+ def init_log_file(parser: argparse.ArgumentParser) -> None:
114
+ """Add output log file command-line option"""
115
+ parser.add_argument(
116
+ "-L",
117
+ "--log-file",
118
+ metavar="PATH",
119
+ type=str,
120
+ help="Send output to PATH instead of stderr.",
121
+ action=OnceArgument,
122
+ )
123
+
124
+ @property
125
+ def _help_formatters(self) -> List[str]:
126
+ """
127
+ Formatters that are shown as available in help, 'text' formatter
128
+ should not appear
129
+ """
130
+ return [formatter for formatter in self.formatters if formatter != "text"]
131
+
132
+ def init_formatters(self, parser: argparse.ArgumentParser) -> None:
133
+ """Add formatters command-line options."""
134
+ formatters = self._help_formatters
135
+ if formatters:
136
+ parser.add_argument(
137
+ "-f",
138
+ "--format",
139
+ metavar="NAME",
140
+ action=OnceArgument,
141
+ help=f"Select the output format: {', '.join(formatters)}",
142
+ )
143
+
144
+ @property
145
+ def name(self) -> str:
146
+ """Get action name"""
147
+ return self.callback_name
148
+
149
+ @property
150
+ def method(self) -> Union[CommandCallback, SubCommandCallback]:
151
+ """Get action method"""
152
+ return self.callback
153
+
154
+ @property
155
+ def doc(self) -> str:
156
+ """Get action help message"""
157
+ return self.callback_doc
158
+
159
+ def _format(self, parser: argparse.ArgumentParser, info: Dict[str, Any], *args: Any) -> None:
160
+ parser_args, _ = parser.parse_known_args(*args)
161
+
162
+ default_format = "text"
163
+ try:
164
+ formatarg = parser_args.format or default_format
165
+ except AttributeError:
166
+ formatarg = default_format
167
+
168
+ try:
169
+ formatter = self.formatters[formatarg]
170
+ except KeyError as exc:
171
+ raise PdfFillerException(
172
+ f"{formatarg} is not a known format. Supported formatters are: "
173
+ f"{', '.join(self._help_formatters)}"
174
+ ) from exc
175
+
176
+ formatter(info)
177
+
178
+
179
+ class PdfFillerCommand(BaseCommand):
180
+ """Main PdfFiller command object"""
181
+
182
+ def __init__(
183
+ self,
184
+ cb: CommandCallback,
185
+ group: Optional[str] = None,
186
+ formatters: Optional[Dict[str, FormatterCallback]] = None,
187
+ callback_name: Optional[str] = None,
188
+ ) -> None:
189
+ if not callback_name:
190
+ callback_name = cb.__name__.replace("_", "-")
191
+ super().__init__(callback_name, cb, formatters=formatters)
192
+ self.subcommands: Dict[str, "PdfFillerSubCommand"] = {}
193
+ self.group_name = group # or "Other"
194
+
195
+ def add_subcommand(self, subcommand: "PdfFillerSubCommand") -> None:
196
+ """Register new sub-command"""
197
+ subcommand.set_name(self.callback_name)
198
+ self.subcommands[subcommand.callback_name] = subcommand
199
+
200
+ def _docs(self) -> PdfFillerArgumentParser:
201
+ parser = PdfFillerArgumentParser(
202
+ description=self.callback_doc,
203
+ prog=f"pdffiller {self.callback_name}",
204
+ formatter_class=SmartFormatter,
205
+ add_help=False,
206
+ )
207
+ return parser
208
+
209
+ def run(self, *args: Any) -> None:
210
+ """Parse and execute requested command"""
211
+ parser = PdfFillerArgumentParser(
212
+ description=self.callback_doc,
213
+ prog=f"pdffiller {self.callback_name}",
214
+ formatter_class=SmartFormatter,
215
+ add_help=False,
216
+ )
217
+ # pylint: disable=protected-access
218
+ parser._command = self
219
+ info = self.callback(parser, *args)
220
+
221
+ if not self.subcommands:
222
+ self._format(parser, info, *args)
223
+ else:
224
+ subcommand_parser: SubParserType = parser.add_subparsers(
225
+ dest="subcommand",
226
+ )
227
+ subcommand_parser.required = True
228
+ try:
229
+ sub = self.subcommands[args[0][0]]
230
+ except (KeyError, IndexError): # display help
231
+ for sub in self.subcommands.values():
232
+ sub.set_parser(subcommand_parser)
233
+ parser.print_help()
234
+ raise InvalidSubCommandNameException( # pylint: disable=raise-missing-from
235
+ args[0][0] if len(args[0]) else ""
236
+ )
237
+
238
+ sub.set_parser(subcommand_parser)
239
+ sub.run(parser, *args)
240
+
241
+ @property
242
+ def group(self) -> Optional[str]:
243
+ """Gets group name."""
244
+ return self.group_name
245
+
246
+
247
+ class PdfFillerSubCommand(BaseCommand):
248
+ """PdfFiller sub-command"""
249
+
250
+ parser: argparse.ArgumentParser
251
+
252
+ def __init__(
253
+ self,
254
+ cb: SubCommandCallback,
255
+ formatters: Optional[Dict[str, FormatterCallback]] = None,
256
+ ) -> None:
257
+ super().__init__("", cb, formatters=formatters)
258
+ self.subcommand_name = cb.__name__.replace("_", "-")
259
+
260
+ def run(self, parent_parser: PdfFillerArgumentParser, *args: object) -> None:
261
+ """Execute the sub-command"""
262
+ setattr(self.parser, "_command", self)
263
+ info = self.callback(parent_parser, self.parser, *args)
264
+ self._format(parent_parser, info, *args)
265
+
266
+ def set_name(self, parent_name: str) -> None:
267
+ """Set sub-command name"""
268
+ self.callback_name = self.subcommand_name.replace(f"{parent_name}-", "", 1)
269
+
270
+ def set_parser(self, subcommand_parser: SubParserType) -> None:
271
+ """Set the associated parser"""
272
+ self.parser = subcommand_parser.add_parser(
273
+ self.callback_name, help=self.callback_doc, add_help=True
274
+ )
275
+ self.parser.description = self.callback_doc
276
+
277
+
278
+ def pdffiller_command(
279
+ group: Optional[str],
280
+ formatters: Optional[Dict[str, FormatterCallback]] = None,
281
+ name: Optional[str] = None,
282
+ ) -> Callable[[CommandCallback], PdfFillerCommand]:
283
+ """Register a PdfFiller command"""
284
+ return lambda f: PdfFillerCommand(f, group, formatters=formatters, callback_name=name)
285
+
286
+
287
+ def pdffiller_subcommand(
288
+ formatters: Optional[Dict[str, FormatterCallback]] = None
289
+ ) -> Callable[[SubCommandCallback], PdfFillerSubCommand]:
290
+ """Register a PdfFiller sub-command"""
291
+ return lambda f: PdfFillerSubCommand(f, formatters=formatters)
File without changes