diff --git a/edbob/pyramid/forms/formalchemy/__init__.py b/edbob/pyramid/forms/formalchemy/__init__.py
index 4715a96..77cd0b4 100644
--- a/edbob/pyramid/forms/formalchemy/__init__.py
+++ b/edbob/pyramid/forms/formalchemy/__init__.py
@@ -104,7 +104,7 @@ def required(value, field=None):
raise formalchemy.ValidationError(msg)
-def pretty_datetime(value):
+def pretty_datetime(value, from_='local', to='local'):
"""
Formats a ``datetime.datetime`` instance and returns a "pretty"
human-readable string from it, e.g. "42 minutes ago". ``value`` is
@@ -113,10 +113,10 @@ def pretty_datetime(value):
if not isinstance(value, datetime.datetime):
return str(value) if value else ''
- value = edbob.local_time(value)
- fmt = formalchemy.fields.DateTimeFieldRenderer.format
+ if not value.tzinfo:
+ value = edbob.local_time(value, from_=from_, to=to)
return literal('%s' % (
- value.strftime(fmt),
+ value.strftime('%Y-%m-%d %H:%M:%S %Z%z'),
pretty.date(value)))
diff --git a/edbob/time.py b/edbob/time.py
index 1f0d63a..46bfeca 100644
--- a/edbob/time.py
+++ b/edbob/time.py
@@ -29,91 +29,150 @@
import datetime
import pytz
import logging
+import warnings
+
+import edbob
-__all__ = ['local_time', 'set_timezone', 'utc_time']
+__all__ = ['utc_time', 'local_time']
+
+timezones = {}
log = logging.getLogger(__name__)
-timezone = None
-
def init(config):
"""
- Initializes the time framework. Currently this only sets the local
- timezone according to config.
- """
-
- tz = config.get('edbob.time', 'timezone')
- if tz:
- set_timezone(tz)
- log.info("Timezone set to '%s'" % tz)
- else:
- log.warning("No timezone configured; falling back to US/Central")
- set_timezone('US/Central')
-
-
-def local_time(timestamp=None):
- """
- Tries to return a "localized" version of ``timestamp``, which should be a
- UTC-based ``datetime.datetime`` instance.
-
- If a local timezone has been configured, then
- ``datetime.datetime.utcnow()`` will be called to obtain a value for
- ``timestamp`` if one is not specified. Then ``timestamp`` will be modified
- in such a way that its ``tzinfo`` member contains the local timezone, but
- the effective UTC value for the timestamp remains accurate.
-
- If a local timezone has *not* been configured, then
- ``datetime.datetime.now()`` will be called instead to obtain the value
- should none be specified. ``timestamp`` will be returned unchanged.
- """
-
- if timezone:
- if timestamp is None:
- timestamp = datetime.datetime.utcnow()
- timestamp = pytz.utc.localize(timestamp)
- return timestamp.astimezone(timezone)
-
- if timestamp is None:
- timestamp = datetime.datetime.now()
- return timestamp
-
-
-def set_timezone(tz):
- """
- Sets edbob's notion of the "local" timezone. ``tz`` should be an Olson
- name.
+ Reads configuration to become aware of all timezones which may concern the
+ application.
.. highlight:: ini
- You usually don't need to call this yourself, since it's called by
- :func:`edbob.init()` whenever the config file includes a timezone (but
- only as long as ``edbob.time`` is configured to be initialized)::
-
- [edbob]
- init = edbob.time
+ The bare minimum configuration required is the ``zone.local`` setting::
[edbob.time]
- timezone = US/Central
+ zone.local = US/Pacific
+
+ Multiple timezones may be configured like so::
+
+ [edbob.time]
+ zone.local = America/Los_Angeles
+ zone.head_office = America/New_York
+ zone.that_other_place = Asia/Manila
+ zone.some_app = US/Central
+
+ See `Wikipedia
+ `_ for a
+ (presumably) full list of valid timezone names.
+
+ .. note::
+ A ``zone.utc`` option is automatically created for you; there is no need
+ to define it.
"""
- global timezone
+ for key in config.options('edbob.time'):
+ if key.startswith('zone.'):
+ tz = config.get('edbob.time', key)
+ if tz:
+ key = key[5:]
+ log.info("'%s' timezone set to '%s'" % (key, tz))
+ set_timezone(tz, key)
- if tz is None:
- timezone = None
- else:
- timezone = pytz.timezone(tz)
+ if 'local' not in timezones:
+ tz = config.get('edbob.time', 'timezone')
+ if tz:
+ warnings.warn("Config option 'timezone' in 'edbob.time' section is deprecated. "
+ "Please set 'zone.local' instead.",
+ DeprecationWarning)
+ set_timezone(tz)
+ else:
+ log.warning("'local' timezone not configured; falling back to 'America/Chicago'")
+ set_timezone('America/Chicago')
+
+ set_timezone('UTC', 'utc')
-def utc_time(timestamp=None):
+def get_timezone(key='local'):
"""
- Returns a timestamp whose ``tzinfo`` member is set to the UTC timezone.
-
- If ``timestamp`` is not provided, then ``datetime.datetime.utcnow()`` will
- be called to obtain the value.
+ Returns the timezone referenced by ``key``.
"""
- if timestamp is None:
- timestamp = datetime.datetime.utcnow()
- return pytz.utc.localize(timestamp)
+ if key not in timezones:
+ edbob.config.require('edbob.time', 'zone.%s' % key)
+ return timezones[key]
+
+
+def set_timezone(tz, key='local'):
+ """
+ Stores a timezone in the global dictionary, using ``key``.
+
+ ``tz`` must be a valid "Olson" time zone name, e.g. ``'US/Central'``. See
+ `Wikipedia `_
+ for a (presumably) full list of valid names.
+ """
+
+ timezones[key] = pytz.timezone(tz)
+
+
+def local_time(stamp=None, from_='local', naive=False):
+ """
+ Returns a :class:`datetime.datetime` instance, with its ``tzinfo`` member
+ set to the timezone referenced by the key ``'local'``. If ``naive`` is
+ ``True``, the result is stripped of its ``tzinfo`` member.
+
+ If ``stamp`` is provided, and it is not already "aware," then its value is
+ interpreted as being local to the timezone referenced by ``from_``.
+
+ If ``stamp`` is not provided, the current time is assumed.
+ """
+
+ if not stamp:
+ stamp = utc_time()
+ elif not stamp.tzinfo:
+ stamp = localize(stamp, from_=from_)
+ return localize(stamp, naive=naive)
+
+
+def localize(stamp, from_='local', to='local', naive=False):
+ """
+ Creates a "localized" version of ``stamp`` and returns it.
+
+ ``stamp`` must be a :class:`datetime.datetime` instance. If it is naive,
+ its value is interpreted as being local to the timezone referenced by
+ ``from_``. If it is already aware (i.e. not naive), then ``from_`` is
+ ignored.
+
+ The timezone referenced by ``to`` is used to determine the final, "local"
+ value for the timestamp. If ``naive`` is ``True``, the timestamp is
+ stripped of its ``tzinfo`` member before being returned. Otherwise, it
+ will remain aware of its timezone.
+ """
+
+ if not stamp.tzinfo:
+ tz = get_timezone(from_)
+ stamp = tz.localize(stamp)
+ tz = get_timezone(to)
+ stamp = stamp.astimezone(tz)
+ if naive:
+ stamp = stamp.replace(tzinfo=None)
+ return stamp
+
+
+def utc_time(stamp=None, from_='local', naive=False):
+ """
+ Returns a :class:`datetime.datetime` instance, with its ``tzinfo`` member
+ set to the UTC timezone. If ``naive`` is ``True``, the result is stripped
+ of its ``tzinfo`` member.
+
+ If ``stamp`` is provided, and it is not already "aware," then its value is
+ interpreted as being local to the timezone referenced by ``from_``.
+
+ If ``stamp`` is not provided, the current time is assumed.
+ """
+
+ if not stamp:
+ stamp = datetime.datetime.utcnow()
+ stamp = pytz.utc.localize(stamp)
+ elif not stamp.tzinfo:
+ stamp = localize(stamp, from_=from_)
+ return localize(stamp, to='utc', naive=naive)