Skip to content

Commit a11e8b7

Browse files
authored
Add Python 3.10 support and bump version number (#41)
1 parent 9c20e99 commit a11e8b7

File tree

16 files changed

+132
-126
lines changed

16 files changed

+132
-126
lines changed

.github/workflows/unit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
strategy:
1515
matrix:
16-
python: [3.7, 3.8, 3.9]
16+
python: [3.7, 3.8, 3.9, '3.10']
1717
steps:
1818
- name: Checkout
1919
uses: actions/checkout@v2

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ We are working to support more App Engine bundled service APIs for Python 3. To
1313

1414
In your `requirements.txt` file, add the following:
1515

16-
`appengine-python-standard>=0.2.4`
16+
`appengine-python-standard>=0.3.0`
1717

1818
In your app's `app.yaml`, add the following:
1919

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="appengine-python-standard",
8-
version="0.2.4",
8+
version="0.3.0",
99
author="Google LLC",
1010
description="Google App Engine services SDK for Python 3",
1111
long_description=long_description,
@@ -23,7 +23,7 @@
2323
"protobuf>=3.19.0",
2424
"pytz>=2021.1",
2525
"requests>=2.25.1",
26-
"ruamel.yaml>=0.15,<0.16",
26+
"ruamel.yaml>=0.17.7",
2727
"six>=1.15.0",
2828
"urllib3>=1.26.2,<2",
2929
],

src/google/appengine/api/blobstore/blobstore_stub.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -192,23 +192,6 @@ def storage(self):
192192
"""
193193
return self.__storage
194194

195-
def _GetEnviron(self, name):
196-
"""Helper method ensures environment configured as expected.
197-
198-
Args:
199-
name: Name of environment variable to get.
200-
201-
Returns:
202-
Environment variable associated with name.
203-
204-
Raises:
205-
ConfigurationError if required environment variable is not found.
206-
"""
207-
try:
208-
return os.environ[name]
209-
except KeyError:
210-
raise ConfigurationError('%s is not set in environment.' % name)
211-
212195
def _CreateSession(self,
213196
success_path,
214197
user,

src/google/appengine/api/oauth/oauth_api.py

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- `OAuthServiceFailureError`: OAuthService exception
3131
"""
3232

33+
import contextvars
3334
import json
3435
import os
3536
import six
@@ -50,6 +51,18 @@
5051

5152

5253

54+
_OAUTH_AUTH_DOMAIN = contextvars.ContextVar('OAUTH_AUTH_DOMAIN')
55+
_OAUTH_EMAIL = contextvars.ContextVar('OAUTH_EMAIL')
56+
_OAUTH_USER_ID = contextvars.ContextVar('OAUTH_USER_ID')
57+
_OAUTH_CLIENT_ID = contextvars.ContextVar('OAUTH_CLIENT_ID')
58+
_OAUTH_IS_ADMIN = contextvars.ContextVar('OAUTH_IS_ADMIN')
59+
_OAUTH_ERROR_CODE = contextvars.ContextVar('OAUTH_ERROR_CODE')
60+
_OAUTH_ERROR_DETAIL = contextvars.ContextVar('OAUTH_ERROR_DETAIL')
61+
_OAUTH_LAST_SCOPE = contextvars.ContextVar('OAUTH_LAST_SCOPE')
62+
_OAUTH_AUTHORIZED_SCOPES = contextvars.ContextVar('OAUTH_AUTHORIZED_SCOPES')
63+
64+
_TESTBED_RESET_TOKENS = dict()
65+
5366

5467
class Error(Exception):
5568
"""Base error class for this module."""
@@ -117,7 +130,7 @@ def is_current_user_admin(_scope=None):
117130
"""
118131

119132
_maybe_call_get_oauth_user(_scope)
120-
return os.environ.get('OAUTH_IS_ADMIN', '0') == '1'
133+
return _OAUTH_IS_ADMIN.get(None)
121134

122135

123136
def get_oauth_consumer_key():
@@ -165,14 +178,14 @@ def get_authorized_scopes(scope):
165178

166179

167180
def _maybe_call_get_oauth_user(scope):
168-
"""Makes an GetOAuthUser RPC and stores the results in os.environ.
181+
"""Makes an GetOAuthUser RPC and stores the results in context.
169182
170183
This method will only make the RPC if 'OAUTH_ERROR_CODE' has not already
171184
been set or 'OAUTH_LAST_SCOPE' is different to str(_scopes).
172185
173186
Args:
174-
scope: The custom OAuth scope or an iterable of scopes at least one of
175-
which is accepted.
187+
scope: The custom OAuth scope or an iterable of scopes at least one of which
188+
is accepted.
176189
"""
177190

178191
if not scope:
@@ -181,8 +194,8 @@ def _maybe_call_get_oauth_user(scope):
181194
scope_str = scope
182195
else:
183196
scope_str = str(sorted(scope))
184-
if ('OAUTH_ERROR_CODE' not in os.environ or
185-
os.environ.get('OAUTH_LAST_SCOPE', None) != scope_str or
197+
if (_OAUTH_ERROR_CODE.get(None) is None or
198+
_OAUTH_LAST_SCOPE.get(None) != scope_str or
186199
os.environ.get('TESTONLY_OAUTH_SKIP_CACHE')):
187200
req = user_service_pb2.GetOAuthUserRequest()
188201
if scope:
@@ -194,35 +207,39 @@ def _maybe_call_get_oauth_user(scope):
194207
resp = user_service_pb2.GetOAuthUserResponse()
195208
try:
196209
apiproxy_stub_map.MakeSyncCall('user', 'GetOAuthUser', req, resp)
197-
os.environ['OAUTH_EMAIL'] = resp.email
198-
os.environ['OAUTH_AUTH_DOMAIN'] = resp.auth_domain
199-
os.environ['OAUTH_USER_ID'] = resp.user_id
200-
os.environ['OAUTH_CLIENT_ID'] = resp.client_id
201-
202-
os.environ['OAUTH_AUTHORIZED_SCOPES'] = json.dumps(list(resp.scopes))
203-
if resp.is_admin:
204-
os.environ['OAUTH_IS_ADMIN'] = '1'
205-
else:
206-
os.environ['OAUTH_IS_ADMIN'] = '0'
207-
os.environ['OAUTH_ERROR_CODE'] = ''
210+
token = _OAUTH_EMAIL.set(resp.email)
211+
_TESTBED_RESET_TOKENS[_OAUTH_EMAIL] = token
212+
token = _OAUTH_AUTH_DOMAIN.set(resp.auth_domain)
213+
_TESTBED_RESET_TOKENS[_OAUTH_AUTH_DOMAIN] = token
214+
token = _OAUTH_USER_ID.set(resp.user_id)
215+
_TESTBED_RESET_TOKENS[_OAUTH_USER_ID] = token
216+
token = _OAUTH_CLIENT_ID.set(resp.client_id)
217+
_TESTBED_RESET_TOKENS[_OAUTH_CLIENT_ID] = token
218+
token = _OAUTH_AUTHORIZED_SCOPES.set(json.dumps(list(resp.scopes)))
219+
_TESTBED_RESET_TOKENS[_OAUTH_AUTHORIZED_SCOPES] = token
220+
token = _OAUTH_IS_ADMIN.set(resp.is_admin)
221+
_TESTBED_RESET_TOKENS[_OAUTH_IS_ADMIN] = token
222+
token = _OAUTH_ERROR_CODE.set('')
223+
_TESTBED_RESET_TOKENS[_OAUTH_ERROR_CODE] = token
208224
except apiproxy_errors.ApplicationError as e:
209-
os.environ['OAUTH_ERROR_CODE'] = str(e.application_error)
210-
os.environ['OAUTH_ERROR_DETAIL'] = e.error_detail
211-
os.environ['OAUTH_LAST_SCOPE'] = scope_str
225+
token = _OAUTH_ERROR_CODE.set(str(e.application_error))
226+
_TESTBED_RESET_TOKENS[_OAUTH_ERROR_CODE] = token
227+
token = _OAUTH_ERROR_DETAIL.set(e.error_detail)
228+
_TESTBED_RESET_TOKENS[_OAUTH_ERROR_DETAIL] = token
229+
token = _OAUTH_LAST_SCOPE.set(scope_str)
230+
_TESTBED_RESET_TOKENS[_OAUTH_LAST_SCOPE] = token
212231
_maybe_raise_exception()
213232

214233

215234
def _maybe_raise_exception():
216-
"""Raises an error if one has been stored in os.environ.
235+
"""Raises an error if one has been stored in context.
217236
218237
This method requires that 'OAUTH_ERROR_CODE' has already been set (an empty
219238
string indicates that there is no actual error).
220239
"""
221-
assert 'OAUTH_ERROR_CODE' in os.environ
222-
error = os.environ['OAUTH_ERROR_CODE']
240+
error = _OAUTH_ERROR_CODE.get()
223241
if error:
224-
assert 'OAUTH_ERROR_DETAIL' in os.environ
225-
error_detail = os.environ['OAUTH_ERROR_DETAIL']
242+
error_detail = _OAUTH_ERROR_DETAIL.get()
226243
if error == str(user_service_pb2.UserServiceError.NOT_ALLOWED):
227244
raise NotAllowedError(error_detail)
228245
elif error == str(user_service_pb2.UserServiceError.OAUTH_INVALID_REQUEST):
@@ -236,42 +253,38 @@ def _maybe_raise_exception():
236253

237254

238255
def _get_user_from_environ():
239-
"""Returns a User based on values stored in os.environ.
256+
"""Returns a User based on values stored in context.
240257
241258
This method requires that 'OAUTH_EMAIL', 'OAUTH_AUTH_DOMAIN', and
242259
'OAUTH_USER_ID' have already been set.
243260
244261
Returns:
245262
User
246263
"""
247-
assert 'OAUTH_EMAIL' in os.environ
248-
assert 'OAUTH_AUTH_DOMAIN' in os.environ
249-
assert 'OAUTH_USER_ID' in os.environ
250-
return users.User(email=os.environ['OAUTH_EMAIL'],
251-
_auth_domain=os.environ['OAUTH_AUTH_DOMAIN'],
252-
_user_id=os.environ['OAUTH_USER_ID'])
264+
return users.User(
265+
email=_OAUTH_EMAIL.get(),
266+
_auth_domain=_OAUTH_AUTH_DOMAIN.get(),
267+
_user_id=_OAUTH_USER_ID.get())
253268

254269

255270
def _get_client_id_from_environ():
256-
"""Returns Client ID based on values stored in os.environ.
271+
"""Returns Client ID based on values stored in context.
257272
258273
This method requires that 'OAUTH_CLIENT_ID' has already been set.
259274
260275
Returns:
261276
string: the value of Client ID.
262277
"""
263-
assert 'OAUTH_CLIENT_ID' in os.environ
264-
return os.environ['OAUTH_CLIENT_ID']
278+
return _OAUTH_CLIENT_ID.get()
265279

266280

267281
def _get_authorized_scopes_from_environ():
268-
"""Returns authorized scopes based on values stored in os.environ.
282+
"""Returns authorized scopes based on values stored in context.
269283
270284
This method requires that 'OAUTH_AUTHORIZED_SCOPES' has already been set.
271285
272286
Returns:
273287
list: the list of OAuth scopes.
274288
"""
275-
assert 'OAUTH_AUTHORIZED_SCOPES' in os.environ
276289

277-
return json.loads(os.environ['OAUTH_AUTHORIZED_SCOPES'])
290+
return json.loads(_OAUTH_AUTHORIZED_SCOPES.get())

src/google/appengine/api/request_info.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929

3030
import collections
3131
import logging
32-
import os
3332

33+
from google.appengine.runtime import context
3434
from six.moves import urllib
3535

3636

@@ -636,23 +636,18 @@ def get_request_url(self, request_id):
636636
Returns:
637637
The URL of the request as a string.
638638
"""
639-
try:
640-
host = os.environ['HTTP_HOST']
641-
except KeyError:
642-
host = os.environ['SERVER_NAME']
643-
port = os.environ['SERVER_PORT']
639+
host = context.get('HTTP_HOST')
640+
if not host:
641+
host = context.get('SERVER_NAME')
642+
port = context.get('SERVER_PORT')
644643
if port != '80':
645644
host += ':' + port
646645
url = 'http://' + host
647-
url += urllib.parse.quote(os.environ.get('PATH_INFO', '/'))
648-
if os.environ.get('QUERY_STRING'):
649-
url += '?' + os.environ['QUERY_STRING']
646+
url += urllib.parse.quote(context.get('PATH_INFO', '/'))
647+
if context.get('QUERY_STRING'):
648+
url += '?' + context.get('QUERY_STRING')
650649
return url
651650

652-
def get_request_environ(self, request_id):
653-
"""Returns a dict containing the WSGI environ for the request."""
654-
return os.environ
655-
656651
def get_module(self, request_id):
657652
"""Returns the name of the module serving this request.
658653

src/google/appengine/api/urlfetch.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"""URL downloading API."""
2020

2121
import email
22-
import os
2322
import threading
2423

2524
import six
@@ -32,6 +31,7 @@
3231
from google.appengine.api import urlfetch_service_pb2
3332
from google.appengine.api.urlfetch_errors import *
3433
from google.appengine.runtime import apiproxy_errors
34+
from google.appengine.runtime import context
3535

3636

3737

@@ -195,16 +195,15 @@ def _is_fetching_self(url, method):
195195
Boolean indicating whether or not it seems that the app is trying to fetch
196196
itself.
197197
"""
198-
if (method != GET or
199-
"HTTP_HOST" not in os.environ or
200-
"PATH_INFO" not in os.environ):
198+
if (method != GET or context.get('HTTP_HOST', None) is None or
199+
context.get('PATH_INFO', None) is None):
201200
return False
202201

203202
_, host_port, path, _, _ = six.moves.urllib.parse.urlsplit(url)
204203

205-
if host_port == os.environ['HTTP_HOST']:
204+
if host_port == context.get('HTTP_HOST'):
206205

207-
current_path = urllib.parse.unquote(os.environ['PATH_INFO'])
206+
current_path = urllib.parse.unquote(context.get('PATH_INFO'))
208207
desired_path = urllib.parse.unquote(path)
209208

210209
if (current_path == desired_path or

src/google/appengine/api/user_service_stub.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@
2424

2525

2626
import logging
27-
import os
2827
from six.moves import urllib
2928
import six.moves.urllib.parse
3029
from google.appengine.api import apiproxy_stub
30+
from google.appengine.api.oauth import oauth_api
3131
from google.appengine.api import user_service_pb2
3232
from google.appengine.runtime import apiproxy_errors
33+
from google.appengine.runtime.context import ctx_test_util
3334

3435
_DEFAULT_LOGIN_URL = 'https://www.google.com/accounts/Login?continue=%s'
3536
_DEFAULT_LOGOUT_URL = 'https://www.google.com/accounts/Logout?continue=%s'
@@ -84,12 +85,12 @@ def __init__(self,
8485
self._logout_url = logout_url
8586
self.__scopes = None
8687

87-
self.SetOAuthUser(is_admin=(os.environ.get('OAUTH_IS_ADMIN', '0') == '1'))
88+
self.SetOAuthUser(is_admin=oauth_api._OAUTH_IS_ADMIN.get(False))
8889

8990

9091

9192

92-
os.environ['AUTH_DOMAIN'] = auth_domain
93+
ctx_test_util.set_both('AUTH_DOMAIN', auth_domain)
9394

9495
def SetOAuthUser(self,
9596
email=_OAUTH_EMAIL,

src/google/appengine/api/users.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from google.appengine.api import apiproxy_stub_map
3434
from google.appengine.api import user_service_pb2
3535
from google.appengine.runtime import apiproxy_errors
36+
from google.appengine.runtime import context
3637

3738

3839

@@ -102,12 +103,12 @@ def __init__(self, email=None, _auth_domain=None,
102103

103104

104105
if _auth_domain is None:
105-
_auth_domain = os.environ.get('AUTH_DOMAIN')
106+
_auth_domain = context.get('AUTH_DOMAIN')
106107
assert _auth_domain
107108

108109
if email is None and federated_identity is None:
109-
email = os.environ.get('USER_EMAIL', email)
110-
_user_id = os.environ.get('USER_ID', _user_id)
110+
email = context.get('USER_EMAIL', email)
111+
_user_id = context.get('USER_ID', _user_id)
111112
federated_identity = os.environ.get('FEDERATED_IDENTITY',
112113
federated_identity)
113114
federated_provider = os.environ.get('FEDERATED_PROVIDER',
@@ -345,7 +346,7 @@ def is_current_user_admin():
345346
Returns:
346347
`True` if the user is an administrator; all other user types return `False`.
347348
"""
348-
return (os.environ.get('USER_IS_ADMIN', '0')) == '1'
349+
return (context.get('USER_IS_ADMIN', '0')) == '1'
349350

350351

351352
IsCurrentUserAdmin = is_current_user_admin

src/google/appengine/datastore/datastore_index_xml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def ProcessIndexNode(self, node):
136136
self.errors.append(
137137
'Value for ancestor should be true or false, not "%s"' % ancestor)
138138
properties = []
139-
property_nodes = [n for n in node.getchildren() if n.tag == 'property']
139+
property_nodes = [n for n in node if n.tag == 'property']
140140

141141

142142
has_geospatial = any(

0 commit comments

Comments
 (0)