pendulum icon indicating copy to clipboard operation
pendulum copied to clipboard

AttributeError: 'NoneType' object has no attribute 'convert'

Open Q-back opened this issue 3 years ago • 6 comments

  • [x] I am on the latest Pendulum version.
  • [x] I have searched the issues of this repo and believe that this is not a duplicate.
  • OS version and name: MacOS 10.14.6
  • Pendulum version: 2.1.2

Issue

It's highly probable to break the adding/substracting time feature after messing with the timezone. And the error is very confusing.

How to reproduce

Run the following script:

import pendulum
import datetime
import pytz

duration = pendulum.duration(days=2)
timezone = pendulum.timezone('UTC')
datetime = pendulum.datetime(2020, 12, 12, tz=tz)
datetime = dt.astimezone(pytz.UTC)  # Let's break some things
result = datetime - duration  # Exception is raised!

Exception

...
    dt = self.tz.convert(dt)
AttributeError: 'NoneType' object has no attribute 'convert'

How to fix

Go to https://github.com/sdispater/pendulum/blob/2.1.2/pendulum/datetime.py#L225 and change the line to something like return pendulum.timezone(self.tzinfo). Try to cast it to pendulum's timezone.

The story behind a bug

  1. I'm using Django Rest Framework, and I have serializer like this:
class SomeSerializer(serializers.Serializer):
    date = serializers.DateTimeField(...)

    def to_internal_value(self, data):
        data['date'] = pendulum.now()
        return super().to_internal_value(data)
  1. Django Rest Framework does call .as_timezone with some pytz.UTC data somewhere like in fields.py
    def enforce_timezone(self, value):
        """
        When `self.default_timezone` is `None`, always return naive datetimes.
        When `self.default_timezone` is not `None`, always return aware datetimes.
        """
        field_timezone = getattr(self, 'timezone', self.default_timezone())

        if field_timezone is not None:
            if timezone.is_aware(value):
                try:
                    return value.astimezone(field_timezone)
        ...

Q-back avatar Dec 29 '20 21:12 Q-back

I have the same issue using pendulum 2.1.2.

I believe this issue was also reported in #160 ( 2017 closed ) and #472 ( 2020 open ).

Assigning datetime.timezone.utc in astimezone, the timezone is not the expected datetime.timezone type, but a pendulum.tz.timezone.FixedTimezone. The pendulum.Datetime then throws exceptions in operations including adding a datetime.timedelta, operations that behave as expected in datetime.

  • datetime
import datetime
import pendulum

epoch = '2021-04-30T14:34:56'
date = datetime.datetime.strptime( epoch, '%Y-%m-%dT%H:%M:%S' )
print( f'Date = {date.isoformat( )} Timezone = {date.tzinfo}' )
date = date.astimezone( datetime.timezone.utc )
print( f'Date = {date.isoformat( )} Timezone = {date.tzinfo}' )
date += datetime.timedelta( seconds = 1 )

Date = 2021-04-30T14:34:56 Timezone = None Date = 2021-04-30T18:34:56+00:00 Timezone = UTC

  • pendulum
date = pendulum.parse( epoch )
print( f'Date = {date.isoformat( )} Timezone = {date.tzinfo}' )
date = date.astimezone( datetime.timezone.utc )  # Problem.
print( f'Date = {date.isoformat( )} Timezone = {date.tzinfo}' )
try :
    date += datetime.timedelta( seconds = 1 )
except Exception as ex :
    print( f'Exception = {ex.__class__.__name__} {ex}' )

Date = 2021-04-30T14:34:56+00:00 Timezone = Timezone('UTC') Date = 2021-04-30T14:34:56+00:00 Timezone = UTC Exception = AttributeError 'NoneType' object has no attribute 'convert'

larryturner avatar Apr 30 '21 17:04 larryturner

Pendulum version: 2.1.2

import pendulum

now = pendulum.now()
s = now.isoformat()
now2 = pendulum.DateTime.fromisoformat(s)
now2.add(seconds=1)

You get:

Traceback (most recent call last):
  File ".../arrow_test.py", line 13, in <module>
    now2.add(seconds=1)
  File ".../venv/lib/python3.10/site-packages/pendulum/datetime.py", line 667, in add
    dt = self.tz.convert(dt)
AttributeError: 'NoneType' object has no attribute 'convert'

I'd like to use Pendulum with TinyDB by providing a custom serializer that uses isoformat and fromisoformat, but apparently the library is broken and abandoned.

Nitrooo avatar Jun 20 '22 21:06 Nitrooo

@Q-back s answer, but modified for the __lower__ is to return pendulum.timezone(self.tzinfo.key) on line 225 : Go to https://github.com/sdispater/pendulum/blob/2.1.2/pendulum/datetime.py#L225

specialorange avatar Sep 30 '22 22:09 specialorange

edit: see my updated code in a more recent comment

For those of you that encounter this problem and don't control the library (such as running your app on Google appspot), monkey patch it in Django. Put this in monkey_pendulum/__init__.py (the root of your Django project) and add monkey_pendulum to your settings.INSTALLED_APPS

#
# https://github.com/sdispater/pendulum/issues/527
#

import logging
import pendulum

from pendulum.datetime import DateTime
from pendulum.tz.timezone import Timezone

logging.getLogger().warning('monkey patching pendulum.datetime.DateTime')


def monkey_timezone(self):  # type: () -> Optional[Timezone]
    if not isinstance(self.tzinfo, Timezone):
        return pendulum.timezone(self.tzinfo.key)

    return self.tzinfo


DateTime.timezone = property(monkey_timezone)

FirefighterBlu3 avatar Jan 29 '23 03:01 FirefighterBlu3

I have encountered this issue when trying to convert a project from datetime to pendulum. Our code base is full of datetime.now().astimezone() in order to work with timezone aware datetimes. Given the declaration of pendulum to be a drop-in replacement for datetime, I had expected I could just replace this with pendulum.DateTime.now().astimezone() but as reported above this does not work.

I would like to add to this that on the latest alpha and on this project's master branch, this behavior seems to be broken in a different way. The subtraction no longer raises an exception, but the result no longer seems to have any timezone information whatsoever. It seems to be a naive datetime object, in UTC (implicit).

>>> import pendulum
>>> import datetime
>>> now_in_some_non_local_non_utc_timezone = pendulum.DateTime.now(tz=datetime.timezone.max)
>>> now_in_some_non_local_non_utc_timezone
DateTime(2023, 9, 22, 12, 29, 3, 536950, tzinfo=FixedTimezone(86340, name="+23:59"))
>>> now_in_some_non_local_non_utc_timezone.tz
FixedTimezone(86340, name="+23:59")
>>> now_in_some_non_local_non_utc_timezone.tzinfo
FixedTimezone(86340, name="+23:59")
>>> now_in_local_timezone = now_in_some_non_local_non_utc_timezone.astimezone()
>>> now_in_local_timezone
DateTime(2023, 9, 21, 14, 30, 3, 536950, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'CEST'))
>>> now_in_local_timezone.tz
>>> now_in_local_timezone.tzinfo
datetime.timezone(datetime.timedelta(seconds=7200), 'CEST')
>>> one_second_ago_in_local_timezone = now_in_local_timezone - pendulum.Duration(seconds=1)
>>> one_second_ago_in_local_timezone
DateTime(2023, 9, 21, 12, 30, 2, 536950)
>>> one_second_ago_in_local_timezone.tz
>>> one_second_ago_in_local_timezone.tzinfo
>>> one_second_ago_in_local_timezone < now_in_local_timezone
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes

sanderr avatar Sep 21 '23 12:09 sanderr

followup to my fixup above, I've encountered other situations so, here's the monkey patch. this one includes:

  • logging so you can see what package is being used for the datetime object
  • an override for a UTC object because "+00:00" isn't recognized as a tz key for the name
  • a limited traceback for other packages to show where the call is originating from
#
# https://github.com/sdispater/pendulum/issues/527
#

import logging
import pendulum
import traceback

from pendulum.datetime import DateTime
from pendulum.tz.timezone import FixedTimezone, Timezone

logger = logging.getLogger()
logger.warning('monkey patching pendulum.datetime.DateTime')


def monkey_timezone(self):  # type: () -> Optional[Timezone]
    if not isinstance(self.tzinfo, Timezone):
        if hasattr(self.tzinfo, 'key'):
            # zoneinfo timezone
            logger.info(f'looking up monkey_timezone(zoneinfo) using tzinfo.key {self.tzinfo.key}')
            return pendulum.timezone(self.tzinfo.key)
        elif hasattr(self.tzinfo, 'name'):
            # pendulum Timezone
            logger.info(f'looking up monkey_timezone(pendulum) using tzinfo.name {self.tzinfo.name}')
            if self.tzinfo.name == '+00:00':
                return pendulum.timezone('UTC')
            else:
                return pendulum.timezone(self.tzinfo.name)
        elif hasattr(self.tzinfo, 'zone'):
            # pytz
            logger.info(f'looking up monkey_timezone(pytz) using tzinfo.zone {self.tzinfo.zone}')
            return pendulum.timezone(self.tzinfo.zone)
    elif isinstance(self.tzinfo, FixedTimezone):
        # pendulum.FixedTimezone
        logger.info(f'looking up monkey_timezone(FixedTimezone), probably naive object {self.tzinfo}')
        return self.tzinfo

    logger.info(f'uhoh, monkey_timezone(naive?) simply returning tzinfo {type(self.tzinfo)}')
    stack = traceback.format_stack(limit=6)
    stack = ''.join([x for x in stack if 'venv' not in x and 'monkey' not in x])
    logger.info(stack)
    return self.tzinfo


DateTime.timezone = property(monkey_timezone)

FirefighterBlu3 avatar Jan 28 '24 00:01 FirefighterBlu3