Repository: sdispater/pendulum
Pull request #158 (2017-11-08 02:58:12)

Fix instance() behaviour with pytz offsets
`pytz` has a class of timezone called a `FixedOffset`. This class only
stores an offset, not a zone name.

When calling `instance()` on a datetime that was using one of these
`FixedOffset` objects as a timezone, Pendulum would totally ignore the
offset and use the local timezone instead. This would happen even if a
`tz` argument was directly supplied to the `instance()` call.

Additionally, the fallback to a fixed offset would always use the
datetime's timezone, even it if was `None` and a `tz` argument was
supplied to the `instance()` call.

This commit makes 2 small changes:
- The `zone` property of the "effective timezone" (the datetime's
  `tzinfo` with a fallback to the passed in `tz`) is only used if it is
  non-null. This fixes using `FixedOffset` objects as timezones.
- The fallback to a fixed offset now uses the offset of the "effective
  timezone" instead of the datetime's offset. This correctly handles
  cases where the timezone on the datetime is `None` and a fallback is
  provided.


Patch:
diff --git a/README.rst b/README.rst
index 4d0ce85..8887e5e 100644
--- a/README.rst
+++ b/README.rst
@@ -133,7 +133,7 @@ Even though the ``Pendulum`` class is a subclass of ``datetime`` there are some
 it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with
 a possible solution, if any:
 
-* ``sqlite3`` will use the the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
 
 .. code-block:: python
 
@@ -142,7 +142,7 @@ a possible solution, if any:
 
     register_adapter(Pendulum, lambda val: val.isoformat(' '))
 
-* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
 
 .. code-block:: python
 
diff --git a/pendulum/parsing/parser.py b/pendulum/parsing/parser.py
index 90ed56b..d64fd7c 100644
--- a/pendulum/parsing/parser.py
+++ b/pendulum/parsing/parser.py
@@ -305,7 +305,7 @@ class Parser(object):
 
         :rtype: dict
         """
-        if self.is_strict():
+        if self.is_exact():
             return parsed
 
         if any(('year' not in parsed, 'month' not in parsed, 'day' not in parsed)):
diff --git a/pendulum/pendulum.py b/pendulum/pendulum.py
index c73e8c6..74a04f5 100644
--- a/pendulum/pendulum.py
+++ b/pendulum/pendulum.py
@@ -205,13 +205,13 @@ class Pendulum(Date, datetime.datetime):
         # Checking for pytz/tzinfo
         if isinstance(tz, datetime.tzinfo) and not isinstance(tz, (Timezone, TimezoneInfo)):
             # pytz
-            if hasattr(tz, 'localize'):
+            if hasattr(tz, 'localize') and tz.zone:
                 tz = tz.zone
             else:
                 # We have no sure way to figure out
                 # the timezone name, we fallback
                 # on a fixed offset
-                tz = dt.utcoffset().total_seconds() / 3600
+                tz = tz.utcoffset(dt).total_seconds() / 3600
 
         return cls(
             dt.year, dt.month, dt.day,


Test patch:
diff --git a/tests/pendulum_tests/test_construct.py b/tests/pendulum_tests/test_construct.py
index a103638..ffb0059 100644
--- a/tests/pendulum_tests/test_construct.py
+++ b/tests/pendulum_tests/test_construct.py
@@ -135,6 +135,25 @@ class ConstructTest(AbstractTestCase):
         )
         self.assertEqual('Europe/Paris', now.timezone_name)
 
+    def test_instance_timezone_aware_datetime_pytz_offset(self):
+        # Impossible timezone of +21 (won't accidentally match the local offset)
+        fixed_offset = pytz.FixedOffset(21 * 60)
+
+        now = Pendulum.instance(
+            datetime.now(fixed_offset)
+        )
+        self.assertEqual(21, now.offset_hours)
+
+        now = Pendulum.instance(
+            datetime.now(), fixed_offset
+        )
+        self.assertEqual(21, now.offset_hours)
+
+        now = Pendulum.instance(
+            datetime.now(fixed_offset), pytz.timezone('Europe/Paris')
+        )
+        self.assertEqual(21, now.offset_hours)
+
     def test_instance_timezone_aware_datetime_any_tzinfo(self):
         dt = datetime(2016, 8, 7, 12, 34, 56, tzinfo=tz.gettz('Europe/Paris'))
         now = Pendulum.instance(dt)