OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # |
| 3 # Copyright 2010 Google Inc. |
| 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 """Common utility library.""" |
| 18 from __future__ import with_statement |
| 19 |
| 20 import datetime |
| 21 import functools |
| 22 import inspect |
| 23 import os |
| 24 import re |
| 25 import sys |
| 26 |
| 27 import six |
| 28 |
| 29 __all__ = [ |
| 30 'Error', |
| 31 'decode_datetime', |
| 32 'get_package_for_module', |
| 33 'positional', |
| 34 'TimeZoneOffset', |
| 35 'total_seconds', |
| 36 ] |
| 37 |
| 38 |
| 39 class Error(Exception): |
| 40 """Base class for protorpc exceptions.""" |
| 41 |
| 42 |
| 43 _TIME_ZONE_RE_STRING = r""" |
| 44 # Examples: |
| 45 # +01:00 |
| 46 # -05:30 |
| 47 # Z12:00 |
| 48 ((?P<z>Z) | (?P<sign>[-+]) |
| 49 (?P<hours>\d\d) : |
| 50 (?P<minutes>\d\d))$ |
| 51 """ |
| 52 _TIME_ZONE_RE = re.compile(_TIME_ZONE_RE_STRING, re.IGNORECASE | re.VERBOSE) |
| 53 |
| 54 |
| 55 def positional(max_positional_args): |
| 56 """A decorator to declare that only the first N arguments may be positional. |
| 57 |
| 58 This decorator makes it easy to support Python 3 style keyword-only |
| 59 parameters. For example, in Python 3 it is possible to write: |
| 60 |
| 61 def fn(pos1, *, kwonly1=None, kwonly1=None): |
| 62 ... |
| 63 |
| 64 All named parameters after * must be a keyword: |
| 65 |
| 66 fn(10, 'kw1', 'kw2') # Raises exception. |
| 67 fn(10, kwonly1='kw1') # Ok. |
| 68 |
| 69 Example: |
| 70 To define a function like above, do: |
| 71 |
| 72 @positional(1) |
| 73 def fn(pos1, kwonly1=None, kwonly2=None): |
| 74 ... |
| 75 |
| 76 If no default value is provided to a keyword argument, it |
| 77 becomes a required keyword argument: |
| 78 |
| 79 @positional(0) |
| 80 def fn(required_kw): |
| 81 ... |
| 82 |
| 83 This must be called with the keyword parameter: |
| 84 |
| 85 fn() # Raises exception. |
| 86 fn(10) # Raises exception. |
| 87 fn(required_kw=10) # Ok. |
| 88 |
| 89 When defining instance or class methods always remember to account for |
| 90 'self' and 'cls': |
| 91 |
| 92 class MyClass(object): |
| 93 |
| 94 @positional(2) |
| 95 def my_method(self, pos1, kwonly1=None): |
| 96 ... |
| 97 |
| 98 @classmethod |
| 99 @positional(2) |
| 100 def my_method(cls, pos1, kwonly1=None): |
| 101 ... |
| 102 |
| 103 One can omit the argument to 'positional' altogether, and then no |
| 104 arguments with default values may be passed positionally. This |
| 105 would be equivalent to placing a '*' before the first argument |
| 106 with a default value in Python 3. If there are no arguments with |
| 107 default values, and no argument is given to 'positional', an error |
| 108 is raised. |
| 109 |
| 110 @positional |
| 111 def fn(arg1, arg2, required_kw1=None, required_kw2=0): |
| 112 ... |
| 113 |
| 114 fn(1, 3, 5) # Raises exception. |
| 115 fn(1, 3) # Ok. |
| 116 fn(1, 3, required_kw1=5) # Ok. |
| 117 |
| 118 Args: |
| 119 max_positional_arguments: Maximum number of positional arguments. All |
| 120 parameters after the this index must be keyword only. |
| 121 |
| 122 Returns: |
| 123 A decorator that prevents using arguments after max_positional_args from |
| 124 being used as positional parameters. |
| 125 |
| 126 Raises: |
| 127 TypeError if a keyword-only argument is provided as a positional |
| 128 parameter. |
| 129 ValueError if no maximum number of arguments is provided and the function |
| 130 has no arguments with default values. |
| 131 """ |
| 132 def positional_decorator(wrapped): |
| 133 @functools.wraps(wrapped) |
| 134 def positional_wrapper(*args, **kwargs): |
| 135 if len(args) > max_positional_args: |
| 136 plural_s = '' |
| 137 if max_positional_args != 1: |
| 138 plural_s = 's' |
| 139 raise TypeError('%s() takes at most %d positional argument%s ' |
| 140 '(%d given)' % (wrapped.__name__, |
| 141 max_positional_args, |
| 142 plural_s, len(args))) |
| 143 return wrapped(*args, **kwargs) |
| 144 return positional_wrapper |
| 145 |
| 146 if isinstance(max_positional_args, six.integer_types): |
| 147 return positional_decorator |
| 148 else: |
| 149 args, _, _, defaults = inspect.getargspec(max_positional_args) |
| 150 if defaults is None: |
| 151 raise ValueError( |
| 152 'Functions with no keyword arguments must specify ' |
| 153 'max_positional_args') |
| 154 return positional(len(args) - len(defaults))(max_positional_args) |
| 155 |
| 156 |
| 157 @positional(1) |
| 158 def get_package_for_module(module): |
| 159 """Get package name for a module. |
| 160 |
| 161 Helper calculates the package name of a module. |
| 162 |
| 163 Args: |
| 164 module: Module to get name for. If module is a string, try to find |
| 165 module in sys.modules. |
| 166 |
| 167 Returns: |
| 168 If module contains 'package' attribute, uses that as package name. |
| 169 Else, if module is not the '__main__' module, the module __name__. |
| 170 Else, the base name of the module file name. Else None. |
| 171 """ |
| 172 if isinstance(module, six.string_types): |
| 173 try: |
| 174 module = sys.modules[module] |
| 175 except KeyError: |
| 176 return None |
| 177 |
| 178 try: |
| 179 return six.text_type(module.package) |
| 180 except AttributeError: |
| 181 if module.__name__ == '__main__': |
| 182 try: |
| 183 file_name = module.__file__ |
| 184 except AttributeError: |
| 185 pass |
| 186 else: |
| 187 base_name = os.path.basename(file_name) |
| 188 split_name = os.path.splitext(base_name) |
| 189 if len(split_name) == 1: |
| 190 return six.text_type(base_name) |
| 191 else: |
| 192 return u'.'.join(split_name[:-1]) |
| 193 |
| 194 return six.text_type(module.__name__) |
| 195 |
| 196 |
| 197 def total_seconds(offset): |
| 198 """Backport of offset.total_seconds() from python 2.7+.""" |
| 199 seconds = offset.days * 24 * 60 * 60 + offset.seconds |
| 200 microseconds = seconds * 10**6 + offset.microseconds |
| 201 return microseconds / (10**6 * 1.0) |
| 202 |
| 203 |
| 204 class TimeZoneOffset(datetime.tzinfo): |
| 205 """Time zone information as encoded/decoded for DateTimeFields.""" |
| 206 |
| 207 def __init__(self, offset): |
| 208 """Initialize a time zone offset. |
| 209 |
| 210 Args: |
| 211 offset: Integer or timedelta time zone offset, in minutes from UTC. |
| 212 This can be negative. |
| 213 """ |
| 214 super(TimeZoneOffset, self).__init__() |
| 215 if isinstance(offset, datetime.timedelta): |
| 216 offset = total_seconds(offset) / 60 |
| 217 self.__offset = offset |
| 218 |
| 219 def utcoffset(self, _): |
| 220 """Get the a timedelta with the time zone's offset from UTC. |
| 221 |
| 222 Returns: |
| 223 The time zone offset from UTC, as a timedelta. |
| 224 """ |
| 225 return datetime.timedelta(minutes=self.__offset) |
| 226 |
| 227 def dst(self, _): |
| 228 """Get the daylight savings time offset. |
| 229 |
| 230 The formats that ProtoRPC uses to encode/decode time zone |
| 231 information don't contain any information about daylight |
| 232 savings time. So this always returns a timedelta of 0. |
| 233 |
| 234 Returns: |
| 235 A timedelta of 0. |
| 236 |
| 237 """ |
| 238 return datetime.timedelta(0) |
| 239 |
| 240 |
| 241 def decode_datetime(encoded_datetime): |
| 242 """Decode a DateTimeField parameter from a string to a python datetime. |
| 243 |
| 244 Args: |
| 245 encoded_datetime: A string in RFC 3339 format. |
| 246 |
| 247 Returns: |
| 248 A datetime object with the date and time specified in encoded_datetime. |
| 249 |
| 250 Raises: |
| 251 ValueError: If the string is not in a recognized format. |
| 252 """ |
| 253 # Check if the string includes a time zone offset. Break out the |
| 254 # part that doesn't include time zone info. Convert to uppercase |
| 255 # because all our comparisons should be case-insensitive. |
| 256 time_zone_match = _TIME_ZONE_RE.search(encoded_datetime) |
| 257 if time_zone_match: |
| 258 time_string = encoded_datetime[:time_zone_match.start(1)].upper() |
| 259 else: |
| 260 time_string = encoded_datetime.upper() |
| 261 |
| 262 if '.' in time_string: |
| 263 format_string = '%Y-%m-%dT%H:%M:%S.%f' |
| 264 else: |
| 265 format_string = '%Y-%m-%dT%H:%M:%S' |
| 266 |
| 267 decoded_datetime = datetime.datetime.strptime(time_string, format_string) |
| 268 |
| 269 if not time_zone_match: |
| 270 return decoded_datetime |
| 271 |
| 272 # Time zone info was included in the parameter. Add a tzinfo |
| 273 # object to the datetime. Datetimes can't be changed after they're |
| 274 # created, so we'll need to create a new one. |
| 275 if time_zone_match.group('z'): |
| 276 offset_minutes = 0 |
| 277 else: |
| 278 sign = time_zone_match.group('sign') |
| 279 hours, minutes = [int(value) for value in |
| 280 time_zone_match.group('hours', 'minutes')] |
| 281 offset_minutes = hours * 60 + minutes |
| 282 if sign == '-': |
| 283 offset_minutes *= -1 |
| 284 |
| 285 return datetime.datetime(decoded_datetime.year, |
| 286 decoded_datetime.month, |
| 287 decoded_datetime.day, |
| 288 decoded_datetime.hour, |
| 289 decoded_datetime.minute, |
| 290 decoded_datetime.second, |
| 291 decoded_datetime.microsecond, |
| 292 TimeZoneOffset(offset_minutes)) |
OLD | NEW |