From 696db8cf20d4ba737fdeb62157e846dc95981f0b Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 8 Apr 2020 00:19:42 +0200 Subject: [PATCH] Rework Python 2-3 compat (#268) --- pyicloud/base.py | 39 ++++------ pyicloud/cmdline.py | 8 +- pyicloud/services/account.py | 120 +++++++++++++++++++----------- pyicloud/services/calendar.py | 2 +- pyicloud/services/findmyiphone.py | 27 ++++--- pyicloud/services/photos.py | 14 ++-- pyicloud/services/ubiquity.py | 8 +- pyicloud/utils.py | 12 +-- tests/test_account.py | 58 ++++++++++----- tests/test_cmdline.py | 8 +- 10 files changed, 169 insertions(+), 127 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 23e2b4f..2c9a807 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -1,14 +1,14 @@ """Library base file.""" -import six -import uuid +from six import PY2, string_types +from uuid import uuid1 import inspect import json import logging from requests import Session -import sys -import tempfile -import os +from tempfile import gettempdir +from os import path, mkdir from re import match +import http.cookiejar as cookielib from pyicloud.exceptions import ( PyiCloudFailedLoginException, @@ -27,11 +27,6 @@ from pyicloud.services import ( ) from pyicloud.utils import get_password_from_keyring -if six.PY3: - import http.cookiejar as cookielib -else: - import cookielib # pylint: disable=import-error - LOGGER = logging.getLogger(__name__) @@ -99,7 +94,7 @@ class PyiCloudSession(Session): reason = data.get("errorMessage") reason = reason or data.get("reason") reason = reason or data.get("errorReason") - if not reason and isinstance(data.get("error"), six.string_types): + if not reason and isinstance(data.get("error"), string_types): reason = data.get("error") if not reason and data.get("error"): reason = "Unknown reason" @@ -166,7 +161,7 @@ class PyiCloudService(object): password = get_password_from_keyring(apple_id) self.data = {} - self.client_id = client_id or str(uuid.uuid1()).upper() + self.client_id = client_id or str(uuid1()).upper() self.with_family = with_family self.user = {"apple_id": apple_id, "password": password} @@ -176,11 +171,9 @@ class PyiCloudService(object): self._base_login_url = "%s/login" % self.SETUP_ENDPOINT if cookie_directory: - self._cookie_directory = os.path.expanduser( - os.path.normpath(cookie_directory) - ) + self._cookie_directory = path.expanduser(path.normpath(cookie_directory)) else: - self._cookie_directory = os.path.join(tempfile.gettempdir(), "pyicloud",) + self._cookie_directory = path.join(gettempdir(), "pyicloud") self.session = PyiCloudSession(self) self.session.verify = verify @@ -194,7 +187,7 @@ class PyiCloudService(object): cookiejar_path = self._get_cookiejar_path() self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path) - if os.path.exists(cookiejar_path): + if path.exists(cookiejar_path): try: self.session.cookies.load() LOGGER.debug("Read cookies from %s", cookiejar_path) @@ -242,8 +235,8 @@ class PyiCloudService(object): self.params.update({"dsid": self.data["dsInfo"]["dsid"]}) self._webservices = self.data["webservices"] - if not os.path.exists(self._cookie_directory): - os.mkdir(self._cookie_directory) + if not path.exists(self._cookie_directory): + mkdir(self._cookie_directory) self.session.cookies.save() LOGGER.debug("Cookies saved to %s", self._get_cookiejar_path()) @@ -252,7 +245,7 @@ class PyiCloudService(object): def _get_cookiejar_path(self): """Get path for cookiejar file.""" - return os.path.join( + return path.join( self._cookie_directory, "".join([c for c in self.user.get("apple_id") if match(r"\w", c)]), ) @@ -373,9 +366,9 @@ class PyiCloudService(object): def __str__(self): as_unicode = self.__unicode__() - if sys.version_info[0] >= 3: - return as_unicode - return as_unicode.encode("utf-8", "ignore") + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode def __repr__(self): return "<%s>" % str(self) diff --git a/pyicloud/cmdline.py b/pyicloud/cmdline.py index 6101fa0..a6bbcf7 100644 --- a/pyicloud/cmdline.py +++ b/pyicloud/cmdline.py @@ -5,10 +5,10 @@ A Command Line Wrapper to allow easy use of pyicloud for command line scripts, and related. """ from __future__ import print_function +from builtins import input import argparse import pickle import sys -import six from click import confirm @@ -16,12 +16,6 @@ from pyicloud import PyiCloudService from pyicloud.exceptions import PyiCloudFailedLoginException from . import utils -# fmt: off -if six.PY2: - input = raw_input # pylint: disable=redefined-builtin,invalid-name,undefined-variable -else: - input = input # pylint: disable=bad-option-value,self-assigning-variable,invalid-name -# fmt: on DEVICE_ERROR = "Please use the --device switch to indicate which device to use." diff --git a/pyicloud/services/account.py b/pyicloud/services/account.py index f771036..f07e51e 100644 --- a/pyicloud/services/account.py +++ b/pyicloud/services/account.py @@ -1,7 +1,7 @@ """Account service.""" -import sys - -import six +from __future__ import division +from six import PY2, python_2_unicode_compatible +from collections import OrderedDict from pyicloud.utils import underscore_to_camelcase @@ -68,27 +68,41 @@ class AccountService(object): return self._storage + def __unicode__(self): + return "{devices: %s, family: %s, storage: %s bytes free}" % ( + len(self.devices), + len(self.family), + self.storage.usage.available_storage_in_bytes, + ) -@six.python_2_unicode_compatible + def __str__(self): + as_unicode = self.__unicode__() + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode + + def __repr__(self): + return "<%s: %s>" % (type(self).__name__, str(self)) + + +@python_2_unicode_compatible class AccountDevice(dict): """Account device.""" def __getattr__(self, key): return self[underscore_to_camelcase(key)] + def __unicode__(self): + return "{model: %s, name: %s}" % (self.model_display_name, self.name) + def __str__(self): - return u"{display_name}: {name}".format( - display_name=self.model_display_name, name=self.name, - ) + as_unicode = self.__unicode__() + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode def __repr__(self): - return "<{display}>".format( - display=( - six.text_type(self) - if sys.version_info[0] >= 3 - else six.text_type(self).encode("utf8", "replace") - ) - ) + return "<%s: %s>" % (type(self).__name__, str(self)) class FamilyMember(object): @@ -193,19 +207,20 @@ class FamilyMember(object): return self._attrs[key] return getattr(self, key) - def __str__(self): - return u"{full_name}: {age_classification}".format( - full_name=self.full_name, age_classification=self.age_classification, + def __unicode__(self): + return "{name: %s, age_classification: %s}" % ( + self.full_name, + self.age_classification, ) + def __str__(self): + as_unicode = self.__unicode__() + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode + def __repr__(self): - return "<{display}>".format( - display=( - six.text_type(self) - if sys.version_info[0] >= 3 - else six.text_type(self).encode("utf8", "replace") - ) - ) + return "<%s: %s>" % (type(self).__name__, str(self)) class AccountStorageUsageForMedia(object): @@ -234,17 +249,17 @@ class AccountStorageUsageForMedia(object): """Gets the usage in bytes.""" return self.usage_data["usageInBytes"] + def __unicode__(self): + return "{key: %s, usage: %s bytes}" % (self.key, self.usage_in_bytes) + def __str__(self): - return u"{key}: {usage}".format(key=self.key, usage=self.usage_in_bytes) + as_unicode = self.__unicode__() + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode def __repr__(self): - return "<{display}>".format( - display=( - six.text_type(self) - if sys.version_info[0] >= 3 - else six.text_type(self).encode("utf8", "replace") - ) - ) + return "<%s: %s>" % (type(self).__name__, str(self)) class AccountStorageUsage(object): @@ -267,7 +282,7 @@ class AccountStorageUsage(object): @property def used_storage_in_percent(self): """Gets the used storage in percent.""" - return self.used_storage_in_bytes * 100 / self.total_storage_in_bytes + return round(self.used_storage_in_bytes * 100 / self.total_storage_in_bytes, 2) @property def available_storage_in_bytes(self): @@ -277,7 +292,9 @@ class AccountStorageUsage(object): @property def available_storage_in_percent(self): """Gets the available storage in percent.""" - return self.available_storage_in_bytes * 100 / self.total_storage_in_bytes + return round( + self.available_storage_in_bytes * 100 / self.total_storage_in_bytes, 2 + ) @property def total_storage_in_bytes(self): @@ -309,19 +326,20 @@ class AccountStorageUsage(object): """Gets the paid quota.""" return self.quota_data["paidQuota"] - def __str__(self): - return u"{used_percent}%% used of {total} bytes".format( - used_percent=self.used_storage_in_percent, total=self.total_storage_in_bytes + def __unicode__(self): + return "%s%% used of %s bytes" % ( + self.used_storage_in_percent, + self.total_storage_in_bytes, ) + def __str__(self): + as_unicode = self.__unicode__() + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode + def __repr__(self): - return "<{display}>".format( - display=( - six.text_type(self) - if sys.version_info[0] >= 3 - else six.text_type(self).encode("utf8", "replace") - ) - ) + return "<%s: %s>" % (type(self).__name__, str(self)) class AccountStorage(object): @@ -331,9 +349,21 @@ class AccountStorage(object): self.usage = AccountStorageUsage( storage_data.get("storageUsageInfo"), storage_data.get("quotaStatus") ) - self.usages_by_media = {} + self.usages_by_media = OrderedDict() for usage_media in storage_data.get("storageUsageByMedia"): self.usages_by_media[usage_media["mediaKey"]] = AccountStorageUsageForMedia( usage_media ) + + def __unicode__(self): + return "{usage: %s, usages_by_media: %s}" % (self.usage, self.usages_by_media) + + def __str__(self): + as_unicode = self.__unicode__() + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode + + def __repr__(self): + return "<%s: %s>" % (type(self).__name__, str(self)) diff --git a/pyicloud/services/calendar.py b/pyicloud/services/calendar.py index 9dc0422..cbe1288 100644 --- a/pyicloud/services/calendar.py +++ b/pyicloud/services/calendar.py @@ -17,7 +17,7 @@ class CalendarService(object): self._service_root = service_root self._calendar_endpoint = "%s/ca" % self._service_root self._calendar_refresh_url = "%s/events" % self._calendar_endpoint - self._calendar_event_detail_url = "%s/eventdetail" % (self._calendar_endpoint,) + self._calendar_event_detail_url = "%s/eventdetail" % self._calendar_endpoint self._calendars = "%s/startup" % self._calendar_endpoint self.response = {} diff --git a/pyicloud/services/findmyiphone.py b/pyicloud/services/findmyiphone.py index d0fe84e..9f677d7 100644 --- a/pyicloud/services/findmyiphone.py +++ b/pyicloud/services/findmyiphone.py @@ -1,8 +1,7 @@ """Find my iPhone service.""" import json -import sys -import six +from six import PY2, text_type from pyicloud.exceptions import PyiCloudNoDevicesException @@ -70,26 +69,26 @@ class FindMyiPhoneServiceManager(object): def __getitem__(self, key): if isinstance(key, int): - if six.PY3: - key = list(self.keys())[key] - else: + if PY2: key = self.keys()[key] + else: + key = list(self.keys())[key] return self._devices[key] def __getattr__(self, attr): return getattr(self._devices, attr) def __unicode__(self): - return six.text_type(self._devices) + return text_type(self._devices) def __str__(self): as_unicode = self.__unicode__() - if sys.version_info[0] >= 3: - return as_unicode - return as_unicode.encode("utf-8", "ignore") + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode def __repr__(self): - return six.text_type(self) + return text_type(self) class AppleDevice(object): @@ -204,13 +203,13 @@ class AppleDevice(object): def __unicode__(self): display_name = self["deviceDisplayName"] name = self["name"] - return "%s: %s" % (display_name, name,) + return "%s: %s" % (display_name, name) def __str__(self): as_unicode = self.__unicode__() - if sys.version_info[0] >= 3: - return as_unicode - return as_unicode.encode("utf-8", "ignore") + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode def __repr__(self): return "" % str(self) diff --git a/pyicloud/services/photos.py b/pyicloud/services/photos.py index 78b2f5a..30510ab 100644 --- a/pyicloud/services/photos.py +++ b/pyicloud/services/photos.py @@ -1,14 +1,16 @@ """Photo service.""" -import sys import json import base64 +from six import PY2 + +# fmt: off +from six.moves.urllib.parse import urlencode # pylint: disable=bad-option-value,relative-import +# fmt: on from datetime import datetime from pyicloud.exceptions import PyiCloudServiceNotActivatedException from pytz import UTC -from future.moves.urllib.parse import urlencode - class PhotosService(object): """The 'Photos' iCloud service.""" @@ -473,9 +475,9 @@ class PhotoAlbum(object): def __str__(self): as_unicode = self.__unicode__() - if sys.version_info[0] >= 3: - return as_unicode - return as_unicode.encode("utf-8", "ignore") + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode def __repr__(self): return "<%s: '%s'>" % (type(self).__name__, self) diff --git a/pyicloud/services/ubiquity.py b/pyicloud/services/ubiquity.py index 8c6c420..8ec8726 100644 --- a/pyicloud/services/ubiquity.py +++ b/pyicloud/services/ubiquity.py @@ -1,6 +1,6 @@ """File service.""" from datetime import datetime -import sys +from six import PY2 class UbiquityService(object): @@ -112,9 +112,9 @@ class UbiquityNode(object): def __str__(self): as_unicode = self.__unicode__() - if sys.version_info[0] >= 3: - return as_unicode - return as_unicode.encode("utf-8", "ignore") + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode def __repr__(self): return "<%s: '%s'>" % (self.type.capitalize(), self) diff --git a/pyicloud/utils.py b/pyicloud/utils.py index d90f3ca..04ba6fa 100644 --- a/pyicloud/utils.py +++ b/pyicloud/utils.py @@ -1,7 +1,7 @@ """Utils.""" import getpass import keyring -import sys +from sys import stdout from .exceptions import PyiCloudNoStoredPasswordAvailableException @@ -9,7 +9,7 @@ from .exceptions import PyiCloudNoStoredPasswordAvailableException KEYRING_SYSTEM = "pyicloud://icloud-password" -def get_password(username, interactive=sys.stdout.isatty()): +def get_password(username, interactive=stdout.isatty()): """Get the password from a username.""" try: return get_password_from_keyring(username) @@ -18,7 +18,7 @@ def get_password(username, interactive=sys.stdout.isatty()): raise return getpass.getpass( - "Enter iCloud password for {username}: ".format(username=username,) + "Enter iCloud password for {username}: ".format(username=username) ) @@ -40,7 +40,7 @@ def get_password_from_keyring(username): "No pyicloud password for {username} could be found " "in the system keychain. Use the `--store-in-keyring` " "command-line option for storing a password for this " - "username.".format(username=username,) + "username.".format(username=username) ) return result @@ -48,12 +48,12 @@ def get_password_from_keyring(username): def store_password_in_keyring(username, password): """Store the password of a username.""" - return keyring.set_password(KEYRING_SYSTEM, username, password,) + return keyring.set_password(KEYRING_SYSTEM, username, password) def delete_password_in_keyring(username): """Delete the password of a username.""" - return keyring.delete_password(KEYRING_SYSTEM, username,) + return keyring.delete_password(KEYRING_SYSTEM, username) def underscore_to_camelcase(word, initial_capital=False): diff --git a/tests/test_account.py b/tests/test_account.py index 1e6bbc3..7d0b999 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,5 +1,7 @@ """Account service tests.""" from unittest import TestCase +from six import PY3 + from . import PyiCloudServiceMock from .const import AUTHENTICATED_USER, VALID_PASSWORD @@ -12,6 +14,12 @@ class AccountServiceTest(TestCase): def setUp(self): self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD).account + def test_repr(self): + """Tests representation.""" + # fmt: off + assert repr(self.service) == "" + # fmt: on + def test_devices(self): """Tests devices.""" assert self.service.devices @@ -32,6 +40,10 @@ class AccountServiceTest(TestCase): assert device["modelSmallPhotoURL2x"] assert device["modelSmallPhotoURL1x"] assert device["modelDisplayName"] + # fmt: off + if PY3: + assert repr(device) == "" + # fmt: on def test_family(self): """Tests family members.""" @@ -51,30 +63,39 @@ class AccountServiceTest(TestCase): assert not member.has_ask_to_buy_enabled assert not member.share_my_location_enabled_family_members assert member.dsid_for_purchases + # fmt: off + assert repr(member) == "" + # fmt: on def test_storage(self): """Tests storage.""" assert self.service.storage + # fmt: off + if PY3: + assert repr(self.service.storage) == "), ('backup', ), ('docs', ), ('mail', )])}>" + # fmt: on + def test_storage_usage(self): + """Tests storage usage.""" assert self.service.storage.usage - assert ( - self.service.storage.usage.comp_storage_in_bytes - or self.service.storage.usage.comp_storage_in_bytes == 0 - ) - assert self.service.storage.usage.used_storage_in_bytes - assert self.service.storage.usage.used_storage_in_percent - assert self.service.storage.usage.available_storage_in_bytes - assert self.service.storage.usage.available_storage_in_percent - assert self.service.storage.usage.total_storage_in_bytes - assert ( - self.service.storage.usage.commerce_storage_in_bytes - or self.service.storage.usage.commerce_storage_in_bytes == 0 - ) - assert not self.service.storage.usage.quota_over - assert not self.service.storage.usage.quota_tier_max - assert not self.service.storage.usage.quota_almost_full - assert not self.service.storage.usage.quota_paid + usage = self.service.storage.usage + assert usage.comp_storage_in_bytes or usage.comp_storage_in_bytes == 0 + assert usage.used_storage_in_bytes + assert usage.used_storage_in_percent + assert usage.available_storage_in_bytes + assert usage.available_storage_in_percent + assert usage.total_storage_in_bytes + assert usage.commerce_storage_in_bytes or usage.commerce_storage_in_bytes == 0 + assert not usage.quota_over + assert not usage.quota_tier_max + assert not usage.quota_almost_full + assert not usage.quota_paid + # fmt: off + assert repr(usage) == "" + # fmt: on + def test_storage_usages_by_media(self): + """Tests storage usages by media.""" assert self.service.storage.usages_by_media for usage_media in self.service.storage.usages_by_media.values(): @@ -82,3 +103,6 @@ class AccountServiceTest(TestCase): assert usage_media.label assert usage_media.color assert usage_media.usage_in_bytes or usage_media.usage_in_bytes == 0 + # fmt: off + assert repr(usage_media) == "" + # fmt: on diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 7f02f31..0076f0f 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -5,15 +5,15 @@ from .const import AUTHENTICATED_USER, REQUIRES_2SA_USER, VALID_PASSWORD from .const_findmyiphone import FMI_FAMILY_WORKING import os -import sys +from six import PY2 import pickle import pytest from unittest import TestCase -if sys.version_info >= (3, 3): - from unittest.mock import patch # pylint: disable=no-name-in-module,import-error -else: +if PY2: from mock import patch +else: + from unittest.mock import patch # pylint: disable=no-name-in-module,import-error class TestCmdline(TestCase):