diff --git a/docs/conf.py b/docs/conf.py index 70461062d3..94ed69b319 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -306,9 +306,8 @@ def _load_theme( def generate_tmt_docs(app: Sphinx) -> None: - """ - Run `make generate` to populate the auto-generated documentations - """ + """ Run `make generate` to populate the auto-generated sources """ + conf_dir = Path(app.confdir) subprocess.run(["make", "generate"], cwd=conf_dir) diff --git a/pyproject.toml b/pyproject.toml index 547cc01903..158ca6d153 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -288,6 +288,7 @@ lint.select = [ "W", # pycodestyle "I", # isort "N", # pep8-naming + "D", # pydocstyle "UP", # pyupgrade "B", # flake8-bugbear "C4", # flake8-comprehensions @@ -321,11 +322,42 @@ lint.ignore = [ "PLE1205", # Too many arguments for `logging` format string "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` "RUF013", # PEP 484 prohibits implicit `Optional` + + # pydocstyle + # TODO: some of these will be enabled in their own patches + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in __init__ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D204", # 1 blank line required after class docstring + "D205", # 1 blank line required between summary line and description + "D210", # No whitespaces allowed surrounding docstring text + "D212", # Multi-line docstring summary should start at the first line + "D301", # Use r""" if any backslashes in a docstring + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D415", # First line should end with a period, question mark, or exclamation point ] [tool.ruff.lint.flake8-bugbear] extend-immutable-calls = ["tmt.utils.field"] +[tool.ruff.lint.pydocstyle] +# "The PEP 257 convention includes all D errors apart from: D203, D212, +# D213, D214, D215, D404, D405, D406, D407, D408, D409, D410, D411, D413, +# D415, D416, and D417." +# +# See https://docs.astral.sh/ruff/faq/#does-ruff-support-numpy-or-google-style-docstrings for +# the most up-to-date info. +convention = "pep257" +property-decorators = ["tmt.utils.cached_property"] + [tool.ruff.lint.isort] known-first-party = ["tmt"] diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index d2a73aa799..59a53d270e 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -176,9 +176,7 @@ def test_pickleable_tree() -> None: def test_expand_node_data(monkeypatch) -> None: - """ - Test :py:func:`tmt.base.expand_node_data` handles various forms of variables. - """ + """ :py:func:`tmt.base.expand_node_data` handles various forms of variables """ # From ``_data` and `_expected` we construct lists with items, including # another list and a dictionary, to verify `expand_node_data()` handles diff --git a/tests/unit/test_package_managers.py b/tests/unit/test_package_managers.py index d4262f6db5..1053521f67 100644 --- a/tests/unit/test_package_managers.py +++ b/tests/unit/test_package_managers.py @@ -80,9 +80,7 @@ def has_legacy_dnf(container: ContainerData) -> bool: def has_dnf5_preinstalled(container: ContainerData) -> bool: - """ - Checks whether a container provides ``dnf5``. - """ + """ Checks whether a container provides ``dnf5`` """ return container.image_url_or_id in ( CONTAINER_FEDORA_RAWHIDE.url, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 02fddcb459..d6c244b3dd 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -931,9 +931,7 @@ def test_in_repository( # tmt.utils.wait() & waiting for things to happen # def test_wait_bad_tick(root_logger): - """ - :py:func:`wait` shall raise an exception when invalid ``tick`` is given. - """ + """ :py:func:`wait` shall raise an exception when invalid ``tick`` is given """ with pytest.raises(GeneralError, match='Tick must be a positive integer'): wait(Common(logger=root_logger), lambda: False, datetime.timedelta(seconds=1), tick=-1) diff --git a/tmt/cli.py b/tmt/cli.py index 2232f20250..4e821d3903 100644 --- a/tmt/cli.py +++ b/tmt/cli.py @@ -1994,9 +1994,7 @@ def lint( @main.group(cls=CustomGroup) def setup(**kwargs: Any) -> None: - """ - Setup the environment for working with tmt. - """ + """ Setup the environment for working with tmt """ @setup.group(cls=CustomGroup) @@ -2050,9 +2048,7 @@ def setup_completion(shell: str, install: bool) -> None: '~/.bashrc'. """) def completion_bash(context: Context, install: bool, **kwargs: Any) -> None: - """ - Setup shell completions for bash. - """ + """ Setup shell completions for bash """ setup_completion('bash', install) @@ -2065,9 +2061,7 @@ def completion_bash(context: Context, install: bool, **kwargs: Any) -> None: '~/.zshrc'. """) def completion_zsh(context: Context, install: bool, **kwargs: Any) -> None: - """ - Setup shell completions for zsh. - """ + """ Setup shell completions for zsh """ setup_completion('zsh', install) @@ -2077,7 +2071,5 @@ def completion_zsh(context: Context, install: bool, **kwargs: Any) -> None: '--install', '-i', 'install', is_flag=True, help="Persistently store the script to '~/.config/fish/completions/tmt.fish'.") def completion_fish(context: Context, install: bool, **kwargs: Any) -> None: - """ - Setup shell completions for fish. - """ + """ Setup shell completions for fish """ setup_completion('fish', install) diff --git a/tmt/convert.py b/tmt/convert.py index 733ae1dbe8..36dbadc505 100644 --- a/tmt/convert.py +++ b/tmt/convert.py @@ -56,9 +56,7 @@ def read_manual( disabled: bool, with_script: bool, logger: tmt.log.Logger) -> None: - """ - Reads metadata of manual test cases from Nitrate - """ + """ Reads metadata of manual test cases from Nitrate """ import tmt.export.nitrate nitrate = tmt.export.nitrate.import_nitrate() # Turns off nitrate caching diff --git a/tmt/hardware.py b/tmt/hardware.py index b522fc8028..ea38b49c7a 100644 --- a/tmt/hardware.py +++ b/tmt/hardware.py @@ -81,9 +81,7 @@ class Operator(enum.Enum): - """ - Binary operators defined by specification. - """ + """ Binary operators defined by specification """ EQ = '==' NEQ = '!=' @@ -184,9 +182,7 @@ class Operator(enum.Enum): class ConstraintNameComponents(NamedTuple): - """ - Components of a constraint name. - """ + """ Components of a constraint name """ #: ``disk`` of ``disk[1].size`` name: str @@ -198,9 +194,7 @@ class ConstraintNameComponents(NamedTuple): @dataclasses.dataclass class ConstraintComponents: - """ - Components of a constraint. - """ + """ Components of a constraint """ name: str peer_index: Optional[int] @@ -303,9 +297,7 @@ def not_contains(haystack: list[str], needle: str) -> bool: class ParseError(tmt.utils.MetadataError): - """ - Raised when HW requirement parsing fails. - """ + """ Raised when HW requirement parsing fails """ def __init__(self, constraint_name: str, raw_value: str, message: Optional[str] = None) -> None: @@ -329,9 +321,7 @@ def __init__(self, constraint_name: str, raw_value: str, @dataclasses.dataclass(repr=False) class BaseConstraint(SpecBasedContainer[Spec, Spec]): - """ - Base class for all classes representing one or more constraints. - """ + """ Base class for all classes representing one or more constraints """ @classmethod def from_spec(cls, spec: Any) -> 'BaseConstraint': @@ -394,9 +384,7 @@ def variant(self) -> list['Constraint[Any]']: @dataclasses.dataclass(repr=False) class CompoundConstraint(BaseConstraint): - """ - Base class for all *compound* constraints. - """ + """ Base class for all *compound* constraints """ def __init__( self, @@ -467,9 +455,7 @@ def variants( @dataclasses.dataclass(repr=False) class Constraint(BaseConstraint, Generic[ConstraintValueT]): - """ - A constraint imposing a particular limit to one of the guest properties. - """ + """ A constraint imposing a particular limit to one of the guest properties """ # Name of the constraint. Used for logging purposes, usually matches the # name of the system property. @@ -744,9 +730,7 @@ def from_specification( @dataclasses.dataclass(repr=False) class And(CompoundConstraint): - """ - Represents constraints that are grouped in ``and`` fashion. - """ + """ Represents constraints that are grouped in ``and`` fashion """ def __init__(self, constraints: Optional[list[BaseConstraint]] = None) -> None: """ @@ -802,9 +786,7 @@ def variants( @dataclasses.dataclass(repr=False) class Or(CompoundConstraint): - """ - Represents constraints that are grouped in ``or`` fashion. - """ + """ Represents constraints that are grouped in ``or`` fashion """ def __init__(self, constraints: Optional[list[BaseConstraint]] = None) -> None: """ diff --git a/tmt/steps/__init__.py b/tmt/steps/__init__.py index 0080787d03..42e03a002b 100644 --- a/tmt/steps/__init__.py +++ b/tmt/steps/__init__.py @@ -2084,9 +2084,7 @@ def push( guest: 'Guest', filename_base: Optional[Path] = None, logger: tmt.log.Logger) -> Environment: - """ - Save and push topology to a given guest. - """ + """ Save and push topology to a given guest """ topology_filepaths = self.save(dirpath=dirpath, filename_base=filename_base) diff --git a/tmt/steps/discover/__init__.py b/tmt/steps/discover/__init__.py index fc12018a95..04885a292d 100644 --- a/tmt/steps/discover/__init__.py +++ b/tmt/steps/discover/__init__.py @@ -141,9 +141,7 @@ def download_distgit_source( ) def log_import_plan_details(self) -> None: - """ - Log details about the imported plan - """ + """ Log details about the imported plan """ parent = cast(Optional[tmt.steps.discover.Discover], self.parent) if parent and parent.plan._original_plan and \ parent.plan._original_plan._remote_plan_fmf_id: @@ -155,9 +153,7 @@ def log_import_plan_details(self) -> None: self.verbose(f'import {key}', value, 'green') def post_dist_git(self, created_content: list[Path]) -> None: - """ - Discover tests after dist-git applied patches - """ + """ Discover tests after dist-git applied patches """ pass diff --git a/tmt/steps/execute/__init__.py b/tmt/steps/execute/__init__.py index 1f109dbc04..8038eb1a62 100644 --- a/tmt/steps/execute/__init__.py +++ b/tmt/steps/execute/__init__.py @@ -527,9 +527,7 @@ def prepare_tests(self, guest: Guest, logger: tmt.log.Logger) -> list[TestInvoca return invocations def prepare_scripts(self, guest: "tmt.steps.provision.Guest") -> None: - """ - Prepare additional scripts for testing - """ + """ Prepare additional scripts for testing """ # Install all scripts on guest for script in self.scripts: source = SCRIPTS_SRC_DIR / script.path.name @@ -590,9 +588,7 @@ def load_tmt_report_results(self, invocation: TestInvocation) -> list["tmt.Resul note=note)] def load_custom_results(self, invocation: TestInvocation) -> list["tmt.Result"]: - """ - Process custom results.yaml file created by the test itself. - """ + """ Process custom results.yaml file created by the test itself """ test, guest = invocation.test, invocation.guest custom_results_path_yaml = invocation.test_data_path / 'results.yaml' @@ -783,9 +779,7 @@ def run_checks_after_test( class Execute(tmt.steps.Step): - """ - Run tests using the specified executor. - """ + """ Run tests using the specified executor """ # Internal executor is the default implementation DEFAULT_HOW = 'tmt' diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 8e2cf16184..77234a2078 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -406,6 +406,8 @@ def _query_package_manager( def _query_has_selinux(self, guest: 'Guest') -> Optional[bool]: """ + Detect whether guest uses SELinux. + For detection ``/proc/filesystems`` is used, see ``man 5 filesystems`` for details. """ @@ -1090,9 +1092,7 @@ def push(self, destination: Optional[Path] = None, options: Optional[list[str]] = None, superuser: bool = False) -> None: - """ - Push files to the guest - """ + """ Push files to the guest """ raise NotImplementedError @@ -1101,9 +1101,7 @@ def pull(self, destination: Optional[Path] = None, options: Optional[list[str]] = None, extend_options: Optional[list[str]] = None) -> None: - """ - Pull files from the guest - """ + """ Pull files from the guest """ raise NotImplementedError diff --git a/tmt/steps/report/polarion.py b/tmt/steps/report/polarion.py index 3f5f61f011..d2bc4e1dc5 100644 --- a/tmt/steps/report/polarion.py +++ b/tmt/steps/report/polarion.py @@ -183,9 +183,7 @@ class ReportPolarionData(tmt.steps.report.ReportStepData): @tmt.steps.provides_method('polarion') class ReportPolarion(tmt.steps.report.ReportPlugin[ReportPolarionData]): - """ - Write test results into an xUnit file and upload to Polarion. - """ + """ Write test results into an xUnit file and upload to Polarion """ _data_class = ReportPolarionData diff --git a/tmt/steps/report/reportportal.py b/tmt/steps/report/reportportal.py index c75a20c0e3..6bf830b1f0 100644 --- a/tmt/steps/report/reportportal.py +++ b/tmt/steps/report/reportportal.py @@ -224,9 +224,7 @@ class ReportReportPortal(tmt.steps.report.ReportPlugin[ReportReportPortalData]): } def handle_response(self, response: requests.Response) -> None: - """ - Check the endpoint response and raise an exception if needed. - """ + """ Check the endpoint response and raise an exception if needed """ if not response.ok: raise tmt.utils.ReportError( @@ -236,9 +234,7 @@ def handle_response(self, response: requests.Response) -> None: self.debug("Message from the endpoint", response.text) def check_options(self) -> None: - """ - Write warning if there might be caused an unexpected behaviour by the option combinations - """ + """ Check options for known troublesome combinations """ # TODO: Update restriction of forbidden option combinations based on feedback. if self.data.launch_per_plan and self.data.suite_per_plan: diff --git a/tmt/utils.py b/tmt/utils.py index 6f4a4ed4e8..c16d4e403a 100644 --- a/tmt/utils.py +++ b/tmt/utils.py @@ -3149,9 +3149,7 @@ def default(cls, key: str, default: Any = None) -> Any: # def inject_to(self, obj: Any) -> None: - """ - Inject keys from this container into attributes of a given object - """ + """ Inject keys from this container into attributes of a given object """ for name, value in self.items(): setattr(obj, name, value) @@ -4194,9 +4192,7 @@ def inject_auth_git_url(url: str) -> str: def clonable_git_url(url: str) -> str: - """ - Modify the git repo url so it can be cloned - """ + """ Modify the git repo url so it can be cloned """ url = rewrite_git_url(url, CLONABLE_GIT_URL_PATTERNS) return inject_auth_git_url(url) @@ -4289,9 +4285,7 @@ def run(command: Command) -> str: class TimeoutHTTPAdapter(requests.adapters.HTTPAdapter): - """ - Spice up request's session with custom timeout. - """ + """ Spice up request's session with custom timeout """ def __init__(self, *args: Any, **kwargs: Any) -> None: self.timeout = kwargs.pop('timeout', None) @@ -4354,9 +4348,8 @@ def increment( # ignore[type-arg]: base class is a generic class, but we cannot list # its parameter type, because in Python 3.6 the class "is not subscriptable". class retry_session(contextlib.AbstractContextManager): # type: ignore[type-arg] # noqa: N801 - """ - Context manager for requests.Session() with retries and timeout - """ + """ Context manager for :py:class:`requests.Session` with retries and timeout """ + @staticmethod def create( retries: int = DEFAULT_RETRY_SESSION_RETRIES, @@ -4745,8 +4738,6 @@ class StructuredField: these single-line items. Note that the section cannot contain both plain text data and key-value pairs. - Example: - .. code-block:: python field = qe.StructuredField() @@ -4813,6 +4804,7 @@ class StructuredField: print field.get("hardware", "hostrequire") ['hypervisor=', 'labcontroller=lab.example.com'] + """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -5653,9 +5645,7 @@ def _prenormalize_fmf_node(node: fmf.Tree, schema_name: str, logger: tmt.log.Log import tmt.steps def _process_step(step_name: str, step: dict[Any, Any]) -> None: - """ - Process a single step configuration. - """ + """ Process a single step configuration """ # If `how` is set, don't touch it, and there's nothing to do. if 'how' in step: @@ -5686,9 +5676,7 @@ def _process_step(step_name: str, step: dict[Any, Any]) -> None: step['how'] = step_class.DEFAULT_HOW def _process_step_collection(step_name: str, step_collection: Any) -> None: - """ - Process a collection of step configurations. - """ + """ Process a collection of step configurations """ # Ignore anything that is not a step. if step_name not in tmt.steps.STEPS: @@ -6255,8 +6243,7 @@ def _iter_key_annotations(cls) -> Iterator[tuple[str, Any]]: keys declared by the class itself, all following the order in which keys were defined in their respective classes. - Yields: - pairs of key name and its annotations. + :yields: pairs of key name and its annotations. """ def _iter_class_annotations(klass: type) -> Iterator[tuple[str, Any]]: @@ -6285,8 +6272,7 @@ def keys(cls) -> Iterator[str]: keys declared by the class itself, all following the order in which keys were defined in their respective classes. - Yields: - key names. + :yields: key names. """ for keyname, _ in cls._iter_key_annotations(): @@ -6300,8 +6286,7 @@ def items(self) -> Iterator[tuple[str, Any]]: keys declared by the class itself, all following the order in which keys were defined in their respective classes. - Yields: - pairs of key name and its value. + :yields: pairs of key name and its value. """ # SIM118 Use `{key} in {dict}` instead of `{key} in {dict}.keys(). # "Type[SerializableContainerDerivedType]" has no attribute "__iter__" (not iterable) @@ -7082,7 +7067,8 @@ def retry( *args: Any, **kwargs: Any ) -> T: - """ Retry functionality to be used elsewhere in the code. + """ + Retry functionality to be used elsewhere in the code. :param func: function to be called with all unclaimed positional and keyword arguments. @@ -7107,9 +7093,7 @@ def retry( def get_url_content(url: str) -> str: - """ - Get content of a given URL as a string. - """ + """ Get content of a given URL as a string """ try: with retry_session() as session: response = session.get(url) @@ -7124,9 +7108,7 @@ def get_url_content(url: str) -> str: def is_url(url: str) -> bool: - """ - Check if the given string is a valid URL. - """ + """ Check if the given string is a valid URL """ parsed = urllib.parse.urlparse(url) return bool(parsed.scheme and parsed.netloc)