22import json
33import os
44import re
5+ from collections .abc import Callable
56from datetime import datetime , timezone
6- from inspect import getcallargs , getfullargspec
7+ from enum import Enum
8+ from inspect import getcallargs , getfullargspec , signature
79from io import StringIO
810from os import path
911from pathlib import Path
12+ from types import UnionType
13+ from typing import Any , Dict , List , Set , Tuple , Union , get_args , get_type_hints , get_origin # noqa: UP035
1014from unittest .mock import patch
1115
1216from pyinfra .api import Config , Inventory
@@ -59,7 +63,31 @@ def get_temp_filename(*args):
5963 return "_tempfile_"
6064
6165
62- def parse_value (value ):
66+ AGGREGATES = {Dict , List , Set , Tuple , Union , UnionType , dict , list , set , tuple } # noqa: UP006
67+
68+
69+ def get_enum_map (op : Callable [..., Any ]) -> dict [str , type [Enum ]]:
70+ """
71+ Returns a map from type name to type for all enum types used in `ops` parameters.
72+ """
73+ result : dict [str , type [Enum ]] = {}
74+ type_hints = get_type_hints (op )
75+ for param in signature (op ).parameters :
76+ if param not in type_hints :
77+ continue
78+ to_do = [type_hints [param ]]
79+ while len (to_do ) > 0 :
80+ the_type = to_do .pop (0 )
81+ origin = get_origin (the_type )
82+ if (origin is not None ) and (origin in AGGREGATES ):
83+ to_do .extend (get_args (the_type ))
84+ elif isinstance (the_type , type ) and issubclass (the_type , Enum ):
85+ result [the_type .__name__ ] = the_type
86+
87+ return result
88+
89+
90+ def parse_value (value , enum_map : dict [str , type [Enum ]] | None = None ):
6391 """
6492 Convert JSON types to more complex Python types because JSON is lacking.
6593 """
@@ -69,17 +97,29 @@ def parse_value(value):
6997 return datetime .fromisoformat (value [9 :])
7098 if value .startswith ("path:" ):
7199 return Path (value [5 :])
100+ if value .startswith ("enum:" ):
101+ if len (pieces := value .split (":" )) != 3 : # enum:<enum_type_name>:<value>
102+ raise ValueError (f"invalid enum specifier: { value } " )
103+ try :
104+ result = (enum_map or {})[pieces [1 ]](pieces [2 ])
105+ except KeyError :
106+ raise ValueError (f"enum '{ pieces [1 ]} ' not defined for '{ value } '" ) from None
107+ except ValueError :
108+ raise ValueError (f"value '{ pieces [2 ]} ' not valid in '{ value } '" ) from None
109+ else :
110+ return result
111+
72112 if value .startswith ("io:" ):
73113 return StringIO (value [3 :])
74114 return value
75115
76116 if isinstance (value , list ):
77117 if value and value [0 ] == "set:" :
78- return set (parse_value (value ) for value in value [1 :])
79- return [parse_value (value ) for value in value ]
118+ return set (parse_value (value , enum_map ) for value in value [1 :])
119+ return [parse_value (value , enum_map ) for value in value ]
80120
81121 if isinstance (value , dict ):
82- return {key : parse_value (value ) for key , value in value .items ()}
122+ return {key : parse_value (value , enum_map ) for key , value in value .items ()}
83123
84124 return value
85125
0 commit comments