App Engine Python SDK version 1.8.9
[gae.git] / python / google / appengine / tools / gen_protorpc.py
blob101a70ff12f29bb6ecf5f0b6e0f8a7d5c5c04728
1 #!/usr/bin/env python
3 # Copyright 2011 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
18 """Command line tool for generating ProtoRPC definitions from descriptors."""
20 import errno
21 import logging
22 import optparse
23 import os
24 import sys
26 from protorpc import descriptor
27 from protorpc import generate_python
28 from protorpc import protobuf
29 from protorpc import registry
30 from protorpc import transport
31 from protorpc import util
33 EXCLUDED_PACKAGES = frozenset(['protorpc.registry',
34 'protorpc.messages',
35 'protorpc.descriptor',
36 'protorpc.message_types',
39 commands = {}
42 def usage():
43 """Print usage help and exit with an error code."""
44 parser.print_help()
45 sys.exit(2)
48 def fatal_error(message):
49 """Print fatal error messages exit with an error code.
51 Args:
52 message: Message to print to stderr before exit.
53 """
54 sys.stderr.write(message)
55 sys.exit(1)
58 def open_input_file(filename):
59 """Open file for reading.
61 Args:
62 filename: Name of input file to open or None to open stdin.
64 Returns:
65 Opened file if string provided, stdin if filename is None.
66 """
67 # TODO(rafek): Detect missing or invalid files, generating user friendly
68 # error messages.
69 if filename is None:
70 return sys.stdin
71 else:
72 try:
73 return open(filename, 'rb')
74 except IOError, err:
75 fatal_error(str(err))
78 @util.positional(1)
79 def generate_file_descriptor(dest_dir, file_descriptor, force_overwrite):
80 """Generate a single file descriptor to destination directory.
82 Will generate a single Python file from a file descriptor under dest_dir.
83 The sub-directory where the file is generated is determined by the package
84 name of descriptor.
86 Descriptors without package names will not be generated.
88 Descriptors that are part of the ProtoRPC distribution will not be generated.
90 Args:
91 dest_dir: Directory under which to generate files.
92 file_descriptor: FileDescriptor instance to generate source code from.
93 force_overwrite: If True, existing files will be overwritten.
94 """
95 package = file_descriptor.package
96 if not package:
97 # TODO(rafek): Option to cause an error on this condition.
98 logging.warn('Will not generate descriptor without package name')
99 return
101 if package in EXCLUDED_PACKAGES:
102 logging.warn('Will not generate main ProtoRPC class %s' % package)
103 return
105 package_path = package.split('.')
106 directory = package_path[:-1]
107 package_file_name = package_path[-1]
108 directory_name = os.path.join(dest_dir, *directory)
109 output_file_name = os.path.join(directory_name,
110 '%s.py' % (package_file_name,))
112 try:
113 os.makedirs(directory_name)
114 except OSError, err:
115 if err.errno != errno.EEXIST:
116 raise
118 if not force_overwrite and os.path.exists(output_file_name):
119 logging.warn('Not overwriting %s with package %s',
120 output_file_name, package)
121 return
123 output_file = open(output_file_name, 'w')
125 logging.info('Writing package %s to %s',
126 file_descriptor.package, output_file_name)
127 generate_python.format_python_file(file_descriptor, output_file)
130 @util.positional(1)
131 def command(name, required=(), optional=()):
132 """Decorator used for declaring commands used on command line.
134 Each command of this tool can have any number of sequential required
135 parameters and optional parameters. The required and optional parameters
136 will be displayed in the command usage. Arguments passed in to the command
137 are checked to ensure they have at least the required parameters and not
138 too many parameters beyond the optional ones. When there are not enough
139 or too few parameters the usage message is generated and the program exits
140 with an error code.
142 Functions decorated thus are added to commands by their name.
144 Resulting decorated functions will have required and optional attributes
145 assigned to them so that appear in the usage message.
147 Args:
148 name: Name of command that will follow the program name on the command line.
149 required: List of required parameter names as displayed in the usage
150 message.
151 optional: List of optional parameter names as displayed in the usage
152 message.
154 def check_params_decorator(function):
155 def check_params_wrapper(options, *args):
156 if not (len(required) <= len(args) <= len(required) + len(optional)):
157 sys.stderr.write("Incorrect usage for command '%s'\n\n" % name)
158 usage()
159 function(options, *args)
160 check_params_wrapper.required = required
161 check_params_wrapper.optional = optional
162 commands[name] = check_params_wrapper
163 return check_params_wrapper
164 return check_params_decorator
167 @command('file', optional=['input-filename', 'output-filename'])
168 def file_command(options, input_filename=None, output_filename=None):
169 """Generate a single descriptor file to Python.
171 Args:
172 options: Parsed command line options.
173 input_filename: File to read protobuf FileDescriptor from. If None
174 will read from stdin.
175 output_filename: File to write Python source code to. If None will
176 generate to stdout.
178 with open_input_file(input_filename) as input_file:
179 descriptor_content = input_file.read()
181 if output_filename:
182 output_file = open(output_filename, 'w')
183 else:
184 output_file = sys.stdout
186 file_descriptor = protobuf.decode_message(descriptor.FileDescriptor,
187 descriptor_content)
188 generate_python.format_python_file(file_descriptor, output_file)
191 @command('fileset', optional=['filename'])
192 def fileset_command(options, input_filename=None):
193 """Generate source directory structure from FileSet.
195 Args:
196 options: Parsed command line options.
197 input_filename: File to read protobuf FileSet from. If None will read from
198 stdin.
200 with open_input_file(input_filename) as input_file:
201 descriptor_content = input_file.read()
203 dest_dir = os.path.expanduser(options.dest_dir)
205 if not os.path.isdir(dest_dir) and os.path.exists(dest_dir):
206 fatal_error("Destination '%s' is not a directory" % dest_dir)
208 file_set = protobuf.decode_message(descriptor.FileSet,
209 descriptor_content)
211 for file_descriptor in file_set.files:
212 generate_file_descriptor(dest_dir, file_descriptor=file_descriptor,
213 force_overwrite=options.force)
216 @command('registry',
217 required=['host'],
218 optional=['service-name', 'registry-path'])
219 def registry_command(options,
220 host,
221 service_name=None,
222 registry_path='/protorpc'):
223 """Generate source directory structure from remote registry service.
225 Args:
226 options: Parsed command line options.
227 host: Web service host where registry service is located. May include
228 port.
229 service_name: Name of specific service to read. Will generate only Python
230 files that service is dependent on. If None, will generate source code
231 for all services known by the registry.
232 registry_path: Path to find registry if not the default 'protorpc'.
234 dest_dir = os.path.expanduser(options.dest_dir)
236 url = 'http://%s%s' % (host, registry_path)
237 reg = registry.RegistryService.Stub(transport.HttpTransport(url))
239 if service_name is None:
240 service_names = [service.name for service in reg.services().services]
241 else:
242 service_names = [service_name]
244 file_set = reg.get_file_set(names=service_names).file_set
246 for file_descriptor in file_set.files:
247 generate_file_descriptor(dest_dir, file_descriptor=file_descriptor,
248 force_overwrite=options.force)
251 def make_opt_parser():
252 """Create options parser with automatically generated command help.
254 Will iterate over all functions in commands and generate an appropriate
255 usage message for them with all their required and optional parameters.
257 command_descriptions = []
258 for name in sorted(commands.iterkeys()):
259 command = commands[name]
260 params = ' '.join(['<%s>' % param for param in command.required] +
261 ['[<%s>]' % param for param in command.optional])
262 command_descriptions.append('%%prog [options] %s %s' % (name, params))
263 command_usage = 'usage: %s\n' % '\n '.join(command_descriptions)
265 parser = optparse.OptionParser(usage=command_usage)
266 parser.add_option('-d', '--dest_dir',
267 dest='dest_dir',
268 default=os.getcwd(),
269 help='Write generated files to DIR',
270 metavar='DIR')
271 parser.add_option('-f', '--force',
272 action='store_true',
273 dest='force',
274 default=False,
275 help='Force overwrite of existing files')
276 return parser
278 parser = make_opt_parser()
281 def main():
282 # TODO(rafek): Customize verbosity.
283 logging.basicConfig(level=logging.INFO)
284 options, positional = parser.parse_args()
286 if not positional:
287 usage()
289 command_name = positional[0]
290 command = commands.get(command_name)
291 if not command:
292 sys.stderr.write("Unknown command '%s'\n\n" % command_name)
293 usage()
294 parameters = positional[1:]
296 command(options, *parameters)
299 if __name__ == '__main__':
300 main()