7
7
"""
8
8
9
9
import itertools
10
+ import inspect
11
+ import typing
10
12
from functools import partial , wraps
11
13
import pathlib
12
14
import warnings
13
15
14
- from sigtools import modifiers
16
+ from sigtools import modifiers , signature
15
17
import attr
16
18
17
19
from clize import errors , util
18
20
19
21
22
+ class ClizeAnnotations :
23
+ def __init__ (self , annotations ):
24
+ self .clize_annotations = util .maybe_iter (annotations )
25
+
26
+ def __repr__ (self ):
27
+ arg = ', ' .join (repr (item ) for item in self .clize_annotations )
28
+ return f"clize.Clize[{ arg } ]"
29
+
30
+ @classmethod
31
+ def get_clize_annotations (cls , top_level_annotation ):
32
+ if top_level_annotation is inspect .Parameter .empty :
33
+ return ParsedAnnotation ()
34
+
35
+ if _is_annotated_instance (top_level_annotation ):
36
+ return ParsedAnnotation (top_level_annotation .__origin__ , tuple (_extract_annotated_metadata (top_level_annotation .__metadata__ )))
37
+
38
+ return ParsedAnnotation (clize_annotations = util .maybe_iter (top_level_annotation ))
39
+
40
+
41
+ @attr .define
42
+ class ParsedAnnotation :
43
+ type_annotation : typing .Any = inspect .Parameter .empty
44
+ clize_annotations : typing .Tuple [typing .Any ] = ()
45
+
46
+
47
+ def _is_annotated_instance (annotation ):
48
+ try :
49
+ annotation .__origin__
50
+ annotation .__metadata__
51
+ except AttributeError :
52
+ return False
53
+ else :
54
+ return True
55
+
56
+
57
+ def _extract_annotated_metadata (metadata ):
58
+ for item in metadata :
59
+ if _is_annotated_instance (item ):
60
+ yield from _extract_annotated_metadata (item .__metadata__ )
61
+ elif isinstance (item , ClizeAnnotations ):
62
+ yield from item .clize_annotations
63
+
64
+
20
65
class ParameterFlag (object ):
21
66
def __init__ (self , name , prefix = 'clize.Parameter' ):
22
67
self .name = name
@@ -790,7 +835,7 @@ class _NamedWithMixin(cls, OptionParameter): pass
790
835
791
836
792
837
def _use_class (pos_cls , varargs_cls , named_cls , varkwargs_cls , kwargs ,
793
- param , annotations ):
838
+ param , annotations , * , type_annotation ):
794
839
named = param .kind in (param .KEYWORD_ONLY , param .VAR_KEYWORD )
795
840
aliases = [util .name_py2cli (param .name , named )]
796
841
default = util .UNSET
@@ -811,7 +856,19 @@ def _use_class(pos_cls, varargs_cls, named_cls, varkwargs_cls, kwargs,
811
856
if Parameter .LAST_OPTION in annotations :
812
857
kwargs ['last_option' ] = True
813
858
814
- prev_conv = None
859
+ exclusive_converter = None
860
+ set_converter = False
861
+
862
+ if type_annotation != param .empty :
863
+ try :
864
+ # we specifically don't set exclusive_converter
865
+ # so that clize annotations can set a different converter
866
+ conv = get_value_converter (type_annotation )
867
+ except ValueError :
868
+ pass
869
+ else :
870
+ set_converter = True
871
+
815
872
for thing in annotations :
816
873
if isinstance (thing , Parameter ):
817
874
return thing
@@ -826,11 +883,13 @@ def _use_class(pos_cls, varargs_cls, named_cls, varkwargs_cls, kwargs,
826
883
except ValueError :
827
884
pass
828
885
else :
829
- if prev_conv is not None :
886
+ if exclusive_converter is not None :
830
887
raise ValueError (
831
888
"Value converter specified twice in annotation: "
832
- "{0.__name__} {1.__name__}" .format (prev_conv , thing ))
833
- prev_conv = thing
889
+ f"{ exclusive_converter .__name__ } { thing .__name__ } "
890
+ )
891
+ exclusive_converter = thing
892
+ set_converter = True
834
893
continue
835
894
if isinstance (thing , str ):
836
895
if not named :
@@ -853,9 +912,10 @@ def _use_class(pos_cls, varargs_cls, named_cls, varkwargs_cls, kwargs,
853
912
854
913
kwargs ['default' ] = default if not kwargs .get ('required' ) else util .UNSET
855
914
kwargs ['conv' ] = conv
856
- if prev_conv is None and default is not util .UNSET and default is not None :
915
+ if not set_converter and default is not util .UNSET and default is not None :
857
916
try :
858
917
kwargs ['conv' ] = get_value_converter (type (default ))
918
+ set_converter = True
859
919
except ValueError :
860
920
raise ValueError (
861
921
"Cannot find value converter for default value {!r}. "
@@ -864,6 +924,14 @@ def _use_class(pos_cls, varargs_cls, named_cls, varkwargs_cls, kwargs,
864
924
"convert the value, make sure it is decorated "
865
925
"with clize.parser.value_converter()"
866
926
.format (default ))
927
+ if not set_converter and type_annotation is not inspect .Parameter .empty :
928
+ raise ValueError (
929
+ f"Cannot find a value converter for type { type_annotation } . "
930
+ "Please specify one as an annotation.\n "
931
+ "If the type should be used to "
932
+ "convert the value, make sure it is decorated "
933
+ "with clize.parser.value_converter()"
934
+ )
867
935
868
936
if named :
869
937
kwargs ['aliases' ] = aliases
@@ -998,10 +1066,9 @@ def from_signature(cls, sig, extra=(), **kwargs):
998
1066
def convert_parameter (cls , param ):
999
1067
"""Convert a Python parameter to a CLI parameter."""
1000
1068
param_annotation = param .upgraded_annotation .source_value ()
1001
- if param .annotation != param .empty :
1002
- annotations = util .maybe_iter (param_annotation )
1003
- else :
1004
- annotations = []
1069
+ ca = ClizeAnnotations .get_clize_annotations (param_annotation )
1070
+ annotations = ca .clize_annotations
1071
+ type_annotation = ca .type_annotation
1005
1072
1006
1073
if Parameter .IGNORE in annotations :
1007
1074
return Parameter .IGNORE
@@ -1014,9 +1081,29 @@ def convert_parameter(cls, param):
1014
1081
else :
1015
1082
conv = cls .converter
1016
1083
1017
- return conv (param , annotations )
1018
-
1019
-
1084
+ try :
1085
+ return conv (param , annotations , type_annotation = type_annotation )
1086
+ except TypeError as e :
1087
+ if "type_annotation" in signature (conv ).parameters :
1088
+ raise e
1089
+ else :
1090
+ result = conv (param , annotations )
1091
+ name = getattr (conv , "__name__" , repr (conv ))
1092
+ while isinstance (conv , partial ):
1093
+ conv = conv .func
1094
+ impl_name = getattr (conv , "__qualname__" , name )
1095
+ module = getattr (conv , "__module__" )
1096
+ if module :
1097
+ impl_name = f"{ module } .{ impl_name } "
1098
+ warnings .warn (
1099
+ (
1100
+ "Clize 6.0 will require parameter converters "
1101
+ "to support the 'type_annotation' keyword argument: "
1102
+ f"converter '{ name } ' ({ impl_name } ) should be updated to accept it"
1103
+ ),
1104
+ DeprecationWarning ,
1105
+ )
1106
+ return result
1020
1107
1021
1108
def read_arguments (self , args , name ):
1022
1109
"""Returns a `.CliBoundArguments` instance for this CLI signature
0 commit comments