Skip to content
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

feat!: Remove Python 2 support #657

Merged
merged 13 commits into from Jan 12, 2022
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
andrewsg marked this conversation as resolved.
Show resolved Hide resolved

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
andrewsg marked this conversation as resolved.
Show resolved Hide resolved
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
andrewsg marked this conversation as resolved.
Show resolved Hide resolved
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
andrewsg marked this conversation as resolved.
Show resolved Hide resolved
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