From 08de6eba0d83ae57ab802400f7c27bdf6692d264 Mon Sep 17 00:00:00 2001 From: Adam Layne Date: Tue, 9 Jun 2026 14:28:59 -0700 Subject: [PATCH] feat: standard library logging for embedding applications Follow the documented stdlib convention for logging in libraries: attach a NullHandler to the "reqif" package logger and log through per-module logging.getLogger(__name__) loggers. Applications that embed reqif can now attach handlers to the "reqif" logger hierarchy to observe library diagnostics; nothing is emitted by default. Convert the one library-code print() (unknown child tag warning in SpecObjectParser.unparse, previously noqa: T201) to logger.warning. The CLI attaches a stdout handler that renders warnings in the exact format print() produced, so CLI output is unchanged. Recoverable schema errors collected during parsing are logged at DEBUG (their canonical reporting channel remains the bundle's exceptions list, which the validate command prints). CLI commands keep using print() for their own user-facing output. A "Logging" section in the README documents how library users opt in. --- README.md | 31 +++++++++++++++++++++++++++++ reqif/__init__.py | 8 ++++++++ reqif/cli/main.py | 24 ++++++++++++++++++++++ reqif/parser.py | 10 ++++++++++ reqif/parsers/spec_object_parser.py | 5 ++++- 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3aa83b..fe008b8 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,37 @@ with open(output_file_path, "w", encoding="UTF-8") as output_file: The contents of `reqif_xml_output` should be the same as the contents of the `input_file`. +### Logging + +The `reqif` library logs its diagnostic messages (for example, warnings about +unknown tags) through Python's standard +[`logging`](https://docs.python.org/3/howto/logging.html) module, using the +`"reqif"` logger hierarchy. Following the +[standard convention for libraries](https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library), +the library does not configure any handlers itself, so by default an +application that imports `reqif` sees no log output. + +To see the library's warnings on the console, attach a handler to the +`"reqif"` logger: + +```py +import logging + +logging.getLogger("reqif").addHandler(logging.StreamHandler()) +``` + +Any other `logging` configuration works as well, e.g. `logging.basicConfig()` +or a handler that writes the records to a file. To also see the messages that +accompany recoverable schema errors (the errors themselves are collected in +`reqif_bundle.exceptions`), set the logger's level to `DEBUG`: + +```py +logging.getLogger("reqif").setLevel(logging.DEBUG) +``` + +The `reqif` command-line tool attaches its own handler, so its output does +not depend on this configuration. + ## Using ReqIF as a command-line tool After installing the `reqif` Pip package, the `reqif` command becomes available diff --git a/reqif/__init__.py b/reqif/__init__.py index 66298dd..24a2038 100644 --- a/reqif/__init__.py +++ b/reqif/__init__.py @@ -1,5 +1,13 @@ +import logging import os.path __version__ = "0.0.50" +# Follow the standard library convention for logging in libraries: attach a +# NullHandler to the package root logger so that reqif emits no output unless +# the embedding application configures handlers for the "reqif" logger +# hierarchy. +# https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library +logging.getLogger("reqif").addHandler(logging.NullHandler()) + PATH_TO_REQIF_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) diff --git a/reqif/cli/main.py b/reqif/cli/main.py index e2c70c8..422f966 100644 --- a/reqif/cli/main.py +++ b/reqif/cli/main.py @@ -1,3 +1,4 @@ +import logging import os import sys @@ -20,6 +21,17 @@ sys.exit(1) +class _LowercaseLevelFormatter(logging.Formatter): + """ + Renders log records the way the CLI used to print() them, e.g. + "warning: Unknown child tag: FOO." with a lowercase level prefix. + """ + + def format(self, record: logging.LogRecord) -> str: + record.levelname = record.levelname.lower() + return super().format(record) + + def main() -> None: # How to make python 3 print() utf8 # https://stackoverflow.com/a/3597849/598057 @@ -28,6 +40,18 @@ def main() -> None: 1, "w", encoding="utf-8", closefd=False ) + # The reqif library logs through the "reqif" logger hierarchy and emits + # nothing by default. The CLI shows the library's warnings on stdout in + # the same format the CLI printed them before the library switched to + # logging. The handler is constructed after the sys.stdout reassignment + # above so that it writes to the UTF-8-configured stream. + reqif_log_handler = logging.StreamHandler(stream=sys.stdout) + reqif_log_handler.setFormatter( + _LowercaseLevelFormatter("%(levelname)s: %(message)s") + ) + logging.getLogger("reqif").addHandler(reqif_log_handler) + logging.getLogger("reqif").setLevel(logging.WARNING) + parser = create_reqif_args_parser() if parser.is_passthrough_command: diff --git a/reqif/parser.py b/reqif/parser.py index 42ad855..3513929 100644 --- a/reqif/parser.py +++ b/reqif/parser.py @@ -3,6 +3,7 @@ # ruff: noqa: A005 import copy import io +import logging import os import zipfile from collections import OrderedDict, defaultdict @@ -63,6 +64,8 @@ ) from reqif.reqif_bundle import ReqIFBundle, ReqIFZBundle +logger = logging.getLogger(__name__) + class ReqIFParser: @staticmethod @@ -315,6 +318,13 @@ def _parse_reqif_content( spec_relation.target ) except ReqIFMissingTagException as exception: + # The canonical reporting channel for recoverable schema + # errors is the returned bundle's "exceptions" list (the + # validate command prints them from there). Log at DEBUG + # only, so that embedding applications can observe the + # errors as they occur without the CLI reporting each of + # them twice. + logger.debug("%s", exception.get_description()) exceptions.append(exception) # diff --git a/reqif/parsers/spec_object_parser.py b/reqif/parsers/spec_object_parser.py index 13bebc2..7b3e58b 100644 --- a/reqif/parsers/spec_object_parser.py +++ b/reqif/parsers/spec_object_parser.py @@ -1,3 +1,4 @@ +import logging from typing import List, Optional from reqif.models.reqif_spec_object import ( @@ -6,6 +7,8 @@ ) from reqif.parsers.attribute_value_parser import AttributeValueParser +logger = logging.getLogger(__name__) + class SpecObjectParser: @staticmethod @@ -77,7 +80,7 @@ def unparse(spec_object: ReqIFSpecObject) -> str: elif child_tag == "TYPE": output += SpecObjectParser._unparse_spec_object_type(spec_object) else: - print(f"warning: Unknown child tag: {child_tag}.") # noqa: T201 + logger.warning("Unknown child tag: %s.", child_tag) output += " \n"