Clean up imports of dnspython modules

Add a pylint plugin that enforces:
  - There is no bare `import dns` statement.
  - All `dns.<module>` used are explicitly imported.
  - There are no unused `dns.<module>` imports.

Fix all the imports to conform with this check.
This commit is contained in:
Štěpán Balážik 2026-02-09 19:22:44 +01:00
parent 1d5924c82f
commit d3186c7038
50 changed files with 249 additions and 56 deletions

View file

@ -13,6 +13,7 @@
import time
import dns.message
import dns.rrset
import pytest
from isctest.instance import NamedInstance

View file

@ -18,8 +18,6 @@ import os
import sys
import time
import dns.exception
import dns.message
import dns.name
import dns.rcode
import dns.rdataclass

View file

@ -11,7 +11,7 @@
from re import compile as Re
import dns.message
import dns.rcode
import pytest
import isctest

View file

@ -9,7 +9,8 @@
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
import dns
import dns.rrset
import isctest

View file

@ -11,7 +11,6 @@
from typing import AsyncGenerator
import dns
import dns.rcode
from isctest.asyncserver import (

View file

@ -0,0 +1,177 @@
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
from pylint.checkers import BaseChecker
import astroid
class DnsExplicitImportsChecker(BaseChecker):
name = "dns-explicit-imports"
msgs = {
"W9001": (
"Bare 'import dns' is discouraged; import required submodules explicitly",
"dns-bare-import",
"Emitted when the package root 'dns' is imported directly.",
),
"W9002": (
"Missing explicit import for '%s' (add `import %s`)",
"dns-missing-submodule-import",
"Emitted when code references dns.<...> but the corresponding module prefix "
"was not imported with `import dns.<...>`.",
),
"W9003": (
"Unused explicit import for '%s' (remove `import %s`)",
"dns-unused-submodule-import",
"Emitted when a dns.<...> module is imported explicitly but not used.",
),
}
def __init__(self, linter=None):
super().__init__(linter)
self._imported = {}
self._imported_aliases = set()
self._required = {}
def visit_module(self, node): # pylint: disable=unused-argument
self._imported = {}
self._imported_aliases = set()
self._required = {}
def leave_module(self, node): # pylint: disable=unused-argument
for mod, use_node in sorted(self._required.items()):
if mod in self._imported:
continue
prefix = mod + "."
if any(name.startswith(prefix) for name in self._imported):
continue
self.add_message(
"dns-missing-submodule-import",
node=use_node,
args=(mod, mod),
)
for mod, import_node in sorted(self._imported.items()):
if mod in self._imported_aliases:
continue
if any(
name == mod or name.startswith(mod + ".") for name in self._required
):
continue
self.add_message(
"dns-unused-submodule-import",
node=import_node,
args=(mod, mod),
)
def visit_import(self, node):
for name, _asname in node.names:
if name == "dns":
self.add_message("dns-bare-import", node=node)
continue
if name.startswith("dns."):
self._imported.setdefault(name, node)
if _asname:
self._imported_aliases.add(name)
def visit_importfrom(self, node): # pylint: disable=unused-argument
return
def visit_attribute(self, node):
parent = node.parent
# For `dns.a.b.c`, astroid visits intermediate attributes too.
# Process only the rightmost node to avoid duplicate bookkeeping.
if isinstance(parent, astroid.nodes.Attribute) and parent.expr is node:
return
mod = self._dns_module_for_attribute(node)
if mod is None:
return
self._required.setdefault(mod, node)
@staticmethod
def _dns_attribute_nodes(node):
"""
Return the chain of Attribute nodes as a list.
For `dns.a.b.c`, return the list of Attribute nodes for `dns.a`, `dns.a.b`, and `dns.a.b.c`.
Return None if the chain is not rooted in `dns`.
"""
if not isinstance(node, astroid.nodes.Attribute):
return None
nodes = []
expr = node
while isinstance(expr, astroid.nodes.Attribute):
nodes.append(expr)
expr = expr.expr
if not isinstance(expr, astroid.nodes.Name) or expr.name != "dns":
return None
return list(reversed(nodes))
@classmethod
def _dns_module_for_attribute(cls, node):
"""
For dns.a.b.c, return the longest dns.a.b... prefix that is likely to be a module,
or None if the chain is not rooted in dns.
"""
last_module = None
chain_nodes = cls._dns_attribute_nodes(node)
if chain_nodes is None:
return None
full = "dns." + ".".join(part.attrname for part in chain_nodes)
# Prefer inferred module names to avoid treating classes/constants as
# modules (e.g. `dns.name.NameRelation` should resolve to `dns.name`).
for chain_node in chain_nodes:
inferred = cls._infer_module_name(chain_node)
if inferred is not None and full.startswith(inferred):
last_module = inferred
if last_module is not None:
return last_module
# Fallback when inference is unavailable: assume the terminal segment
# is not a module symbol and require the parent path.
parts = full.split(".")
if len(parts) <= 2:
return full
return ".".join(parts[:-1])
@staticmethod
def _infer_module_name(node):
"""Infer `dns.<module>` for a node; return None if inference is unsure."""
try:
for inferred in node.infer():
if inferred is astroid.util.Uninferable:
continue
# Inference can return either a Module node directly or another
# symbol rooted in a module; normalize both to module name.
module = (
inferred
if isinstance(inferred, astroid.nodes.Module)
else inferred.root()
)
name = module.name
if name.startswith("dns."):
return name
# Inference can fail for dynamic/partial code; fall back gracefully.
except astroid.AstroidError:
pass
return None
def register(linter):
linter.register_checker(DnsExplicitImportsChecker(linter))

View file

@ -17,8 +17,11 @@ import os
from cryptography.hazmat.primitives.asymmetric import ec
from dns.rdtypes.dnskeybase import Flag
import dns
import dns.dnssec
import dns.name
import dns.rdataclass
import dns.rdatatype
import dns.rdtypes.ANY.RRSIG
import dns.zone
import pytest

View file

@ -14,6 +14,7 @@
import os
import re
import dns.rcode
import dns.rrset
import pytest

View file

@ -16,8 +16,8 @@ import struct
import subprocess
import time
import dns
import dns.exception
import dns.message
import dns.name
import dns.rdataclass
import dns.rdatatype

View file

@ -11,7 +11,7 @@
from dns import rdataclass, rdatatype
import dns
import dns.name
import isctest

View file

@ -18,7 +18,6 @@ from filters.common import (
prime_cache,
)
import isctest
import isctest.mark
pytestmark = pytest.mark.extra_artifacts(ARTIFACTS)

View file

@ -10,6 +10,7 @@
# information regarding copyright ownership.
import dns.edns
import dns.flags
import dns.message
import pytest

View file

@ -13,7 +13,7 @@ import glob
import os
import subprocess
import dns
import dns.rcode
import pytest
import isctest

View file

@ -40,7 +40,6 @@ import dns.rdataset
import dns.rdatatype
import dns.rrset
import dns.tsig
import dns.version
import dns.zone
_UdpHandler = Callable[

View file

@ -21,6 +21,7 @@ import dns.edns
import dns.flags
import dns.message
import dns.rcode
import dns.rrset
import dns.zone
import isctest.log

View file

@ -26,7 +26,6 @@ from hypothesis.strategies import (
sampled_from,
)
import dns.message
import dns.name
import dns.rdataclass
import dns.rdatatype

View file

@ -17,6 +17,7 @@ from typing import NamedTuple
import os
import re
import dns.exception
import dns.rcode
import dns.update

View file

@ -20,11 +20,17 @@ import os
import re
import time
import dns
import dns.dnssec
import dns.exception
import dns.message
import dns.name
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.rrset
import dns.tsig
import dns.zone
import dns.zonefile
from isctest.instance import NamedInstance
from isctest.run import EnvCmd

View file

@ -14,8 +14,12 @@ from typing import Any, Callable
import os
import time
import dns.exception
import dns.flags
import dns.message
import dns.query
import dns.rcode
import dns.rdataclass
import isctest.log

View file

@ -9,6 +9,7 @@
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
import dns.rrset
import dns.zone
import pytest

View file

@ -16,7 +16,12 @@ import shutil
import subprocess
import time
import dns
import dns.exception
import dns.name
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.tsig
import dns.update
import pytest

View file

@ -11,6 +11,7 @@
import itertools
import dns.flags
import dns.rrset
import pytest

View file

@ -18,7 +18,6 @@ import pytest
from isctest.vars.algorithms import Algorithm
import isctest
import isctest.mark
pytestmark = pytest.mark.extra_artifacts(
[

View file

@ -14,7 +14,10 @@ from re import compile as Re
import os
import dns
import dns.name
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.update
import pytest

View file

@ -26,7 +26,6 @@ from hypothesis import assume, given
import dns.dnssec
import dns.message
import dns.name
import dns.query
import dns.rcode
import dns.rdataclass
import dns.rdatatype

View file

@ -11,7 +11,9 @@
from datetime import timedelta
import dns
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import pytest
import isctest

View file

@ -12,15 +12,15 @@
import shutil
import time
import dns
import dns.update
import dns.name
import dns.rdataclass
import dns.rdatatype
import pytest
from isctest.vars.algorithms import Algorithm
from nsec3.common import NSEC3_MARK, check_nsec3_case
import isctest
import isctest.mark
pytestmark = NSEC3_MARK

View file

@ -11,7 +11,7 @@
import os
import dns
import dns.rcode
import dns.update
import pytest

View file

@ -12,8 +12,10 @@
import os
import time
import dns
import dns.update
import dns.name
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import pytest
from isctest.vars.algorithms import RSASHA1, Algorithm

View file

@ -11,15 +11,13 @@
import os
import dns
import dns.update
import dns.rdatatype
import pytest
from isctest.vars.algorithms import Algorithm
from nsec3.common import NSEC3_MARK, check_nsec3_case, check_nsec3param
import isctest
import isctest.mark
pytestmark = NSEC3_MARK

View file

@ -13,14 +13,13 @@ from datetime import timedelta
import os
import dns
import dns.update
import dns.rcode
import dns.rdatatype
from isctest.vars.algorithms import RSASHA256
from nsec3.common import NSEC3_MARK, check_auth_nsec3, check_nsec3param
import isctest
import isctest.mark
pytestmark = NSEC3_MARK

View file

@ -16,14 +16,9 @@ import os
import re
import sys
import dns
import dns.exception
import dns.message
import dns.name
import dns.query
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.zone
import pytest
import isctest

View file

@ -13,7 +13,6 @@ information regarding copyright ownership.
from typing import AsyncGenerator
import dns.message
import dns.name
import dns.rcode
import dns.rdatatype

View file

@ -13,7 +13,6 @@ from datetime import timedelta
import os
import dns
import dns.update
from isctest.kasp import Iret, SettimeOptions

View file

@ -13,7 +13,6 @@
import os
import dns
import dns.rcode
import dns.rrset
import pytest

View file

@ -21,11 +21,9 @@ import itertools
from dns.name import Name
import dns
import dns.name
import pytest
import isctest
import isctest.name
# set of properies present in the tested zone - read by tests_zone_analyzer.py

View file

@ -9,7 +9,7 @@
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
import dns
import dns.rcode
import isctest

View file

@ -20,7 +20,6 @@ import signal
import subprocess
import time
import dns
import dns.exception
import pytest

View file

@ -16,8 +16,6 @@ from time import sleep
import os
import dns.message
import dns.query
import dns.rcode
import isctest

View file

@ -12,6 +12,8 @@
import concurrent.futures
import time
import dns.exception
import dns.rcode
import dns.update
import pytest

View file

@ -12,7 +12,7 @@
import os
import dns.message
import dns.rrset
import pytest
import isctest

View file

@ -24,7 +24,9 @@ from hypothesis import assume, example, given
from hypothesis.strategies import ip_addresses
import dns.message
import dns.reversename
import dns.name
import dns.rcode
import dns.rrset
import pytest
from isctest.hypothesis.strategies import dns_names

View file

@ -15,9 +15,11 @@ import socket
import struct
import time
import dns
import dns.flags
import dns.message
import dns.name
import dns.query
import dns.rrset
import pytest
pytestmark = pytest.mark.extra_artifacts(

View file

@ -14,7 +14,6 @@
import socket
import time
import dns
import dns.edns
import dns.message
import dns.name

View file

@ -16,6 +16,7 @@ import time
import dns.message
import dns.query
import dns.tsig
import dns.tsigkeyring
import pytest

View file

@ -20,8 +20,6 @@ import argparse
import struct
import time
import dns
import dns.message
import dns.name
import dns.rdata
import dns.rdataclass

View file

@ -33,11 +33,7 @@ Limitations - untested properties:
from hypothesis import assume, example, given, settings
import dns
import dns.message
import dns.name
import dns.query
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.rrset

View file

@ -18,6 +18,11 @@ import socket
import time
import dns.message
import dns.query
import dns.rdataclass
import dns.rdatatype
import dns.tsig
import dns.zone
import pytest
from isctest.util import param

View file

@ -18,7 +18,7 @@ import shutil
import signal
import time
import dns.message
import dns.zone
import pytest
import isctest

View file

@ -58,6 +58,7 @@ source-roots = [
]
load-plugins = [
"re_compile_checker",
"dns_import_checker",
]
[tool.vulture]
@ -72,6 +73,7 @@ exclude = [
"doc/man/conf.py",
"isctest",
"re_compile_checker.py",
"dns_import_checker.py",
]
ignore_decorators = [
"@pytest.fixture",