Completed
Push — develop ( b7d53e...9f91db )
by A
01:51 queued 28s
created

PortRange.parse()   F

Complexity

Conditions 16

Size

Total Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 16
c 3
b 0
f 0
dl 0
loc 58
rs 3.2279

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 PortRange.parse() 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
# Copyright (c) 2014-2016 Scaleway and Contributors. All Rights Reserved.
4
#                         Kevin Deldycke <[email protected]>
5
#                         Gilles Dartiguelongue <[email protected]>
6
#
7
# Licensed under the BSD 2-Clause License (the "License"); you may not use this
8
# file except in compliance with the License. You may obtain a copy of the
9
# License at https://opensource.org/licenses/BSD-2-Clause
10
11
""" Port range utilities and helpers.
12
"""
13
14
from __future__ import (absolute_import, division, print_function,
15
                        unicode_literals)
16
17
import math
18
import sys
19
from collections import Iterable
20
21
22
try:
23
    from itertools import imap as iter_map
24
except ImportError:  # pragma: no cover
25
    iter_map = map
26
27
PY3 = sys.version_info == 3
28
if PY3:
29
    basestring = (str, bytes)
30
31
__version__ = '1.1.0'
32
33
34
class PortRange(object):
35
36
    """ Port range with support of a CIDR-like (binary) notation.
37
38
    In strict mode (disabled by default) we'll enforce the following rules:
39
        * port base must be a power of two (offsets not allowed);
40
        * port range must be within the 1-65535 inclusive range.
41
42
    This mode can be disabled on object creation.
43
    """
44
45
    # Separators constants for CIDR and range notation.
46
    CIDR_SEP = '/'
47
    RANGE_SEP = '-'
48
49
    # Max port lenght, in bits.
50
    port_lenght = 16
51
    # Max port range integer values
52
    port_min = 1
53
    port_max = (2 ** port_lenght) - 1
54
55
    # Base values on which all other properties are computed.
56
    port_from = None
57
    port_to = None
58
59
    def __init__(self, port_range, strict=False):
60
        """ Set up class with a port_from and port_to integer. """
61
        self.strict = strict
62
        self.port_from, self.port_to = self.parse(port_range)
63
64
    def parse(self, port_range):
65
        """ Parse and normalize a string or iterable into a port range. """
66
        # Any string containing a CIDR separator is parsed as a CIDR-like
67
        # notation, others as a range or single port.
68
        cidr_notation = False
69
        if isinstance(port_range, basestring):
70
            cidr_notation = self.CIDR_SEP in port_range
71
            separator = self.CIDR_SEP if cidr_notation else self.RANGE_SEP
72
            port_range = port_range.split(separator, 1)
73
74
        # We expect here a list of elements castable to integers.
75
        if not isinstance(port_range, Iterable):
76
            port_range = [port_range]
77
        try:
78
            port_range = list(iter_map(int, port_range))
79
        except TypeError:
80
            raise ValueError("Can't parse range as a list of integers.")
81
82
        # At this point we should have a list of one or two integers.
83
        if not 0 < len(port_range) < 3:
84
            raise ValueError("Expecting a list of one or two elements.")
85
86
        # Transform CIDR notation into a port range and validates it.
87
        if cidr_notation:
88
            base, prefix = port_range
89
            port_range = self._cidr_to_range(base, prefix)
90
91
        # Let the parser fix a reverse-ordered range in non-strict mode.
92
        if not self.strict:
93
            port_range.sort()
94
95
        # Get port range bounds.
96
        port_from = port_range[0]
97
        # Single port gets their upper bound set to None.
98
        port_to = port_range[1] if len(port_range) == 2 else None
99
100
        # Validate constraints in strict mode.
101
        if self.strict:
102
            # Disallow out-of-bounds values.
103
            if not (self.port_min <= port_from <= self.port_max) or (
104
                    port_to is not None and not (
105
                        self.port_min <= port_to <= self.port_max)):
106
                raise ValueError("Out of bounds.")
107
            # Disallow reversed range.
108
            if port_to is not None and port_from > port_to:
109
                raise ValueError("Invalid reversed port range.")
110
111
        # Clamp down lower bound, then cap it.
112
        port_from = min([max([port_from, self.port_min]), self.port_max])
113
114
        # Single port gets its upper bound aligned to its lower one.
115
        if port_to is None:
116
            port_to = port_from
117
118
        # Cap upper bound.
119
        port_to = min([port_to, self.port_max])
120
121
        return port_from, port_to
122
123
    def __repr__(self):
124
        """ Print all components of the range. """
125
        return (
126
            '{}(port_from={}, port_to={}, base={}, offset={}, prefix={}, '
127
            'mask={}, is_single_port={}, is_cidr={})').format(
128
                self.__class__.__name__, self.port_from, self.port_to,
129
                self.base, self.offset, self.prefix, self.mask,
130
                self.is_single_port, self.is_cidr)
131
132
    def __str__(self):
133
        """ Return the most appropriate string representation. """
134
        if self.is_single_port:
135
            return str(self.port_from)
136
        try:
137
            return self.cidr_string
138
        except ValueError:
139
            return self.range_string
140
141
    @property
142
    def cidr_string(self):
143
        """ Return a clean CIDR-like notation if possible. """
144
        if not self.is_cidr:
145
            raise ValueError(
146
                "Range can't be rendered using a CIDR-like notation.")
147
        return '{}{}{}'.format(self.base, self.CIDR_SEP, self.prefix)
148
149
    @property
150
    def range_string(self):
151
        """ Return a clean range notation. """
152
        return '{}{}{}'.format(self.port_from, self.RANGE_SEP, self.port_to)
153
154
    @classmethod
155
    def _is_power_of_two(cls, value):
156
        """ Helper to check if a value is a power of 2. """
157
        return math.log(value, 2) % 1 == 0
158
159
    @classmethod
160
    def _nearest_power_of_two(cls, value):
161
        """ Return nearsest power of 2. """
162
        return int(2 ** math.floor(math.log(value, 2)))
163
164
    @classmethod
165
    def _mask(cls, prefix):
166
        """ Compute the mask. """
167
        return cls.port_lenght - prefix
168
169
    @classmethod
170
    def _raw_upper_bound(cls, base, prefix):
171
        """ Compute a raw upper bound. """
172
        return base + (2 ** cls._mask(prefix)) - 1
173
174
    def _cidr_to_range(self, base, prefix):
175
        """ Transform a CIDR-like notation into a port range. """
176
        # Validates base and prefix values.
177
        if not self.port_min <= base <= self.port_max:
178
            raise ValueError("Port base out of bounds.")
179
        if not 1 <= prefix <= self.port_lenght:
180
            raise ValueError("CIDR-like prefix out of bounds.")
181
182
        # Disallow offsets in strict mode.
183
        if (self.strict and prefix != self.port_lenght
184
                and not self._is_power_of_two(base)):
185
            raise ValueError("Port base is not a power of two.")
186
187
        port_from = base
188
        port_to = self._raw_upper_bound(base, prefix)
189
        return [port_from, port_to]
190
191
    @property
192
    def bounds(self):
193
        """ Return lower and upper bounds of the port range. """
194
        return self.port_from, self.port_to
195
196
    @property
197
    def base(self):
198
        """ Alias to port_from, used as a starting point for CIDR notation. """
199
        return self.port_from
200
201
    @property
202
    def offset(self):
203
        """ Port base offset from its nearest power of two. """
204
        return self.base - self._nearest_power_of_two(self.base)
205
206
    @property
207
    def prefix(self):
208
        """ A power-of-two delta means a valid CIDR-like prefix. """
209
        # Check that range delta is a power of 2
210
        port_delta = self.port_to - self.port_from + 1
211
        if not self._is_power_of_two(port_delta):
212
            return None
213
        return self.port_lenght - int(math.log(port_delta, 2))
214
215
    @property
216
    def mask(self):
217
        """ Port range binary mask, based on CIDR-like prefix. """
218
        return self._mask(self.prefix) if self.prefix else None
219
220
    @property
221
    def cidr(self):
222
        """ Return components of the CIDR-like notation. """
223
        return self.base, self.prefix
224
225
    @property
226
    def is_single_port(self):
227
        """ Is the range a single port? """
228
        return True if self.port_from == self.port_to else False
229
230
    @property
231
    def is_cidr(self):
232
        """ Is the range can be expressed using a CIDR-like notation? """
233
        return True if self.prefix is not None else False
234