Passed
Push — master ( da4639...75155f )
by Dominik
14:07
created

_tree_to_overpassql_recursive()   F

Complexity

Conditions 54

Size

Total Lines 109

Duplication

Lines 24
Ratio 22.02 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
cc 54
c 8
b 0
f 0
dl 24
loc 109
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like _tree_to_overpassql_recursive() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
## ~*~ coding: utf-8 ~*~
2
#-
3
# OSMAlchemy - OpenStreetMap to SQLAlchemy bridge
4
# Copyright (c) 2016 Dominik George <[email protected]>
5
# Copyright (c) 2016 Eike Tim Jesinghaus <[email protected]>
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining a copy
8
# of this software and associated documentation files (the "Software"), to deal
9
# in the Software without restriction, including without limitation the rights
10
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
# copies of the Software, and to permit persons to whom the Software is
12
# furnished to do so, subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included in all
15
# copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
# SOFTWARE.
24
#
25
# Alternatively, you are free to use OSMAlchemy under Simplified BSD, The
26
# MirOS Licence, GPL-2+, LGPL-2.1+, AGPL-3+ or the same terms as Python
27
# itself.
28
29 1
""" Utility code for OSMAlchemy's overpass code. """
30
31 1
import operator
32 1
import overpass
33 1
from sqlalchemy.sql.elements import BinaryExpression, BooleanClauseList, BindParameter
34 1
from sqlalchemy.sql.annotation import AnnotatedColumn
0 ignored issues
show
Bug introduced by
The name AnnotatedColumn does not seem to exist in module sqlalchemy.sql.annotation.
Loading history...
35
36 1
def _generate_overpass_api(endpoint=None):
37
    """ Create and initialise the Overpass API object.
38
39
    Passing the endpoint argument will override the default
40
    endpoint URL.
41
    """
42
43
    # Create API object with default settings
44
    api = overpass.API()
45
46
    # Change endpoint if desired
47
    if endpoint is not None:
48
        api.endpoint = endpoint
49
50
    return api
51
52 1
def _get_single_element_by_id(api, type, id, recurse_down=True):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in type.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
Bug Best Practice introduced by
This seems to re-define the built-in id.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
Coding Style Naming introduced by
The name id does not conform to the argument naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
53
    """ Retrieves a single OpenStreetMap element by its id.
54
55
      api - an initialised Overpass API object
56
      type - the element type to query, one of node, way or relation
57
      id - the id of the element to retrieve
58
      recurse_down - whether to get child nodes of ways and relations
59
    """
60
61
    # Construct query
62
    q = "%s(%d);%s" % (type, id, "(._;>;);" if recurse_down else "")
0 ignored issues
show
Coding Style Naming introduced by
The name q does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
63
64
    # Run query
65
    r = api.Get(q, responseformat="xml")
0 ignored issues
show
Coding Style Naming introduced by
The name r does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
66
67
    # Return data
68
    return r
69
70 1
def _get_elements_by_query(api, query, recurse_down=True):
71
    """ Runs a query and returns the resulting OSM XML.
72
73
      api - an initialised Overpass API object
74
      query - the OverpassQL query
75
      recurse_down - whether to get child nodes of ways and relations
76
    """
77
78
    # Run query
79
    r = api.Get("%s%s" % (query, "(._;>;);" if recurse_down else ""), responseformat="xml")
0 ignored issues
show
Coding Style Naming introduced by
The name r does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
80
81
    # Return data
82
    return r
83
84
# Define operator to string mapping
85 1
_ops = {operator.eq: "==",
0 ignored issues
show
Coding Style Naming introduced by
The name _ops does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
86
        operator.ne: "!=",
87
        operator.lt: "<",
88
        operator.gt: ">",
89
        operator.le: "<=",
90
        operator.ge: ">=",
91
        operator.and_: "&&",
92
        operator.or_: "||"}
93
94 1
def _where_to_tree(clause, target):
0 ignored issues
show
best-practice introduced by
Too many return statements (8/6)
Loading history...
95
    """ Transform an SQLAlchemy whereclause to an expression tree.
96
97
    This function analyses a Query.whereclause object and turns it
98
    into a more general data structure.
99
    """
100
101
    if type(clause) is BinaryExpression:
0 ignored issues
show
introduced by
Using type() instead of isinstance() for a typecheck.
Loading history...
102
        # This is something like "latitude >= 51.0"
103
        left = clause.left
104
        right = clause.right
105
        op = clause.operator
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
106
107
        # Left part should be a column
108
        if type(left) is AnnotatedColumn:
0 ignored issues
show
introduced by
Using type() instead of isinstance() for a typecheck.
Loading history...
109
            # Get table class and field
110
            model = left._annotations["parentmapper"].class_
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _annotations was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
111
            field = left
112
113
            # Only use if we are looking for this model
114
            if model is target:
115
                # Store field name
116
                left = field.name
117
            else:
118
                return None
119
        else:
120
            # Right now, we cannot cope with anything but a column on the left
121
            return None
122
123
        # Right part should be a literal value
124
        if type(right) is BindParameter:
0 ignored issues
show
introduced by
Using type() instead of isinstance() for a typecheck.
Loading history...
125
            # Extract literal value
126
            right = right.value
127
        else:
128
            # Right now, we cannot cope with something else here
129
            return None
130
131
        # Look for a known operator
132
        if op in _ops.keys():
133
            # Get string representation
134
            op = _ops[op]
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
135
        else:
136
            # Right now, we cannot cope with other operators
137
            return None
138
139
        # Return polish notation tuple of this clause
140
        return (op, left, right)
141
    elif type(clause) is BooleanClauseList:
0 ignored issues
show
introduced by
Using type() instead of isinstance() for a typecheck.
Loading history...
142
        # This is an AND or OR operation
143
        op = clause.operator
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
144
        clauses = []
145
146
        # Iterate over all the clauses in this operation
147
        for clause in clause.clauses:
148
            # Recursively analyse clauses
149
            res = _where_to_tree(clause, target)
150
            # None is returned for unsupported clauses or operations
151
            if res is not None:
152
                # Append polish notation result to clauses list
153
                clauses.append(res)
154
155
        # Look for a known operator
156
        if op in _ops.keys():
157
            # Get string representation
158
            op = _ops[op]
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
159
        else:
160
            # Right now, we cannot cope with anything else
161
            return None
162
163
        # Return polish notation tuple of this clause
164
        return (op, clauses)
165
    else:
166
        # We hit an unsupported type of clause
167
        return None
168
169 1
def _trees_to_overpassql(tree_dict):
170
    """ Transform an expression tree (from _where_to_tree) into OverpassQL. """
171
172
    # Called recursively on all subtrees
173 1
    def _tree_to_overpassql_recursive(tree, type_, op):
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the argument naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
174
        # Empty result string
175 1
        result = ""
176
177
        # Test if we got a tree or an atom
178 1
        if isinstance(tree[1], list):
179
            # We are in a subtree
180
181
            # Store operation of subtree (conjunction/disjunction)
182 1 View Code Duplication
            op = tree[0]
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
183
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
184
            # Empty bounding box
185 1
            bbox = [None, None, None, None]
186
187
            # List of genrated set names
188 1
            set_names = []
189 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
190
            # Iterate over all elements in the conjunction/disjunction
191 1
            for t in tree[1]:
0 ignored issues
show
Coding Style Naming introduced by
The name t does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
192
                # Check if element is a tree or an atom
193 1
                if isinstance(t[1], list):
194
                    # Recurse into inner tree
195
                    result_inner_tree = _tree_to_overpassql_recursive(tree[1], op)
0 ignored issues
show
Bug introduced by
It seems like a value for argument op is missing in the function call.
Loading history...
196 View Code Duplication
                    # Store resulting query and its name
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
197
                    result += "%s" % result_inner_tree[1]
198
                    set_names.append(result_inner_tree[0])
199
                else:
200
                    # Parse atom
201
202
                    # latitude and longitude comparisons form a bounding box
203 1 View Code Duplication
                    if t[1] == "latitude" and t[0] == ">":
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
204
                        # South edge
205 1
                        if bbox[0] is None:
206 1
                            bbox[0] = float(t[2])
207
                        elif op == "&&" and bbox[0] <= t[2]:
208
                            bbox[0] = float(t[2])
209
                        elif op == "||" and bbox[0] >= t[2]:
210
                            bbox[0] = float(t[2])
211 1
                    elif t[1] == "latitude" and t[0] == "<":
212
                        # North edge
213 1
                        if bbox[2] is None:
214 1
                            bbox[2] = float(t[2])
215
                        elif op == "&&" and bbox[2] >= t[2]:
216
                            bbox[2] = float(t[2])
217
                        elif op == "||" and bbox[2] <= t[2]:
218
                            bbox[2] = float(t[2])
219 1
                    elif t[1] == "longitude" and t[0] == ">":
220
                        # West edge
221 1
                        if bbox[1] is None:
222 1
                            bbox[1] = float(t[2])
223
                        elif op == "&&" and bbox[1] <= t[2]:
224
                            bbox[1] = float(t[2])
225
                        elif op == "||" and bbox[1] >= t[2]:
226
                            bbox[1] = float(t[2])
227 1
                    elif t[1] == "longitude" and t[0] == "<":
228
                        # East edge
229 1
                        if bbox[3] is None:
230 1
                            bbox[3] = float(t[2])
231
                        elif op == "&&" and bbox[3] >= t[2]:
232
                            bbox[3] = float(t[2])
233
                        elif op == "||" and bbox[3] <= t[2]:
234
                            bbox[3] = float(t[2])
235
                    # Query for an element with specific id
236 1
                    elif t[1] == "id" and t[0] == "==":
237
                        # Build query
238
                        idquery = "%s(%i)" % (type_, t[2])
239
                        # Store resulting query and its name
240
                        set_name = "s%i" % id(idquery)
241
                        result += "%s->.%s;" % (idquery, set_name)
242
                        set_names.append(set_name)
243 1
                    elif t[1] == "id":
244
                        # We got an id query, but not with equality comparison
245
                        raise ValueError("id can only be queried with equality")
246
                    # Everything else must be a tag query
247
                    else:
248
                        # Build query
249 1
                        tagquery = "%s[\"%s\"=\"%s\"]" % (type_, t[1], t[2])
250
                        # Store resulting query and its name
251 1
                        set_name = "s%i" % id(tagquery)
252 1
                        result += "%s->.%s;" % (tagquery, set_name)
253 1
                        set_names.append(set_name)
254
255
            # Check if any component of the bounding box was set
256 1
            if bbox != [None, None, None, None]:
257
                # Amend minima/maxima
258 1
                if bbox[0] is None:
259 1
                    bbox[0] = -90.0
260 1
                if bbox[1] is None:
261 1
                    bbox[1] = -180.0
262 1
                if bbox[2] is None:
263 1
                    bbox[2] = 90.0
264 1
                if bbox[3] is None:
265 1
                    bbox[3] = 180.0
266
267
                # Build query
268 1
                bboxquery = "%s(%s,%s,%s,%s)" % (type_, bbox[0], bbox[1], bbox[2], bbox[3])
269
                # Store resulting query and its name
270 1
                set_name = "s%i" % id(bboxquery)
271 1
                result += "%s->.%s;" % (bboxquery, set_name)
272 1
                set_names.append(set_name)
273
274
            # Build conjunction or disjunction according to current operation
275 1
            if op == "&&":
276
                # Conjunction, build an intersection
277 1
                result += "%s." % type_
278 1
                result += ".".join(set_names)
279 1
            elif op == "||":
280
                # Disjunction, build a union
281 1
                result += "("
282 1
                for s in set_names:
0 ignored issues
show
Coding Style Naming introduced by
The name s does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
283 1
                    result += ".%s;" % s
284 1
                result += ")"
285
        else:
286
            # We got a bare atom
287
288
            # latitude and longitude are components of a bounding box query
289 1
            if tree[1] == "latitude" and tree[0] == ">":
290
                # South edge
291
                result = "%s(%s,-180.0,90.0,180.0)" % (type_, tree[2])
292 1
            elif tree[1] == "latitude" and tree[0] == "<":
293
                # West edge
294
                result = "%s(-90.0,-180.0,%s,180.0)" % (type_, tree[2])
295 1
            elif tree[1] == "longitude" and tree[0] == ">":
296
                # North edge
297
                result = "%s(-90.0,%s,-90.0,180.0)" % (type_, tree[2])
298 1
            elif tree[1] == "longitude" and tree[0] == "<":
299
                # East edge
300
                result = "%s(-90.0,-180.0,-90.0,%s)" % (type_, tree[2])
301
            # Query for an id
302 1
            elif tree[1] == "id" and tree[0] == "==":
303 1
                result = "%s(%i)" % (type_, tree[2])
304 1
            elif tree[1] == "id":
305
                # We got an id query, but not with equality comparison
306 1
                raise ValueError("id can only be queried with equality")
307
            # Everything else must be a tag query
308
            else:
309
                result = "%s[\"%s\"=\"%s\"]" % (type_, tree[1], tree[2])
310
311
        # generate a name for the complete set and return it, along with the query
312 1
        set_name = id(result)
313 1
        result += "->.s%i;" % set_name
314 1
        return (set_name, result)
315
316
    # Run tree transformation for each type in the input tree
317 1
    results = []
318 1
    for type_ in tree_dict.keys():
319
        # Get real type name (OSMNode→node,…)
320 1
        real_type = type_[3:].lower()
321
        # Do transformation and store query and name
322 1
        results.append(_tree_to_overpassql_recursive(tree_dict[type_], real_type, None))
323
324
    # Build finally resulting query in a union
325 1
    result = ""
326 1
    set_names = "("
327 1
    for r in results:
0 ignored issues
show
Coding Style Naming introduced by
The name r does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
328 1
        result += r[1]
329 1
        set_names += ".s%s; " % r[0]
330 1
    set_names = "%s);" % set_names.strip()
331 1
    result += set_names
332
333
    # Return final query
334
    return result
335