| #!/usr/bin/env python |
| # |
| # Copyright 2011 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. |
| # |
| |
| """Command line tool for generating ProtoRPC definitions from descriptors.""" |
| |
| import errno |
| import logging |
| import optparse |
| import os |
| import sys |
| |
| from protorpc import descriptor |
| from protorpc import generate_python |
| from protorpc import protobuf |
| from protorpc import registry |
| from protorpc import transport |
| from protorpc import util |
| |
| EXCLUDED_PACKAGES = frozenset(['protorpc.registry', |
| 'protorpc.messages', |
| 'protorpc.descriptor', |
| 'protorpc.message_types', |
| ]) |
| |
| commands = {} |
| |
| |
| def usage(): |
| """Print usage help and exit with an error code.""" |
| parser.print_help() |
| sys.exit(2) |
| |
| |
| def fatal_error(message): |
| """Print fatal error messages exit with an error code. |
| |
| Args: |
| message: Message to print to stderr before exit. |
| """ |
| sys.stderr.write(message) |
| sys.exit(1) |
| |
| |
| def open_input_file(filename): |
| """Open file for reading. |
| |
| Args: |
| filename: Name of input file to open or None to open stdin. |
| |
| Returns: |
| Opened file if string provided, stdin if filename is None. |
| """ |
| # TODO(rafek): Detect missing or invalid files, generating user friendly |
| # error messages. |
| if filename is None: |
| return sys.stdin |
| else: |
| try: |
| return open(filename, 'rb') |
| except IOError, err: |
| fatal_error(str(err)) |
| |
| |
| @util.positional(1) |
| def generate_file_descriptor(dest_dir, file_descriptor, force_overwrite): |
| """Generate a single file descriptor to destination directory. |
| |
| Will generate a single Python file from a file descriptor under dest_dir. |
| The sub-directory where the file is generated is determined by the package |
| name of descriptor. |
| |
| Descriptors without package names will not be generated. |
| |
| Descriptors that are part of the ProtoRPC distribution will not be generated. |
| |
| Args: |
| dest_dir: Directory under which to generate files. |
| file_descriptor: FileDescriptor instance to generate source code from. |
| force_overwrite: If True, existing files will be overwritten. |
| """ |
| package = file_descriptor.package |
| if not package: |
| # TODO(rafek): Option to cause an error on this condition. |
| logging.warn('Will not generate descriptor without package name') |
| return |
| |
| if package in EXCLUDED_PACKAGES: |
| logging.warn('Will not generate main ProtoRPC class %s' % package) |
| return |
| |
| package_path = package.split('.') |
| directory = package_path[:-1] |
| package_file_name = package_path[-1] |
| directory_name = os.path.join(dest_dir, *directory) |
| output_file_name = os.path.join(directory_name, |
| '%s.py' % (package_file_name,)) |
| |
| try: |
| os.makedirs(directory_name) |
| except OSError, err: |
| if err.errno != errno.EEXIST: |
| raise |
| |
| if not force_overwrite and os.path.exists(output_file_name): |
| logging.warn('Not overwriting %s with package %s', |
| output_file_name, package) |
| return |
| |
| output_file = open(output_file_name, 'w') |
| |
| logging.info('Writing package %s to %s', |
| file_descriptor.package, output_file_name) |
| generate_python.format_python_file(file_descriptor, output_file) |
| |
| |
| @util.positional(1) |
| def command(name, required=(), optional=()): |
| """Decorator used for declaring commands used on command line. |
| |
| Each command of this tool can have any number of sequential required |
| parameters and optional parameters. The required and optional parameters |
| will be displayed in the command usage. Arguments passed in to the command |
| are checked to ensure they have at least the required parameters and not |
| too many parameters beyond the optional ones. When there are not enough |
| or too few parameters the usage message is generated and the program exits |
| with an error code. |
| |
| Functions decorated thus are added to commands by their name. |
| |
| Resulting decorated functions will have required and optional attributes |
| assigned to them so that appear in the usage message. |
| |
| Args: |
| name: Name of command that will follow the program name on the command line. |
| required: List of required parameter names as displayed in the usage |
| message. |
| optional: List of optional parameter names as displayed in the usage |
| message. |
| """ |
| def check_params_decorator(function): |
| def check_params_wrapper(options, *args): |
| if not (len(required) <= len(args) <= len(required) + len(optional)): |
| sys.stderr.write("Incorrect usage for command '%s'\n\n" % name) |
| usage() |
| function(options, *args) |
| check_params_wrapper.required = required |
| check_params_wrapper.optional = optional |
| commands[name] = check_params_wrapper |
| return check_params_wrapper |
| return check_params_decorator |
| |
| |
| @command('file', optional=['input-filename', 'output-filename']) |
| def file_command(options, input_filename=None, output_filename=None): |
| """Generate a single descriptor file to Python. |
| |
| Args: |
| options: Parsed command line options. |
| input_filename: File to read protobuf FileDescriptor from. If None |
| will read from stdin. |
| output_filename: File to write Python source code to. If None will |
| generate to stdout. |
| """ |
| with open_input_file(input_filename) as input_file: |
| descriptor_content = input_file.read() |
| |
| if output_filename: |
| output_file = open(output_filename, 'w') |
| else: |
| output_file = sys.stdout |
| |
| file_descriptor = protobuf.decode_message(descriptor.FileDescriptor, |
| descriptor_content) |
| generate_python.format_python_file(file_descriptor, output_file) |
| |
| |
| @command('fileset', optional=['filename']) |
| def fileset_command(options, input_filename=None): |
| """Generate source directory structure from FileSet. |
| |
| Args: |
| options: Parsed command line options. |
| input_filename: File to read protobuf FileSet from. If None will read from |
| stdin. |
| """ |
| with open_input_file(input_filename) as input_file: |
| descriptor_content = input_file.read() |
| |
| dest_dir = os.path.expanduser(options.dest_dir) |
| |
| if not os.path.isdir(dest_dir) and os.path.exists(dest_dir): |
| fatal_error("Destination '%s' is not a directory" % dest_dir) |
| |
| file_set = protobuf.decode_message(descriptor.FileSet, |
| descriptor_content) |
| |
| for file_descriptor in file_set.files: |
| generate_file_descriptor(dest_dir, file_descriptor=file_descriptor, |
| force_overwrite=options.force) |
| |
| |
| @command('registry', |
| required=['host'], |
| optional=['service-name', 'registry-path']) |
| def registry_command(options, |
| host, |
| service_name=None, |
| registry_path='/protorpc'): |
| """Generate source directory structure from remote registry service. |
| |
| Args: |
| options: Parsed command line options. |
| host: Web service host where registry service is located. May include |
| port. |
| service_name: Name of specific service to read. Will generate only Python |
| files that service is dependent on. If None, will generate source code |
| for all services known by the registry. |
| registry_path: Path to find registry if not the default 'protorpc'. |
| """ |
| dest_dir = os.path.expanduser(options.dest_dir) |
| |
| url = 'http://%s%s' % (host, registry_path) |
| reg = registry.RegistryService.Stub(transport.HttpTransport(url)) |
| |
| if service_name is None: |
| service_names = [service.name for service in reg.services().services] |
| else: |
| service_names = [service_name] |
| |
| file_set = reg.get_file_set(names=service_names).file_set |
| |
| for file_descriptor in file_set.files: |
| generate_file_descriptor(dest_dir, file_descriptor=file_descriptor, |
| force_overwrite=options.force) |
| |
| |
| def make_opt_parser(): |
| """Create options parser with automatically generated command help. |
| |
| Will iterate over all functions in commands and generate an appropriate |
| usage message for them with all their required and optional parameters. |
| """ |
| command_descriptions = [] |
| for name in sorted(commands.iterkeys()): |
| command = commands[name] |
| params = ' '.join(['<%s>' % param for param in command.required] + |
| ['[<%s>]' % param for param in command.optional]) |
| command_descriptions.append('%%prog [options] %s %s' % (name, params)) |
| command_usage = 'usage: %s\n' % '\n '.join(command_descriptions) |
| |
| parser = optparse.OptionParser(usage=command_usage) |
| parser.add_option('-d', '--dest_dir', |
| dest='dest_dir', |
| default=os.getcwd(), |
| help='Write generated files to DIR', |
| metavar='DIR') |
| parser.add_option('-f', '--force', |
| action='store_true', |
| dest='force', |
| default=False, |
| help='Force overwrite of existing files') |
| return parser |
| |
| parser = make_opt_parser() |
| |
| |
| def main(): |
| # TODO(rafek): Customize verbosity. |
| logging.basicConfig(level=logging.INFO) |
| options, positional = parser.parse_args() |
| |
| if not positional: |
| usage() |
| |
| command_name = positional[0] |
| command = commands.get(command_name) |
| if not command: |
| sys.stderr.write("Unknown command '%s'\n\n" % command_name) |
| usage() |
| parameters = positional[1:] |
| |
| command(options, *parameters) |
| |
| |
| if __name__ == '__main__': |
| main() |