Skip to content

Commit

Permalink
feat!: Remove Python 2 support (#657)
Browse files Browse the repository at this point in the history
* Remove Python 2 support from setup.py and noxfile.py

* Remove Py2 compatibility code, except in system tests

* Remove Py2 compatibility code in system tests

* Remove six from setup.py

* Remove Py2.7 constraints file

* Update README

* respond to feedback

* remove kokoro presubmit for 2.7

* Update README.rst

Co-authored-by: cojenco <cathyo@google.com>

Co-authored-by: cojenco <cathyo@google.com>
  • Loading branch information
andrewsg and cojenco committed Jan 12, 2022
1 parent 48cd000 commit b611670
Show file tree
Hide file tree
Showing 22 changed files with 108 additions and 213 deletions.
7 changes: 0 additions & 7 deletions .kokoro/presubmit/system-2.7.cfg

This file was deleted.

3 changes: 3 additions & 0 deletions README.rst
Expand Up @@ -65,6 +65,9 @@ Unsupported Python Versions
Python == 3.5: the last released version which supported Python 3.5 was
``google-cloud-storage 1.32.0``, released 2020-10-16.

Python == 2.7: the last released version which supported Python 2.7 was
``google-cloud-storage 1.44.0``, released 2022-01-05.

Mac/Linux
^^^^^^^^^

Expand Down
20 changes: 2 additions & 18 deletions google/cloud/storage/_helpers.py
Expand Up @@ -19,11 +19,9 @@

import base64
from hashlib import md5
from datetime import datetime
import os
from urllib.parse import urlsplit

from six import string_types
from six.moves.urllib.parse import urlsplit
from google import resumable_media
from google.cloud.storage.constants import _DEFAULT_TIMEOUT
from google.cloud.storage.retry import DEFAULT_RETRY
Expand Down Expand Up @@ -453,20 +451,6 @@ def _base64_md5hash(buffer_object):
return base64.b64encode(digest_bytes)


def _convert_to_timestamp(value):
"""Convert non-none datetime to timestamp.
:type value: :class:`datetime.datetime`
:param value: The datetime to convert.
:rtype: int
:returns: The timestamp.
"""
utc_naive = value.replace(tzinfo=None) - value.utcoffset()
mtime = (utc_naive - datetime(1970, 1, 1)).total_seconds()
return mtime


def _add_etag_match_headers(headers, **match_parameters):
"""Add generation match parameters into the given parameters list.
Expand All @@ -480,7 +464,7 @@ def _add_etag_match_headers(headers, **match_parameters):
value = match_parameters.get(snakecase_name)

if value is not None:
if isinstance(value, string_types):
if isinstance(value, str):
value = [value]
headers[header_name] = ", ".join(value)

Expand Down
19 changes: 10 additions & 9 deletions google/cloud/storage/_signing.py
Expand Up @@ -20,7 +20,8 @@
import hashlib
import json

import six
import http
import urllib

import google.auth.credentials

Expand Down Expand Up @@ -110,15 +111,15 @@ def get_expiration_seconds_v2(expiration):
micros = _helpers._microseconds_from_datetime(expiration)
expiration = micros // 10 ** 6

if not isinstance(expiration, six.integer_types):
if not isinstance(expiration, int):
raise TypeError(
"Expected an integer timestamp, datetime, or "
"timedelta. Got %s" % type(expiration)
)
return expiration


_EXPIRATION_TYPES = six.integer_types + (datetime.datetime, datetime.timedelta)
_EXPIRATION_TYPES = (int, datetime.datetime, datetime.timedelta)


def get_expiration_seconds_v4(expiration):
Expand All @@ -142,7 +143,7 @@ def get_expiration_seconds_v4(expiration):

now = NOW().replace(tzinfo=_helpers.UTC)

if isinstance(expiration, six.integer_types):
if isinstance(expiration, int):
seconds = expiration

if isinstance(expiration, datetime.datetime):
Expand Down Expand Up @@ -250,7 +251,7 @@ def canonicalize_v2(method, resource, query_parameters, headers):
(key.lower(), value and value.strip() or "")
for key, value in query_parameters.items()
)
encoded_qp = six.moves.urllib.parse.urlencode(normalized_qp)
encoded_qp = urllib.parse.urlencode(normalized_qp)
canonical_resource = "{}?{}".format(resource, encoded_qp)
return _Canonical(method, canonical_resource, normalized_qp, headers)

Expand Down Expand Up @@ -410,7 +411,7 @@ def generate_signed_url_v2(
return "{endpoint}{resource}?{querystring}".format(
endpoint=api_access_endpoint,
resource=resource,
querystring=six.moves.urllib.parse.urlencode(sorted_signed_query_params),
querystring=urllib.parse.urlencode(sorted_signed_query_params),
)


Expand Down Expand Up @@ -563,7 +564,7 @@ def generate_signed_url_v4(

header_names = [key.lower() for key in headers]
if "host" not in header_names:
headers["Host"] = six.moves.urllib.parse.urlparse(api_access_endpoint).netloc
headers["Host"] = urllib.parse.urlparse(api_access_endpoint).netloc

if method.upper() == "RESUMABLE":
method = "POST"
Expand Down Expand Up @@ -686,7 +687,7 @@ def _sign_message(message, access_token, service_account_email):
request = requests.Request()
response = request(url=url, method=method, body=body, headers=headers)

if response.status != six.moves.http_client.OK:
if response.status != http.client.OK:
raise exceptions.TransportError(
"Error calling the IAM signBytes API: {}".format(response.data)
)
Expand Down Expand Up @@ -723,4 +724,4 @@ def _quote_param(param):
"""
if not isinstance(param, bytes):
param = str(param)
return six.moves.urllib.parse.quote(param, safe="~")
return urllib.parse.quote(param, safe="~")
20 changes: 3 additions & 17 deletions google/cloud/storage/batch.py
Expand Up @@ -24,7 +24,6 @@
import json

import requests
import six

from google.cloud import _helpers
from google.cloud import exceptions
Expand Down Expand Up @@ -65,13 +64,7 @@ def __init__(self, method, uri, headers, body):
lines.append("")
lines.append(body)
payload = "\r\n".join(lines)
if six.PY2:
# email.message.Message is an old-style class, so we
# cannot use 'super()'.
MIMEApplication.__init__(self, payload, "http", encode_noop)
else: # pragma: NO COVER Python3
super_init = super(MIMEApplicationHTTP, self).__init__
super_init(payload, "http", encode_noop)
super().__init__(payload, "http", encode_noop)


class _FutureDict(object):
Expand Down Expand Up @@ -219,11 +212,7 @@ def _prepare_batch_request(self):
multi.attach(subrequest)
timeout = _timeout

# The `email` package expects to deal with "native" strings
if six.PY2: # pragma: NO COVER Python3
buf = io.BytesIO()
else:
buf = io.StringIO()
buf = io.StringIO()
generator = Generator(buf, False, 0)
generator.flatten(multi)
payload = buf.getvalue()
Expand Down Expand Up @@ -315,10 +304,7 @@ def _generate_faux_mime_message(parser, response):
[b"Content-Type: ", content_type, b"\nMIME-Version: 1.0\n\n", response.content]
)

if six.PY2:
return parser.parsestr(faux_message)
else: # pragma: NO COVER Python3
return parser.parsestr(faux_message.decode("utf-8"))
return parser.parsestr(faux_message.decode("utf-8"))


def _unpack_batch_response(response):
Expand Down
18 changes: 6 additions & 12 deletions google/cloud/storage/blob.py
Expand Up @@ -35,15 +35,13 @@
import mimetypes
import os
import re
from urllib.parse import parse_qsl
from urllib.parse import quote
from urllib.parse import urlencode
from urllib.parse import urlsplit
from urllib.parse import urlunsplit
import warnings

import six
from six.moves.urllib.parse import parse_qsl
from six.moves.urllib.parse import quote
from six.moves.urllib.parse import urlencode
from six.moves.urllib.parse import urlsplit
from six.moves.urllib.parse import urlunsplit

from google import resumable_media
from google.resumable_media.requests import ChunkedDownload
from google.resumable_media.requests import Download
Expand All @@ -64,7 +62,6 @@
from google.cloud.storage._helpers import _PropertyMixin
from google.cloud.storage._helpers import _scalar_property
from google.cloud.storage._helpers import _bucket_bound_hostname_url
from google.cloud.storage._helpers import _convert_to_timestamp
from google.cloud.storage._helpers import _raise_if_more_than_one_set
from google.cloud.storage._helpers import _api_core_retry_to_resumable_media_retry
from google.cloud.storage._signing import generate_signed_url_v2
Expand Down Expand Up @@ -1303,10 +1300,7 @@ def download_to_filename(

updated = self.updated
if updated is not None:
if six.PY2:
mtime = _convert_to_timestamp(updated)
else:
mtime = updated.timestamp()
mtime = updated.timestamp()
os.utime(file_obj.name, (mtime, mtime))

def download_as_bytes(
Expand Down
6 changes: 2 additions & 4 deletions google/cloud/storage/bucket.py
Expand Up @@ -18,11 +18,9 @@
import copy
import datetime
import json
from urllib.parse import urlsplit
import warnings

import six
from six.moves.urllib.parse import urlsplit

from google.api_core import datetime_helpers
from google.cloud._helpers import _datetime_to_rfc3339
from google.cloud._helpers import _NOW
Expand Down Expand Up @@ -1705,7 +1703,7 @@ def delete_blobs(
for blob in blobs:
try:
blob_name = blob
if not isinstance(blob_name, six.string_types):
if not isinstance(blob_name, str):
blob_name = blob.name
self.delete_blob(
blob_name,
Expand Down
10 changes: 2 additions & 8 deletions google/cloud/storage/retry.py
Expand Up @@ -20,19 +20,13 @@
from google.auth import exceptions as auth_exceptions


# ConnectionError is a built-in exception only in Python3 and not in Python2.
try:
_RETRYABLE_STDLIB_TYPES = (ConnectionError,)
except NameError:
_RETRYABLE_STDLIB_TYPES = ()


_RETRYABLE_TYPES = _RETRYABLE_STDLIB_TYPES + (
_RETRYABLE_TYPES = (
api_exceptions.TooManyRequests, # 429
api_exceptions.InternalServerError, # 500
api_exceptions.BadGateway, # 502
api_exceptions.ServiceUnavailable, # 503
api_exceptions.GatewayTimeout, # 504
ConnectionError,
requests.ConnectionError,
requests_exceptions.ChunkedEncodingError,
)
Expand Down
4 changes: 2 additions & 2 deletions noxfile.py
Expand Up @@ -28,8 +28,8 @@
BLACK_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"]

DEFAULT_PYTHON_VERSION = "3.8"
SYSTEM_TEST_PYTHON_VERSIONS = ["2.7", "3.8"]
UNIT_TEST_PYTHON_VERSIONS = ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"]
SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"]
UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"]
CONFORMANCE_TEST_PYTHON_VERSIONS = ["3.8"]

_DEFAULT_STORAGE_HOST = "https://storage.googleapis.com"
Expand Down
21 changes: 6 additions & 15 deletions setup.py
Expand Up @@ -28,19 +28,12 @@
# 'Development Status :: 5 - Production/Stable'
release_status = "Development Status :: 5 - Production/Stable"
dependencies = [
"google-auth >= 1.25.0, < 2.0dev; python_version<'3.0'",
"google-auth >= 1.25.0, < 3.0dev; python_version>='3.6'",
"google-api-core >= 1.29.0, < 2.0dev; python_version<'3.0'",
"google-api-core >= 1.29.0, < 3.0dev; python_version>='3.6'",
"google-cloud-core >= 1.6.0, < 2.0dev; python_version<'3.0'",
"google-cloud-core >= 1.6.0, < 3.0dev; python_version>='3.6'",
"google-resumable-media >= 1.3.0, < 2.0dev; python_version<'3.0'",
"google-resumable-media >= 1.3.0, < 3.0dev; python_version>='3.6'",
"google-auth >= 1.25.0, < 3.0dev",
"google-api-core >= 1.29.0, < 3.0dev",
"google-cloud-core >= 1.6.0, < 3.0dev",
"google-resumable-media >= 1.3.0",
"requests >= 2.18.0, < 3.0.0dev",
"protobuf < 3.18.0; python_version<'3.0'",
"protobuf; python_version>='3.6'",
"googleapis-common-protos < 1.53.0; python_version<'3.0'",
"six",
"protobuf",
]
extras = {}

Expand Down Expand Up @@ -84,8 +77,6 @@
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
Expand All @@ -100,7 +91,7 @@
namespace_packages=namespaces,
install_requires=dependencies,
extras_require=extras,
python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*",
python_requires=">=3.6",
include_package_data=True,
zip_safe=False,
)
2 changes: 0 additions & 2 deletions testing/constraints-2.7.txt

This file was deleted.

14 changes: 1 addition & 13 deletions tests/system/_helpers.py
Expand Up @@ -14,8 +14,6 @@

import os

import six

from google.api_core import exceptions

from test_utils.retry import RetryErrors
Expand All @@ -27,17 +25,7 @@
retry_429_503 = RetryErrors(
[exceptions.TooManyRequests, exceptions.ServiceUnavailable], max_tries=10
)

# Work around https://github.com/googleapis/python-test-utils/issues/36
if six.PY3:
retry_failures = RetryErrors(AssertionError)
else:

def retry_failures(decorated): # no-op
wrapped = RetryErrors(AssertionError)(decorated)
wrapped.__wrapped__ = decorated
return wrapped

retry_failures = RetryErrors(AssertionError)

user_project = os.environ.get("GOOGLE_CLOUD_TESTS_USER_PROJECT")
testing_mtls = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE") == "true"
Expand Down
3 changes: 1 addition & 2 deletions tests/system/test_blob.py
Expand Up @@ -20,7 +20,6 @@
import warnings

import pytest
import six
import mock

from google import resumable_media
Expand All @@ -33,7 +32,7 @@

def _check_blob_hash(blob, info):
md5_hash = blob.md5_hash
if not isinstance(md5_hash, six.binary_type):
if not isinstance(md5_hash, bytes):
md5_hash = md5_hash.encode("utf-8")

assert md5_hash == info["hash"]
Expand Down

0 comments on commit b611670

Please sign in to comment.