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"