From 58d68ce340265af6f40641473e81ff577789ae5b Mon Sep 17 00:00:00 2001 From: "plazer1@EliteBook" Date: Tue, 8 Sep 2026 19:09:28 +0200 Subject: [PATCH 01/11] #91: Use `RawTextHelpFormatter` instead of `RawDescriptionHelpFormatter` --- plac_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plac_core.py b/plac_core.py index fe8b21b..a6a2e89 100644 --- a/plac_core.py +++ b/plac_core.py @@ -157,7 +157,7 @@ def pconf(obj): """ cfg = dict(description=(textwrap.dedent(obj.__doc__.rstrip()) if obj.__doc__ else None), - formatter_class=argparse.RawDescriptionHelpFormatter) + formatter_class=argparse.RawTextHelpFormatter) for name in dir(obj): if name in PARSER_CFG: # argument of ArgumentParser cfg[name] = getattr(obj, name) From 0e75b7bba884d9d04dc619793ebefb7bcca13608 Mon Sep 17 00:00:00 2001 From: "plazer1@EliteBook" Date: Tue, 8 Sep 2026 19:17:55 +0200 Subject: [PATCH 02/11] improved formatter --- plac_core.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/plac_core.py b/plac_core.py index a6a2e89..cdd73fe 100644 --- a/plac_core.py +++ b/plac_core.py @@ -147,6 +147,27 @@ def from_(cls, obj): NONE = object() # sentinel use to signal the absence of a default + +class HelpFormatter(argparse.HelpFormatter): + + @staticmethod + def clean_text(text): + return textwrap.dedent(text.removeprefix('\n')) + + def _fill_text(self, text, width, indent): + """ + For general program description. + """ + clean_text = self.clean_text(text) + return textwrap.indent(clean_text, indent) + + def _split_lines(self, text, width): + """ + For argument descriptions. + """ + return self.clean_text(text).splitlines() + + PARSER_CFG = getfullargspec(argparse.ArgumentParser.__init__).args[1:] # the default arguments accepted by an ArgumentParser object @@ -157,7 +178,7 @@ def pconf(obj): """ cfg = dict(description=(textwrap.dedent(obj.__doc__.rstrip()) if obj.__doc__ else None), - formatter_class=argparse.RawTextHelpFormatter) + formatter_class=HelpFormatter) for name in dir(obj): if name in PARSER_CFG: # argument of ArgumentParser cfg[name] = getattr(obj, name) From 116c0de45b408394067f9a3ca82c8ac43998f5b8 Mon Sep 17 00:00:00 2001 From: "plazer1@EliteBook" Date: Tue, 8 Sep 2026 22:57:39 +0200 Subject: [PATCH 03/11] make `MultilineHelpFormatter` optional; add way to pass `formatter_class` to `call` --- .gitignore | 1 + plac_core.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c23ab75..80095ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +**/__pycache__/ doc/conf.shelve.db docs/ # pixi environments diff --git a/plac_core.py b/plac_core.py index cdd73fe..a4bc0a3 100644 --- a/plac_core.py +++ b/plac_core.py @@ -148,7 +148,10 @@ def from_(cls, obj): NONE = object() # sentinel use to signal the absence of a default -class HelpFormatter(argparse.HelpFormatter): +class MultilineFormatter(argparse.HelpFormatter): + """ + This formatter preserves newlines in description of arguments. + """ @staticmethod def clean_text(text): @@ -178,7 +181,7 @@ def pconf(obj): """ cfg = dict(description=(textwrap.dedent(obj.__doc__.rstrip()) if obj.__doc__ else None), - formatter_class=HelpFormatter) + formatter_class=argparse.RawDescriptionHelpFormatter) for name in dir(obj): if name in PARSER_CFG: # argument of ArgumentParser cfg[name] = getattr(obj, name) @@ -440,17 +443,19 @@ def iterable(obj): return hasattr(obj, '__iter__') and not inspect.isclass(obj) and not isinstance(obj, (str, bytes)) -def call(obj, arglist=None, eager=True, version=None): +def call(obj, arglist=None, eager=True, version=None, **parser_confparams): """ If obj is a function or a bound method, parse the given arglist by using the parser inferred from the annotations of obj and call obj with the parsed arguments. If obj is an object with attribute .commands, dispatch to the + associated subparser. + :param parser_confparams: keyword arguments passed to ArgumentParser """ if arglist is None: arglist = sys.argv[1:] - parser = parser_from(obj) + parser = parser_from(obj, **parser_confparams) if version: parser.add_argument( '--version', '-v', action='version', version=version) From 35bd33b565b364a6d3f263141991de58cb0c59d0 Mon Sep 17 00:00:00 2001 From: "plazer1@EliteBook" Date: Tue, 8 Sep 2026 23:20:26 +0200 Subject: [PATCH 04/11] revert unintended change --- plac_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plac_core.py b/plac_core.py index a4bc0a3..ca4648a 100644 --- a/plac_core.py +++ b/plac_core.py @@ -449,8 +449,8 @@ def call(obj, arglist=None, eager=True, version=None, **parser_confparams): by using the parser inferred from the annotations of obj and call obj with the parsed arguments. If obj is an object with attribute .commands, dispatch to the - associated subparser. + :param parser_confparams: keyword arguments passed to ArgumentParser """ if arglist is None: From 709e8c50e17ae4be805ee3ac454635997bc9fe15 Mon Sep 17 00:00:00 2001 From: plazer1 <21986469+plazer1@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:18:18 +0200 Subject: [PATCH 05/11] Update README.md Add mention of the new functionality to readme --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 49b9ddb..0cee1b8 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,41 @@ options: -d, --debug debug mode ``` +### Multi-line help + +You may want to add a line break in the help string for a parameter, +to make it more readable in case it's long. In such case `plac` will +automatically remove these line breaks by default, whether you create +them using explicit newline characters or using a multi-line string literal. +To preserve line breaks in argument help strings, pass the parameter +`formatter_class=plac.MultilineFormatter` to `plac.call()`: + +```python +@plac.opt('iter', help="Number of iterations.\nMore iterations provide" + + " better results but take more time to compute.") +... + +if __name__ == '__main__': + plac.call(main, formatter_class=plac.MultilineFormatter) +``` + +This will remove indentation of help strings while keeping the new-line +characters intact. Removing indentation is useful in case you use +multi-line string literals, as in: + +```python +class Application: + @plac.flg('debug', help=""" + Debug mode: outputs detailed, possibly sensitive information at + the cost of decreased performance. Should not be used in production. + """) + def run(debug=False): + ... +``` + +If you wish to preserve the indentation, you can use +`argparse.RawTextHelpFormatter` as the `formatter_class` instead. + ## Decorator reference To use `plac` all you need to know are the following three decorators: From 42c2e1a0d40bd34a2b4c012a7511fa271f7b58e7 Mon Sep 17 00:00:00 2001 From: plazer1 <21986469+plazer1@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:18:51 +0200 Subject: [PATCH 06/11] Update plac_core.py Python 2 compat --- plac_core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plac_core.py b/plac_core.py index ca4648a..0e55e38 100644 --- a/plac_core.py +++ b/plac_core.py @@ -155,7 +155,9 @@ class MultilineFormatter(argparse.HelpFormatter): @staticmethod def clean_text(text): - return textwrap.dedent(text.removeprefix('\n')) + if text.startswith('\n'): + text = text[1:] + return textwrap.dedent(text) def _fill_text(self, text, width, indent): """ From ad91d7a5ddf4b2a85445f43d7c4bcd19df2eb9cc Mon Sep 17 00:00:00 2001 From: "plazer1@EliteBook" Date: Wed, 9 Sep 2026 07:27:40 +0200 Subject: [PATCH 07/11] remove `MultilineHelpFormatter` -- the logic was too flawed and failed in more complex cases --- README.md | 26 ++++++++++++++++++-------- plac_core.py | 26 -------------------------- 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 0cee1b8..2c702ce 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,12 @@ to make it more readable in case it's long. In such case `plac` will automatically remove these line breaks by default, whether you create them using explicit newline characters or using a multi-line string literal. To preserve line breaks in argument help strings, pass the parameter -`formatter_class=plac.MultilineFormatter` to `plac.call()`: +`formatter_class=plac.MultilineFormatter` to `plac.call()` +and use a double line break in the help string to emit a single line break +in the output: ```python -@plac.opt('iter', help="Number of iterations.\nMore iterations provide" + +@plac.opt('iter', help="Number of iterations.\n\nMore iterations provide" + " better results but take more time to compute.") ... @@ -118,22 +120,30 @@ if __name__ == '__main__': plac.call(main, formatter_class=plac.MultilineFormatter) ``` -This will remove indentation of help strings while keeping the new-line -characters intact. Removing indentation is useful in case you use -multi-line string literals, as in: +This will remove indentation of help strings and break long lines on +word boundaries just like the standard formatter does. Removing indentation +is necessary in case you use multi-line string literals, as in: ```python class Application: @plac.flg('debug', help=""" Debug mode: outputs detailed, possibly sensitive information at - the cost of decreased performance. Should not be used in production. + the cost of decreased performance.\n + Should not be used in production. """) def run(debug=False): ... ``` -If you wish to preserve the indentation, you can use -`argparse.RawTextHelpFormatter` as the `formatter_class` instead. +This produces the following help for the `debug` argument: +``` + -d, --debug Debug mode: outputs detailed, possibly sensitive information at the cost of decreased performance. + Should not be used in production. +``` + +If you wish to preserve the indentation, you can use `argparse.RawTextHelpFormatter` +as the `formatter_class` instead; however, there are some caveats, +e.g. it does not reflow the text according to screen width. ## Decorator reference diff --git a/plac_core.py b/plac_core.py index 0e55e38..db4cfe8 100644 --- a/plac_core.py +++ b/plac_core.py @@ -147,32 +147,6 @@ def from_(cls, obj): NONE = object() # sentinel use to signal the absence of a default - -class MultilineFormatter(argparse.HelpFormatter): - """ - This formatter preserves newlines in description of arguments. - """ - - @staticmethod - def clean_text(text): - if text.startswith('\n'): - text = text[1:] - return textwrap.dedent(text) - - def _fill_text(self, text, width, indent): - """ - For general program description. - """ - clean_text = self.clean_text(text) - return textwrap.indent(clean_text, indent) - - def _split_lines(self, text, width): - """ - For argument descriptions. - """ - return self.clean_text(text).splitlines() - - PARSER_CFG = getfullargspec(argparse.ArgumentParser.__init__).args[1:] # the default arguments accepted by an ArgumentParser object From 3cfe7ee18b75a2721e7c0e81e6cedcff568fdc76 Mon Sep 17 00:00:00 2001 From: "plazer1@EliteBook" Date: Wed, 9 Sep 2026 07:34:20 +0200 Subject: [PATCH 08/11] update readme --- README.md | 42 +++++++++--------------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 2c702ce..af1c4a3 100644 --- a/README.md +++ b/README.md @@ -100,50 +100,26 @@ options: -d, --debug debug mode ``` -### Multi-line help +### Custom help formatter You may want to add a line break in the help string for a parameter, to make it more readable in case it's long. In such case `plac` will automatically remove these line breaks by default, whether you create them using explicit newline characters or using a multi-line string literal. -To preserve line breaks in argument help strings, pass the parameter -`formatter_class=plac.MultilineFormatter` to `plac.call()` -and use a double line break in the help string to emit a single line break -in the output: +To preserve line breaks in argument help strings, you can use a custom +`HelpFormatter` by passing it as parameter `formatter_class=` to `plac.call()`: ```python -@plac.opt('iter', help="Number of iterations.\n\nMore iterations provide" + - " better results but take more time to compute.") -... +class MyCustomFormatter(argparse.HelpFormatter): + ... if __name__ == '__main__': - plac.call(main, formatter_class=plac.MultilineFormatter) + plac.call(main, formatter_class=MyCustomFormatter) ``` -This will remove indentation of help strings and break long lines on -word boundaries just like the standard formatter does. Removing indentation -is necessary in case you use multi-line string literals, as in: - -```python -class Application: - @plac.flg('debug', help=""" - Debug mode: outputs detailed, possibly sensitive information at - the cost of decreased performance.\n - Should not be used in production. - """) - def run(debug=False): - ... -``` - -This produces the following help for the `debug` argument: -``` - -d, --debug Debug mode: outputs detailed, possibly sensitive information at the cost of decreased performance. - Should not be used in production. -``` - -If you wish to preserve the indentation, you can use `argparse.RawTextHelpFormatter` -as the `formatter_class` instead; however, there are some caveats, -e.g. it does not reflow the text according to screen width. +If you wish to preserve the help strings verbatim, you can use +`argparse.RawTextHelpFormatter` as the `formatter_class`; however, there are +some caveats, e.g. it does not de-dent the text like the standard formatter does. ## Decorator reference From 73516673a654b5664872c71cbd6079dfd3f95f78 Mon Sep 17 00:00:00 2001 From: plazer1 <21986469+plazer1@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:35:58 +0200 Subject: [PATCH 09/11] README.md: mention there are other possible kwargs, not just `formatter_class` --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index af1c4a3..619bb63 100644 --- a/README.md +++ b/README.md @@ -100,14 +100,18 @@ options: -d, --debug debug mode ``` -### Custom help formatter - -You may want to add a line break in the help string for a parameter, -to make it more readable in case it's long. In such case `plac` will -automatically remove these line breaks by default, whether you create -them using explicit newline characters or using a multi-line string literal. -To preserve line breaks in argument help strings, you can use a custom -`HelpFormatter` by passing it as parameter `formatter_class=` to `plac.call()`: +### Customizing the underlying `argparse.ArgumentParser` + +It is possible to pass keyword arguments to `call`, which then get +passed on to the underlying `argparse.ArgumentParser`. This allows for +advanced control of behavior, such as using a custom help formatter. + +For example, you may want to add a line break in the help string for +a parameter, to make it more readable in case it's long. In such case +`plac` will automatically remove these line breaks by default, whether +you create them using escape newline characters (`\n`) or using +a multi-line string literal. To preserve line breaks in argument +help strings, you can use a custom `HelpFormatter` by passing itas parameter `formatter_class=` to `plac.call()`: ```python class MyCustomFormatter(argparse.HelpFormatter): From eed0d172b6c38629ef7be8a798a4bc5ed9a2a1b5 Mon Sep 17 00:00:00 2001 From: plazer1 <21986469+plazer1@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:41:02 +0200 Subject: [PATCH 10/11] Update README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 619bb63..3623c2f 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ a parameter, to make it more readable in case it's long. In such case `plac` will automatically remove these line breaks by default, whether you create them using escape newline characters (`\n`) or using a multi-line string literal. To preserve line breaks in argument -help strings, you can use a custom `HelpFormatter` by passing itas parameter `formatter_class=` to `plac.call()`: +help strings, you can use a custom `HelpFormatter` by passing it +as keyword argument `formatter_class=` to `plac.call()`: ```python class MyCustomFormatter(argparse.HelpFormatter): @@ -122,8 +123,10 @@ if __name__ == '__main__': ``` If you wish to preserve the help strings verbatim, you can use -`argparse.RawTextHelpFormatter` as the `formatter_class`; however, there are -some caveats, e.g. it does not de-dent the text like the standard formatter does. +`argparse.RawTextHelpFormatter` as the `formatter_class`; however, +there are some caveats, e.g. it does not de-dent the text like +the standard formatter does (although this can be easily accomplished +using `textwrap.dedent`). ## Decorator reference From 261d2233cf4b030232a89a34660569ffb7203729 Mon Sep 17 00:00:00 2001 From: "plazer1@EliteBook" Date: Thu, 10 Sep 2026 00:00:01 +0200 Subject: [PATCH 11/11] wrap `ArgumentParser` config in a dict instead of using kwargs --- README.md | 18 ++++++++++++------ plac_core.py | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3623c2f..44534f9 100644 --- a/README.md +++ b/README.md @@ -102,24 +102,30 @@ options: ### Customizing the underlying `argparse.ArgumentParser` -It is possible to pass keyword arguments to `call`, which then get -passed on to the underlying `argparse.ArgumentParser`. This allows for -advanced control of behavior, such as using a custom help formatter. +It is possible to pass argument `parser_config` to `call`, which then gets +passed on to the underlying `argparse.ArgumentParser` as keyword arguments. +This allows for advanced control of behavior, such as using a custom +help formatter. For example, you may want to add a line break in the help string for a parameter, to make it more readable in case it's long. In such case `plac` will automatically remove these line breaks by default, whether you create them using escape newline characters (`\n`) or using a multi-line string literal. To preserve line breaks in argument -help strings, you can use a custom `HelpFormatter` by passing it -as keyword argument `formatter_class=` to `plac.call()`: +help strings, you can use a custom `HelpFormatter` by passing a parser +config containing the `formatter_class` key to `plac.call()`: ```python class MyCustomFormatter(argparse.HelpFormatter): ... if __name__ == '__main__': - plac.call(main, formatter_class=MyCustomFormatter) + plac.call( + main, + parser_config=dict( + formatter_class=MyCustomFormatter + ) + ) ``` If you wish to preserve the help strings verbatim, you can use diff --git a/plac_core.py b/plac_core.py index db4cfe8..893a6dd 100644 --- a/plac_core.py +++ b/plac_core.py @@ -419,7 +419,7 @@ def iterable(obj): return hasattr(obj, '__iter__') and not inspect.isclass(obj) and not isinstance(obj, (str, bytes)) -def call(obj, arglist=None, eager=True, version=None, **parser_confparams): +def call(obj, arglist=None, eager=True, version=None, parser_config={}): """ If obj is a function or a bound method, parse the given arglist by using the parser inferred from the annotations of obj @@ -427,11 +427,11 @@ def call(obj, arglist=None, eager=True, version=None, **parser_confparams): If obj is an object with attribute .commands, dispatch to the associated subparser. - :param parser_confparams: keyword arguments passed to ArgumentParser + :param parser_config: dict of keyword arguments passed to ArgumentParser """ if arglist is None: arglist = sys.argv[1:] - parser = parser_from(obj, **parser_confparams) + parser = parser_from(obj, **parser_config) if version: parser.add_argument( '--version', '-v', action='version', version=version)