X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=juju%2Fclient%2Ffacade.py;h=5f37c27305d957a8689804304ec2dbb553d42462;hb=refs%2Ftags%2F0.4.0;hp=59163a978bd84189a99baec0a187887ba70ee2fe;hpb=173b900fcd95b2436af55df2618302146f4a2f40;p=osm%2FN2VC.git diff --git a/juju/client/facade.py b/juju/client/facade.py index 59163a9..5f37c27 100644 --- a/juju/client/facade.py +++ b/juju/client/facade.py @@ -1,10 +1,13 @@ import argparse import builtins +from collections import defaultdict import functools +from glob import glob import json import keyword from pathlib import Path import pprint +import re import textwrap from typing import Sequence, Mapping, TypeVar, Any, Union import typing @@ -13,6 +16,13 @@ from . import codegen _marker = object() +JUJU_VERSION = re.compile('[0-9]+\.[0-9-]+[\.\-][0-9a-z]+(\.[0-9]+)?') +VERSION_MAP = defaultdict(dict) +# Workaround for https://bugs.launchpad.net/juju/+bug/1683906 +NAUGHTY_CLASSES = ['ClientFacade', 'Client', 'FullStatus', 'ModelStatusInfo', + 'ModelInfo'] + + # Map basic types to Python's typing with a callable SCHEMA_TO_PYTHON = { 'string': str, @@ -24,6 +34,66 @@ SCHEMA_TO_PYTHON = { } +# Friendly warning message to stick at the top of generated files. +HEADER = """\ +# DO NOT CHANGE THIS FILE! This file is auto-generated by facade.py. +# Changes will be overwritten/lost when the file is regenerated. + +""" + + +# Classes and helper functions that we'll write to _client.py +LOOKUP_FACADE = ''' +def lookup_facade(name, version): + """ + Given a facade name and version, attempt to pull that facade out + of the correct client.py file. + + """ + try: + facade = getattr(CLIENTS[str(version)], name) + except KeyError: + raise ImportError("No facades found for version {}".format(version)) + except AttributeError: + raise ImportError( + "No facade with name '{}' in version {}".format(name, version)) + return facade + + +''' + +TYPE_FACTORY = ''' +class TypeFactory: + @classmethod + def from_connection(cls, connection): + """ + Given a connected Connection object, return an initialized and + connected instance of an API Interface matching the name of + this class. + + @param connection: initialized Connection object. + + """ + version = connection.facades[cls.__name__[:-6]] + + c = lookup_facade(cls.__name__, version) + c = c() + c.connect(connection) + + return c + + +''' + +CLIENT_TABLE = ''' +CLIENTS = {{ + {clients} +}} + + +''' + + class KindRegistry(dict): def register(self, name, version, obj): self[name] = {version: { @@ -60,7 +130,8 @@ class TypeRegistry(dict): _types = TypeRegistry() _registry = KindRegistry() -classes = {} +CLASSES = {} +factories = codegen.Capture() def booler(v): @@ -124,7 +195,7 @@ class Args(list): self.append((name, rtype)) def do_explode(self, kind): - if kind in basic_types: + if kind in basic_types or type(kind) is typing.TypeVar: return False if not issubclass(kind, (typing.Sequence, typing.Mapping)): @@ -183,14 +254,16 @@ class Args(list): def buildTypes(schema, capture): - global classes INDENT = " " for kind in sorted((k for k in _types if not isinstance(k, str)), key=lambda x: str(x)): name = _types[kind] - args = Args(kind) - if name in classes: + if name in capture and not name in NAUGHTY_CLASSES: continue + args = Args(kind) + # Write Factory class for _client.py + make_factory(name) + # Write actual class source = [""" class {}(Type): _toSchema = {} @@ -200,8 +273,9 @@ class {}(Type): {} '''""".format( name, - args.PyToSchemaMapping(), - args.SchemaToPyMapping(), + # pprint these to get stable ordering across regens + pprint.pformat(args.PyToSchemaMapping(), width=999), + pprint.pformat(args.SchemaToPyMapping(), width=999), ", " if args else "", args.as_kwargs(), textwrap.indent(args.get_doc(), INDENT * 2)) @@ -218,14 +292,22 @@ class {}(Type): if arg_type in basic_types: source.append("{}self.{} = {}".format(INDENT * 2, arg_name, arg_name)) elif issubclass(arg_type, typing.Sequence): - value_type = arg_type_name.__parameters__[0] + value_type = ( + arg_type_name.__parameters__[0] + if len(arg_type_name.__parameters__) + else None + ) if type(value_type) is typing.TypeVar: source.append("{}self.{} = [{}.from_json(o) for o in {} or []]".format( INDENT * 2, arg_name, strcast(value_type), arg_name)) else: source.append("{}self.{} = {}".format(INDENT * 2, arg_name, arg_name)) elif issubclass(arg_type, typing.Mapping): - value_type = arg_type_name.__parameters__[1] + value_type = ( + arg_type_name.__parameters__[1] + if len(arg_type_name.__parameters__) > 1 + else None + ) if type(value_type) is typing.TypeVar: source.append("{}self.{} = {{k: {}.from_json(v) for k, v in ({} or dict()).items()}}".format( INDENT * 2, arg_name, strcast(value_type), arg_name)) @@ -238,13 +320,14 @@ class {}(Type): source.append("{}self.{} = {}".format(INDENT * 2, arg_name, arg_name)) source = "\n".join(source) - capture.write(source) - capture.write("\n\n") + capture.clear(name) + capture[name].write(source) + capture[name].write("\n\n") co = compile(source, __name__, "exec") ns = _getns() exec(co, ns) cls = ns[name] - classes[name] = cls + CLASSES[name] = cls def retspec(defs): @@ -255,6 +338,8 @@ def retspec(defs): # Error or the expected Type if not defs: return None + if defs in basic_types: + return strcast(defs, False) rtypes = _registry.getObj(_types[defs]) if not rtypes: return None @@ -309,12 +394,19 @@ def ReturnMapping(cls): if cls is None: return reply if 'error' in reply: - cls = classes['Error'] + cls = CLASSES['Error'] if issubclass(cls, typing.Sequence): result = [] item_cls = cls.__parameters__[0] for item in reply: result.append(item_cls.from_json(item)) + """ + if 'error' in item: + cls = CLASSES['Error'] + else: + cls = item_cls + result.append(cls.from_json(item)) + """ else: result = cls.from_json(reply['response']) @@ -329,9 +421,9 @@ def makeFunc(cls, name, params, result, async=True): assignments = [] toschema = args.PyToSchemaMapping() for arg in args._get_arg_str(False, False): - assignments.append("{}params[\'{}\'] = {}".format(INDENT, - toschema[arg], - arg)) + assignments.append("{}_params[\'{}\'] = {}".format(INDENT, + toschema[arg], + arg)) assignments = "\n".join(assignments) res = retspec(result) source = """ @@ -343,8 +435,8 @@ def makeFunc(cls, name, params, result, async=True): Returns -> {res} ''' # map input types to rpc msg - params = dict() - msg = dict(Type='{cls.name}', Request='{name}', Version={cls.version}, Params=params) + _params = dict() + msg = dict(type='{cls.name}', request='{name}', version={cls.version}, params=_params) {assignments} reply = {await}self.rpc(msg) return reply @@ -372,7 +464,7 @@ def buildMethods(cls, capture): for methodname in sorted(properties): method, source = _buildMethod(cls, methodname) setattr(cls, methodname, method) - capture.write(source, depth=1) + capture["{}Facade".format(cls.__name__)].write(source, depth=1) def _buildMethod(cls, name): @@ -386,7 +478,10 @@ def _buildMethod(cls, name): params = _types.get(spec['$ref']) spec = prop.get('Result') if spec: - result = _types.get(spec['$ref']) + if '$ref' in spec: + result = _types.get(spec['$ref']) + else: + result = SCHEMA_TO_PYTHON[spec['type']] return makeFunc(cls, name, params, result) @@ -422,6 +517,8 @@ class Type: @classmethod def from_json(cls, data): + if isinstance(data, cls): + return data if isinstance(data, str): data = json.loads(data) d = {} @@ -431,7 +528,6 @@ class Type: try: return cls(**d) except TypeError: - print(cls) raise def serialize(self): @@ -478,7 +574,7 @@ class Schema(dict): if not defs: return for d, data in defs.items(): - if d in _registry: + if d in _registry and not d in NAUGHTY_CLASSES: continue node = self.deref(data, d) kind = node.get("type") @@ -560,49 +656,148 @@ def _getns(): return ns - -def generate_facacdes(options): - global classes - schemas = json.loads(Path(options.schema).read_text("utf-8")) - capture = codegen.CodeWriter() - capture.write(textwrap.dedent("""\ - # DO NOT CHANGE THIS FILE! This file is auto-generated by facade.py. - # Changes will be overwritten/lost when the file is regenerated. - - from juju.client.facade import Type, ReturnMapping - - """)) - schemas = [Schema(s) for s in schemas] - - for schema in schemas: - schema.buildDefinitions() - buildTypes(schema, capture) - - for schema in schemas: - # TODO generate class now with a metaclass that takes the schema - # the generated class has the right name and it in turn uses - # the metaclass to populate cls - cls, source = buildFacade(schema) - capture.write(source) - buildMethods(cls, capture) - classes[schema.name] = cls - - return capture +def make_factory(name): + if name in factories: + del factories[name] + factories[name].write("class {}(TypeFactory):\n pass\n\n".format(name)) + + +def write_facades(captures, options): + """ + Write the Facades to the appropriate _client.py + + """ + for version in sorted(captures.keys()): + filename = "{}/_client{}.py".format(options.output_dir, version) + with open(filename, "w") as f: + f.write(HEADER) + f.write("from juju.client.facade import Type, ReturnMapping\n") + f.write("from juju.client._definitions import *\n\n") + for key in sorted( + [k for k in captures[version].keys() if "Facade" in k]): + print(captures[version][key], file=f) + + # Return the last (most recent) version for use in other routines. + return version + + +def write_definitions(captures, options, version): + """ + Write auxillary (non versioned) classes to + _definitions.py The auxillary classes currently get + written redudantly into each capture object, so we can look in + one of them -- we just use the last one from the loop above. + + """ + with open("{}/_definitions.py".format(options.output_dir), "w") as f: + f.write(HEADER) + f.write("from juju.client.facade import Type, ReturnMapping\n\n") + for key in sorted( + [k for k in captures[version].keys() if "Facade" not in k]): + print(captures[version][key], file=f) + + +def write_client(captures, options): + """ + Write the TypeFactory classes to _client.py, along with some + imports and tables so that we can look up versioned Facades. + + """ + with open("{}/_client.py".format(options.output_dir), "w") as f: + f.write(HEADER) + f.write("from juju.client._definitions import *\n\n") + clients = ", ".join("_client{}".format(v) for v in captures) + f.write("from juju.client import " + clients + "\n\n") + f.write(CLIENT_TABLE.format(clients=",\n ".join( + ['"{}": _client{}'.format(v, v) for v in captures]))) + f.write(LOOKUP_FACADE) + f.write(TYPE_FACTORY) + for key in sorted([k for k in factories.keys() if "Facade" in k]): + print(factories[key], file=f) + + +def write_version_map(options): + """ + In order to work around + https://bugs.launchpad.net/juju/+bug/1682925, we build a map of + the facades that each version supports, and write it to disk here. + + """ + with open("{}/version_map.py".format(options.output_dir), "w") as f: + f.write(HEADER) + f.write("VERSION_MAP = {\n") + for juju_version in sorted(VERSION_MAP.keys()): + f.write(' "{}": {{\n'.format(juju_version)) + for key in VERSION_MAP[juju_version]: + f.write(' "{}": {},\n'.format( + key, VERSION_MAP[juju_version][key])) + f.write(' },\n') + f.write("}\n") + + +def generate_facades(options): + captures = defaultdict(codegen.Capture) + schemas = {} + for p in sorted(glob(options.schema)): + if 'latest' in p: + juju_version = 'latest' + else: + try: + juju_version = re.search(JUJU_VERSION, p).group() + except AttributeError: + print("Cannot extract a juju version from {}".format(p)) + print("Schemas must include a juju version in the filename") + raise SystemExit(1) + + new_schemas = json.loads(Path(p).read_text("utf-8")) + schemas[juju_version] = [Schema(s) for s in new_schemas] + + # Build all of the auxillary (unversioned) classes + # TODO: get rid of some of the excess trips through loops in the + # called functions. + for juju_version in sorted(schemas.keys()): + for schema in schemas[juju_version]: + schema.buildDefinitions() + buildTypes(schema, captures[schema.version]) + VERSION_MAP[juju_version][schema.name] = schema.version + + # Build the Facade classes + for juju_version in sorted(schemas.keys()): + for schema in schemas[juju_version]: + cls, source = buildFacade(schema) + cls_name = "{}Facade".format(schema.name) + + captures[schema.version].clear(cls_name) + # Make the factory class for _client.py + make_factory(cls_name) + # Make the actual class + captures[schema.version][cls_name].write(source) + # Build the methods for each Facade class. + buildMethods(cls, captures[schema.version]) + # Mark this Facade class as being done for this version -- + # helps mitigate some excessive looping. + CLASSES[schema.name] = cls + + return captures def setup(): parser = argparse.ArgumentParser() - parser.add_argument("-s", "--schema", default="schemas.json") - parser.add_argument("-o", "--output", default="client.py") + parser.add_argument("-s", "--schema", default="juju/client/schemas*") + parser.add_argument("-o", "--output_dir", default="juju/client") options = parser.parse_args() return options def main(): options = setup() - capture = generate_facacdes(options) - with open(options.output, "w") as fp: - print(capture, file=fp) + # Generate some text blobs + captures = generate_facades(options) + # ... and write them out + last_version = write_facades(captures, options) + write_definitions(captures, options, last_version) + write_client(captures, options) + write_version_map(options) if __name__ == '__main__': main()