pendulum
pendulum copied to clipboard
AttributeError: 'NoneType' object has no attribute 'convert'
- [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
- 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)
- Django Rest Framework does call
.as_timezone
with somepytz.UTC
data somewhere like infields.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)
...
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'
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.
@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
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 addmonkey_pendulum
to yoursettings.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)
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
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)