diff --git a/README.md b/README.md index 2424ff313..60736d2fa 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,6 @@ Install with See [docs/docker-instructions.md](#docs/docker-instructions.md) for how to build a docker image. - Known issues ------------ @@ -150,13 +149,13 @@ Known issues master](https://github.com/RDFLib/rdflib) if you need to serialise to turtle. - Attributions and credits ------------------------ EMMOntoPy is maintained by [EMMC-ASBL](https://emmc.eu/). So far it has mainly been developed by [SINTEF](https://www.sintef.no/). ### Contributing projects + - [EMMC-CSA](https://emmc.info/about-emmc-csa/); Grant Agreement No: 723867 diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 331b20267..0103c4efe 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -29,7 +29,12 @@ asstring, read_catalog, infer_version, convert_imported ) from ontopy.utils import ( - FMAP, OWLREADY2_FORMATS, isinteractive, ReadCatalogError + FMAP, + IncompatibleVersion, + isinteractive, + OWLREADY2_FORMATS, + ReadCatalogError, + _validate_installed_version, ) from ontopy.ontograph import OntoGraph # FIXME: deprecate... @@ -490,6 +495,24 @@ def save(self, filename=None, format=None, overwrite=False, **kwargs): if not format: format = guess_format(filename, fmap=FMAP) + if ( + not _validate_installed_version( + package="rdflib", min_version="6.0.0" + ) + and format == FMAP.get("ttl", "") + ): + from rdflib import __version__ as __rdflib_version__ + + warnings.warn( + IncompatibleVersion( + "To correctly convert to Turtle format, rdflib must be " + "version 6.0.0 or greater, however, the detected rdflib " + "version used by your Python interpreter is " + f"{__rdflib_version__!r}. For more information see the " + "'Known issues' section of the README." + ) + ) + if format in OWLREADY2_FORMATS: revmap = {v: k for k, v in FMAP.items()} super().save(file=filename, format=revmap[format], **kwargs) diff --git a/ontopy/utils.py b/ontopy/utils.py index 735a2e227..48612f81d 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -6,7 +6,9 @@ import datetime import tempfile import types +from typing import TYPE_CHECKING import urllib.request +import warnings import xml.etree.ElementTree as ET from rdflib import Graph, URIRef @@ -15,6 +17,11 @@ import owlready2 +if TYPE_CHECKING: + from packaging.version import Version, LegacyVersion + from typing import Union + + # Format mappings: file extension -> rdflib format name FMAP = { 'n3': 'ntriples', @@ -28,6 +35,16 @@ OWLREADY2_FORMATS = 'rdfxml', 'owl', 'xml', 'ntriples' +class IncompatibleVersion(Warning): + """An installed dependency version may be incompatible with a functionality + of this package - or rather an outcome of a functionality. + This is not critical, hence this is only a warning.""" + + +class UnknownVersion(Exception): + """Cannot retrieve version from a package.""" + + def isinteractive(): """Returns true if we are running from an interactive interpreater, false otherwise.""" @@ -324,6 +341,55 @@ def write_catalog(mappings, output='catalog-v001.xml'): f.write('\n'.join(s) + '\n') +def _validate_installed_version( + package: str, min_version: "Union[str, Version, LegacyVersion]" +) -> bool: + """Validate an installed package. + + Examine whether a minimum version is installed in the used Python + interpreter for a specific package. + + Parameters: + package: The package to be investigated as a string, e.g., `"rdflib"`. + min_version: The minimum version expected to be installed. + + Raises: + UnknownVersion: If the supplied package does not have a `__version__` + attribute. + + Returns: + Whether or not the installed version is equal to or greater than the + `min_version`. + + """ + import importlib + from packaging.version import ( + parse as parse_version, LegacyVersion, Version + ) + + if isinstance(min_version, str): + min_version = parse_version(min_version) + elif isinstance(min_version, (LegacyVersion, Version)): + # We have the format we want + pass + else: + raise TypeError( + "min_version should be either a str, LegacyVersion or Version. " + "The latter classes being from the packaging.version module." + ) + + installed_package = importlib.import_module( + name=".", package=package + ) + installed_package_version = getattr(installed_package, "__version__", None) + if not installed_package_version: + raise UnknownVersion( + f"Cannot retrieve version information from package {package!r}." + ) + + return parse_version(installed_package_version) >= min_version + + def convert_imported(input, output, input_format=None, output_format='xml', url_from_catalog=None, catalog_file='catalog-v001.xml'): """Convert imported ontologies. @@ -331,6 +397,11 @@ def convert_imported(input, output, input_format=None, output_format='xml', Store the output in a directory structure matching the source files. This require catalog file(s) to be present. + Warning: + To convert to Turtle (`.ttl`) format, you must have installed + `rdflib>=6.0.0`. See [Known issues](../README.md#Known-issues) in the + README for more information. + Args: input: input ontology file name output: output ontology file path. The directory part of `output` @@ -395,6 +466,23 @@ def recur(graph, outext): # Write output files fmt = input_format if input_format else guess_format(input, fmap=FMAP) + + if ( + not _validate_installed_version(package="rdflib", min_version="6.0.0") + and (output_format == FMAP.get("ttl", "") or outext == "ttl") + ): + from rdflib import __version__ as __rdflib_version__ + + warnings.warn( + IncompatibleVersion( + "To correctly convert to Turtle format, rdflib must be " + "version 6.0.0 or greater, however, the detected rdflib " + "version used by your Python interpreter is " + f"{__rdflib_version__!r}. For more information see the " + "'Known issues' section of the README." + ) + ) + g = Graph() g.parse(input, format=fmt) g.serialize(destination=output, format=output_format) @@ -410,6 +498,12 @@ def squash_imported(input, output, input_format=None, output_format='xml', only be used if it exists in the same directory as the input file. The the squash rdflib graph is returned. + + Warning: + To convert to Turtle (`.ttl`) format, you must have installed + `rdflib>=6.0.0`. See [Known issues](../README.md#Known-issues) in the + README for more information. + """ inroot = os.path.dirname(os.path.abspath(input)) @@ -440,6 +534,27 @@ def recur(g): graph.parse(input, format=input_format) recur(graph) if output: + if ( + not _validate_installed_version( + package="rdflib", min_version="6.0.0" + ) + and ( + output_format == FMAP.get("ttl", "") + or os.path.splitext(output)[1] == "ttl" + ) + ): + from rdflib import __version__ as __rdflib_version__ + + warnings.warn( + IncompatibleVersion( + "To correctly convert to Turtle format, rdflib must be " + "version 6.0.0 or greater, however, the detected rdflib " + "version used by your Python interpreter is " + f"{__rdflib_version__!r}. For more information see the " + "'Known issues' section of the README." + ) + ) + graph.serialize(destination=output, format=output_format) return graph diff --git a/requirements.txt b/requirements.txt index 54487189e..1ac60b397 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -Cython>=0.29.21 -Owlready2>=0.28,!=0.32,!=0.34 -graphviz>=0.16 -PyYAML>=5.4.1 -blessings>=1.7 -Pygments>=2.7.4 -rdflib~=4.2.1 -semver>=2.8.1 -pydot>=1.4.1 +Cython>=0.29.21,<0.30 +Owlready2>=0.28,<0.35,!=0.32,!=0.34 +graphviz>=0.16,<0.17 +PyYAML>=5.4.1,<6 +blessings>=1.7,<2 +Pygments>=2.7.4,<3 +rdflib>=4.2.1,<7 +semver>=2.8.1,<3 +pydot>=1.4.1,<2 diff --git a/tools/ontoconvert b/tools/ontoconvert index 253e8d363..8ae027e8e 100755 --- a/tools/ontoconvert +++ b/tools/ontoconvert @@ -2,10 +2,18 @@ """Converts file format of input ontology and write it to output file(s). """ import argparse +import os +import warnings from rdflib.util import Graph, guess_format -from ontopy.utils import convert_imported, squash_imported +from ontopy.utils import ( + convert_imported, + FMAP, + IncompatibleVersion, + squash_imported, + _validate_installed_version, +) from ontopy.factpluspluswrapper.factppgraph import FaCTPPGraph @@ -64,28 +72,54 @@ def main(): output_format = 'xml' # Perform conversion - if args.recursive: - convert_imported(args.input, args.output, - input_format=input_format, - output_format=output_format, - url_from_catalog=args.url_from_catalog) - elif args.inferred: - g = squash_imported(args.input, None, - input_format=input_format) - fg = FaCTPPGraph(g) - if args.base_iri: - fg.base_iri = args.base_iri - g2 = fg.inferred_graph() - g2.serialize(destination=args.output, format=output_format) - elif args.squash: - squash_imported(args.input, args.output, - input_format=input_format, - output_format=output_format, - url_from_catalog=args.url_from_catalog) - else: - g = Graph() - g.parse(args.input, format=input_format) - g.serialize(destination=args.output, format=output_format) + with warnings.catch_warnings(record=True) as warnings_handle: + warnings.simplefilter("always") + if args.recursive: + convert_imported(args.input, args.output, + input_format=input_format, + output_format=output_format, + url_from_catalog=args.url_from_catalog) + elif args.inferred: + g = squash_imported(args.input, None, + input_format=input_format) + fg = FaCTPPGraph(g) + if args.base_iri: + fg.base_iri = args.base_iri + g2 = fg.inferred_graph() + g2.serialize(destination=args.output, format=output_format) + elif args.squash: + squash_imported(args.input, args.output, + input_format=input_format, + output_format=output_format, + url_from_catalog=args.url_from_catalog) + else: + if ( + not _validate_installed_version( + package="rdflib", min_version="6.0.0" + ) + and ( + output_format == FMAP.get("ttl", "") + or os.path.splitext(args.output)[1] == "ttl" + ) + ): + from rdflib import __version__ as __rdflib_version__ + + warnings.warn( + IncompatibleVersion( + "To correctly convert to Turtle format, rdflib must be" + " version 6.0.0 or greater, however, the detected " + "rdflib version used by your Python interpreter is " + f"{__rdflib_version__!r}. For more information see the" + " 'Known issues' section of the README." + ) + ) + + g = Graph() + g.parse(args.input, format=input_format) + g.serialize(destination=args.output, format=output_format) + + for warning in warnings_handle: + print(f"\033[93mWARNING\033[0m: [{warning.category.__name__}] {warning.message}") if __name__ == '__main__':