├── netcop ├── py.typed ├── __init__.py └── parser.py ├── requirements-dev.txt ├── .gitignore ├── setup.cfg ├── pyproject.toml ├── setup.py ├── LICENSE ├── .github └── workflows │ └── python-package.yml ├── README.md ├── tests └── test_conf.py └── pylintrc /netcop/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ruff 2 | mypy 3 | pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | !.gitignore 4 | .* 5 | *.egg-info -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .*,__pycache__,build,dist 3 | max-line-length = 120 4 | 5 | -------------------------------------------------------------------------------- /netcop/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Netcop - NETwork COnfig Parser 3 | 4 | This Python library helps navigating and querying textual (CLI-style) configs of 5 | network devices. 6 | """ 7 | 8 | from .parser import Conf # noqa: F401 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | # Enable Pyflakes `E` and `F` codes by default. 3 | select = [ 4 | "E", "F", # pyflakes 5 | "I", # isort 6 | "UP", # pyupgrade 7 | "RUF", # ruff-specific rules 8 | ] 9 | target-version = "py38" 10 | line-length = 120 11 | 12 | [tool.mypy] 13 | ignore_missing_imports = true 14 | # disallow_untyped_defs = true 15 | check_untyped_defs = true 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="netcop", 8 | version="1.1.0", 9 | author="Alexey Andriyanov", 10 | author_email="alanm@bk.ru", 11 | description="A vendor-agnostic parser of network configs", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/andriyanov/netcop", 15 | packages=["netcop"], 16 | classifiers=[ 17 | "Programming Language :: Python", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexey Andriyanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: [3.8, 3.11] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 29 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 30 | pip install -e . 31 | - name: Lint 32 | run: | 33 | ruff . 34 | mypy . 35 | - name: Test with pytest 36 | run: | 37 | pytest tests 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netcop — NETwork COnfig Parser 2 | 3 | This Python library helps navigating and querying textual (CLI-style) configs of network devices. It may be useful for solving such problems like: 4 | - listing a device interfaces 5 | - extracting IP address or VLAN configurations of a network interface 6 | - checking if a particular option is properly set in all the relevant blocks 7 | 8 | It does not support modifying and comparing of configs, as the [CiscoConfigParse][1] does, but provides a nice and simple query API. 9 | 10 | ## Installation 11 | Netcop works with both Python 2.7 and Python 3. 12 | 13 | To install it as a package, use this command: 14 | 15 | python3 -m pip install netcop 16 | 17 | 18 | ## Vendor compatibility 19 | Netcop works by parsing hierarchical text configs that use newline-separated statements, whitespace indentation of blocks and keywords prefixes as a config path. Thus, it is not limited to a particular vendor's syntax. 20 | 21 | In particular, these types of configs are supported by Netcop: 22 | - Cisco IOS, NX-OS, IOS-XR 23 | - Huawei VRP 24 | - Juniper JunOS 25 | - Quagga / FRR 26 | 27 | There should be many more of them, I have not checked others yet. 28 | 29 | However, Netcop does not have any idea of the config semantics — it can't guess the type of data relying by a given config path whether it is a list, an int, a string or an IP address. It's always a user who knows the semantics and treats a given path to be of a particular type. 30 | 31 | 32 | ## Usage guide 33 | 34 | Let's say we have this simple config to parse: 35 | ```python 36 | c = netcop.Conf(''' 37 | interface Port-channel1 38 | no ip address 39 | ! 40 | interface Port-channel2 41 | ip address 10.0.0.2 255.255.0.0 42 | ip address 10.1.0.2 255.255.0.0 secondary 43 | ! 44 | interface Loopback0 45 | ip address 1.1.1.1 255.255.255.255 46 | ! 47 | ''') 48 | ``` 49 | 50 | Below are some examples of processing this config. 51 | 52 | ### Indexing 53 | The result of parsing looks very much like a Python `dict` and Netcop tries hard to keep its API similar to what you can expect from a dict. 54 | 55 | The key operation you can do with a `Conf` object is to get a sub-object by a string key with the `[]` operator. 56 | 57 | Then we just use any of the following expressions to get a part (slice) of the config as another `Conf` object that has the same API for subsequent queries: 58 | - `c['interface']` 59 | - `c['interface Port-channel1']` 60 | - `c['interface Port-channel2 ip address']` 61 | 62 | 63 | To illustrate the way a config tree is organized, let's take a look at these three queries that return the same result: 64 | - `c['interface Port-channel2 ip address']` 65 | - `c['interface']['Port-channel2 ip']['address']` 66 | - `c['interface']['Port-channel2']['ip address']` 67 | 68 | Unlike the dict's `[]`, this operator never causes the `KeyError` exception, it returns an empty `Conf` object instead. 69 | 70 | ### Iterating 71 | 72 | To obtain a sequence of interface names, you just need to use `.keys()` method: 73 | 74 | ```python 75 | [i for i in c['interface'].keys()] 76 | ``` 77 | Output: 78 | 79 | ['Port-channel1', 'Port-channel2', 'Loopback0'] 80 | 81 | Just as it is with a dict, you can get the same result by iterating over an object: 82 | ```python 83 | [i for i in c['interface']] 84 | ``` 85 | 86 | Likewise, to get IP addresses assigned to all the interfaces, use this snippet: 87 | ```python 88 | for ifname in c['interface']: 89 | for ip in c['interface'][ifname]['ip address']: 90 | print(ifname, ip) 91 | ``` 92 | Output: 93 | 94 | Port-channel2 10.0.0.2 95 | Port-channel2 10.1.0.2 96 | Loopback0 1.1.1.1 97 | 98 | Just as it is with a `dict`, you can use `.items()` to avoid redundant key lookups (resulting output is the same): 99 | ```python 100 | for ifname, iface_c in c['interface'].items(): 101 | for ip in iface_c['ip address']: 102 | print(ifname, ip) 103 | ``` 104 | 105 | ### Iterating over raw config lines 106 | 107 | There're 3 ways of traversing lines of the config, or a sub-part of it: 108 | - `Conf.tails`: returns the list of matched lines tails, excluding matched prefix. Does 109 | not return lines from the nested blocks. Lines are trimmed from whitespace. 110 | ```python 111 | cfg = Conf(""" 112 | snmp host A 113 | snmp host B 114 | """) 115 | print(cfg["snmp"].tails) 116 | ``` 117 | Output: 118 | ``` 119 | ["host A", "host B"] 120 | ``` 121 | - `Conf.lines()`: Iterates over raw matched lines, like `.dump()` does. Lines may be 122 | trimmed, if you specified prefix that covers the line partially, and may not be 123 | trimmed if the prefix is not specified. 124 | - `Conf.orig_lines()`: Like `.lines()`, but lines are always in the same form as in the 125 | original config (full and untrimmed), no matter which prefix is specified. 126 | 127 | ### Checking 128 | In a bool context a `Conf` object returns if it's empty, or in other words, if a specified config path exists. 129 | ```python 130 | # __bool__ operator works: 131 | bool(c) == True 132 | bool(c['interface Loopback0']) == True 133 | bool(c['interface Blah']) == False 134 | bool(c['interface Port-channel1 no ip address']) == True 135 | bool(c['interface Port-channel2 no ip address']) == False 136 | 137 | # same for __contains__ operator: 138 | ('interface Loopback0' in c) == True 139 | ('interface Blah' in c) == False 140 | ('interface Port-channel1 no ip address' in c) == True 141 | ('interface Port-channel2 no ip address' in c) == False 142 | ``` 143 | 144 | ### Getting values 145 | So far, we have seen how to iterate over multiple values by a given path. What if we're sure that there is only one value for a path? Then you can use any of the scalar properties of a `Conf` object: 146 | - `.word` - a single string keyword 147 | - `.int` - an integer value 148 | - `.ip`, `.cidr` (*since Python 3.3*) - a `IPAddress` or `IPNetwork` object from the `ipaddress` standard library 149 | - `.tail` - all the tailing keywords as a string 150 | 151 | You should note that in contrast to indexing and iterating operations that can never fail, the scalar getters can raise `KeyError` or `TypeError` if there are no values for a given path, or if there are multiple ones. 152 | 153 | Here is an example: 154 | 155 | ```python 156 | c['interface Loopback0 ip address'].word == '1.1.1.1' 157 | c['interface Loopback0 ip address'].ip == IPv4Address('1.1.1.1') 158 | c['interface Loopback0 ip address'].tail == '1.1.1.1 255.55.255.255' 159 | c['interface Port-channel2 ip address'].ip # KeyError raised 160 | ``` 161 | 162 | There are also list properties `.ints`, `.ips` and `.cidrs`, which are just type-converting iterators and shorthands for expressions like `[int(x) for x in c]`. 163 | 164 | ### Wildcard indexing with .expand() 165 | Netcop does not use regular expressions to make index lookups, it requires exact keywords in the config path. However, there are cases when it is useful to specify a config path with a pattern: 166 | ```python 167 | for ifname, ip in c.expand('interface po* ip address *'): 168 | print(ifname, ip) 169 | ``` 170 | Resulting output: 171 | 172 | Port-channel2 10.0.0.2 173 | Port-channel2 10.1.0.2 174 | 175 | The `.expand()` method iterates over all possible paths in a config by a given selector with wildcards using glob syntax. It returns tuples with the length equal to the number of wildcard placeholders in a given key. 176 | 177 | There's a special trailing glob pattern supported `~`. It means capture the rest of the line and can only occur at the end of the expand query string, separated by space. 178 | Example: 179 | ```python 180 | >>> list(c.expand("interface * ip address ~")) 181 | [ 182 | ("Port-channel2", "10.0.0.2 255.255.0.0"), 183 | ("Port-channel2", "10.1.0.2 255.255.0.0 secondary"), 184 | ("Loopback0", "1.1.1.1 255.255.255.255"), 185 | ] 186 | ``` 187 | 188 | The number of elements in the returned tuple always equals to the number of caputuring globs in the query string. 189 | If the optional `return_conf=True` kwarg is passed, there's the extra trailing element in the resulting tuples with the subsequent `Conf` object for the matched prefix. 190 | 191 | 192 | [1]: https://github.com/mpenning/ciscoconfparse 193 | -------------------------------------------------------------------------------- /tests/test_conf.py: -------------------------------------------------------------------------------- 1 | # pylint:disable=missing-module-docstring,missing-function-docstring,redefined-outer-name 2 | 3 | from io import StringIO 4 | from ipaddress import ip_address 5 | 6 | from pytest import fixture 7 | 8 | from netcop import Conf 9 | 10 | 11 | @fixture 12 | def conf(): 13 | return Conf( 14 | """ 15 | snmp server 1 16 | snmp server 2 17 | stp mode mstp 1 18 | interface IF1 19 | ip address 1.1.1.1 20 | ip address 2.2.2.2 secondary 21 | stp 22 | more stp 23 | ! 24 | no ip redirects 25 | ! 26 | interface IF2 27 | ip address 1.1.1.2 28 | no ip redirects 29 | ip unnumbered 30 | description hello world 31 | long-description "hello world" end 32 | ! 33 | """ 34 | ) 35 | 36 | 37 | @fixture 38 | def jconf(): 39 | return Conf( 40 | """ 41 | forwarding-options { 42 | sampling { # Traffic is sampled and sent to a flow server. 43 | input { 44 | rate 1; # Samples 1 out of x packets (here, a rate of 1 sample per packet). 45 | } 46 | } 47 | family inet { 48 | output { 49 | flow-server 10.60.2.1 { # The IP address and port of the flow server. 50 | port 2055; 51 | version 5; # Records are sent to the flow server using version 5 format. 52 | } 53 | flow-inactive-timeout 15; 54 | flow-active-timeout 60; 55 | interface sp-2/0/0 { # Adding an interface here enables PIC-based sampling. 56 | engine-id 5; # Engine statements are dynamic, but can be configured. 57 | engine-type 55; 58 | source-address 10.60.2.2; # You must configure this statement. 59 | } 60 | apply-groups [ one two three ]; # comment 61 | } 62 | } 63 | } 64 | """ 65 | ) 66 | 67 | 68 | def test1(conf: Conf): 69 | assert list(conf["snmp server"]) == ["1", "2"] 70 | 71 | assert conf["snmp server 1"] 72 | assert "snmp server 2" in conf 73 | 74 | assert not conf["snmp server 3"] 75 | assert "snmp server 3" not in conf 76 | 77 | assert "stp mode mstp 1" in conf 78 | 79 | 80 | def test2(conf: Conf): 81 | assert conf["interface if1 ip address 1.1.1.1"] 82 | assert not conf["interface if1 ip address 1.1.1.2"] 83 | assert not conf["interface if3 ip address"] 84 | assert conf["stp mode mstp 1"] 85 | 86 | 87 | def test3(conf: Conf): 88 | assert conf["interface if1 no"].tail == "ip redirects" 89 | 90 | 91 | def test4(conf: Conf): 92 | assert len(conf["interface"]) == 2 93 | assert list(conf["interface"]) == ["IF1", "IF2"] 94 | 95 | 96 | def test5(conf: Conf): 97 | expected = {"IF2": "hello world"} 98 | actual = {} 99 | for ifname, iface in conf["interface"].items(): 100 | if iface["description"]: 101 | actual[ifname] = iface["description"].tail 102 | assert expected == actual 103 | 104 | assert conf["interface if2 long-description"].quoted == "hello world" 105 | 106 | 107 | def test6(conf: Conf): 108 | assert conf["stp"].word == "mode" 109 | assert conf["stp mode"].word == "mstp" 110 | assert conf["stp mode mstp"].word == "1" 111 | 112 | 113 | def test_repr(conf: Conf): 114 | assert repr(conf) == "Conf()['']" 115 | assert repr(conf["interface"]) == "Conf()['interface']" 116 | assert repr(conf["interface if1 ip"]) == "Conf()['interface IF1 ip']" 117 | assert repr(conf["interface IF3 ip"]) == "Conf()" 118 | 119 | 120 | def test_dump(): 121 | conf = Conf( 122 | """ 123 | snmp server 1 124 | stp mode mstp 1 125 | ! 126 | interface IF1 127 | ip address 1.1.1.1 128 | ip address 2.2.2.2 secondary 129 | ! 130 | interface IF2 131 | ip address 1.1.1.2 132 | no ip redirects 133 | ! 134 | """ 135 | ) 136 | 137 | buff = StringIO() 138 | conf.dump(file=buff, indent=" ") 139 | assert ( 140 | buff.getvalue() 141 | == """ 142 | snmp server 1 143 | stp mode mstp 1 144 | ! 145 | interface IF1 146 | ip address 1.1.1.1 147 | ip address 2.2.2.2 secondary 148 | ! 149 | interface IF2 150 | ip address 1.1.1.2 151 | no ip redirects 152 | ! 153 | """[ 154 | 1: 155 | ] 156 | ) 157 | 158 | buff = StringIO() 159 | conf["interface if1 ip"].dump(file=buff, indent=" ") 160 | assert ( 161 | buff.getvalue() 162 | == """ 163 | [interface IF1 ip] 164 | address 1.1.1.1 165 | address 2.2.2.2 secondary 166 | """[ 167 | 1: 168 | ] 169 | ) 170 | 171 | buff = StringIO() 172 | conf["stp"].dump(file=buff, indent=" ") 173 | assert buff.getvalue() == "[stp] mode mstp 1\n" 174 | 175 | jconf = Conf( 176 | """ 177 | forwarding-options { 178 | sampling { # Traffic is sampled and sent to a flow server. 179 | input { 180 | rate 1; # Samples 1 out of x packets (here, a rate of 1 sample per packet). 181 | } 182 | } 183 | family inet { 184 | output { 185 | flow-server 10.60.2.1 { # The IP address and port of the flow server. 186 | port 2055; 187 | version 5; # Records are sent to the flow server using version 5 format. 188 | } 189 | flow-inactive-timeout 15; 190 | flow-active-timeout 60; 191 | interface sp-2/0/0 { # Adding an interface here enables PIC-based sampling. 192 | engine-id 5; # Engine statements are dynamic, but can be configured. 193 | engine-type 55; 194 | source-address 10.60.2.2; # You must configure this statement. 195 | } 196 | } 197 | } 198 | } 199 | """ 200 | ) 201 | 202 | buff = StringIO() 203 | jconf.dump(file=buff, indent=" ") 204 | assert ( 205 | buff.getvalue() 206 | == """ 207 | forwarding-options { 208 | sampling { # Traffic is sampled and sent to a flow server. 209 | input { 210 | rate 1; # Samples 1 out of x packets (here, a rate of 1 sample per packet). 211 | } 212 | } 213 | family inet { 214 | output { 215 | flow-server 10.60.2.1 { # The IP address and port of the flow server. 216 | port 2055; 217 | version 5; # Records are sent to the flow server using version 5 format. 218 | } 219 | flow-inactive-timeout 15; 220 | flow-active-timeout 60; 221 | interface sp-2/0/0 { # Adding an interface here enables PIC-based sampling. 222 | engine-id 5; # Engine statements are dynamic, but can be configured. 223 | engine-type 55; 224 | source-address 10.60.2.2; # You must configure this statement. 225 | } 226 | } 227 | } 228 | } 229 | """[ 230 | 1: 231 | ] 232 | ) 233 | 234 | buff = StringIO() 235 | key = "forwarding-options family inet output flow-server" 236 | jconf[key].dump(file=buff, indent=" ") 237 | assert ( 238 | buff.getvalue() 239 | == """[%s] 10.60.2.1 { # The IP address and port of the flow server. 240 | port 2055; 241 | version 5; # Records are sent to the flow server using version 5 format. 242 | """ 243 | % key 244 | ) 245 | 246 | 247 | def test_junos(jconf: Conf): 248 | f_o = jconf["forwarding-options"] 249 | assert f_o["sampling input rate"].int == 1 250 | assert f_o["family inet"] 251 | if "ip_address" in globals(): 252 | assert f_o["family inet output flow-server"].ips == [ip_address("10.60.2.1")] 253 | 254 | assert f_o["family inet output apply-groups"].junos_list == ["one", "two", "three"] 255 | assert f_o["sampling input rate"].junos_list == ["1"] 256 | 257 | 258 | def test_expand(conf: Conf): 259 | assert list(conf.expand("interface * ip address *")) == [ 260 | ("IF1", "1.1.1.1"), 261 | ("IF1", "2.2.2.2"), 262 | ("IF2", "1.1.1.2"), 263 | ] 264 | 265 | assert list(conf.expand("interface * ip blah *")) == [] 266 | 267 | # assert list(conf.expand('interface')) == [(), ()] 268 | 269 | 270 | def test_expand2(conf: Conf): 271 | assert list(conf.expand("interface * ip address ~")) == [ 272 | ("IF1", "1.1.1.1"), 273 | ("IF1", "2.2.2.2 secondary"), 274 | ("IF2", "1.1.1.2"), 275 | ] 276 | 277 | assert list(conf.expand("interface * ip blah ~")) == [] 278 | 279 | assert conf["interface IF1 ip"].tails == [ 280 | "address 1.1.1.1", 281 | "address 2.2.2.2 secondary", 282 | ] 283 | 284 | 285 | def test_expand3(conf: Conf): 286 | assert list(conf.expand("interface ~")) == [ 287 | ("IF1",), 288 | ("IF2",), 289 | ] 290 | assert conf["interface"].tails == [ 291 | "IF1", 292 | "IF2", 293 | ] 294 | 295 | 296 | def test_tails(conf: Conf): 297 | assert conf["interface Something"].tails == [] 298 | assert conf["interface IF1 ip address 1.1.1.1"].tails == [] 299 | assert conf["interface IF1 stp"].tails == [ 300 | "more stp", 301 | ] 302 | 303 | 304 | def test_lines(conf: Conf): 305 | assert conf["interface IF1"].lines() == [ 306 | " ip address 1.1.1.1", 307 | " ip address 2.2.2.2 secondary", 308 | " stp", 309 | " more stp", 310 | " !", 311 | " no ip redirects", 312 | ] 313 | 314 | assert conf["interface IF1 ip"].orig_lines() == [ 315 | " ip address 1.1.1.1", 316 | " ip address 2.2.2.2 secondary", 317 | ] 318 | 319 | assert conf["interface"].orig_lines() == [ 320 | 'interface IF1', 321 | ' ip address 1.1.1.1', 322 | ' ip address 2.2.2.2 secondary', 323 | ' stp', 324 | ' more stp', 325 | ' !', 326 | ' no ip redirects', 327 | 'interface IF2', 328 | ' ip address 1.1.1.2', 329 | ' no ip redirects', 330 | ' ip unnumbered', 331 | ' description hello world', 332 | ' long-description "hello world" end', 333 | ] 334 | -------------------------------------------------------------------------------- /netcop/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Netcop - NETwork COnfig Parser 3 | 4 | This Python library helps navigating and querying textual (CLI-style) configs of 5 | network devices. 6 | """ 7 | 8 | import fnmatch 9 | import re 10 | import sys 11 | from ipaddress import ip_address, ip_network 12 | from typing import Dict, Iterator, List, Optional, Tuple 13 | 14 | 15 | # ==== 16 | class Conf: 17 | """ 18 | Get subnode of the config tree by a string key 19 | A key can be either a single keyword or space-separated sequence of them. 20 | 21 | Consider this example config: 22 | interface Ethernet1/0/1 23 | ip address 10.0.0.1/24 24 | ip address 10.0.0.100/24 secondary 25 | spanning-tree enable 26 | 27 | The conf['interface'] lookup will return the following node: 28 | Ethernet1/0/1 29 | ip address 10.0.0.1/24 30 | ip address 10.0.0.100/24 secondary 31 | spanning-tree enable 32 | 33 | In case we use a sequence of keywords as a key like that: 34 | conf['interface Ethernet1/0/1 ip address'] 35 | then the result would be: 36 | 10.0.0.1/24 37 | 10.0.0.100/24 secondary 38 | 39 | Moreover, the same result could be retrieved by multiple sequential lookups: 40 | conf['interface']['Ethernet1/0/1']['ip']['address'] 41 | """ 42 | 43 | __slots__ = ("_line", "_orig_line", "_lineno", "_trace", "_children", "_index") 44 | 45 | def __init__(self, text: str = "", lines: Optional[List[str]] = None): 46 | """ 47 | Parse given config into a tree. 48 | Config may be either a text or a sequence of lines (e.g. filehandle). 49 | """ 50 | self._line: Optional[str] = None 51 | self._orig_line: str = "" 52 | self._lineno = 0 53 | self._trace: Tuple[str, ...] = () 54 | self._children: List[Conf] = [] 55 | self._index: Dict[str, Tuple[str, List[Conf]]] = {} 56 | if text: 57 | self._parse(text.splitlines()) 58 | elif lines: 59 | self._parse(lines) 60 | if self._children: 61 | self._line = "" 62 | 63 | @classmethod 64 | def _new(cls, line, lineno, children=(), orig_line=None) -> "Conf": 65 | ret = cls() 66 | ret._line = line 67 | ret._orig_line = orig_line or line or "" 68 | ret._lineno = lineno 69 | ret._children = list(children) 70 | return ret 71 | 72 | def _parse(self, lines: List[str]) -> None: 73 | stack = [(self, "")] 74 | 75 | for lineno, line in enumerate(lines): 76 | line = line.rstrip() 77 | m = re.match(r"(\s*)", line) 78 | if not m: 79 | # if is used to avoid unnesessary string formatting 80 | assert m, "regexp must always match, line %r" % line 81 | indent = m.group(1) 82 | if line == indent: 83 | continue 84 | node = Conf._new(line, lineno) 85 | if len(indent) > len(stack[-1][1]): 86 | stack[-1][0]._children.append(node) 87 | stack.append((node, indent)) 88 | else: 89 | while len(indent) <= len(stack[-1][1]) and stack[-1][0] is not self: 90 | stack.pop() 91 | stack[-1][0]._children.append(node) 92 | stack.append((node, indent)) 93 | 94 | @staticmethod 95 | def _next_token(string): 96 | no_more = ("", "", "") 97 | if not string: 98 | return no_more 99 | items = string.split(None, 1) 100 | if not items: 101 | return no_more 102 | token = items[0] 103 | if token in ("{", "}", "#", "!"): 104 | return no_more 105 | rest = "" 106 | if len(items) == 2: 107 | rest = items[1] 108 | if token.endswith(";") and (rest == "" or rest.startswith(("#", "!"))): 109 | token = token[:-1] 110 | return token.lower(), token, rest 111 | 112 | @property 113 | def trace(self): 114 | """ 115 | Get the string index of the node agaisnt root of the tree 116 | """ 117 | return " ".join(self._trace) 118 | 119 | def __repr__(self): 120 | fmt_line = "" 121 | if self._line: 122 | fmt_line = repr(self._line) 123 | ret = f"{self.__class__.__name__}({fmt_line})" 124 | if self._trace or self._line is not None: 125 | ret += "[%r]" % self.trace 126 | return ret 127 | 128 | def dump(self, file=None, indent=" ", show_header=True): 129 | """ 130 | Write the indented config subtree to a given file 131 | sys.stdout is used if file argument is unspecified. 132 | """ 133 | if self._line is None: 134 | return 135 | if file is None: 136 | file = sys.stdout 137 | 138 | def _put_line(line, level): 139 | if indent is None: 140 | file.write(line + "\n") 141 | else: 142 | file.write((indent * level) + line.strip() + "\n") 143 | 144 | if self._trace and show_header: 145 | file.write("[%s]" % (self.trace)) 146 | file.write("\n" if not self._line else " ") 147 | 148 | for line, level in self._iter_lines(-1, False): 149 | _put_line(line, level) 150 | 151 | def _iter_lines(self, depth, orig_lines): 152 | if self._line: 153 | yield (self._orig_line if orig_lines else self._line), depth 154 | for c in self._children: 155 | yield from c._iter_lines(depth + 1, orig_lines) 156 | 157 | def lines(self) -> List[str]: 158 | """ 159 | Returns list of matched lines, relative to match prefix 160 | """ 161 | return [line for line, _ in self._iter_lines(0, False)] 162 | 163 | def orig_lines(self) -> List[str]: 164 | """ 165 | Returns list of original lines from the config that matched the prefix 166 | """ 167 | return [line for line, _ in self._iter_lines(0, True)] 168 | 169 | def _ensure_scalar(self): 170 | self._reindex() 171 | if len(self._index) == 0: 172 | raise KeyError( 173 | "No entries in node [%r], line %d" % (self.trace, self._lineno) 174 | ) 175 | if len(self._index) > 1: 176 | raise KeyError( 177 | "Multiple entries (%d) match the key [%r], line %d" 178 | % ( 179 | len(self._index), 180 | self.trace, 181 | self._lineno, 182 | ) 183 | ) 184 | return next(iter(self._index)) 185 | 186 | def _reindex(self): 187 | if self._index: 188 | return 189 | index = {} 190 | token_lc, token, rest = self._next_token(self._line) 191 | if token: 192 | new = Conf._new(rest, self._lineno, self._children, self._orig_line) 193 | index[token_lc] = (token, [new]) 194 | else: 195 | for c in self._children: 196 | token_lc, token, rest = self._next_token(c._line) 197 | if token: 198 | new = Conf._new(rest, c._lineno, c._children, c._orig_line) 199 | index.setdefault(token_lc, (token, []))[1].append(new) 200 | self._index = index 201 | 202 | def expand(self, key: str, return_conf: bool = False) -> Iterator[Tuple]: 203 | """ 204 | Iterates over all possible paths in config by given :key selector with wildcards 205 | Returns tuples with the length equal to the number of wildcard placeholders in 206 | the :key. 207 | If :return_conf is set, the resulting tuples also contain the trailing Conf() 208 | object at their last element. 209 | 210 | Example: 211 | for ifname, ip in Conf.expand('interface po* ip address *'): 212 | # prints all the IPs assigned to port-channel interfaces one per line 213 | print(ifname, ip) 214 | for ifname, ip, conf in Conf.expand('interface po* ip address *', True): 215 | # the same, but only for primary IPs 216 | if not conf['secondary']: 217 | print(ifname, ip) 218 | """ 219 | if not key: 220 | if return_conf: 221 | yield (self,) # type: ignore 222 | else: 223 | yield () 224 | return 225 | self._reindex() 226 | _, token, rest = self._next_token(key) 227 | if token == "~": 228 | if rest: 229 | raise ValueError("'~' should be the last token in query") 230 | if self._line: 231 | yield ( 232 | self._line.strip(), 233 | self._expand_cfg(self.trace + " " + self._line), 234 | ) if return_conf else (self._line.strip(),) 235 | else: 236 | for c in self._children: 237 | assert c._line 238 | yield ( 239 | c._line.strip(), 240 | c._expand_cfg(self.trace + " " + c._line), 241 | ) if return_conf else (c._line.strip(),) 242 | elif any(x in token for x in "*?["): 243 | for k in fnmatch.filter(self, token): 244 | for ret in self[k].expand(rest, return_conf): 245 | yield (k, *ret) 246 | elif self[token]: 247 | for ret in self[token].expand(rest, return_conf): 248 | yield ret 249 | 250 | def _expand_cfg(self, trace_str: str) -> "Conf": 251 | ret = self._new( 252 | None if not self._children else "", 253 | self._lineno, 254 | self._children, 255 | self._orig_line, 256 | ) 257 | ret._trace = tuple(trace_str.split()) 258 | return ret 259 | 260 | # ==== dict-like API 261 | def __getitem__(self, key: str) -> "Conf": 262 | token_lc, _, rest = self._next_token(key) 263 | if not token_lc: 264 | return self 265 | self._reindex() 266 | pair = self._index.get(token_lc) 267 | if not pair: 268 | return Conf() 269 | token, ret_list = pair 270 | 271 | if len(ret_list) == 1: 272 | ret = ret_list[0] 273 | else: 274 | # ret_list can not be empty 275 | ret = Conf._new("", ret_list[0]._lineno, ret_list, ret_list[0]._orig_line) 276 | 277 | ret._trace = (*self._trace, token) 278 | 279 | if rest: 280 | return ret[rest] 281 | return ret 282 | 283 | def __iter__(self): 284 | """ 285 | Get the sequence of unique keywords following the node 286 | """ 287 | self._reindex() 288 | return (x[0] for x in self._index.values()) 289 | 290 | def __len__(self): 291 | """ 292 | Number of the unique keywords following the node 293 | """ 294 | self._reindex() 295 | return len(self._index) 296 | 297 | def __contains__(self, key): 298 | """ 299 | Whether the [key] operator return a non-empty node 300 | """ 301 | self._reindex() 302 | return bool(self[key]) 303 | 304 | def __bool__(self): 305 | """ 306 | Whether the node is empty 307 | """ 308 | return self._line is not None 309 | 310 | def __nonzero__(self): 311 | return self.__bool__() 312 | 313 | def items(self): 314 | """ 315 | Get a sequence of (key, value) pairs just as with dict. 316 | keys are direct descendant strings, values are Conf subtrees 317 | """ 318 | self._reindex() 319 | return ((k, self[k]) for k in self) 320 | 321 | def keys(self): 322 | """ 323 | Get a sequence of direct descendant strings just as with dict. 324 | """ 325 | self._reindex() 326 | return (x for x in self) 327 | 328 | def values(self): 329 | """ 330 | Get a sequence of Conf subtrees 331 | """ 332 | self._reindex() 333 | return (x[1] for x in self._index.values()) 334 | 335 | def get(self, key, default=None, type=None): 336 | """ 337 | Get the following keyword by the given path (key) 338 | Optinal arguments are the default value and type convertion procedure. 339 | """ 340 | try: 341 | ret = self[key].word 342 | except KeyError: 343 | return default 344 | if type: 345 | return type(ret) 346 | return ret 347 | 348 | # ==== scalar API 349 | @property 350 | def word(self): 351 | """ 352 | Get the single following keyword. 353 | In case there is no one or there are multiple ones, a KeyError is raised. 354 | """ 355 | return self._ensure_scalar() 356 | 357 | @property 358 | def tail(self): 359 | """ 360 | Get the following keywords in the config line as a single string. 361 | In case there is no single assosiated line one or there are multiple ones, a 362 | KeyError is raised. 363 | """ 364 | self._ensure_scalar() 365 | items = [] 366 | rest = self._line 367 | while rest: 368 | _, token, rest = self._next_token(rest) 369 | if token: 370 | items.append(token) 371 | return " ".join(items) 372 | 373 | @property 374 | def tails(self): 375 | return [x for x, in self.expand("~")] 376 | 377 | @property 378 | def quoted(self): 379 | """ 380 | Get the quoted string (without surrounding quotes) directly following the node. 381 | If there is no quoted string following, returns just the next keyword, 382 | like .word. 383 | In case there is no single assosiated line one or there are multiple ones, 384 | a KeyError is raised. 385 | """ 386 | self._ensure_scalar() 387 | assert self._line is not None 388 | quote_char = self._line[:1] 389 | try: 390 | if quote_char in ['"', "'"]: 391 | return self._line[1 : self._line.index(quote_char, 1)] 392 | except ValueError: 393 | raise ValueError( 394 | "No ending <%s> found in [%r], line %d: %r" 395 | % (quote_char, self.trace, self._lineno, self._line) 396 | ) 397 | return next(iter(self)) 398 | 399 | @property 400 | def int(self): 401 | """ 402 | Get the single following keyword casted to int. 403 | In case there is no one or there are multiple ones, a KeyError is raised. 404 | """ 405 | return int(self.word) 406 | 407 | @property 408 | def ints(self): 409 | """ 410 | Get the list of all following keywords casted to int. 411 | TypeError may be raised in case there is a keyword that can not be casted. 412 | """ 413 | return [int(x) for x in self] 414 | 415 | @property 416 | def lineno(self): 417 | """ 418 | Get the number of the line in initial config text 419 | KeyError may be raised in case there is no corresponding line 420 | """ 421 | self._ensure_scalar() 422 | return self._lineno 423 | 424 | @property 425 | def junos_list(self): 426 | """ 427 | Get the list of following keywords surrounded in [ ] 428 | If there is no surrounding [ ], return the list of the single keyword that 429 | follows 430 | """ 431 | self._ensure_scalar() 432 | assert self._line is not None 433 | if self._line.startswith("["): 434 | items = self.tail.split() 435 | if items[0] == "[" and items[-1] == "]": 436 | return items[1:-1] 437 | return [self.word] 438 | 439 | if "ip_address" in globals(): 440 | 441 | @property 442 | def ip(self): 443 | """ 444 | Get the single following ip as an IPAddress object. 445 | In case there is no one or there are multiple ones, a KeyError is raised. 446 | TypeError may be raised in case there is a keyword that can not be casted to 447 | the IPAddress type. 448 | """ 449 | return ip_address(self.word) 450 | 451 | @property 452 | def ips(self): 453 | """ 454 | Get the list of all following keywords casted to IPAddress. 455 | TypeError may be raised in case there is a keyword that can not be casted. 456 | """ 457 | return [ip_address(x) for x in self] 458 | 459 | @property 460 | def cidr(self): 461 | """ 462 | Get the single following IP network as an IPNetwork object. 463 | In case there is no one or there are multiple ones, a KeyError is raised. 464 | TypeError may be raised in case there is a keyword that can not be casted to 465 | the IPNetwork type. 466 | """ 467 | return ip_network(self.word) 468 | 469 | @property 470 | def cidrs(self): 471 | """ 472 | Get the list of all following keywords casted to IPNetwork. 473 | TypeError may be raised in case there is a keyword that can not be casted. 474 | """ 475 | return [ip_network(x) for x in self] 476 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape 142 | 143 | # Enable the message, report, category or checker with the given id(s). You can 144 | # either give multiple identifier separated by comma (,) or put this option 145 | # multiple time (only on the command line, not in the configuration file where 146 | # it should appear only once). See also the "--disable" option for examples. 147 | enable=c-extension-no-member 148 | 149 | 150 | [REPORTS] 151 | 152 | # Python expression which should return a score less than or equal to 10. You 153 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 154 | # which contain the number of messages in each category, as well as 'statement' 155 | # which is the total number of statements analyzed. This score is used by the 156 | # global evaluation report (RP0004). 157 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 158 | 159 | # Template used to display messages. This is a python new-style format string 160 | # used to format the message information. See doc for all details. 161 | #msg-template= 162 | 163 | # Set the output format. Available formats are text, parseable, colorized, json 164 | # and msvs (visual studio). You can also give a reporter class, e.g. 165 | # mypackage.mymodule.MyReporterClass. 166 | output-format=text 167 | 168 | # Tells whether to display a full report or only the messages. 169 | reports=no 170 | 171 | # Activate the evaluation score. 172 | score=yes 173 | 174 | 175 | [REFACTORING] 176 | 177 | # Maximum number of nested blocks for function / method body 178 | max-nested-blocks=5 179 | 180 | # Complete name of functions that never returns. When checking for 181 | # inconsistent-return-statements if a never returning function is called then 182 | # it will be considered as an explicit return statement and no message will be 183 | # printed. 184 | never-returning-functions=sys.exit 185 | 186 | 187 | [BASIC] 188 | 189 | # Naming style matching correct argument names. 190 | argument-naming-style=snake_case 191 | 192 | # Regular expression matching correct argument names. Overrides argument- 193 | # naming-style. 194 | #argument-rgx= 195 | 196 | # Naming style matching correct attribute names. 197 | attr-naming-style=snake_case 198 | 199 | # Regular expression matching correct attribute names. Overrides attr-naming- 200 | # style. 201 | #attr-rgx= 202 | 203 | # Bad variable names which should always be refused, separated by a comma. 204 | bad-names=foo, 205 | bar, 206 | baz, 207 | toto, 208 | tutu, 209 | tata 210 | 211 | # Bad variable names regexes, separated by a comma. If names match any regex, 212 | # they will always be refused 213 | bad-names-rgxs= 214 | 215 | # Naming style matching correct class attribute names. 216 | class-attribute-naming-style=any 217 | 218 | # Regular expression matching correct class attribute names. Overrides class- 219 | # attribute-naming-style. 220 | #class-attribute-rgx= 221 | 222 | # Naming style matching correct class names. 223 | class-naming-style=PascalCase 224 | 225 | # Regular expression matching correct class names. Overrides class-naming- 226 | # style. 227 | #class-rgx= 228 | 229 | # Naming style matching correct constant names. 230 | const-naming-style=UPPER_CASE 231 | 232 | # Regular expression matching correct constant names. Overrides const-naming- 233 | # style. 234 | #const-rgx= 235 | 236 | # Minimum line length for functions/classes that require docstrings, shorter 237 | # ones are exempt. 238 | docstring-min-length=-1 239 | 240 | # Naming style matching correct function names. 241 | function-naming-style=snake_case 242 | 243 | # Regular expression matching correct function names. Overrides function- 244 | # naming-style. 245 | #function-rgx= 246 | 247 | # Good variable names which should always be accepted, separated by a comma. 248 | good-names=i, 249 | j, 250 | k, 251 | ex, 252 | Run, 253 | m, c, ip, x 254 | _ 255 | 256 | # Good variable names regexes, separated by a comma. If names match any regex, 257 | # they will always be accepted 258 | good-names-rgxs= 259 | 260 | # Include a hint for the correct naming format with invalid-name. 261 | include-naming-hint=no 262 | 263 | # Naming style matching correct inline iteration names. 264 | inlinevar-naming-style=any 265 | 266 | # Regular expression matching correct inline iteration names. Overrides 267 | # inlinevar-naming-style. 268 | #inlinevar-rgx= 269 | 270 | # Naming style matching correct method names. 271 | method-naming-style=snake_case 272 | 273 | # Regular expression matching correct method names. Overrides method-naming- 274 | # style. 275 | #method-rgx= 276 | 277 | # Naming style matching correct module names. 278 | module-naming-style=snake_case 279 | 280 | # Regular expression matching correct module names. Overrides module-naming- 281 | # style. 282 | #module-rgx= 283 | 284 | # Colon-delimited sets of names that determine each other's naming style when 285 | # the name regexes allow several styles. 286 | name-group= 287 | 288 | # Regular expression which should only match function or class names that do 289 | # not require a docstring. 290 | no-docstring-rgx=^_ 291 | 292 | # List of decorators that produce properties, such as abc.abstractproperty. Add 293 | # to this list to register other decorators that produce valid properties. 294 | # These decorators are taken in consideration only for invalid-name. 295 | property-classes=abc.abstractproperty 296 | 297 | # Naming style matching correct variable names. 298 | variable-naming-style=snake_case 299 | 300 | # Regular expression matching correct variable names. Overrides variable- 301 | # naming-style. 302 | #variable-rgx= 303 | 304 | 305 | [FORMAT] 306 | 307 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 308 | expected-line-ending-format= 309 | 310 | # Regexp for a line that is allowed to be longer than the limit. 311 | ignore-long-lines=^\s*(# )??$ 312 | 313 | # Number of spaces of indent required inside a hanging or continued line. 314 | indent-after-paren=4 315 | 316 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 317 | # tab). 318 | indent-string=' ' 319 | 320 | # Maximum number of characters on a single line. 321 | max-line-length=120 322 | 323 | # Maximum number of lines in a module. 324 | max-module-lines=1000 325 | 326 | # List of optional constructs for which whitespace checking is disabled. `dict- 327 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 328 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 329 | # `empty-line` allows space-only lines. 330 | no-space-check=trailing-comma, 331 | dict-separator 332 | 333 | # Allow the body of a class to be on the same line as the declaration if body 334 | # contains single statement. 335 | single-line-class-stmt=no 336 | 337 | # Allow the body of an if to be on the same line as the test if there is no 338 | # else. 339 | single-line-if-stmt=no 340 | 341 | 342 | [LOGGING] 343 | 344 | # The type of string formatting that logging methods do. `old` means using % 345 | # formatting, `new` is for `{}` formatting. 346 | logging-format-style=old 347 | 348 | # Logging modules to check that the string format arguments are in logging 349 | # function parameter format. 350 | logging-modules=logging 351 | 352 | 353 | [MISCELLANEOUS] 354 | 355 | # List of note tags to take in consideration, separated by a comma. 356 | notes=FIXME, 357 | XXX, 358 | TODO 359 | 360 | # Regular expression of note tags to take in consideration. 361 | #notes-rgx= 362 | 363 | 364 | [SIMILARITIES] 365 | 366 | # Ignore comments when computing similarities. 367 | ignore-comments=yes 368 | 369 | # Ignore docstrings when computing similarities. 370 | ignore-docstrings=yes 371 | 372 | # Ignore imports when computing similarities. 373 | ignore-imports=no 374 | 375 | # Minimum lines number of a similarity. 376 | min-similarity-lines=4 377 | 378 | 379 | [SPELLING] 380 | 381 | # Limits count of emitted suggestions for spelling mistakes. 382 | max-spelling-suggestions=4 383 | 384 | # Spelling dictionary name. Available dictionaries: none. To make it work, 385 | # install the python-enchant package. 386 | spelling-dict= 387 | 388 | # List of comma separated words that should not be checked. 389 | spelling-ignore-words= 390 | 391 | # A path to a file that contains the private dictionary; one word per line. 392 | spelling-private-dict-file= 393 | 394 | # Tells whether to store unknown words to the private dictionary (see the 395 | # --spelling-private-dict-file option) instead of raising a message. 396 | spelling-store-unknown-words=no 397 | 398 | 399 | [STRING] 400 | 401 | # This flag controls whether inconsistent-quotes generates a warning when the 402 | # character used as a quote delimiter is used inconsistently within a module. 403 | check-quote-consistency=no 404 | 405 | # This flag controls whether the implicit-str-concat should generate a warning 406 | # on implicit string concatenation in sequences defined over several lines. 407 | check-str-concat-over-line-jumps=no 408 | 409 | 410 | [TYPECHECK] 411 | 412 | # List of decorators that produce context managers, such as 413 | # contextlib.contextmanager. Add to this list to register other decorators that 414 | # produce valid context managers. 415 | contextmanager-decorators=contextlib.contextmanager 416 | 417 | # List of members which are set dynamically and missed by pylint inference 418 | # system, and so shouldn't trigger E1101 when accessed. Python regular 419 | # expressions are accepted. 420 | generated-members= 421 | 422 | # Tells whether missing members accessed in mixin class should be ignored. A 423 | # mixin class is detected if its name ends with "mixin" (case insensitive). 424 | ignore-mixin-members=yes 425 | 426 | # Tells whether to warn about missing members when the owner of the attribute 427 | # is inferred to be None. 428 | ignore-none=yes 429 | 430 | # This flag controls whether pylint should warn about no-member and similar 431 | # checks whenever an opaque object is returned when inferring. The inference 432 | # can return multiple potential results while evaluating a Python object, but 433 | # some branches might not be evaluated, which results in partial inference. In 434 | # that case, it might be useful to still emit no-member and other checks for 435 | # the rest of the inferred objects. 436 | ignore-on-opaque-inference=yes 437 | 438 | # List of class names for which member attributes should not be checked (useful 439 | # for classes with dynamically set attributes). This supports the use of 440 | # qualified names. 441 | ignored-classes=optparse.Values,thread._local,_thread._local 442 | 443 | # List of module names for which member attributes should not be checked 444 | # (useful for modules/projects where namespaces are manipulated during runtime 445 | # and thus existing member attributes cannot be deduced by static analysis). It 446 | # supports qualified module names, as well as Unix pattern matching. 447 | ignored-modules= 448 | 449 | # Show a hint with possible names when a member name was not found. The aspect 450 | # of finding the hint is based on edit distance. 451 | missing-member-hint=yes 452 | 453 | # The minimum edit distance a name should have in order to be considered a 454 | # similar match for a missing member name. 455 | missing-member-hint-distance=1 456 | 457 | # The total number of similar names that should be taken in consideration when 458 | # showing a hint for a missing member. 459 | missing-member-max-choices=1 460 | 461 | # List of decorators that change the signature of a decorated function. 462 | signature-mutators= 463 | 464 | 465 | [VARIABLES] 466 | 467 | # List of additional names supposed to be defined in builtins. Remember that 468 | # you should avoid defining new builtins when possible. 469 | additional-builtins= 470 | 471 | # Tells whether unused global variables should be treated as a violation. 472 | allow-global-unused-variables=yes 473 | 474 | # List of strings which can identify a callback function by name. A callback 475 | # name must start or end with one of those strings. 476 | callbacks=cb_, 477 | _cb 478 | 479 | # A regular expression matching the name of dummy variables (i.e. expected to 480 | # not be used). 481 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 482 | 483 | # Argument names that match this expression will be ignored. Default to name 484 | # with leading underscore. 485 | ignored-argument-names=_.*|^ignored_|^unused_ 486 | 487 | # Tells whether we should check for unused import in __init__ files. 488 | init-import=no 489 | 490 | # List of qualified module names which can have objects that can redefine 491 | # builtins. 492 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 493 | 494 | 495 | [CLASSES] 496 | 497 | # List of method names used to declare (i.e. assign) instance attributes. 498 | defining-attr-methods=__init__, 499 | __new__, 500 | setUp, 501 | __post_init__ 502 | 503 | # List of member names, which should be excluded from the protected access 504 | # warning. 505 | exclude-protected=_asdict, 506 | _fields, 507 | _replace, 508 | _source, 509 | _make 510 | 511 | # List of valid names for the first argument in a class method. 512 | valid-classmethod-first-arg=cls 513 | 514 | # List of valid names for the first argument in a metaclass class method. 515 | valid-metaclass-classmethod-first-arg=cls 516 | 517 | 518 | [DESIGN] 519 | 520 | # Maximum number of arguments for function / method. 521 | max-args=5 522 | 523 | # Maximum number of attributes for a class (see R0902). 524 | max-attributes=7 525 | 526 | # Maximum number of boolean expressions in an if statement (see R0916). 527 | max-bool-expr=5 528 | 529 | # Maximum number of branch for function / method body. 530 | max-branches=12 531 | 532 | # Maximum number of locals for function / method body. 533 | max-locals=15 534 | 535 | # Maximum number of parents for a class (see R0901). 536 | max-parents=7 537 | 538 | # Maximum number of public methods for a class (see R0904). 539 | max-public-methods=20 540 | 541 | # Maximum number of return / yield for function / method body. 542 | max-returns=6 543 | 544 | # Maximum number of statements in function / method body. 545 | max-statements=50 546 | 547 | # Minimum number of public methods for a class (see R0903). 548 | min-public-methods=2 549 | 550 | 551 | [IMPORTS] 552 | 553 | # List of modules that can be imported at any level, not just the top level 554 | # one. 555 | allow-any-import-level= 556 | 557 | # Allow wildcard imports from modules that define __all__. 558 | allow-wildcard-with-all=no 559 | 560 | # Analyse import fallback blocks. This can be used to support both Python 2 and 561 | # 3 compatible code, which means that the block might have code that exists 562 | # only in one or another interpreter, leading to false positives when analysed. 563 | analyse-fallback-blocks=no 564 | 565 | # Deprecated modules which should not be used, separated by a comma. 566 | deprecated-modules=optparse,tkinter.tix 567 | 568 | # Create a graph of external dependencies in the given file (report RP0402 must 569 | # not be disabled). 570 | ext-import-graph= 571 | 572 | # Create a graph of every (i.e. internal and external) dependencies in the 573 | # given file (report RP0402 must not be disabled). 574 | import-graph= 575 | 576 | # Create a graph of internal dependencies in the given file (report RP0402 must 577 | # not be disabled). 578 | int-import-graph= 579 | 580 | # Force import order to recognize a module as part of the standard 581 | # compatibility libraries. 582 | known-standard-library= 583 | 584 | # Force import order to recognize a module as part of a third party library. 585 | known-third-party=enchant 586 | 587 | # Couples of modules and preferred modules, separated by a comma. 588 | preferred-modules= 589 | 590 | 591 | [EXCEPTIONS] 592 | 593 | # Exceptions that will emit a warning when being caught. Defaults to 594 | # "BaseException, Exception". 595 | overgeneral-exceptions=BaseException, 596 | Exception 597 | --------------------------------------------------------------------------------