6 from pathlib
import Path
9 from typing
import Sequence
, Mapping
, TypeVar
, Any
, Union
16 # Map basic types to Python's typing with a callable
27 class KindRegistry(dict):
28 def register(self
, name
, version
, obj
):
29 self
[name
] = {version
: {
33 def lookup(self
, name
, version
=None):
34 """If version is omitted, max version is used"""
35 versions
= self
.get(name
)
39 return versions
[version
]
40 return versions
[max(versions
)]
42 def getObj(self
, name
, version
=None):
43 result
= self
.lookup(name
, version
)
45 obj
= result
["object"]
50 class TypeRegistry(dict):
53 refname
= Schema
.referenceName(name
)
54 if refname
not in self
:
55 result
= TypeVar(refname
)
56 self
[refname
] = result
57 self
[result
] = refname
61 _types
= TypeRegistry()
62 _registry
= KindRegistry()
67 if isinstance(v
, str):
74 return _types
.get(ref
)
78 return getRefType(obj
["$ref"])
82 kind
= obj
.get('type')
84 raise ValueError("%s has no type" % obj
)
85 result
= SCHEMA_TO_PYTHON
.get(kind
)
87 raise ValueError("%s has type %s" % (obj
, kind
))
91 basic_types
= [str, bool, int, float]
95 result
= name
.replace("-", "_")
96 result
= result
.lower()
97 if keyword
.iskeyword(result
) or result
in dir(builtins
):
102 def strcast(kind
, keep_builtins
=False):
103 if issubclass(kind
, typing
.GenericMeta
):
105 if str(kind
).startswith('~'):
107 if (kind
in basic_types
or
108 type(kind
) in basic_types
) and keep_builtins
is False:
114 def __init__(self
, defs
):
117 rtypes
= _registry
.getObj(_types
[defs
])
119 if not self
.do_explode(rtypes
[0][1]):
120 for name
, rtype
in rtypes
:
121 self
.append((name
, rtype
))
123 for name
, rtype
in rtypes
:
124 self
.append((name
, rtype
))
126 def do_explode(self
, kind
):
127 if kind
in basic_types
or type(kind
) is typing
.TypeVar
:
129 if not issubclass(kind
, (typing
.Sequence
,
132 self
.extend(Args(kind
))
136 def PyToSchemaMapping(self
):
142 def SchemaToPyMapping(self
):
148 def _format(self
, name
, rtype
, typed
=True):
150 return "{} : {}".format(
155 return name_to_py(name
)
157 def _get_arg_str(self
, typed
=False, joined
=", "):
161 parts
.append(self
._format
(item
[0], item
[1], typed
))
163 return joined
.join(parts
)
171 parts
.append('{}=None'.format(name_to_py(item
[0])))
172 return ', '.join(parts
)
176 return self
._get
_arg
_str
(True)
179 return self
._get
_arg
_str
(False)
182 return self
._get
_arg
_str
(True, "\n")
185 def buildTypes(schema
, capture
):
188 for kind
in sorted((k
for k
in _types
if not isinstance(k
, str)),
189 key
=lambda x
: str(x
)):
198 def __init__(self{}{}):
203 # pprint these to get stable ordering across regens
204 pprint
.pformat(args
.PyToSchemaMapping(), width
=999),
205 pprint
.pformat(args
.SchemaToPyMapping(), width
=999),
206 ", " if args
else "",
208 textwrap
.indent(args
.get_doc(), INDENT
* 2))
210 assignments
= args
._get
_arg
_str
(False, False)
213 source
.append("{}pass".format(INDENT
* 2))
216 arg_name
= name_to_py(arg
[0])
218 arg_type_name
= strcast(arg_type
)
219 if arg_type
in basic_types
:
220 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
221 elif issubclass(arg_type
, typing
.Sequence
):
223 arg_type_name
.__parameters
__[0]
224 if len(arg_type_name
.__parameters
__)
227 if type(value_type
) is typing
.TypeVar
:
228 source
.append("{}self.{} = [{}.from_json(o) for o in {} or []]".format(
229 INDENT
* 2, arg_name
, strcast(value_type
), arg_name
))
231 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
232 elif issubclass(arg_type
, typing
.Mapping
):
234 arg_type_name
.__parameters
__[1]
235 if len(arg_type_name
.__parameters
__) > 1
238 if type(value_type
) is typing
.TypeVar
:
239 source
.append("{}self.{} = {{k: {}.from_json(v) for k, v in ({} or dict()).items()}}".format(
240 INDENT
* 2, arg_name
, strcast(value_type
), arg_name
))
242 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
243 elif type(arg_type
) is typing
.TypeVar
:
244 source
.append("{}self.{} = {}.from_json({}) if {} else None".format(
245 INDENT
* 2, arg_name
, arg_type_name
, arg_name
, arg_name
))
247 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
249 source
= "\n".join(source
)
250 capture
.write(source
)
251 capture
.write("\n\n")
252 co
= compile(source
, __name__
, "exec")
261 # only return 1, so if there is more than one type
262 # we need to include a union
263 # In truth there is only 1 return
264 # Error or the expected Type
267 if defs
in basic_types
:
268 return strcast(defs
, False)
269 rtypes
= _registry
.getObj(_types
[defs
])
273 return Union
[tuple([strcast(r
[1], True) for r
in rtypes
])]
274 return strcast(rtypes
[0][1], False)
277 def return_type(defs
):
280 rtypes
= _registry
.getObj(_types
[defs
])
291 def type_anno_func(func
, defs
, is_result
=False):
295 rtypes
= _registry
.getObj(_types
[defs
])
300 elif len(rtypes
) > 1:
301 annos
[kn
] = Union
[tuple([r
[1] for r
in rtypes
])]
303 annos
[kn
] = rtypes
[0][1]
305 for name
, rtype
in rtypes
:
306 name
= name_to_py(name
)
308 func
.__annotations
__.update(annos
)
312 def ReturnMapping(cls
):
313 # Annotate the method with a return Type
314 # so the value can be cast
317 async def wrapper(*args
, **kwargs
):
319 reply
= await f(*args
, **kwargs
)
323 cls
= classes
['Error']
324 if issubclass(cls
, typing
.Sequence
):
326 item_cls
= cls
.__parameters
__[0]
328 result
.append(item_cls
.from_json(item
))
331 cls = classes['Error']
334 result.append(cls.from_json(item))
337 result
= cls
.from_json(reply
['response'])
344 def makeFunc(cls
, name
, params
, result
, async=True):
348 toschema
= args
.PyToSchemaMapping()
349 for arg
in args
._get
_arg
_str
(False, False):
350 assignments
.append("{}_params[\'{}\'] = {}".format(INDENT
,
353 assignments
= "\n".join(assignments
)
354 res
= retspec(result
)
357 @ReturnMapping({rettype})
358 {async}def {name}(self{argsep}{args}):
363 # map input types to rpc msg
365 msg = dict(type='{cls.name}', request='{name}', version={cls.version}, params=_params)
367 reply = {await}self.rpc(msg)
372 fsource
= source
.format(async="async " if async else "",
374 argsep
=", " if args
else "",
377 rettype
=result
.__name
__ if result
else None,
378 docstring
=textwrap
.indent(args
.get_doc(), INDENT
),
380 assignments
=assignments
,
381 await="await " if async else "")
388 def buildMethods(cls
, capture
):
389 properties
= cls
.schema
['properties']
390 for methodname
in sorted(properties
):
391 method
, source
= _buildMethod(cls
, methodname
)
392 setattr(cls
, methodname
, method
)
393 capture
.write(source
, depth
=1)
396 def _buildMethod(cls
, name
):
399 method
= cls
.schema
['properties'][name
]
400 if 'properties' in method
:
401 prop
= method
['properties']
402 spec
= prop
.get('Params')
404 params
= _types
.get(spec
['$ref'])
405 spec
= prop
.get('Result')
408 result
= _types
.get(spec
['$ref'])
410 result
= SCHEMA_TO_PYTHON
[spec
['type']]
411 return makeFunc(cls
, name
, params
, result
)
414 def buildFacade(schema
):
415 cls
= type(schema
.name
, (Type
,), dict(name
=schema
.name
,
416 version
=schema
.version
,
419 class {name}Facade(Type):
423 """.format(name
=schema
.name
,
424 version
=schema
.version
,
425 schema
=textwrap
.indent(pprint
.pformat(schema
), " "))
429 class TypeEncoder(json
.JSONEncoder
):
430 def default(self
, obj
):
431 if isinstance(obj
, Type
):
432 return obj
.serialize()
433 return json
.JSONEncoder
.default(self
, obj
)
437 def connect(self
, connection
):
438 self
.connection
= connection
440 async def rpc(self
, msg
):
441 result
= await self
.connection
.rpc(msg
, encoder
=TypeEncoder
)
445 def from_json(cls
, data
):
446 if isinstance(data
, cls
):
448 if isinstance(data
, str):
449 data
= json
.loads(data
)
451 for k
, v
in (data
or {}).items():
452 d
[cls
._toPy
.get(k
, k
)] = v
461 for attr
, tgt
in self
._toSchema
.items():
462 d
[tgt
] = getattr(self
, attr
)
466 return json
.dumps(self
.serialize())
470 def __init__(self
, schema
):
471 self
.name
= schema
['Name']
472 self
.version
= schema
['Version']
473 self
.update(schema
['Schema'])
476 def referenceName(cls
, ref
):
477 if ref
.startswith("#/definitions/"):
478 ref
= ref
.rsplit("/", 1)[-1]
481 def resolveDefinition(self
, ref
):
482 return self
['definitions'][self
.referenceName(ref
)]
484 def deref(self
, prop
, name
):
485 if not isinstance(prop
, dict):
486 raise TypeError(prop
)
487 if "$ref" not in prop
:
490 target
= self
.resolveDefinition(prop
["$ref"])
493 def buildDefinitions(self
):
494 # here we are building the types out
495 # anything in definitions is a type
496 # but these may contain references themselves
497 # so we dfs to the bottom and build upwards
498 # when a types is already in the registry
499 defs
= self
.get('definitions')
502 for d
, data
in defs
.items():
505 node
= self
.deref(data
, d
)
506 kind
= node
.get("type")
508 result
= self
.buildObject(node
, d
)
509 elif kind
== "array":
511 _registry
.register(d
, self
.version
, result
)
512 # XXX: This makes sure that the type gets added to the global
513 # _types dict even if no other type in the schema has a ref
517 def buildObject(self
, node
, name
=None, d
=0):
518 # we don't need to build types recursively here
519 # they are all in definitions already
520 # we only want to include the type reference
521 # which we can derive from the name
524 props
= node
.get("properties")
525 pprops
= node
.get("patternProperties")
527 # Sort these so the __init__ arg list for each Type remains
528 # consistently ordered across regens of client.py
529 for p
in sorted(props
):
532 add((p
, refType(prop
)))
536 add((p
, self
.buildArray(prop
, d
+ 1)))
537 elif kind
== "object":
538 struct
.extend(self
.buildObject(prop
, p
, d
+ 1))
540 add((p
, objType(prop
)))
542 if ".*" not in pprops
:
544 "Cannot handle actual pattern in patternProperties %s" %
548 add((name
, Mapping
[str, refType(pprop
)]))
550 ppkind
= pprop
["type"]
551 if ppkind
== "array":
552 add((name
, self
.buildArray(pprop
, d
+ 1)))
554 add((name
, Mapping
[str, SCHEMA_TO_PYTHON
[ppkind
]]))
556 if not struct
and node
.get('additionalProperties', False):
557 add((name
, Mapping
[str, SCHEMA_TO_PYTHON
['object']]))
561 def buildArray(self
, obj
, d
=0):
562 # return a sequence from an array in the schema
564 return Sequence
[refType(obj
)]
566 kind
= obj
.get("type")
567 if kind
and kind
== "array":
569 return self
.buildArray(items
, d
+ 1)
571 return Sequence
[objType(obj
)]
577 'ReturnMapping': ReturnMapping
579 # Copy our types into the globals of the method
580 for facade
in _registry
:
581 ns
[facade
] = _registry
.getObj(facade
)
586 def generate_facacdes(options
):
588 schemas
= json
.loads(Path(options
.schema
).read_text("utf-8"))
589 capture
= codegen
.CodeWriter()
590 capture
.write(textwrap
.dedent("""\
591 # DO NOT CHANGE THIS FILE! This file is auto-generated by facade.py.
592 # Changes will be overwritten/lost when the file is regenerated.
594 from juju.client.facade import Type, ReturnMapping
597 schemas
= [Schema(s
) for s
in schemas
]
599 for schema
in schemas
:
600 schema
.buildDefinitions()
601 buildTypes(schema
, capture
)
603 for schema
in schemas
:
604 # TODO generate class now with a metaclass that takes the schema
605 # the generated class has the right name and it in turn uses
606 # the metaclass to populate cls
607 cls
, source
= buildFacade(schema
)
608 capture
.write(source
)
609 buildMethods(cls
, capture
)
610 classes
[schema
.name
] = cls
615 parser
= argparse
.ArgumentParser()
616 parser
.add_argument("-s", "--schema", default
="schemas.json")
617 parser
.add_argument("-o", "--output", default
="client.py")
618 options
= parser
.parse_args()
623 capture
= generate_facacdes(options
)
624 with
open(options
.output
, "w") as fp
:
625 print(capture
, file=fp
)
628 if __name__
== '__main__':