Skip to content

Commit

Permalink
Remove pytz dependency (#518)
Browse files Browse the repository at this point in the history
* Remove pytz

Everything that pytz was being utilised for can be serviced perfectly
fine by the standard library. Meanwhile, pytz is known as a footgun:

https://blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.html

* Introduce compatibility timezone code for python2
  • Loading branch information
djmattyg007 committed Mar 13, 2022
1 parent 8c18750 commit 196b5f5
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 27 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Support more lenient usernames and group names in FTP servers
([#507](https://github.com/PyFilesystem/pyfilesystem2/pull/507)).
Closes [#506](https://github.com/PyFilesystem/pyfilesystem2/issues/506).
- Removed dependency on pytz ([#518](https://github.com/PyFilesystem/pyfilesystem2/pull/518)).
Closes [#516](https://github.com/PyFilesystem/pyfilesystem2/issues/518).

### Fixed

Expand Down
11 changes: 7 additions & 4 deletions fs/_ftp_parse.py
Expand Up @@ -3,17 +3,20 @@
from __future__ import unicode_literals

import unicodedata
import datetime
import re
import time
from datetime import datetime

from pytz import UTC
try:
from datetime import timezone
except ImportError:
from ._tzcompat import timezone # type: ignore

from .enums import ResourceType
from .permissions import Permissions


EPOCH_DT = datetime.datetime.fromtimestamp(0, UTC)
EPOCH_DT = datetime.fromtimestamp(0, timezone.utc)


RE_LINUX = re.compile(
Expand Down Expand Up @@ -98,7 +101,7 @@ def _parse_time(t, formats):
day = _t.tm_mday
hour = _t.tm_hour
minutes = _t.tm_min
dt = datetime.datetime(year, month, day, hour, minutes, tzinfo=UTC)
dt = datetime(year, month, day, hour, minutes, tzinfo=timezone.utc)

epoch_time = (dt - EPOCH_DT).total_seconds()
return epoch_time
Expand Down
30 changes: 30 additions & 0 deletions fs/_tzcompat.py
@@ -0,0 +1,30 @@
"""Compatibility shim for python2's lack of datetime.timezone.
This is the example code from the Python 2 documentation:
https://docs.python.org/2.7/library/datetime.html#tzinfo-objects
"""

from datetime import tzinfo, timedelta


ZERO = timedelta(0)


class UTC(tzinfo):
"""UTC"""

def utcoffset(self, dt):
return ZERO

def tzname(self, dt):
return "UTC"

def dst(self, dt):
return ZERO


utc = UTC()


class timezone:
utc = utc
12 changes: 8 additions & 4 deletions fs/test.py
Expand Up @@ -26,7 +26,6 @@
from fs.opener import open_fs
from fs.subfs import ClosingSubFS, SubFS

import pytz
import six
from six import text_type

Expand All @@ -35,6 +34,11 @@
else:
import collections.abc as collections_abc

try:
from datetime import timezone
except ImportError:
from ._tzcompat import timezone # type: ignore


UNICODE_TEXT = """
Expand Down Expand Up @@ -1196,17 +1200,17 @@ def test_settimes(self):
can_write_acccess = info.is_writeable("details", "accessed")
can_write_modified = info.is_writeable("details", "modified")
if can_write_acccess:
self.assertEqual(info.accessed, datetime(2016, 7, 5, tzinfo=pytz.UTC))
self.assertEqual(info.accessed, datetime(2016, 7, 5, tzinfo=timezone.utc))
if can_write_modified:
self.assertEqual(info.modified, datetime(2016, 7, 5, tzinfo=pytz.UTC))
self.assertEqual(info.modified, datetime(2016, 7, 5, tzinfo=timezone.utc))

def test_touch(self):
self.fs.touch("new.txt")
self.assert_isfile("new.txt")
self.fs.settimes("new.txt", datetime(2016, 7, 5))
info = self.fs.getinfo("new.txt", namespaces=["details"])
if info.is_writeable("details", "accessed"):
self.assertEqual(info.accessed, datetime(2016, 7, 5, tzinfo=pytz.UTC))
self.assertEqual(info.accessed, datetime(2016, 7, 5, tzinfo=timezone.utc))
now = time.time()
self.fs.touch("new.txt")
accessed = self.fs.getinfo("new.txt", namespaces=["details"]).raw[
Expand Down
15 changes: 8 additions & 7 deletions fs/time.py
Expand Up @@ -7,17 +7,16 @@
import typing
from calendar import timegm
from datetime import datetime
from pytz import UTC, timezone

try:
from datetime import timezone
except ImportError:
from ._tzcompat import timezone # type: ignore

if typing.TYPE_CHECKING:
from typing import Optional


utcfromtimestamp = datetime.utcfromtimestamp
utclocalize = UTC.localize
GMT = timezone("GMT")


def datetime_to_epoch(d):
# type: (datetime) -> int
"""Convert datetime to epoch."""
Expand All @@ -39,4 +38,6 @@ def epoch_to_datetime(t): # noqa: D103
def epoch_to_datetime(t):
# type: (Optional[int]) -> Optional[datetime]
"""Convert epoch time to a UTC datetime."""
return utclocalize(utcfromtimestamp(t)) if t is not None else None
if t is None:
return None
return datetime.fromtimestamp(t, tz=timezone.utc)
1 change: 0 additions & 1 deletion setup.cfg
Expand Up @@ -43,7 +43,6 @@ setup_requires =
setuptools >=38.3.0
install_requires =
appdirs~=1.4.3
pytz
setuptools
six ~=1.10
enum34 ~=1.1.6 ; python_version < '3.4'
Expand Down
17 changes: 10 additions & 7 deletions tests/test_info.py
@@ -1,15 +1,18 @@
from __future__ import unicode_literals

import datetime
from datetime import datetime
import unittest

import pytz

from fs.enums import ResourceType
from fs.info import Info
from fs.permissions import Permissions
from fs.time import datetime_to_epoch

try:
from datetime import timezone
except ImportError:
from fs._tzcompat import timezone # type: ignore


class TestInfo(unittest.TestCase):
def test_empty(self):
Expand Down Expand Up @@ -71,10 +74,10 @@ def test_basic(self):

def test_details(self):
dates = [
datetime.datetime(2016, 7, 5, tzinfo=pytz.UTC),
datetime.datetime(2016, 7, 6, tzinfo=pytz.UTC),
datetime.datetime(2016, 7, 7, tzinfo=pytz.UTC),
datetime.datetime(2016, 7, 8, tzinfo=pytz.UTC),
datetime(2016, 7, 5, tzinfo=timezone.utc),
datetime(2016, 7, 6, tzinfo=timezone.utc),
datetime(2016, 7, 7, tzinfo=timezone.utc),
datetime(2016, 7, 8, tzinfo=timezone.utc),
]
epochs = [datetime_to_epoch(d) for d in dates]

Expand Down
11 changes: 7 additions & 4 deletions tests/test_time.py
Expand Up @@ -3,18 +3,21 @@
from datetime import datetime
import unittest

import pytz

from fs.time import datetime_to_epoch, epoch_to_datetime

try:
from datetime import timezone
except ImportError:
from fs._tzcompat import timezone # type: ignore


class TestEpoch(unittest.TestCase):
def test_epoch_to_datetime(self):
self.assertEqual(
epoch_to_datetime(142214400), datetime(1974, 7, 5, tzinfo=pytz.UTC)
epoch_to_datetime(142214400), datetime(1974, 7, 5, tzinfo=timezone.utc)
)

def test_datetime_to_epoch(self):
self.assertEqual(
datetime_to_epoch(datetime(1974, 7, 5, tzinfo=pytz.UTC)), 142214400
datetime_to_epoch(datetime(1974, 7, 5, tzinfo=timezone.utc)), 142214400
)

0 comments on commit 196b5f5

Please sign in to comment.