feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env python
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@@ -0,0 +1,356 @@
#!/usr/bin/env python
# Copyright 2003 Google Inc. All Rights Reserved.
#
# 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.
"""Generic entry point for Google applications.
To use this module, simply define a 'main' function with a single
'argv' argument and add the following to the end of your source file:
if __name__ == '__main__':
app.run()
TODO(user): Remove silly main-detection logic, and force all clients
of this module to check __name__ explicitly. Fix all current clients
that don't check __name__.
"""
import errno
import os
import pdb
import socket
import stat
import struct
import sys
import time
import traceback
import gflags as flags
FLAGS = flags.FLAGS
flags.DEFINE_boolean('run_with_pdb', 0, 'Set to true for PDB debug mode')
flags.DEFINE_boolean('run_with_profiling', 0,
'Set to true for profiling the script. '
'Execution will be slower, and the output format might '
'change over time.')
flags.DEFINE_boolean('use_cprofile_for_profiling', True,
'Use cProfile instead of the profile module for '
'profiling. This has no effect unless '
'--run_with_profiling is set.')
# If main() exits via an abnormal exception, call into these
# handlers before exiting.
EXCEPTION_HANDLERS = []
help_text_wrap = False # Whether to enable TextWrap in help output
class Error(Exception):
pass
class UsageError(Error):
"""The arguments supplied by the user are invalid.
Raise this when the arguments supplied are invalid from the point of
view of the application. For example when two mutually exclusive
flags have been supplied or when there are not enough non-flag
arguments. It is distinct from flags.FlagsError which covers the lower
level of parsing and validating individual flags.
"""
def __init__(self, message, exitcode=1):
Error.__init__(self, message)
self.exitcode = exitcode
class HelpFlag(flags.BooleanFlag):
"""Special boolean flag that displays usage and raises SystemExit."""
def __init__(self):
flags.BooleanFlag.__init__(self, 'help', 0, 'show this help',
short_name='?', allow_override=1)
def Parse(self, arg):
if arg:
usage(writeto_stdout=1)
sys.exit(1)
class HelpXMLFlag(flags.BooleanFlag):
"""Similar to HelpFlag, but generates output in XML format."""
def __init__(self):
flags.BooleanFlag.__init__(self, 'helpxml', False,
'like --help, but generates XML output',
allow_override=1)
def Parse(self, arg):
if arg:
flags.FLAGS.WriteHelpInXMLFormat(sys.stdout)
sys.exit(1)
class HelpshortFlag(flags.BooleanFlag):
"""Special bool flag that calls usage(shorthelp=1) and raises SystemExit."""
def __init__(self):
flags.BooleanFlag.__init__(self, 'helpshort', 0,
'show usage only for this module',
allow_override=1)
def Parse(self, arg):
if arg:
usage(shorthelp=1, writeto_stdout=1)
sys.exit(1)
class BuildDataFlag(flags.BooleanFlag):
"""Boolean flag that writes build data to stdout and exits."""
def __init__(self):
flags.BooleanFlag.__init__(self, 'show_build_data', 0,
'show build data and exit')
def Parse(self, arg):
if arg:
sys.stdout.write(build_data.BuildData())
sys.exit(0)
def parse_flags_with_usage(args):
"""Try parsing the flags, printing usage and exiting if unparseable."""
try:
argv = FLAGS(args)
return argv
except flags.FlagsError as error:
sys.stderr.write('FATAL Flags parsing error: %s\n' % error)
sys.stderr.write('Pass --help or --helpshort to see help on flags.\n')
sys.exit(1)
_define_help_flags_called = False
def DefineHelpFlags():
"""Register help flags. Idempotent."""
# Use a global to ensure idempotence.
# pylint: disable-msg=W0603
global _define_help_flags_called
if not _define_help_flags_called:
flags.DEFINE_flag(HelpFlag())
flags.DEFINE_flag(HelpXMLFlag())
flags.DEFINE_flag(HelpshortFlag())
flags.DEFINE_flag(BuildDataFlag())
_define_help_flags_called = True
def RegisterAndParseFlagsWithUsage():
"""Register help flags, parse arguments and show usage if appropriate.
Returns:
remaining arguments after flags parsing
"""
DefineHelpFlags()
argv = parse_flags_with_usage(sys.argv)
return argv
def really_start(main=None):
"""Initializes flag values, and calls main with non-flag arguments.
Only non-flag arguments are passed to main(). The return value of main() is
used as the exit status.
Args:
main: Main function to run with the list of non-flag arguments, or None
so that sys.modules['__main__'].main is to be used.
"""
argv = RegisterAndParseFlagsWithUsage()
if main is None:
main = sys.modules['__main__'].main
try:
if FLAGS.run_with_pdb:
sys.exit(pdb.runcall(main, argv))
else:
if FLAGS.run_with_profiling:
# Avoid import overhead since most apps (including performance-sensitive
# ones) won't be run with profiling.
import atexit
if FLAGS.use_cprofile_for_profiling:
import cProfile as profile
else:
import profile
profiler = profile.Profile()
atexit.register(profiler.print_stats)
retval = profiler.runcall(main, argv)
sys.exit(retval)
else:
sys.exit(main(argv))
except UsageError as error:
usage(shorthelp=1, detailed_error=error, exitcode=error.exitcode)
def run():
"""Begin executing the program.
- Parses command line flags with the flag module.
- If there are any errors, print usage().
- Calls main() with the remaining arguments.
- If main() raises a UsageError, print usage and the error message.
"""
return _actual_start()
def _actual_start():
"""Another layer in the starting stack."""
# Get raw traceback
tb = None
try:
raise ZeroDivisionError('')
except ZeroDivisionError:
tb = sys.exc_info()[2]
assert tb
# Look at previous stack frame's previous stack frame (previous
# frame is run() or start(); the frame before that should be the
# frame of the original caller, which should be __main__ or appcommands
prev_prev_frame = tb.tb_frame.f_back.f_back
if not prev_prev_frame:
return
prev_prev_name = prev_prev_frame.f_globals.get('__name__', None)
if (prev_prev_name != '__main__'
and not prev_prev_name.endswith('.appcommands')):
return
# just in case there's non-trivial stuff happening in __main__
del tb
sys.exc_clear()
try:
really_start()
except SystemExit as e:
raise
except Exception as e:
# Call any installed exception handlers which may, for example,
# log to a file or send email.
for handler in EXCEPTION_HANDLERS:
try:
if handler.Wants(e):
handler.Handle(e)
except:
# We don't want to stop for exceptions in the exception handlers but
# we shouldn't hide them either.
sys.stderr.write(traceback.format_exc())
raise
# All handlers have had their chance, now die as we would have normally.
raise
def usage(shorthelp=0, writeto_stdout=0, detailed_error=None, exitcode=None):
"""Write __main__'s docstring to stderr with some help text.
Args:
shorthelp: print only flags from this module, rather than all flags.
writeto_stdout: write help message to stdout, rather than to stderr.
detailed_error: additional detail about why usage info was presented.
exitcode: if set, exit with this status code after writing help.
"""
if writeto_stdout:
stdfile = sys.stdout
else:
stdfile = sys.stderr
doc = sys.modules['__main__'].__doc__
if not doc:
doc = '\nUSAGE: %s [flags]\n' % sys.argv[0]
doc = flags.TextWrap(doc, indent=' ', firstline_indent='')
else:
# Replace all '%s' with sys.argv[0], and all '%%' with '%'.
num_specifiers = doc.count('%') - 2 * doc.count('%%')
try:
doc %= (sys.argv[0],) * num_specifiers
except (OverflowError, TypeError, ValueError):
# Just display the docstring as-is.
pass
if help_text_wrap:
doc = flags.TextWrap(flags.DocToHelp(doc))
if shorthelp:
flag_str = FLAGS.MainModuleHelp()
else:
flag_str = str(FLAGS)
try:
stdfile.write(doc)
if flag_str:
stdfile.write('\nflags:\n')
stdfile.write(flag_str)
stdfile.write('\n')
if detailed_error is not None:
stdfile.write('\n%s\n' % detailed_error)
except IOError as e:
# We avoid printing a huge backtrace if we get EPIPE, because
# "foo.par --help | less" is a frequent use case.
if e.errno != errno.EPIPE:
raise
if exitcode is not None:
sys.exit(exitcode)
class ExceptionHandler(object):
"""Base exception handler from which other may inherit."""
def Wants(self, unused_exc):
"""Check if this exception handler want to handle this exception.
Args:
unused_exc: Exception, the current exception
Returns:
boolean
This base handler wants to handle all exceptions, override this
method if you want to be more selective.
"""
return True
def Handle(self, exc):
"""Do something with the current exception.
Args:
exc: Exception, the current exception
This method must be overridden.
"""
raise NotImplementedError()
def InstallExceptionHandler(handler):
"""Install an exception handler.
Args:
handler: an object conforming to the interface defined in ExceptionHandler
Raises:
TypeError: handler was not of the correct type
All installed exception handlers will be called if main() exits via
an abnormal exception, i.e. not one of SystemExit, KeyboardInterrupt,
FlagsError or UsageError.
"""
if not isinstance(handler, ExceptionHandler):
raise TypeError('handler of type %s does not inherit from ExceptionHandler'
% type(handler))
EXCEPTION_HANDLERS.append(handler)

View File

@@ -0,0 +1,784 @@
#!/usr/bin/env python
# Copyright 2007 Google Inc. All Rights Reserved.
#
# 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.
"""This module is the base for programs that provide multiple commands.
This provides command line tools that have a few shared global flags,
followed by a command name, followed by command specific flags,
then by arguments. That is:
tool [--global_flags] command [--command_flags] [args]
The module is built on top of app.py and 'overrides' a bit of it. However
the interface is mostly the same. The main difference is that your main
is supposed to register commands and return without further execution
of the commands; pre checking is of course welcome! Also your
global initialization should call appcommands.Run() rather than app.run().
To register commands use AddCmd() or AddCmdFunc(). AddCmd() is used
for commands that derive from class Cmd and the AddCmdFunc() is used
to wrap simple functions.
This module itself registers the command 'help' that allows users
to retrieve help for all or specific commands.
Example:
<code>
from mx import DateTime
class CmdDate(appcommands.Cmd):
\"\"\"This docstring contains the help for the date command.\"\"\"
def Run(self, argv):
print DateTime.now()
def main(argv):
appcommands.AddCmd('date', CmdDate, command_aliases=['data_now'])
if __name__ == '__main__':
appcommands.Run()
</code>
In the above example the name of the registered command on the command line is
'date'. Thus, to get the date you would execute:
tool date
The above example also added the command alias 'data_now' which allows to
replace 'tool date' with 'tool data_now'.
To get a list of available commands run:
tool help
For help with a specific command, you would execute:
tool help date
For help on flags run one of the following:
tool --help
Note that 'tool --help' gives you information on global flags, just like for
applications that do not use appcommand. Likewise 'tool --helpshort' and the
other help-flags from app.py are also available.
The above example also demonstrates that you only have to call
appcommands.Run()
and register your commands in main() to initialize your program with appcommands
(and app).
Handling of flags:
Flags can be registered just as with any other google tool using flags.py.
In addition you can also provide command specific flags. To do so simply add
flags registering code into the __init__ function of your Cmd classes passing
parameter flag_values to any flags registering calls. These flags will get
copied to the global flag list, so that once the command is detected they
behave just like any other flag. That means these flags won't be available
for other commands. Note that it is possible to register flags with more
than one command.
Getting help:
This module activates formatting and wrapping to help output. That is
the main difference to help created from app.py. So just as with app.py,
appcommands.py will create help from the main modules main __doc__.
But it adds the new 'help' command that allows you to get a list of
all available commands. Each command's help will be followed by the
registered command specific flags along with their defaults and help.
After help for all commands there will also be a list of all registered
global flags with their defaults and help.
The text for the command's help can best be supplied by overwriting the
__doc__ property of the Cmd classes for commands registered with AddCmd() or
the __doc__ property of command functions registered AddCmdFunc().
Inner working:
This module interacts with app.py by replacing its inner start dispatcher.
The replacement version basically does the same, registering help flags,
checking whether help flags were present, and calling the main module's main
function. However unlike app.py, this module epxpects main() to only register
commands and then to return. After having all commands registered
appcommands.py will then parse the remaining arguments for any registered
command. If one is found it will get executed. Otherwise a short usage info
will be displayed.
Each provided command must be an instance of Cmd. If commands get registered
from global functions using AddCmdFunc() then the helper class _FunctionalCmd
will be used in the registering process.
"""
import os
import pdb
import sys
import traceback
from google.apputils import app
import gflags as flags
FLAGS = flags.FLAGS
# module exceptions:
class AppCommandsError(Exception):
"""The base class for all flags errors."""
pass
_cmd_argv = None # remaining arguments with index 0 = sys.argv[0]
_cmd_list = {} # list of commands index by name (_Cmd instances)
_cmd_alias_list = {} # list of command_names index by command_alias
def GetAppBasename():
"""Returns the friendly basename of this application."""
base = os.path.basename(sys.argv[0]).split('.')
return base[0]
def ShortHelpAndExit(message=None):
"""Display optional message, followed by a note on how to get help, then exit.
Args:
message: optional message to display
"""
sys.stdout.flush()
if message is not None:
sys.stderr.write('%s\n' % message)
sys.stderr.write("Run '%s help' to get help\n" % GetAppBasename())
sys.exit(1)
def GetCommandList():
"""Return list of registered commands."""
# pylint: disable-msg=W0602
global _cmd_list
return _cmd_list
def GetCommandAliasList():
"""Return list of registered command aliases."""
# pylint: disable-msg=W0602
global _cmd_alias_list
return _cmd_alias_list
def GetFullCommandList():
"""Return list of registered commands, including aliases."""
all_cmds = dict(GetCommandList())
for cmd_alias, cmd_name in GetCommandAliasList().iteritems():
all_cmds[cmd_alias] = all_cmds.get(cmd_name)
return all_cmds
def GetCommandByName(name):
"""Get the command or None if name is not a registered command.
Args:
name: name of command to look for
Returns:
Cmd instance holding the command or None
"""
return GetCommandList().get(GetCommandAliasList().get(name))
def GetCommandArgv():
"""Return list of remaining args."""
return _cmd_argv
def GetMaxCommandLength():
"""Returns the length of the longest registered command."""
return max([len(cmd_name) for cmd_name in GetCommandList()])
class Cmd(object):
"""Abstract class describing and implementing a command.
When creating code for a command, at least you have to derive this class
and override method Run(). The other methods of this class might be
overridden as well. Check their documentation for details. If the command
needs any specific flags, use __init__ for registration.
"""
def __init__(self, name, flag_values, command_aliases=None):
"""Initialize and check whether self is actually a Cmd instance.
This can be used to register command specific flags. If you do so
remember that you have to provide the 'flag_values=flag_values'
parameter to any flags.DEFINE_*() call.
Args:
name: Name of the command
flag_values: FlagValues() instance that needs to be passed as
flag_values parameter to any flags registering call.
command_aliases: A list of command aliases that the command can be run as.
Raises:
AppCommandsError: if self is Cmd (Cmd is abstract)
"""
self._command_name = name
self._command_aliases = command_aliases
self._command_flags = flag_values
self._all_commands_help = None
if type(self) is Cmd:
raise AppCommandsError('Cmd is abstract and cannot be instantiated')
def Run(self, argv):
"""Execute the command. Must be provided by the implementing class.
Args:
argv: Remaining command line arguments after parsing flags and command
(that is a copy of sys.argv at the time of the function call with
all parsed flags removed).
Returns:
0 for success, anything else for failure (must return with integer).
Alternatively you may return None (or not use a return statement at all).
Raises:
AppCommandsError: Always as in must be overwritten
"""
raise AppCommandsError('%s.%s.Run() is not implemented' % (
type(self).__module__, type(self).__name__))
def CommandRun(self, argv):
"""Execute the command with given arguments.
First register and parse additional flags. Then run the command.
Returns:
Command return value.
Args:
argv: Remaining command line arguments after parsing command and flags
(that is a copy of sys.argv at the time of the function call with
all parsed flags removed).
"""
# Register flags global when run normally
FLAGS.AppendFlagValues(self._command_flags)
# Prepare flags parsing, to redirect help, to show help for command
orig_app_usage = app.usage
def ReplacementAppUsage(shorthelp=0, writeto_stdout=1, detailed_error=None,
exitcode=None):
AppcommandsUsage(shorthelp, writeto_stdout, detailed_error, exitcode=1,
show_cmd=self._command_name, show_global_flags=True)
app.usage = ReplacementAppUsage
# Parse flags and restore app.usage afterwards
try:
try:
argv = ParseFlagsWithUsage(argv)
# Run command
if FLAGS.run_with_pdb:
ret = pdb.runcall(self.Run, argv)
else:
ret = self.Run(argv)
if ret is None:
ret = 0
else:
assert isinstance(ret, int)
return ret
except app.UsageError as error:
app.usage(shorthelp=1, detailed_error=error, exitcode=error.exitcode)
finally:
# Restore app.usage and remove this command's flags from the global flags.
app.usage = orig_app_usage
for flag_name in self._command_flags.FlagDict():
delattr(FLAGS, flag_name)
def CommandGetHelp(self, unused_argv, cmd_names=None):
"""Get help string for command.
Args:
unused_argv: Remaining command line flags and arguments after parsing
command (that is a copy of sys.argv at the time of the
function call with all parsed flags removed); unused in this
default implementation, but may be used in subclasses.
cmd_names: Complete list of commands for which help is being shown at
the same time. This is used to determine whether to return
_all_commands_help, or the command's docstring.
(_all_commands_help is used, if not None, when help is being
shown for more than one command, otherwise the command's
docstring is used.)
Returns:
Help string, one of the following (by order):
- Result of the registered 'help' function (if any)
- Doc string of the Cmd class (if any)
- Default fallback string
"""
if (type(cmd_names) is list and len(cmd_names) > 1 and
self._all_commands_help is not None):
return flags.DocToHelp(self._all_commands_help)
elif self.__doc__:
return flags.DocToHelp(self.__doc__)
else:
return 'No help available'
def CommandGetAliases(self):
"""Get aliases for command.
Returns:
aliases: list of aliases for the command.
"""
return self._command_aliases
class _FunctionalCmd(Cmd):
"""Class to wrap functions as CMD instances.
Args:
cmd_func: command function
"""
def __init__(self, name, flag_values, cmd_func, all_commands_help=None,
**kargs):
"""Create a functional command.
Args:
name: Name of command
flag_values: FlagValues() instance that needs to be passed as flag_values
parameter to any flags registering call.
cmd_func: Function to call when command is to be executed.
"""
Cmd.__init__(self, name, flag_values, **kargs)
self._all_commands_help = all_commands_help
self._cmd_func = cmd_func
def CommandGetHelp(self, unused_argv, cmd_names=None):
"""Get help for command.
Args:
unused_argv: Remaining command line flags and arguments after parsing
command (that is a copy of sys.argv at the time of the
function call with all parsed flags removed); unused in this
implementation.
cmd_names: By default, if help is being shown for more than one command,
and this command defines _all_commands_help, then
_all_commands_help will be displayed instead of the class
doc. cmd_names is used to determine the number of commands
being displayed and if only a single command is display then
the class doc is returned.
Returns:
__doc__ property for command function or a message stating there is no
help.
"""
if (type(cmd_names) is list and len(cmd_names) > 1 and
self._all_commands_help is not None):
return flags.DocToHelp(self._all_commands_help)
if self._cmd_func.__doc__ is not None:
return flags.DocToHelp(self._cmd_func.__doc__)
else:
return 'No help available'
def Run(self, argv):
"""Execute the command with given arguments.
Args:
argv: Remaining command line flags and arguments after parsing command
(that is a copy of sys.argv at the time of the function call with
all parsed flags removed).
Returns:
Command return value.
"""
return self._cmd_func(argv)
def _AddCmdInstance(command_name, cmd, command_aliases=None):
"""Add a command from a Cmd instance.
Args:
command_name: name of the command which will be used in argument parsing
cmd: Cmd instance to register
command_aliases: A list of command aliases that the command can be run as.
Raises:
AppCommandsError: is command is already registered OR cmd is not a subclass
of Cmd
AppCommandsError: if name is already registered OR name is not a string OR
name is too short OR name does not start with a letter OR
name contains any non alphanumeric characters besides
'_', '-', or ':'.
"""
# Update global command list.
# pylint: disable-msg=W0602
global _cmd_list
global _cmd_alias_list
if not issubclass(cmd.__class__, Cmd):
raise AppCommandsError('Command must be an instance of commands.Cmd')
for name in [command_name] + (command_aliases or []):
_CheckCmdName(name)
_cmd_alias_list[name] = command_name
_cmd_list[command_name] = cmd
def _CheckCmdName(name_or_alias):
"""Only allow strings for command names and aliases (reject unicode as well).
Args:
name_or_alias: properly formatted string name or alias.
Raises:
AppCommandsError: is command is already registered OR cmd is not a subclass
of Cmd
AppCommandsError: if name is already registered OR name is not a string OR
name is too short OR name does not start with a letter OR
name contains any non alphanumeric characters besides
'_', '-', or ':'.
"""
if name_or_alias in GetCommandAliasList():
raise AppCommandsError("Command or Alias '%s' already defined" %
name_or_alias)
if not isinstance(name_or_alias, str) or len(name_or_alias) <= 1:
raise AppCommandsError("Command '%s' not a string or too short"
% str(name_or_alias))
if not name_or_alias[0].isalpha():
raise AppCommandsError("Command '%s' does not start with a letter"
% name_or_alias)
if [c for c in name_or_alias if not (c.isalnum() or c in ('_', '-', ':'))]:
raise AppCommandsError("Command '%s' contains non alphanumeric characters"
% name_or_alias)
def AddCmd(command_name, cmd_factory, **kargs):
"""Add a command from a Cmd subclass or factory.
Args:
command_name: name of the command which will be used in argument parsing
cmd_factory: A callable whose arguments match those of Cmd.__init__ and
returns a Cmd. In the simplest case this is just a subclass
of Cmd.
command_aliases: A list of command aliases that the command can be run as.
Raises:
AppCommandsError: if calling cmd_factory does not return an instance of Cmd.
"""
cmd = cmd_factory(command_name, flags.FlagValues(), **kargs)
if not isinstance(cmd, Cmd):
raise AppCommandsError('Command must be an instance of commands.Cmd')
_AddCmdInstance(command_name, cmd, **kargs)
def AddCmdFunc(command_name, cmd_func, command_aliases=None,
all_commands_help=None):
"""Add a new command to the list of registered commands.
Args:
command_name: name of the command which will be used in argument
parsing
cmd_func: command function, this function received the remaining
arguments as its only parameter. It is supposed to do the
command work and then return with the command result that
is being used as the shell exit code.
command_aliases: A list of command aliases that the command can be run as.
all_commands_help: Help message to be displayed in place of func.__doc__
when all commands are displayed.
"""
_AddCmdInstance(command_name,
_FunctionalCmd(command_name, flags.FlagValues(), cmd_func,
command_aliases=command_aliases,
all_commands_help=all_commands_help),
command_aliases)
class _CmdHelp(Cmd):
"""Standard help command.
Allows to provide help for all or specific commands.
"""
def Run(self, argv):
"""Execute help command.
If an argument is given and that argument is a registered command
name, then help specific to that command is being displayed.
If the command is unknown then a fatal error will be displayed. If
no argument is present then help for all commands will be presented.
If a specific command help is being generated, the list of commands is
temporarily replaced with one containing only that command. Thus the call
to usage() will only show help for that command. Otherwise call usage()
will show help for all registered commands as it sees all commands.
Args:
argv: Remaining command line flags and arguments after parsing command
(that is a copy of sys.argv at the time of the function call with
all parsed flags removed).
So argv[0] is the program and argv[1] will be the first argument to
the call. For instance 'tool.py help command' will result in argv
containing ('tool.py', 'command'). In this case the list of
commands is searched for 'command'.
Returns:
1 for failure
"""
if len(argv) > 1 and argv[1] in GetFullCommandList():
show_cmd = argv[1]
else:
show_cmd = None
AppcommandsUsage(shorthelp=0, writeto_stdout=1, detailed_error=None,
exitcode=1, show_cmd=show_cmd, show_global_flags=False)
def CommandGetHelp(self, unused_argv, cmd_names=None):
"""Returns: Help for command."""
cmd_help = ('Help for all or selected command:\n'
'\t%(prog)s help [<command>]\n\n'
'To retrieve help with global flags:\n'
'\t%(prog)s --help\n\n'
'To retrieve help with flags only from the main module:\n'
'\t%(prog)s --helpshort [<command>]\n\n'
% {'prog': GetAppBasename()})
return flags.DocToHelp(cmd_help)
def GetSynopsis():
"""Get synopsis for program.
Returns:
Synopsis including program basename.
"""
return '%s [--global_flags] <command> [--command_flags] [args]' % (
GetAppBasename())
def _UsageFooter(detailed_error, cmd_names):
"""Output a footer at the end of usage or help output.
Args:
detailed_error: additional detail about why usage info was presented.
cmd_names: list of command names for which help was shown or None.
Returns:
Generated footer that contains 'Run..' messages if appropriate.
"""
footer = []
if not cmd_names or len(cmd_names) == 1:
footer.append("Run '%s help' to see the list of available commands."
% GetAppBasename())
if not cmd_names or len(cmd_names) == len(GetCommandList()):
footer.append("Run '%s help <command>' to get help for <command>."
% GetAppBasename())
if detailed_error is not None:
if footer:
footer.append('')
footer.append('%s' % detailed_error)
return '\n'.join(footer)
def AppcommandsUsage(shorthelp=0, writeto_stdout=0, detailed_error=None,
exitcode=None, show_cmd=None, show_global_flags=False):
"""Output usage or help information.
Extracts the __doc__ string from the __main__ module and writes it to
stderr. If that string contains a '%s' then that is replaced by the command
pathname. Otherwise a default usage string is being generated.
The output varies depending on the following:
- FLAGS.help
- FLAGS.helpshort
- show_cmd
- show_global_flags
Args:
shorthelp: print only command and main module flags, rather than all.
writeto_stdout: write help message to stdout, rather than to stderr.
detailed_error: additional details about why usage info was presented.
exitcode: if set, exit with this status code after writing help.
show_cmd: show help for this command only (name of command).
show_global_flags: show help for global flags.
"""
if writeto_stdout:
stdfile = sys.stdout
else:
stdfile = sys.stderr
prefix = ''.rjust(GetMaxCommandLength() + 2)
# Deal with header, containing general tool documentation
doc = sys.modules['__main__'].__doc__
if doc:
help_msg = flags.DocToHelp(doc.replace('%s', sys.argv[0]))
stdfile.write(flags.TextWrap(help_msg, flags.GetHelpWidth()))
stdfile.write('\n\n\n')
if not doc or doc.find('%s') == -1:
synopsis = 'USAGE: ' + GetSynopsis()
stdfile.write(flags.TextWrap(synopsis, flags.GetHelpWidth(), ' ',
''))
stdfile.write('\n\n\n')
# Special case just 'help' registered, that means run as 'tool --help'.
if len(GetCommandList()) == 1:
cmd_names = []
else:
# Show list of commands
if show_cmd is None or show_cmd == 'help':
cmd_names = GetCommandList().keys()
cmd_names.sort()
stdfile.write('Any of the following commands:\n')
doc = ', '.join(cmd_names)
stdfile.write(flags.TextWrap(doc, flags.GetHelpWidth(), ' '))
stdfile.write('\n\n\n')
# Prepare list of commands to show help for
if show_cmd is not None:
cmd_names = [show_cmd] # show only one command
elif FLAGS.help or FLAGS.helpshort or shorthelp:
cmd_names = []
else:
cmd_names = GetCommandList().keys() # show all commands
cmd_names.sort()
# Show the command help (none, one specific, or all)
for name in cmd_names:
command = GetCommandByName(name)
cmd_help = command.CommandGetHelp(GetCommandArgv(), cmd_names=cmd_names)
cmd_help = cmd_help.strip()
all_names = ', '.join([name] + (command.CommandGetAliases() or []))
if len(all_names) + 1 >= len(prefix) or not cmd_help:
# If command/alias list would reach over help block-indent
# start the help block on a new line.
stdfile.write(flags.TextWrap(all_names, flags.GetHelpWidth()))
stdfile.write('\n')
prefix1 = prefix
else:
prefix1 = all_names.ljust(GetMaxCommandLength() + 2)
if cmd_help:
stdfile.write(flags.TextWrap(cmd_help, flags.GetHelpWidth(), prefix,
prefix1))
stdfile.write('\n\n')
else:
stdfile.write('\n')
# When showing help for exactly one command we show its flags
if len(cmd_names) == 1:
# Need to register flags for command prior to be able to use them.
# We do not register them globally so that they do not reappear.
# pylint: disable-msg=W0212
cmd_flags = command._command_flags
if cmd_flags.RegisteredFlags():
stdfile.write('%sFlags for %s:\n' % (prefix, name))
stdfile.write(cmd_flags.GetHelp(prefix+' '))
stdfile.write('\n\n')
stdfile.write('\n')
# Now show global flags as asked for
if show_global_flags:
stdfile.write('Global flags:\n')
if shorthelp:
stdfile.write(FLAGS.MainModuleHelp())
else:
stdfile.write(FLAGS.GetHelp())
stdfile.write('\n')
else:
stdfile.write("Run '%s --help' to get help for global flags."
% GetAppBasename())
stdfile.write('\n%s\n' % _UsageFooter(detailed_error, cmd_names))
if exitcode is not None:
sys.exit(exitcode)
def ParseFlagsWithUsage(argv):
"""Parse the flags, exiting (after printing usage) if they are unparseable.
Args:
argv: command line arguments
Returns:
remaining command line arguments after parsing flags
"""
# Update the global commands.
# pylint: disable-msg=W0603
global _cmd_argv
try:
_cmd_argv = FLAGS(argv)
return _cmd_argv
except flags.FlagsError as error:
ShortHelpAndExit('FATAL Flags parsing error: %s' % error)
def GetCommand(command_required):
"""Get the command or return None (or issue an error) if there is none.
Args:
command_required: whether to issue an error if no command is present
Returns:
command or None, if command_required is True then return value is a valid
command or the program will exit. The program also exits if a command was
specified but that command does not exist.
"""
# Update the global commands.
# pylint: disable-msg=W0603
global _cmd_argv
_cmd_argv = ParseFlagsWithUsage(_cmd_argv)
if len(_cmd_argv) < 2:
if command_required:
ShortHelpAndExit('FATAL Command expected but none given')
return None
command = GetCommandByName(_cmd_argv[1])
if command is None:
ShortHelpAndExit("FATAL Command '%s' unknown" % _cmd_argv[1])
del _cmd_argv[1]
return command
def _CommandsStart():
"""Main initialization.
This initializes flag values, and calls __main__.main(). Only non-flag
arguments are passed to main(). The return value of main() is used as the
exit status.
"""
app.RegisterAndParseFlagsWithUsage()
# The following is supposed to return after registering additional commands
try:
sys.modules['__main__'].main(GetCommandArgv())
# If sys.exit was called, return with error code.
except SystemExit as e:
sys.exit(e.code)
except Exception as error:
traceback.print_exc() # Print a backtrace to stderr.
ShortHelpAndExit('\nFATAL error in main: %s' % error)
if len(GetCommandArgv()) > 1:
command = GetCommand(command_required=True)
else:
command = GetCommandByName('help')
sys.exit(command.CommandRun(GetCommandArgv()))
def Run():
"""This must be called from __main__ modules main, instead of app.run().
app.run will base its actions on its stacktrace.
Returns:
app.run()
"""
app.parse_flags_with_usage = ParseFlagsWithUsage
app.really_start = _CommandsStart
app.usage = _ReplacementAppUsage
return app.run()
# Always register 'help' command
AddCmd('help', _CmdHelp)
def _ReplacementAppUsage(shorthelp=0, writeto_stdout=0, detailed_error=None,
exitcode=None):
AppcommandsUsage(shorthelp, writeto_stdout, detailed_error, exitcode=exitcode,
show_cmd=None, show_global_flags=True)
if __name__ == '__main__':
Run()

View File

@@ -0,0 +1,421 @@
#!/usr/bin/env python
# Copyright 2002 Google Inc. All Rights Reserved.
#
# 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.
"""Set of classes and functions for dealing with dates and timestamps.
The BaseTimestamp and Timestamp are timezone-aware wrappers around Python
datetime.datetime class.
"""
import calendar
import copy
import datetime
import re
import sys
import time
import types
from dateutil import parser
import pytz
_MICROSECONDS_PER_SECOND = 1000000
_MICROSECONDS_PER_SECOND_F = float(_MICROSECONDS_PER_SECOND)
def SecondsToMicroseconds(seconds):
"""Convert seconds to microseconds.
Args:
seconds: number
Returns:
microseconds
"""
return seconds * _MICROSECONDS_PER_SECOND
def _GetCurrentTimeMicros():
"""Get the current time in microseconds, in UTC.
Returns:
The number of microseconds since the epoch.
"""
return int(SecondsToMicroseconds(time.time()))
def GetSecondsSinceEpoch(time_tuple):
"""Convert time_tuple (in UTC) to seconds (also in UTC).
Args:
time_tuple: tuple with at least 6 items.
Returns:
seconds.
"""
return calendar.timegm(time_tuple[:6] + (0, 0, 0))
def GetTimeMicros(time_tuple):
"""Get a time in microseconds.
Arguments:
time_tuple: A (year, month, day, hour, minute, second) tuple (the python
time tuple format) in the UTC time zone.
Returns:
The number of microseconds since the epoch represented by the input tuple.
"""
return int(SecondsToMicroseconds(GetSecondsSinceEpoch(time_tuple)))
UTC = pytz.UTC
US_PACIFIC = pytz.timezone('US/Pacific')
class TimestampError(ValueError):
"""Generic timestamp-related error."""
pass
class TimezoneNotSpecifiedError(TimestampError):
"""This error is raised when timezone is not specified."""
pass
class TimeParseError(TimestampError):
"""This error is raised when we can't parse the input."""
pass
# TODO(user): this class needs to handle daylight better
class LocalTimezoneClass(datetime.tzinfo):
"""This class defines local timezone."""
ZERO = datetime.timedelta(0)
HOUR = datetime.timedelta(hours=1)
STDOFFSET = datetime.timedelta(seconds=-time.timezone)
if time.daylight:
DSTOFFSET = datetime.timedelta(seconds=-time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
def utcoffset(self, dt):
"""datetime -> minutes east of UTC (negative for west of UTC)."""
if self._isdst(dt):
return self.DSTOFFSET
else:
return self.STDOFFSET
def dst(self, dt):
"""datetime -> DST offset in minutes east of UTC."""
if self._isdst(dt):
return self.DSTDIFF
else:
return self.ZERO
def tzname(self, dt):
"""datetime -> string name of time zone."""
return time.tzname[self._isdst(dt)]
def _isdst(self, dt):
"""Return true if given datetime is within local DST."""
tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
def __repr__(self):
"""Return string '<Local>'."""
return '<Local>'
def localize(self, dt, unused_is_dst=False):
"""Convert naive time to local time."""
if dt.tzinfo is not None:
raise ValueError('Not naive datetime (tzinfo is already set)')
return dt.replace(tzinfo=self)
def normalize(self, dt, unused_is_dst=False):
"""Correct the timezone information on the given datetime."""
if dt.tzinfo is None:
raise ValueError('Naive time - no tzinfo set')
return dt.replace(tzinfo=self)
LocalTimezone = LocalTimezoneClass()
class BaseTimestamp(datetime.datetime):
"""Our kind of wrapper over datetime.datetime.
The objects produced by methods now, today, fromtimestamp, utcnow,
utcfromtimestamp are timezone-aware (with correct timezone).
We also overload __add__ and __sub__ method, to fix the result of arithmetic
operations.
"""
LocalTimezone = LocalTimezone
@classmethod
def AddLocalTimezone(cls, obj):
"""If obj is naive, add local timezone to it."""
if not obj.tzinfo:
return obj.replace(tzinfo=cls.LocalTimezone)
return obj
@classmethod
def Localize(cls, obj):
"""If obj is naive, localize it to cls.LocalTimezone."""
if not obj.tzinfo:
return cls.LocalTimezone.localize(obj)
return obj
def __add__(self, *args, **kwargs):
"""x.__add__(y) <==> x+y."""
r = super(BaseTimestamp, self).__add__(*args, **kwargs)
return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second,
r.microsecond, r.tzinfo)
def __sub__(self, *args, **kwargs):
"""x.__add__(y) <==> x-y."""
r = super(BaseTimestamp, self).__sub__(*args, **kwargs)
if isinstance(r, datetime.datetime):
return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second,
r.microsecond, r.tzinfo)
return r
@classmethod
def now(cls, *args, **kwargs):
"""Get a timestamp corresponding to right now.
Args:
args: Positional arguments to pass to datetime.datetime.now().
kwargs: Keyword arguments to pass to datetime.datetime.now(). If tz is not
specified, local timezone is assumed.
Returns:
A new BaseTimestamp with tz's local day and time.
"""
return cls.AddLocalTimezone(
super(BaseTimestamp, cls).now(*args, **kwargs))
@classmethod
def today(cls):
"""Current BaseTimestamp.
Same as self.__class__.fromtimestamp(time.time()).
Returns:
New self.__class__.
"""
return cls.AddLocalTimezone(super(BaseTimestamp, cls).today())
@classmethod
def fromtimestamp(cls, *args, **kwargs):
"""Get a new localized timestamp from a POSIX timestamp.
Args:
args: Positional arguments to pass to datetime.datetime.fromtimestamp().
kwargs: Keyword arguments to pass to datetime.datetime.fromtimestamp().
If tz is not specified, local timezone is assumed.
Returns:
A new BaseTimestamp with tz's local day and time.
"""
return cls.Localize(
super(BaseTimestamp, cls).fromtimestamp(*args, **kwargs))
@classmethod
def utcnow(cls):
"""Return a new BaseTimestamp representing UTC day and time."""
return super(BaseTimestamp, cls).utcnow().replace(tzinfo=pytz.utc)
@classmethod
def utcfromtimestamp(cls, *args, **kwargs):
"""timestamp -> UTC datetime from a POSIX timestamp (like time.time())."""
return super(BaseTimestamp, cls).utcfromtimestamp(
*args, **kwargs).replace(tzinfo=pytz.utc)
@classmethod
def strptime(cls, date_string, format, tz=None):
"""Parse date_string according to format and construct BaseTimestamp.
Args:
date_string: string passed to time.strptime.
format: format string passed to time.strptime.
tz: if not specified, local timezone assumed.
Returns:
New BaseTimestamp.
"""
if tz is None:
return cls.Localize(cls(*(time.strptime(date_string, format)[:6])))
return tz.localize(cls(*(time.strptime(date_string, format)[:6])))
def astimezone(self, *args, **kwargs):
"""tz -> convert to time in new timezone tz."""
r = super(BaseTimestamp, self).astimezone(*args, **kwargs)
return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second,
r.microsecond, r.tzinfo)
@classmethod
def FromMicroTimestamp(cls, ts):
"""Create new Timestamp object from microsecond UTC timestamp value.
Args:
ts: integer microsecond UTC timestamp
Returns:
New cls()
"""
return cls.utcfromtimestamp(ts/_MICROSECONDS_PER_SECOND_F)
def AsSecondsSinceEpoch(self):
"""Return number of seconds since epoch (timestamp in seconds)."""
return GetSecondsSinceEpoch(self.utctimetuple())
def AsMicroTimestamp(self):
"""Return microsecond timestamp constructed from this object."""
return (SecondsToMicroseconds(self.AsSecondsSinceEpoch()) +
self.microsecond)
@classmethod
def combine(cls, datepart, timepart, tz=None):
"""Combine date and time into timestamp, timezone-aware.
Args:
datepart: datetime.date
timepart: datetime.time
tz: timezone or None
Returns:
timestamp object
"""
result = super(BaseTimestamp, cls).combine(datepart, timepart)
if tz:
result = tz.localize(result)
return result
# Conversions from interval suffixes to number of seconds.
# (m => 60s, d => 86400s, etc)
_INTERVAL_CONV_DICT = {'s': 1}
_INTERVAL_CONV_DICT['m'] = 60 * _INTERVAL_CONV_DICT['s']
_INTERVAL_CONV_DICT['h'] = 60 * _INTERVAL_CONV_DICT['m']
_INTERVAL_CONV_DICT['d'] = 24 * _INTERVAL_CONV_DICT['h']
_INTERVAL_CONV_DICT['D'] = _INTERVAL_CONV_DICT['d']
_INTERVAL_CONV_DICT['w'] = 7 * _INTERVAL_CONV_DICT['d']
_INTERVAL_CONV_DICT['W'] = _INTERVAL_CONV_DICT['w']
_INTERVAL_CONV_DICT['M'] = 30 * _INTERVAL_CONV_DICT['d']
_INTERVAL_CONV_DICT['Y'] = 365 * _INTERVAL_CONV_DICT['d']
_INTERVAL_REGEXP = re.compile('^([0-9]+)([%s])?' % ''.join(_INTERVAL_CONV_DICT))
def ConvertIntervalToSeconds(interval):
"""Convert a formatted string representing an interval into seconds.
Args:
interval: String to interpret as an interval. A basic interval looks like
"<number><suffix>". Complex intervals consisting of a chain of basic
intervals are also allowed.
Returns:
An integer representing the number of seconds represented by the interval
string, or None if the interval string could not be decoded.
"""
total = 0
while interval:
match = _INTERVAL_REGEXP.match(interval)
if not match:
return None
try:
num = int(match.group(1))
except ValueError:
return None
suffix = match.group(2)
if suffix:
multiplier = _INTERVAL_CONV_DICT.get(suffix)
if not multiplier:
return None
num *= multiplier
total += num
interval = interval[match.end(0):]
return total
class Timestamp(BaseTimestamp):
"""This subclass contains methods to parse W3C and interval date spec.
The interval date specification is in the form "1D", where "D" can be
"s"econds "m"inutes "h"ours "D"ays "W"eeks "M"onths "Y"ears.
"""
INTERVAL_CONV_DICT = _INTERVAL_CONV_DICT
INTERVAL_REGEXP = _INTERVAL_REGEXP
@classmethod
def _StringToTime(cls, timestring, tz=None):
"""Use dateutil.parser to convert string into timestamp.
dateutil.parser understands ISO8601 which is really handy.
Args:
timestring: string with datetime
tz: optional timezone, if timezone is omitted from timestring.
Returns:
New Timestamp.
"""
r = parser.parse(timestring)
if not r.tzinfo:
r = (tz or cls.LocalTimezone).localize(r)
result = cls(r.year, r.month, r.day, r.hour, r.minute, r.second,
r.microsecond, r.tzinfo)
return result
@classmethod
def _IntStringToInterval(cls, timestring):
"""Parse interval date specification and create a timedelta object."""
return datetime.timedelta(seconds=ConvertIntervalToSeconds(timestring))
@classmethod
def FromString(cls, value, tz=None):
"""Try to create a Timestamp from a string."""
val = cls._StringToTime(value, tz)
if val:
return val
val = cls._IntStringToInterval(value)
if val:
return cls.utcnow() - val
raise TimeParseError(value)
# What's written below is a clear python bug. I mean, okay, I can apply
# negative timezone to it and end result will be inconversible.
MAXIMUM_PYTHON_TIMESTAMP = Timestamp(
9999, 12, 31, 23, 59, 59, 999999, UTC)
# This is also a bug. It is called 32bit time_t. I hate it.
# This is fixed in 2.5, btw.
MAXIMUM_MICROSECOND_TIMESTAMP = 0x80000000 * _MICROSECONDS_PER_SECOND - 1
MAXIMUM_MICROSECOND_TIMESTAMP_AS_TS = Timestamp(2038, 1, 19, 3, 14, 7, 999999)

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python
# Copyright 2004 Google Inc. All Rights Reserved.
#
# 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.
"""Import this module to add a hook to call pdb on uncaught exceptions.
To enable this, do the following in your top-level application:
import google.apputils.debug
and then in your main():
google.apputils.debug.Init()
Then run your program with --pdb.
"""
import sys
import gflags as flags
flags.DEFINE_boolean('pdb', 0, 'Drop into pdb on uncaught exceptions')
old_excepthook = None
def _DebugHandler(exc_class, value, tb):
if not flags.FLAGS.pdb or hasattr(sys, 'ps1') or not sys.stderr.isatty():
# we aren't in interactive mode or we don't have a tty-like
# device, so we call the default hook
old_excepthook(exc_class, value, tb)
else:
# Don't impose import overhead on apps that never raise an exception.
import traceback
import pdb
# we are in interactive mode, print the exception...
traceback.print_exception(exc_class, value, tb)
print
# ...then start the debugger in post-mortem mode.
pdb.pm()
def Init():
# Must back up old excepthook.
global old_excepthook # pylint: disable-msg=W0603
old_excepthook = sys.excepthook
sys.excepthook = _DebugHandler

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python
# Copyright 2007 Google Inc. All Rights Reserved.
#
# 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.
"""Simple file system utilities."""
__author__ = ('elaforge@google.com (Evan LaForge)',
'matthewb@google.com (Matthew Blecker)')
import errno
import os
import pwd
import shutil
import stat
import tempfile
class PasswdError(Exception):
"""Exception class for errors loading a password from a file."""
def ListDirPath(dir_name):
"""Like os.listdir with prepended dir_name, which is often more convenient."""
return [os.path.join(dir_name, fn) for fn in os.listdir(dir_name)]
def Read(filename):
"""Read entire contents of file with name 'filename'."""
fp = open(filename)
try:
return fp.read()
finally:
fp.close()
def Write(filename, contents, overwrite_existing=True, mode=0o666):
"""Create a file 'filename' with 'contents', with the mode given in 'mode'.
The 'mode' is modified by the umask, as in open(2). If
'overwrite_existing' is False, the file will be opened in O_EXCL mode.
Args:
filename: str; the name of the file
contents: str; the data to write to the file
overwrite_existing: bool; whether or not to allow the write if the file
already exists
mode: int; permissions with which to create the file (default is 0666 octal)
"""
flags = os.O_WRONLY | os.O_TRUNC | os.O_CREAT
if not overwrite_existing:
flags |= os.O_EXCL
fd = os.open(filename, flags, mode)
try:
os.write(fd, contents)
finally:
os.close(fd)
def AtomicWrite(filename, contents, mode=0o666):
"""Create a file 'filename' with 'contents' atomically.
As in Write, 'mode' is modified by the umask. This creates and moves
a temporary file, and errors doing the above will be propagated normally,
though it will try to clean up the temporary file in that case.
This is very similar to the prodlib function with the same name.
Args:
filename: str; the name of the file
contents: str; the data to write to the file
mode: int; permissions with which to create the file (default is 0666 octal)
"""
(fd, tmp_filename) = tempfile.mkstemp(dir=os.path.dirname(filename))
try:
os.write(fd, contents)
finally:
os.close(fd)
try:
os.chmod(tmp_filename, mode)
os.rename(tmp_filename, filename)
except OSError as exc:
try:
os.remove(tmp_filename)
except OSError as e:
exc = OSError('%s. Additional errors cleaning up: %s' % (exc, e))
raise exc
def MkDirs(directory, force_mode=None):
"""Makes a directory including its parent directories.
This function is equivalent to os.makedirs() but it avoids a race
condition that os.makedirs() has. The race is between os.mkdir() and
os.path.exists() which fail with errors when run in parallel.
Args:
directory: str; the directory to make
force_mode: optional octal, chmod dir to get rid of umask interaction
Raises:
Whatever os.mkdir() raises when it fails for any reason EXCLUDING
"dir already exists". If a directory already exists, it does not
raise anything. This behaviour is different than os.makedirs()
"""
name = os.path.normpath(directory)
dirs = name.split(os.path.sep)
for i in range(0, len(dirs)):
path = os.path.sep.join(dirs[:i+1])
try:
if path:
os.mkdir(path)
# only chmod if we created
if force_mode is not None:
os.chmod(path, force_mode)
except OSError as exc:
if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
raise
def RmDirs(dir_name):
"""Removes dir_name and every non-empty directory in dir_name.
Unlike os.removedirs and shutil.rmtree, this function doesn't raise an error
if the directory does not exist.
Args:
dir_name: Directory to be removed.
"""
try:
shutil.rmtree(dir_name)
except OSError as err:
if err.errno != errno.ENOENT:
raise
try:
parent_directory = os.path.dirname(dir_name)
while parent_directory:
try:
os.rmdir(parent_directory)
except OSError as err:
if err.errno != errno.ENOENT:
raise
parent_directory = os.path.dirname(parent_directory)
except OSError as err:
if err.errno not in (errno.EACCES, errno.ENOTEMPTY):
raise
def HomeDir(user=None):
"""Find the home directory of a user.
Args:
user: int, str, or None - the uid or login of the user to query for,
or None (the default) to query for the current process' effective user
Returns:
str - the user's home directory
Raises:
TypeError: if user is not int, str, or None.
"""
if user is None:
pw_struct = pwd.getpwuid(os.geteuid())
elif isinstance(user, int):
pw_struct = pwd.getpwuid(user)
elif isinstance(user, str):
pw_struct = pwd.getpwnam(user)
else:
raise TypeError('user must be None or an instance of int or str')
return pw_struct.pw_dir

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python
# Copyright 2010 Google Inc. All Rights Reserved.
#
# 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.
"""Wrapper around setuptools' pkg_resources with more Google-like names.
This module is not very useful on its own, but many Google open-source projects
are used to a different naming scheme, and this module makes the transition
easier.
"""
__author__ = 'dborowitz@google.com (Dave Borowitz)'
import atexit
import pkg_resources
def _Call(func, name):
"""Call a pkg_resources function.
Args:
func: A function from pkg_resources that takes the arguments
(package_or_requirement, resource_name); for more info,
see http://peak.telecommunity.com/DevCenter/PkgResources
name: A name of the form 'module.name:path/to/resource'; this should
generally be built from __name__ in the calling module.
Returns:
The result of calling the function on the split resource name.
"""
pkg_name, resource_name = name.split(':', 1)
return func(pkg_name, resource_name)
def GetResource(name):
"""Get a resource as a string; see _Call."""
return _Call(pkg_resources.resource_string, name)
def GetResourceAsFile(name):
"""Get a resource as a file-like object; see _Call."""
return _Call(pkg_resources.resource_stream, name)
_extracted_files = False
def GetResourceFilename(name):
"""Get a filename for a resource; see _Call."""
global _extracted_files # pylint: disable-msg=W0603
if not _extracted_files:
atexit.register(pkg_resources.cleanup_resources)
_extracted_files = True
return _Call(pkg_resources.resource_filename, name)

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python
# Copyright 2010 Google Inc. All Rights Reserved.
#
# 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.
"""Script for running Google-style applications.
Unlike normal scripts run through setuptools console_script entry points,
Google-style applications must be run as top-level scripts.
Given an already-imported module, users can use the RunScriptModule function to
set up the appropriate executable environment to spawn a new Python process to
run the module as a script.
To use this technique in your project, first create a module called e.g.
stubs.py with contents like:
from google.apputils import run_script_module
def RunMyScript():
import my.script
run_script_module.RunScriptModule(my.script)
def RunMyOtherScript():
import my.other_script
run_script_module.RunScriptModule(my.other_script)
Then, set up entry points in your setup.py that point to the functions in your
stubs module:
setup(
...
entry_points = {
'console_scripts': [
'my_script = my.stubs:RunMyScript',
'my_other_script = my.stubs.RunMyOtherScript',
],
},
)
When your project is installed, setuptools will generate minimal wrapper scripts
to call your stub functions, which in turn execv your script modules. That's it!
"""
from __future__ import print_function
__author__ = 'dborowitz@google.com (Dave Borowitz)'
import os
import re
import sys
def FindEnv(progname):
"""Find the program in the system path.
Args:
progname: The name of the program.
Returns:
The full pathname of the program.
Raises:
AssertionError: if the program was not found.
"""
for path in os.environ['PATH'].split(':'):
fullname = os.path.join(path, progname)
if os.access(fullname, os.X_OK):
return fullname
raise AssertionError(
"Could not find an executable named '%s' in the system path" % progname)
def GetPdbArgs(python):
"""Try to get the path to pdb.py and return it in a list.
Args:
python: The full path to a Python executable.
Returns:
A list of strings. If a relevant pdb.py was found, this will be
['/path/to/pdb.py']; if not, return ['-m', 'pdb'] and hope for the best.
(This latter technique will fail for Python 2.2.)
"""
# Usually, python is /usr/bin/pythonxx and pdb is /usr/lib/pythonxx/pdb.py
components = python.split('/')
if len(components) >= 2:
pdb_path = '/'.join(components[0:-2] + ['lib'] +
components[-1:] + ['pdb.py'])
if os.access(pdb_path, os.R_OK):
return [pdb_path]
# No pdb module found in the python path, default to -m pdb
return ['-m', 'pdb']
def StripDelimiters(s, beg, end):
if s[0] == beg:
assert s[-1] == end
return (s[1:-1], True)
else:
return (s, False)
def StripQuotes(s):
(s, stripped) = StripDelimiters(s, '"', '"')
if not stripped:
(s, stripped) = StripDelimiters(s, "'", "'")
return s
def PrintOurUsage():
"""Print usage for the stub script."""
print('Stub script %s (auto-generated). Options:' % sys.argv[0])
print ('--helpstub '
'Show help for stub script.')
print ('--debug_binary '
'Run python under debugger specified by --debugger.')
print ('--debugger=<debugger> '
"Debugger for --debug_binary. Default: 'gdb --args'.")
print ('--debug_script '
'Run wrapped script with python debugger module (pdb).')
print ('--show_command_and_exit '
'Print command which would be executed and exit.')
print ('These options must appear first in the command line, all others will '
'be passed to the wrapped script.')
def RunScriptModule(module):
"""Run a module as a script.
Locates the module's file and runs it in the current interpreter, or
optionally a debugger.
Args:
module: The module object to run.
"""
args = sys.argv[1:]
debug_binary = False
debugger = 'gdb --args'
debug_script = False
show_command_and_exit = False
while args:
if args[0] == '--helpstub':
PrintOurUsage()
sys.exit(0)
if args[0] == '--debug_binary':
debug_binary = True
args = args[1:]
continue
if args[0] == '--debug_script':
debug_script = True
args = args[1:]
continue
if args[0] == '--show_command_and_exit':
show_command_and_exit = True
args = args[1:]
continue
matchobj = re.match('--debugger=(.+)', args[0])
if matchobj is not None:
debugger = StripQuotes(matchobj.group(1))
args = args[1:]
continue
break
# Now look for my main python source file
# TODO(user): This will fail if the module was zipimported, which means
# no egg depending on this script runner can be zip_safe.
main_filename = module.__file__
assert os.path.exists(main_filename), ('Cannot exec() %r: file not found.' %
main_filename)
assert os.access(main_filename, os.R_OK), ('Cannot exec() %r: file not'
' readable.' % main_filename)
args = [main_filename] + args
if debug_binary:
debugger_args = debugger.split()
program = debugger_args[0]
# If pathname is not absolute, determine full path using PATH
if not os.path.isabs(program):
program = FindEnv(program)
python_path = sys.executable
command_vec = [python_path]
if debug_script:
command_vec.extend(GetPdbArgs(python_path))
args = [program] + debugger_args[1:] + command_vec + args
elif debug_script:
args = [sys.executable] + GetPdbArgs(program) + args
else:
program = sys.executable
args = [sys.executable] + args
if show_command_and_exit:
print('program: "%s"' % program)
print('args:', args)
sys.exit(0)
try:
sys.stdout.flush()
os.execv(program, args)
except EnvironmentError as e:
if not getattr(e, 'filename', None):
e.filename = program # Add info to error message
raise

View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python
# Copyright 2010 Google Inc. All Rights Reserved.
#
# 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.
"""Setuptools extension for running Google-style Python tests.
Google-style Python tests differ from normal Python tests in that each test
module is intended to be executed as an independent script. In particular, the
test fixture code in basetest.main() that executes module-wide setUp() and
tearDown() depends on __main__ being the module under test. This conflicts with
the usual setuptools test style, which uses a single TestSuite to run all of a
package's tests.
This package provides a new setuptools command, google_test, that runs all of
the google-style tests found in a specified directory.
NOTE: This works by overriding sys.modules['__main__'] with the module under
test, but still runs tests in the same process. Thus it will *not* work if your
tests depend on any of the following:
- Per-process (as opposed to per-module) initialization.
- Any entry point that is not basetest.main().
To use the google_test command in your project, do something like the following:
In setup.py:
setup(
name = "mypackage",
...
setup_requires = ["google-apputils>=0.2"],
google_test_dir = "tests",
)
Run:
$ python setup.py google_test
"""
from distutils import errors
import imp
import os
import re
import shlex
import sys
import traceback
from setuptools.command import test
def ValidateGoogleTestDir(unused_dist, unused_attr, value):
"""Validate that the test directory is a directory."""
if not os.path.isdir(value):
raise errors.DistutilsSetupError('%s is not a directory' % value)
class GoogleTest(test.test):
"""Command to run Google-style tests after in-place build."""
description = 'run Google-style tests after in-place build'
_DEFAULT_PATTERN = r'_(?:unit|reg)?test\.py$'
user_options = [
('test-dir=', 'd', 'Look for test modules in specified directory.'),
('test-module-pattern=', 'p',
('Pattern for matching test modules. Defaults to %r. '
'Only source files (*.py) will be considered, even if more files match '
'this pattern.' % _DEFAULT_PATTERN)),
('test-args=', 'a',
('Arguments to pass to basetest.main(). May only make sense if '
'test_module_pattern matches exactly one test.')),
]
def initialize_options(self):
self.test_dir = None
self.test_module_pattern = self._DEFAULT_PATTERN
self.test_args = ''
# Set to a dummy value, since we don't call the superclass methods for
# options parsing.
self.test_suite = True
def finalize_options(self):
if self.test_dir is None:
if self.distribution.google_test_dir:
self.test_dir = self.distribution.google_test_dir
else:
raise errors.DistutilsOptionError('No test directory specified')
self.test_module_pattern = re.compile(self.test_module_pattern)
self.test_args = shlex.split(self.test_args)
def _RunTestModule(self, module_path):
"""Run a module as a test module given its path.
Args:
module_path: The path to the module to test; must end in '.py'.
Returns:
True if the tests in this module pass, False if not or if an error occurs.
"""
path, filename = os.path.split(module_path)
old_argv = sys.argv[:]
old_path = sys.path[:]
old_modules = sys.modules.copy()
# Make relative imports in test modules work with our mangled sys.path.
sys.path.insert(0, path)
module_name = filename.replace('.py', '')
import_tuple = imp.find_module(module_name, [path])
module = imp.load_module(module_name, *import_tuple)
sys.modules['__main__'] = module
sys.argv = [module.__file__] + self.test_args
# Late import since this must be run with the project's sys.path.
import basetest
try:
try:
sys.stderr.write('Testing %s\n' % module_name)
basetest.main()
# basetest.main() should always call sys.exit, so this is very bad.
return False
except SystemExit as e:
returncode, = e.args
return not returncode
except:
traceback.print_exc()
return False
finally:
sys.argv[:] = old_argv
sys.path[:] = old_path
sys.modules.clear()
sys.modules.update(old_modules)
def run_tests(self):
ok = True
for path, _, filenames in os.walk(self.test_dir):
for filename in filenames:
if not filename.endswith('.py'):
continue
file_path = os.path.join(path, filename)
if self.test_module_pattern.search(file_path):
ok &= self._RunTestModule(file_path)
sys.exit(int(not ok))

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python
#
# Copyright 2003 Google Inc. All Rights Reserved.
"""Utility functions for dealing with command interpreters."""
import os
# Running windows?
win32 = (os.name == 'nt')
def ShellEscapeList(words):
"""Turn a list of words into a shell-safe string.
Args:
words: A list of words, e.g. for a command.
Returns:
A string of shell-quoted and space-separated words.
"""
if win32:
return ' '.join(words)
s = ''
for word in words:
# Single quote word, and replace each ' in word with '"'"'
s += "'" + word.replace("'", "'\"'\"'") + "' "
return s[:-1]
def ShellifyStatus(status):
"""Translate from a wait() exit status to a command shell exit status."""
if not win32:
if os.WIFEXITED(status):
# decode and return exit status
status = os.WEXITSTATUS(status)
else:
# On Unix, the wait() produces a 16 bit return code. Unix shells
# lossily compress this to an 8 bit value, using the formula below.
# Shell status code < 128 means the process exited normally, status
# code >= 128 means the process died because of a signal.
status = 128 + os.WTERMSIG(status)
return status

View File

@@ -0,0 +1,204 @@
#!/usr/bin/env python
# Copyright 2005 Google Inc. All Rights Reserved.
#
# 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.
"""A useful class for digesting, on a high-level, where time in a program goes.
Usage:
sw = StopWatch()
sw.start()
sw.start('foo')
foo()
sw.stop('foo')
args = overhead_code()
sw.start('bar')
bar(args)
sw.stop('bar')
sw.dump()
If you start a new timer when one is already running, then the other one will
stop running, and restart when you stop this timer. This behavior is very
useful for when you want to try timing for a subcall without remembering
what is already running. For instance:
sw.start('all_this')
do_some_stuff()
sw.start('just_that')
small_but_expensive_function()
sw.stop('just_that')
cleanup_code()
sw.stop('all_this')
In this case, the output will be what you want: the time spent in
small_but_expensive function will show up in the timer for just_that and not
all_this.
"""
import StringIO
import time
__owner__ = 'dbentley@google.com (Dan Bentley)'
class StopWatch(object):
"""Class encapsulating a timer; see above for example usage.
Instance variables:
timers: map of stopwatch name -> time for each currently running stopwatch,
where time is seconds from the epoch of when this stopwatch was
started.
accum: map of stopwatch name -> accumulated time, in seconds, it has
already been run for.
stopped: map of timer name -> list of timer names that are blocking it.
counters: map of timer name -> number of times it has been started.
"""
def __init__(self):
self.timers = {}
self.accum = {}
self.stopped = {}
self.counters = {}
def start(self, timer='total', stop_others=True):
"""Start a timer.
Args:
timer: str; name of the timer to start, defaults to the overall timer.
stop_others: bool; if True, stop all other running timers. If False, then
you can have time that is spent inside more than one timer
and there's a good chance that the overhead measured will be
negative.
"""
if stop_others:
stopped = []
for other in list(self.timers):
if not other == 'total':
self.stop(other)
stopped.append(other)
self.stopped[timer] = stopped
self.counters[timer] = self.counters.get(timer, 0) + 1
self.timers[timer] = time.time()
def stop(self, timer='total'):
"""Stop a running timer.
This includes restarting anything that was stopped on behalf of this timer.
Args:
timer: str; name of the timer to stop, defaults to the overall timer.
Raises:
RuntimeError: if timer refers to a timer that was never started.
"""
if timer not in self.timers:
raise RuntimeError(
'Tried to stop timer that was never started: %s' % timer)
self.accum[timer] = self.timervalue(timer)
del self.timers[timer]
for stopped in self.stopped.get(timer, []):
self.start(stopped, stop_others=0)
def timervalue(self, timer='total', now=None):
"""Return the value seen by this timer so far.
If the timer is stopped, this will be the accumulated time it has seen.
If the timer is running, this will be the time it has seen up to now.
If the timer has never been started, this will be zero.
Args:
timer: str; the name of the timer to report on.
now: long; if provided, the time to use for 'now' for running timers.
"""
if not now:
now = time.time()
if timer in self.timers:
# Timer is running now.
return self.accum.get(timer, 0.0) + (now - self.timers[timer])
elif timer in self.accum:
# Timer is stopped.
return self.accum[timer]
else:
# Timer is never started.
return 0.0
def overhead(self, now=None):
"""Calculate the overhead.
Args:
now: (optional) time to use as the current time.
Returns:
The overhead, that is, time spent in total but not in any sub timer. This
may be negative if time was counted in two sub timers. Avoid this by
always using stop_others.
"""
total = self.timervalue('total', now)
if total == 0.0:
return 0.0
all_timers = sum(self.accum.itervalues())
return total - (all_timers - total)
def results(self, verbose=False):
"""Get the results of this stopwatch.
Args:
verbose: bool; if True, show all times; otherwise, show only the total.
Returns:
A list of tuples showing the output of this stopwatch, of the form
(name, value, num_starts) for each timer. Note that if the total timer
is not used, non-verbose results will be the empty list.
"""
now = time.time()
all_names = self.accum.keys()
names = []
if 'total' in all_names:
all_names.remove('total')
all_names.sort()
if verbose:
names = all_names
results = [(name, self.timervalue(name, now=now), self.counters[name])
for name in names]
if verbose:
results.append(('overhead', self.overhead(now=now), 1))
if 'total' in self.accum or 'total' in self.timers:
results.append(('total', self.timervalue('total', now=now),
self.counters['total']))
return results
def dump(self, verbose=False):
"""Describes where time in this stopwatch was spent.
Args:
verbose: bool; if True, show all timers; otherwise, show only the total.
Returns:
A string describing the stopwatch.
"""
output = StringIO.StringIO()
results = self.results(verbose=verbose)
maxlength = max([len(result[0]) for result in results])
for result in results:
output.write('%*s: %6.2fs\n' % (maxlength, result[0], result[1]))
return output.getvalue()
# Create a stopwatch to be publicly used.
sw = StopWatch()