Skip to content

Commit 7f07f1f

Browse files
authored
[7.x] Surface deprecation warnings from Elasticsearch
1 parent 606287f commit 7f07f1f

File tree

9 files changed

+194
-30
lines changed

9 files changed

+194
-30
lines changed

elasticsearch/__init__.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,7 @@
66
__versionstr__ = ".".join(map(str, VERSION))
77

88
import logging
9-
10-
try: # Python 2.7+
11-
from logging import NullHandler
12-
except ImportError:
13-
14-
class NullHandler(logging.Handler):
15-
def emit(self, record):
16-
pass
17-
18-
19-
import sys
9+
import warnings
2010

2111
logger = logging.getLogger("elasticsearch")
2212
logger.addHandler(logging.NullHandler())
@@ -26,4 +16,47 @@ def emit(self, record):
2616
from .connection_pool import ConnectionPool, ConnectionSelector, RoundRobinSelector
2717
from .serializer import JSONSerializer
2818
from .connection import Connection, RequestsHttpConnection, Urllib3HttpConnection
29-
from .exceptions import *
19+
from .exceptions import (
20+
ImproperlyConfigured,
21+
ElasticsearchException,
22+
SerializationError,
23+
TransportError,
24+
NotFoundError,
25+
ConflictError,
26+
RequestError,
27+
ConnectionError,
28+
SSLError,
29+
ConnectionTimeout,
30+
AuthenticationException,
31+
AuthorizationException,
32+
ElasticsearchDeprecationWarning,
33+
)
34+
35+
# Only raise one warning per deprecation message so as not
36+
# to spam up the user if the same action is done multiple times.
37+
warnings.simplefilter("default", category=ElasticsearchDeprecationWarning, append=True)
38+
39+
__all__ = [
40+
"Elasticsearch",
41+
"Transport",
42+
"ConnectionPool",
43+
"ConnectionSelector",
44+
"RoundRobinSelector",
45+
"JSONSerializer",
46+
"Connection",
47+
"RequestsHttpConnection",
48+
"Urllib3HttpConnection",
49+
"ImproperlyConfigured",
50+
"ElasticsearchException",
51+
"SerializationError",
52+
"TransportError",
53+
"NotFoundError",
54+
"ConflictError",
55+
"RequestError",
56+
"ConnectionError",
57+
"SSLError",
58+
"ConnectionTimeout",
59+
"AuthenticationException",
60+
"AuthorizationException",
61+
"ElasticsearchDeprecationWarning",
62+
]

elasticsearch/connection/base.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22
import binascii
33
import gzip
44
import io
5+
import re
56
from platform import python_version
7+
import warnings
68

79
try:
810
import simplejson as json
911
except ImportError:
1012
import json
1113

12-
from ..exceptions import TransportError, ImproperlyConfigured, HTTP_EXCEPTIONS
14+
from ..exceptions import (
15+
TransportError,
16+
ImproperlyConfigured,
17+
ElasticsearchDeprecationWarning,
18+
HTTP_EXCEPTIONS,
19+
)
1320
from .. import __versionstr__
1421

1522
logger = logging.getLogger("elasticsearch")
@@ -21,6 +28,8 @@
2128
if not _tracer_already_configured:
2229
tracer.propagate = False
2330

31+
_WARNING_RE = re.compile(r"\"([^\"]*)\"")
32+
2433

2534
class Connection(object):
2635
"""
@@ -132,6 +141,35 @@ def _gzip_compress(self, body):
132141
f.write(body)
133142
return buf.getvalue()
134143

144+
def _raise_warnings(self, warning_headers):
145+
"""If 'headers' contains a 'Warning' header raise
146+
the warnings to be seen by the user. Takes an iterable
147+
of string values from any number of 'Warning' headers.
148+
"""
149+
if not warning_headers:
150+
return
151+
152+
# Grab only the message from each header, the rest is discarded.
153+
# Format is: '(number) Elasticsearch-(version)-(instance) "(message)"'
154+
warning_messages = []
155+
for header in warning_headers:
156+
# Because 'Requests' does it's own folding of multiple HTTP headers
157+
# into one header delimited by commas (totally standard compliant, just
158+
# annoying for cases like this) we need to expect there may be
159+
# more than one message per 'Warning' header.
160+
matches = _WARNING_RE.findall(header)
161+
if matches:
162+
warning_messages.extend(matches)
163+
else:
164+
# Don't want to throw away any warnings, even if they
165+
# don't follow the format we have now. Use the whole header.
166+
warning_messages.append(header)
167+
168+
for message in warning_messages:
169+
warnings.warn(
170+
message, category=ElasticsearchDeprecationWarning, stacklevel=6
171+
)
172+
135173
def _pretty_json(self, data):
136174
# pretty JSON in tracer curl logs
137175
try:

elasticsearch/connection/http_requests.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ def perform_request(
156156
raise ConnectionTimeout("TIMEOUT", str(e), e)
157157
raise ConnectionError("N/A", str(e), e)
158158

159+
# raise warnings if any from the 'Warnings' header.
160+
warnings_headers = (
161+
(response.headers["warning"],) if "warning" in response.headers else ()
162+
)
163+
self._raise_warnings(warnings_headers)
164+
159165
# raise errors based on http status codes, let the client handle those if needed
160166
if (
161167
not (200 <= response.status_code < 300)

elasticsearch/connection/http_urllib3.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ def perform_request(
240240
raise ConnectionTimeout("TIMEOUT", str(e), e)
241241
raise ConnectionError("N/A", str(e), e)
242242

243+
# raise warnings if any from the 'Warnings' header.
244+
warning_headers = response.headers.get_all("warning", ())
245+
self._raise_warnings(warning_headers)
246+
243247
# raise errors based on http status codes, let the client handle those if needed
244248
if not (200 <= response.status < 300) and response.status not in ignore:
245249
self.log_request_fail(

elasticsearch/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ class AuthorizationException(TransportError):
136136
""" Exception representing a 403 status code. """
137137

138138

139+
class ElasticsearchDeprecationWarning(Warning):
140+
""" Warning that is raised when a deprecated option
141+
is flagged via the 'Warning' HTTP header.
142+
"""
143+
144+
139145
# more generic mappings from status_code to python exceptions
140146
HTTP_EXCEPTIONS = {
141147
400: RequestError,

elasticsearch/helpers/test.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,15 @@ def setUpClass(cls):
5151

5252
def tearDown(self):
5353
super(ElasticsearchTestCase, self).tearDown()
54-
self.client.indices.delete(index="*", ignore=404)
54+
# Hidden indices expanded in wildcards in ES 7.7
55+
expand_wildcards = ["open", "closed"]
56+
if self.es_version >= (7, 7):
57+
expand_wildcards.append("hidden")
58+
59+
self.client.indices.delete(
60+
index="*", ignore=404, expand_wildcards=expand_wildcards
61+
)
5562
self.client.indices.delete_template(name="*", ignore=404)
56-
self.client.indices.delete_alias(index="_all", name="_all", ignore=404)
5763

5864
@property
5965
def es_version(self):

test_elasticsearch/test_connection.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io
55
from mock import Mock, patch
66
import urllib3
7+
from urllib3._collections import HTTPHeaderDict
78
import warnings
89
from requests.auth import AuthBase
910
from platform import python_version
@@ -87,14 +88,56 @@ def test_parse_cloud_id(self):
8788
"8af7ee35420f458e903026b4064081f2.westeurope.azure.elastic-cloud.com",
8889
)
8990

91+
def test_empty_warnings(self):
92+
con = Connection()
93+
with warnings.catch_warnings(record=True) as w:
94+
con._raise_warnings(())
95+
con._raise_warnings([])
96+
97+
self.assertEquals(w, [])
98+
99+
def test_raises_warnings(self):
100+
con = Connection()
101+
102+
with warnings.catch_warnings(record=True) as warn:
103+
con._raise_warnings(['299 Elasticsearch-7.6.1-aa751 "this is deprecated"'])
104+
105+
self.assertEquals([str(w.message) for w in warn], ["this is deprecated"])
106+
107+
with warnings.catch_warnings(record=True) as warn:
108+
con._raise_warnings(
109+
[
110+
'299 Elasticsearch-7.6.1-aa751 "this is also deprecated"',
111+
'299 Elasticsearch-7.6.1-aa751 "this is also deprecated"',
112+
'299 Elasticsearch-7.6.1-aa751 "guess what? deprecated"',
113+
]
114+
)
115+
116+
self.assertEquals(
117+
[str(w.message) for w in warn],
118+
["this is also deprecated", "guess what? deprecated"],
119+
)
120+
121+
def test_raises_warnings_when_folded(self):
122+
con = Connection()
123+
with warnings.catch_warnings(record=True) as warn:
124+
con._raise_warnings(
125+
[
126+
'299 Elasticsearch-7.6.1-aa751 "warning",'
127+
'299 Elasticsearch-7.6.1-aa751 "folded"',
128+
]
129+
)
130+
131+
self.assertEquals([str(w.message) for w in warn], ["warning", "folded"])
132+
90133

91134
class TestUrllib3Connection(TestCase):
92135
def _get_mock_connection(self, connection_params={}, response_body=b"{}"):
93136
con = Urllib3HttpConnection(**connection_params)
94137

95138
def _dummy_urlopen(*args, **kwargs):
96139
dummy_response = Mock()
97-
dummy_response.headers = {}
140+
dummy_response.headers = HTTPHeaderDict({})
98141
dummy_response.status = 200
99142
dummy_response.data = response_body
100143
_dummy_urlopen.call_args = (args, kwargs)

test_elasticsearch/test_server/test_common.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from os.path import exists, join, dirname, pardir
99
import yaml
1010
from shutil import rmtree
11+
import warnings
1112

12-
from elasticsearch import TransportError, RequestError
13+
from elasticsearch import TransportError, RequestError, ElasticsearchDeprecationWarning
1314
from elasticsearch.compat import string_types
1415
from elasticsearch.helpers.test import _get_version
1516

@@ -30,6 +31,7 @@
3031
"headers",
3132
"catch_unauthorized",
3233
"default_shards",
34+
"warnings",
3335
}
3436

3537
# broken YAML tests on some releases
@@ -40,6 +42,8 @@
4042
# Disallowing expensive queries is 7.7+
4143
"TestSearch320DisallowQueries",
4244
"TestIndicesPutIndexTemplate10Basic",
45+
"TestIndicesGetIndexTemplate10Basic",
46+
"TestIndicesGetIndexTemplate20GetMissing",
4347
}
4448
}
4549

@@ -150,6 +154,7 @@ def _lookup(self, path):
150154

151155
def run_code(self, test):
152156
""" Execute an instruction based on it's type. """
157+
print(test)
153158
for action in test:
154159
self.assertEquals(1, len(action))
155160
action_type, action = list(action.items())[0]
@@ -164,6 +169,7 @@ def run_do(self, action):
164169
api = self.client
165170
headers = action.pop("headers", None)
166171
catch = action.pop("catch", None)
172+
warn = action.pop("warnings", None)
167173
self.assertEquals(1, len(action))
168174

169175
method, args = list(action.items())[0]
@@ -184,17 +190,34 @@ def run_do(self, action):
184190
for k in args:
185191
args[k] = self._resolve(args[k])
186192

187-
try:
188-
self.last_response = api(**args)
189-
except Exception as e:
190-
if not catch:
191-
raise
192-
self.run_catch(catch, e)
193-
else:
194-
if catch:
195-
raise AssertionError(
196-
"Failed to catch %r in %r." % (catch, self.last_response)
197-
)
193+
warnings.simplefilter("always", category=ElasticsearchDeprecationWarning)
194+
with warnings.catch_warnings(record=True) as caught_warnings:
195+
try:
196+
self.last_response = api(**args)
197+
except Exception as e:
198+
if not catch:
199+
raise
200+
self.run_catch(catch, e)
201+
else:
202+
if catch:
203+
raise AssertionError(
204+
"Failed to catch %r in %r." % (catch, self.last_response)
205+
)
206+
207+
# Filter out warnings raised by other components.
208+
caught_warnings = [
209+
str(w.message)
210+
for w in caught_warnings
211+
if w.category == ElasticsearchDeprecationWarning
212+
]
213+
214+
# Sorting removes the issue with order raised. We only care about
215+
# if all warnings are raised in the single API call.
216+
if warn is not None and sorted(warn) != sorted(caught_warnings):
217+
raise AssertionError(
218+
"Expected warnings not equal to actual warnings: expected=%r actual=%r"
219+
% (warn, caught_warnings)
220+
)
198221

199222
def _get_nodes(self):
200223
if not hasattr(self, "_node_info"):

tox.ini

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@ setenv =
77
commands =
88
python setup.py test
99

10-
[testenv:lint]
10+
[testenv:blacken]
1111
deps =
12-
flake8
1312
black
1413
commands =
1514
black --target-version=py27 \
1615
elasticsearch/ \
1716
test_elasticsearch/ \
1817
setup.py
18+
19+
[testenv:lint]
20+
deps =
21+
flake8
22+
black
23+
commands =
1924
black --target-version=py27 --check \
2025
elasticsearch/ \
2126
test_elasticsearch/ \

0 commit comments

Comments
 (0)