Skip to content

Commit bffbefa

Browse files
authored
Add warnings for convert_default, handle edge case (#94)
1 parent 4bad129 commit bffbefa

File tree

6 files changed

+182
-83
lines changed

6 files changed

+182
-83
lines changed

clize/converters.py

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# clize -- A command-line argument parser for Python
22
# Copyright (C) 2011-2016 by Yann Kaiser and contributors. See AUTHORS and
33
# COPYING for details.
4-
4+
import contextlib
55
import sys
66
import io
77
import os
8+
import warnings
89
from functools import partial
910

1011
from sigtools.modifiers import autokwoargs
@@ -67,51 +68,53 @@ def __exit__(self, *exc_info):
6768
if self.arg != self.stdio or not self.keep_stdio_open:
6869
self.f.close()
6970

70-
def _none_guard(cls, maybe_none, *args, **kwargs):
71-
if maybe_none is None:
72-
return None
73-
else:
74-
return cls(maybe_none, *args, **kwargs)
7571

72+
@contextlib.contextmanager
73+
def _silence_convert_default_warning():
74+
with warnings.catch_warnings():
75+
warnings.filterwarnings("ignore", "The convert_default parameter of value_converter", DeprecationWarning, r"clize\..*")
76+
yield
7677

77-
@parser.value_converter(name='FILE', convert_default=True)
78-
@autokwoargs(exceptions=['arg'])
79-
def file(arg=util.UNSET, stdio='-', keep_stdio_open=False, **kwargs):
80-
"""Takes a file argument and provides a Python object that opens a file
8178

82-
::
79+
with _silence_convert_default_warning():
80+
@parser.value_converter(name='FILE', convert_default=True)
81+
@autokwoargs(exceptions=['arg'])
82+
def file(arg=util.UNSET, stdio='-', keep_stdio_open=False, **kwargs):
83+
"""Takes a file argument and provides a Python object that opens a file
8384
84-
def main(in_: file(), out: file(mode='w')):
85-
with in_ as infile, out as outfile:
86-
outfile.write(infile.read())
85+
::
8786
88-
:param stdio: If this value is passed as argument, it will be interpreted
89-
as *stdin* or *stdout* depending on the ``mode`` parameter supplied.
90-
:param keep_stdio_open: If true, does not close the file if it is *stdin*
91-
or *stdout*.
87+
def main(in_: file(), out: file(mode='w')):
88+
with in_ as infile, out as outfile:
89+
outfile.write(infile.read())
9290
93-
Other arguments will be relayed to `io.open`.
91+
:param stdio: If this value is passed as argument, it will be interpreted
92+
as *stdin* or *stdout* depending on the ``mode`` parameter supplied.
93+
:param keep_stdio_open: If true, does not close the file if it is *stdin*
94+
or *stdout*.
9495
95-
This converter also opens the file or stream designated by the default
96-
value::
96+
Other arguments will be relayed to `io.open`.
9797
98-
def main(inf: file()='-'):
99-
with inf as f:
100-
print(f)
98+
You can specify a default file name using `clize.Parameter.cli_default`::
10199
102-
.. code-block:: console
100+
def main(inf: (file(), Parameter.cli_default("-"))):
101+
with inf as f:
102+
print(f)
103103
104-
$ python3 ./main.py
105-
<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>
104+
.. code-block:: console
106105
106+
$ python3 ./main.py
107+
<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>
107108
108-
"""
109-
if arg is not util.UNSET:
110-
return _none_guard(_FileOpener, arg, kwargs, stdio, keep_stdio_open)
111-
return parser.value_converter(
112-
partial(_none_guard, _FileOpener, kwargs=kwargs,
113-
stdio=stdio, keep_stdio_open=keep_stdio_open),
114-
name='FILE', convert_default=True)
109+
110+
"""
111+
if arg is not util.UNSET:
112+
return _FileOpener(arg, kwargs, stdio, keep_stdio_open)
113+
with _silence_convert_default_warning():
114+
return parser.value_converter(
115+
partial(_FileOpener, kwargs=kwargs,
116+
stdio=stdio, keep_stdio_open=keep_stdio_open),
117+
name='FILE', convert_default=True)
115118

116119

117120
def _convert_ioerror(arg, exc):

clize/parser.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,7 @@ def __init__(self, **kwargs):
232232
display_name='<internal>', **kwargs)
233233

234234

235-
@modifiers.kwoargs(start='name')
236-
def value_converter(func=None, name=None, convert_default=False):
235+
def value_converter(func=None, *, name=None, convert_default=None, convert_default_filter=lambda s: isinstance(s, str)):
237236
"""Callables decorated with this can be used as a value converter.
238237
239238
:param str name: Use this name to designate the parameter value type.
@@ -244,18 +243,31 @@ def value_converter(func=None, name=None, convert_default=False):
244243
The default is the name of the decorated function or type, modified to
245244
follow this rule.
246245
247-
:param bool convert_default: If true, the value converter will be called
246+
:param bool convert_default: *Deprecated*: use the `Parameter.cli_default()` annotation instead.
247+
248+
If true, the value converter will be called
248249
with the default parameter value if none was supplied. Otherwise, the
249250
default parameter is used as-is.
250251
251252
Make sure to handle `None` appropriately if you override this.
252253
254+
:param function convert_convert_default_filter: *Deprecated* Avoid ``convert_default`` completely.
255+
256+
If ``convert_default`` is true, controls when the converter is called.
257+
The converter is used only if the given function returns true.
258+
253259
See :ref:`value converter`.
254260
"""
261+
if convert_default is not None:
262+
warnings.warn("The convert_default parameter of value_converter is deprecated. "
263+
"Direct users to use clize.Parameter.cli_default() instead.",
264+
DeprecationWarning,
265+
stacklevel=2)
255266
def decorate(func):
256267
info = {
257268
'name': util.name_type2cli(func) if name is None else name,
258269
'convert_default': convert_default,
270+
'convert_default_filter': convert_default_filter,
259271
}
260272
try:
261273
func._clize__value_converter = info
@@ -390,19 +402,30 @@ def help_parens(self):
390402

391403
def post_parse(self, ba):
392404
super(ParameterWithValue, self).post_parse(ba)
393-
try:
394-
info = self.conv._clize__value_converter
395-
except AttributeError:
396-
pass
397-
else:
398-
if self in ba.not_provided:
399-
if self.cli_default is not util.UNSET:
400-
self.set_value(
401-
ba,
402-
self.cli_default.value_after_conversion(partial(self.coerce_value, ba=ba))
403-
)
404-
elif self.default is not util.UNSET and info['convert_default']:
405-
self.set_value(ba, self.coerce_value(self.default, ba))
405+
if self in ba.not_provided:
406+
default_value = self.default_value_if_non_source_default(ba)
407+
if default_value is not util.UNSET:
408+
self.set_value(ba, default_value)
409+
410+
def default_value_if_non_source_default(self, ba):
411+
if self.cli_default is not util.UNSET:
412+
return self.cli_default.value_after_conversion(partial(self.coerce_value, ba=ba))
413+
if self.default is not util.UNSET:
414+
try:
415+
info = self.conv._clize__value_converter
416+
except AttributeError:
417+
pass
418+
else:
419+
if info['convert_default'] and info['convert_default_filter'](self.default):
420+
return self.conv(self.default)
421+
return util.UNSET
422+
423+
def default_value(self, ba):
424+
converted_default = self.default_value_if_non_source_default(ba)
425+
if converted_default is not util.UNSET:
426+
return converted_default
427+
return self.default
428+
406429

407430

408431
class NamedParameter(Parameter):
@@ -617,10 +640,10 @@ def set_value(self, ba, val):
617640
return
618641
else:
619642
if arg is util.UNSET:
620-
if param.cli_default != util.UNSET:
621-
ba.args.append(param.cli_default.value_after_conversion(partial(self.coerce_value, ba=ba)))
622-
elif param.default != util.UNSET:
623-
ba.args.append(param.default)
643+
default_value = param.default_value(ba)
644+
if default_value is not util.UNSET:
645+
ba.args.append(default_value)
646+
# ba.args.append(param.cli_default.value_after_conversion(partial(self.coerce_value, ba=ba)))
624647
else:
625648
raise ValueError(
626649
"Can't set parameters after required parameters")
@@ -967,6 +990,24 @@ def _use_class(pos_cls, varargs_cls, named_cls, varkwargs_cls, kwargs,
967990
"with clize.parser.value_converter()"
968991
)
969992

993+
if kwargs['default'] is not util.UNSET:
994+
try:
995+
info = getattr(kwargs['conv'], "_clize__value_converter")
996+
except AttributeError:
997+
pass
998+
else:
999+
if info["convert_default"] and info["convert_default_filter"](kwargs["default"]):
1000+
warnings.warn(
1001+
f"For parameter '{param.name}': "
1002+
"Default argument conversion is deprecated. "
1003+
"Instead, please use clize.Parameter.cli_default(value) "
1004+
"when you need a default value that "
1005+
"is converted the same way as a value passed in from the command line, "
1006+
"as opposed to values passed in from calling the function in Python, "
1007+
"which aren't converted by Clize.",
1008+
DeprecationWarning
1009+
)
1010+
9701011
if named:
9711012
kwargs['aliases'] = aliases
9721013
if param.kind == param.VAR_KEYWORD:

clize/runner.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# clize -- A command-line argument parser for Python
22
# Copyright (C) 2011-2016 by Yann Kaiser and contributors. See AUTHORS and
33
# COPYING for details.
4+
import contextlib
45
import pathlib
56
import sys
67
import os
78
import typing
9+
import warnings
810
from functools import partial, update_wrapper
911
import itertools
1012
import shutil
@@ -213,14 +215,33 @@ def helper(self):
213215
@util.property_once
214216
def signature(self):
215217
"""The `.parser.CliSignature` object used to parse arguments."""
216-
return parser.CliSignature.from_signature(
217-
self.func_signature,
218-
extra=itertools.chain(self._process_alt(self.alt), self.extra))
218+
extra = itertools.chain(self._process_alt(self.alt), self.extra)
219+
with self._move_warnings_to_func():
220+
return parser.CliSignature.from_signature(
221+
self.func_signature,
222+
extra=extra)
219223

220224
@util.property_once
221225
def func_signature(self):
222226
return signature(self.func)
223227

228+
@contextlib.contextmanager
229+
def _move_warnings_to_func(self):
230+
try:
231+
code = self.func.__code__
232+
filename = code.co_filename
233+
lineno = code.co_firstlineno
234+
module = self.func.__module__
235+
module_globals = self.func.__globals__
236+
except AttributeError:
237+
yield
238+
else:
239+
with warnings.catch_warnings(record=True) as caught_warnings:
240+
yield
241+
for warning in caught_warnings:
242+
registry = module_globals.setdefault("__warningregistry__", {})
243+
warnings.warn_explicit(warning.message, warning.category, filename, lineno, module, registry, module_globals)
244+
224245
def _process_alt(self, alt):
225246
if self.help_names:
226247
p = parser.FallbackCommandParameter(

clize/tests/test_converters.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from sigtools import support, modifiers
1414

15-
from clize import parser, errors, converters
15+
from clize import parser, errors, converters, Parameter
1616
from clize.tests.util import Fixtures, SignatureFixtures, Tests
1717

1818

@@ -149,11 +149,25 @@ def func(afile):
149149
self.assertFalse(stderr.getvalue())
150150
self.assertTrue(self.completed)
151151

152-
def test_default_value(self):
152+
def test_deprecated_default_value(self):
153153
path = os.path.join(self.temp, 'default')
154154
open(path, 'w').close()
155-
@modifiers.annotate(afile=converters.file())
156-
def func(afile=path):
155+
def func(afile: converters.file()=path):
156+
with afile as f:
157+
self.assertEqual(f.name, path)
158+
self.assertEqual(f.mode, 'r')
159+
self.assertTrue(f.closed)
160+
self.completed = True
161+
with self.assertWarns(DeprecationWarning):
162+
stdout, stderr = self.crun(func, ['test'])
163+
self.assertFalse(stdout.getvalue())
164+
self.assertFalse(stderr.getvalue())
165+
self.assertTrue(self.completed)
166+
167+
def test_cli_default_value(self):
168+
path = os.path.join(self.temp, 'default')
169+
open(path, 'w').close()
170+
def func(afile: (converters.file(), Parameter.cli_default(path))):
157171
with afile as f:
158172
self.assertEqual(f.name, path)
159173
self.assertEqual(f.mode, 'r')
@@ -165,8 +179,7 @@ def func(afile=path):
165179
self.assertTrue(self.completed)
166180

167181
def test_default_none_value(self):
168-
@modifiers.annotate(afile=converters.file())
169-
def func(afile=None):
182+
def func(afile: converters.file() = None):
170183
self.assertIs(afile, None)
171184
self.completed = True
172185
stdout, stderr = self.crun(func, ['test'])

0 commit comments

Comments
 (0)