| #!/usr/bin/env python |
| # |
| # Copyright 2010 Google Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| """Services descriptor definitions. |
| |
| Contains message definitions and functions for converting |
| service classes into transmittable message format. |
| |
| Describing an Enum instance, Enum class, Field class or Message class will |
| generate an appropriate descriptor object that describes that class. |
| This message can itself be used to transmit information to clients wishing |
| to know the description of an enum value, enum, field or message without |
| needing to download the source code. This format is also compatible with |
| other, non-Python languages. |
| |
| The descriptors are modeled to be binary compatible with: |
| |
| http://code.google.com/p/protobuf/source/browse/trunk/src/google/protobuf/descriptor.proto |
| |
| NOTE: The names of types and fields are not always the same between these |
| descriptors and the ones defined in descriptor.proto. This was done in order |
| to make source code files that use these descriptors easier to read. For |
| example, it is not necessary to prefix TYPE to all the values in |
| FieldDescriptor.Variant as is done in descriptor.proto FieldDescriptorProto.Type. |
| |
| Example: |
| |
| class Pixel(messages.Message): |
| |
| x = messages.IntegerField(1, required=True) |
| y = messages.IntegerField(2, required=True) |
| |
| color = messages.BytesField(3) |
| |
| # Describe Pixel class using message descriptor. |
| fields = [] |
| |
| field = FieldDescriptor() |
| field.name = 'x' |
| field.number = 1 |
| field.label = FieldDescriptor.Label.REQUIRED |
| field.variant = FieldDescriptor.Variant.INT64 |
| fields.append(field) |
| |
| field = FieldDescriptor() |
| field.name = 'y' |
| field.number = 2 |
| field.label = FieldDescriptor.Label.REQUIRED |
| field.variant = FieldDescriptor.Variant.INT64 |
| fields.append(field) |
| |
| field = FieldDescriptor() |
| field.name = 'color' |
| field.number = 3 |
| field.label = FieldDescriptor.Label.OPTIONAL |
| field.variant = FieldDescriptor.Variant.BYTES |
| fields.append(field) |
| |
| message = MessageDescriptor() |
| message.name = 'Pixel' |
| message.fields = fields |
| |
| # Describing is the equivalent of building the above message. |
| message == describe_message(Pixel) |
| |
| Public Classes: |
| EnumValueDescriptor: Describes Enum values. |
| EnumDescriptor: Describes Enum classes. |
| FieldDescriptor: Describes field instances. |
| FileDescriptor: Describes a single 'file' unit. |
| FileSet: Describes a collection of file descriptors. |
| MessageDescriptor: Describes Message classes. |
| MethodDescriptor: Describes a method of a service. |
| ServiceDescriptor: Describes a services. |
| |
| Public Functions: |
| describe_enum_value: Describe an individual enum-value. |
| describe_enum: Describe an Enum class. |
| describe_field: Describe a Field definition. |
| describe_file: Describe a 'file' unit from a Python module or object. |
| describe_file_set: Describe a file set from a list of modules or objects. |
| describe_message: Describe a Message definition. |
| describe_method: Describe a Method definition. |
| describe_service: Describe a Service definition. |
| """ |
| import six |
| |
| __author__ = 'rafek@google.com (Rafe Kaplan)' |
| |
| import codecs |
| import types |
| |
| from . import messages |
| from . import util |
| |
| |
| __all__ = ['EnumDescriptor', |
| 'EnumValueDescriptor', |
| 'FieldDescriptor', |
| 'MessageDescriptor', |
| 'MethodDescriptor', |
| 'FileDescriptor', |
| 'FileSet', |
| 'ServiceDescriptor', |
| 'DescriptorLibrary', |
| |
| 'describe_enum', |
| 'describe_enum_value', |
| 'describe_field', |
| 'describe_message', |
| 'describe_method', |
| 'describe_file', |
| 'describe_file_set', |
| 'describe_service', |
| 'describe', |
| 'import_descriptor_loader', |
| ] |
| |
| |
| # NOTE: MessageField is missing because message fields cannot have |
| # a default value at this time. |
| # TODO(rafek): Support default message values. |
| # |
| # Map to functions that convert default values of fields of a given type |
| # to a string. The function must return a value that is compatible with |
| # FieldDescriptor.default_value and therefore a unicode string. |
| _DEFAULT_TO_STRING_MAP = { |
| messages.IntegerField: six.text_type, |
| messages.FloatField: six.text_type, |
| messages.BooleanField: lambda value: value and u'true' or u'false', |
| messages.BytesField: lambda value: codecs.escape_encode(value)[0], |
| messages.StringField: lambda value: value, |
| messages.EnumField: lambda value: six.text_type(value.number), |
| } |
| |
| _DEFAULT_FROM_STRING_MAP = { |
| messages.IntegerField: int, |
| messages.FloatField: float, |
| messages.BooleanField: lambda value: value == u'true', |
| messages.BytesField: lambda value: codecs.escape_decode(value)[0], |
| messages.StringField: lambda value: value, |
| messages.EnumField: int, |
| } |
| |
| |
| class EnumValueDescriptor(messages.Message): |
| """Enum value descriptor. |
| |
| Fields: |
| name: Name of enumeration value. |
| number: Number of enumeration value. |
| """ |
| |
| # TODO(rafek): Why are these listed as optional in descriptor.proto. |
| # Harmonize? |
| name = messages.StringField(1, required=True) |
| number = messages.IntegerField(2, |
| required=True, |
| variant=messages.Variant.INT32) |
| |
| |
| class EnumDescriptor(messages.Message): |
| """Enum class descriptor. |
| |
| Fields: |
| name: Name of Enum without any qualification. |
| values: Values defined by Enum class. |
| """ |
| |
| name = messages.StringField(1) |
| values = messages.MessageField(EnumValueDescriptor, 2, repeated=True) |
| |
| |
| class FieldDescriptor(messages.Message): |
| """Field definition descriptor. |
| |
| Enums: |
| Variant: Wire format hint sub-types for field. |
| Label: Values for optional, required and repeated fields. |
| |
| Fields: |
| name: Name of field. |
| number: Number of field. |
| variant: Variant of field. |
| type_name: Type name for message and enum fields. |
| default_value: String representation of default value. |
| """ |
| |
| Variant = messages.Variant |
| |
| class Label(messages.Enum): |
| """Field label.""" |
| |
| OPTIONAL = 1 |
| REQUIRED = 2 |
| REPEATED = 3 |
| |
| name = messages.StringField(1, required=True) |
| number = messages.IntegerField(3, |
| required=True, |
| variant=messages.Variant.INT32) |
| label = messages.EnumField(Label, 4, default=Label.OPTIONAL) |
| variant = messages.EnumField(Variant, 5) |
| type_name = messages.StringField(6) |
| |
| # For numeric types, contains the original text representation of the value. |
| # For booleans, "true" or "false". |
| # For strings, contains the default text contents (not escaped in any way). |
| # For bytes, contains the C escaped value. All bytes < 128 are that are |
| # traditionally considered unprintable are also escaped. |
| default_value = messages.StringField(7) |
| |
| |
| class MessageDescriptor(messages.Message): |
| """Message definition descriptor. |
| |
| Fields: |
| name: Name of Message without any qualification. |
| fields: Fields defined for message. |
| message_types: Nested Message classes defined on message. |
| enum_types: Nested Enum classes defined on message. |
| """ |
| |
| name = messages.StringField(1) |
| fields = messages.MessageField(FieldDescriptor, 2, repeated=True) |
| |
| message_types = messages.MessageField( |
| 'protorpc.descriptor.MessageDescriptor', 3, repeated=True) |
| enum_types = messages.MessageField(EnumDescriptor, 4, repeated=True) |
| |
| |
| class MethodDescriptor(messages.Message): |
| """Service method definition descriptor. |
| |
| Fields: |
| name: Name of service method. |
| request_type: Fully qualified or relative name of request message type. |
| response_type: Fully qualified or relative name of response message type. |
| """ |
| |
| name = messages.StringField(1) |
| |
| request_type = messages.StringField(2) |
| response_type = messages.StringField(3) |
| |
| |
| class ServiceDescriptor(messages.Message): |
| """Service definition descriptor. |
| |
| Fields: |
| name: Name of Service without any qualification. |
| methods: Remote methods of Service. |
| """ |
| |
| name = messages.StringField(1) |
| |
| methods = messages.MessageField(MethodDescriptor, 2, repeated=True) |
| |
| |
| class FileDescriptor(messages.Message): |
| """Description of file containing protobuf definitions. |
| |
| Fields: |
| package: Fully qualified name of package that definitions belong to. |
| message_types: Message definitions contained in file. |
| enum_types: Enum definitions contained in file. |
| service_types: Service definitions contained in file. |
| """ |
| |
| package = messages.StringField(2) |
| |
| # TODO(rafek): Add dependency field |
| |
| message_types = messages.MessageField(MessageDescriptor, 4, repeated=True) |
| enum_types = messages.MessageField(EnumDescriptor, 5, repeated=True) |
| service_types = messages.MessageField(ServiceDescriptor, 6, repeated=True) |
| |
| |
| class FileSet(messages.Message): |
| """A collection of FileDescriptors. |
| |
| Fields: |
| files: Files in file-set. |
| """ |
| |
| files = messages.MessageField(FileDescriptor, 1, repeated=True) |
| |
| |
| def describe_enum_value(enum_value): |
| """Build descriptor for Enum instance. |
| |
| Args: |
| enum_value: Enum value to provide descriptor for. |
| |
| Returns: |
| Initialized EnumValueDescriptor instance describing the Enum instance. |
| """ |
| enum_value_descriptor = EnumValueDescriptor() |
| enum_value_descriptor.name = six.text_type(enum_value.name) |
| enum_value_descriptor.number = enum_value.number |
| return enum_value_descriptor |
| |
| |
| def describe_enum(enum_definition): |
| """Build descriptor for Enum class. |
| |
| Args: |
| enum_definition: Enum class to provide descriptor for. |
| |
| Returns: |
| Initialized EnumDescriptor instance describing the Enum class. |
| """ |
| enum_descriptor = EnumDescriptor() |
| enum_descriptor.name = enum_definition.definition_name().split('.')[-1] |
| |
| values = [] |
| for number in enum_definition.numbers(): |
| value = enum_definition.lookup_by_number(number) |
| values.append(describe_enum_value(value)) |
| |
| if values: |
| enum_descriptor.values = values |
| |
| return enum_descriptor |
| |
| |
| def describe_field(field_definition): |
| """Build descriptor for Field instance. |
| |
| Args: |
| field_definition: Field instance to provide descriptor for. |
| |
| Returns: |
| Initialized FieldDescriptor instance describing the Field instance. |
| """ |
| field_descriptor = FieldDescriptor() |
| field_descriptor.name = field_definition.name |
| field_descriptor.number = field_definition.number |
| field_descriptor.variant = field_definition.variant |
| |
| if isinstance(field_definition, messages.EnumField): |
| field_descriptor.type_name = field_definition.type.definition_name() |
| |
| if isinstance(field_definition, messages.MessageField): |
| field_descriptor.type_name = field_definition.message_type.definition_name() |
| |
| if field_definition.default is not None: |
| field_descriptor.default_value = _DEFAULT_TO_STRING_MAP[ |
| type(field_definition)](field_definition.default) |
| |
| # Set label. |
| if field_definition.repeated: |
| field_descriptor.label = FieldDescriptor.Label.REPEATED |
| elif field_definition.required: |
| field_descriptor.label = FieldDescriptor.Label.REQUIRED |
| else: |
| field_descriptor.label = FieldDescriptor.Label.OPTIONAL |
| |
| return field_descriptor |
| |
| |
| def describe_message(message_definition): |
| """Build descriptor for Message class. |
| |
| Args: |
| message_definition: Message class to provide descriptor for. |
| |
| Returns: |
| Initialized MessageDescriptor instance describing the Message class. |
| """ |
| message_descriptor = MessageDescriptor() |
| message_descriptor.name = message_definition.definition_name().split('.')[-1] |
| |
| fields = sorted(message_definition.all_fields(), |
| key=lambda v: v.number) |
| if fields: |
| message_descriptor.fields = [describe_field(field) for field in fields] |
| |
| try: |
| nested_messages = message_definition.__messages__ |
| except AttributeError: |
| pass |
| else: |
| message_descriptors = [] |
| for name in nested_messages: |
| value = getattr(message_definition, name) |
| message_descriptors.append(describe_message(value)) |
| |
| message_descriptor.message_types = message_descriptors |
| |
| try: |
| nested_enums = message_definition.__enums__ |
| except AttributeError: |
| pass |
| else: |
| enum_descriptors = [] |
| for name in nested_enums: |
| value = getattr(message_definition, name) |
| enum_descriptors.append(describe_enum(value)) |
| |
| message_descriptor.enum_types = enum_descriptors |
| |
| return message_descriptor |
| |
| |
| def describe_method(method): |
| """Build descriptor for service method. |
| |
| Args: |
| method: Remote service method to describe. |
| |
| Returns: |
| Initialized MethodDescriptor instance describing the service method. |
| """ |
| method_info = method.remote |
| descriptor = MethodDescriptor() |
| descriptor.name = method_info.method.__name__ |
| descriptor.request_type = method_info.request_type.definition_name() |
| descriptor.response_type = method_info.response_type.definition_name() |
| |
| return descriptor |
| |
| |
| def describe_service(service_class): |
| """Build descriptor for service. |
| |
| Args: |
| service_class: Service class to describe. |
| |
| Returns: |
| Initialized ServiceDescriptor instance describing the service. |
| """ |
| descriptor = ServiceDescriptor() |
| descriptor.name = service_class.__name__ |
| methods = [] |
| remote_methods = service_class.all_remote_methods() |
| for name in sorted(remote_methods.keys()): |
| if name == 'get_descriptor': |
| continue |
| |
| method = remote_methods[name] |
| methods.append(describe_method(method)) |
| if methods: |
| descriptor.methods = methods |
| |
| return descriptor |
| |
| |
| def describe_file(module): |
| """Build a file from a specified Python module. |
| |
| Args: |
| module: Python module to describe. |
| |
| Returns: |
| Initialized FileDescriptor instance describing the module. |
| """ |
| # May not import remote at top of file because remote depends on this |
| # file |
| # TODO(rafek): Straighten out this dependency. Possibly move these functions |
| # from descriptor to their own module. |
| from . import remote |
| |
| descriptor = FileDescriptor() |
| descriptor.package = util.get_package_for_module(module) |
| |
| if not descriptor.package: |
| descriptor.package = None |
| |
| message_descriptors = [] |
| enum_descriptors = [] |
| service_descriptors = [] |
| |
| # Need to iterate over all top level attributes of the module looking for |
| # message, enum and service definitions. Each definition must be itself |
| # described. |
| for name in sorted(dir(module)): |
| value = getattr(module, name) |
| |
| if isinstance(value, type): |
| if issubclass(value, messages.Message): |
| message_descriptors.append(describe_message(value)) |
| |
| elif issubclass(value, messages.Enum): |
| enum_descriptors.append(describe_enum(value)) |
| |
| elif issubclass(value, remote.Service): |
| service_descriptors.append(describe_service(value)) |
| |
| if message_descriptors: |
| descriptor.message_types = message_descriptors |
| |
| if enum_descriptors: |
| descriptor.enum_types = enum_descriptors |
| |
| if service_descriptors: |
| descriptor.service_types = service_descriptors |
| |
| return descriptor |
| |
| |
| def describe_file_set(modules): |
| """Build a file set from a specified Python modules. |
| |
| Args: |
| modules: Iterable of Python module to describe. |
| |
| Returns: |
| Initialized FileSet instance describing the modules. |
| """ |
| descriptor = FileSet() |
| file_descriptors = [] |
| for module in modules: |
| file_descriptors.append(describe_file(module)) |
| |
| if file_descriptors: |
| descriptor.files = file_descriptors |
| |
| return descriptor |
| |
| |
| def describe(value): |
| """Describe any value as a descriptor. |
| |
| Helper function for describing any object with an appropriate descriptor |
| object. |
| |
| Args: |
| value: Value to describe as a descriptor. |
| |
| Returns: |
| Descriptor message class if object is describable as a descriptor, else |
| None. |
| """ |
| from . import remote |
| if isinstance(value, types.ModuleType): |
| return describe_file(value) |
| elif callable(value) and hasattr(value, 'remote'): |
| return describe_method(value) |
| elif isinstance(value, messages.Field): |
| return describe_field(value) |
| elif isinstance(value, messages.Enum): |
| return describe_enum_value(value) |
| elif isinstance(value, type): |
| if issubclass(value, messages.Message): |
| return describe_message(value) |
| elif issubclass(value, messages.Enum): |
| return describe_enum(value) |
| elif issubclass(value, remote.Service): |
| return describe_service(value) |
| return None |
| |
| |
| @util.positional(1) |
| def import_descriptor_loader(definition_name, importer=__import__): |
| """Find objects by importing modules as needed. |
| |
| A definition loader is a function that resolves a definition name to a |
| descriptor. |
| |
| The import finder resolves definitions to their names by importing modules |
| when necessary. |
| |
| Args: |
| definition_name: Name of definition to find. |
| importer: Import function used for importing new modules. |
| |
| Returns: |
| Appropriate descriptor for any describable type located by name. |
| |
| Raises: |
| DefinitionNotFoundError when a name does not refer to either a definition |
| or a module. |
| """ |
| # Attempt to import descriptor as a module. |
| if definition_name.startswith('.'): |
| definition_name = definition_name[1:] |
| if not definition_name.startswith('.'): |
| leaf = definition_name.split('.')[-1] |
| if definition_name: |
| try: |
| module = importer(definition_name, '', '', [leaf]) |
| except ImportError: |
| pass |
| else: |
| return describe(module) |
| |
| try: |
| # Attempt to use messages.find_definition to find item. |
| return describe(messages.find_definition(definition_name, |
| importer=__import__)) |
| except messages.DefinitionNotFoundError as err: |
| # There are things that find_definition will not find, but if the parent |
| # is loaded, its children can be searched for a match. |
| split_name = definition_name.rsplit('.', 1) |
| if len(split_name) > 1: |
| parent, child = split_name |
| try: |
| parent_definition = import_descriptor_loader(parent, importer=importer) |
| except messages.DefinitionNotFoundError: |
| # Fall through to original error. |
| pass |
| else: |
| # Check the parent definition for a matching descriptor. |
| if isinstance(parent_definition, FileDescriptor): |
| search_list = parent_definition.service_types or [] |
| elif isinstance(parent_definition, ServiceDescriptor): |
| search_list = parent_definition.methods or [] |
| elif isinstance(parent_definition, EnumDescriptor): |
| search_list = parent_definition.values or [] |
| elif isinstance(parent_definition, MessageDescriptor): |
| search_list = parent_definition.fields or [] |
| else: |
| search_list = [] |
| |
| for definition in search_list: |
| if definition.name == child: |
| return definition |
| |
| # Still didn't find. Reraise original exception. |
| raise err |
| |
| |
| class DescriptorLibrary(object): |
| """A descriptor library is an object that contains known definitions. |
| |
| A descriptor library contains a cache of descriptor objects mapped by |
| definition name. It contains all types of descriptors except for |
| file sets. |
| |
| When a definition name is requested that the library does not know about |
| it can be provided with a descriptor loader which attempt to resolve the |
| missing descriptor. |
| """ |
| |
| @util.positional(1) |
| def __init__(self, |
| descriptors=None, |
| descriptor_loader=import_descriptor_loader): |
| """Constructor. |
| |
| Args: |
| descriptors: A dictionary or dictionary-like object that can be used |
| to store and cache descriptors by definition name. |
| definition_loader: A function used for resolving missing descriptors. |
| The function takes a definition name as its parameter and returns |
| an appropriate descriptor. It may raise DefinitionNotFoundError. |
| """ |
| self.__descriptor_loader = descriptor_loader |
| self.__descriptors = descriptors or {} |
| |
| def lookup_descriptor(self, definition_name): |
| """Lookup descriptor by name. |
| |
| Get descriptor from library by name. If descriptor is not found will |
| attempt to find via descriptor loader if provided. |
| |
| Args: |
| definition_name: Definition name to find. |
| |
| Returns: |
| Descriptor that describes definition name. |
| |
| Raises: |
| DefinitionNotFoundError if not descriptor exists for definition name. |
| """ |
| try: |
| return self.__descriptors[definition_name] |
| except KeyError: |
| pass |
| |
| if self.__descriptor_loader: |
| definition = self.__descriptor_loader(definition_name) |
| self.__descriptors[definition_name] = definition |
| return definition |
| else: |
| raise messages.DefinitionNotFoundError( |
| 'Could not find definition for %s' % definition_name) |
| |
| def lookup_package(self, definition_name): |
| """Determines the package name for any definition. |
| |
| Determine the package that any definition name belongs to. May check |
| parent for package name and will resolve missing descriptors if provided |
| descriptor loader. |
| |
| Args: |
| definition_name: Definition name to find package for. |
| """ |
| while True: |
| descriptor = self.lookup_descriptor(definition_name) |
| if isinstance(descriptor, FileDescriptor): |
| return descriptor.package |
| else: |
| index = definition_name.rfind('.') |
| if index < 0: |
| return None |
| definition_name = definition_name[:index] |