diff --git a/src/main/python/Pipfile b/src/main/python/Pipfile new file mode 100644 index 0000000..0186711 --- /dev/null +++ b/src/main/python/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +lxml = "*" +pytest = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/src/main/python/Pipfile.lock b/src/main/python/Pipfile.lock new file mode 100644 index 0000000..9ea1c74 --- /dev/null +++ b/src/main/python/Pipfile.lock @@ -0,0 +1,144 @@ +{ + "_meta": { + "hash": { + "sha256": "699760c9e65da6fcbfbb9563e6abf25f96688ab8e2d2d45836da8f5c8d22496e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", + "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" + ], + "markers": "python_version >= '3.6'", + "version": "==22.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "lxml": { + "hashes": [ + "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7", + "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726", + "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03", + "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140", + "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a", + "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05", + "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03", + "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419", + "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4", + "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e", + "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67", + "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50", + "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894", + "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf", + "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947", + "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1", + "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd", + "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3", + "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92", + "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3", + "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457", + "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74", + "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf", + "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1", + "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4", + "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975", + "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5", + "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe", + "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7", + "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1", + "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2", + "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409", + "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f", + "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f", + "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5", + "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24", + "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e", + "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4", + "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a", + "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c", + "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de", + "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f", + "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b", + "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5", + "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7", + "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a", + "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c", + "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9", + "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e", + "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab", + "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941", + "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5", + "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45", + "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7", + "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892", + "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746", + "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c", + "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53", + "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe", + "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184", + "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38", + "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df", + "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9", + "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b", + "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2", + "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0", + "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda", + "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b", + "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5", + "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380", + "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33", + "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8", + "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1", + "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889", + "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9", + "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f", + "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c" + ], + "index": "pypi", + "version": "==4.9.2" + }, + "packaging": { + "hashes": [ + "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", + "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "pytest": { + "hashes": [ + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" + ], + "index": "pypi", + "version": "==7.2.1" + } + }, + "develop": {} +} diff --git a/src/main/python/README.md b/src/main/python/README.md new file mode 100644 index 0000000..2c0245a --- /dev/null +++ b/src/main/python/README.md @@ -0,0 +1,3 @@ +# pydita Python scripts and modules + +These modules and scripts provide general purpose DITA processing. \ No newline at end of file diff --git a/src/main/python/__init__.py b/src/main/python/__init__.py new file mode 100644 index 0000000..9bc5b52 --- /dev/null +++ b/src/main/python/__init__.py @@ -0,0 +1,3 @@ +""" +.. include:: README.md +""" \ No newline at end of file diff --git a/src/main/python/ditalib/README.md b/src/main/python/ditalib/README.md new file mode 100644 index 0000000..bfaf707 --- /dev/null +++ b/src/main/python/ditalib/README.md @@ -0,0 +1,30 @@ +# Python DITA support library + +Provides support for processing DITA content with full key +and DITAVAL awareness. + +Provides the following services: + +* Map resolution: resolve trees of maps into a single in-memory XML document with all submap information preserved and (optionally) with metadata propagated per the DITA 1.3/2.0 rules. +* Key space construction and management: Construct DITA 1.3/2.0 key spaces and make them available for key resolution and key space reporting. +* DITA processing utilities, including DITA @class value checking, topicref type checking (topichead, topicgroup, map reference, etc.), and reference resolution. +* DITAVAL filtering and reporting: Construct DITA filters that can be used to filter DITA elements and report on the details of a filter (conditions, actions, etc.) +* General error collection and reporting facilities beyond Python's built-in logging. +* Generic XML processing utilities, including configuring parsers with Open Toolkit-managed entity resolution libraries. + +## Configuration + +In order to create parsers initialized with an Open Toolkit entity catalog you need to tell ditalib where to find the Open Toolkit, which you can do in several ways: + +* Create a file `.build.properties` in your home directory (`$HOME`) with the entry `dita.ot.dir`: + ``` + dita.ot.dir=${user.home}/ditaot/ditaot-3_7_4 + ``` + + This file can have other properties. +* Set the environment variable `DITA_OT_DIR` with the path to the OT: + ``` + % export DITA_OT_DIR="{HOME}/ditaot/ditaot-3_7_4" + ``` + +* (TBD) Run the ` \ No newline at end of file diff --git a/src/main/python/ditalib/__init__.py b/src/main/python/ditalib/__init__.py new file mode 100644 index 0000000..7b1702c --- /dev/null +++ b/src/main/python/ditalib/__init__.py @@ -0,0 +1,15 @@ +""" +.. include:: README.md +""" + +import os, sys + +# Add the python dir to the import path +# so local module imports will work + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +libDir = os.path.abspath(os.path.join(SCRIPT_DIR, '../..')) +sys.path.append(libDir) + +print('### sys.path:') +print(sys.path) \ No newline at end of file diff --git a/src/main/python/ditalib/config.py b/src/main/python/ditalib/config.py new file mode 100644 index 0000000..5375e33 --- /dev/null +++ b/src/main/python/ditalib/config.py @@ -0,0 +1,53 @@ +"""Manages and provides access to configuration information needed to do DITA processing. +""" + +import os, sys +from io import IOBase + +homeDir: str = os.environ.get("HOME") +buildPropertiesFile: str = ".build.properties" +buildPropertiesPath: str = os.path.join(homeDir, buildPropertiesFile) +otDirProperty: str = "dita.ot.dir" +otDirEnvVariable: str = "DITA_OT_DIR" + +def readPropertiesFile(filePath: str) -> dict[str, str]: + """Reads the specified Java properties file (name=value) into a + dictionary. + + Args: + + filePath (str): Path to the properties file to load. + + Returns: + + dict[str, str]: Dictionary where keys are the property names and values are the property values. + """ + props: dict[str, str] = {} + if os.path.exists(filePath): + f: IOBase = open(filePath,'r') + for line in f.readlines(): + if line.startswith('#'): + continue + (name, value) = line.split('=') + props[name] = value.strip() + f.close() + else: + raise FileNotFoundError(filePath) + return props + +def getDitaOtPath() -> str: + """Gets the configured DITA OT path if it can find it. + + Returns: + + str: The absolute path to the configured DITA OT or None if + the configuration is not found. + """ + + otPath: str = os.environ.get(otDirEnvVariable) + if otPath is None: + properties: dict[str, str] = readPropertiesFile(buildPropertiesPath) + otPath: str = properties.get(otDirProperty) + + return otPath + diff --git a/src/main/python/ditalib/ditacontext.py b/src/main/python/ditalib/ditacontext.py new file mode 100644 index 0000000..3a7ecce --- /dev/null +++ b/src/main/python/ditalib/ditacontext.py @@ -0,0 +1,17 @@ +"""Object that maintains DITA-related context for use in DITA-aware +processing of DITA elements. +""" + +from lxml import etree +from lxml.etree import Element + +from ditalib.logging import Errors + +class DitaContext: + """Provides access to current context needed to do DITA processing. + """ + + def __init__(self): + self._mapcontext: Element = None + self._ditavalFilter: DitavalFilter = DitavalFilter() + self._errors: Errors = Errors() \ No newline at end of file diff --git a/src/main/python/ditalib/logging.py b/src/main/python/ditalib/logging.py new file mode 100644 index 0000000..231bf00 --- /dev/null +++ b/src/main/python/ditalib/logging.py @@ -0,0 +1,83 @@ +"""Error capture and logging utilities beyond built-in logging facilities. +""" + +from typing import Union +from datetime import datetime +from copy import copy, deepcopy + +# Serverity levels: +INFO: str = "info" +ERROR: str = "error" +FATAL: str = "fatal" +WARN: str = "warning" + +class ErrorRecord(): + """Captures the details about an error + """ + + def __init__(self, key: str, err: Exception, severity:str=ERROR,timestamp:datetime=datetime.now()): + """A single error record + + Args: + + key (str): The key for the record, such as a file name. + + err (Exception): The exception that describes the error being recorded. + + severity (str, optional): The error's severity. Defaults to ERROR. + + timestamp (datetime, optional): The time the error occurred. Defaults to datetime.now(). + """ + self._key = key + self._err = Exception + self._severity = severity + self._timestamp: datetime = timestamp + + def getKey(self) -> str: + return self._key + + def getException(self) -> Exception: + return self._err + + def getSeverity(self) -> str: + return self._severity + +class Errors(dict): + """Maintains a dictionary of named things to ErrorRecord objects. + """ + + def __init__(self): + super().__init__() + + def logErrorRecord(self, error: ErrorRecord) -> None: + """Logs an error record. + + Args: + + error (ErrorRecord): The error record to be logged. + """ + errors: list[ErrorRecord] = self.get(error.getKey()) + if errors is None: + errors = [] + self[error.getKey()] = errors + errors.append(error) + + def logError(self, key: str, err: Union[str, Exception], severity="error") -> None: + """Log an error. + + Args: + + key (str): Key to associate with the error, such as filename. + + err (Exception): Message or exception that describes the error being logged. + + severity (str, optional): _description_. Defaults to "error". + """ + exception: Exception = None + if isinstance(err, Exception): + exception = err + else: + exception = Exception(err) + + error: ErrorRecord = ErrorRecord(key, exception, severity=severity) + self.logErrorRecord(error) \ No newline at end of file diff --git a/src/main/python/ditalib/xmlutils.py b/src/main/python/ditalib/xmlutils.py new file mode 100644 index 0000000..d40393c --- /dev/null +++ b/src/main/python/ditalib/xmlutils.py @@ -0,0 +1,47 @@ +"""Utilities for working with XML data: parsers, etc. +""" + +import os, sys +from lxml import etree +from lxml.etree import Element, ElementTree, XMLParser + +import logging +from logging import Logger + +from ditalib import config + +logger: Logger = logging.getLogger("xmlutils") + +# Configure the catalog path for DTD-aware parsing: +ditaOtDir = config.getDitaOtPath() +if ditaOtDir is None: + logger.warn("Failed to get a DITA OT directory from the configuration. Run configure_ditalib.py to correct this.") +else: + catalogPath = os.path.join(ditaOtDir, "catalog-dita.xml") + os.environ["XML_CATALOG_FILES"] = catalogPath + +# NOTE: Per the XMLParser docs, parsers should not be reused and there's +# no performance cost to creating new parsers. + +def getDTDAwareParser() -> XMLParser: + """Gets an XML parser configured for DTD-aware parsing using the + configured XML entity resolution catalog. + + Returns: + + XMLParser: Parser configured to do DTD-aware parsing. + + """ + parser = XMLParser(load_dtd=True, attribute_defaults=True) + return parser + +def getNoDTDParser() -> XMLParser: + """Get a parser that does not do DTD-aware parsing. + + Returns: + + XMLParser: Parser configured to not do DTD-aware parsing. + """ + parser = XMLParser(load_dtd=False, attribute_defaults=False) + return parser + diff --git a/src/main/python/scripts/configure_ditalib.py b/src/main/python/scripts/configure_ditalib.py new file mode 100755 index 0000000..e39b430 --- /dev/null +++ b/src/main/python/scripts/configure_ditalib.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +"""Configure the local ditalib environment + +* Set the location of the DITA Open Toolkit you want to use. +* ??? +""" + +import os +import sys +from io import IOBase + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +libDir = os.path.abspath(os.path.join(SCRIPT_DIR, '..')) +sys.path.append(libDir) + +from lxml import etree +from lxml.etree import XMLParser +from lxml.etree import ElementTree + +from ditalib import config + +print('### sys.path:') +print(sys.path) + +def reportErrorAndExit(msg: str): + print(f'[ERROR] {msg}') + print(f'[ERROR] Check your DITA Open Toolkit installation to make sure it\'s good.') + sys.exit(1) + +print(""" +[INFO] Checking the configuration details for full use of the ditalib modules. +[INFO] +[INFO] In order to do grammar-aware parsing of DITA documents, ditalib needs an +[INFO] entity resolution catalog, which is most easily found in a DITA Open Toolkit. +[INFO] This code checks that your configured Open Toolkit has a parsable catalog file. +[INFO] +""") + +otDirEnvVariable = "DITA_OT_DIR" +otDir: str = os.environ.get(otDirEnvVariable) +homeDir: str = os.environ.get("HOME") +buildPropertiesFile: str = ".build.properties" +buildPropertiesPath: str = os.path.join(homeDir, buildPropertiesFile) +otDirProperty: str = "dita.ot.dir" +saveConfig: bool = False + +if otDir is not None: + print(f'[INFO] Found environment variable {otDirEnvVariable}') + +if otDir is None: + props: dict[str, str] = {} + if os.path.exists(buildPropertiesPath): + f: IOBase = open(buildPropertiesPath,'r') + for line in f.readlines(): + if line.startswith('#'): + continue + (name, value) = line.split('=') + props[name] = value + f.close() + otDir = props.get(otDirProperty) + if otDir is not None: + print(f'[INFO] Found property "{otDirProperty}" in configuration file "{buildPropertiesPath}"') + +if otDir is None: + inputDir = input("Enter the path to the Open Toolkit you want to use (you can use '~'): ") + if inputDir is None or inputDir == "": + print(f'[INFO] No directory entered, quiting.') + sys.exit(0) + otDir = os.path.expanduser(inputDir) + saveConfig = True + +# This should always succeed at this point +if otDir is not None: + print(f'[INFO] DITA Open Toolkit directory is set to "{otDir}"') + +if not os.path.exists(otDir): + reportErrorAndExit(f'Open toolkit directory "{otDir}" not found.') +if os.path.exists(otDir): + print(f'[INFO] Directory "{otDir}" exists, checking Open Toolkit to make sure it\'s usable...') + catalogXmlFile: str = "catalog-dita.xml" + catalogXmlFilePath: str = os.path.join(otDir, catalogXmlFile) + expectedFirstLine: str = '' + try: + f: IOBase = open(catalogXmlFilePath, 'r') + except Exception as err: + reportErrorAndExit(f'Got {err.__class__.__name__} exception "{err}" opening file "{catalogXmlFilePath}') + line: str = f.readline() + if line is None: + reportErrorAndExit(f'Did not find expected file {catalogXmlFile}.') + + if line is not None and line.strip() != expectedFirstLine: + reportErrorAndExit(f'Found expected file {catalogXmlFile} but it did not have expected first line "{expectedFirstLine}".') + f.close() + print(f'[INFO] Found expected {catalogXmlFile} file.') + try: + parsed: ElementTree = etree.parse(catalogXmlFilePath, XMLParser(load_dtd=False, attribute_defaults=False)) + except Exception as err: + reportErrorAndExit(f'Got {err.__class__.__name__} exception "{err}" parsing entity catalog file {catalogXmlFile}') + if parsed is None: + reportErrorAndExit(f'Found {catalogXmlFile} but failed to parse it.') + + print(f'[INFO] Successfully parsed {catalogXmlFile}, should be good to go with this Open Toolkit.') + +if saveConfig: + print(f'[INFO] Saving configuration settings to ~/.build.properties') + lines: list[str] = ['# Python ditalib settings\n', f'{otDirProperty}={otDir}\n'] + if os.path.exists(buildPropertiesPath): + print(f'[INFO] Adding {otDirProperty} entry to "{buildPropertiesPath}"...') + try: + f = open(buildPropertiesPath, 'a') + f.writelines(lines) + f.close() + print(f'[INFO] Configuration file updated.') + except Exception as err: + reportErrorAndExit(f'Got {err.__class__.__name__} exception "{err}" opening file {buildPropertiesPath} for write.') + else: + print(f'[INFO] Creating configuration file "{buildPropertiesPath}"...') + try: + f = open(buildPropertiesPath, 'w') + f.writelines(lines) + f.close() + print(f'[INFO] Configuration file created.') + except Exception as err: + reportErrorAndExit(f'Got {err.__class__.__name__} exception "{err}" opening file {buildPropertiesPath} for write.') + \ No newline at end of file diff --git a/src/main/python/test/__init__.py b/src/main/python/test/__init__.py new file mode 100644 index 0000000..48f0dba --- /dev/null +++ b/src/main/python/test/__init__.py @@ -0,0 +1,8 @@ +import os, sys + +# Add the python dir to the import path +# so local module imports will work + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +libDir = os.path.abspath(os.path.join(SCRIPT_DIR, '..')) +sys.path.append(libDir) diff --git a/src/main/python/test/fixtures.py b/src/main/python/test/fixtures.py new file mode 100644 index 0000000..21526a9 --- /dev/null +++ b/src/main/python/test/fixtures.py @@ -0,0 +1,27 @@ +"""pytest fixtures for unit tests +""" + +import os +import sys +import pytest + +@pytest.fixture +def resourcesDir() -> str: + """Gets the resources directory path for test cases. + + Returns: + + str: The resources directory path + """ + return os.path.join(os.path.dirname(__file__), "resources") + +@pytest.fixture +def rootMap1Path(resourcesDir) -> str: + """The path to root map 1. + + Returns: + + str: path to the root-map-01.ditamap file. + """ + return os.path.join(resourcesDir, "dita", "root_map_1.ditamap") + diff --git a/src/main/python/test/resources/config/test.properties b/src/main/python/test/resources/config/test.properties new file mode 100644 index 0000000..c4a5d22 --- /dev/null +++ b/src/main/python/test/resources/config/test.properties @@ -0,0 +1,6 @@ +# Properties file for testing +prop1=value1 +prop2=value2 +prop3.name=This is a value with spaces +# A comment +prop4-more-name="Value in quotes" diff --git a/src/main/python/test/resources/dita-ot/catalog-dita.xml b/src/main/python/test/resources/dita-ot/catalog-dita.xml new file mode 100644 index 0000000..5169a2c --- /dev/null +++ b/src/main/python/test/resources/dita-ot/catalog-dita.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/main/python/test/resources/dita/root_map_1.ditamap b/src/main/python/test/resources/dita/root_map_1.ditamap new file mode 100644 index 0000000..e5b0c3c --- /dev/null +++ b/src/main/python/test/resources/dita/root_map_1.ditamap @@ -0,0 +1,6 @@ + + + + Root Map 1 + + diff --git a/src/main/python/test/resources/dita/topics/topic-01.dita b/src/main/python/test/resources/dita/topics/topic-01.dita new file mode 100644 index 0000000..d7b77a7 --- /dev/null +++ b/src/main/python/test/resources/dita/topics/topic-01.dita @@ -0,0 +1,8 @@ + + + + Topic 01 + +

First topic in root map 1

+ +
diff --git a/src/main/python/test/test_config.py b/src/main/python/test/test_config.py new file mode 100644 index 0000000..13f7446 --- /dev/null +++ b/src/main/python/test/test_config.py @@ -0,0 +1,40 @@ +"""Test configuration module. +""" + +import os +import sys + +from .fixtures import resourcesDir, rootMap1Path + +from ditalib import config + +def test_readPropertiesFile(resourcesDir): + configPath: str = os.path.join(resourcesDir, "config", "test.properties") + assert os.path.exists(configPath), f'Expected to find test resource "{configPath}"' + props: dict[str, str] = config.readPropertiesFile(configPath) + assert props is not None, f'Exected to get a props dictionary.' + name: str = "prop1" + value: str = props[name] + expected: str = "value1" + assert value == expected, f'Expected value "{expected}" for property "{name}", got "{value}"' + name: str = "prop2" + value: str = props[name] + expected: str = "value2" + assert value == expected, f'Expected value "{expected}" for property "{name}", got "{value}"' + # prop3.name=This is a value with spaces + name: str = "prop3.name" + value: str = props[name] + expected: str = "This is a value with spaces" + assert value == expected, f'Expected value "{expected}" for property "{name}", got "{value}"' + # prop4-more-name="Value in quotes" + name: str = "prop4-more-name" + value: str = props[name] + expected: str = '"Value in quotes"' + assert value == expected, f'Expected value "{expected}" for property "{name}", got "{value}"' + +def test_getDitaOtPath(resourcesDir): + os.environ[config.otDirEnvVariable] = os.path.join(resourcesDir, "dita-ot") + path: str = config.getDitaOtPath() + assert path is not None, f'Expected to get a value for the OT path' + assert os.path.exists(path), f'Expected path "{path}" to exist' + \ No newline at end of file diff --git a/src/main/python/test/test_logging.py b/src/main/python/test/test_logging.py new file mode 100644 index 0000000..d6aeef2 --- /dev/null +++ b/src/main/python/test/test_logging.py @@ -0,0 +1,30 @@ +"""Test configuration module. +""" + +import os +import sys + +from .fixtures import resourcesDir, rootMap1Path + +import ditalib.logging +from ditalib.logging import Errors, ErrorRecord + +def test_readPropertiesFile(resourcesDir): + assert ditalib.logging.WARN == "warning", f'Expected ditalib.logging.WARN to be "warning", got "{ditalib.logging.WARN}"' + assert ditalib.logging.ERROR == "error", f'Expected ditalib.logging.ERROR to be "error", got "{ditalib.logging.ERROR}"' + errors: Errors = Errors() + assert len(errors.keys()) == 0, f'Expected to have an empty dictionary, got {len(errors.key())}.' + key: str = "key1" + errors.logError(key, "Error message 1", severity=ditalib.logging.WARN) + assert len(errors.keys()) == 1, f'Expected to have 1 key, got {len(errors.key())}.' + errors.logError(key, "Error message 2") + assert len(errors.keys()) == 1, f'Expected to have 1 key, got {len(errors.key())}.' + errorsForKey: list[ErrorRecord] = errors.get(key) + assert errorsForKey is not None, f'Expected to get a list of errors for key "{key}"' + assert len(errorsForKey) == 2, f'Expected to have 2 errors in the list, have {len(errorsForKey)}.' + error: ErrorRecord = errorsForKey[0] + assert error.getKey() == key, f'Expected key to be "{key}", got "{error.getKey()}"' + assert error.getSeverity() == ditalib.logging.WARN, f'Expected severity of WARN, got {error.getSeverity()}' + #err: Exception = error.getException() + #assert err is not None, f'Expected to have an Exception object.' + #assert str(err) == "Error message 1", f'Expected exception message "Error message 1", got "{str(err)}"' \ No newline at end of file diff --git a/src/main/python/test/test_xmlutils.py b/src/main/python/test/test_xmlutils.py new file mode 100644 index 0000000..9cf4cc3 --- /dev/null +++ b/src/main/python/test/test_xmlutils.py @@ -0,0 +1,30 @@ +"""Tests for the xmlutils module +""" + + + +from lxml import etree +from lxml.etree import Element, ElementTree, XMLParser + +from ditalib import xmlutils + +from .fixtures import resourcesDir, rootMap1Path + +def test_getDTDAwareParser(rootMap1Path): + parser: XMLParser = xmlutils.getDTDAwareParser() + assert parser is not None, f'Expected to get a parser.' + parsed: ElementTree = etree.parse(rootMap1Path, parser) + assert parsed is not None, f'Expected to get a parsed document' + elem: Element = parsed.getroot() + assert elem is not None, f'Expected to get a root element' + assert elem.get("class") is not None, f'Expected to get a @class attribute. Atts are {elem.attrib}' + +def test_getNoDTDParser(rootMap1Path): + parser: XMLParser = xmlutils.getNoDTDParser() + assert parser is not None, f'Expected to get a parser.' + parsed: ElementTree = etree.parse(rootMap1Path, parser) + assert parsed is not None, f'Expected to get a parsed document' + elem: Element = parsed.getroot() + assert elem is not None, f'Expected to get a root element' + assert elem.get("class") is None, f'Expected to not get a @class attribute. Atts are {elem.attrib}' + \ No newline at end of file