Skip to content

CircuitPython API Compatibility and Bugfixes #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Introduction

Basic date and time types. Implements a subset of the `CPython datetime module <https://docs.python.org/3/library/datetime.html>`_.

NOTE: This library has a large memory footprint and is intended for hardware such as the SAMD51, ESP32-S2, and nRF52.

Dependencies
=============
This driver depends on:
Expand Down
156 changes: 24 additions & 132 deletions adafruit_datetime.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -130,76 +130,6 @@ def _format_offset(off):
return s


# pylint: disable=invalid-name, too-many-locals, too-many-nested-blocks, too-many-branches, too-many-statements
def _wrap_strftime(time_obj, strftime_fmt, timetuple):
# Don't call utcoffset() or tzname() unless actually needed.
f_replace = None # the string to use for %f
z_replace = None # the string to use for %z
Z_replace = None # the string to use for %Z

# Scan strftime_fmt for %z and %Z escapes, replacing as needed.
newformat = []
push = newformat.append
i, n = 0, len(strftime_fmt)
while i < n:
ch = strftime_fmt[i]
i += 1
if ch == "%":
if i < n:
ch = strftime_fmt[i]
i += 1
if ch == "f":
if f_replace is None:
f_replace = "%06d" % getattr(time_obj, "microsecond", 0)
newformat.append(f_replace)
elif ch == "z":
if z_replace is None:
z_replace = ""
if hasattr(time_obj, "utcoffset"):
offset = time_obj.utcoffset()
if offset is not None:
sign = "+"
if offset.days < 0:
offset = -offset
sign = "-"
h, rest = divmod(offset, timedelta(hours=1))
m, rest = divmod(rest, timedelta(minutes=1))
s = rest.seconds
u = offset.microseconds
if u:
z_replace = "%c%02d%02d%02d.%06d" % (
sign,
h,
m,
s,
u,
)
elif s:
z_replace = "%c%02d%02d%02d" % (sign, h, m, s)
else:
z_replace = "%c%02d%02d" % (sign, h, m)
assert "%" not in z_replace
newformat.append(z_replace)
elif ch == "Z":
if Z_replace is None:
Z_replace = ""
if hasattr(time_obj, "tzname"):
s = time_obj.tzname()
if s is not None:
# strftime is going to have at this: escape %
Z_replace = s.replace("%", "%%")
newformat.append(Z_replace)
else:
push("%")
push(ch)
else:
push("%")
else:
push(ch)
newformat = "".join(newformat)
return _time.strftime(newformat, timetuple)


# Utility functions - timezone
def _check_tzname(name):
""""Just raise TypeError if the arg isn't None or a string."""
Expand Down Expand Up @@ -370,7 +300,7 @@ def _ord2ymd(n):
class timedelta:
"""A timedelta object represents a duration, the difference between two dates or times."""

# pylint: disable=too-many-arguments
# pylint: disable=too-many-arguments, too-many-locals, too-many-statements
def __new__(
cls,
days=0,
Expand Down Expand Up @@ -859,13 +789,15 @@ def __new__(cls, offset, name=_Omitted):
raise ValueError(
"offset must be a timedelta" " representing a whole number of minutes"
)
cls._offset = offset
cls._name = name
return cls._create(offset, name)

# pylint: disable=protected-access
# pylint: disable=protected-access, bad-super-call
@classmethod
def _create(cls, offset, name=None):
"""High-level creation for a timezone object."""
self = tzinfo.__new__(cls)
self = super(tzinfo, cls).__new__(cls)
self._offset = offset
self._name = name
return self
Expand Down Expand Up @@ -998,15 +930,6 @@ def isoformat(self, timespec="auto"):
# For a time t, str(t) is equivalent to t.isoformat()
__str__ = isoformat

def strftime(self, fmt):
"""Format using strftime(). The date part of the timestamp passed
to underlying strftime should not be used.
"""
# The year must be >= 1000 else Python's strftime implementation
# can raise a bogus exception.
timetuple = (1900, 1, 1, self._hour, self._minute, self._second, 0, 1, -1)
return _wrap_strftime(self, fmt, timetuple)

def utcoffset(self):
"""Return the timezone offset in minutes east of UTC (negative west of
UTC)."""
Expand Down Expand Up @@ -1123,8 +1046,6 @@ def _tzstr(self, sep=":"):
def __format__(self, fmt):
if not isinstance(fmt, str):
raise TypeError("must be str, not %s" % type(fmt).__name__)
if len(fmt) != 0:
return self.strftime(fmt)
return str(self)

def __repr__(self):
Expand Down Expand Up @@ -1259,7 +1180,11 @@ def _fromtimestamp(cls, t, utc, tz):
t -= 1
us += 1000000

converter = _time.gmtime if utc else _time.localtime
if utc:
raise NotImplementedError(
"CircuitPython does not currently implement time.gmtime."
)
converter = _time.localtime
struct_time = converter(t)
ss = min(struct_time[5], 59) # clamp out leap seconds if the platform has them
result = cls(
Expand All @@ -1272,39 +1197,7 @@ def _fromtimestamp(cls, t, utc, tz):
us,
tz,
)
if tz is None:
# As of version 2015f max fold in IANA database is
# 23 hours at 1969-09-30 13:00:00 in Kwajalein.
# Let's probe 24 hours in the past to detect a transition:
max_fold_seconds = 24 * 3600

struct_time = converter(t - max_fold_seconds)[:6]
probe1 = cls(
struct_time[0],
struct_time[1],
struct_time[2],
struct_time[3],
struct_time[4],
struct_time[5],
us,
tz,
)
trans = result - probe1 - timedelta(0, max_fold_seconds)
if trans.days < 0:
struct_time = converter(t + trans // timedelta(0, 1))[:6]
probe2 = cls(
struct_time[0],
struct_time[1],
struct_time[2],
struct_time[3],
struct_time[4],
struct_time[5],
us,
tz,
)
if probe2 == result:
result._fold = 1
else:
if tz is not None:
result = tz.fromutc(result)
return result

Expand All @@ -1316,7 +1209,7 @@ def fromtimestamp(cls, timestamp, tz=None):
@classmethod
def now(cls, timezone=None):
"""Return the current local date and time."""
return cls.fromtimestamp(_time.time(), timezone)
return cls.fromtimestamp(_time.time(), tz=timezone)

@classmethod
def utcfromtimestamp(cls, timestamp):
Expand Down Expand Up @@ -1449,19 +1342,18 @@ def weekday(self):
"""Return the day of the week as an integer, where Monday is 0 and Sunday is 6."""
return (self.toordinal() + 6) % 7

def strftime(self, fmt):
"""Format using strftime(). The date part of the timestamp passed
to underlying strftime should not be used.
"""
# The year must be >= 1000 else Python's strftime implementation
# can raise a bogus exception.
timetuple = (1900, 1, 1, self._hour, self._minute, self._second, 0, 1, -1)
return _wrap_strftime(self, fmt, timetuple)

def __format__(self, fmt):
if len(fmt) != 0:
return self.strftime(fmt)
return str(self)
def ctime(self):
"Return string representing the datetime."
weekday = self.toordinal() % 7 or 7
return "%s %s %2d %02d:%02d:%02d %04d" % (
_DAYNAMES[weekday],
_MONTHNAMES[self._month],
self._day,
self._hour,
self._minute,
self._second,
self._year,
)

def __repr__(self):
"""Convert to formal string, for repr()."""
Expand Down
10 changes: 2 additions & 8 deletions examples/datetime_simpletest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# Example of working with a `datetime` object
# from https://docs.python.org/3/library/datetime.html#examples-of-usage-datetime
from adafruit_datetime import datetime, date, time, timezone
from adafruit_datetime import datetime, date, time

# Using datetime.combine()
d = date(2005, 7, 14)
Expand All @@ -20,17 +20,11 @@

# Using datetime.now()
print("Current time (GMT +1):", datetime.now())
print("Current UTC time: ", datetime.now(timezone.utc))

# Using datetime.timetuple() to get tuple of all attributes
dt = datetime(2006, 11, 21, 16, 30)
tt = dt.timetuple()
for it in tt:
print(it)

# Formatting a datetime
print(
"The {1} is {0:%d}, the {2} is {0:%B}, the {3} is {0:%I:%M%p}.".format(
dt, "day", "month", "time"
)
)
print("Today is: ", dt.ctime())
7 changes: 0 additions & 7 deletions examples/datetime_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,3 @@

# Timezone name
print("Timezone Name:", t.tzname())

# Return a string representing the time, controlled by an explicit format string
strf_time = t.strftime("%H:%M:%S %Z")
print("Formatted time string:", strf_time)

# Specifies a format string in formatted string literals
print("The time is {:%H:%M}.".format(t))
18 changes: 8 additions & 10 deletions tests/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ def test_fromtimestamp(self):
got = self.theclass.fromtimestamp(ts)
self.verify_field_equality(expected, got)

@unittest.skip("gmtime not implemented in CircuitPython")
def test_utcfromtimestamp(self):
import time

Expand All @@ -514,8 +515,6 @@ def test_utcfromtimestamp(self):
got = self.theclass.utcfromtimestamp(ts)
self.verify_field_equality(expected, got)

# TODO
@unittest.skip("Wait until we bring in UTCOFFSET")
# Run with US-style DST rules: DST begins 2 a.m. on second Sunday in
# March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0).
@support.run_with_tz("EST+05EDT,M3.2.0,M11.1.0")
Expand Down Expand Up @@ -547,8 +546,6 @@ def test_timestamp_naive(self):
else:
self.assertEqual(self.theclass.fromtimestamp(s), t)

# TODO
@unittest.skip("Hold off on this test until we bring timezone in")
def test_timestamp_aware(self):
t = self.theclass(1970, 1, 1, tzinfo=timezone.utc)
self.assertEqual(t.timestamp(), 0.0)
Expand All @@ -559,6 +556,7 @@ def test_timestamp_aware(self):
)
self.assertEqual(t.timestamp(), 18000 + 3600 + 2 * 60 + 3 + 4 * 1e-6)

@unittest.skip("Not implemented - gmtime")
@support.run_with_tz("MSK-03") # Something east of Greenwich
def test_microsecond_rounding(self):
for fts in [self.theclass.fromtimestamp, self.theclass.utcfromtimestamp]:
Expand Down Expand Up @@ -599,8 +597,7 @@ def test_microsecond_rounding(self):
self.assertEqual(t.second, 0)
self.assertEqual(t.microsecond, 7812)

# TODO
@unittest.skip("timezone not implemented")
@unittest.skip("gmtime not implemented in CircuitPython")
def test_timestamp_limits(self):
# minimum timestamp
min_dt = self.theclass.min.replace(tzinfo=timezone.utc)
Expand Down Expand Up @@ -649,6 +646,7 @@ def test_insane_fromtimestamp(self):
for insane in -1e200, 1e200:
self.assertRaises(OverflowError, self.theclass.fromtimestamp, insane)

@unittest.skip("Not implemented - gmtime")
def test_insane_utcfromtimestamp(self):
# It's possible that some platform maps time_t to double,
# and that this test will fail there. This test should
Expand All @@ -657,7 +655,7 @@ def test_insane_utcfromtimestamp(self):
for insane in -1e200, 1e200:
self.assertRaises(OverflowError, self.theclass.utcfromtimestamp, insane)

@unittest.skip("Not implemented - utcnow")
@unittest.skip("gmtime not implemented in CircuitPython")
def test_utcnow(self):
import time

Expand All @@ -672,7 +670,7 @@ def test_utcnow(self):
# Else try again a few times.
self.assertLessEqual(abs(from_timestamp - from_now), tolerance)

@unittest.skip("Not implemented - strptime")
@unittest.skip("gmtime not implemented in CircuitPython")
def test_strptime(self):
string = "2004-12-01 13:02:47.197"
format = "%Y-%m-%d %H:%M:%S.%f"
Expand Down Expand Up @@ -735,7 +733,7 @@ def test_strptime(self):
with self.assertRaises(ValueError):
strptime("-000", "%z")

@unittest.skip("Not implemented - strptime")
@unittest.skip("gmtime not implemented in CircuitPython")
def test_strptime_single_digit(self):
# bpo-34903: Check that single digit dates and times are allowed.

Expand Down Expand Up @@ -798,7 +796,7 @@ def test_more_timetuple(self):
self.assertEqual(tt.tm_yday, t.toordinal() - date(t.year, 1, 1).toordinal() + 1)
self.assertEqual(tt.tm_isdst, -1)

@unittest.skip("Not implemented - strftime")
@unittest.skip("gmtime not implemented in CircuitPython")
def test_more_strftime(self):
# This tests fields beyond those tested by the TestDate.test_strftime.
t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ def test_1653736(self):
t = self.theclass(second=1)
self.assertRaises(TypeError, t.isoformat, foo=3)

@unittest.skip("strftime not implemented for CircuitPython time objects")
def test_strftime(self):
t = self.theclass(1, 2, 3, 4)
self.assertEqual(t.strftime("%H %M %S %f"), "01 02 03 000004")
Expand All @@ -231,6 +232,7 @@ def test_strftime(self):
except UnicodeEncodeError:
pass

@unittest.skip("strftime not implemented for CircuitPython time objects")
def test_format(self):
t = self.theclass(1, 2, 3, 4)
self.assertEqual(t.__format__(""), str(t))
Expand Down