├── 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 |
--------------------------------------------------------------------------------