| Total Complexity | 54 |
| Total Lines | 192 |
| Duplicated Lines | 0 % |
Complex classes like port_range.PortRange 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 -*- |
||
| 33 | class PortRange(object): |
||
| 34 | """ Port range with support of a CIDR-like (binary) notation. |
||
| 35 | |||
| 36 | In strict mode (disabled by default) we'll enforce the following rules: |
||
| 37 | * port base must be a power of two (offsets not allowed); |
||
| 38 | * port CIDR should not produce overflowing upper bound. |
||
| 39 | |||
| 40 | This mode can be disabled on object creation. |
||
| 41 | """ |
||
| 42 | # Separators constants |
||
| 43 | CIDR_SEP = '/' |
||
| 44 | RANGE_SEP = '-' |
||
| 45 | |||
| 46 | # Max port lenght, in bits |
||
| 47 | port_lenght = 16 |
||
| 48 | # Max port range integer values |
||
| 49 | port_min = 1 |
||
| 50 | port_max = (2 ** port_lenght) - 1 |
||
| 51 | |||
| 52 | # Base values on which all other properties are computed. |
||
| 53 | port_from = None |
||
| 54 | port_to = None |
||
| 55 | |||
| 56 | def __init__(self, port_range, strict=False): |
||
| 57 | """ Set up class with a port_from and port_to integer. """ |
||
| 58 | self.port_from, self.port_to = self.parse(port_range, strict=strict) |
||
| 59 | |||
| 60 | def parse(self, port_range, strict=False): |
||
| 61 | """ Parse and normalize port range string into a port range. """ |
||
| 62 | if isinstance(port_range, basestring) and self.CIDR_SEP in port_range: |
||
| 63 | base, prefix = self.parse_cidr(port_range, strict) |
||
| 64 | port_from, port_to = self._cidr_to_range(base, prefix) |
||
| 65 | else: |
||
| 66 | port_from, port_to = self.parse_range(port_range) |
||
| 67 | if not port_from or not port_to: |
||
| 68 | raise ValueError("Invalid ports.") |
||
| 69 | # Check upper bound |
||
| 70 | if strict: |
||
| 71 | # Disallow overflowing upper bound |
||
| 72 | if port_to > self.port_max: |
||
| 73 | raise ValueError("Overflowing upper bound.") |
||
| 74 | else: |
||
| 75 | # Cap upper bound |
||
| 76 | port_to = port_to if port_to < self.port_max else self.port_max |
||
| 77 | return port_from, port_to |
||
| 78 | |||
| 79 | def parse_cidr(self, port_range, strict=False): |
||
| 80 | """ Split a string and extract port base and CIDR prefix. |
||
| 81 | |||
| 82 | Always returns a list of 2 integers. Defaults to None. |
||
| 83 | """ |
||
| 84 | # Separate base and prefix |
||
| 85 | elements = list(iter_map(int, port_range.split(self.CIDR_SEP, 2))) |
||
| 86 | elements += [None, None] |
||
| 87 | base, prefix = elements[:2] |
||
| 88 | # Normalize prefix value |
||
| 89 | if prefix is None: |
||
| 90 | prefix = self.port_lenght |
||
| 91 | # Validates base and prefix values |
||
| 92 | if not base or base < self.port_min or base > self.port_max: |
||
| 93 | raise ValueError("Invalid port base.") |
||
| 94 | if not prefix or prefix < 1 or prefix > self.port_lenght: |
||
| 95 | raise ValueError("Invalid CIDR-like prefix.") |
||
| 96 | # Enable rigorous rules |
||
| 97 | if strict: |
||
| 98 | # Disallow offsets |
||
| 99 | if prefix != self.port_lenght and not self._is_power_of_two(base): |
||
| 100 | raise ValueError("Port base is not a power of Two.") |
||
| 101 | return base, prefix |
||
| 102 | |||
| 103 | def parse_range(self, port_range): |
||
| 104 | """ Normalize port range to a sorted list of no more than 2 integers. |
||
| 105 | |||
| 106 | Excludes None values while parsing. |
||
| 107 | """ |
||
| 108 | if isinstance(port_range, basestring): |
||
| 109 | port_range = port_range.split(self.RANGE_SEP, 2) |
||
| 110 | if not isinstance(port_range, (set, list, tuple)): |
||
| 111 | port_range = [port_range] |
||
| 112 | port_range = [int(port) |
||
| 113 | for port in port_range |
||
| 114 | if port and int(port)][:2] |
||
| 115 | port_range.sort() |
||
| 116 | # Fill out missing slots by None values |
||
| 117 | port_range += [None] * (2 - len(port_range)) |
||
| 118 | port_from, port_to = port_range |
||
| 119 | if not port_from or port_from < self.port_min or \ |
||
| 120 | port_from > self.port_max: |
||
| 121 | raise ValueError("Invalid port range lower bound.") |
||
| 122 | if not port_to: |
||
| 123 | port_to = port_from |
||
| 124 | return port_from, port_to |
||
| 125 | |||
| 126 | def __repr__(self): |
||
| 127 | """ Print all components of the range. """ |
||
| 128 | return '{}(port_from={}, port_to={}, base={}, offset={}, prefix={}, ' \ |
||
| 129 | 'mask={})'.format(self.__class__.__name__, self.port_from, |
||
| 130 | self.port_to, self.base, self.offset, |
||
| 131 | self.prefix, self.mask) |
||
| 132 | |||
| 133 | def __str__(self): |
||
| 134 | """ Returns the most appropriate string representation. """ |
||
| 135 | if self.is_single_port: |
||
| 136 | return str(self.port_from) |
||
| 137 | try: |
||
| 138 | return self.cidr_string |
||
| 139 | except ValueError: |
||
| 140 | return self.range_string |
||
| 141 | |||
| 142 | @property |
||
| 143 | def cidr_string(self): |
||
| 144 | """ Returns a clean CIDR-like notation if possible. """ |
||
| 145 | if not self.is_cidr: |
||
| 146 | raise ValueError( |
||
| 147 | "Range can't be rendered using a CIDR-like notation.") |
||
| 148 | return '{}{}{}'.format(self.base, self.CIDR_SEP, self.prefix) |
||
| 149 | |||
| 150 | @property |
||
| 151 | def range_string(self): |
||
| 152 | """ Returns a clean range notation. """ |
||
| 153 | return '{}{}{}'.format(self.port_from, self.RANGE_SEP, self.port_to) |
||
| 154 | |||
| 155 | @classmethod |
||
| 156 | def _is_power_of_two(cls, value): |
||
| 157 | """ Helper to check if a value is a power of 2. """ |
||
| 158 | return math.log(value, 2) % 1 == 0 |
||
| 159 | |||
| 160 | @classmethod |
||
| 161 | def _nearest_power_of_two(cls, value): |
||
| 162 | """ Returns nearsest power of 2. """ |
||
| 163 | return int(2 ** math.floor(math.log(value, 2))) |
||
| 164 | |||
| 165 | @classmethod |
||
| 166 | def _mask(cls, prefix): |
||
| 167 | """ Compute the mask. """ |
||
| 168 | return cls.port_lenght - prefix |
||
| 169 | |||
| 170 | @classmethod |
||
| 171 | def _raw_upper_bound(cls, base, prefix): |
||
| 172 | """ Compute a raw upper bound. """ |
||
| 173 | return base + (2 ** cls._mask(prefix)) - 1 |
||
| 174 | |||
| 175 | @classmethod |
||
| 176 | def _cidr_to_range(cls, base, prefix): |
||
| 177 | """ Transform a CIDR-like notation into a port range. """ |
||
| 178 | port_from = base |
||
| 179 | port_to = cls._raw_upper_bound(base, prefix) |
||
| 180 | return port_from, port_to |
||
| 181 | |||
| 182 | @property |
||
| 183 | def bounds(self): |
||
| 184 | """ Returns lower and upper bounds of the port range. """ |
||
| 185 | return self.port_from, self.port_to |
||
| 186 | |||
| 187 | @property |
||
| 188 | def base(self): |
||
| 189 | """ Alias to port_from, used as a starting point for CIDR notation. """ |
||
| 190 | return self.port_from |
||
| 191 | |||
| 192 | @property |
||
| 193 | def offset(self): |
||
| 194 | """ Port base offset from its nearest power of two. """ |
||
| 195 | return self.base - self._nearest_power_of_two(self.base) |
||
| 196 | |||
| 197 | @property |
||
| 198 | def prefix(self): |
||
| 199 | """ A power-of-two delta means a valid CIDR-like prefix. """ |
||
| 200 | # Check that range delta is a power of 2 |
||
| 201 | port_delta = self.port_to - self.port_from + 1 |
||
| 202 | if not self._is_power_of_two(port_delta): |
||
| 203 | return None |
||
| 204 | return self.port_lenght - int(math.log(port_delta, 2)) |
||
| 205 | |||
| 206 | @property |
||
| 207 | def mask(self): |
||
| 208 | """ Port range binary mask, based on CIDR-like prefix. """ |
||
| 209 | return self._mask(self.prefix) if self.prefix else None |
||
| 210 | |||
| 211 | @property |
||
| 212 | def cidr(self): |
||
| 213 | """ Returns components of the CIDR-like notation. """ |
||
| 214 | return self.base, self.prefix |
||
| 215 | |||
| 216 | @property |
||
| 217 | def is_single_port(self): |
||
| 218 | """ Is the range a single port ? """ |
||
| 219 | return True if self.port_from == self.port_to else False |
||
| 220 | |||
| 221 | @property |
||
| 222 | def is_cidr(self): |
||
| 223 | """ Is the range can be expressed using a CIDR-like notation. """ |
||
| 224 | return True if self.prefix is not None else False |
||
| 225 |