@@ -202,11 +202,17 @@ def attrib(
202202 specified default value or factory.
203203
204204 .. seealso:: `init`
205- :param typing.Callable converter: `callable` that is called by
205+ :param typing.Callable | Converter converter: `callable` that is called by
206206 *attrs*-generated ``__init__`` methods to convert attribute's value to
207- the desired format. It is given the passed-in value, and the returned
208- value will be used as the new value of the attribute. The value is
209- converted before being passed to the validator, if any.
207+ the desired format.
208+
209+ If a vanilla callable is passed, it is given the passed-in value as the
210+ only positional argument. It is possible to receive additional
211+ arguments by wrapping the callable in a `Converter`.
212+
213+ Either way, the returned value will be used as the new value of the
214+ attribute. The value is converted before being passed to the
215+ validator, if any.
210216
211217 .. seealso:: :ref:`converters`
212218 :param dict | None metadata: An arbitrary mapping, to be used by
@@ -2208,7 +2214,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr):
22082214 Use the cached object.setattr to set *attr_name* to *value_var*, but run
22092215 its converter first.
22102216 """
2211- return "_setattr('%s', %s(%s))" % (
2217+ return "_setattr('%s', %s(%s, self ))" % (
22122218 attr_name ,
22132219 _INIT_CONVERTER_PAT % (attr_name ,),
22142220 value_var ,
@@ -2234,7 +2240,7 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr):
22342240 if has_on_setattr :
22352241 return _setattr_with_converter (attr_name , value_var , True )
22362242
2237- return "self.%s = %s(%s)" % (
2243+ return "self.%s = %s(%s, self )" % (
22382244 attr_name ,
22392245 _INIT_CONVERTER_PAT % (attr_name ,),
22402246 value_var ,
@@ -2267,7 +2273,7 @@ def fmt_setter_with_converter(attr_name, value_var, has_on_setattr):
22672273 attr_name , value_var , has_on_setattr
22682274 )
22692275
2270- return "_inst_dict['%s'] = %s(%s)" % (
2276+ return "_inst_dict['%s'] = %s(%s, self )" % (
22712277 attr_name ,
22722278 _INIT_CONVERTER_PAT % (attr_name ,),
22732279 value_var ,
@@ -2344,10 +2350,15 @@ def _attrs_to_init_script(
23442350 has_factory = isinstance (a .default , Factory )
23452351 maybe_self = "self" if has_factory and a .default .takes_self else ""
23462352
2353+ if a .converter and not isinstance (a .converter , Converter ):
2354+ converter = Converter (a .converter , takes_self = False )
2355+ else :
2356+ converter = a .converter
2357+
23472358 if a .init is False :
23482359 if has_factory :
23492360 init_factory_name = _INIT_FACTORY_PAT % (a .name ,)
2350- if a . converter is not None :
2361+ if converter is not None :
23512362 lines .append (
23522363 fmt_setter_with_converter (
23532364 attr_name ,
@@ -2356,7 +2367,7 @@ def _attrs_to_init_script(
23562367 )
23572368 )
23582369 names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = (
2359- a . converter
2370+ converter
23602371 )
23612372 else :
23622373 lines .append (
@@ -2367,17 +2378,15 @@ def _attrs_to_init_script(
23672378 )
23682379 )
23692380 names_for_globals [init_factory_name ] = a .default .factory
2370- elif a . converter is not None :
2381+ elif converter is not None :
23712382 lines .append (
23722383 fmt_setter_with_converter (
23732384 attr_name ,
23742385 f"attr_dict['{ attr_name } '].default" ,
23752386 has_on_setattr ,
23762387 )
23772388 )
2378- names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = (
2379- a .converter
2380- )
2389+ names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = converter
23812390 else :
23822391 lines .append (
23832392 fmt_setter (
@@ -2393,15 +2402,13 @@ def _attrs_to_init_script(
23932402 else :
23942403 args .append (arg )
23952404
2396- if a . converter is not None :
2405+ if converter is not None :
23972406 lines .append (
23982407 fmt_setter_with_converter (
23992408 attr_name , arg_name , has_on_setattr
24002409 )
24012410 )
2402- names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = (
2403- a .converter
2404- )
2411+ names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = converter
24052412 else :
24062413 lines .append (fmt_setter (attr_name , arg_name , has_on_setattr ))
24072414
@@ -2414,7 +2421,7 @@ def _attrs_to_init_script(
24142421 lines .append (f"if { arg_name } is not NOTHING:" )
24152422
24162423 init_factory_name = _INIT_FACTORY_PAT % (a .name ,)
2417- if a . converter is not None :
2424+ if converter is not None :
24182425 lines .append (
24192426 " "
24202427 + fmt_setter_with_converter (
@@ -2430,9 +2437,7 @@ def _attrs_to_init_script(
24302437 has_on_setattr ,
24312438 )
24322439 )
2433- names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = (
2434- a .converter
2435- )
2440+ names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = converter
24362441 else :
24372442 lines .append (
24382443 " " + fmt_setter (attr_name , arg_name , has_on_setattr )
@@ -2453,26 +2458,22 @@ def _attrs_to_init_script(
24532458 else :
24542459 args .append (arg_name )
24552460
2456- if a . converter is not None :
2461+ if converter is not None :
24572462 lines .append (
24582463 fmt_setter_with_converter (
24592464 attr_name , arg_name , has_on_setattr
24602465 )
24612466 )
2462- names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = (
2463- a .converter
2464- )
2467+ names_for_globals [_INIT_CONVERTER_PAT % (a .name ,)] = converter
24652468 else :
24662469 lines .append (fmt_setter (attr_name , arg_name , has_on_setattr ))
24672470
24682471 if a .init is True :
2469- if a .type is not None and a . converter is None :
2472+ if a .type is not None and converter is None :
24702473 annotations [arg_name ] = a .type
2471- elif a .converter is not None :
2472- # Try to get the type from the converter.
2473- t = _AnnotationExtractor (a .converter ).get_first_param_type ()
2474- if t :
2475- annotations [arg_name ] = t
2474+ elif converter is not None and converter ._first_param_type :
2475+ # Use the type from the converter if present.
2476+ annotations [arg_name ] = converter ._first_param_type
24762477
24772478 if attrs_to_validate : # we can skip this if there are no validators.
24782479 names_for_globals ["_config" ] = _config
@@ -2984,6 +2985,77 @@ def __setstate__(self, state):
29842985Factory = _add_hash (_add_eq (_add_repr (Factory , attrs = _f ), attrs = _f ), attrs = _f )
29852986
29862987
2988+ class Converter :
2989+ """
2990+ Stores a converter callable.
2991+
2992+ Allows for the wrapped converter to take additional arguments.
2993+
2994+ :param Callable converter: A callable that converts a value.
2995+ :param bool takes_self: Pass the partially initialized instance that is
2996+ being initialized as a positional argument. (default: `True`)
2997+
2998+ .. versionadded:: 24.1.0
2999+ """
3000+
3001+ __slots__ = ("converter" , "takes_self" , "_first_param_type" , "__call__" )
3002+
3003+ def __init__ (self , converter , * , takes_self = True ):
3004+ self .converter = converter
3005+ self .takes_self = takes_self
3006+
3007+ ann = _AnnotationExtractor (converter )
3008+
3009+ self ._first_param_type = ann .get_first_param_type ()
3010+
3011+ # Defining __call__ as a regular method leads to __annotations__ being
3012+ # overwritten at a class level.
3013+ def __call__ (value , inst ):
3014+ if not self .takes_self :
3015+ return self .converter (value )
3016+
3017+ return self .converter (value , inst )
3018+
3019+ __call__ .__annotations__ .update (
3020+ ann .get_annotations_for_converter_callable ()
3021+ )
3022+ self .__call__ = __call__
3023+
3024+ def __getstate__ (self ):
3025+ """
3026+ Return a dict containing only converter and takes_self -- the rest gets
3027+ computed when loading.
3028+ """
3029+ return {"converter" : self .converter , "takes_self" : self .takes_self }
3030+
3031+ def __setstate__ (self , state ):
3032+ """
3033+ Load instance from state.
3034+ """
3035+ self .__init__ (** state )
3036+
3037+
3038+ _f = [
3039+ Attribute (
3040+ name = name ,
3041+ default = NOTHING ,
3042+ validator = None ,
3043+ repr = True ,
3044+ cmp = None ,
3045+ eq = True ,
3046+ order = False ,
3047+ hash = True ,
3048+ init = True ,
3049+ inherited = False ,
3050+ )
3051+ for name in ("converter" , "takes_self" )
3052+ ]
3053+
3054+ Converter = _add_hash (
3055+ _add_eq (_add_repr (Converter , attrs = _f ), attrs = _f ), attrs = _f
3056+ )
3057+
3058+
29873059def make_class (
29883060 name , attrs , bases = (object ,), class_body = None , ** attributes_arguments
29893061):
@@ -3116,16 +3188,19 @@ def pipe(*converters):
31163188 .. versionadded:: 20.1.0
31173189 """
31183190
3119- def pipe_converter (val ):
3191+ def pipe_converter (val , inst ):
31203192 for converter in converters :
3121- val = converter (val )
3193+ if isinstance (converter , Converter ):
3194+ val = converter (val , inst )
3195+ else :
3196+ val = converter (val )
31223197
31233198 return val
31243199
31253200 if not converters :
31263201 # If the converter list is empty, pipe_converter is the identity.
31273202 A = typing .TypeVar ("A" )
3128- pipe_converter .__annotations__ = {"val" : A , "return" : A }
3203+ pipe_converter .__annotations__ . update ( {"val" : A , "return" : A })
31293204 else :
31303205 # Get parameter type from first converter.
31313206 t = _AnnotationExtractor (converters [0 ]).get_first_param_type ()
@@ -3137,4 +3212,4 @@ def pipe_converter(val):
31373212 if rt :
31383213 pipe_converter .__annotations__ ["return" ] = rt
31393214
3140- return pipe_converter
3215+ return Converter ( pipe_converter , takes_self = True )
0 commit comments