├── LICENSE ├── MANIFEST.in ├── README ├── ez_setup.py ├── google ├── __init__.py └── apputils │ ├── __init__.py │ ├── app.py │ ├── appcommands.py │ ├── basetest.py │ ├── datelib.py │ ├── debug.py │ ├── file_util.py │ ├── humanize.py │ ├── resources.py │ ├── run_script_module.py │ ├── setup_command.py │ ├── shellutil.py │ └── stopwatch.py ├── setup.py ├── tests ├── __init__.py ├── app_test.py ├── app_test_helper.py ├── app_unittest.sh ├── appcommands_example.py ├── appcommands_unittest.sh ├── basetest_sh_test.sh ├── basetest_test.py ├── data │ ├── a │ └── b ├── datelib_unittest.py ├── file_util_test.py ├── humanize_test.py ├── resources_test.py ├── sh_test.py ├── shellutil_unittest.py └── stopwatch_unittest.py └── tox.ini /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py 2 | include LICENSE 3 | include README 4 | recursive-include google *.py 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Google Application Utilities for Python 2 | ======================================= 3 | 4 | This project is a small collection of utilities for building Python 5 | applications. It includes some of the same set of utilities used to build and 6 | run internal Python apps at Google. 7 | 8 | Features: 9 | 10 | * Simple application startup integrated with python-gflags. 11 | * Subcommands for command-line applications. 12 | * Option to drop into pdb on uncaught exceptions. 13 | * Helper functions for dealing with files. 14 | * High-level profiling tools. 15 | * Timezone-aware wrappers for datetime.datetime classes. 16 | * Improved TestCase with the same methods as unittest2, plus helpful flags for 17 | test startup. 18 | * google_test setuptools command for running tests. 19 | * Helper module for creating application stubs. 20 | 21 | 22 | Installation 23 | ============ 24 | 25 | To install the package, simply run: 26 | python setup.py install 27 | 28 | 29 | Google-Style Tests 30 | ================== 31 | 32 | Google-style tests (those run with basetest.main()) differ from setuptools-style 33 | tests in that test modules are designed to be run as __main__. Setting up your 34 | project to use Google-style tests is easy: 35 | 36 | 1. Create one or more test modules named '*_test.py' in a directory. Each test 37 | module should have a main block that runs basetest.main(): 38 | # In tests/my_test.py 39 | from google.apputils import basetest 40 | 41 | class MyTest(basetest.TestCase): 42 | def testSomething(self): 43 | self.assertTrue('my test') 44 | 45 | if __name__ == '__main__': 46 | basetest.main() 47 | 48 | 2. Add a setup requirement on google-apputils and set the test_dir option: 49 | # In setup.py 50 | setup( 51 | ... 52 | setup_requires = ['google-apputils>=0.2'], 53 | test_dir = 'tests', 54 | ) 55 | 56 | 3. Run your tests: 57 | python setup.py google_test 58 | 59 | 60 | Google-Style Stub Scripts 61 | ========================= 62 | 63 | Google-style binaries (run with app.run()) are intended to be executed directly 64 | at the top level, so you should not use a setuptools console_script entry point 65 | to point at your main(). You can use distutils-style scripts if you want. 66 | 67 | Another alternative is to use google.apputils.run_script_module, which is a 68 | handy wrapper to execute a module directly as if it were a script: 69 | 70 | 1. Create a module like 'stubs.py' in your project: 71 | # In my/stubs.py 72 | from google.apputils import run_script_module 73 | 74 | def RunMyScript(): 75 | import my.script 76 | run_script_module.RunScriptModule(my.script) 77 | 78 | def RunMyOtherScript(): 79 | import my.other_script 80 | run_script_module.RunScriptModule(my.other_script) 81 | 82 | 2. Set up entry points in setup.py that point to the functions in your stubs 83 | module: 84 | # In setup.py 85 | setup( 86 | ... 87 | entry_points = { 88 | 'console_scripts': [ 89 | 'my_script = my.stubs:RunMyScript', 90 | 'my_other_script = my.stubs.RunMyOtherScript', 91 | ], 92 | }, 93 | ) 94 | 95 | There are also useful flags you can pass to your scripts to help you debug your 96 | binaries; run your binary with --helpstub to see the full list. 97 | -------------------------------------------------------------------------------- /ez_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Bootstrap setuptools installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from ez_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import sys 17 | DEFAULT_VERSION = "0.6c11" 18 | DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] 19 | 20 | md5_data = { 21 | 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', 22 | 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', 23 | 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', 24 | 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', 25 | 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', 26 | 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', 27 | 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', 28 | 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', 29 | 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 30 | 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 31 | 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 32 | 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 33 | 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 34 | 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 35 | 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', 36 | } 37 | 38 | import sys, os 39 | try: from hashlib import md5 40 | except ImportError: from md5 import md5 41 | 42 | def _validate_md5(egg_name, data): 43 | if egg_name in md5_data: 44 | digest = md5(data).hexdigest() 45 | if digest != md5_data[egg_name]: 46 | print >>sys.stderr, ( 47 | "md5 validation of %s failed! (Possible download problem?)" 48 | % egg_name 49 | ) 50 | sys.exit(2) 51 | return data 52 | 53 | def use_setuptools( 54 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, 55 | download_delay=15 56 | ): 57 | """Automatically find/download setuptools and make it available on sys.path 58 | 59 | `version` should be a valid setuptools version number that is available 60 | as an egg for download under the `download_base` URL (which should end with 61 | a '/'). `to_dir` is the directory where setuptools will be downloaded, if 62 | it is not already available. If `download_delay` is specified, it should 63 | be the number of seconds that will be paused before initiating a download, 64 | should one be required. If an older version of setuptools is installed, 65 | this routine will print a message to ``sys.stderr`` and raise SystemExit in 66 | an attempt to abort the calling script. 67 | """ 68 | was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules 69 | def do_download(): 70 | egg = download_setuptools(version, download_base, to_dir, download_delay) 71 | sys.path.insert(0, egg) 72 | import setuptools; setuptools.bootstrap_install_from = egg 73 | try: 74 | import pkg_resources 75 | except ImportError: 76 | return do_download() 77 | try: 78 | pkg_resources.require("setuptools>="+version); return 79 | except pkg_resources.VersionConflict, e: 80 | if was_imported: 81 | print >>sys.stderr, ( 82 | "The required version of setuptools (>=%s) is not available, and\n" 83 | "can't be installed while this script is running. Please install\n" 84 | " a more recent version first, using 'easy_install -U setuptools'." 85 | "\n\n(Currently using %r)" 86 | ) % (version, e.args[0]) 87 | sys.exit(2) 88 | except pkg_resources.DistributionNotFound: 89 | pass 90 | 91 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 92 | return do_download() 93 | 94 | def download_setuptools( 95 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, 96 | delay = 15 97 | ): 98 | """Download setuptools from a specified location and return its filename 99 | 100 | `version` should be a valid setuptools version number that is available 101 | as an egg for download under the `download_base` URL (which should end 102 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 103 | `delay` is the number of seconds to pause before an actual download attempt. 104 | """ 105 | import urllib2, shutil 106 | egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) 107 | url = download_base + egg_name 108 | saveto = os.path.join(to_dir, egg_name) 109 | src = dst = None 110 | if not os.path.exists(saveto): # Avoid repeated downloads 111 | try: 112 | from distutils import log 113 | if delay: 114 | log.warn(""" 115 | --------------------------------------------------------------------------- 116 | This script requires setuptools version %s to run (even to display 117 | help). I will attempt to download it for you (from 118 | %s), but 119 | you may need to enable firewall access for this script first. 120 | I will start the download in %d seconds. 121 | 122 | (Note: if this machine does not have network access, please obtain the file 123 | 124 | %s 125 | 126 | and place it in this directory before rerunning this script.) 127 | ---------------------------------------------------------------------------""", 128 | version, download_base, delay, url 129 | ); from time import sleep; sleep(delay) 130 | log.warn("Downloading %s", url) 131 | src = urllib2.urlopen(url) 132 | # Read/write all in one block, so we don't create a corrupt file 133 | # if the download is interrupted. 134 | data = _validate_md5(egg_name, src.read()) 135 | dst = open(saveto,"wb"); dst.write(data) 136 | finally: 137 | if src: src.close() 138 | if dst: dst.close() 139 | return os.path.realpath(saveto) 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | def main(argv, version=DEFAULT_VERSION): 177 | """Install or upgrade setuptools and EasyInstall""" 178 | try: 179 | import setuptools 180 | except ImportError: 181 | egg = None 182 | try: 183 | egg = download_setuptools(version, delay=0) 184 | sys.path.insert(0,egg) 185 | from setuptools.command.easy_install import main 186 | return main(list(argv)+[egg]) # we're done here 187 | finally: 188 | if egg and os.path.exists(egg): 189 | os.unlink(egg) 190 | else: 191 | if setuptools.__version__ == '0.0.1': 192 | print >>sys.stderr, ( 193 | "You have an obsolete version of setuptools installed. Please\n" 194 | "remove it from your system entirely before rerunning this script." 195 | ) 196 | sys.exit(2) 197 | 198 | req = "setuptools>="+version 199 | import pkg_resources 200 | try: 201 | pkg_resources.require(req) 202 | except pkg_resources.VersionConflict: 203 | try: 204 | from setuptools.command.easy_install import main 205 | except ImportError: 206 | from easy_install import main 207 | main(list(argv)+[download_setuptools(delay=0)]) 208 | sys.exit(0) # try to force an exit 209 | else: 210 | if argv: 211 | from setuptools.command.easy_install import main 212 | main(argv) 213 | else: 214 | print "Setuptools version",version,"or greater has been installed." 215 | print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' 216 | 217 | def update_md5(filenames): 218 | """Update our built-in md5 registry""" 219 | 220 | import re 221 | 222 | for name in filenames: 223 | base = os.path.basename(name) 224 | f = open(name,'rb') 225 | md5_data[base] = md5(f.read()).hexdigest() 226 | f.close() 227 | 228 | data = [" %r: %r,\n" % it for it in md5_data.items()] 229 | data.sort() 230 | repl = "".join(data) 231 | 232 | import inspect 233 | srcfile = inspect.getsourcefile(sys.modules[__name__]) 234 | f = open(srcfile, 'rb'); src = f.read(); f.close() 235 | 236 | match = re.search("\nmd5_data = {\n([^}]+)}", src) 237 | if not match: 238 | print >>sys.stderr, "Internal error!" 239 | sys.exit(2) 240 | 241 | src = src[:match.start(1)] + repl + src[match.end(1):] 242 | f = open(srcfile,'w') 243 | f.write(src) 244 | f.close() 245 | 246 | 247 | if __name__=='__main__': 248 | if len(sys.argv)>2 and sys.argv[1]=='--md5update': 249 | update_md5(sys.argv[2:]) 250 | else: 251 | main(sys.argv[1:]) 252 | -------------------------------------------------------------------------------- /google/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | try: 3 | import pkg_resources 4 | pkg_resources.declare_namespace(__name__) 5 | except ImportError: 6 | from pkgutil import extend_path 7 | __path__ = extend_path(__path__, __name__) 8 | -------------------------------------------------------------------------------- /google/apputils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | try: 3 | import pkg_resources 4 | pkg_resources.declare_namespace(__name__) 5 | except ImportError: 6 | from pkgutil import extend_path 7 | __path__ = extend_path(__path__, __name__) 8 | -------------------------------------------------------------------------------- /google/apputils/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2003 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Generic entry point for Google applications. 17 | 18 | To use this module, simply define a 'main' function with a single 19 | 'argv' argument and add the following to the end of your source file: 20 | 21 | if __name__ == '__main__': 22 | app.run() 23 | 24 | TODO(user): Remove silly main-detection logic, and force all clients 25 | of this module to check __name__ explicitly. Fix all current clients 26 | that don't check __name__. 27 | """ 28 | import errno 29 | import os 30 | import pdb 31 | import socket 32 | import stat 33 | import struct 34 | import sys 35 | import time 36 | import traceback 37 | import gflags as flags 38 | FLAGS = flags.FLAGS 39 | 40 | flags.DEFINE_boolean('run_with_pdb', 0, 'Set to true for PDB debug mode') 41 | flags.DEFINE_boolean('pdb_post_mortem', 0, 42 | 'Set to true to handle uncaught exceptions with PDB ' 43 | 'post mortem.') 44 | flags.DEFINE_boolean('run_with_profiling', 0, 45 | 'Set to true for profiling the script. ' 46 | 'Execution will be slower, and the output format might ' 47 | 'change over time.') 48 | flags.DEFINE_string('profile_file', None, 49 | 'Dump profile information to a file (for python -m ' 50 | 'pstats). Implies --run_with_profiling.') 51 | flags.DEFINE_boolean('use_cprofile_for_profiling', True, 52 | 'Use cProfile instead of the profile module for ' 53 | 'profiling. This has no effect unless ' 54 | '--run_with_profiling is set.') 55 | 56 | # If main() exits via an abnormal exception, call into these 57 | # handlers before exiting. 58 | 59 | EXCEPTION_HANDLERS = [] 60 | help_text_wrap = False # Whether to enable TextWrap in help output 61 | 62 | 63 | class Error(Exception): 64 | pass 65 | 66 | 67 | class UsageError(Error): 68 | """The arguments supplied by the user are invalid. 69 | 70 | Raise this when the arguments supplied are invalid from the point of 71 | view of the application. For example when two mutually exclusive 72 | flags have been supplied or when there are not enough non-flag 73 | arguments. It is distinct from flags.FlagsError which covers the lower 74 | level of parsing and validating individual flags. 75 | """ 76 | 77 | def __init__(self, message, exitcode=1): 78 | Error.__init__(self, message) 79 | self.exitcode = exitcode 80 | 81 | 82 | class HelpFlag(flags.BooleanFlag): 83 | """Special boolean flag that displays usage and raises SystemExit.""" 84 | NAME = 'help' 85 | 86 | def __init__(self): 87 | flags.BooleanFlag.__init__(self, self.NAME, 0, 'show this help', 88 | short_name='?', allow_override=1) 89 | 90 | def Parse(self, arg): 91 | if arg: 92 | usage(shorthelp=1, writeto_stdout=1) 93 | # Advertise --helpfull on stdout, since usage() was on stdout. 94 | print 95 | print 'Try --helpfull to get a list of all flags.' 96 | sys.exit(1) 97 | 98 | 99 | class HelpshortFlag(HelpFlag): 100 | """--helpshort is an alias for --help.""" 101 | NAME = 'helpshort' 102 | 103 | 104 | class HelpfullFlag(flags.BooleanFlag): 105 | """Display help for flags in this module and all dependent modules.""" 106 | 107 | def __init__(self): 108 | flags.BooleanFlag.__init__(self, 'helpfull', 0, 'show full help', 109 | allow_override=1) 110 | 111 | def Parse(self, arg): 112 | if arg: 113 | usage(writeto_stdout=1) 114 | sys.exit(1) 115 | 116 | 117 | class HelpXMLFlag(flags.BooleanFlag): 118 | """Similar to HelpfullFlag, but generates output in XML format.""" 119 | 120 | def __init__(self): 121 | flags.BooleanFlag.__init__(self, 'helpxml', False, 122 | 'like --helpfull, but generates XML output', 123 | allow_override=1) 124 | 125 | def Parse(self, arg): 126 | if arg: 127 | flags.FLAGS.WriteHelpInXMLFormat(sys.stdout) 128 | sys.exit(1) 129 | 130 | 131 | class BuildDataFlag(flags.BooleanFlag): 132 | """Boolean flag that writes build data to stdout and exits.""" 133 | 134 | def __init__(self): 135 | flags.BooleanFlag.__init__(self, 'show_build_data', 0, 136 | 'show build data and exit') 137 | 138 | def Parse(self, arg): 139 | if arg: 140 | sys.stdout.write(build_data.BuildData()) 141 | sys.exit(0) 142 | 143 | 144 | def parse_flags_with_usage(args): 145 | """Try parsing the flags, printing usage and exiting if unparseable.""" 146 | try: 147 | argv = FLAGS(args) 148 | return argv 149 | except flags.FlagsError, error: 150 | sys.stderr.write('FATAL Flags parsing error: %s\n' % error) 151 | sys.stderr.write('Pass --helpshort or --helpfull to see help on flags.\n') 152 | sys.exit(1) 153 | 154 | 155 | _define_help_flags_called = False 156 | 157 | 158 | def DefineHelpFlags(): 159 | """Register help flags. Idempotent.""" 160 | # Use a global to ensure idempotence. 161 | # pylint: disable=global-statement 162 | global _define_help_flags_called 163 | 164 | if not _define_help_flags_called: 165 | flags.DEFINE_flag(HelpFlag()) 166 | flags.DEFINE_flag(HelpshortFlag()) # alias for --help 167 | flags.DEFINE_flag(HelpfullFlag()) 168 | flags.DEFINE_flag(HelpXMLFlag()) 169 | flags.DEFINE_flag(BuildDataFlag()) 170 | _define_help_flags_called = True 171 | 172 | 173 | def RegisterAndParseFlagsWithUsage(): 174 | """Register help flags, parse arguments and show usage if appropriate. 175 | 176 | Returns: 177 | remaining arguments after flags parsing 178 | """ 179 | DefineHelpFlags() 180 | 181 | argv = parse_flags_with_usage(sys.argv) 182 | return argv 183 | 184 | 185 | def really_start(main=None): 186 | """Initializes flag values, and calls main with non-flag arguments. 187 | 188 | Only non-flag arguments are passed to main(). The return value of main() is 189 | used as the exit status. 190 | 191 | Args: 192 | main: Main function to run with the list of non-flag arguments, or None 193 | so that sys.modules['__main__'].main is to be used. 194 | """ 195 | argv = RegisterAndParseFlagsWithUsage() 196 | 197 | if main is None: 198 | main = sys.modules['__main__'].main 199 | 200 | try: 201 | if FLAGS.run_with_pdb: 202 | sys.exit(pdb.runcall(main, argv)) 203 | else: 204 | if FLAGS.run_with_profiling or FLAGS.profile_file: 205 | # Avoid import overhead since most apps (including performance-sensitive 206 | # ones) won't be run with profiling. 207 | import atexit 208 | if FLAGS.use_cprofile_for_profiling: 209 | import cProfile as profile 210 | else: 211 | import profile 212 | profiler = profile.Profile() 213 | if FLAGS.profile_file: 214 | atexit.register(profiler.dump_stats, FLAGS.profile_file) 215 | else: 216 | atexit.register(profiler.print_stats) 217 | retval = profiler.runcall(main, argv) 218 | sys.exit(retval) 219 | else: 220 | sys.exit(main(argv)) 221 | except UsageError, error: 222 | usage(shorthelp=1, detailed_error=error, exitcode=error.exitcode) 223 | except: 224 | if FLAGS.pdb_post_mortem: 225 | traceback.print_exc() 226 | pdb.post_mortem() 227 | raise 228 | 229 | 230 | def run(): 231 | """Begin executing the program. 232 | 233 | - Parses command line flags with the flag module. 234 | - If there are any errors, print usage(). 235 | - Calls main() with the remaining arguments. 236 | - If main() raises a UsageError, print usage and the error message. 237 | """ 238 | return _actual_start() 239 | 240 | 241 | def _actual_start(): 242 | """Another layer in the starting stack.""" 243 | # Get raw traceback 244 | tb = None 245 | try: 246 | raise ZeroDivisionError('') 247 | except ZeroDivisionError: 248 | tb = sys.exc_info()[2] 249 | assert tb 250 | 251 | # Look at previous stack frame's previous stack frame (previous 252 | # frame is run() or start(); the frame before that should be the 253 | # frame of the original caller, which should be __main__ or appcommands 254 | prev_prev_frame = tb.tb_frame.f_back.f_back 255 | if not prev_prev_frame: 256 | return 257 | prev_prev_name = prev_prev_frame.f_globals.get('__name__', None) 258 | if (prev_prev_name != '__main__' 259 | and not prev_prev_name.endswith('.appcommands')): 260 | return 261 | # just in case there's non-trivial stuff happening in __main__ 262 | del tb 263 | if hasattr(sys, 'exc_clear'): 264 | sys.exc_clear() # This functionality is gone in Python 3. 265 | 266 | try: 267 | really_start() 268 | except SystemExit, e: 269 | raise 270 | except Exception, e: 271 | # Call any installed exception handlers which may, for example, 272 | # log to a file or send email. 273 | for handler in EXCEPTION_HANDLERS: 274 | try: 275 | if handler.Wants(e): 276 | handler.Handle(e) 277 | except: 278 | # We don't want to stop for exceptions in the exception handlers but 279 | # we shouldn't hide them either. 280 | sys.stderr.write(traceback.format_exc()) 281 | raise 282 | # All handlers have had their chance, now die as we would have normally. 283 | raise 284 | 285 | 286 | def usage(shorthelp=0, writeto_stdout=0, detailed_error=None, exitcode=None): 287 | """Write __main__'s docstring to stderr with some help text. 288 | 289 | Args: 290 | shorthelp: print only flags from this module, rather than all flags. 291 | writeto_stdout: write help message to stdout, rather than to stderr. 292 | detailed_error: additional detail about why usage info was presented. 293 | exitcode: if set, exit with this status code after writing help. 294 | """ 295 | if writeto_stdout: 296 | stdfile = sys.stdout 297 | else: 298 | stdfile = sys.stderr 299 | 300 | doc = sys.modules['__main__'].__doc__ 301 | if not doc: 302 | doc = '\nUSAGE: %s [flags]\n' % sys.argv[0] 303 | doc = flags.TextWrap(doc, indent=' ', firstline_indent='') 304 | else: 305 | # Replace all '%s' with sys.argv[0], and all '%%' with '%'. 306 | num_specifiers = doc.count('%') - 2 * doc.count('%%') 307 | try: 308 | doc %= (sys.argv[0],) * num_specifiers 309 | except (OverflowError, TypeError, ValueError): 310 | # Just display the docstring as-is. 311 | pass 312 | if help_text_wrap: 313 | doc = flags.TextWrap(flags.DocToHelp(doc)) 314 | if shorthelp: 315 | flag_str = FLAGS.MainModuleHelp() 316 | else: 317 | flag_str = str(FLAGS) 318 | try: 319 | stdfile.write(doc) 320 | if flag_str: 321 | stdfile.write('\nflags:\n') 322 | stdfile.write(flag_str) 323 | stdfile.write('\n') 324 | if detailed_error is not None: 325 | stdfile.write('\n%s\n' % detailed_error) 326 | except IOError, e: 327 | # We avoid printing a huge backtrace if we get EPIPE, because 328 | # "foo.par --help | less" is a frequent use case. 329 | if e.errno != errno.EPIPE: 330 | raise 331 | if exitcode is not None: 332 | sys.exit(exitcode) 333 | 334 | 335 | class ExceptionHandler(object): 336 | """Base exception handler from which other may inherit.""" 337 | 338 | def Wants(self, unused_exc): 339 | """Check if this exception handler want to handle this exception. 340 | 341 | Args: 342 | unused_exc: Exception, the current exception 343 | 344 | Returns: 345 | boolean 346 | 347 | This base handler wants to handle all exceptions, override this 348 | method if you want to be more selective. 349 | """ 350 | return True 351 | 352 | def Handle(self, exc): 353 | """Do something with the current exception. 354 | 355 | Args: 356 | exc: Exception, the current exception 357 | 358 | This method must be overridden. 359 | """ 360 | raise NotImplementedError() 361 | 362 | 363 | def InstallExceptionHandler(handler): 364 | """Install an exception handler. 365 | 366 | Args: 367 | handler: an object conforming to the interface defined in ExceptionHandler 368 | 369 | Raises: 370 | TypeError: handler was not of the correct type 371 | 372 | All installed exception handlers will be called if main() exits via 373 | an abnormal exception, i.e. not one of SystemExit, KeyboardInterrupt, 374 | FlagsError or UsageError. 375 | """ 376 | if not isinstance(handler, ExceptionHandler): 377 | raise TypeError('handler of type %s does not inherit from ExceptionHandler' 378 | % type(handler)) 379 | EXCEPTION_HANDLERS.append(handler) 380 | -------------------------------------------------------------------------------- /google/apputils/appcommands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2007 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """This module is the base for programs that provide multiple commands. 17 | 18 | This provides command line tools that have a few shared global flags, 19 | followed by a command name, followed by command specific flags, 20 | then by arguments. That is: 21 | tool [--global_flags] command [--command_flags] [args] 22 | 23 | The module is built on top of app.py and 'overrides' a bit of it. However 24 | the interface is mostly the same. The main difference is that your main 25 | is supposed to register commands and return without further execution 26 | of the commands; pre checking is of course welcome! Also your 27 | global initialization should call appcommands.Run() rather than app.run(). 28 | 29 | To register commands use AddCmd() or AddCmdFunc(). AddCmd() is used 30 | for commands that derive from class Cmd and the AddCmdFunc() is used 31 | to wrap simple functions. 32 | 33 | This module itself registers the command 'help' that allows users 34 | to retrieve help for all or specific commands. 'help' is the default 35 | command executed if no command is expressed, unless a different default 36 | command is set with SetDefaultCommand. 37 | 38 | Example: 39 | 40 | 41 | from mx import DateTime 42 | 43 | 44 | class CmdDate(appcommands.Cmd): 45 | \"\"\"This docstring contains the help for the date command.\"\"\" 46 | 47 | def Run(self, argv): 48 | print DateTime.now() 49 | 50 | 51 | def main(argv): 52 | appcommands.AddCmd('date', CmdDate, command_aliases=['data_now']) 53 | 54 | 55 | if __name__ == '__main__': 56 | appcommands.Run() 57 | 58 | 59 | In the above example the name of the registered command on the command line is 60 | 'date'. Thus, to get the date you would execute: 61 | tool date 62 | The above example also added the command alias 'data_now' which allows to 63 | replace 'tool date' with 'tool data_now'. 64 | 65 | To get a list of available commands run: 66 | tool help 67 | For help with a specific command, you would execute: 68 | tool help date 69 | For help on flags run one of the following: 70 | tool --help 71 | Note that 'tool --help' gives you information on global flags, just like for 72 | applications that do not use appcommand. Likewise 'tool --helpshort' and the 73 | other help-flags from app.py are also available. 74 | 75 | The above example also demonstrates that you only have to call 76 | appcommands.Run() 77 | and register your commands in main() to initialize your program with appcommands 78 | (and app). 79 | 80 | Handling of flags: 81 | Flags can be registered just as with any other google tool using flags.py. 82 | In addition you can also provide command specific flags. To do so simply add 83 | flags registering code into the __init__ function of your Cmd classes passing 84 | parameter flag_values to any flags registering calls. These flags will get 85 | copied to the global flag list, so that once the command is detected they 86 | behave just like any other flag. That means these flags won't be available 87 | for other commands. Note that it is possible to register flags with more 88 | than one command. 89 | 90 | Getting help: 91 | This module activates formatting and wrapping to help output. That is 92 | the main difference to help created from app.py. So just as with app.py, 93 | appcommands.py will create help from the main modules main __doc__. 94 | But it adds the new 'help' command that allows you to get a list of 95 | all available commands. Each command's help will be followed by the 96 | registered command specific flags along with their defaults and help. 97 | After help for all commands there will also be a list of all registered 98 | global flags with their defaults and help. 99 | 100 | The text for the command's help can best be supplied by overwriting the 101 | __doc__ property of the Cmd classes for commands registered with AddCmd() or 102 | the __doc__ property of command functions registered AddCmdFunc(). 103 | 104 | Inner working: 105 | This module interacts with app.py by replacing its inner start dispatcher. 106 | The replacement version basically does the same, registering help flags, 107 | checking whether help flags were present, and calling the main module's main 108 | function. However unlike app.py, this module epxpects main() to only register 109 | commands and then to return. After having all commands registered 110 | appcommands.py will then parse the remaining arguments for any registered 111 | command. If one is found it will get executed. Otherwise a short usage info 112 | will be displayed. 113 | 114 | Each provided command must be an instance of Cmd. If commands get registered 115 | from global functions using AddCmdFunc() then the helper class _FunctionalCmd 116 | will be used in the registering process. 117 | """ 118 | 119 | 120 | 121 | import os 122 | import pdb 123 | import sys 124 | import traceback 125 | 126 | from google.apputils import app 127 | import gflags as flags 128 | 129 | FLAGS = flags.FLAGS 130 | 131 | 132 | # module exceptions: 133 | class AppCommandsError(Exception): 134 | """The base class for all flags errors.""" 135 | pass 136 | 137 | 138 | _cmd_argv = None # remaining arguments with index 0 = sys.argv[0] 139 | _cmd_list = {} # list of commands index by name (_Cmd instances) 140 | _cmd_alias_list = {} # list of command_names index by command_alias 141 | _cmd_default = 'help' # command to execute if none explicitly given 142 | 143 | 144 | def GetAppBasename(): 145 | """Returns the friendly basename of this application.""" 146 | return os.path.basename(sys.argv[0]) 147 | 148 | 149 | def ShortHelpAndExit(message=None): 150 | """Display optional message, followed by a note on how to get help, then exit. 151 | 152 | Args: 153 | message: optional message to display 154 | """ 155 | sys.stdout.flush() 156 | if message is not None: 157 | sys.stderr.write('%s\n' % message) 158 | sys.stderr.write("Run '%s help' to get help\n" % GetAppBasename()) 159 | sys.exit(1) 160 | 161 | 162 | def GetCommandList(): 163 | """Return list of registered commands.""" 164 | # pylint: disable=global-variable-not-assigned 165 | global _cmd_list 166 | return _cmd_list 167 | 168 | 169 | def GetCommandAliasList(): 170 | """Return list of registered command aliases.""" 171 | # pylint: disable=global-variable-not-assigned 172 | global _cmd_alias_list 173 | return _cmd_alias_list 174 | 175 | 176 | def GetFullCommandList(): 177 | """Return list of registered commands, including aliases.""" 178 | all_cmds = dict(GetCommandList()) 179 | for cmd_alias, cmd_name in GetCommandAliasList().iteritems(): 180 | all_cmds[cmd_alias] = all_cmds.get(cmd_name) 181 | return all_cmds 182 | 183 | 184 | def GetCommandByName(name): 185 | """Get the command or None if name is not a registered command. 186 | 187 | Args: 188 | name: name of command to look for 189 | 190 | Returns: 191 | Cmd instance holding the command or None 192 | """ 193 | return GetCommandList().get(GetCommandAliasList().get(name)) 194 | 195 | 196 | def GetCommandArgv(): 197 | """Return list of remaining args.""" 198 | return _cmd_argv 199 | 200 | 201 | def GetMaxCommandLength(): 202 | """Returns the length of the longest registered command.""" 203 | return max([len(cmd_name) for cmd_name in GetCommandList()]) 204 | 205 | 206 | class Cmd(object): 207 | """Abstract class describing and implementing a command. 208 | 209 | When creating code for a command, at least you have to derive this class 210 | and override method Run(). The other methods of this class might be 211 | overridden as well. Check their documentation for details. If the command 212 | needs any specific flags, use __init__ for registration. 213 | """ 214 | 215 | def __init__(self, name, flag_values, command_aliases=None, 216 | all_commands_help=None, help_full=None): 217 | """Initialize and check whether self is actually a Cmd instance. 218 | 219 | This can be used to register command specific flags. If you do so 220 | remember that you have to provide the 'flag_values=flag_values' 221 | parameter to any flags.DEFINE_*() call. 222 | 223 | Args: 224 | name: Name of the command 225 | flag_values: FlagValues() instance that needs to be passed as 226 | flag_values parameter to any flags registering call. 227 | command_aliases: A list of aliases that the command can be run as. 228 | all_commands_help: A short description of the command that is shown when 229 | the user requests help for all commands at once. 230 | help_full: A long description of the command and usage that is 231 | shown when the user requests help for just this 232 | command. If unspecified, the command's docstring is 233 | used instead. 234 | 235 | Raises: 236 | AppCommandsError: if self is Cmd (Cmd is abstract) 237 | """ 238 | self._command_name = name 239 | self._command_aliases = command_aliases 240 | self._command_flags = flag_values 241 | self._all_commands_help = all_commands_help 242 | self._help_full = help_full 243 | if type(self) is Cmd: 244 | raise AppCommandsError('Cmd is abstract and cannot be instantiated') 245 | 246 | def Run(self, unused_argv): 247 | """Execute the command. Must be provided by the implementing class. 248 | 249 | Args: 250 | unused_argv: Remaining command line arguments after parsing flags and 251 | command (in other words, a copy of sys.argv at the time of 252 | the function call with all parsed flags removed). 253 | 254 | Returns: 255 | 0 for success, anything else for failure (must return with integer). 256 | Alternatively you may return None (or not use a return statement at all). 257 | 258 | Raises: 259 | AppCommandsError: Always, as in must be overwritten 260 | """ 261 | raise AppCommandsError('%s.%s.Run() is not implemented' % ( 262 | type(self).__module__, type(self).__name__)) 263 | 264 | def CommandRun(self, argv): 265 | """Execute the command with given arguments. 266 | 267 | First register and parse additional flags. Then run the command. 268 | 269 | Returns: 270 | Command return value. 271 | 272 | Args: 273 | argv: Remaining command line arguments after parsing command and flags 274 | (that is a copy of sys.argv at the time of the function call with 275 | all parsed flags removed). 276 | """ 277 | # Register flags global when run normally 278 | FLAGS.AppendFlagValues(self._command_flags) 279 | # Prepare flags parsing, to redirect help, to show help for command 280 | orig_app_usage = app.usage 281 | 282 | def ReplacementAppUsage(shorthelp=0, writeto_stdout=1, detailed_error=None, 283 | exitcode=None): 284 | AppcommandsUsage(shorthelp, writeto_stdout, detailed_error, 285 | exitcode=exitcode, show_cmd=self._command_name, 286 | show_global_flags=True) 287 | app.usage = ReplacementAppUsage 288 | # Parse flags and restore app.usage afterwards 289 | try: 290 | try: 291 | argv = ParseFlagsWithUsage(argv) 292 | # Run command 293 | ret = self.Run(argv) 294 | if ret is None: 295 | ret = 0 296 | else: 297 | assert isinstance(ret, int) 298 | return ret 299 | except app.UsageError, error: 300 | app.usage(shorthelp=1, detailed_error=error, exitcode=error.exitcode) 301 | except: 302 | if FLAGS.pdb_post_mortem: 303 | traceback.print_exc() 304 | pdb.post_mortem() 305 | raise 306 | finally: 307 | # Restore app.usage and remove this command's flags from the global flags. 308 | app.usage = orig_app_usage 309 | for flag_name in self._command_flags.FlagDict(): 310 | delattr(FLAGS, flag_name) 311 | 312 | def CommandGetHelp(self, unused_argv, cmd_names=None): 313 | """Get help string for command. 314 | 315 | Args: 316 | unused_argv: Remaining command line flags and arguments after parsing 317 | command (that is a copy of sys.argv at the time of the 318 | function call with all parsed flags removed); unused in this 319 | default implementation, but may be used in subclasses. 320 | cmd_names: Complete list of commands for which help is being shown at 321 | the same time. This is used to determine whether to return 322 | _all_commands_help, or the command's docstring. 323 | (_all_commands_help is used, if not None, when help is being 324 | shown for more than one command, otherwise the command's 325 | docstring is used.) 326 | 327 | Returns: 328 | Help string, one of the following (by order): 329 | - Result of the registered 'help' function (if any) 330 | - Doc string of the Cmd class (if any) 331 | - Default fallback string 332 | """ 333 | if (type(cmd_names) is list and len(cmd_names) > 1 and 334 | self._all_commands_help is not None): 335 | return flags.DocToHelp(self._all_commands_help) 336 | elif self._help_full is not None: 337 | return flags.DocToHelp(self._help_full) 338 | elif self.__doc__: 339 | return flags.DocToHelp(self.__doc__) 340 | else: 341 | return 'No help available' 342 | 343 | def CommandGetAliases(self): 344 | """Get aliases for command. 345 | 346 | Returns: 347 | aliases: list of aliases for the command. 348 | """ 349 | return self._command_aliases 350 | 351 | def CommandGetName(self): 352 | """Get name of command. 353 | 354 | Returns: 355 | Command name. 356 | """ 357 | return self._command_name 358 | 359 | 360 | class _FunctionalCmd(Cmd): 361 | """Class to wrap functions as CMD instances. 362 | 363 | Args: 364 | cmd_func: command function 365 | """ 366 | 367 | def __init__(self, name, flag_values, cmd_func, **kargs): 368 | """Create a functional command. 369 | 370 | Args: 371 | name: Name of command 372 | flag_values: FlagValues() instance that needs to be passed as flag_values 373 | parameter to any flags registering call. 374 | cmd_func: Function to call when command is to be executed. 375 | **kargs: Additional keyword arguments to be passed to the 376 | superclass constructor. 377 | """ 378 | if 'help_full' not in kargs: 379 | kargs['help_full'] = cmd_func.__doc__ 380 | super(_FunctionalCmd, self).__init__(name, flag_values, **kargs) 381 | self._cmd_func = cmd_func 382 | 383 | def Run(self, argv): 384 | """Execute the command with given arguments. 385 | 386 | Args: 387 | argv: Remaining command line flags and arguments after parsing command 388 | (that is a copy of sys.argv at the time of the function call with 389 | all parsed flags removed). 390 | 391 | Returns: 392 | Command return value. 393 | """ 394 | return self._cmd_func(argv) 395 | 396 | 397 | def _AddCmdInstance(command_name, cmd, command_aliases=None, **_): 398 | """Add a command from a Cmd instance. 399 | 400 | Args: 401 | command_name: name of the command which will be used in argument parsing 402 | cmd: Cmd instance to register 403 | command_aliases: A list of command aliases that the command can be run as. 404 | 405 | Raises: 406 | AppCommandsError: If cmd is not a subclass of Cmd. 407 | AppCommandsError: If name is already registered OR name is not a string OR 408 | name is too short OR name does not start with a letter OR 409 | name contains any non alphanumeric characters besides 410 | '_'. 411 | """ 412 | # Update global command list. 413 | # pylint: disable=global-variable-not-assigned 414 | global _cmd_list 415 | global _cmd_alias_list 416 | if not issubclass(cmd.__class__, Cmd): 417 | raise AppCommandsError('Command must be an instance of commands.Cmd') 418 | 419 | for name in [command_name] + (command_aliases or []): 420 | _CheckCmdName(name) 421 | _cmd_alias_list[name] = command_name 422 | 423 | _cmd_list[command_name] = cmd 424 | 425 | 426 | def _CheckCmdName(name_or_alias): 427 | """Only allow strings for command names and aliases (reject unicode as well). 428 | 429 | Args: 430 | name_or_alias: properly formatted string name or alias. 431 | 432 | Raises: 433 | AppCommandsError: If name is already registered OR name is not a string OR 434 | name is too short OR name does not start with a letter OR 435 | name contains any non alphanumeric characters besides 436 | '_'. 437 | """ 438 | if name_or_alias in GetCommandAliasList(): 439 | raise AppCommandsError("Command or Alias '%s' already defined" % 440 | name_or_alias) 441 | if not isinstance(name_or_alias, str) or len(name_or_alias) <= 1: 442 | raise AppCommandsError("Command '%s' not a string or too short" 443 | % str(name_or_alias)) 444 | if not name_or_alias[0].isalpha(): 445 | raise AppCommandsError("Command '%s' does not start with a letter" 446 | % name_or_alias) 447 | if [c for c in name_or_alias if not (c.isalnum() or c == '_')]: 448 | raise AppCommandsError("Command '%s' contains non alphanumeric characters" 449 | % name_or_alias) 450 | 451 | 452 | def AddCmd(command_name, cmd_factory, **kwargs): 453 | """Add a command from a Cmd subclass or factory. 454 | 455 | Args: 456 | command_name: name of the command which will be used in argument parsing 457 | cmd_factory: A callable whose arguments match those of Cmd.__init__ and 458 | returns a Cmd. In the simplest case this is just a subclass 459 | of Cmd. 460 | **kwargs: Additional keyword arguments to be passed to the 461 | cmd_factory at initialization. Also passed to 462 | _AddCmdInstance to catch command_aliases. 463 | 464 | Raises: 465 | AppCommandsError: if calling cmd_factory does not return an instance of Cmd. 466 | """ 467 | cmd = cmd_factory(command_name, flags.FlagValues(), **kwargs) 468 | 469 | if not isinstance(cmd, Cmd): 470 | raise AppCommandsError('Command must be an instance of commands.Cmd') 471 | 472 | _AddCmdInstance(command_name, cmd, **kwargs) 473 | 474 | 475 | def AddCmdFunc(command_name, cmd_func, command_aliases=None, 476 | all_commands_help=None): 477 | """Add a new command to the list of registered commands. 478 | 479 | Args: 480 | command_name: name of the command which will be used in argument 481 | parsing 482 | cmd_func: command function, this function received the remaining 483 | arguments as its only parameter. It is supposed to do the 484 | command work and then return with the command result that 485 | is being used as the shell exit code. 486 | command_aliases: A list of command aliases that the command can be run as. 487 | all_commands_help: Help message to be displayed in place of func.__doc__ 488 | when all commands are displayed. 489 | """ 490 | _AddCmdInstance(command_name, 491 | _FunctionalCmd(command_name, flags.FlagValues(), cmd_func, 492 | command_aliases=command_aliases, 493 | all_commands_help=all_commands_help), 494 | command_aliases=command_aliases) 495 | 496 | 497 | class _CmdHelp(Cmd): 498 | """Standard help command. 499 | 500 | Allows to provide help for all or specific commands. 501 | """ 502 | 503 | def __init__(self, *args, **kwargs): 504 | if 'help_full' not in kwargs: 505 | kwargs['help_full'] = ( 506 | 'Help for all or selected command:\n' 507 | '\t%(prog)s help []\n\n' 508 | 'To retrieve help with global flags:\n' 509 | '\t%(prog)s --help\n\n' 510 | 'To retrieve help with flags only from the main module:\n' 511 | '\t%(prog)s --helpshort []\n\n' 512 | % {'prog': GetAppBasename()}) 513 | super(_CmdHelp, self).__init__(*args, **kwargs) 514 | 515 | def Run(self, argv): 516 | """Execute help command. 517 | 518 | If an argument is given and that argument is a registered command 519 | name, then help specific to that command is being displayed. 520 | If the command is unknown then a fatal error will be displayed. If 521 | no argument is present then help for all commands will be presented. 522 | 523 | If a specific command help is being generated, the list of commands is 524 | temporarily replaced with one containing only that command. Thus the call 525 | to usage() will only show help for that command. Otherwise call usage() 526 | will show help for all registered commands as it sees all commands. 527 | 528 | Args: 529 | argv: Remaining command line flags and arguments after parsing command 530 | (that is a copy of sys.argv at the time of the function call with 531 | all parsed flags removed). 532 | So argv[0] is the program and argv[1] will be the first argument to 533 | the call. For instance 'tool.py help command' will result in argv 534 | containing ('tool.py', 'command'). In this case the list of 535 | commands is searched for 'command'. 536 | 537 | Returns: 538 | 1 for failure 539 | """ 540 | if len(argv) > 1 and argv[1] in GetFullCommandList(): 541 | show_cmd = argv[1] 542 | else: 543 | show_cmd = None 544 | AppcommandsUsage(shorthelp=0, writeto_stdout=1, detailed_error=None, 545 | exitcode=1, show_cmd=show_cmd, show_global_flags=False) 546 | 547 | 548 | def GetSynopsis(): 549 | """Get synopsis for program. 550 | 551 | Returns: 552 | Synopsis including program basename. 553 | """ 554 | return '%s [--global_flags] [--command_flags] [args]' % ( 555 | GetAppBasename()) 556 | 557 | 558 | def _UsageFooter(detailed_error, cmd_names): 559 | """Output a footer at the end of usage or help output. 560 | 561 | Args: 562 | detailed_error: additional detail about why usage info was presented. 563 | cmd_names: list of command names for which help was shown or None. 564 | Returns: 565 | Generated footer that contains 'Run..' messages if appropriate. 566 | """ 567 | footer = [] 568 | if not cmd_names or len(cmd_names) == 1: 569 | footer.append("Run '%s help' to see the list of available commands." 570 | % GetAppBasename()) 571 | if not cmd_names or len(cmd_names) == len(GetCommandList()): 572 | footer.append("Run '%s help ' to get help for ." 573 | % GetAppBasename()) 574 | if detailed_error is not None: 575 | if footer: 576 | footer.append('') 577 | footer.append('%s' % detailed_error) 578 | return '\n'.join(footer) 579 | 580 | 581 | def AppcommandsUsage(shorthelp=0, writeto_stdout=0, detailed_error=None, 582 | exitcode=None, show_cmd=None, show_global_flags=False): 583 | """Output usage or help information. 584 | 585 | Extracts the __doc__ string from the __main__ module and writes it to 586 | stderr. If that string contains a '%s' then that is replaced by the command 587 | pathname. Otherwise a default usage string is being generated. 588 | 589 | The output varies depending on the following: 590 | - FLAGS.help 591 | - FLAGS.helpshort 592 | - show_cmd 593 | - show_global_flags 594 | 595 | Args: 596 | shorthelp: print only command and main module flags, rather than all. 597 | writeto_stdout: write help message to stdout, rather than to stderr. 598 | detailed_error: additional details about why usage info was presented. 599 | exitcode: if set, exit with this status code after writing help. 600 | show_cmd: show help for this command only (name of command). 601 | show_global_flags: show help for global flags. 602 | """ 603 | if writeto_stdout: 604 | stdfile = sys.stdout 605 | else: 606 | stdfile = sys.stderr 607 | 608 | prefix = ''.rjust(GetMaxCommandLength() + 2) 609 | # Deal with header, containing general tool documentation 610 | doc = sys.modules['__main__'].__doc__ 611 | if doc: 612 | help_msg = flags.DocToHelp(doc.replace('%s', sys.argv[0])) 613 | stdfile.write(flags.TextWrap(help_msg, flags.GetHelpWidth())) 614 | stdfile.write('\n\n\n') 615 | if not doc or doc.find('%s') == -1: 616 | synopsis = 'USAGE: ' + GetSynopsis() 617 | stdfile.write(flags.TextWrap(synopsis, flags.GetHelpWidth(), ' ', 618 | '')) 619 | stdfile.write('\n\n\n') 620 | # Special case just 'help' registered, that means run as 'tool --help'. 621 | if len(GetCommandList()) == 1: 622 | cmd_names = [] 623 | else: 624 | # Show list of commands 625 | if show_cmd is None or show_cmd == 'help': 626 | cmd_names = GetCommandList().keys() 627 | cmd_names.sort() 628 | stdfile.write('Any of the following commands:\n') 629 | doc = ', '.join(cmd_names) 630 | stdfile.write(flags.TextWrap(doc, flags.GetHelpWidth(), ' ')) 631 | stdfile.write('\n\n\n') 632 | # Prepare list of commands to show help for 633 | if show_cmd is not None: 634 | cmd_names = [show_cmd] # show only one command 635 | elif FLAGS.help or FLAGS.helpshort or shorthelp: 636 | cmd_names = [] 637 | else: 638 | cmd_names = GetCommandList().keys() # show all commands 639 | cmd_names.sort() 640 | # Show the command help (none, one specific, or all) 641 | for name in cmd_names: 642 | command = GetCommandByName(name) 643 | try: 644 | cmd_help = command.CommandGetHelp(GetCommandArgv(), cmd_names=cmd_names) 645 | except Exception as error: # pylint: disable=broad-except 646 | cmd_help = "Internal error for command '%s': %s." % (name, str(error)) 647 | cmd_help = cmd_help.strip() 648 | all_names = ', '.join( 649 | [command.CommandGetName()] + (command.CommandGetAliases() or [])) 650 | if len(all_names) + 1 >= len(prefix) or not cmd_help: 651 | # If command/alias list would reach over help block-indent 652 | # start the help block on a new line. 653 | stdfile.write(flags.TextWrap(all_names, flags.GetHelpWidth())) 654 | stdfile.write('\n') 655 | prefix1 = prefix 656 | else: 657 | prefix1 = all_names.ljust(GetMaxCommandLength() + 2) 658 | if cmd_help: 659 | stdfile.write(flags.TextWrap(cmd_help, flags.GetHelpWidth(), prefix, 660 | prefix1)) 661 | stdfile.write('\n\n') 662 | else: 663 | stdfile.write('\n') 664 | # When showing help for exactly one command we show its flags 665 | if len(cmd_names) == 1: 666 | # Need to register flags for command prior to be able to use them. 667 | # We do not register them globally so that they do not reappear. 668 | # pylint: disable=protected-access 669 | cmd_flags = command._command_flags 670 | if cmd_flags.RegisteredFlags(): 671 | stdfile.write('%sFlags for %s:\n' % (prefix, name)) 672 | stdfile.write(cmd_flags.GetHelp(prefix+' ')) 673 | stdfile.write('\n\n') 674 | stdfile.write('\n') 675 | # Now show global flags as asked for 676 | if show_global_flags: 677 | stdfile.write('Global flags:\n') 678 | if shorthelp: 679 | stdfile.write(FLAGS.MainModuleHelp()) 680 | else: 681 | stdfile.write(FLAGS.GetHelp()) 682 | stdfile.write('\n') 683 | else: 684 | stdfile.write("Run '%s --help' to get help for global flags." 685 | % GetAppBasename()) 686 | stdfile.write('\n%s\n' % _UsageFooter(detailed_error, cmd_names)) 687 | if exitcode is not None: 688 | sys.exit(exitcode) 689 | 690 | 691 | def ParseFlagsWithUsage(argv): 692 | """Parse the flags, exiting (after printing usage) if they are unparseable. 693 | 694 | Args: 695 | argv: command line arguments 696 | 697 | Returns: 698 | remaining command line arguments after parsing flags 699 | """ 700 | # Update the global commands. 701 | # pylint: disable=global-statement 702 | global _cmd_argv 703 | try: 704 | _cmd_argv = FLAGS(argv) 705 | return _cmd_argv 706 | except flags.FlagsError, error: 707 | ShortHelpAndExit('FATAL Flags parsing error: %s' % error) 708 | 709 | 710 | def GetCommand(command_required): 711 | """Get the command or return None (or issue an error) if there is none. 712 | 713 | Args: 714 | command_required: whether to issue an error if no command is present 715 | 716 | Returns: 717 | command or None, if command_required is True then return value is a valid 718 | command or the program will exit. The program also exits if a command was 719 | specified but that command does not exist. 720 | """ 721 | # Update the global commands. 722 | # pylint: disable=global-statement 723 | global _cmd_argv 724 | _cmd_argv = ParseFlagsWithUsage(_cmd_argv) 725 | if len(_cmd_argv) < 2: 726 | if command_required: 727 | ShortHelpAndExit('FATAL Command expected but none given') 728 | return None 729 | command = GetCommandByName(_cmd_argv[1]) 730 | if command is None: 731 | ShortHelpAndExit("FATAL Command '%s' unknown" % _cmd_argv[1]) 732 | del _cmd_argv[1] 733 | return command 734 | 735 | 736 | def SetDefaultCommand(default_command): 737 | """Change the default command to execute if none is explicitly given. 738 | 739 | Args: 740 | default_command: str, the name of the command to execute by default. 741 | """ 742 | # pylint: disable=global-statement,g-bad-name 743 | global _cmd_default 744 | _cmd_default = default_command 745 | 746 | 747 | def _CommandsStart(unused_argv): 748 | """Main initialization. 749 | 750 | Calls __main__.main(), and then the command indicated by the first 751 | non-flag argument, or 'help' if no argument was given. (The command 752 | to execute if no flag is given can be changed via SetDefaultCommand). 753 | 754 | Only non-flag arguments are passed to main(). If main does not call 755 | sys.exit, the return value of the command is used as the exit status. 756 | """ 757 | # The following is supposed to return after registering additional commands 758 | try: 759 | sys.modules['__main__'].main(GetCommandArgv()) 760 | # If sys.exit was called, return with error code. 761 | except SystemExit, e: 762 | sys.exit(e.code) 763 | except Exception, error: 764 | traceback.print_exc() # Print a backtrace to stderr. 765 | ShortHelpAndExit('\nFATAL error in main: %s' % error) 766 | 767 | if len(GetCommandArgv()) > 1: 768 | command = GetCommand(command_required=True) 769 | else: 770 | command = GetCommandByName(_cmd_default) 771 | if command is None: 772 | ShortHelpAndExit("FATAL Command '%s' unknown" % _cmd_default) 773 | sys.exit(command.CommandRun(GetCommandArgv())) 774 | 775 | 776 | def Run(): 777 | """This must be called from __main__ modules main, instead of app.run(). 778 | 779 | app.run will base its actions on its stacktrace. 780 | 781 | Returns: 782 | app.run() 783 | """ 784 | app.parse_flags_with_usage = ParseFlagsWithUsage 785 | original_really_start = app.really_start 786 | 787 | def InterceptReallyStart(): 788 | original_really_start(main=_CommandsStart) 789 | app.really_start = InterceptReallyStart 790 | app.usage = _ReplacementAppUsage 791 | return app.run() 792 | 793 | 794 | # Always register 'help' command 795 | AddCmd('help', _CmdHelp) 796 | 797 | 798 | def _ReplacementAppUsage(shorthelp=0, writeto_stdout=0, detailed_error=None, 799 | exitcode=None): 800 | AppcommandsUsage(shorthelp, writeto_stdout, detailed_error, exitcode=exitcode, 801 | show_cmd=None, show_global_flags=True) 802 | 803 | 804 | if __name__ == '__main__': 805 | Run() 806 | -------------------------------------------------------------------------------- /google/apputils/datelib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2002 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Set of classes and functions for dealing with dates and timestamps. 17 | 18 | The BaseTimestamp and Timestamp are timezone-aware wrappers around Python 19 | datetime.datetime class. 20 | """ 21 | 22 | 23 | 24 | import calendar 25 | import copy 26 | import datetime 27 | import re 28 | import sys 29 | import time 30 | import types 31 | import warnings 32 | 33 | import dateutil.parser 34 | import pytz 35 | 36 | 37 | _MICROSECONDS_PER_SECOND = 1000000 38 | _MICROSECONDS_PER_SECOND_F = float(_MICROSECONDS_PER_SECOND) 39 | 40 | 41 | def SecondsToMicroseconds(seconds): 42 | """Convert seconds to microseconds. 43 | 44 | Args: 45 | seconds: number 46 | Returns: 47 | microseconds 48 | """ 49 | return seconds * _MICROSECONDS_PER_SECOND 50 | 51 | 52 | def MicrosecondsToSeconds(microseconds): 53 | """Convert microseconds to seconds. 54 | 55 | Args: 56 | microseconds: A number representing some duration of time measured in 57 | microseconds. 58 | Returns: 59 | A number representing the same duration of time measured in seconds. 60 | """ 61 | return microseconds / _MICROSECONDS_PER_SECOND_F 62 | 63 | 64 | def _GetCurrentTimeMicros(): 65 | """Get the current time in microseconds, in UTC. 66 | 67 | Returns: 68 | The number of microseconds since the epoch. 69 | """ 70 | return int(SecondsToMicroseconds(time.time())) 71 | 72 | 73 | def GetSecondsSinceEpoch(time_tuple): 74 | """Convert time_tuple (in UTC) to seconds (also in UTC). 75 | 76 | Args: 77 | time_tuple: tuple with at least 6 items. 78 | Returns: 79 | seconds. 80 | """ 81 | return calendar.timegm(time_tuple[:6] + (0, 0, 0)) 82 | 83 | 84 | def GetTimeMicros(time_tuple): 85 | """Get a time in microseconds. 86 | 87 | Arguments: 88 | time_tuple: A (year, month, day, hour, minute, second) tuple (the python 89 | time tuple format) in the UTC time zone. 90 | 91 | Returns: 92 | The number of microseconds since the epoch represented by the input tuple. 93 | """ 94 | return int(SecondsToMicroseconds(GetSecondsSinceEpoch(time_tuple))) 95 | 96 | 97 | def DatetimeToUTCMicros(date): 98 | """Converts a datetime object to microseconds since the epoch in UTC. 99 | 100 | Args: 101 | date: A datetime to convert. 102 | Returns: 103 | The number of microseconds since the epoch, in UTC, represented by the input 104 | datetime. 105 | """ 106 | # Using this guide: http://wiki.python.org/moin/WorkingWithTime 107 | # And this conversion guide: http://docs.python.org/library/time.html 108 | 109 | # Turn the date parameter into a tuple (struct_time) that can then be 110 | # manipulated into a long value of seconds. During the conversion from 111 | # struct_time to long, the source date in UTC, and so it follows that the 112 | # correct transformation is calendar.timegm() 113 | micros = calendar.timegm(date.utctimetuple()) * _MICROSECONDS_PER_SECOND 114 | return micros + date.microsecond 115 | 116 | 117 | def DatetimeToUTCMillis(date): 118 | """Converts a datetime object to milliseconds since the epoch in UTC. 119 | 120 | Args: 121 | date: A datetime to convert. 122 | Returns: 123 | The number of milliseconds since the epoch, in UTC, represented by the input 124 | datetime. 125 | """ 126 | return DatetimeToUTCMicros(date) / 1000 127 | 128 | 129 | def UTCMicrosToDatetime(micros, tz=None): 130 | """Converts a microsecond epoch time to a datetime object. 131 | 132 | Args: 133 | micros: A UTC time, expressed in microseconds since the epoch. 134 | tz: The desired tzinfo for the datetime object. If None, the 135 | datetime will be naive. 136 | Returns: 137 | The datetime represented by the input value. 138 | """ 139 | # The conversion from micros to seconds for input into the 140 | # utcfromtimestamp function needs to be done as a float to make sure 141 | # we dont lose the sub-second resolution of the input time. 142 | dt = datetime.datetime.utcfromtimestamp( 143 | micros / _MICROSECONDS_PER_SECOND_F) 144 | if tz is not None: 145 | dt = tz.fromutc(dt) 146 | return dt 147 | 148 | 149 | def UTCMillisToDatetime(millis, tz=None): 150 | """Converts a millisecond epoch time to a datetime object. 151 | 152 | Args: 153 | millis: A UTC time, expressed in milliseconds since the epoch. 154 | tz: The desired tzinfo for the datetime object. If None, the 155 | datetime will be naive. 156 | Returns: 157 | The datetime represented by the input value. 158 | """ 159 | return UTCMicrosToDatetime(millis * 1000, tz) 160 | 161 | 162 | UTC = pytz.UTC 163 | US_PACIFIC = pytz.timezone('US/Pacific') 164 | 165 | 166 | class TimestampError(ValueError): 167 | """Generic timestamp-related error.""" 168 | pass 169 | 170 | 171 | class TimezoneNotSpecifiedError(TimestampError): 172 | """This error is raised when timezone is not specified.""" 173 | pass 174 | 175 | 176 | class TimeParseError(TimestampError): 177 | """This error is raised when we can't parse the input.""" 178 | pass 179 | 180 | 181 | # TODO(user): this class needs to handle daylight better 182 | 183 | 184 | class LocalTimezoneClass(datetime.tzinfo): 185 | """This class defines local timezone.""" 186 | 187 | ZERO = datetime.timedelta(0) 188 | HOUR = datetime.timedelta(hours=1) 189 | 190 | STDOFFSET = datetime.timedelta(seconds=-time.timezone) 191 | if time.daylight: 192 | DSTOFFSET = datetime.timedelta(seconds=-time.altzone) 193 | else: 194 | DSTOFFSET = STDOFFSET 195 | 196 | DSTDIFF = DSTOFFSET - STDOFFSET 197 | 198 | def utcoffset(self, dt): 199 | """datetime -> minutes east of UTC (negative for west of UTC).""" 200 | if self._isdst(dt): 201 | return self.DSTOFFSET 202 | else: 203 | return self.STDOFFSET 204 | 205 | def dst(self, dt): 206 | """datetime -> DST offset in minutes east of UTC.""" 207 | if self._isdst(dt): 208 | return self.DSTDIFF 209 | else: 210 | return self.ZERO 211 | 212 | def tzname(self, dt): 213 | """datetime -> string name of time zone.""" 214 | return time.tzname[self._isdst(dt)] 215 | 216 | def _isdst(self, dt): 217 | """Return true if given datetime is within local DST.""" 218 | tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, 219 | dt.weekday(), 0, -1) 220 | stamp = time.mktime(tt) 221 | tt = time.localtime(stamp) 222 | return tt.tm_isdst > 0 223 | 224 | def __repr__(self): 225 | """Return string ''.""" 226 | return '' 227 | 228 | def localize(self, dt, unused_is_dst=False): 229 | """Convert naive time to local time.""" 230 | if dt.tzinfo is not None: 231 | raise ValueError('Not naive datetime (tzinfo is already set)') 232 | return dt.replace(tzinfo=self) 233 | 234 | def normalize(self, dt, unused_is_dst=False): 235 | """Correct the timezone information on the given datetime.""" 236 | if dt.tzinfo is None: 237 | raise ValueError('Naive time - no tzinfo set') 238 | return dt.replace(tzinfo=self) 239 | 240 | 241 | LocalTimezone = LocalTimezoneClass() 242 | 243 | 244 | class BaseTimestamp(datetime.datetime): 245 | """Our kind of wrapper over datetime.datetime. 246 | 247 | The objects produced by methods now, today, fromtimestamp, utcnow, 248 | utcfromtimestamp are timezone-aware (with correct timezone). 249 | We also overload __add__ and __sub__ method, to fix the result of arithmetic 250 | operations. 251 | """ 252 | 253 | LocalTimezone = LocalTimezone 254 | 255 | @classmethod 256 | def AddLocalTimezone(cls, obj): 257 | """If obj is naive, add local timezone to it.""" 258 | if not obj.tzinfo: 259 | return obj.replace(tzinfo=cls.LocalTimezone) 260 | return obj 261 | 262 | @classmethod 263 | def Localize(cls, obj): 264 | """If obj is naive, localize it to cls.LocalTimezone.""" 265 | if not obj.tzinfo: 266 | return cls.LocalTimezone.localize(obj) 267 | return obj 268 | 269 | def __add__(self, *args, **kwargs): 270 | """x.__add__(y) <==> x+y.""" 271 | r = super(BaseTimestamp, self).__add__(*args, **kwargs) 272 | return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second, 273 | r.microsecond, r.tzinfo) 274 | 275 | def __sub__(self, *args, **kwargs): 276 | """x.__add__(y) <==> x-y.""" 277 | r = super(BaseTimestamp, self).__sub__(*args, **kwargs) 278 | if isinstance(r, datetime.datetime): 279 | return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second, 280 | r.microsecond, r.tzinfo) 281 | return r 282 | 283 | @classmethod 284 | def now(cls, *args, **kwargs): 285 | """Get a timestamp corresponding to right now. 286 | 287 | Args: 288 | args: Positional arguments to pass to datetime.datetime.now(). 289 | kwargs: Keyword arguments to pass to datetime.datetime.now(). If tz is not 290 | specified, local timezone is assumed. 291 | 292 | Returns: 293 | A new BaseTimestamp with tz's local day and time. 294 | """ 295 | return cls.AddLocalTimezone( 296 | super(BaseTimestamp, cls).now(*args, **kwargs)) 297 | 298 | @classmethod 299 | def today(cls): 300 | """Current BaseTimestamp. 301 | 302 | Same as self.__class__.fromtimestamp(time.time()). 303 | Returns: 304 | New self.__class__. 305 | """ 306 | return cls.AddLocalTimezone(super(BaseTimestamp, cls).today()) 307 | 308 | @classmethod 309 | def fromtimestamp(cls, *args, **kwargs): 310 | """Get a new localized timestamp from a POSIX timestamp. 311 | 312 | Args: 313 | args: Positional arguments to pass to datetime.datetime.fromtimestamp(). 314 | kwargs: Keyword arguments to pass to datetime.datetime.fromtimestamp(). 315 | If tz is not specified, local timezone is assumed. 316 | 317 | Returns: 318 | A new BaseTimestamp with tz's local day and time. 319 | """ 320 | return cls.Localize( 321 | super(BaseTimestamp, cls).fromtimestamp(*args, **kwargs)) 322 | 323 | @classmethod 324 | def utcnow(cls): 325 | """Return a new BaseTimestamp representing UTC day and time.""" 326 | return super(BaseTimestamp, cls).utcnow().replace(tzinfo=pytz.utc) 327 | 328 | @classmethod 329 | def utcfromtimestamp(cls, *args, **kwargs): 330 | """timestamp -> UTC datetime from a POSIX timestamp (like time.time()).""" 331 | return super(BaseTimestamp, cls).utcfromtimestamp( 332 | *args, **kwargs).replace(tzinfo=pytz.utc) 333 | 334 | @classmethod 335 | def strptime(cls, date_string, format, tz=None): 336 | """Parse date_string according to format and construct BaseTimestamp. 337 | 338 | Args: 339 | date_string: string passed to time.strptime. 340 | format: format string passed to time.strptime. 341 | tz: if not specified, local timezone assumed. 342 | Returns: 343 | New BaseTimestamp. 344 | """ 345 | date_time = super(BaseTimestamp, cls).strptime(date_string, format) 346 | return (tz.localize if tz else cls.Localize)(date_time) 347 | 348 | def astimezone(self, *args, **kwargs): 349 | """tz -> convert to time in new timezone tz.""" 350 | r = super(BaseTimestamp, self).astimezone(*args, **kwargs) 351 | return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second, 352 | r.microsecond, r.tzinfo) 353 | 354 | @classmethod 355 | def FromMicroTimestamp(cls, ts): 356 | """Create new Timestamp object from microsecond UTC timestamp value. 357 | 358 | Args: 359 | ts: integer microsecond UTC timestamp 360 | Returns: 361 | New cls() 362 | """ 363 | return cls.utcfromtimestamp(ts/_MICROSECONDS_PER_SECOND_F) 364 | 365 | def AsSecondsSinceEpoch(self): 366 | """Return number of seconds since epoch (timestamp in seconds).""" 367 | return GetSecondsSinceEpoch(self.utctimetuple()) 368 | 369 | def AsMicroTimestamp(self): 370 | """Return microsecond timestamp constructed from this object.""" 371 | return (SecondsToMicroseconds(self.AsSecondsSinceEpoch()) + 372 | self.microsecond) 373 | 374 | @classmethod 375 | def combine(cls, datepart, timepart, tz=None): 376 | """Combine date and time into timestamp, timezone-aware. 377 | 378 | Args: 379 | datepart: datetime.date 380 | timepart: datetime.time 381 | tz: timezone or None 382 | Returns: 383 | timestamp object 384 | """ 385 | result = super(BaseTimestamp, cls).combine(datepart, timepart) 386 | if tz: 387 | result = tz.localize(result) 388 | return result 389 | 390 | 391 | # Conversions from interval suffixes to number of seconds. 392 | # (m => 60s, d => 86400s, etc) 393 | _INTERVAL_CONV_DICT = {'s': 1} 394 | _INTERVAL_CONV_DICT['m'] = 60 * _INTERVAL_CONV_DICT['s'] 395 | _INTERVAL_CONV_DICT['h'] = 60 * _INTERVAL_CONV_DICT['m'] 396 | _INTERVAL_CONV_DICT['d'] = 24 * _INTERVAL_CONV_DICT['h'] 397 | _INTERVAL_CONV_DICT['D'] = _INTERVAL_CONV_DICT['d'] 398 | _INTERVAL_CONV_DICT['w'] = 7 * _INTERVAL_CONV_DICT['d'] 399 | _INTERVAL_CONV_DICT['W'] = _INTERVAL_CONV_DICT['w'] 400 | _INTERVAL_CONV_DICT['M'] = 30 * _INTERVAL_CONV_DICT['d'] 401 | _INTERVAL_CONV_DICT['Y'] = 365 * _INTERVAL_CONV_DICT['d'] 402 | _INTERVAL_REGEXP = re.compile('^([0-9]+)([%s])?' % ''.join(_INTERVAL_CONV_DICT)) 403 | 404 | 405 | def ConvertIntervalToSeconds(interval): 406 | """Convert a formatted string representing an interval into seconds. 407 | 408 | Args: 409 | interval: String to interpret as an interval. A basic interval looks like 410 | "". Complex intervals consisting of a chain of basic 411 | intervals are also allowed. 412 | 413 | Returns: 414 | An integer representing the number of seconds represented by the interval 415 | string, or None if the interval string could not be decoded. 416 | """ 417 | total = 0 418 | while interval: 419 | match = _INTERVAL_REGEXP.match(interval) 420 | if not match: 421 | return None 422 | 423 | try: 424 | num = int(match.group(1)) 425 | except ValueError: 426 | return None 427 | 428 | suffix = match.group(2) 429 | if suffix: 430 | multiplier = _INTERVAL_CONV_DICT.get(suffix) 431 | if not multiplier: 432 | return None 433 | num *= multiplier 434 | 435 | total += num 436 | interval = interval[match.end(0):] 437 | return total 438 | 439 | 440 | class Timestamp(BaseTimestamp): 441 | """This subclass contains methods to parse W3C and interval date spec. 442 | 443 | The interval date specification is in the form "1D", where "D" can be 444 | "s"econds "m"inutes "h"ours "D"ays "W"eeks "M"onths "Y"ears. 445 | """ 446 | INTERVAL_CONV_DICT = _INTERVAL_CONV_DICT 447 | INTERVAL_REGEXP = _INTERVAL_REGEXP 448 | 449 | @classmethod 450 | def _StringToTime(cls, timestring, tz=None): 451 | """Use dateutil.parser to convert string into timestamp. 452 | 453 | dateutil.parser understands ISO8601 which is really handy. 454 | 455 | Args: 456 | timestring: string with datetime 457 | tz: optional timezone, if timezone is omitted from timestring. 458 | 459 | Returns: 460 | New Timestamp or None if unable to parse the timestring. 461 | """ 462 | try: 463 | r = dateutil.parser.parse(timestring) 464 | # dateutil will raise ValueError if it's an unknown format -- or 465 | # TypeError in some cases, due to bugs. 466 | except (TypeError, ValueError): 467 | return None 468 | if not r.tzinfo: 469 | r = (tz or cls.LocalTimezone).localize(r) 470 | result = cls(r.year, r.month, r.day, r.hour, r.minute, r.second, 471 | r.microsecond, r.tzinfo) 472 | 473 | return result 474 | 475 | @classmethod 476 | def _IntStringToInterval(cls, timestring): 477 | """Parse interval date specification and create a timedelta object. 478 | 479 | Args: 480 | timestring: string interval. 481 | 482 | Returns: 483 | A datetime.timedelta representing the specified interval or None if 484 | unable to parse the timestring. 485 | """ 486 | seconds = ConvertIntervalToSeconds(timestring) 487 | return datetime.timedelta(seconds=seconds) if seconds else None 488 | 489 | @classmethod 490 | def FromString(cls, value, tz=None): 491 | """Create a Timestamp from a string. 492 | 493 | Args: 494 | value: String interval or datetime. 495 | e.g. "2013-01-05 13:00:00" or "1d" 496 | tz: optional timezone, if timezone is omitted from timestring. 497 | 498 | Returns: 499 | A new Timestamp. 500 | 501 | Raises: 502 | TimeParseError if unable to parse value. 503 | """ 504 | result = cls._StringToTime(value, tz=tz) 505 | if result: 506 | return result 507 | 508 | result = cls._IntStringToInterval(value) 509 | if result: 510 | return cls.utcnow() - result 511 | 512 | raise TimeParseError(value) 513 | 514 | 515 | # What's written below is a clear python bug. I mean, okay, I can apply 516 | # negative timezone to it and end result will be inconversible. 517 | 518 | MAXIMUM_PYTHON_TIMESTAMP = Timestamp( 519 | 9999, 12, 31, 23, 59, 59, 999999, UTC) 520 | 521 | # This is also a bug. It is called 32bit time_t. I hate it. 522 | # This is fixed in 2.5, btw. 523 | 524 | MAXIMUM_MICROSECOND_TIMESTAMP = 0x80000000 * _MICROSECONDS_PER_SECOND - 1 525 | MAXIMUM_MICROSECOND_TIMESTAMP_AS_TS = Timestamp(2038, 1, 19, 3, 14, 7, 999999) 526 | -------------------------------------------------------------------------------- /google/apputils/debug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2004 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # This code must be source compatible with Python 2.6 through 3.3. 17 | 18 | """Import this module to add a hook to call pdb on uncaught exceptions. 19 | 20 | To enable this, do the following in your top-level application: 21 | 22 | import google.apputils.debug 23 | 24 | and then in your main(): 25 | 26 | google.apputils.debug.Init() 27 | 28 | Then run your program with --pdb. 29 | """ 30 | 31 | 32 | 33 | import sys 34 | 35 | import gflags as flags 36 | 37 | flags.DEFINE_boolean('pdb', 0, 'Drop into pdb on uncaught exceptions') 38 | 39 | old_excepthook = None 40 | 41 | 42 | def _DebugHandler(exc_class, value, tb): 43 | if not flags.FLAGS.pdb or hasattr(sys, 'ps1') or not sys.stderr.isatty(): 44 | # we aren't in interactive mode or we don't have a tty-like 45 | # device, so we call the default hook 46 | old_excepthook(exc_class, value, tb) 47 | else: 48 | # Don't impose import overhead on apps that never raise an exception. 49 | import traceback 50 | import pdb 51 | # we are in interactive mode, print the exception... 52 | traceback.print_exception(exc_class, value, tb) 53 | sys.stdout.write('\n') 54 | # ...then start the debugger in post-mortem mode. 55 | pdb.pm() 56 | 57 | 58 | def Init(): 59 | # Must back up old excepthook. 60 | global old_excepthook # pylint: disable=global-statement 61 | if old_excepthook is None: 62 | old_excepthook = sys.excepthook 63 | sys.excepthook = _DebugHandler 64 | -------------------------------------------------------------------------------- /google/apputils/file_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2007 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Simple file system utilities.""" 17 | 18 | __author__ = ('elaforge@google.com (Evan LaForge)', 19 | 'matthewb@google.com (Matthew Blecker)') 20 | 21 | import contextlib 22 | import errno 23 | import os 24 | import pwd 25 | import shutil 26 | import stat 27 | import tempfile 28 | 29 | 30 | class PasswdError(Exception): 31 | """Exception class for errors loading a password from a file.""" 32 | 33 | 34 | def ListDirPath(dir_name): 35 | """Like os.listdir with prepended dir_name, which is often more convenient.""" 36 | return [os.path.join(dir_name, fn) for fn in os.listdir(dir_name)] 37 | 38 | 39 | def Read(filename): 40 | """Read entire contents of file with name 'filename'.""" 41 | with open(filename) as fp: 42 | return fp.read() 43 | 44 | 45 | def Write(filename, contents, overwrite_existing=True, mode=0666, gid=None): 46 | """Create a file 'filename' with 'contents', with the mode given in 'mode'. 47 | 48 | The 'mode' is modified by the umask, as in open(2). If 49 | 'overwrite_existing' is False, the file will be opened in O_EXCL mode. 50 | 51 | An optional gid can be specified. 52 | 53 | Args: 54 | filename: str; the name of the file 55 | contents: str; the data to write to the file 56 | overwrite_existing: bool; whether or not to allow the write if the file 57 | already exists 58 | mode: int; permissions with which to create the file (default is 0666 octal) 59 | gid: int; group id with which to create the file 60 | """ 61 | flags = os.O_WRONLY | os.O_TRUNC | os.O_CREAT 62 | if not overwrite_existing: 63 | flags |= os.O_EXCL 64 | fd = os.open(filename, flags, mode) 65 | try: 66 | os.write(fd, contents) 67 | finally: 68 | os.close(fd) 69 | if gid is not None: 70 | os.chown(filename, -1, gid) 71 | 72 | 73 | def AtomicWrite(filename, contents, mode=0666, gid=None): 74 | """Create a file 'filename' with 'contents' atomically. 75 | 76 | As in Write, 'mode' is modified by the umask. This creates and moves 77 | a temporary file, and errors doing the above will be propagated normally, 78 | though it will try to clean up the temporary file in that case. 79 | 80 | This is very similar to the prodlib function with the same name. 81 | 82 | An optional gid can be specified. 83 | 84 | Args: 85 | filename: str; the name of the file 86 | contents: str; the data to write to the file 87 | mode: int; permissions with which to create the file (default is 0666 octal) 88 | gid: int; group id with which to create the file 89 | """ 90 | fd, tmp_filename = tempfile.mkstemp(dir=os.path.dirname(filename)) 91 | try: 92 | os.write(fd, contents) 93 | finally: 94 | os.close(fd) 95 | try: 96 | os.chmod(tmp_filename, mode) 97 | if gid is not None: 98 | os.chown(tmp_filename, -1, gid) 99 | os.rename(tmp_filename, filename) 100 | except OSError, exc: 101 | try: 102 | os.remove(tmp_filename) 103 | except OSError, e: 104 | exc = OSError('%s. Additional errors cleaning up: %s' % (exc, e)) 105 | raise exc 106 | 107 | 108 | @contextlib.contextmanager 109 | def TemporaryFileWithContents(contents, **kw): 110 | """A contextmanager that writes out a string to a file on disk. 111 | 112 | This is useful whenever you need to call a function or command that expects a 113 | file on disk with some contents that you have in memory. The context manager 114 | abstracts the writing, flushing, and deletion of the temporary file. This is a 115 | common idiom that boils down to a single with statement. 116 | 117 | Note: if you need a temporary file-like object for calling an internal 118 | function, you should use a StringIO as a file-like object and not this. 119 | Temporary files should be avoided unless you need a file name or contents in a 120 | file on disk to be read by some other function or program. 121 | 122 | Args: 123 | contents: a string with the contents to write to the file. 124 | **kw: Optional arguments passed on to tempfile.NamedTemporaryFile. 125 | Yields: 126 | The temporary file object, opened in 'w' mode. 127 | 128 | """ 129 | temporary_file = tempfile.NamedTemporaryFile(**kw) 130 | temporary_file.write(contents) 131 | temporary_file.flush() 132 | yield temporary_file 133 | temporary_file.close() 134 | 135 | 136 | # TODO(user): remove after migration to Python 3.2. 137 | # This context manager can be replaced with tempfile.TemporaryDirectory in 138 | # Python 3.2 (http://bugs.python.org/issue5178, 139 | # http://docs.python.org/dev/library/tempfile.html#tempfile.TemporaryDirectory). 140 | @contextlib.contextmanager 141 | def TemporaryDirectory(suffix='', prefix='tmp', base_path=None): 142 | """A context manager to create a temporary directory and clean up on exit. 143 | 144 | The parameters are the same ones expected by tempfile.mkdtemp. 145 | The directory will be securely and atomically created. 146 | Everything under it will be removed when exiting the context. 147 | 148 | Args: 149 | suffix: optional suffix. 150 | prefix: options prefix. 151 | base_path: the base path under which to create the temporary directory. 152 | Yields: 153 | The absolute path of the new temporary directory. 154 | """ 155 | temp_dir_path = tempfile.mkdtemp(suffix, prefix, base_path) 156 | try: 157 | yield temp_dir_path 158 | finally: 159 | try: 160 | shutil.rmtree(temp_dir_path) 161 | except OSError, e: 162 | if e.message == 'Cannot call rmtree on a symbolic link': 163 | # Interesting synthetic exception made up by shutil.rmtree. 164 | # Means we received a symlink from mkdtemp. 165 | # Also means must clean up the symlink instead. 166 | os.unlink(temp_dir_path) 167 | else: 168 | raise 169 | 170 | 171 | def MkDirs(directory, force_mode=None): 172 | """Makes a directory including its parent directories. 173 | 174 | This function is equivalent to os.makedirs() but it avoids a race 175 | condition that os.makedirs() has. The race is between os.mkdir() and 176 | os.path.exists() which fail with errors when run in parallel. 177 | 178 | Args: 179 | directory: str; the directory to make 180 | force_mode: optional octal, chmod dir to get rid of umask interaction 181 | Raises: 182 | Whatever os.mkdir() raises when it fails for any reason EXCLUDING 183 | "dir already exists". If a directory already exists, it does not 184 | raise anything. This behaviour is different than os.makedirs() 185 | """ 186 | name = os.path.normpath(directory) 187 | dirs = name.split(os.path.sep) 188 | for i in range(0, len(dirs)): 189 | path = os.path.sep.join(dirs[:i+1]) 190 | try: 191 | if path: 192 | os.mkdir(path) 193 | # only chmod if we created 194 | if force_mode is not None: 195 | os.chmod(path, force_mode) 196 | except OSError, exc: 197 | if not (exc.errno == errno.EEXIST and os.path.isdir(path)): 198 | raise 199 | 200 | 201 | def RmDirs(dir_name): 202 | """Removes dir_name and every subsequently empty directory above it. 203 | 204 | Unlike os.removedirs and shutil.rmtree, this function doesn't raise an error 205 | if the directory does not exist. 206 | 207 | Args: 208 | dir_name: Directory to be removed. 209 | """ 210 | try: 211 | shutil.rmtree(dir_name) 212 | except OSError, err: 213 | if err.errno != errno.ENOENT: 214 | raise 215 | 216 | try: 217 | parent_directory = os.path.dirname(dir_name) 218 | while parent_directory: 219 | try: 220 | os.rmdir(parent_directory) 221 | except OSError, err: 222 | if err.errno != errno.ENOENT: 223 | raise 224 | 225 | parent_directory = os.path.dirname(parent_directory) 226 | except OSError, err: 227 | if err.errno not in (errno.EACCES, errno.ENOTEMPTY, errno.EPERM): 228 | raise 229 | 230 | 231 | def HomeDir(user=None): 232 | """Find the home directory of a user. 233 | 234 | Args: 235 | user: int, str, or None - the uid or login of the user to query for, 236 | or None (the default) to query for the current process' effective user 237 | 238 | Returns: 239 | str - the user's home directory 240 | 241 | Raises: 242 | TypeError: if user is not int, str, or None. 243 | """ 244 | if user is None: 245 | pw_struct = pwd.getpwuid(os.geteuid()) 246 | elif isinstance(user, int): 247 | pw_struct = pwd.getpwuid(user) 248 | elif isinstance(user, str): 249 | pw_struct = pwd.getpwnam(user) 250 | else: 251 | raise TypeError('user must be None or an instance of int or str') 252 | return pw_struct.pw_dir 253 | -------------------------------------------------------------------------------- /google/apputils/humanize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2008 Google Inc. All Rights Reserved. 4 | 5 | """Lightweight routines for producing more friendly output. 6 | 7 | Usage examples: 8 | 9 | 'New messages: %s' % humanize.Commas(star_count) 10 | -> 'New messages: 58,192' 11 | 12 | 'Found %s.' % humanize.Plural(error_count, 'error') 13 | -> 'Found 2 errors.' 14 | 15 | 'Found %s.' % humanize.Plural(error_count, 'ox', 'oxen') 16 | -> 'Found 2 oxen.' 17 | 18 | 'Copied at %s.' % humanize.DecimalPrefix(rate, 'bps', precision=3) 19 | -> 'Copied at 42.6 Mbps.' 20 | 21 | 'Free RAM: %s' % humanize.BinaryPrefix(bytes_free, 'B') 22 | -> 'Free RAM: 742 MiB' 23 | 24 | 'Finished all tasks in %s.' % humanize.Duration(elapsed_time) 25 | -> 'Finished all tasks in 34m 5s.' 26 | 27 | These libraries are not a substitute for full localization. If you 28 | need localization, then you will have to think about translating 29 | strings, formatting numbers in different ways, and so on. Use 30 | ICU if your application is user-facing. Use these libraries if 31 | your application is an English-only internal tool, and you are 32 | tired of seeing "1 results" or "3450134804 bytes used". 33 | 34 | Compare humanize.*Prefix() to C++ utilites HumanReadableNumBytes and 35 | HumanReadableInt in strings/human_readable.h. 36 | """ 37 | 38 | 39 | 40 | import datetime 41 | import math 42 | import re 43 | 44 | SIBILANT_ENDINGS = frozenset(['sh', 'ss', 'tch', 'ax', 'ix', 'ex']) 45 | DIGIT_SPLITTER = re.compile(r'\d+|\D+').findall 46 | 47 | # These are included because they are common technical terms. 48 | SPECIAL_PLURALS = { 49 | 'index': 'indices', 50 | 'matrix': 'matrices', 51 | 'vertex': 'vertices', 52 | } 53 | 54 | VOWELS = frozenset('AEIOUaeiou') 55 | 56 | 57 | def Commas(value): 58 | """Formats an integer with thousands-separating commas. 59 | 60 | Args: 61 | value: An integer. 62 | 63 | Returns: 64 | A string. 65 | """ 66 | if value < 0: 67 | sign = '-' 68 | value = -value 69 | else: 70 | sign = '' 71 | result = [] 72 | while value >= 1000: 73 | result.append('%03d' % (value % 1000)) 74 | value /= 1000 75 | result.append('%d' % value) 76 | return sign + ','.join(reversed(result)) 77 | 78 | 79 | def Plural(quantity, singular, plural=None): 80 | """Formats an integer and a string into a single pluralized string. 81 | 82 | Args: 83 | quantity: An integer. 84 | singular: A string, the singular form of a noun. 85 | plural: A string, the plural form. If not specified, then simple 86 | English rules of regular pluralization will be used. 87 | 88 | Returns: 89 | A string. 90 | """ 91 | return '%d %s' % (quantity, PluralWord(quantity, singular, plural)) 92 | 93 | 94 | def PluralWord(quantity, singular, plural=None): 95 | """Builds the plural of an English word. 96 | 97 | Args: 98 | quantity: An integer. 99 | singular: A string, the singular form of a noun. 100 | plural: A string, the plural form. If not specified, then simple 101 | English rules of regular pluralization will be used. 102 | 103 | Returns: 104 | the plural form of the word. 105 | """ 106 | if quantity == 1: 107 | return singular 108 | if plural: 109 | return plural 110 | if singular in SPECIAL_PLURALS: 111 | return SPECIAL_PLURALS[singular] 112 | 113 | # We need to guess what the English plural might be. Keep this 114 | # function simple! It doesn't need to know about every possiblity; 115 | # only regular rules and the most common special cases. 116 | # 117 | # Reference: http://en.wikipedia.org/wiki/English_plural 118 | 119 | for ending in SIBILANT_ENDINGS: 120 | if singular.endswith(ending): 121 | return '%ses' % singular 122 | if singular.endswith('o') and singular[-2:-1] not in VOWELS: 123 | return '%ses' % singular 124 | if singular.endswith('y') and singular[-2:-1] not in VOWELS: 125 | return '%sies' % singular[:-1] 126 | return '%ss' % singular 127 | 128 | 129 | def WordSeries(words, conjunction='and'): 130 | """Convert a list of words to an English-language word series. 131 | 132 | Args: 133 | words: A list of word strings. 134 | conjunction: A coordinating conjunction. 135 | 136 | Returns: 137 | A single string containing all the words in the list separated by commas, 138 | the coordinating conjunction, and a serial comma, as appropriate. 139 | """ 140 | num_words = len(words) 141 | if num_words == 0: 142 | return '' 143 | elif num_words == 1: 144 | return words[0] 145 | elif num_words == 2: 146 | return (' %s ' % conjunction).join(words) 147 | else: 148 | return '%s, %s %s' % (', '.join(words[:-1]), conjunction, words[-1]) 149 | 150 | 151 | def AddIndefiniteArticle(noun): 152 | """Formats a noun with an appropriate indefinite article. 153 | 154 | Args: 155 | noun: A string representing a noun. 156 | 157 | Returns: 158 | A string containing noun prefixed with an indefinite article, e.g., 159 | "a thing" or "an object". 160 | """ 161 | if not noun: 162 | raise ValueError('argument must be a word: {!r}'.format(noun)) 163 | if noun[0] in VOWELS: 164 | return 'an ' + noun 165 | else: 166 | return 'a ' + noun 167 | 168 | 169 | def DecimalPrefix(quantity, unit, precision=1, min_scale=0, max_scale=None): 170 | """Formats an integer and a unit into a string, using decimal prefixes. 171 | 172 | The unit will be prefixed with an appropriate multiplier such that 173 | the formatted integer is less than 1,000 (as long as the raw integer 174 | is less than 10**27). For example: 175 | 176 | DecimalPrefix(576012, 'bps') -> '576 kbps' 177 | DecimalPrefix(576012, '') -> '576 k' 178 | DecimalPrefix(576, '') -> '576' 179 | DecimalPrefix(1574215, 'bps', 2) -> '1.6 Mbps' 180 | 181 | Only the SI prefixes which are powers of 10**3 will be used, so 182 | DecimalPrefix(100, 'thread') is '100 thread', not '1 hthread'. 183 | 184 | See also: 185 | BinaryPrefix() 186 | 187 | Args: 188 | quantity: A number. 189 | unit: A string, the dimension for quantity, with no multipliers (e.g. 190 | "bps"). If quantity is dimensionless, the empty string. 191 | precision: An integer, the minimum number of digits to display. 192 | min_scale: minimum power of 1000 to scale to, (None = unbounded). 193 | max_scale: maximum power of 1000 to scale to, (None = unbounded). 194 | 195 | Returns: 196 | A string, composed by the decimal (scaled) representation of quantity at the 197 | required precision, possibly followed by a space, the appropriate multiplier 198 | and the unit. 199 | """ 200 | return _Prefix(quantity, unit, precision, DecimalScale, min_scale=min_scale, 201 | max_scale=max_scale) 202 | 203 | 204 | def BinaryPrefix(quantity, unit, precision=1): 205 | """Formats an integer and a unit into a string, using binary prefixes. 206 | 207 | The unit will be prefixed with an appropriate multiplier such that 208 | the formatted integer is less than 1,024 (as long as the raw integer 209 | is less than 2**90). For example: 210 | 211 | BinaryPrefix(576012, 'B') -> '562 KiB' 212 | BinaryPrefix(576012, '') -> '562 Ki' 213 | 214 | See also: 215 | DecimalPrefix() 216 | 217 | Args: 218 | quantity: A number. 219 | unit: A string, the dimension for quantity, with no multipliers (e.g. 220 | "B"). If quantity is dimensionless, the empty string. 221 | precision: An integer, the minimum number of digits to display. 222 | 223 | Returns: 224 | A string, composed by the decimal (scaled) representation of quantity at the 225 | required precision, possibly followed by a space, the appropriate multiplier 226 | and the unit. 227 | """ 228 | return _Prefix(quantity, unit, precision, BinaryScale) 229 | 230 | 231 | def _Prefix(quantity, unit, precision, scale_callable, **args): 232 | """Formats an integer and a unit into a string. 233 | 234 | Args: 235 | quantity: A number. 236 | unit: A string, the dimension for quantity, with no multipliers (e.g. 237 | "bps"). If quantity is dimensionless, the empty string. 238 | precision: An integer, the minimum number of digits to display. 239 | scale_callable: A callable, scales the number and units. 240 | **args: named arguments passed to scale_callable. 241 | 242 | Returns: 243 | A string. 244 | """ 245 | separator = ' ' if unit else '' 246 | 247 | if not quantity: 248 | return '0%s%s' % (separator, unit) 249 | 250 | if quantity in [float('inf'), float('-inf')] or math.isnan(quantity): 251 | return '%f%s%s' % (quantity, separator, unit) 252 | 253 | scaled_quantity, scaled_unit = scale_callable(quantity, unit, **args) 254 | 255 | if scaled_unit: 256 | separator = ' ' 257 | 258 | print_pattern = '%%.%df%%s%%s' % max(0, (precision - int( 259 | math.log(abs(scaled_quantity), 10)) - 1)) 260 | 261 | return print_pattern % (scaled_quantity, separator, scaled_unit) 262 | 263 | 264 | # Prefixes and corresponding min_scale and max_scale for decimal formating. 265 | DECIMAL_PREFIXES = ('y', 'z', 'a', 'f', 'p', 'n', u'µ', 'm', 266 | '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') 267 | DECIMAL_MIN_SCALE = -8 268 | DECIMAL_MAX_SCALE = 8 269 | 270 | 271 | def DecimalScale(quantity, unit, min_scale=0, max_scale=None): 272 | """Get the scaled value and decimal prefixed unit in a tupple. 273 | 274 | DecimalScale(576012, 'bps') -> (576.012, 'kbps') 275 | DecimalScale(1574215, 'bps') -> (1.574215, 'Mbps') 276 | 277 | Args: 278 | quantity: A number. 279 | unit: A string. 280 | min_scale: minimum power of 1000 to normalize to (None = unbounded) 281 | max_scale: maximum power of 1000 to normalize to (None = unbounded) 282 | 283 | Returns: 284 | A tuple of a scaled quantity (float) and BinaryPrefix for the 285 | units (string). 286 | """ 287 | if min_scale is None or min_scale < DECIMAL_MIN_SCALE: 288 | min_scale = DECIMAL_MIN_SCALE 289 | if max_scale is None or max_scale > DECIMAL_MAX_SCALE: 290 | max_scale = DECIMAL_MAX_SCALE 291 | powers = DECIMAL_PREFIXES[ 292 | min_scale - DECIMAL_MIN_SCALE:max_scale - DECIMAL_MIN_SCALE + 1] 293 | return _Scale(quantity, unit, 1000, powers, min_scale) 294 | 295 | 296 | def BinaryScale(quantity, unit): 297 | """Get the scaled value and binary prefixed unit in a tupple. 298 | 299 | BinaryPrefix(576012, 'B') -> (562.51171875, 'KiB') 300 | 301 | Args: 302 | quantity: A number. 303 | unit: A string. 304 | 305 | Returns: 306 | A tuple of a scaled quantity (float) and BinaryPrefix for the 307 | units (string). 308 | """ 309 | return _Scale(quantity, unit, 1024, 310 | ('Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')) 311 | 312 | 313 | def _Scale(quantity, unit, multiplier, prefixes=None, min_scale=None): 314 | """Returns the formatted quantity and unit into a tuple. 315 | 316 | Args: 317 | quantity: A number. 318 | unit: A string 319 | multiplier: An integer, the ratio between prefixes. 320 | prefixes: A sequence of strings. 321 | If empty or None, no scaling is done. 322 | min_scale: minimum power of multiplier corresponding to the first prefix. 323 | If None assumes prefixes are for positive powers only. 324 | 325 | Returns: 326 | A tuple containing the raw scaled quantity (float) and the prefixed unit. 327 | """ 328 | if (not prefixes or not quantity or math.isnan(quantity) or 329 | quantity in [float('inf'), float('-inf')]): 330 | return float(quantity), unit 331 | 332 | if min_scale is None: 333 | min_scale = 0 334 | prefixes = ('',) + tuple(prefixes) 335 | value, prefix = quantity, '' 336 | for power, prefix in enumerate(prefixes, min_scale): 337 | # This is more numerically accurate than '/ multiplier ** power'. 338 | value = float(quantity) * multiplier ** -power 339 | if abs(value) < multiplier: 340 | break 341 | return value, prefix + unit 342 | 343 | # Contains the fractions where the full range [1/n ... (n - 1) / n] 344 | # is defined in Unicode. 345 | FRACTIONS = { 346 | 3: (None, u'⅓', u'⅔', None), 347 | 5: (None, u'⅕', u'⅖', u'⅗', u'⅘', None), 348 | 8: (None, u'⅛', u'¼', u'⅜', u'½', u'⅝', u'¾', u'⅞', None), 349 | } 350 | 351 | FRACTION_ROUND_DOWN = 1.0 / (max(FRACTIONS.keys()) * 2.0) 352 | FRACTION_ROUND_UP = 1.0 - FRACTION_ROUND_DOWN 353 | 354 | 355 | def PrettyFraction(number, spacer=''): 356 | """Convert a number into a string that might include a unicode fraction. 357 | 358 | This method returns the integer representation followed by the closest 359 | fraction of a denominator 2, 3, 4, 5 or 8. 360 | For instance, 0.33 will be converted to 1/3. 361 | The resulting representation should be less than 1/16 off. 362 | 363 | Args: 364 | number: a python number 365 | spacer: an optional string to insert between the integer and the fraction 366 | default is an empty string. 367 | 368 | Returns: 369 | a unicode string representing the number. 370 | """ 371 | # We do not want small negative numbers to display as -0. 372 | if number < -FRACTION_ROUND_DOWN: 373 | return u'-%s' % PrettyFraction(-number) 374 | number = abs(number) 375 | rounded = int(number) 376 | fract = number - rounded 377 | if fract >= FRACTION_ROUND_UP: 378 | return str(rounded + 1) 379 | errors_fractions = [] 380 | for denominator, fraction_elements in FRACTIONS.items(): 381 | numerator = int(round(denominator * fract)) 382 | error = abs(fract - (float(numerator) / float(denominator))) 383 | errors_fractions.append((error, fraction_elements[numerator])) 384 | unused_error, fraction_text = min(errors_fractions) 385 | if rounded and fraction_text: 386 | return u'%d%s%s' % (rounded, spacer, fraction_text) 387 | if rounded: 388 | return str(rounded) 389 | if fraction_text: 390 | return fraction_text 391 | return u'0' 392 | 393 | 394 | def Duration(duration, separator=' '): 395 | """Formats a nonnegative number of seconds into a human-readable string. 396 | 397 | Args: 398 | duration: A float duration in seconds. 399 | separator: A string separator between days, hours, minutes and seconds. 400 | 401 | Returns: 402 | Formatted string like '5d 12h 30m 45s'. 403 | """ 404 | try: 405 | delta = datetime.timedelta(seconds=duration) 406 | except OverflowError: 407 | return '>=' + TimeDelta(datetime.timedelta.max) 408 | return TimeDelta(delta, separator=separator) 409 | 410 | 411 | def TimeDelta(delta, separator=' '): 412 | """Format a datetime.timedelta into a human-readable string. 413 | 414 | Args: 415 | delta: The datetime.timedelta to format. 416 | separator: A string separator between days, hours, minutes and seconds. 417 | 418 | Returns: 419 | Formatted string like '5d 12h 30m 45s'. 420 | """ 421 | parts = [] 422 | seconds = delta.seconds 423 | if delta.days: 424 | parts.append('%dd' % delta.days) 425 | if seconds >= 3600: 426 | parts.append('%dh' % (seconds // 3600)) 427 | seconds %= 3600 428 | if seconds >= 60: 429 | parts.append('%dm' % (seconds // 60)) 430 | seconds %= 60 431 | seconds += delta.microseconds / 1e6 432 | if seconds or not parts: 433 | parts.append('%gs' % seconds) 434 | return separator.join(parts) 435 | 436 | 437 | def NaturalSortKey(data): 438 | """Key function for "natural sort" ordering. 439 | 440 | This key function results in a lexigraph sort. For example: 441 | - ['1, '3', '20'] (not ['1', '20', '3']). 442 | - ['Model 9', 'Model 70 SE', 'Model 70 SE2'] 443 | (not ['Model 70 SE', 'Model 70 SE2', 'Model 9']). 444 | 445 | Usage: 446 | new_list = sorted(old_list, key=humanize.NaturalSortKey) 447 | or 448 | list_sort_in_place.sort(key=humanize.NaturalSortKey) 449 | 450 | Based on code by Steven Bazyl . 451 | 452 | Args: 453 | data: str, The key being compared in a sort. 454 | 455 | Returns: 456 | A list which is comparable to other lists for the purpose of sorting. 457 | """ 458 | segments = DIGIT_SPLITTER(data) 459 | for i, value in enumerate(segments): 460 | if value.isdigit(): 461 | segments[i] = int(value) 462 | return segments 463 | 464 | 465 | def UnixTimestamp(unix_ts, tz): 466 | """Format a UNIX timestamp into a human-readable string. 467 | 468 | Args: 469 | unix_ts: UNIX timestamp (number of seconds since epoch). May be a floating 470 | point number. 471 | tz: datetime.tzinfo object, timezone to use when formatting. Typical uses 472 | might want to rely on datelib or pytz to provide the tzinfo object, e.g. 473 | use datelib.UTC, datelib.US_PACIFIC, or pytz.timezone('Europe/Dublin'). 474 | 475 | Returns: 476 | Formatted string like '2013-11-17 11:08:27.720000 PST'. 477 | """ 478 | date_time = datetime.datetime.fromtimestamp(unix_ts, tz) 479 | return date_time.strftime('%Y-%m-%d %H:%M:%S.%f %Z') 480 | 481 | 482 | def AddOrdinalSuffix(value): 483 | """Adds an ordinal suffix to a non-negative integer (e.g. 1 -> '1st'). 484 | 485 | Args: 486 | value: A non-negative integer. 487 | 488 | Returns: 489 | A string containing the integer with a two-letter ordinal suffix. 490 | """ 491 | if value < 0 or value != int(value): 492 | raise ValueError('argument must be a non-negative integer: %s' % value) 493 | 494 | if value % 100 in (11, 12, 13): 495 | suffix = 'th' 496 | else: 497 | rem = value % 10 498 | if rem == 1: 499 | suffix = 'st' 500 | elif rem == 2: 501 | suffix = 'nd' 502 | elif rem == 3: 503 | suffix = 'rd' 504 | else: 505 | suffix = 'th' 506 | 507 | return str(value) + suffix 508 | -------------------------------------------------------------------------------- /google/apputils/resources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2010 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Wrapper around setuptools' pkg_resources with more Google-like names. 17 | 18 | This module is not very useful on its own, but many Google open-source projects 19 | are used to a different naming scheme, and this module makes the transition 20 | easier. 21 | """ 22 | 23 | __author__ = 'dborowitz@google.com (Dave Borowitz)' 24 | 25 | import atexit 26 | 27 | import pkg_resources 28 | 29 | 30 | def _Call(func, name): 31 | """Call a pkg_resources function. 32 | 33 | Args: 34 | func: A function from pkg_resources that takes the arguments 35 | (package_or_requirement, resource_name); for more info, 36 | see http://peak.telecommunity.com/DevCenter/PkgResources 37 | name: A name of the form 'module.name:path/to/resource'; this should 38 | generally be built from __name__ in the calling module. 39 | 40 | Returns: 41 | The result of calling the function on the split resource name. 42 | """ 43 | pkg_name, resource_name = name.split(':', 1) 44 | return func(pkg_name, resource_name) 45 | 46 | 47 | def GetResource(name): 48 | """Get a resource as a string; see _Call.""" 49 | return _Call(pkg_resources.resource_string, name) 50 | 51 | 52 | def GetResourceAsFile(name): 53 | """Get a resource as a file-like object; see _Call.""" 54 | return _Call(pkg_resources.resource_stream, name) 55 | 56 | 57 | _extracted_files = False 58 | 59 | 60 | def GetResourceFilename(name): 61 | """Get a filename for a resource; see _Call.""" 62 | global _extracted_files # pylint: disable=global-statement 63 | if not _extracted_files: 64 | atexit.register(pkg_resources.cleanup_resources) 65 | _extracted_files = True 66 | 67 | return _Call(pkg_resources.resource_filename, name) 68 | -------------------------------------------------------------------------------- /google/apputils/run_script_module.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2010 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Script for running Google-style applications. 17 | 18 | Unlike normal scripts run through setuptools console_script entry points, 19 | Google-style applications must be run as top-level scripts. 20 | 21 | Given an already-imported module, users can use the RunScriptModule function to 22 | set up the appropriate executable environment to spawn a new Python process to 23 | run the module as a script. 24 | 25 | To use this technique in your project, first create a module called e.g. 26 | stubs.py with contents like: 27 | 28 | from google.apputils import run_script_module 29 | 30 | def RunMyScript(): 31 | import my.script 32 | run_script_module.RunScriptModule(my.script) 33 | 34 | def RunMyOtherScript(): 35 | import my.other_script 36 | run_script_module.RunScriptModule(my.other_script) 37 | 38 | Then, set up entry points in your setup.py that point to the functions in your 39 | stubs module: 40 | 41 | setup( 42 | ... 43 | entry_points = { 44 | 'console_scripts': [ 45 | 'my_script = my.stubs:RunMyScript', 46 | 'my_other_script = my.stubs.RunMyOtherScript', 47 | ], 48 | }, 49 | ) 50 | 51 | When your project is installed, setuptools will generate minimal wrapper scripts 52 | to call your stub functions, which in turn execv your script modules. That's it! 53 | """ 54 | 55 | __author__ = 'dborowitz@google.com (Dave Borowitz)' 56 | 57 | import os 58 | import re 59 | import sys 60 | 61 | 62 | def FindEnv(progname): 63 | """Find the program in the system path. 64 | 65 | Args: 66 | progname: The name of the program. 67 | 68 | Returns: 69 | The full pathname of the program. 70 | 71 | Raises: 72 | AssertionError: if the program was not found. 73 | """ 74 | for path in os.environ['PATH'].split(':'): 75 | fullname = os.path.join(path, progname) 76 | if os.access(fullname, os.X_OK): 77 | return fullname 78 | raise AssertionError( 79 | "Could not find an executable named '%s' in the system path" % progname) 80 | 81 | 82 | def GetPdbArgs(python): 83 | """Try to get the path to pdb.py and return it in a list. 84 | 85 | Args: 86 | python: The full path to a Python executable. 87 | 88 | Returns: 89 | A list of strings. If a relevant pdb.py was found, this will be 90 | ['/path/to/pdb.py']; if not, return ['-m', 'pdb'] and hope for the best. 91 | (This latter technique will fail for Python 2.2.) 92 | """ 93 | # Usually, python is /usr/bin/pythonxx and pdb is /usr/lib/pythonxx/pdb.py 94 | components = python.split('/') 95 | if len(components) >= 2: 96 | pdb_path = '/'.join(components[0:-2] + ['lib'] + 97 | components[-1:] + ['pdb.py']) 98 | if os.access(pdb_path, os.R_OK): 99 | return [pdb_path] 100 | 101 | # No pdb module found in the python path, default to -m pdb 102 | return ['-m', 'pdb'] 103 | 104 | 105 | def StripDelimiters(s, beg, end): 106 | if s[0] == beg: 107 | assert s[-1] == end 108 | return (s[1:-1], True) 109 | else: 110 | return (s, False) 111 | 112 | 113 | def StripQuotes(s): 114 | (s, stripped) = StripDelimiters(s, '"', '"') 115 | if not stripped: 116 | (s, stripped) = StripDelimiters(s, "'", "'") 117 | return s 118 | 119 | 120 | def PrintOurUsage(): 121 | """Print usage for the stub script.""" 122 | print 'Stub script %s (auto-generated). Options:' % sys.argv[0] 123 | print ('--helpstub ' 124 | 'Show help for stub script.') 125 | print ('--debug_binary ' 126 | 'Run python under debugger specified by --debugger.') 127 | print ('--debugger= ' 128 | "Debugger for --debug_binary. Default: 'gdb --args'.") 129 | print ('--debug_script ' 130 | 'Run wrapped script with python debugger module (pdb).') 131 | print ('--show_command_and_exit ' 132 | 'Print command which would be executed and exit.') 133 | print ('These options must appear first in the command line, all others will ' 134 | 'be passed to the wrapped script.') 135 | 136 | 137 | def RunScriptModule(module): 138 | """Run a module as a script. 139 | 140 | Locates the module's file and runs it in the current interpreter, or 141 | optionally a debugger. 142 | 143 | Args: 144 | module: The module object to run. 145 | """ 146 | args = sys.argv[1:] 147 | 148 | debug_binary = False 149 | debugger = 'gdb --args' 150 | debug_script = False 151 | show_command_and_exit = False 152 | 153 | while args: 154 | if args[0] == '--helpstub': 155 | PrintOurUsage() 156 | sys.exit(0) 157 | if args[0] == '--debug_binary': 158 | debug_binary = True 159 | args = args[1:] 160 | continue 161 | if args[0] == '--debug_script': 162 | debug_script = True 163 | args = args[1:] 164 | continue 165 | if args[0] == '--show_command_and_exit': 166 | show_command_and_exit = True 167 | args = args[1:] 168 | continue 169 | matchobj = re.match('--debugger=(.+)', args[0]) 170 | if matchobj is not None: 171 | debugger = StripQuotes(matchobj.group(1)) 172 | args = args[1:] 173 | continue 174 | break 175 | 176 | # Now look for my main python source file 177 | # TODO(dborowitz): This will fail if the module was zipimported, which means 178 | # no egg depending on this script runner can be zip_safe. 179 | main_filename = module.__file__ 180 | assert os.path.exists(main_filename), ('Cannot exec() %r: file not found.' % 181 | main_filename) 182 | assert os.access(main_filename, os.R_OK), ('Cannot exec() %r: file not' 183 | ' readable.' % main_filename) 184 | 185 | args = [main_filename] + args 186 | 187 | if debug_binary: 188 | debugger_args = debugger.split() 189 | program = debugger_args[0] 190 | # If pathname is not absolute, determine full path using PATH 191 | if not os.path.isabs(program): 192 | program = FindEnv(program) 193 | python_path = sys.executable 194 | command_vec = [python_path] 195 | if debug_script: 196 | command_vec.extend(GetPdbArgs(python_path)) 197 | args = [program] + debugger_args[1:] + command_vec + args 198 | 199 | elif debug_script: 200 | args = [sys.executable] + GetPdbArgs(program) + args 201 | 202 | else: 203 | program = sys.executable 204 | args = [sys.executable] + args 205 | 206 | if show_command_and_exit: 207 | print 'program: "%s"' % program 208 | print 'args:', args 209 | sys.exit(0) 210 | 211 | try: 212 | sys.stdout.flush() 213 | os.execv(program, args) 214 | except EnvironmentError as e: 215 | if not getattr(e, 'filename', None): 216 | e.filename = program # Add info to error message 217 | raise 218 | -------------------------------------------------------------------------------- /google/apputils/setup_command.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2010 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Setuptools extension for running Google-style Python tests. 17 | 18 | Google-style Python tests differ from normal Python tests in that each test 19 | module is intended to be executed as an independent script. In particular, the 20 | test fixture code in basetest.main() that executes module-wide setUp() and 21 | tearDown() depends on __main__ being the module under test. This conflicts with 22 | the usual setuptools test style, which uses a single TestSuite to run all of a 23 | package's tests. 24 | 25 | This package provides a new setuptools command, google_test, that runs all of 26 | the google-style tests found in a specified directory. 27 | 28 | NOTE: This works by overriding sys.modules['__main__'] with the module under 29 | test, but still runs tests in the same process. Thus it will *not* work if your 30 | tests depend on any of the following: 31 | - Per-process (as opposed to per-module) initialization. 32 | - Any entry point that is not basetest.main(). 33 | 34 | To use the google_test command in your project, do something like the following: 35 | 36 | In setup.py: 37 | setup( 38 | name = "mypackage", 39 | ... 40 | setup_requires = ["google-apputils>=0.2"], 41 | google_test_dir = "tests", 42 | ) 43 | 44 | Run: 45 | $ python setup.py google_test 46 | """ 47 | 48 | from distutils import errors 49 | import imp 50 | import os 51 | import re 52 | import shlex 53 | import sys 54 | import traceback 55 | 56 | from setuptools.command import test 57 | 58 | 59 | def ValidateGoogleTestDir(unused_dist, unused_attr, value): 60 | """Validate that the test directory is a directory.""" 61 | if not os.path.isdir(value): 62 | raise errors.DistutilsSetupError('%s is not a directory' % value) 63 | 64 | 65 | class GoogleTest(test.test): 66 | """Command to run Google-style tests after in-place build.""" 67 | 68 | description = 'run Google-style tests after in-place build' 69 | 70 | _DEFAULT_PATTERN = r'_(?:unit|reg)?test\.py$' 71 | 72 | user_options = [ 73 | ('test-dir=', 'd', 'Look for test modules in specified directory.'), 74 | ('test-module-pattern=', 'p', 75 | ('Pattern for matching test modules. Defaults to %r. ' 76 | 'Only source files (*.py) will be considered, even if more files match ' 77 | 'this pattern.' % _DEFAULT_PATTERN)), 78 | ('test-args=', 'a', 79 | ('Arguments to pass to basetest.main(). May only make sense if ' 80 | 'test_module_pattern matches exactly one test.')), 81 | ] 82 | 83 | def initialize_options(self): 84 | self.test_dir = None 85 | self.test_module_pattern = self._DEFAULT_PATTERN 86 | self.test_args = '' 87 | 88 | # Set to a dummy value, since we don't call the superclass methods for 89 | # options parsing. 90 | self.test_suite = True 91 | 92 | def finalize_options(self): 93 | if self.test_dir is None: 94 | if self.distribution.google_test_dir: 95 | self.test_dir = self.distribution.google_test_dir 96 | else: 97 | raise errors.DistutilsOptionError('No test directory specified') 98 | 99 | self.test_module_pattern = re.compile(self.test_module_pattern) 100 | self.test_args = shlex.split(self.test_args) 101 | 102 | def _RunTestModule(self, module_path): 103 | """Run a module as a test module given its path. 104 | 105 | Args: 106 | module_path: The path to the module to test; must end in '.py'. 107 | 108 | Returns: 109 | True if the tests in this module pass, False if not or if an error occurs. 110 | """ 111 | 112 | path, filename = os.path.split(module_path) 113 | 114 | old_argv = sys.argv[:] 115 | old_path = sys.path[:] 116 | old_modules = sys.modules.copy() 117 | 118 | # Make relative imports in test modules work with our mangled sys.path. 119 | sys.path.insert(0, path) 120 | 121 | module_name = filename.replace('.py', '') 122 | import_tuple = imp.find_module(module_name, [path]) 123 | module = imp.load_module(module_name, *import_tuple) 124 | 125 | sys.modules['__main__'] = module 126 | sys.argv = [module.__file__] + self.test_args 127 | 128 | # Late import since this must be run with the project's sys.path. 129 | import basetest 130 | try: 131 | try: 132 | sys.stderr.write('Testing %s\n' % module_name) 133 | basetest.main() 134 | 135 | # basetest.main() should always call sys.exit, so this is very bad. 136 | return False 137 | except SystemExit as e: 138 | returncode, = e.args 139 | return not returncode 140 | except: 141 | traceback.print_exc() 142 | return False 143 | finally: 144 | sys.argv[:] = old_argv 145 | sys.path[:] = old_path 146 | sys.modules.clear() 147 | sys.modules.update(old_modules) 148 | 149 | def run_tests(self): 150 | ok = True 151 | for path, _, filenames in os.walk(self.test_dir): 152 | for filename in filenames: 153 | if not filename.endswith('.py'): 154 | continue 155 | file_path = os.path.join(path, filename) 156 | if self.test_module_pattern.search(file_path): 157 | ok &= self._RunTestModule(file_path) 158 | 159 | sys.exit(int(not ok)) 160 | -------------------------------------------------------------------------------- /google/apputils/shellutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This code must be source compatible with Python 2.4 through 3.3. 3 | # 4 | # Copyright 2003 Google Inc. All Rights Reserved. 5 | 6 | """Utility functions for dealing with command interpreters.""" 7 | 8 | 9 | 10 | import os 11 | 12 | # Running windows? 13 | win32 = (os.name == 'nt') 14 | 15 | 16 | def ShellEscapeList(words): 17 | """Turn a list of words into a shell-safe string. 18 | 19 | Args: 20 | words: A list of words, e.g. for a command. 21 | 22 | Returns: 23 | A string of shell-quoted and space-separated words. 24 | """ 25 | 26 | if win32: 27 | return ' '.join(words) 28 | 29 | s = '' 30 | for word in words: 31 | # Single quote word, and replace each ' in word with '"'"' 32 | s += "'" + word.replace("'", "'\"'\"'") + "' " 33 | 34 | return s[:-1] 35 | 36 | 37 | def ShellifyStatus(status): 38 | """Translate from a wait() exit status to a command shell exit status.""" 39 | 40 | if not win32: 41 | if os.WIFEXITED(status): 42 | # decode and return exit status 43 | status = os.WEXITSTATUS(status) 44 | else: 45 | # On Unix, the wait() produces a 16 bit return code. Unix shells 46 | # lossily compress this to an 8 bit value, using the formula below. 47 | # Shell status code < 128 means the process exited normally, status 48 | # code >= 128 means the process died because of a signal. 49 | status = 128 + os.WTERMSIG(status) 50 | return status 51 | -------------------------------------------------------------------------------- /google/apputils/stopwatch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2005 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """A useful class for digesting, on a high-level, where time in a program goes. 17 | 18 | Usage: 19 | 20 | sw = StopWatch() 21 | sw.start() 22 | sw.start('foo') 23 | foo() 24 | sw.stop('foo') 25 | args = overhead_code() 26 | sw.start('bar') 27 | bar(args) 28 | sw.stop('bar') 29 | sw.dump() 30 | 31 | If you start a new timer when one is already running, then the other one will 32 | stop running, and restart when you stop this timer. This behavior is very 33 | useful for when you want to try timing for a subcall without remembering 34 | what is already running. For instance: 35 | 36 | sw.start('all_this') 37 | do_some_stuff() 38 | sw.start('just_that') 39 | small_but_expensive_function() 40 | sw.stop('just_that') 41 | cleanup_code() 42 | sw.stop('all_this') 43 | 44 | In this case, the output will be what you want: the time spent in 45 | small_but_expensive function will show up in the timer for just_that and not 46 | all_this. 47 | """ 48 | 49 | import StringIO 50 | import time 51 | 52 | 53 | __owner__ = 'dbentley@google.com (Dan Bentley)' 54 | 55 | 56 | class StopWatch(object): 57 | """Class encapsulating a timer; see above for example usage. 58 | 59 | Instance variables: 60 | timers: map of stopwatch name -> time for each currently running stopwatch, 61 | where time is seconds from the epoch of when this stopwatch was 62 | started. 63 | accum: map of stopwatch name -> accumulated time, in seconds, it has 64 | already been run for. 65 | stopped: map of timer name -> list of timer names that are blocking it. 66 | counters: map of timer name -> number of times it has been started. 67 | """ 68 | 69 | def __init__(self): 70 | self.timers = {} 71 | self.accum = {} 72 | self.stopped = {} 73 | self.counters = {} 74 | 75 | def start(self, timer='total', stop_others=True): 76 | """Start a timer. 77 | 78 | Args: 79 | timer: str; name of the timer to start, defaults to the overall timer. 80 | stop_others: bool; if True, stop all other running timers. If False, then 81 | you can have time that is spent inside more than one timer 82 | and there's a good chance that the overhead measured will be 83 | negative. 84 | """ 85 | if stop_others: 86 | stopped = [] 87 | for other in list(self.timers): 88 | if not other == 'total': 89 | self.stop(other) 90 | stopped.append(other) 91 | self.stopped[timer] = stopped 92 | self.counters[timer] = self.counters.get(timer, 0) + 1 93 | self.timers[timer] = time.time() 94 | 95 | def stop(self, timer='total'): 96 | """Stop a running timer. 97 | 98 | This includes restarting anything that was stopped on behalf of this timer. 99 | 100 | Args: 101 | timer: str; name of the timer to stop, defaults to the overall timer. 102 | 103 | Raises: 104 | RuntimeError: if timer refers to a timer that was never started. 105 | """ 106 | if timer not in self.timers: 107 | raise RuntimeError( 108 | 'Tried to stop timer that was never started: %s' % timer) 109 | self.accum[timer] = self.timervalue(timer) 110 | del self.timers[timer] 111 | for stopped in self.stopped.get(timer, []): 112 | self.start(stopped, stop_others=0) 113 | 114 | def timervalue(self, timer='total', now=None): 115 | """Return the value seen by this timer so far. 116 | 117 | If the timer is stopped, this will be the accumulated time it has seen. 118 | If the timer is running, this will be the time it has seen up to now. 119 | If the timer has never been started, this will be zero. 120 | 121 | Args: 122 | timer: str; the name of the timer to report on. 123 | now: long; if provided, the time to use for 'now' for running timers. 124 | """ 125 | if not now: 126 | now = time.time() 127 | 128 | if timer in self.timers: 129 | # Timer is running now. 130 | return self.accum.get(timer, 0.0) + (now - self.timers[timer]) 131 | elif timer in self.accum: 132 | # Timer is stopped. 133 | return self.accum[timer] 134 | else: 135 | # Timer is never started. 136 | return 0.0 137 | 138 | def overhead(self, now=None): 139 | """Calculate the overhead. 140 | 141 | Args: 142 | now: (optional) time to use as the current time. 143 | 144 | Returns: 145 | The overhead, that is, time spent in total but not in any sub timer. This 146 | may be negative if time was counted in two sub timers. Avoid this by 147 | always using stop_others. 148 | """ 149 | total = self.timervalue('total', now) 150 | if total == 0.0: 151 | return 0.0 152 | 153 | all_timers = sum(self.accum.itervalues()) 154 | return total - (all_timers - total) 155 | 156 | def results(self, verbose=False): 157 | """Get the results of this stopwatch. 158 | 159 | Args: 160 | verbose: bool; if True, show all times; otherwise, show only the total. 161 | 162 | Returns: 163 | A list of tuples showing the output of this stopwatch, of the form 164 | (name, value, num_starts) for each timer. Note that if the total timer 165 | is not used, non-verbose results will be the empty list. 166 | """ 167 | now = time.time() 168 | 169 | all_names = self.accum.keys() 170 | names = [] 171 | 172 | if 'total' in all_names: 173 | all_names.remove('total') 174 | all_names.sort() 175 | if verbose: 176 | names = all_names 177 | 178 | results = [(name, self.timervalue(name, now=now), self.counters[name]) 179 | for name in names] 180 | if verbose: 181 | results.append(('overhead', self.overhead(now=now), 1)) 182 | if 'total' in self.accum or 'total' in self.timers: 183 | results.append(('total', self.timervalue('total', now=now), 184 | self.counters['total'])) 185 | return results 186 | 187 | def dump(self, verbose=False): 188 | """Describes where time in this stopwatch was spent. 189 | 190 | Args: 191 | verbose: bool; if True, show all timers; otherwise, show only the total. 192 | 193 | Returns: 194 | A string describing the stopwatch. 195 | """ 196 | output = StringIO.StringIO() 197 | results = self.results(verbose=verbose) 198 | maxlength = max([len(result[0]) for result in results]) 199 | for result in results: 200 | output.write('%*s: %6.2fs\n' % (maxlength, result[0], result[1])) 201 | return output.getvalue() 202 | 203 | # Create a stopwatch to be publicly used. 204 | sw = StopWatch() 205 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2010 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | 18 | try: 19 | import setuptools 20 | except ImportError: 21 | import ez_setup 22 | ez_setup.use_setuptools() 23 | import setuptools 24 | 25 | from setuptools import setup, find_packages 26 | from setuptools.command import test 27 | 28 | REQUIRE = [ 29 | "python-dateutil>=1.4", 30 | "python-gflags>=1.4", 31 | "pytz>=2010", 32 | ] 33 | 34 | TEST_REQUIRE = ["mox>=0.5"] 35 | 36 | if sys.version_info[:2] < (2, 7): 37 | # unittest2 is a backport of Python 2.7's unittest. 38 | TEST_REQUIRE.append("unittest2>=0.5.1") 39 | 40 | 41 | # Mild hackery to get around the fact that we want to include a 42 | # GoogleTest as one of the cmdclasses for our package, but we 43 | # can't reference it until our package is installed. We simply 44 | # make a wrapper class that actually creates objects of the 45 | # appropriate class at runtime. 46 | class GoogleTestWrapper(test.test, object): 47 | test_dir = None 48 | 49 | def __new__(cls, *args, **kwds): 50 | from google.apputils import setup_command 51 | dist = setup_command.GoogleTest(*args, **kwds) 52 | dist.test_dir = GoogleTestWrapper.test_dir 53 | return dist 54 | 55 | setup( 56 | name="google-apputils", 57 | version="0.4.2", 58 | packages=find_packages(exclude=["tests"]), 59 | namespace_packages=["google"], 60 | entry_points={ 61 | "distutils.commands": [ 62 | "google_test = google.apputils.setup_command:GoogleTest", 63 | ], 64 | 65 | "distutils.setup_keywords": [ 66 | ("google_test_dir = google.apputils.setup_command" 67 | ":ValidateGoogleTestDir"), 68 | ], 69 | }, 70 | 71 | install_requires=REQUIRE, 72 | tests_require=REQUIRE + TEST_REQUIRE, 73 | 74 | # The entry_points above allow other projects to understand the 75 | # google_test command and test_dir option by specifying 76 | # setup_requires("google-apputils"). However, those entry_points only get 77 | # registered when this project is installed, and we need to run Google-style 78 | # tests for this project before it is installed. So we need to manually set 79 | # up the command and option mappings, for this project only, and we use 80 | # a wrapper class that exists before the install happens. 81 | cmdclass={"google_test": GoogleTestWrapper}, 82 | command_options={"google_test": {"test_dir": ("setup.py", "tests")}}, 83 | 84 | author="Google Inc.", 85 | author_email="opensource@google.com", 86 | url="https://github.com/google/google-apputils", 87 | ) 88 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /tests/app_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2008 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Tests for app.py .""" 17 | 18 | 19 | 20 | 21 | import os 22 | import shutil 23 | import socket 24 | import sys 25 | 26 | import mox 27 | 28 | from google.apputils import basetest 29 | 30 | from google.apputils import app 31 | import gflags as flags 32 | 33 | FLAGS = flags.FLAGS 34 | 35 | 36 | class TestFunctions(basetest.TestCase): 37 | 38 | def testInstallExceptionHandler(self): 39 | self.assertRaises(TypeError, app.InstallExceptionHandler, 1) 40 | 41 | 42 | if __name__ == '__main__': 43 | basetest.main() 44 | -------------------------------------------------------------------------------- /tests/app_test_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2005 Google Inc. All Rights Reserved. 4 | # 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 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 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. 16 | 17 | """Helper script used by app_unittest.sh""" 18 | 19 | 20 | 21 | import sys 22 | 23 | import gflags as flags 24 | from google.apputils import app 25 | 26 | FLAGS = flags.FLAGS 27 | flags.DEFINE_boolean("raise_exception", False, "throw MyException from main") 28 | 29 | class MyException(Exception): 30 | pass 31 | 32 | def main(args): 33 | if FLAGS.raise_exception: 34 | raise MyException 35 | 36 | 37 | if __name__ == '__main__': 38 | app.run() 39 | -------------------------------------------------------------------------------- /tests/app_unittest.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2003 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # Author: Douglas Greiman 17 | 18 | PYTHON=$(which python) 19 | function die { 20 | echo "$1" 21 | exit 1 22 | } 23 | 24 | APP_PACKAGE="google.apputils" 25 | 26 | # This should exit with error code because no main defined 27 | $PYTHON -c "from ${APP_PACKAGE} import app; app.run()" 2>/dev/null && \ 28 | die "Test 1 failed" 29 | 30 | # Standard use. This should exit successfully 31 | $PYTHON -c "from ${APP_PACKAGE} import app 32 | a = 0 33 | def main(argv): 34 | global a 35 | a = 1 36 | app.run() 37 | assert a == 1" || \ 38 | die "Test 2 failed" 39 | # app.run called in exec block, script read from -c string. Should succeed. 40 | $PYTHON -c "from ${APP_PACKAGE} import app 41 | a = 0 42 | s=''' 43 | def main(argv): 44 | global a 45 | a = 1 46 | app.run() 47 | ''' 48 | exec s 49 | assert a == 1" || \ 50 | die "Test 4 failed" 51 | 52 | # app.run called in exec block, script read from file. Should succeed. 53 | PYFILE=$TEST_TMPDIR/tmp.py 54 | cat >$PYFILE </dev/null || \ 76 | die "Test 11 failed" 77 | 78 | # Test that the usage() function works 79 | $PYTHON -c "from ${APP_PACKAGE} import app 80 | def main(argv): 81 | app.usage() 82 | app.run() 83 | " 2>&1 | egrep '^ --' >/dev/null || \ 84 | die "Test 12 failed" 85 | 86 | # Test that shorthelp doesn't give flags in this case. 87 | $PYTHON -c "from ${APP_PACKAGE} import app 88 | def main(argv): 89 | app.usage(shorthelp=1) 90 | app.run() 91 | " 2>&1 | grep '^ --' >/dev/null && \ 92 | die "Test 13 failed" 93 | 94 | # Test writeto_stdout. 95 | $PYTHON -c "from ${APP_PACKAGE} import app 96 | def main(argv): 97 | app.usage(shorthelp=1, writeto_stdout=1) 98 | app.run() 99 | " | grep 'USAGE' >/dev/null || \ 100 | die "Test 14 failed" 101 | 102 | # Test detailed_error 103 | $PYTHON -c "from ${APP_PACKAGE} import app 104 | def main(argv): 105 | app.usage(shorthelp=1, writeto_stdout=1, detailed_error='BAZBAZ') 106 | app.run() 107 | " 2>&1 | grep 'BAZBAZ' >/dev/null || \ 108 | die "Test 15 failed" 109 | 110 | # Test exitcode 111 | $PYTHON -c "from ${APP_PACKAGE} import app 112 | def main(argv): 113 | app.usage(writeto_stdout=1, exitcode=1) 114 | app.run() 115 | " >/dev/null 116 | if [ "$?" -ne "1" ]; then 117 | die "Test 16 failed" 118 | fi 119 | 120 | # Test --help (this could use wrapping which is tested elsewhere) 121 | $PYTHON -c "from ${APP_PACKAGE} import app 122 | def main(argv): 123 | print 'FAIL' 124 | app.run() 125 | " 2>&1 --help | grep 'USAGE: -c \[flags\]' >/dev/null || \ 126 | die "Test 17 failed" 127 | 128 | # Test --help does not wrap for __main__.__doc__ 129 | $PYTHON -c "from ${APP_PACKAGE} import app 130 | import sys 131 | def main(argv): 132 | print 'FAIL' 133 | doc = [] 134 | for i in xrange(10): 135 | doc.append(str(i)) 136 | doc.append('12345678 ') 137 | sys.modules['__main__'].__doc__ = ''.join(doc) 138 | app.run() 139 | " 2>&1 --help | grep '712345678 812345678' >/dev/null || \ 140 | die "Test 18 failed" 141 | 142 | # Test --help with forced wrap for __main__.__doc__ 143 | $PYTHON -c "from ${APP_PACKAGE} import app 144 | import sys 145 | def main(argv): 146 | print 'FAIL' 147 | doc = [] 148 | for i in xrange(10): 149 | doc.append(str(i)) 150 | doc.append('12345678 ') 151 | sys.modules['__main__'].__doc__ = ''.join(doc) 152 | app.SetEnableHelpWrapping() 153 | app.run() 154 | " 2>&1 --help | grep '712345678 812345678' >/dev/null && \ 155 | die "Test 19 failed" 156 | 157 | 158 | # Test UsageError 159 | $PYTHON -c "from ${APP_PACKAGE} import app 160 | def main(argv): 161 | raise app.UsageError('You made a usage error') 162 | app.run() 163 | " 2>&1 | grep "You made a usage error" >/dev/null || \ 164 | die "Test 20 failed" 165 | 166 | # Test UsageError exit code 167 | $PYTHON -c "from ${APP_PACKAGE} import app 168 | def main(argv): 169 | raise app.UsageError('You made a usage error', exitcode=64) 170 | app.run() 171 | " > /dev/null 2>&1 172 | if [ "$?" -ne "64" ]; then 173 | die "Test 21 failed" 174 | fi 175 | 176 | # Test catching top-level exceptions. We should get the exception name on 177 | # stderr. 178 | ./app_test_helper.py \ 179 | --raise_exception 2>&1 | grep -q 'MyException' || die "Test 23 failed" 180 | 181 | # Test exception handlers are called 182 | have_handler_output=$TEST_TMPDIR/handler.txt 183 | $PYTHON -c "from ${APP_PACKAGE} import app 184 | def main(argv): 185 | raise ValueError('look for me') 186 | 187 | class TestExceptionHandler(app.ExceptionHandler): 188 | def __init__(self, msg): 189 | self.msg = msg 190 | 191 | def Handle(self, exc): 192 | print '%s %s' % (self.msg, exc) 193 | 194 | app.InstallExceptionHandler(TestExceptionHandler('first')) 195 | app.InstallExceptionHandler(TestExceptionHandler('second')) 196 | app.run() 197 | " > $have_handler_output 2>&1 198 | grep -q "first look for me" $have_handler_output || die "Test 24 failed" 199 | grep -q "second look for me" $have_handler_output || die "Test 25 failed" 200 | 201 | no_handler_output=$TEST_TMPDIR/no_handler.txt 202 | # Test exception handlers are not called for "normal" exits 203 | for exc in "SystemExit(1)" "app.UsageError('foo')"; do 204 | $PYTHON -c "from ${APP_PACKAGE} import app 205 | def main(argv): 206 | raise $exc 207 | 208 | class TestExceptionHandler(app.ExceptionHandler): 209 | def Handle(self, exc): 210 | print 'handler was called' 211 | 212 | app.InstallExceptionHandler(TestExceptionHandler()) 213 | app.run() 214 | " > $no_handler_output 2>&1 215 | grep -q "handler was called" $no_handler_output && die "Test 26 ($exc) failed" 216 | done 217 | 218 | 219 | # Test --help expands docstring. 220 | $PYTHON -c " 221 | '''USAGE: %s [flags]''' 222 | from ${APP_PACKAGE} import app 223 | def main(argv): print 'FAIL' 224 | app.run() 225 | " --help 2>&1 | 226 | fgrep 'USAGE: -c [flags]' >/dev/null || 227 | die "Test 27 failed" 228 | 229 | 230 | # Test --help expands docstring. 231 | $PYTHON -c " 232 | '''USAGE: %s --fmt=\"%%s\" --fraction=50%%''' 233 | from ${APP_PACKAGE} import app 234 | def main(argv): print 'FAIL' 235 | app.run() 236 | " --help 2>&1 | 237 | fgrep 'USAGE: -c --fmt="%s" --fraction=50%' >/dev/null || 238 | die "Test 28 failed" 239 | 240 | 241 | # Test --help expands docstring. 242 | $PYTHON -c " 243 | '''>%s|%%s|%%%s|%%%%s|%%%%%s<''' 244 | from ${APP_PACKAGE} import app 245 | def main(argv): print 'FAIL' 246 | app.run() 247 | " --help 2>&1 | 248 | fgrep '>-c|%s|%-c|%%s|%%-c<' >/dev/null || 249 | die "Test 29 failed" 250 | 251 | 252 | # Test bad docstring. 253 | $PYTHON -c " 254 | '''>%@<''' 255 | from ${APP_PACKAGE} import app 256 | def main(argv): print 'FAIL' 257 | app.run() 258 | " --help 2>&1 | 259 | fgrep '>%@<' >/dev/null || 260 | die "Test 30 failed" 261 | 262 | readonly HELP_PROG=" 263 | from ${APP_PACKAGE} import app 264 | def main(argv): print 'HI' 265 | app.run() 266 | " 267 | 268 | 269 | echo "PASS" 270 | -------------------------------------------------------------------------------- /tests/appcommands_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2007 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Test tool to demonstrate appcommands.py usage. 17 | 18 | This tool shows how to use appcommands.py. 19 | """ 20 | 21 | 22 | 23 | from google.apputils import appcommands 24 | import gflags as flags 25 | 26 | FLAGS = flags.FLAGS 27 | 28 | flags.DEFINE_string('hint', '', 'Global hint to show in commands') 29 | 30 | 31 | # Name taken from app.py 32 | class Test1(appcommands.Cmd): 33 | """Help for test1. As described by a docstring.""" 34 | 35 | def __init__(self, name, flag_values, **kargs): 36 | """Init and register flags specific to command.""" 37 | super(Test1, self).__init__(name, flag_values, **kargs) 38 | # Flag --fail1 is specific to this command 39 | flags.DEFINE_boolean('fail1', False, 'Make test1 fail', 40 | flag_values=flag_values) 41 | flags.DEFINE_string('foo', '', 'Param foo', flag_values=flag_values) 42 | flags.DEFINE_string('bar', '', 'Param bar', flag_values=flag_values) 43 | flags.DEFINE_integer('intfoo', 0, 'Integer foo', flag_values=flag_values) 44 | flags.DEFINE_boolean('allhelp', False, 'Get _all_commands_help string', 45 | flag_values=flag_values) 46 | 47 | def Run(self, unused_argv): 48 | """Output 'Command1' and flag info. 49 | 50 | Args: 51 | unused_argv: Remaining arguments after parsing flags and command 52 | 53 | Returns: 54 | Value of flag fail1 55 | """ 56 | print 'Command1' 57 | if FLAGS.hint: 58 | print "Hint1:'%s'" % FLAGS.hint 59 | print "Foo1:'%s'" % FLAGS.foo 60 | print "Bar1:'%s'" % FLAGS.bar 61 | if FLAGS.allhelp: 62 | print "AllHelp:'%s'" % self._all_commands_help 63 | return FLAGS.fail1 * 1 64 | 65 | 66 | class Test2(appcommands.Cmd): 67 | """Help for test2.""" 68 | 69 | def __init__(self, name, flag_values, **kargs): 70 | """Init and register flags specific to command.""" 71 | super(Test2, self).__init__(name, flag_values, **kargs) 72 | flags.DEFINE_boolean('fail2', False, 'Make test2 fail', 73 | flag_values=flag_values) 74 | flags.DEFINE_string('foo', '', 'Param foo', flag_values=flag_values) 75 | flags.DEFINE_string('bar', '', 'Param bar', flag_values=flag_values) 76 | 77 | def Run(self, unused_argv): 78 | """Output 'Command2' and flag info. 79 | 80 | Args: 81 | unused_argv: Remaining arguments after parsing flags and command 82 | 83 | Returns: 84 | Value of flag fail2 85 | """ 86 | print 'Command2' 87 | if FLAGS.hint: 88 | print "Hint2:'%s'" % FLAGS.hint 89 | print "Foo2:'%s'" % FLAGS.foo 90 | print "Bar2:'%s'" % FLAGS.bar 91 | return FLAGS.fail2 * 1 92 | 93 | 94 | def Test3(unused_argv): 95 | """Help for test3.""" 96 | print 'Command3' 97 | 98 | 99 | def Test4(unused_argv): 100 | """Help for test4.""" 101 | print 'Command4' 102 | 103 | 104 | def main(unused_argv): 105 | """Register the commands.""" 106 | appcommands.AddCmd('test1', Test1, 107 | command_aliases=['testalias1', 'testalias2']) 108 | appcommands.AddCmd('test1b', Test1, 109 | command_aliases=['testalias1b', 'testalias2b'], 110 | all_commands_help='test1b short help', help_full="""test1b 111 | is my very favorite test 112 | because it has verbose help messages""") 113 | appcommands.AddCmd('test2', Test2) 114 | appcommands.AddCmdFunc('test3', Test3) 115 | appcommands.AddCmdFunc('test4', Test4, command_aliases=['testalias3'], 116 | all_commands_help='replacetest4help') 117 | 118 | 119 | if __name__ == '__main__': 120 | appcommands.Run() 121 | -------------------------------------------------------------------------------- /tests/appcommands_unittest.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2007 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # Author: mboerger@google.com 17 | 18 | PYTHON=$(which python) 19 | function die { 20 | echo "$1" 21 | exit 1 22 | } 23 | 24 | IMPORTS="from google.apputils import app 25 | from google.apputils import appcommands 26 | import gflags as flags" 27 | 28 | # This should exit with error code because no main defined 29 | $PYTHON -c "${IMPORTS} 30 | appcommands.Run()" >/dev/null 2>&1 && \ 31 | die "Test 1 failed" 32 | 33 | # Standard use. This should exit successfully 34 | $PYTHON -c "${IMPORTS} 35 | import sys 36 | def test(argv): 37 | return 0 38 | def main(argv): 39 | appcommands.AddCmdFunc('test', test) 40 | appcommands.Run() 41 | sys.exit(1)" test || \ 42 | die "Test 2 failed" 43 | 44 | # Even with no return from Cmds Run() does not return 45 | $PYTHON -c "${IMPORTS} 46 | import sys 47 | def test(argv): 48 | return 49 | def main(argv): 50 | appcommands.AddCmdFunc('test', test) 51 | appcommands.Run() 52 | sys.exit(1)" test || \ 53 | die "Test 3 failed" 54 | 55 | # Standard use with returning an error code. 56 | $PYTHON -c "${IMPORTS} 57 | import sys 58 | def test(argv): 59 | return 1 60 | def main(argv): 61 | appcommands.AddCmdFunc('test', test) 62 | appcommands.Run() 63 | sys.exit(0)" test && \ 64 | die "Test 4 failed" 65 | 66 | # Executing two commands in single mode does not work (execute only first) 67 | $PYTHON -c "${IMPORTS} 68 | def test1(argv): 69 | return 0 70 | def test2(argv): 71 | return 1 72 | def main(argv): 73 | appcommands.AddCmdFunc('test1', test1) 74 | appcommands.AddCmdFunc('test2', test2) 75 | appcommands.Run()" test1 test2 || \ 76 | die "Test 5 failed" 77 | 78 | # Registering a command twice does not work. 79 | $PYTHON -c "${IMPORTS} 80 | def test1(argv): 81 | return 0 82 | def main(argv): 83 | appcommands.AddCmdFunc('test', test1) 84 | appcommands.AddCmdFunc('test', test1) 85 | appcommands.Run()" test >/dev/null 2>&1 && \ 86 | die "Test 6 failed" 87 | 88 | # Executing help, returns non zero return code (1), then check result 89 | RES=`$PYTHON -c "${IMPORTS} 90 | def test1(argv): 91 | '''Help1''' 92 | return 0 93 | def test2(argv): 94 | '''Help2''' 95 | return 0 96 | def main(argv): 97 | appcommands.AddCmdFunc('test1', test1) 98 | appcommands.AddCmdFunc('test2', test2) 99 | appcommands.Run()" help` && die "Test 7 failed" 100 | 101 | echo "${RES}" | grep -q "USAGE: " || die "Test 8 failed" 102 | echo "${RES}" | sed -ne '/following commands:/,/.*/p' | \ 103 | grep -q "help, test1, test2" || die "Test 9 failed" 104 | echo "${RES}" | grep -q -E "(^| )test1[ \t]+Help1($| )" || die "Test 10 failed" 105 | echo "${RES}" | grep -q -E "(^| )test2[ \t]+Help2($| )" || die "Test 11 failed" 106 | 107 | # Executing help for command, returns non zero return code (1), then check result 108 | RES=`$PYTHON -c "${IMPORTS} 109 | def test1(argv): 110 | '''Help1''' 111 | return 0 112 | def test2(argv): 113 | '''Help2''' 114 | return 0 115 | def main(argv): 116 | appcommands.AddCmdFunc('test1', test1) 117 | appcommands.AddCmdFunc('test2', test2) 118 | appcommands.Run()" help test2` && die "Test 12 failed" 119 | 120 | echo "${RES}" | grep -q "USAGE: " || die "Test 13 failed" 121 | echo "${RES}" | grep -q -E "(^| )Any of the following commands:" && die "Test 14 failed" 122 | echo "${RES}" | grep -q -E "(^| )test1[ \t]+" && die "Test 15 failed" 123 | echo "${RES}" | grep -q -E "(^| )test2[ \t]+Help2($| )" || die "Test 16 failed" 124 | 125 | # Returning False succeeds 126 | $PYTHON -c "${IMPORTS} 127 | def test(argv): return False 128 | def main(argv): 129 | appcommands.AddCmdFunc('test', test) 130 | appcommands.Run()" test || die "Test 17 failed" 131 | 132 | # Returning True fails 133 | $PYTHON -c "${IMPORTS} 134 | def test(argv): return True 135 | def main(argv): 136 | appcommands.AddCmdFunc('test', test) 137 | appcommands.Run()" test && die "Test 18 failed" 138 | 139 | # Registering using AddCmd instead of AddCmdFunc, should be the normal case 140 | $PYTHON -c "${IMPORTS} 141 | class test(appcommands.Cmd): 142 | def Run(self, argv): return 0 143 | def main(argv): 144 | appcommands.AddCmd('test', test) 145 | appcommands.Run()" test || die "Test 19 failed" 146 | 147 | # Registering using AddCmd instead of AddCmdFunc, now fail 148 | $PYTHON -c "${IMPORTS} 149 | class test(appcommands.Cmd): 150 | def Run(self, argv): return 1 151 | def main(argv): 152 | appcommands.AddCmd('test', test) 153 | appcommands.Run()" test && die "Test 20 failed" 154 | 155 | TEST=./appcommands_example.py 156 | 157 | if test -s "${TEST}.py"; then 158 | TEST="${TEST}.py" 159 | elif test ! -s "${TEST}"; then 160 | die "Could not locate ${TEST}" 161 | fi 162 | 163 | # Success 164 | $PYTHON $TEST test1 >/dev/null 2>&1 || die "Test 21 failed" 165 | $PYTHON $TEST test1|grep -q 'Command1' 2>&1 || die "Test 22 failed" 166 | $PYTHON $TEST test2|grep -q 'Command2' 2>&1 || die "Test 23 failed" 167 | $PYTHON $TEST test3|grep -q 'Command3' 2>&1 || die "Test 24 failed" 168 | 169 | # Success, --nofail1 belongs to test1 170 | $PYTHON $TEST test1 --nofail1 >/dev/null 2>&1 || die "Test 25 failed" 171 | 172 | # Failure, --fail1 173 | $PYTHON $TEST test1 --fail1 >/dev/null 2>&1 && die "Test 26 failed" 174 | 175 | # Failure, --nofail1 does not belong to test2 176 | $PYTHON $TEST test2 --nofail1 >/dev/null 2>&1 && die "Test 27 failed" 177 | 178 | # Failure, --nofail1 must appear after its command 179 | $PYTHON $TEST --nofail1 test1 >/dev/null 2>&1 && die "Test 28 failed" 180 | 181 | # Failure, explicit from --fail2 182 | $PYTHON $TEST test2 --fail2 >/dev/null 2>&1 && die "Test 29 failed" 183 | 184 | # Success, --hint before command, foo shown with test1 185 | $PYTHON $TEST --hint 'XYZ' test1|grep -q "Hint1:'XYZ'" || die "Test 30 failed" 186 | 187 | # Success, --hint before command, foo shown with test1 188 | $PYTHON $TEST test1 --hint 'XYZ'|grep -q "Hint1:'XYZ'" || die "Test 31 failed" 189 | 190 | # Success, test1b --allhelp, modified _all_commands_help shown 191 | $PYTHON $TEST test1b --allhelp|grep -q "AllHelp:'test1b short help'" || die "Test 32 failed" 192 | 193 | # Failure, test1 --allhelp, modified _all_commands_help not shown 194 | $PYTHON $TEST test1 --allhelp|grep -q "AllHelp:'test1b short help'" && die "Test 33 failed" 195 | 196 | # Test for standard --help 197 | $PYTHON $TEST --help|grep -q "following commands:" && die "Test 34 failed" 198 | $PYTHON $TEST help|grep -q "following commands:" || die "Test 35 failed" 199 | 200 | # No help after command 201 | $PYTHON $TEST test1 --help|grep -q "following commands:" && die "Test 36 failed" 202 | $PYTHON $TEST test1 --help 'XYZ'|grep -q "Hint1:'XYZ'" && die "Test 37 failed" 203 | 204 | # Help specific to command: 205 | $PYTHON $TEST --help test1|grep -q "following commands:" && die "Test 38 failed" 206 | $PYTHON $TEST --help test1|grep -q "test1 *Help for test1" && die "Test 39 failed" 207 | $PYTHON $TEST help test1|grep -q "following commands:" && die "Test 40 failed" 208 | $PYTHON $TEST help test1|grep -q "test1, testalias1, testalias2" || die\ 209 | "Test 41 failed" 210 | $PYTHON $TEST help testalias1|grep -q "test1, testalias1, testalias2" || die\ 211 | "Test 42 failed" 212 | $PYTHON $TEST help testalias1|grep -q "[-]-foo" || die\ 213 | "Test 43 failed" 214 | $PYTHON $TEST help testalias2|grep -q "[-]-foo" || die\ 215 | "Test 44 failed" 216 | $PYTHON $TEST help test4|grep -q "^ *Help for test4" || die "Test 45 failed" 217 | $PYTHON $TEST help testalias3|grep -q "^ *Help for test4" || die\ 218 | "Test 46 failed" 219 | 220 | # Help for cmds with all_command_help. 221 | $PYTHON $TEST help|grep -q "Help for test1. As described by a docstring." || die "Test 47 failed" 222 | $PYTHON $TEST help test1|grep -q "Help for test1. As described by a docstring." || die "Test 48 failed" 223 | $PYTHON $TEST help|grep -q "test1b short help" || die "Test 49 failed" 224 | $PYTHON $TEST help test1b|grep -q "test1b short help" && die "Test 50 failed" 225 | $PYTHON $TEST help|grep -q "is my very favorite test" && die "Test 51 failed" 226 | $PYTHON $TEST help test1b|grep -q "is my very favorite test" || die "Test 52 failed" 227 | $PYTHON $TEST help|grep -q "Help for test4." && die "Test 53 failed" 228 | $PYTHON $TEST help test4|grep -q "Help for test4." || die "Test 54 failed" 229 | $PYTHON $TEST help|grep -q "replacetest4help" || die "Test 55 failed" 230 | $PYTHON $TEST help test4|grep -q "replacetest4help" && die "Test 56 failed" 231 | 232 | # Success, --hint before command, foo shown with test1 233 | $PYTHON $TEST --hint 'XYZ' --help|grep -q "following commands:" && die "Test 57 failed" 234 | $PYTHON $TEST --hint 'XYZ' --help|grep -q "XYZ" && die "Test 58 failed" 235 | $PYTHON $TEST --hint 'XYZ' --help|grep -q "This tool shows how" || die "Test 59 failed" 236 | $PYTHON $TEST --hint 'XYZ' help|grep -q "following commands:" || die "Test 60 failed" 237 | $PYTHON $TEST --hint 'XYZ' help|grep -q "XYZ" && die "Test 61 failed" 238 | $PYTHON $TEST --hint 'XYZ' help|grep -q "This tool shows how" || die "Test 62 failed" 239 | 240 | # A command name with an letters, numbers, or an underscore is fine 241 | $PYTHON -c "${IMPORTS} 242 | def test(argv): 243 | return 0 244 | def main(argv): 245 | appcommands.AddCmdFunc('test', test) 246 | appcommands.AddCmdFunc('test_foo', test) 247 | appcommands.AddCmdFunc('a123', test) 248 | appcommands.Run()" test || die "Test 63 failed" 249 | 250 | # A command name that starts with a non-alphanumeric characters is not ok 251 | $PYTHON -c "${IMPORTS} 252 | def test(argv): 253 | return 0 254 | def main(argv): 255 | appcommands.AddCmdFunc('123', test) 256 | appcommands.Run()" 123 >/dev/null 2>&1 && die "Test 64 failed" 257 | 258 | # A command name that contains other characters is not ok 259 | $PYTHON -c "${IMPORTS} 260 | def test(argv): 261 | return 0 262 | def main(argv): 263 | appcommands.AddCmdFunc('test+1', test) 264 | appcommands.Run()" "test+1" >/dev/null 2>&1 && die "Test 65 failed" 265 | 266 | # If a command raises app.UsageError, usage is printed. 267 | RES=`$PYTHON -c "${IMPORTS} 268 | def test(argv): 269 | '''Help1''' 270 | raise app.UsageError('Ha-ha') 271 | def main(argv): 272 | appcommands.AddCmdFunc('test', test) 273 | appcommands.Run()" test` && die "Test 66 failed" 274 | 275 | echo "${RES}" | grep -q "USAGE: " || die "Test 67 failed" 276 | echo "${RES}" | grep -q -E "(^| )test[ \t]+Help1($| )" || die "Test 68 failed" 277 | echo "${RES}" | grep -q -E "(^| )Ha-ha($| )" || die "Test 69 failed" 278 | 279 | 280 | $PYTHON -c "${IMPORTS} 281 | class Test(appcommands.Cmd): 282 | def Run(self, argv): return 0 283 | def test(*args, **kwargs): 284 | return Test(*args, **kwargs) 285 | def main(argv): 286 | appcommands.AddCmd('test', test) 287 | appcommands.Run()" test || die "Test 73 failed" 288 | 289 | # Success, default command set and correctly run. 290 | RES=`$PYTHON -c "${IMPORTS} 291 | class test(appcommands.Cmd): 292 | def Run(self, argv): 293 | print 'test running correctly' 294 | return 0 295 | def main(argv): 296 | appcommands.AddCmd('test', test) 297 | appcommands.SetDefaultCommand('test') 298 | appcommands.Run()"` || die "Test 74 failed" 299 | 300 | echo "${RES}" | grep -q "test running correctly" || die "Test 75 failed" 301 | 302 | # Failure, default command set but missing. 303 | $PYTHON -c "${IMPORTS} 304 | class test(appcommands.Cmd): 305 | def Run(self, argv): 306 | print 'test running correctly' 307 | return 0 308 | def main(argv): 309 | appcommands.AddCmd('test', test) 310 | appcommands.SetDefaultCommand('missing') 311 | appcommands.Run()" >/dev/null 2>&1 && die "Test 76 failed" 312 | 313 | echo "PASS" 314 | -------------------------------------------------------------------------------- /tests/basetest_sh_test.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2003 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # Author: Douglas Greiman 17 | # Owner: unittest-team@google.com 18 | 19 | EXE=./basetest_test.py 20 | function die { 21 | echo "$1" 22 | exit $2 23 | } 24 | 25 | # Create directories for use 26 | function MaybeMkdir { 27 | for dir in $@; do 28 | if [ ! -d "$dir" ] ; then 29 | mkdir "$dir" || die "Unable to create $dir" 30 | fi 31 | done 32 | } 33 | 34 | # TODO(dborowitz): Clean these up if we die. 35 | MaybeMkdir abc cba def fed ghi jkl 36 | 37 | # Test assertListEqual, assertDictEqual, and assertSameElements 38 | $EXE --testid=5 || die "Test 5 failed" $? 39 | 40 | # Test assertAlmostEqual and assertNotAlmostEqual 41 | $EXE --testid=6 || die "Test 6 failed" $? 42 | 43 | # Test that tests marked as "expected failure" but which passes 44 | # cause an overall failure. 45 | $EXE --testid=7 && die "Test 7 passed unexpectedly" $? 46 | output=$($EXE --testid=8 -- -v 2>&1) && die "Test 8 passed unexpectedly" $? 47 | printf '%s\n' "$output" 48 | grep '^FAILED (expected failures=1, unexpected successes=1)' <<<"$output" \ 49 | && grep '^testDifferentExpectedFailure .* unexpected success' <<<"$output" \ 50 | && grep '^testExpectedFailure .* expected failure' <<<"$output" \ 51 | || die "Test 8 didn't write expected diagnostic" 52 | 53 | # Invoke with no env vars and no flags 54 | ( 55 | unset TEST_RANDOM_SEED 56 | unset TEST_SRCDIR 57 | unset TEST_TMPDIR 58 | $EXE --testid=1 59 | ) || die "Test 1 failed" $? 60 | 61 | # Invoke with env vars but no flags 62 | ( 63 | export TEST_RANDOM_SEED=321 64 | export TEST_SRCDIR=cba 65 | export TEST_TMPDIR=fed 66 | $EXE --testid=2 67 | ) || die "Test 2 failed" $? 68 | 69 | # Invoke with no env vars and all flags 70 | ( 71 | unset TEST_RANDOM_SEED 72 | unset TEST_SRCDIR 73 | unset TEST_TMPDIR 74 | $EXE --testid=3 --test_random_seed=123 --test_srcdir=abc --test_tmpdir=def 75 | ) || die "Test 3 failed" $? 76 | 77 | # Invoke with env vars and all flags 78 | ( 79 | export TEST_RANDOM_SEED=321 80 | export TEST_SRCDIR=cba 81 | export TEST_TMPDIR=fed 82 | $EXE --testid=4 --test_random_seed=123 --test_srcdir=abc --test_tmpdir=def 83 | ) || die "Test 4 failed" $? 84 | 85 | # Cleanup 86 | rm -r abc cba def fed ghi jkl 87 | echo "Pass" 88 | -------------------------------------------------------------------------------- /tests/data/a: -------------------------------------------------------------------------------- 1 | test file a contents 2 | -------------------------------------------------------------------------------- /tests/data/b: -------------------------------------------------------------------------------- 1 | test file b contents 2 | -------------------------------------------------------------------------------- /tests/datelib_unittest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2002 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Unittest for datelib.py module.""" 17 | 18 | 19 | 20 | import datetime 21 | import random 22 | import time 23 | 24 | import pytz 25 | 26 | from google.apputils import basetest 27 | from google.apputils import datelib 28 | 29 | 30 | class TimestampUnitTest(basetest.TestCase): 31 | seed = 1979 32 | 33 | def testTzAwareSuccession(self): 34 | a = datelib.Timestamp.now() 35 | b = datelib.Timestamp.utcnow() 36 | 37 | self.assertLessEqual(a, b) 38 | 39 | def testTzRandomConversion(self): 40 | random.seed(self.seed) 41 | for unused_i in xrange(100): 42 | stz = pytz.timezone(random.choice(pytz.all_timezones)) 43 | a = datelib.Timestamp.FromString('2008-04-12T10:00:00', stz) 44 | 45 | b = a 46 | for unused_j in xrange(100): 47 | b = b.astimezone(pytz.timezone(random.choice(pytz.all_timezones))) 48 | self.assertEqual(a, b) 49 | random.seed() 50 | 51 | def testMicroTimestampConversion(self): 52 | """Test that f1(f2(a)) == a.""" 53 | 54 | def IsEq(x): 55 | self.assertEqual( 56 | x, datelib.Timestamp.FromMicroTimestamp(x).AsMicroTimestamp()) 57 | 58 | IsEq(0) 59 | IsEq(datelib.MAXIMUM_MICROSECOND_TIMESTAMP) 60 | 61 | random.seed(self.seed) 62 | for _ in xrange(100): 63 | IsEq(random.randint(0, datelib.MAXIMUM_MICROSECOND_TIMESTAMP)) 64 | 65 | def testMicroTimestampKnown(self): 66 | self.assertEqual(0, datelib.Timestamp.FromString( 67 | '1970-01-01T00:00:00', pytz.UTC).AsMicroTimestamp()) 68 | 69 | self.assertEqual( 70 | datelib.MAXIMUM_MICROSECOND_TIMESTAMP, 71 | datelib.MAXIMUM_MICROSECOND_TIMESTAMP_AS_TS.AsMicroTimestamp()) 72 | 73 | def testMicroTimestampOrdering(self): 74 | """Test that cmp(a, b) == cmp(f1(a), f1(b)).""" 75 | 76 | def IsEq(a, b): 77 | self.assertEqual( 78 | cmp(a, b), 79 | cmp(datelib.Timestamp.FromMicroTimestamp(a), 80 | datelib.Timestamp.FromMicroTimestamp(b))) 81 | 82 | random.seed(self.seed) 83 | for unused_i in xrange(100): 84 | IsEq( 85 | random.randint(0, datelib.MAXIMUM_MICROSECOND_TIMESTAMP), 86 | random.randint(0, datelib.MAXIMUM_MICROSECOND_TIMESTAMP)) 87 | 88 | def testCombine(self): 89 | for tz in (datelib.UTC, datelib.US_PACIFIC): 90 | self.assertEqual( 91 | tz.localize(datelib.Timestamp(1970, 1, 1, 0, 0, 0, 0)), 92 | datelib.Timestamp.combine( 93 | datelib.datetime.date(1970, 1, 1), 94 | datelib.datetime.time(0, 0, 0), 95 | tz)) 96 | 97 | self.assertEqual( 98 | tz.localize(datelib.Timestamp(9998, 12, 31, 23, 59, 59, 999999)), 99 | datelib.Timestamp.combine( 100 | datelib.datetime.date(9998, 12, 31), 101 | datelib.datetime.time(23, 59, 59, 999999), 102 | tz)) 103 | 104 | def testStrpTime(self): 105 | time_str = '20130829 23:43:19.206' 106 | time_fmt = '%Y%m%d %H:%M:%S.%f' 107 | expected = datelib.Timestamp(2013, 8, 29, 23, 43, 19, 206000) 108 | 109 | for tz in (datelib.UTC, datelib.US_PACIFIC): 110 | if tz == datelib.LocalTimezone: 111 | actual = datelib.Timestamp.strptime(time_str, time_fmt) 112 | else: 113 | actual = datelib.Timestamp.strptime(time_str, time_fmt, tz) 114 | self.assertEqual(tz.localize(expected), actual) 115 | 116 | def testFromString1(self): 117 | for string_zero in ( 118 | '1970-01-01 00:00:00', 119 | '19700101T000000', 120 | '1970-01-01T00:00:00' 121 | ): 122 | for testtz in (datelib.UTC, datelib.US_PACIFIC): 123 | self.assertEqual( 124 | datelib.Timestamp.FromString(string_zero, testtz), 125 | testtz.localize(datelib.Timestamp(1970, 1, 1, 0, 0, 0, 0))) 126 | 127 | self.assertEqual( 128 | datelib.Timestamp.FromString( 129 | '1970-01-01T00:00:00+0000', datelib.US_PACIFIC), 130 | datelib.UTC.localize(datelib.Timestamp(1970, 1, 1, 0, 0, 0, 0))) 131 | 132 | startdate = datelib.US_PACIFIC.localize( 133 | datelib.Timestamp(2009, 1, 1, 3, 0, 0, 0)) 134 | for day in xrange(1, 366): 135 | self.assertEqual( 136 | datelib.Timestamp.FromString(startdate.isoformat()), 137 | startdate, 138 | 'FromString works for day %d since 2009-01-01' % day) 139 | startdate += datelib.datetime.timedelta(days=1) 140 | 141 | def testFromString2(self): 142 | """Test correctness of parsing the local time in a given timezone. 143 | 144 | The result shall always be the same as tz.localize(naive_time). 145 | """ 146 | baseday = datelib.datetime.date(2009, 1, 1).toordinal() 147 | for day_offset in xrange(0, 365): 148 | day = datelib.datetime.date.fromordinal(baseday + day_offset) 149 | naive_day = datelib.datetime.datetime.combine( 150 | day, datelib.datetime.time(0, 45, 9)) 151 | 152 | naive_day_str = naive_day.strftime('%Y-%m-%dT%H:%M:%S') 153 | 154 | self.assertEqual( 155 | datelib.US_PACIFIC.localize(naive_day), 156 | datelib.Timestamp.FromString(naive_day_str, tz=datelib.US_PACIFIC), 157 | 'FromString localizes time incorrectly') 158 | 159 | def testFromStringInterval(self): 160 | expected_date = datetime.datetime.utcnow() - datetime.timedelta(days=1) 161 | expected_s = time.mktime(expected_date.utctimetuple()) 162 | actual_date = datelib.Timestamp.FromString('1d') 163 | actual_s = time.mktime(actual_date.timetuple()) 164 | diff_seconds = actual_s - expected_s 165 | self.assertBetween(diff_seconds, 0, 1) 166 | self.assertRaises( 167 | datelib.TimeParseError, datelib.Timestamp.FromString, 'wat') 168 | 169 | 170 | def _EpochToDatetime(t, tz=None): 171 | if tz is not None: 172 | return datelib.datetime.datetime.fromtimestamp(t, tz) 173 | else: 174 | return datelib.datetime.datetime.utcfromtimestamp(t) 175 | 176 | 177 | class DatetimeConversionUnitTest(basetest.TestCase): 178 | def setUp(self): 179 | self.pst = pytz.timezone('US/Pacific') 180 | self.utc = pytz.utc 181 | self.now = time.time() 182 | 183 | def testDatetimeToUTCMicros(self): 184 | self.assertEqual( 185 | 0, datelib.DatetimeToUTCMicros(_EpochToDatetime(0))) 186 | self.assertEqual( 187 | 1001 * long(datelib._MICROSECONDS_PER_SECOND), 188 | datelib.DatetimeToUTCMicros(_EpochToDatetime(1001))) 189 | self.assertEqual(long(self.now * datelib._MICROSECONDS_PER_SECOND), 190 | datelib.DatetimeToUTCMicros(_EpochToDatetime(self.now))) 191 | 192 | # tzinfo shouldn't change the result 193 | self.assertEqual( 194 | 0, datelib.DatetimeToUTCMicros(_EpochToDatetime(0, tz=self.pst))) 195 | 196 | def testDatetimeToUTCMillis(self): 197 | self.assertEqual( 198 | 0, datelib.DatetimeToUTCMillis(_EpochToDatetime(0))) 199 | self.assertEqual( 200 | 1001 * 1000L, datelib.DatetimeToUTCMillis(_EpochToDatetime(1001))) 201 | self.assertEqual(long(self.now * 1000), 202 | datelib.DatetimeToUTCMillis(_EpochToDatetime(self.now))) 203 | 204 | # tzinfo shouldn't change the result 205 | self.assertEqual( 206 | 0, datelib.DatetimeToUTCMillis(_EpochToDatetime(0, tz=self.pst))) 207 | 208 | def testUTCMicrosToDatetime(self): 209 | self.assertEqual(_EpochToDatetime(0), datelib.UTCMicrosToDatetime(0)) 210 | self.assertEqual(_EpochToDatetime(1.000001), 211 | datelib.UTCMicrosToDatetime(1000001)) 212 | self.assertEqual(_EpochToDatetime(self.now), datelib.UTCMicrosToDatetime( 213 | long(self.now * datelib._MICROSECONDS_PER_SECOND))) 214 | 215 | # Check timezone-aware comparisons 216 | self.assertEqual(_EpochToDatetime(0, self.pst), 217 | datelib.UTCMicrosToDatetime(0, tz=self.pst)) 218 | self.assertEqual(_EpochToDatetime(0, self.pst), 219 | datelib.UTCMicrosToDatetime(0, tz=self.utc)) 220 | 221 | def testUTCMillisToDatetime(self): 222 | self.assertEqual(_EpochToDatetime(0), datelib.UTCMillisToDatetime(0)) 223 | self.assertEqual(_EpochToDatetime(1.001), datelib.UTCMillisToDatetime(1001)) 224 | t = time.time() 225 | dt = _EpochToDatetime(t) 226 | # truncate sub-milli time 227 | dt -= datelib.datetime.timedelta(microseconds=dt.microsecond % 1000) 228 | self.assertEqual(dt, datelib.UTCMillisToDatetime(long(t * 1000))) 229 | 230 | # Check timezone-aware comparisons 231 | self.assertEqual(_EpochToDatetime(0, self.pst), 232 | datelib.UTCMillisToDatetime(0, tz=self.pst)) 233 | self.assertEqual(_EpochToDatetime(0, self.pst), 234 | datelib.UTCMillisToDatetime(0, tz=self.utc)) 235 | 236 | 237 | class MicrosecondsToSecondsUnitTest(basetest.TestCase): 238 | 239 | def testConversionFromMicrosecondsToSeconds(self): 240 | self.assertEqual(0.0, datelib.MicrosecondsToSeconds(0)) 241 | self.assertEqual(7.0, datelib.MicrosecondsToSeconds(7000000)) 242 | self.assertEqual(1.234567, datelib.MicrosecondsToSeconds(1234567)) 243 | self.assertEqual(12345654321.123456, 244 | datelib.MicrosecondsToSeconds(12345654321123456)) 245 | 246 | if __name__ == '__main__': 247 | basetest.main() 248 | -------------------------------------------------------------------------------- /tests/file_util_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2007 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Unittest for common file utilities.""" 17 | 18 | 19 | 20 | import __builtin__ 21 | import errno 22 | import os 23 | import posix 24 | import pwd 25 | import shutil 26 | import stat 27 | import tempfile 28 | 29 | import mox 30 | 31 | from google.apputils import basetest 32 | from google.apputils import file_util 33 | import gflags as flags 34 | 35 | 36 | FLAGS = flags.FLAGS 37 | 38 | # pylint is dumb about mox: 39 | # pylint: disable=no-member 40 | 41 | 42 | class FileUtilTest(basetest.TestCase): 43 | 44 | def testHomeDir(self): 45 | self.assertEqual(file_util.HomeDir(), pwd.getpwuid(os.geteuid()).pw_dir) 46 | self.assertEqual(file_util.HomeDir(0), pwd.getpwuid(0).pw_dir) 47 | self.assertEqual(file_util.HomeDir('root'), pwd.getpwnam('root').pw_dir) 48 | 49 | 50 | 51 | class FileUtilTempdirTest(basetest.TestCase): 52 | 53 | def setUp(self): 54 | self.temp_dir = tempfile.mkdtemp() 55 | self.file_path = self.temp_dir + 'sample.txt' 56 | self.sample_contents = 'Random text: aldmkfhjwoem103u74.' 57 | # To avoid confusion in the mode tests. 58 | self.prev_umask = posix.umask(0) 59 | 60 | def tearDown(self): 61 | shutil.rmtree(self.temp_dir) 62 | posix.umask(self.prev_umask) 63 | 64 | def testWriteOverwrite(self): 65 | file_util.Write(self.file_path, 'original contents') 66 | file_util.Write(self.file_path, self.sample_contents) 67 | with open(self.file_path) as fp: 68 | self.assertEquals(fp.read(), self.sample_contents) 69 | 70 | def testWriteExclusive(self): 71 | file_util.Write(self.file_path, 'original contents') 72 | self.assertRaises(OSError, file_util.Write, self.file_path, 73 | self.sample_contents, overwrite_existing=False) 74 | 75 | def testWriteMode(self): 76 | mode = 0744 77 | file_util.Write(self.file_path, self.sample_contents, mode=mode) 78 | s = os.stat(self.file_path) 79 | self.assertEqual(stat.S_IMODE(s.st_mode), mode) 80 | 81 | def testAtomicWriteSuccessful(self): 82 | file_util.AtomicWrite(self.file_path, self.sample_contents) 83 | with open(self.file_path) as fp: 84 | self.assertEquals(fp.read(), self.sample_contents) 85 | 86 | def testAtomicWriteMode(self): 87 | mode = 0745 88 | file_util.AtomicWrite(self.file_path, self.sample_contents, mode=mode) 89 | s = os.stat(self.file_path) 90 | self.assertEqual(stat.S_IMODE(s.st_mode), mode) 91 | 92 | 93 | class FileUtilMoxTestBase(basetest.TestCase): 94 | 95 | def setUp(self): 96 | self.mox = mox.Mox() 97 | self.sample_contents = 'Contents of the file' 98 | self.file_path = '/path/to/some/file' 99 | self.fd = 'a file descriptor' 100 | 101 | def tearDown(self): 102 | # In case a test fails before it gets to the unset line. 103 | self.mox.UnsetStubs() 104 | 105 | 106 | class FileUtilMoxTest(FileUtilMoxTestBase): 107 | 108 | def testListDirPath(self): 109 | self.mox.StubOutWithMock(os, 'listdir') 110 | dir_contents = ['file1', 'file2', 'file3', 'directory1', 'file4', 111 | 'directory2'] 112 | os.listdir('/path/to/some/directory').AndReturn(dir_contents) 113 | self.mox.ReplayAll() 114 | self.assertListEqual(file_util.ListDirPath('/path/to/some/directory'), 115 | ['%s/%s' % ('/path/to/some/directory', entry) 116 | for entry in dir_contents]) 117 | self.mox.VerifyAll() 118 | 119 | def testSuccessfulRead(self): 120 | file_handle = self.mox.CreateMockAnything() 121 | self.mox.StubOutWithMock(__builtin__, 'open', use_mock_anything=True) 122 | open(self.file_path).AndReturn(file_handle) 123 | file_handle.__enter__().AndReturn(file_handle) 124 | file_handle.read().AndReturn(self.sample_contents) 125 | file_handle.__exit__(None, None, None) 126 | 127 | self.mox.ReplayAll() 128 | try: 129 | self.assertEquals(file_util.Read(self.file_path), self.sample_contents) 130 | self.mox.VerifyAll() 131 | finally: 132 | # Because we mock out the built-in open() function, which the unittest 133 | # library depends on, we need to make sure we revert it before leaving the 134 | # test, otherwise any test failures will cause further internal failures 135 | # and yield no meaningful error output. 136 | self.mox.ResetAll() 137 | self.mox.UnsetStubs() 138 | 139 | def testWriteGroup(self): 140 | self.mox.StubOutWithMock(os, 'open') 141 | self.mox.StubOutWithMock(os, 'write') 142 | self.mox.StubOutWithMock(os, 'close') 143 | self.mox.StubOutWithMock(os, 'chown') 144 | gid = 'new gid' 145 | os.open(self.file_path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 146 | 0666).AndReturn(self.fd) 147 | os.write(self.fd, self.sample_contents) 148 | os.close(self.fd) 149 | os.chown(self.file_path, -1, gid) 150 | self.mox.ReplayAll() 151 | file_util.Write(self.file_path, self.sample_contents, gid=gid) 152 | self.mox.VerifyAll() 153 | 154 | 155 | class AtomicWriteMoxTest(FileUtilMoxTestBase): 156 | 157 | def setUp(self): 158 | super(AtomicWriteMoxTest, self).setUp() 159 | self.mox.StubOutWithMock(tempfile, 'mkstemp') 160 | self.mox.StubOutWithMock(os, 'write') 161 | self.mox.StubOutWithMock(os, 'close') 162 | self.mox.StubOutWithMock(os, 'chmod') 163 | self.mox.StubOutWithMock(os, 'rename') 164 | self.mox.StubOutWithMock(os, 'remove') 165 | self.mode = 'new permissions' 166 | self.gid = 'new gid' 167 | self.temp_filename = '/some/temp/file' 168 | self.os_error = OSError('A problem renaming!') 169 | 170 | tempfile.mkstemp(dir='/path/to/some').AndReturn( 171 | (self.fd, self.temp_filename)) 172 | os.write(self.fd, self.sample_contents) 173 | os.close(self.fd) 174 | os.chmod(self.temp_filename, self.mode) 175 | 176 | def tearDown(self): 177 | self.mox.UnsetStubs() 178 | 179 | def testAtomicWriteGroup(self): 180 | self.mox.StubOutWithMock(os, 'chown') 181 | os.chown(self.temp_filename, -1, self.gid) 182 | os.rename(self.temp_filename, self.file_path) 183 | self.mox.ReplayAll() 184 | file_util.AtomicWrite(self.file_path, self.sample_contents, 185 | mode=self.mode, gid=self.gid) 186 | self.mox.VerifyAll() 187 | 188 | def testAtomicWriteGroupError(self): 189 | self.mox.StubOutWithMock(os, 'chown') 190 | os.chown(self.temp_filename, -1, self.gid).AndRaise(self.os_error) 191 | os.remove(self.temp_filename) 192 | self.mox.ReplayAll() 193 | self.assertRaises(OSError, file_util.AtomicWrite, self.file_path, 194 | self.sample_contents, mode=self.mode, gid=self.gid) 195 | 196 | self.mox.VerifyAll() 197 | 198 | def testRenamingError(self): 199 | os.rename(self.temp_filename, self.file_path).AndRaise(self.os_error) 200 | os.remove(self.temp_filename) 201 | self.mox.ReplayAll() 202 | self.assertRaises(OSError, file_util.AtomicWrite, self.file_path, 203 | self.sample_contents, mode=self.mode) 204 | self.mox.VerifyAll() 205 | 206 | def testRenamingErrorWithRemoveError(self): 207 | extra_error = OSError('A problem removing!') 208 | os.rename(self.temp_filename, self.file_path).AndRaise(self.os_error) 209 | os.remove(self.temp_filename).AndRaise(extra_error) 210 | self.mox.ReplayAll() 211 | try: 212 | file_util.AtomicWrite(self.file_path, self.sample_contents, 213 | mode=self.mode) 214 | except OSError as e: 215 | self.assertEquals(str(e), 216 | 'A problem renaming!. Additional errors cleaning up: ' 217 | 'A problem removing!') 218 | else: 219 | raise self.failureException('OSError not raised by AtomicWrite') 220 | self.mox.VerifyAll() 221 | 222 | 223 | class TemporaryFilesMoxTest(FileUtilMoxTestBase): 224 | 225 | def testTemporaryFileWithContents(self): 226 | contents = 'Inspiration!' 227 | with file_util.TemporaryFileWithContents(contents) as temporary_file: 228 | filename = temporary_file.name 229 | contents_read = open(temporary_file.name).read() 230 | self.assertEqual(contents_read, contents) 231 | 232 | # Ensure that the file does not exist. 233 | self.assertFalse(os.path.exists(filename)) 234 | 235 | 236 | class TemporaryDirsMoxTest(FileUtilMoxTestBase): 237 | 238 | def testTemporaryDirectoryWithException(self): 239 | def Inner(accumulator): 240 | with file_util.TemporaryDirectory(base_path=FLAGS.test_tmpdir) as tmpdir: 241 | self.assertTrue(os.path.isdir(tmpdir)) 242 | accumulator.append(tmpdir) 243 | raise Exception('meh') 244 | 245 | temp_dirs = [] 246 | self.assertRaises(Exception, Inner, temp_dirs) 247 | # Ensure that the directory is removed on exit even when exceptions happen. 248 | self.assertEquals(len(temp_dirs), 1) 249 | self.assertFalse(os.path.isdir(temp_dirs[0])) 250 | 251 | def testTemporaryDirectory(self): 252 | with file_util.TemporaryDirectory(base_path=FLAGS.test_tmpdir) as temp_dir: 253 | self.assertTrue(os.path.isdir(temp_dir)) 254 | 255 | # Ensure that the directory is removed on exit. 256 | self.assertFalse(os.path.isdir(temp_dir)) 257 | 258 | 259 | class MkDirsMoxTest(FileUtilMoxTestBase): 260 | 261 | # pylint is dumb about mox: 262 | # pylint: disable=maybe-no-member 263 | 264 | def setUp(self): 265 | super(MkDirsMoxTest, self).setUp() 266 | self.mox.StubOutWithMock(os, 'mkdir') 267 | self.mox.StubOutWithMock(os, 'chmod') 268 | self.mox.StubOutWithMock(os.path, 'isdir') 269 | self.dir_tree = ['/path', 'to', 'some', 'directory'] 270 | 271 | def tearDown(self): 272 | self.mox.UnsetStubs() 273 | 274 | def testNoErrorsAbsoluteOneDir(self): 275 | # record, replay 276 | os.mkdir('/foo') 277 | self.mox.ReplayAll() 278 | # test, verify 279 | file_util.MkDirs('/foo') 280 | self.mox.VerifyAll() 281 | 282 | def testNoErrorsAbsoluteOneDirWithForceMode(self): 283 | # record, replay 284 | os.mkdir('/foo') 285 | os.chmod('/foo', 0707) 286 | self.mox.ReplayAll() 287 | # test, verify 288 | file_util.MkDirs('/foo', force_mode=0707) 289 | self.mox.VerifyAll() 290 | 291 | def testNoErrorsExistingDirWithForceMode(self): 292 | exist_error = OSError(errno.EEXIST, 'This string not used') 293 | # record, replay 294 | os.mkdir('/foo').AndRaise(exist_error) 295 | # no chmod is called since the dir exists 296 | os.path.isdir('/foo').AndReturn(True) 297 | self.mox.ReplayAll() 298 | # test, verify 299 | file_util.MkDirs('/foo', force_mode=0707) 300 | self.mox.VerifyAll() 301 | 302 | def testNoErrorsAbsoluteSlashDot(self): 303 | # record, replay 304 | os.mkdir('/foo') 305 | self.mox.ReplayAll() 306 | # test, verify 307 | file_util.MkDirs('/foo/.') 308 | self.mox.VerifyAll() 309 | 310 | def testNoErrorsAbsoluteExcessiveSlashDot(self): 311 | """See that normpath removes irrelevant .'s in the path.""" 312 | # record, replay 313 | os.mkdir('/foo') 314 | os.mkdir('/foo/bar') 315 | self.mox.ReplayAll() 316 | # test, verify 317 | file_util.MkDirs('/./foo/./././bar/.') 318 | self.mox.VerifyAll() 319 | 320 | def testNoErrorsAbsoluteTwoDirs(self): 321 | # record, replay 322 | os.mkdir('/foo') 323 | os.mkdir('/foo/bar') 324 | self.mox.ReplayAll() 325 | # test, verify 326 | file_util.MkDirs('/foo/bar') 327 | self.mox.VerifyAll() 328 | 329 | def testNoErrorsPartialTwoDirsWithForceMode(self): 330 | exist_error = OSError(errno.EEXIST, 'This string not used') 331 | # record, replay 332 | os.mkdir('/foo').AndRaise(exist_error) # /foo exists 333 | os.path.isdir('/foo').AndReturn(True) 334 | os.mkdir('/foo/bar') # bar does not 335 | os.chmod('/foo/bar', 0707) 336 | self.mox.ReplayAll() 337 | # test, verify 338 | file_util.MkDirs('/foo/bar', force_mode=0707) 339 | self.mox.VerifyAll() 340 | 341 | def testNoErrorsRelativeOneDir(self): 342 | # record, replay 343 | os.mkdir('foo') 344 | self.mox.ReplayAll() 345 | # test, verify 346 | file_util.MkDirs('foo') 347 | self.mox.VerifyAll() 348 | 349 | def testNoErrorsRelativeTwoDirs(self): 350 | # record, replay 351 | os.mkdir('foo') 352 | os.mkdir('foo/bar') 353 | self.mox.ReplayAll() 354 | # test, verify 355 | file_util.MkDirs('foo/bar') 356 | self.mox.VerifyAll() 357 | 358 | def testDirectoriesExist(self): 359 | exist_error = OSError(errno.EEXIST, 'This string not used') 360 | # record, replay 361 | for i in range(len(self.dir_tree)): 362 | path = os.path.join(*self.dir_tree[:i+1]) 363 | os.mkdir(path).AndRaise(exist_error) 364 | os.path.isdir(path).AndReturn(True) 365 | self.mox.ReplayAll() 366 | # test, verify 367 | file_util.MkDirs(os.path.join(*self.dir_tree)) 368 | self.mox.VerifyAll() 369 | 370 | def testFileInsteadOfDirectory(self): 371 | exist_error = OSError(errno.EEXIST, 'This string not used') 372 | path = self.dir_tree[0] 373 | # record, replay 374 | os.mkdir(path).AndRaise(exist_error) 375 | os.path.isdir(path).AndReturn(False) 376 | self.mox.ReplayAll() 377 | # test, verify 378 | self.assertRaises(OSError, file_util.MkDirs, os.path.join(*self.dir_tree)) 379 | self.mox.VerifyAll() 380 | 381 | def testNonExistsError(self): 382 | non_exist_error = OSError(errno.ETIMEDOUT, 'This string not used') 383 | path = self.dir_tree[0] 384 | # record, replay 385 | os.mkdir(path).AndRaise(non_exist_error) 386 | self.mox.ReplayAll() 387 | # test, verify 388 | self.assertRaises(OSError, file_util.MkDirs, os.path.join(*self.dir_tree)) 389 | self.mox.VerifyAll() 390 | 391 | 392 | class RmDirsTestCase(mox.MoxTestBase): 393 | 394 | def testRmDirs(self): 395 | test_sandbox = os.path.join(FLAGS.test_tmpdir, 'test-rm-dirs') 396 | test_dir = os.path.join(test_sandbox, 'test', 'dir') 397 | 398 | os.makedirs(test_sandbox) 399 | with open(os.path.join(test_sandbox, 'file'), 'w'): 400 | pass 401 | os.makedirs(test_dir) 402 | with open(os.path.join(test_dir, 'file'), 'w'): 403 | pass 404 | 405 | file_util.RmDirs(test_dir) 406 | 407 | self.assertFalse(os.path.exists(os.path.join(test_sandbox, 'test'))) 408 | self.assertTrue(os.path.exists(os.path.join(test_sandbox, 'file'))) 409 | 410 | shutil.rmtree(test_sandbox) 411 | 412 | def testRmDirsForNonExistingDirectory(self): 413 | self.mox.StubOutWithMock(os, 'rmdir') 414 | os.rmdir('path/to') 415 | os.rmdir('path') 416 | 417 | self.mox.StubOutWithMock(shutil, 'rmtree') 418 | shutil.rmtree('path/to/directory').AndRaise( 419 | OSError(errno.ENOENT, "No such file or directory 'path/to/directory'")) 420 | 421 | self.mox.ReplayAll() 422 | 423 | file_util.RmDirs('path/to/directory') 424 | self.mox.VerifyAll() 425 | 426 | def testRmDirsForNonExistingParentDirectory(self): 427 | self.mox.StubOutWithMock(os, 'rmdir') 428 | os.rmdir('path/to').AndRaise( 429 | OSError(errno.ENOENT, "No such file or directory 'path/to'")) 430 | os.rmdir('path') 431 | 432 | self.mox.StubOutWithMock(shutil, 'rmtree') 433 | shutil.rmtree('path/to/directory').AndRaise( 434 | OSError(errno.ENOENT, "No such file or directory 'path/to/directory'")) 435 | 436 | self.mox.ReplayAll() 437 | 438 | file_util.RmDirs('path/to/directory') 439 | self.mox.VerifyAll() 440 | 441 | def testRmDirsForNotEmptyDirectory(self): 442 | self.mox.StubOutWithMock(os, 'rmdir') 443 | os.rmdir('path/to').AndRaise( 444 | OSError(errno.ENOTEMPTY, 'Directory not empty', 'path/to')) 445 | 446 | self.mox.StubOutWithMock(shutil, 'rmtree') 447 | shutil.rmtree('path/to/directory') 448 | 449 | self.mox.ReplayAll() 450 | 451 | file_util.RmDirs('path/to/directory') 452 | self.mox.VerifyAll() 453 | 454 | def testRmDirsForPermissionDeniedOnParentDirectory(self): 455 | self.mox.StubOutWithMock(os, 'rmdir') 456 | os.rmdir('path/to').AndRaise( 457 | OSError(errno.EACCES, 'Permission denied', 'path/to')) 458 | 459 | self.mox.StubOutWithMock(shutil, 'rmtree') 460 | shutil.rmtree('path/to/directory') 461 | 462 | self.mox.ReplayAll() 463 | 464 | file_util.RmDirs('path/to/directory') 465 | self.mox.VerifyAll() 466 | 467 | def testRmDirsWithSimplePath(self): 468 | self.mox.StubOutWithMock(shutil, 'rmtree') 469 | shutil.rmtree('directory') 470 | 471 | self.mox.ReplayAll() 472 | 473 | file_util.RmDirs('directory') 474 | self.mox.VerifyAll() 475 | 476 | 477 | if __name__ == '__main__': 478 | basetest.main() 479 | -------------------------------------------------------------------------------- /tests/humanize_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Test for google.apputils.humanize.""" 5 | 6 | 7 | 8 | import datetime 9 | from google.apputils import basetest 10 | from google.apputils import datelib 11 | from google.apputils import humanize 12 | 13 | 14 | class HumanizeTest(basetest.TestCase): 15 | 16 | def testCommas(self): 17 | self.assertEqual('0', humanize.Commas(0)) 18 | self.assertEqual('100', humanize.Commas(100)) 19 | self.assertEqual('1,000', humanize.Commas(1000)) 20 | self.assertEqual('10,000', humanize.Commas(10000)) 21 | self.assertEqual('1,000,000', humanize.Commas(1e6)) 22 | self.assertEqual('-1,000,000', humanize.Commas(-1e6)) 23 | 24 | def testPlural(self): 25 | self.assertEqual('0 objects', humanize.Plural(0, 'object')) 26 | self.assertEqual('1 object', humanize.Plural(1, 'object')) 27 | self.assertEqual('-1 objects', humanize.Plural(-1, 'object')) 28 | self.assertEqual('42 objects', humanize.Plural(42, 'object')) 29 | self.assertEqual('42 cats', humanize.Plural(42, 'cat')) 30 | self.assertEqual('42 glasses', humanize.Plural(42, 'glass')) 31 | self.assertEqual('42 potatoes', humanize.Plural(42, 'potato')) 32 | self.assertEqual('42 cherries', humanize.Plural(42, 'cherry')) 33 | self.assertEqual('42 monkeys', humanize.Plural(42, 'monkey')) 34 | self.assertEqual('42 oxen', humanize.Plural(42, 'ox', 'oxen')) 35 | self.assertEqual('42 indices', humanize.Plural(42, 'index')) 36 | self.assertEqual( 37 | '42 attorneys general', 38 | humanize.Plural(42, 'attorney general', 'attorneys general')) 39 | 40 | def testPluralWord(self): 41 | self.assertEqual('vaxen', humanize.PluralWord(2, 'vax', plural='vaxen')) 42 | self.assertEqual('cores', humanize.PluralWord(2, 'core')) 43 | self.assertEqual('group', humanize.PluralWord(1, 'group')) 44 | self.assertEqual('cells', humanize.PluralWord(0, 'cell')) 45 | self.assertEqual('degree', humanize.PluralWord(1.0, 'degree')) 46 | self.assertEqual('helloes', humanize.PluralWord(3.14, 'hello')) 47 | 48 | def testWordSeries(self): 49 | self.assertEqual('', humanize.WordSeries([])) 50 | self.assertEqual('foo', humanize.WordSeries(['foo'])) 51 | self.assertEqual('foo and bar', humanize.WordSeries(['foo', 'bar'])) 52 | self.assertEqual( 53 | 'foo, bar, and baz', humanize.WordSeries(['foo', 'bar', 'baz'])) 54 | self.assertEqual( 55 | 'foo, bar, or baz', humanize.WordSeries(['foo', 'bar', 'baz'], 56 | conjunction='or')) 57 | 58 | def testAddIndefiniteArticle(self): 59 | self.assertEqual('a thing', humanize.AddIndefiniteArticle('thing')) 60 | self.assertEqual('an object', humanize.AddIndefiniteArticle('object')) 61 | self.assertEqual('a Porsche', humanize.AddIndefiniteArticle('Porsche')) 62 | self.assertEqual('an Audi', humanize.AddIndefiniteArticle('Audi')) 63 | 64 | def testDecimalPrefix(self): 65 | self.assertEqual('0 m', humanize.DecimalPrefix(0, 'm')) 66 | self.assertEqual('1 km', humanize.DecimalPrefix(1000, 'm')) 67 | self.assertEqual('-1 km', humanize.DecimalPrefix(-1000, 'm')) 68 | self.assertEqual('10 Gbps', humanize.DecimalPrefix(10e9, 'bps')) 69 | self.assertEqual('6000 Yg', humanize.DecimalPrefix(6e27, 'g')) 70 | self.assertEqual('12.1 km', humanize.DecimalPrefix(12100, 'm', precision=3)) 71 | self.assertEqual('12 km', humanize.DecimalPrefix(12100, 'm', precision=2)) 72 | self.assertEqual('1.15 km', humanize.DecimalPrefix(1150, 'm', precision=3)) 73 | self.assertEqual('-1.15 km', humanize.DecimalPrefix(-1150, 'm', 74 | precision=3)) 75 | self.assertEqual('1 k', humanize.DecimalPrefix(1000, '')) 76 | self.assertEqual('-10 G', humanize.DecimalPrefix(-10e9, '')) 77 | self.assertEqual('12', humanize.DecimalPrefix(12, '')) 78 | self.assertEqual('-115', humanize.DecimalPrefix(-115, '')) 79 | self.assertEqual('0', humanize.DecimalPrefix(0, '')) 80 | 81 | self.assertEqual('1.1 s', humanize.DecimalPrefix(1.12, 's', precision=2)) 82 | self.assertEqual('-1.1 s', humanize.DecimalPrefix(-1.12, 's', precision=2)) 83 | self.assertEqual('nan bps', humanize.DecimalPrefix(float('nan'), 'bps')) 84 | self.assertEqual('nan', humanize.DecimalPrefix(float('nan'), '')) 85 | self.assertEqual('inf bps', humanize.DecimalPrefix(float('inf'), 'bps')) 86 | self.assertEqual('-inf bps', humanize.DecimalPrefix(float('-inf'), 'bps')) 87 | self.assertEqual('-inf', humanize.DecimalPrefix(float('-inf'), '')) 88 | 89 | self.assertEqual('-4 mm', 90 | humanize.DecimalPrefix(-0.004, 'm', min_scale=None)) 91 | self.assertEqual('0 m', humanize.DecimalPrefix(0, 'm', min_scale=None)) 92 | self.assertEqual( 93 | u'1 µs', 94 | humanize.DecimalPrefix(0.0000013, 's', min_scale=None)) 95 | self.assertEqual('3 km', humanize.DecimalPrefix(3000, 'm', min_scale=None)) 96 | self.assertEqual( 97 | '5000 TB', 98 | humanize.DecimalPrefix(5e15, 'B', max_scale=4)) 99 | self.assertEqual( 100 | '5 mSWE', 101 | humanize.DecimalPrefix(0.005, 'SWE', min_scale=None)) 102 | self.assertEqual( 103 | '0.0005 ms', 104 | humanize.DecimalPrefix(5e-7, 's', min_scale=-1, precision=2)) 105 | 106 | def testBinaryPrefix(self): 107 | self.assertEqual('0 B', humanize.BinaryPrefix(0, 'B')) 108 | self.assertEqual('1000 B', humanize.BinaryPrefix(1000, 'B')) 109 | self.assertEqual('1 KiB', humanize.BinaryPrefix(1024, 'B')) 110 | self.assertEqual('64 GiB', humanize.BinaryPrefix(2**36, 'B')) 111 | self.assertEqual('65536 Yibit', humanize.BinaryPrefix(2**96, 'bit')) 112 | self.assertEqual('1.25 KiB', humanize.BinaryPrefix(1280, 'B', precision=3)) 113 | self.assertEqual('1.2 KiB', humanize.BinaryPrefix(1280, 'B', precision=2)) 114 | self.assertEqual('1.2 Ki', humanize.BinaryPrefix(1280, '', precision=2)) 115 | self.assertEqual('12', humanize.BinaryPrefix(12, '', precision=2)) 116 | 117 | # Test both int and long versions of the same quantity to make sure they are 118 | # printed in the same way. 119 | self.assertEqual('10.0 QPS', humanize.BinaryPrefix(10, 'QPS', precision=3)) 120 | self.assertEqual('10.0 QPS', humanize.BinaryPrefix(10L, 'QPS', precision=3)) 121 | 122 | def testDecimalScale(self): 123 | self.assertIsInstance(humanize.DecimalScale(0, '')[0], float) 124 | self.assertIsInstance(humanize.DecimalScale(1, '')[0], float) 125 | self.assertEqual((12.1, 'km'), humanize.DecimalScale(12100, 'm')) 126 | self.assertEqual((12.1, 'k'), humanize.DecimalScale(12100, '')) 127 | self.assertEqual((0, ''), humanize.DecimalScale(0, '')) 128 | self.assertEqual( 129 | (12.1, 'km'), 130 | humanize.DecimalScale(12100, 'm', min_scale=0, max_scale=None)) 131 | self.assertEqual( 132 | (12100, 'm'), 133 | humanize.DecimalScale(12100, 'm', min_scale=0, max_scale=0)) 134 | self.assertEqual((1.15, 'Mm'), humanize.DecimalScale(1150000, 'm')) 135 | self.assertEqual((1, 'm'), 136 | humanize.DecimalScale(1, 'm', min_scale=None)) 137 | self.assertEqual((450, 'mSWE'), 138 | humanize.DecimalScale(0.45, 'SWE', min_scale=None)) 139 | self.assertEqual( 140 | (250, u'µm'), 141 | humanize.DecimalScale(1.0 / (4 * 1000), 'm', min_scale=None)) 142 | self.assertEqual( 143 | (0.250, 'km'), 144 | humanize.DecimalScale(250, 'm', min_scale=1)) 145 | self.assertEqual( 146 | (12000, 'mm'), 147 | humanize.DecimalScale(12, 'm', min_scale=None, max_scale=-1)) 148 | 149 | def testBinaryScale(self): 150 | self.assertIsInstance(humanize.BinaryScale(0, '')[0], float) 151 | self.assertIsInstance(humanize.BinaryScale(1, '')[0], float) 152 | value, unit = humanize.BinaryScale(200000000000, 'B') 153 | self.assertAlmostEqual(value, 186.26, 2) 154 | self.assertEqual(unit, 'GiB') 155 | 156 | value, unit = humanize.BinaryScale(3000000000000, 'B') 157 | self.assertAlmostEqual(value, 2.728, 3) 158 | self.assertEqual(unit, 'TiB') 159 | 160 | def testPrettyFraction(self): 161 | # No rounded integer part 162 | self.assertEqual(u'½', humanize.PrettyFraction(0.5)) 163 | # Roundeded integer + fraction 164 | self.assertEqual(u'6⅔', humanize.PrettyFraction(20.0 / 3.0)) 165 | # Rounded integer, no fraction 166 | self.assertEqual(u'2', humanize.PrettyFraction(2.00001)) 167 | # No rounded integer, no fraction 168 | self.assertEqual(u'0', humanize.PrettyFraction(0.001)) 169 | # Round up 170 | self.assertEqual(u'1', humanize.PrettyFraction(0.99)) 171 | # No round up, edge case 172 | self.assertEqual(u'⅞', humanize.PrettyFraction(0.9)) 173 | # Negative fraction 174 | self.assertEqual(u'-⅕', humanize.PrettyFraction(-0.2)) 175 | # Negative close to zero (should not be -0) 176 | self.assertEqual(u'0', humanize.PrettyFraction(-0.001)) 177 | # Smallest fraction that should round down. 178 | self.assertEqual(u'0', humanize.PrettyFraction(1.0 / 16.0)) 179 | # Largest fraction should round up. 180 | self.assertEqual(u'1', humanize.PrettyFraction(15.0 / 16.0)) 181 | # Integer zero. 182 | self.assertEqual(u'0', humanize.PrettyFraction(0)) 183 | # Check that division yields fraction 184 | self.assertEqual(u'⅘', humanize.PrettyFraction(4.0 / 5.0)) 185 | # Custom spacer. 186 | self.assertEqual(u'2 ½', humanize.PrettyFraction(2.5, spacer=' ')) 187 | 188 | def testDuration(self): 189 | self.assertEqual('2h', humanize.Duration(7200)) 190 | self.assertEqual('5d 13h 47m 12s', humanize.Duration(481632)) 191 | self.assertEqual('0s', humanize.Duration(0)) 192 | self.assertEqual('59s', humanize.Duration(59)) 193 | self.assertEqual('1m', humanize.Duration(60)) 194 | self.assertEqual('1m 1s', humanize.Duration(61)) 195 | self.assertEqual('1h 1s', humanize.Duration(3601)) 196 | self.assertEqual('2h-2s', humanize.Duration(7202, separator='-')) 197 | 198 | def testLargeDuration(self): 199 | # The maximum seconds and days that can be stored in a datetime.timedelta 200 | # object, as seconds. max_days is equal to MAX_DELTA_DAYS in Python's 201 | # Modules/datetimemodule.c, converted to seconds. 202 | max_seconds = 3600 * 24 - 1 203 | max_days = 999999999 * 24 * 60 * 60 204 | 205 | self.assertEqual('999999999d', humanize.Duration(max_days)) 206 | self.assertEqual('999999999d 23h 59m 59s', 207 | humanize.Duration(max_days + max_seconds)) 208 | self.assertEqual('>=999999999d 23h 59m 60s', 209 | humanize.Duration(max_days + max_seconds + 1)) 210 | 211 | def testTimeDelta(self): 212 | self.assertEqual('0s', humanize.TimeDelta(datetime.timedelta())) 213 | self.assertEqual('2h', humanize.TimeDelta(datetime.timedelta(hours=2))) 214 | self.assertEqual('1m', humanize.TimeDelta(datetime.timedelta(minutes=1))) 215 | self.assertEqual('5d', humanize.TimeDelta(datetime.timedelta(days=5))) 216 | self.assertEqual('1.25s', humanize.TimeDelta( 217 | datetime.timedelta(seconds=1, microseconds=250000))) 218 | self.assertEqual('1.5s', 219 | humanize.TimeDelta(datetime.timedelta(seconds=1.5))) 220 | self.assertEqual('4d 10h 5m 12.25s', humanize.TimeDelta( 221 | datetime.timedelta(days=4, hours=10, minutes=5, seconds=12, 222 | microseconds=250000))) 223 | 224 | def testUnixTimestamp(self): 225 | self.assertEqual('2013-11-17 11:08:27.723524 PST', 226 | humanize.UnixTimestamp(1384715307.723524, 227 | datelib.US_PACIFIC)) 228 | self.assertEqual('2013-11-17 19:08:27.723524 UTC', 229 | humanize.UnixTimestamp(1384715307.723524, 230 | datelib.UTC)) 231 | 232 | # DST part of the timezone should not depend on the current local time, 233 | # so this should be in PDT (and different from the PST in the first test). 234 | self.assertEqual('2013-05-17 15:47:21.723524 PDT', 235 | humanize.UnixTimestamp(1368830841.723524, 236 | datelib.US_PACIFIC)) 237 | 238 | self.assertEqual('1970-01-01 00:00:00.000000 UTC', 239 | humanize.UnixTimestamp(0, datelib.UTC)) 240 | 241 | def testAddOrdinalSuffix(self): 242 | self.assertEqual('0th', humanize.AddOrdinalSuffix(0)) 243 | self.assertEqual('1st', humanize.AddOrdinalSuffix(1)) 244 | self.assertEqual('2nd', humanize.AddOrdinalSuffix(2)) 245 | self.assertEqual('3rd', humanize.AddOrdinalSuffix(3)) 246 | self.assertEqual('4th', humanize.AddOrdinalSuffix(4)) 247 | self.assertEqual('5th', humanize.AddOrdinalSuffix(5)) 248 | self.assertEqual('10th', humanize.AddOrdinalSuffix(10)) 249 | self.assertEqual('11th', humanize.AddOrdinalSuffix(11)) 250 | self.assertEqual('12th', humanize.AddOrdinalSuffix(12)) 251 | self.assertEqual('13th', humanize.AddOrdinalSuffix(13)) 252 | self.assertEqual('14th', humanize.AddOrdinalSuffix(14)) 253 | self.assertEqual('20th', humanize.AddOrdinalSuffix(20)) 254 | self.assertEqual('21st', humanize.AddOrdinalSuffix(21)) 255 | self.assertEqual('22nd', humanize.AddOrdinalSuffix(22)) 256 | self.assertEqual('23rd', humanize.AddOrdinalSuffix(23)) 257 | self.assertEqual('24th', humanize.AddOrdinalSuffix(24)) 258 | self.assertEqual('63rd', humanize.AddOrdinalSuffix(63)) 259 | self.assertEqual('100000th', humanize.AddOrdinalSuffix(100000)) 260 | self.assertEqual('100001st', humanize.AddOrdinalSuffix(100001)) 261 | self.assertEqual('100011th', humanize.AddOrdinalSuffix(100011)) 262 | self.assertRaises(ValueError, humanize.AddOrdinalSuffix, -1) 263 | self.assertRaises(ValueError, humanize.AddOrdinalSuffix, 0.5) 264 | self.assertRaises(ValueError, humanize.AddOrdinalSuffix, 123.001) 265 | 266 | 267 | class NaturalSortKeyChunkingTest(basetest.TestCase): 268 | 269 | def testChunkifySingleChars(self): 270 | self.assertListEqual( 271 | humanize.NaturalSortKey('a1b2c3'), 272 | ['a', 1, 'b', 2, 'c', 3]) 273 | 274 | def testChunkifyMultiChars(self): 275 | self.assertListEqual( 276 | humanize.NaturalSortKey('aa11bb22cc33'), 277 | ['aa', 11, 'bb', 22, 'cc', 33]) 278 | 279 | def testChunkifyComplex(self): 280 | self.assertListEqual( 281 | humanize.NaturalSortKey('one 11 -- two 44'), 282 | ['one ', 11, ' -- two ', 44]) 283 | 284 | 285 | class NaturalSortKeysortTest(basetest.TestCase): 286 | 287 | def testNaturalSortKeySimpleWords(self): 288 | self.test = ['pair', 'banana', 'apple'] 289 | self.good = ['apple', 'banana', 'pair'] 290 | self.test.sort(key=humanize.NaturalSortKey) 291 | self.assertListEqual(self.test, self.good) 292 | 293 | def testNaturalSortKeySimpleNums(self): 294 | self.test = ['3333', '2222', '9999', '0000'] 295 | self.good = ['0000', '2222', '3333', '9999'] 296 | self.test.sort(key=humanize.NaturalSortKey) 297 | self.assertListEqual(self.test, self.good) 298 | 299 | def testNaturalSortKeySimpleDigits(self): 300 | self.test = ['8', '3', '2'] 301 | self.good = ['2', '3', '8'] 302 | self.test.sort(key=humanize.NaturalSortKey) 303 | self.assertListEqual(self.test, self.good) 304 | 305 | def testVersionStrings(self): 306 | self.test = ['1.2', '0.9', '1.1a2', '1.1a', '1', '1.2.1', '0.9.1'] 307 | self.good = ['0.9', '0.9.1', '1', '1.1a', '1.1a2', '1.2', '1.2.1'] 308 | self.test.sort(key=humanize.NaturalSortKey) 309 | self.assertListEqual(self.test, self.good) 310 | 311 | def testNaturalSortKeySimpleNumLong(self): 312 | self.test = ['11', '9', '1', '200', '19', '20', '900'] 313 | self.good = ['1', '9', '11', '19', '20', '200', '900'] 314 | self.test.sort(key=humanize.NaturalSortKey) 315 | self.assertListEqual(self.test, self.good) 316 | 317 | def testNaturalSortKeyAlNum(self): 318 | self.test = ['x10', 'x9', 'x1', 'x11'] 319 | self.good = ['x1', 'x9', 'x10', 'x11'] 320 | self.test.sort(key=humanize.NaturalSortKey) 321 | self.assertListEqual(self.test, self.good) 322 | 323 | def testNaturalSortKeyNumAlNum(self): 324 | self.test = ['4x10', '4x9', '4x11', '5yy4', '3x1', '2x11'] 325 | self.good = ['2x11', '3x1', '4x9', '4x10', '4x11', '5yy4'] 326 | self.test.sort(key=humanize.NaturalSortKey) 327 | self.assertListEqual(self.test, self.good) 328 | 329 | def testNaturalSortKeyAlNumAl(self): 330 | self.test = ['a9c', 'a4b', 'a10c', 'a1c', 'c10c', 'c10a', 'c9a'] 331 | self.good = ['a1c', 'a4b', 'a9c', 'a10c', 'c9a', 'c10a', 'c10c'] 332 | self.test.sort(key=humanize.NaturalSortKey) 333 | self.assertListEqual(self.test, self.good) 334 | 335 | 336 | class NaturalSortKeyBigTest(basetest.TestCase): 337 | 338 | def testBig(self): 339 | self.test = [ 340 | '1000X Radonius Maximus', '10X Radonius', '200X Radonius', 341 | '20X Radonius', '20X Radonius Prime', '30X Radonius', 342 | '40X Radonius', 'Allegia 50 Clasteron', 'Allegia 500 Clasteron', 343 | 'Allegia 51 Clasteron', 'Allegia 51B Clasteron', 344 | 'Allegia 52 Clasteron', 'Allegia 60 Clasteron', 'Alpha 100', 345 | 'Alpha 2', 'Alpha 200', 'Alpha 2A', 'Alpha 2A-8000', 'Alpha 2A-900', 346 | 'Callisto Morphamax', 'Callisto Morphamax 500', 347 | 'Callisto Morphamax 5000', 'Callisto Morphamax 600', 348 | 'Callisto Morphamax 700', 'Callisto Morphamax 7000', 349 | 'Callisto Morphamax 7000 SE', 'Callisto Morphamax 7000 SE2', 350 | 'QRS-60 Intrinsia Machine', 'QRS-60F Intrinsia Machine', 351 | 'QRS-62 Intrinsia Machine', 'QRS-62F Intrinsia Machine', 352 | 'Xiph Xlater 10000', 'Xiph Xlater 2000', 'Xiph Xlater 300', 353 | 'Xiph Xlater 40', 'Xiph Xlater 5', 'Xiph Xlater 50', 354 | 'Xiph Xlater 500', 'Xiph Xlater 5000', 'Xiph Xlater 58'] 355 | self.good = [ 356 | '10X Radonius', 357 | '20X Radonius', 358 | '20X Radonius Prime', 359 | '30X Radonius', 360 | '40X Radonius', 361 | '200X Radonius', 362 | '1000X Radonius Maximus', 363 | 'Allegia 50 Clasteron', 364 | 'Allegia 51 Clasteron', 365 | 'Allegia 51B Clasteron', 366 | 'Allegia 52 Clasteron', 367 | 'Allegia 60 Clasteron', 368 | 'Allegia 500 Clasteron', 369 | 'Alpha 2', 370 | 'Alpha 2A', 371 | 'Alpha 2A-900', 372 | 'Alpha 2A-8000', 373 | 'Alpha 100', 374 | 'Alpha 200', 375 | 'Callisto Morphamax', 376 | 'Callisto Morphamax 500', 377 | 'Callisto Morphamax 600', 378 | 'Callisto Morphamax 700', 379 | 'Callisto Morphamax 5000', 380 | 'Callisto Morphamax 7000', 381 | 'Callisto Morphamax 7000 SE', 382 | 'Callisto Morphamax 7000 SE2', 383 | 'QRS-60 Intrinsia Machine', 384 | 'QRS-60F Intrinsia Machine', 385 | 'QRS-62 Intrinsia Machine', 386 | 'QRS-62F Intrinsia Machine', 387 | 'Xiph Xlater 5', 388 | 'Xiph Xlater 40', 389 | 'Xiph Xlater 50', 390 | 'Xiph Xlater 58', 391 | 'Xiph Xlater 300', 392 | 'Xiph Xlater 500', 393 | 'Xiph Xlater 2000', 394 | 'Xiph Xlater 5000', 395 | 'Xiph Xlater 10000', 396 | ] 397 | self.test.sort(key=humanize.NaturalSortKey) 398 | self.assertListEqual(self.test, self.good) 399 | 400 | 401 | if __name__ == '__main__': 402 | basetest.main() 403 | -------------------------------------------------------------------------------- /tests/resources_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2010 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Tests for the resources module.""" 17 | 18 | __author__ = 'dborowitz@google.com (Dave Borowitz)' 19 | 20 | from google.apputils import basetest 21 | 22 | from google.apputils import file_util 23 | from google.apputils import resources 24 | 25 | PREFIX = __name__ + ':data/' 26 | 27 | 28 | class ResourcesTest(basetest.TestCase): 29 | 30 | def _CheckTestData(self, func): 31 | self.assertEqual('test file a contents\n', func(PREFIX + 'a')) 32 | self.assertEqual('test file b contents\n', func(PREFIX + 'b')) 33 | 34 | def testGetResource(self): 35 | self._CheckTestData(resources.GetResource) 36 | 37 | def testGetResourceAsFile(self): 38 | self._CheckTestData(lambda n: resources.GetResourceAsFile(n).read()) 39 | 40 | def testGetResourceFilename(self): 41 | self._CheckTestData( 42 | lambda n: file_util.Read(resources.GetResourceFilename(n))) 43 | 44 | 45 | if __name__ == '__main__': 46 | basetest.main() 47 | -------------------------------------------------------------------------------- /tests/sh_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2010 Google Inc. All Rights Reserved. 3 | 4 | """Tests for google.apputils. 5 | 6 | In addition to the test modules under this package, we have a special TestCase 7 | that runs the tests that are shell scripts. 8 | """ 9 | 10 | # TODO(dborowitz): It may be useful to generalize this and provide it to users 11 | # who want to run their own sh_tests. 12 | 13 | import os 14 | import subprocess 15 | import sys 16 | 17 | from google.apputils import basetest 18 | import gflags 19 | 20 | FLAGS = gflags.FLAGS 21 | 22 | 23 | class ShellScriptTests(basetest.TestCase): 24 | """TestCase that runs the various *test.sh scripts.""" 25 | 26 | def RunTestScript(self, script_name): 27 | tests_path = os.path.dirname(__file__) 28 | sh_test_path = os.path.realpath(os.path.join(tests_path, script_name)) 29 | path_with_python = ':'.join(( 30 | os.path.dirname(sys.executable), os.environ.get('PATH'))) 31 | 32 | env = { 33 | 'PATH': path_with_python, 34 | # Setuptools puts dependency eggs in our path, so propagate that. 35 | 'PYTHONPATH': os.pathsep.join(sys.path), 36 | 'TEST_TMPDIR': FLAGS.test_tmpdir, 37 | } 38 | p = subprocess.Popen(sh_test_path, cwd=tests_path, env=env) 39 | self.assertEqual(0, p.wait()) 40 | 41 | def testBaseTest(self): 42 | self.RunTestScript('basetest_sh_test.sh') 43 | 44 | def testApp(self): 45 | self.RunTestScript('app_unittest.sh') 46 | 47 | def testAppCommands(self): 48 | self.RunTestScript('appcommands_unittest.sh') 49 | 50 | 51 | if __name__ == '__main__': 52 | basetest.main() 53 | -------------------------------------------------------------------------------- /tests/shellutil_unittest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This code must be source compatible with Python 2.4 through 3.3. 3 | # 4 | # Copyright 2003 Google Inc. All Rights Reserved. 5 | 6 | """Unittest for shellutil module.""" 7 | 8 | 9 | 10 | import os 11 | # Use unittest instead of basetest to avoid bootstrap issues / circular deps. 12 | import unittest 13 | 14 | from google.apputils import shellutil 15 | 16 | # Running windows? 17 | win32 = (os.name == 'nt') 18 | 19 | 20 | class ShellUtilUnitTest(unittest.TestCase): 21 | def testShellEscapeList(self): 22 | # TODO(user): Actually run some shell commands and test the 23 | # shell escaping works properly. 24 | # Empty list 25 | words = [] 26 | self.assertEqual(shellutil.ShellEscapeList(words), '') 27 | 28 | # Empty string 29 | words = [''] 30 | self.assertEqual(shellutil.ShellEscapeList(words), "''") 31 | 32 | # Single word 33 | words = ['foo'] 34 | self.assertEqual(shellutil.ShellEscapeList(words), "'foo'") 35 | 36 | # Single word with single quote 37 | words = ["foo'bar"] 38 | expected = """ 'foo'"'"'bar' """.strip() 39 | self.assertEqual(shellutil.ShellEscapeList(words), expected) 40 | # .. double quote 41 | words = ['foo"bar'] 42 | expected = """ 'foo"bar' """.strip() 43 | self.assertEqual(shellutil.ShellEscapeList(words), expected) 44 | 45 | # Multiple words 46 | words = ['foo', 'bar'] 47 | self.assertEqual(shellutil.ShellEscapeList(words), "'foo' 'bar'") 48 | 49 | # Words with spaces 50 | words = ['foo', 'bar', "foo'' ''bar"] 51 | expected = """ 'foo' 'bar' 'foo'"'"''"'"' '"'"''"'"'bar' """.strip() 52 | self.assertEqual(shellutil.ShellEscapeList(words), expected) 53 | 54 | # Now I'm just being mean 55 | words = ['foo', 'bar', """ ""'"'" """.strip()] 56 | expected = """ 'foo' 'bar' '""'"'"'"'"'"'"' """.strip() 57 | self.assertEqual(shellutil.ShellEscapeList(words), expected) 58 | 59 | def testShellifyStatus(self): 60 | if not win32: 61 | self.assertEqual(shellutil.ShellifyStatus(0), 0) 62 | self.assertEqual(shellutil.ShellifyStatus(1), 129) 63 | self.assertEqual(shellutil.ShellifyStatus(1 * 256), 1) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /tests/stopwatch_unittest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2006 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Tests for the stopwatch module.""" 17 | 18 | __author__ = 'dbentley@google.com (Dan Bentley)' 19 | 20 | from google.apputils import basetest 21 | 22 | import gflags as flags 23 | from google.apputils import stopwatch 24 | 25 | FLAGS = flags.FLAGS 26 | 27 | 28 | class StubTime(object): 29 | """Simple stub replacement for the time module. 30 | 31 | Only useful for relative calculations, since it always starts at 0. 32 | """ 33 | # These method names match the standard library time module. 34 | 35 | def __init__(self): 36 | self._counter = 0 37 | 38 | def time(self): 39 | """Get the time for this time object. 40 | 41 | A call is always guaranteed to be greater than the previous one. 42 | 43 | Returns: 44 | A monotonically increasing time. 45 | """ 46 | self._counter += 0.0001 47 | return self._counter 48 | 49 | def sleep(self, time): 50 | """Simulate sleeping for the specified number of seconds.""" 51 | self._counter += time 52 | 53 | 54 | class StopwatchUnitTest(basetest.TestCase): 55 | """Stopwatch tests. 56 | 57 | These tests are tricky because timing is difficult. 58 | 59 | Therefore, we test the structure of the results but not the results 60 | themselves for fear it would lead to intermittent but persistent 61 | failures. 62 | """ 63 | 64 | def setUp(self): 65 | self.time = StubTime() 66 | stopwatch.time = self.time 67 | 68 | def testResults(self): 69 | sw = stopwatch.StopWatch() 70 | sw.start() 71 | sw.stop() 72 | 73 | results = sw.results() 74 | 75 | self.assertListEqual([r[0] for r in results], ['total']) 76 | 77 | results = sw.results(verbose=1) 78 | self.assertListEqual([r[0] for r in results], ['overhead', 'total']) 79 | 80 | # test tally part of results. 81 | sw.start('ron') 82 | sw.stop('ron') 83 | sw.start('ron') 84 | sw.stop('ron') 85 | results = sw.results() 86 | results = sw.results(verbose=1) 87 | for r in results: 88 | if r[0] == 'ron': 89 | assert r[2] == 2 90 | 91 | def testSeveralTimes(self): 92 | sw = stopwatch.StopWatch() 93 | sw.start() 94 | 95 | sw.start('a') 96 | sw.start('b') 97 | self.time.sleep(1) 98 | sw.stop('b') 99 | sw.stop('a') 100 | 101 | sw.stop() 102 | 103 | results = sw.results(verbose=1) 104 | self.assertListEqual([r[0] for r in results], 105 | ['a', 'b', 'overhead', 'total']) 106 | 107 | # Make sure overhead is positive 108 | self.assertEqual(results[2][1] > 0, 1) 109 | 110 | def testNoStopOthers(self): 111 | sw = stopwatch.StopWatch() 112 | sw.start() 113 | 114 | sw.start('a') 115 | sw.start('b', stop_others=0) 116 | self.time.sleep(1) 117 | sw.stop('b') 118 | sw.stop('a') 119 | 120 | sw.stop() 121 | 122 | #overhead should be negative, because we ran two timers simultaneously 123 | #It is possible that this could fail in outlandish circumstances. 124 | #If this is a problem in practice, increase the value of the call to 125 | #time.sleep until it passes consistently. 126 | #Or, consider finding a platform where the two calls sw.start() and 127 | #sw.start('a') happen within 1 second. 128 | results = sw.results(verbose=1) 129 | self.assertEqual(results[2][1] < 0, 1) 130 | 131 | def testStopNonExistentTimer(self): 132 | sw = stopwatch.StopWatch() 133 | self.assertRaises(RuntimeError, sw.stop) 134 | self.assertRaises(RuntimeError, sw.stop, 'foo') 135 | 136 | def testResultsDoesntCrashWhenUnstarted(self): 137 | sw = stopwatch.StopWatch() 138 | sw.results() 139 | 140 | def testResultsDoesntCrashWhenUnstopped(self): 141 | sw = stopwatch.StopWatch() 142 | sw.start() 143 | sw.results() 144 | 145 | def testTimerValue(self): 146 | sw = stopwatch.StopWatch() 147 | self.assertAlmostEqual(0, sw.timervalue('a'), 2) 148 | sw.start('a') 149 | self.assertAlmostEqual(0, sw.timervalue('a'), 2) 150 | self.time.sleep(1) 151 | self.assertAlmostEqual(1, sw.timervalue('a'), 2) 152 | sw.stop('a') 153 | self.assertAlmostEqual(1, sw.timervalue('a'), 2) 154 | sw.start('a') 155 | self.time.sleep(1) 156 | self.assertAlmostEqual(2, sw.timervalue('a'), 2) 157 | sw.stop('a') 158 | self.assertAlmostEqual(2, sw.timervalue('a'), 2) 159 | 160 | def testResultsDoesntReset(self): 161 | sw = stopwatch.StopWatch() 162 | sw.start() 163 | self.time.sleep(1) 164 | sw.start('a') 165 | self.time.sleep(1) 166 | sw.stop('a') 167 | sw.stop() 168 | res1 = sw.results(verbose=True) 169 | res2 = sw.results(verbose=True) 170 | self.assertListEqual(res1, res2) 171 | 172 | 173 | if __name__ == '__main__': 174 | basetest.main() 175 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27 3 | 4 | [testenv] 5 | deps = ez-setup 6 | commands = python setup.py google_test 7 | --------------------------------------------------------------------------------