From d8bc035e7b5b71f9f61e288bcb71d148bf86dda5 Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 12 Jan 2025 12:27:11 +1300 Subject: [PATCH 1/5] Use `colour` checks configuration. --- .gitignore | 34 +- .pre-commit-config.yaml | 22 +- CONTRIBUTING.rst | 4 +- LICENSE | 2 +- README.rst | 2 +- colour_clf_io/__init__.py | 119 +++--- colour_clf_io/elements.py | 260 ++++++++----- colour_clf_io/errors.py | 8 +- colour_clf_io/parsing.py | 255 ++++++++----- colour_clf_io/process_list.py | 48 ++- colour_clf_io/process_nodes.py | 357 +++++++++++------- colour_clf_io/tests/conftest.py | 24 +- colour_clf_io/tests/test_clf_common.py | 101 ++++- colour_clf_io/tests/test_clf_parsing.py | 469 +++++++++++++++--------- colour_clf_io/values.py | 54 +-- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/requirements.txt | 21 +- pyproject.toml | 145 +++----- requirements.txt | 178 +++++++++ tasks.py | 57 +-- utilities/export_todo.py | 6 +- utilities/unicode_to_ascii.py | 4 +- 23 files changed, 1386 insertions(+), 788 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 360e0ee..9c42ef2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,24 @@ +# Common Files *.egg-info *.pyc *.pyo .DS_Store .coverage* -.fleet -.idea -.ipynb_checkpoints -.vs -.vscode -.sandbox +uv.lock -__pycache__ +# Common Directories +.fleet/ +.idea/ +.ipynb_checkpoints/ +.python-version +.vs/ +.vscode/ +.sandbox/ +build/ +dist/ +docs/_build/ +docs/generated/ +node_modules/ +references/ -build -dist -docs/_build -docs/_static/Basics_*.png -docs/_static/Examples_*.png -docs/_static/Plotting_*.png -docs/_static/Tutorial_*.png -docs/generated -poetry.lock -references +__pycache__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 354f8a5..5b884e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,12 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v4.5.0" + rev: "v5.0.0" hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: check-yaml - exclude: config-aces-reference.ocio.yaml - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending @@ -16,35 +15,30 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell - args: ["--ignore-words-list=co-ordinates,exitance,fro,hart,ist"] - exclude: "BIBLIOGRAPHY.bib|CONTRIBUTORS.rst" - - repo: https://github.com/ikamensh/flynt - rev: "1.0.1" - hooks: - - id: flynt - args: [--verbose] + args: ["--ignore-words-list=socio-economic"] + exclude: "BIBLIOGRAPHY.bib|CONTRIBUTORS.rst|.*.ipynb" - repo: https://github.com/PyCQA/isort rev: "5.13.2" hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.1.14" + rev: "v0.8.2" hooks: - id: ruff-format - id: ruff + args: [--fix] - repo: https://github.com/adamchainz/blacken-docs - rev: 1.16.0 + rev: 1.19.1 hooks: - id: blacken-docs language_version: python3.10 - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v3.1.0" + rev: "v4.0.0-alpha.8" hooks: - id: prettier - exclude: config-aces-reference.ocio.yaml - repo: https://github.com/pre-commit/pygrep-hooks rev: "v1.10.0" hooks: diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e91f868..35ffccd 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -9,7 +9,7 @@ If you would like to contribute to **Colour**, please refer to the following gui About ----- -| **Colour** by Colour Developers -| Copyright 2013 Colour Developers – `colour-developers@colour-science.org `__ +| **Colour - CLF IO** by Colour Developers +| Copyright 2024 Colour Developers – `colour-developers@colour-science.org `__ | This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause | `https://github.com/colour-science/colour `__ diff --git a/LICENSE b/LICENSE index a70f873..f769d9e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2013 Colour Developers +Copyright 2024 Colour Developers Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.rst b/README.rst index c16d80a..7205525 100644 --- a/README.rst +++ b/README.rst @@ -167,6 +167,6 @@ About ----- | **Colour - CLF IO** by Colour Developers -| Copyright 2015 Colour Developers – `colour-developers@colour-science.org `__ +| Copyright 2024 Colour Developers – `colour-developers@colour-science.org `__ | This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause | `https://github.com/colour-science/colour-clf-io `__ diff --git a/colour_clf_io/__init__.py b/colour_clf_io/__init__.py index b4ad410..5e68010 100644 --- a/colour_clf_io/__init__.py +++ b/colour_clf_io/__init__.py @@ -18,55 +18,17 @@ from __future__ import annotations -__application_name__ = "Colour - CLF IO" - -__major_version__ = "0" -__minor_version__ = "0" -__change_version__ = "0" -__version__ = ".".join((__major_version__, __minor_version__, __change_version__)) +import typing +if typing.TYPE_CHECKING: + from pathlib import Path -# Security issues in lxml should be addressed and no longer be a concern: +# NOTE: Security issues in lxml should be addressed and no longer be a concern: # https://discuss.python.org/t/status-of-defusedxml-and-recommendation-in-docs/34762/6 - -__author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" -__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" -__maintainer__ = "Colour Developers" -__email__ = "colour-developers@colour-science.org" -__status__ = "Production" - -__all__ = [ - "read_clf", - "parse_clf", - "LUT1D", - "LUT3D", - "ProcessNode", - "ProcessList", - "Matrix", - "Range", - "Exponent", - "ExponentStyle", - "ExponentParams", - "ASC_CDL", - "ASC_CDL_Style", - "SatNode", - "SOPNode", - "Interpolation1D", - "Interpolation3D", - "BitDepth", - "Channel", - "CalibrationInfo", - "Info", - "LogParams", - "LogStyle", - "RangeStyle", - "Log", -] - import lxml.etree from .elements import ( + Array, CalibrationInfo, ExponentParams, ExponentStyle, @@ -88,10 +50,61 @@ ProcessNode, Range, ) -from .values import ASC_CDL_Style, BitDepth, Channel, Interpolation1D, Interpolation3D +from .values import ( + ASC_CDL_Style, + BitDepth, + Channel, + Interpolation1D, + Interpolation3D, +) +__author__ = "Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "Array", + "CalibrationInfo", + "ExponentParams", + "ExponentStyle", + "Info", + "LogParams", + "LogStyle", + "RangeStyle", + "SatNode", + "SOPNode", +] +__all__ += ["ProcessList"] +__all__ += [ + "ASC_CDL", + "LUT1D", + "LUT3D", + "Exponent", + "Log", + "Matrix", + "ProcessNode", + "Range", +] +__all__ += [ + "ASC_CDL_Style", + "BitDepth", + "Channel", + "Interpolation1D", + "Interpolation3D", +] + +__application_name__ = "Colour - CLF IO" -def read_clf(path) -> ProcessList: +__major_version__ = "0" +__minor_version__ = "1" +__change_version__ = "0" +__version__ = f"{__major_version__}.{__minor_version__}.{__change_version__}" + + +def read_clf(path: str | Path) -> ProcessList | None: """ Read given *CLF* file and return the resulting `ProcessList`. @@ -108,15 +121,15 @@ def read_clf(path) -> ProcessList: ------ :class: ParsingError If the given file does not contain a valid CLF document. - """ - xml = lxml.etree.parse(path) # noqa: S320 + + xml = lxml.etree.parse(str(path)) # noqa: S320 xml_process_list = xml.getroot() - root = ProcessList.from_xml(xml_process_list) - return root + + return ProcessList.from_xml(xml_process_list) -def parse_clf(text): +def parse_clf(text: str | bytes) -> ProcessList | None: """ Read given string as a *CLF* document and return the resulting `ProcessList`. @@ -133,8 +146,8 @@ def parse_clf(text): ------ :class: ParsingError If the given string does not contain a valid CLF document. - """ + xml = lxml.etree.fromstring(text) # noqa: S320 - root = ProcessList.from_xml(xml) - return root + + return ProcessList.from_xml(xml) diff --git a/colour_clf_io/elements.py b/colour_clf_io/elements.py index ab40b4a..4423921 100644 --- a/colour_clf_io/elements.py +++ b/colour_clf_io/elements.py @@ -4,16 +4,19 @@ Defines objects that hold data from elements contained in a CLF document. These typically are child elements of Process Nodes. - """ from __future__ import annotations import enum +import typing from dataclasses import dataclass -import numpy.typing as npt -from typing_extensions import Self +if typing.TYPE_CHECKING: + import numpy.typing as npt + +if typing.TYPE_CHECKING: + import lxml.etree from colour_clf_io.errors import ParsingError from colour_clf_io.parsing import ( @@ -22,6 +25,7 @@ child_element, child_element_or_exception, map_optional, + must_have, retrieve_attributes, retrieve_attributes_as_float, three_floats, @@ -29,7 +33,7 @@ from colour_clf_io.values import Channel __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" @@ -56,44 +60,61 @@ class Array(XMLParsable): References ---------- - https://docs.acescentral.com/specifications/clf/#array + - https://docs.acescentral.com/specifications/clf/#array """ values: list[float] dim: tuple[int, ...] - @classmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: # noqa: ARG003 + @staticmethod + def from_xml( + xml: lxml.etree._Element | None, + config: ParserConfig, # noqa: ARG004 + ) -> Array | None: """ - Parse and return the Array from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.Array` class instance from the + given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.Array` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. - + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None - dim = tuple(map(int, xml.get("dim").split())) - values = list(map(float, xml.text.split())) - return cls(values=values, dim=dim) - def as_array(self) -> npt.ArrayLike: + dim = xml.get("dim") + must_have( + xml, + 'Array must have a "dim" attribute', + ) + + dimensions = tuple(map(int, dim.split())) # pyright: ignore + values = list(map(float, xml.text.split())) # pyright: ignore + + return Array(values=values, dim=dimensions) + + def as_array(self) -> npt.NDArray: """ Convert the CLF element into a numpy array. Returns ------- - :class:`numpy.ndarray` + :class:`numpy`ndarray`` Array of shape `dim` with the data from `values`. """ + import numpy as np dim = self.dim @@ -110,7 +131,7 @@ class CalibrationInfo(XMLParsable): References ---------- - https://docs.acescentral.com/specifications/clf/#processlist + - https://docs.acescentral.com/specifications/clf/#processlist """ display_device_serial_num: str | None @@ -121,25 +142,34 @@ class CalibrationInfo(XMLParsable): calibration_software_name: str | None calibration_software_version: str | None - @classmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: # noqa: ARG003 + @staticmethod + def from_xml( + xml: lxml.etree._Element | None, + config: ParserConfig, # noqa: ARG004 + ) -> CalibrationInfo | None: """ - Parse and return the Calibration Info from the given XML node. Returns None - if the given element is None. + Parse and return a :class:`colour_clf_io.CalibrationInfo` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.CalibrationInfo` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. - + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + attributes = retrieve_attributes( xml, { @@ -152,7 +182,8 @@ def from_xml(cls, xml, config: ParserConfig) -> Self | None: # noqa: ARG003 "calibration_software_version": "CalibrationSoftwareVersion", }, ) - return cls(**attributes) + + return CalibrationInfo(**attributes) class RangeStyle(enum.Enum): @@ -161,7 +192,7 @@ class RangeStyle(enum.Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#range + - https://docs.acescentral.com/specifications/clf/#range """ CLAMP = "Clamp" @@ -174,7 +205,7 @@ class LogStyle(enum.Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#processList + - https://docs.acescentral.com/specifications/clf/#processList """ LOG_10 = "log10" @@ -193,7 +224,7 @@ class ExponentStyle(enum.Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#exponent + - https://docs.acescentral.com/specifications/clf/#exponent """ BASIC_FWD = "basicFwd" @@ -215,35 +246,45 @@ class SOPNode(XMLParsable): References ---------- - https://docs.acescentral.com/specifications/clf/#asc_cdl + - https://docs.acescentral.com/specifications/clf/#asc_cdl """ slope: tuple[float, float, float] offset: tuple[float, float, float] power: tuple[float, float, float] - @classmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: + @staticmethod + def from_xml( + xml: lxml.etree._Element | None, config: ParserConfig + ) -> SOPNode | None: """ - Parse and return the SOPNode from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.SOPNode` class instance from the + given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.SOPNode` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + slope = three_floats(child_element_or_exception(xml, "Slope", config).text) offset = three_floats(child_element_or_exception(xml, "Offset", config).text) power = three_floats(child_element_or_exception(xml, "Power", config).text) - return cls(slope=slope, offset=offset, power=power) + + return SOPNode(slope=slope, offset=offset, power=power) @dataclass @@ -253,34 +294,47 @@ class SatNode(XMLParsable): References ---------- - https://docs.acescentral.com/specifications/clf/#asc_cdl + - https://docs.acescentral.com/specifications/clf/#asc_cdl """ saturation: float - @classmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: + @staticmethod + def from_xml( + xml: lxml.etree._Element | None, config: ParserConfig + ) -> SatNode | None: """ - Parse and return the SatNode from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.SatNode` class instance from the + given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.SatNode` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + saturation = child_element_or_exception(xml, "Saturation", config).text if saturation is None: - raise ParsingError("Saturation node in SatNode contains no value.") + exception = "Saturation node in SatNode contains no value." + + raise ParsingError(exception) + saturation = float(saturation) - return cls(saturation=saturation) + + return SatNode(saturation=saturation) @dataclass @@ -290,7 +344,7 @@ class Info(XMLParsable): References ---------- - https://docs.acescentral.com/specifications/clf/#processList + - https://docs.acescentral.com/specifications/clf/#processList """ app_release: str | None @@ -300,25 +354,31 @@ class Info(XMLParsable): aces_user_name: str | None calibration_info: CalibrationInfo | None - @classmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: + @staticmethod + def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Info | None: """ - Parse and return the Info from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.Info` class instance from the + given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.Info` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. - + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + attributes = retrieve_attributes( xml, { @@ -330,9 +390,11 @@ def from_xml(cls, xml, config: ParserConfig) -> Self | None: }, ) calibration_info = CalibrationInfo.from_xml( - child_element(xml, "CalibrationInfo", config), config + child_element(xml, "CalibrationInfo", config), # pyright: ignore + config, ) - return cls(calibration_info=calibration_info, **attributes) + + return Info(calibration_info=calibration_info, **attributes) @dataclass @@ -342,7 +404,7 @@ class LogParams(XMLParsable): References ---------- - https://docs.acescentral.com/specifications/clf/#log + - https://docs.acescentral.com/specifications/clf/#log """ base: float | None @@ -354,24 +416,34 @@ class LogParams(XMLParsable): linear_slope: float | None channel: Channel | None - @classmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: # noqa: ARG003 + @staticmethod + def from_xml( + xml: lxml.etree._Element | None, + config: ParserConfig, # noqa: ARG004 + ) -> LogParams | None: """ - Parse and return the Log Param from the given XML node. Returns None if the - given element is None. + Parse and return a :class:`colour_clf_io.LogParams` class instance from + the given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.LogParams` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + attributes = retrieve_attributes_as_float( xml, { @@ -387,7 +459,7 @@ def from_xml(cls, xml, config: ParserConfig) -> Self | None: # noqa: ARG003 channel = map_optional(Channel, xml.get("channel")) - return cls(channel=channel, **attributes) + return LogParams(channel=channel, **attributes) @dataclass @@ -397,31 +469,41 @@ class ExponentParams(XMLParsable): References ---------- - https://docs.acescentral.com/specifications/clf/#exponent + - https://docs.acescentral.com/specifications/clf/#exponent """ exponent: float offset: float | None channel: Channel | None - @classmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: # noqa: ARG003 + @staticmethod + def from_xml( + xml: lxml.etree._Element | None, + config: ParserConfig, # noqa: ARG004 + ) -> ExponentParams | None: """ - Parse and return the Exponent Params from the given XML node. Returns None if - the given element is None. + Parse and return a :class:`colour_clf_io.ExponentParams` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.ExponentParams` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + attributes = retrieve_attributes_as_float( xml, { @@ -430,8 +512,12 @@ def from_xml(cls, xml, config: ParserConfig) -> Self | None: # noqa: ARG003 }, ) exponent = attributes.pop("exponent") + if exponent is None: - raise ParsingError("Exponent process node has no `exponent' value.") + exception = "Exponent process node has no `exponent' value." + + raise ParsingError(exception) + channel = map_optional(Channel, xml.get("channel")) - return cls(channel=channel, exponent=exponent, **attributes) + return ExponentParams(channel=channel, exponent=exponent, **attributes) diff --git a/colour_clf_io/errors.py b/colour_clf_io/errors.py index 1eb8c69..62e586c 100644 --- a/colour_clf_io/errors.py +++ b/colour_clf_io/errors.py @@ -3,18 +3,22 @@ ====== Defines errors that are used as part of the parsing and validation of CLF documents. - """ from __future__ import annotations __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" __status__ = "Production" +__all__ = [ + "ParsingError", + "ValidationError", +] + class ParsingError(Exception): """ diff --git a/colour_clf_io/parsing.py b/colour_clf_io/parsing.py index d4d062a..0cd856e 100644 --- a/colour_clf_io/parsing.py +++ b/colour_clf_io/parsing.py @@ -3,56 +3,58 @@ ======= Defines utilities that are used to parse CLF documents. - """ from __future__ import annotations import collections -import xml.etree -import xml.etree.ElementTree +import typing from abc import ABC, abstractmethod from dataclasses import dataclass from itertools import islice -from typing import Callable, TypeVar +from typing import TypeGuard, TypeVar + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Iterable + from typing import Any -from typing_extensions import Self, TypeGuard +if typing.TYPE_CHECKING: + import lxml.etree from colour_clf_io.errors import ParsingError __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" __status__ = "Production" -__ALL__ = [ +__all__ = [ + "NAMESPACE_NAME", "ParserConfig", "XMLParsable", - "fully_qualified_name", "map_optional", "retrieve_attributes", "retrieve_attributes_as_float", - "must_have", + "check_none", "child_element", "child_elements", "child_element_or_exception", "element_as_text", + "element_as_float", "elements_as_text_list", "sliding_window", "three_floats", - "element_as_float", ] -_T = TypeVar("_T") - -NAMESPACE_NAME = "urn:AMPAS:CLF:v3.0" +NAMESPACE_NAME: str = "urn:AMPAS:CLF:v3.0" @dataclass class ParserConfig: - """Additional settings for parsing the CLF document. + """ + Additional settings for parsing the CLF document. Parameters ---------- @@ -64,17 +66,19 @@ class ParserConfig: namespace_name: str | None = NAMESPACE_NAME def clf_namespace_prefix_mapping(self) -> dict[str, str] | None: - """Return the namespaces prefix mapping used for CLF documents. + """ + Return the namespaces prefix mapping used for CLF documents. Returns ------- - :class:`dict[str, str]` that contains the namespaces prefix mappings. - + :class:`dict[str, str]` or :py:data:`None` + Dictionary that contain the namespaces prefix mappings. """ + if self.namespace_name: return {"clf": self.namespace_name} - else: - return None + + return None class XMLParsable(ABC): @@ -89,9 +93,11 @@ class XMLParsable(ABC): - :meth:`~colour_lf_io.parsing.XMLParsable.from_xml` """ - @classmethod + @staticmethod @abstractmethod - def from_xml(cls, xml, config: ParserConfig) -> Self | None: + def from_xml( + xml: lxml.etree._Element | None, config: ParserConfig + ) -> XMLParsable | None: """ Parse an object of this class from the given XML object. @@ -104,39 +110,41 @@ def from_xml(cls, xml, config: ParserConfig) -> Self | None: Returns ------- - An instance of the parsed object, or :py:data:`None` if parsing failed. - + :class:`colour_clf_io.parsing.XMLParsable` or :py:data:`None` + Parsed object or ``None`` if parsing failed. """ -def map_optional(f: Callable, value): +def map_optional(function: Callable, value: Any | None) -> Any: """ - Apply `f` to value, if `value` is not :py:data:`None`. + Apply ``function`` to value, if ``value`` is not ``None``. Parameters ---------- - f + function The function to apply. value - The value (that might be :py:data:`None`) + The value to apply the function onto Returns ------- - The result of applying `f` to `value`, or :py:data:`None`. - + :class:`object` or :py:data:`None` + The result of applying ``function`` to ``value``. """ + if value is not None: - return f(value) + return function(value) + return None def retrieve_attributes( - xml, attribute_mapping: dict[str, str] + xml: lxml.etree._Element, attribute_mapping: dict[str, str] ) -> dict[str, str | None]: """ - Take a dictionary of keys and attribute names and map the attribute names to the - corresponding values from the given XML element. Note that the keys of the - attribute mapping are not used in any way. + Take a dictionary of keys and attribute names and map the attribute names + to the corresponding values from the given XML element. Note that the keys + of the attribute mapping are not used in any way. Parameters ---------- @@ -149,15 +157,15 @@ def retrieve_attributes( ------- :class:`dict[str, str | None]` The resulting dictionary of keys and attribute values. - """ + return { k: xml.get(attribute_name) for k, attribute_name in attribute_mapping.items() } def retrieve_attributes_as_float( - xml, attribute_mapping: dict[str, str] + xml: lxml.etree._Element, attribute_mapping: dict[str, str] ) -> dict[str, float | None]: """ Take a dictionary of keys and attribute names and map the attribute names to the @@ -177,22 +185,25 @@ def retrieve_attributes_as_float( ------- :class:`dict[str, float | None]` The resulting dictionary of keys and attribute values. - """ + attributes = retrieve_attributes(xml, attribute_mapping) - def as_float(value): - if value is None: - return None + def as_float(value: Any) -> float | None: + """Convert given value to float.""" + try: return float(value) - except ValueError: + except (ValueError, TypeError): return None return {key: as_float(value) for key, value in attributes.items()} -def must_have(value: _T | None, message) -> TypeGuard[_T]: +T = TypeVar("T") + + +def check_none(value: T | None, message: str) -> TypeGuard[T]: """ Assert that `value` is not :py:data:`None`. @@ -210,16 +221,17 @@ def must_have(value: _T | None, message) -> TypeGuard[_T]: Returns ------- :class:`TypeGuard` - """ + if value is None: raise ParsingError(message) + return True def child_element( - xml, name, config: ParserConfig, xpath_function="" -) -> xml.etree.ElementTree.Element | None | str: + xml: lxml.etree._Element, name: str, config: ParserConfig, xpath_function: str = "" +) -> lxml.etree._Element | str | None: """ Return a named child element of the given XML element. @@ -239,25 +251,28 @@ def child_element( :class:`xml.etree.ElementTree.Element` or :class`str` or :py:data:`None` The found child element, or the result of the applied XPath function. :py:data:`None` if the child was not found. - """ elements = child_elements(xml, name, config, xpath_function) element_count = len(elements) + if element_count == 0: return None - elif element_count == 1: + + if element_count == 1: return elements[0] - else: - raise ParsingError( - f"Found multiple elements of type {name} in " - f"element {xml}, but only expected exactly one." - ) + + exception = ( + f"Found multiple elements of type {name} in " + f"element {xml}, but only expected exactly one." + ) + + raise ParsingError(exception) def child_elements( - xml, name, config: ParserConfig, xpath_function="" -) -> list[xml.etree.ElementTree.Element] | list[str]: + xml: lxml.etree._Element, name: str, config: ParserConfig, xpath_function: str = "" +) -> list[lxml.etree._Element] | list[str]: """ Return all child elements with a given name of an XML element. @@ -274,11 +289,11 @@ def child_elements( Returns ------- - :class:`xml.etree.ElementTree.Element` or :class`str` or :py:data:`None` + :class:`xml.etree.ElementTree.Element` or :class`str` The found child element, or the result of the applied XPath function. :py:data:`None` if the child was not found. - """ + if config.clf_namespace_prefix_mapping(): elements = xml.xpath( f"clf:{name}{xpath_function}", @@ -286,15 +301,16 @@ def child_elements( ) else: elements = xml.xpath(f"{name}{xpath_function}") - return elements + + return elements # pyright: ignore def child_element_or_exception( - xml, name, config: ParserConfig -) -> xml.etree.ElementTree.Element: + xml: lxml.etree._Element, name: str, config: ParserConfig +) -> lxml.etree._Element: """ - Return a named child element of the given XML element, or raise an exception if no - such child element is found. + Return a named child element of the given XML element, or raise an exception + if no such child element is found. Parameters ---------- @@ -316,17 +332,26 @@ def child_element_or_exception( :class:`xml.etree.ElementTree.Element` The found child element. """ + element = child_element(xml, name, config) - assert not isinstance(element, str) # noqa: S101 + + if isinstance(element, str): + exception = f'Element "{element}" cannot be a string!' + + raise TypeError(exception) + if element is None: - raise ParsingError( + exception = ( f"Tried to retrieve child element '{name}' from '{xml}' but child was " "not present." ) + + raise ParsingError(exception) + return element -def element_as_text(xml, name, config: ParserConfig) -> str: +def element_as_text(xml: lxml.etree._Element, name: str, config: ParserConfig) -> str: """ Convert a named child of the given XML element to its text value. @@ -342,18 +367,21 @@ def element_as_text(xml, name, config: ParserConfig) -> str: Returns ------- :class:`str` - The text value of the child element. If the child element is not present and - empty string is returned. - + The text value of the child element. If the child element is not present + an empty string is returned. """ + text = child_element(xml, name, config, xpath_function="/text()") + if text is None: return "" - else: - return str(text) + + return str(text) -def element_as_float(xml, name, config: ParserConfig) -> float | None: +def element_as_float( + xml: lxml.etree._Element, name: str, config: ParserConfig +) -> float | None: """ Convert a named child of the given XML element to its float value. @@ -368,24 +396,28 @@ def element_as_float(xml, name, config: ParserConfig) -> float | None: Returns ------- - :class:`float` - The value of the child element as float. If the child element is not or an - invalid float representation, :py:data:`None` is returned. - + :class:`float` or :py:data:`None` + The value of the child element as float. If the child element is not or + an invalid float representation, ``None`` is returned. """ + text = child_element(xml, name, config, xpath_function="/text()") + if text is None: return None - else: - try: - return float(str(text)) - except ValueError: - return None + + try: + return float(str(text)) + except ValueError: + return None -def elements_as_text_list(xml, name, config: ParserConfig): +def elements_as_text_list( + xml: lxml.etree._Element, name: str, config: ParserConfig +) -> list[str]: """ - Return one or more child elements of the given XML element as a list of strings. + Return one or more child elements of the given XML element as a list of + strings. Parameters ---------- @@ -399,23 +431,39 @@ def elements_as_text_list(xml, name, config: ParserConfig): Returns ------- :class:`list` of :class:`str` - A list of string, where each string corresponds to the text representation of - a child element. - + A list of string, where each string corresponds to the text + representation of a child element. """ + if config.clf_namespace_prefix_mapping(): - return xml.xpath( + return xml.xpath( # pyright: ignore f"clf:{name}/text()", namespaces=config.clf_namespace_prefix_mapping() ) - else: - return xml.xpath(f"{name}/text()") + + return xml.xpath(f"{name}/text()") # pyright: ignore -def sliding_window(iterable, n): +def sliding_window(iterable: Iterable, n: int) -> Iterable: """ Collect data into overlapping fixed-length chunks or blocks. - Source: https://docs.python.org/3/library/itertools.html + + Parameters + ---------- + iterable + Iterable to collect the data from + n + Chunk size + + Returns + ------- + Generator + Chunk generator. + + References + ---------- + - https://docs.python.org/3/library/itertools.html """ + it = iter(iterable) window = collections.deque(islice(it, n - 1), maxlen=n) for x in it: @@ -423,31 +471,36 @@ def sliding_window(iterable, n): yield tuple(window) -def three_floats(s: str | None) -> tuple[float, float, float]: +def three_floats(text: str | None) -> tuple[float, float, float]: """ - Parse the given value as a comma separated list of floating point values. + Parse the given text as a comma separated list of floating point values. Parameters ---------- - s + text String to parse. Raises ------ :class:`ParsingError` - If `s` is :py:data:`None`, or cannot be parsed as three floats. + If `text` is :py:data:`None`, or cannot be parsed as three floats. Returns ------- :class:`tuple` of :class:`float` Three floating point values. - """ - if s is None: - raise ParsingError(f"Failed to parse three float values from {s}") - parts = s.split() + + if text is None: + exception = f"Failed to parse three float values from {text}" + + raise ParsingError(exception) + + parts = text.split() + if len(parts) != 3: - raise ParsingError(f"Failed to parse three float values from {s}") - values = tuple(map(float, parts)) - # Repacking here to satisfy type check. - return values[0], values[1], values[2] + exception = f"Failed to parse three float values from {text}" + + raise ParsingError(exception) + + return float(parts[0]), float(parts[1]), float(parts[2]) diff --git a/colour_clf_io/process_list.py b/colour_clf_io/process_list.py index 5812ca9..ae7ccb9 100644 --- a/colour_clf_io/process_list.py +++ b/colour_clf_io/process_list.py @@ -3,7 +3,6 @@ ============ Defines the top level Process List object that represents a CLF process. - """ from __future__ import annotations @@ -28,13 +27,13 @@ ) __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" __status__ = "Production" -__ALL__ = ["ProcessList"] +__all__ = ["ProcessList"] @dataclass @@ -44,7 +43,7 @@ class ProcessList: References ---------- - https://docs.acescentral.com/specifications/clf/#processList + - https://docs.acescentral.com/specifications/clf/#processList """ id: str @@ -61,30 +60,37 @@ class ProcessList: info: Info | None @staticmethod - def from_xml(xml): + def from_xml(xml: lxml.etree._Element | None) -> ProcessList | None: """ - Parse and return the Process List from the given XML node. Returns None if the - given element is None. + Parse and return a :class:`colour_clf_io.ProcessList` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.ProcessList` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. - + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None - id = xml.get("id") # noqa: A001 - must_have(id, "ProcessList must contain an `id` attribute") + + id_ = xml.get("id") + must_have(id_, "ProcessList must contain an `id` attribute") + compatible_clf_version = xml.get("compCLFversion") must_have( compatible_clf_version, - "ProcessList must contain an `compCLFversion` attribute", + 'ProcessList must contain a "compCLFversion" attribute', ) # By default, we would expect the correct namespace as per the specification. @@ -95,9 +101,9 @@ def from_xml(xml): if not namespace: config.namespace_name = None elif namespace != config.namespace_name: - raise ParsingError( - f"Found invalid xmlns attribute in process list: {namespace}" - ) + exception = f"Found invalid xmlns attribute in process list: {namespace}" + + raise ParsingError(exception) name = xml.get("name") inverse_of = xml.get("inverseOf") @@ -111,16 +117,18 @@ def from_xml(xml): process_nodes = filter( lambda node: lxml.etree.QName(node).localname not in ignore_nodes, xml ) + if not process_nodes: warn("Got empty process node.") + process_nodes = [ parse_process_node(xml_node, config) for xml_node in process_nodes ] assert_bit_depth_compatibility(process_nodes) return ProcessList( - id=id, - compatible_CLF_version=compatible_clf_version, + id=id_, # pyright: ignore + compatible_CLF_version=compatible_clf_version, # pyright: ignore process_nodes=process_nodes, name=name, inverse_of=inverse_of, diff --git a/colour_clf_io/process_nodes.py b/colour_clf_io/process_nodes.py index 2ad15d0..5eea963 100644 --- a/colour_clf_io/process_nodes.py +++ b/colour_clf_io/process_nodes.py @@ -3,14 +3,17 @@ ============ Defines the available process nodes in a CLF document. - """ from __future__ import annotations +import typing from abc import ABC from dataclasses import dataclass +if typing.TYPE_CHECKING: + from collections.abc import Callable + import lxml.etree from colour_clf_io.elements import ( @@ -42,8 +45,19 @@ Interpolation3D, ) -__ALL__ = [ +__author__ = "Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "PROCESSING_NODE_CONSTRUCTORS", + "register_process_node_xml_constructor", "ProcessNode", + "assert_bit_depth_compatibility", + "parse_process_node", "LUT1D", "LUT3D", "Matrix", @@ -53,23 +67,28 @@ "ASC_CDL", ] -processing_node_constructors = {} +PROCESSING_NODE_CONSTRUCTORS: dict = {} +""" +Hold the processing node constructors. +""" -def register_process_node_xml_constructor(name): +def register_process_node_xml_constructor(name: str) -> Callable: """ - Add the constructor method to the `processing_node_constructors` dictionary. - Adds the wrapped function as value with the given name as key. + Add the constructor method to the :attr:`PROCESSING_NODE_CONSTRUCTORS` + dictionary. Adds the wrapped function as value with the given name as key. Parameters ---------- name Name to use as key for adding. - """ - def register(constructor): - processing_node_constructors[name] = constructor + def register(constructor: Callable) -> Callable: + """Register the given callable.""" + + PROCESSING_NODE_CONSTRUCTORS[name] = constructor + return constructor return register @@ -82,7 +101,7 @@ class ProcessNode(XMLParsable, ABC): References ---------- - https://docs.acescentral.com/specifications/clf/#processNode + - https://docs.acescentral.com/specifications/clf/#processNode """ id: str | None @@ -92,7 +111,7 @@ class ProcessNode(XMLParsable, ABC): description: str | None @staticmethod - def parse_attributes(xml, config: ParserConfig) -> dict: + def parse_attributes(xml: lxml.etree._Element, config: ParserConfig) -> dict: """ Parse the default attributes of a *ProcessNode* and return them as a dictionary of names and their values. @@ -108,8 +127,8 @@ def parse_attributes(xml, config: ParserConfig) -> dict: ------- :class:`dict` *dict* of attribute names and their values. - """ + attributes = retrieve_attributes( xml, { @@ -120,13 +139,13 @@ def parse_attributes(xml, config: ParserConfig) -> dict: in_bit_depth = BitDepth(xml.get("inBitDepth")) out_bit_depth = BitDepth(xml.get("outBitDepth")) description = element_as_text(xml, "Description", config) - args = { + + return { "in_bit_depth": in_bit_depth, "out_bit_depth": out_bit_depth, "description": description, **attributes, } - return args def assert_bit_depth_compatibility(process_nodes: list[ProcessNode]) -> bool: @@ -135,50 +154,55 @@ def assert_bit_depth_compatibility(process_nodes: list[ProcessNode]) -> bool: Examples -------- - ``` >>> from colour_clf_io.process_nodes import assert_bit_depth_compatibility, LUT1D >>> from colour_clf_io.elements import Array - >>> lut = Array(values=[0,1], dim=(2,1)) - >>> node_i8 = LUT1D( \ - id=None, \ - name=None, \ - description=None, \ - half_domain=False, \ - raw_halfs=False, \ - interpolation = None, \ - array=lut, \ - in_bit_depth=BitDepth.i8, \ - out_bit_depth=BitDepth.i8 ) - >>> node_f16 = LUT1D( \ - id=None, \ - name=None, \ - description=None, \ - half_domain=False, \ - raw_halfs=False, \ - interpolation = None, \ - array=lut, \ - in_bit_depth=BitDepth.f16, \ - out_bit_depth=BitDepth.f16 ) + >>> lut = Array(values=[0, 1], dim=(2, 1)) + >>> node_i8 = LUT1D( + ... id=None, + ... name=None, + ... description=None, + ... half_domain=False, + ... raw_halfs=False, + ... interpolation=None, + ... array=lut, + ... in_bit_depth=BitDepth.i8, + ... out_bit_depth=BitDepth.i8, + ... ) + >>> node_f16 = LUT1D( + ... id=None, + ... name=None, + ... description=None, + ... half_domain=False, + ... raw_halfs=False, + ... interpolation=None, + ... array=lut, + ... in_bit_depth=BitDepth.f16, + ... out_bit_depth=BitDepth.f16, + ... ) >>> assert_bit_depth_compatibility([node_i8, node_i8]) True >>> assert_bit_depth_compatibility( - ... [node_i8, node_f16]) # doctest: +IGNORE_EXCEPTION_DETAIL + ... [node_i8, node_f16] + ... ) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... ValidationError: ... - ``` """ + for node_a, node_b in sliding_window(process_nodes, 2): is_compatible = node_a.out_bit_depth == node_b.in_bit_depth if not is_compatible: - raise ValidationError( + exception = ( f"Encountered incompatible bit depth between two processing nodes: " f"{node_a} and {node_b}" ) + + raise ValidationError(exception) + return True -def parse_process_node(xml, config: ParserConfig): +def parse_process_node(xml: lxml.etree._Element, config: ParserConfig) -> ProcessNode: """ Return the correct process node that corresponds to this XML element. @@ -191,14 +215,18 @@ def parse_process_node(xml, config: ParserConfig): ------ :class: ParsingError If the given element does not match any valid process node, or the node does not - correctly correspond to the specification.. - + correctly correspond to the specification. """ + tag = lxml.etree.QName(xml).localname - constructor = processing_node_constructors.get(tag) + constructor = PROCESSING_NODE_CONSTRUCTORS.get(tag) + if constructor is not None: - return processing_node_constructors[tag](xml, config) - raise ParsingError(f"Encountered invalid processing node with tag '{xml.tag}'") + return PROCESSING_NODE_CONSTRUCTORS[tag](xml, config) + + exception = f"Encountered invalid processing node with tag '{xml.tag}'" + + raise ParsingError(exception) @dataclass @@ -208,7 +236,7 @@ class LUT1D(ProcessNode): References ---------- - https://docs.acescentral.com/specifications/clf/#lut1d + - https://docs.acescentral.com/specifications/clf/#lut1d """ array: Array @@ -218,27 +246,37 @@ class LUT1D(ProcessNode): @staticmethod @register_process_node_xml_constructor("LUT1D") - def from_xml(xml, config: ParserConfig): + def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT1D | None: """ - Parse and return the LUT1D from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.LUT1D` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.LUT1D` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + super_args = ProcessNode.parse_attributes(xml, config) - array = Array.from_xml(child_element(xml, "Array", config), config) + array = Array.from_xml(child_element(xml, "Array", config), config) # pyright: ignore + if array is None: - raise ParsingError("LUT1D processing node does not have an Array element.") + exception = "LUT1D processing node does not have an Array element." + + raise ParsingError(exception) half_domain = xml.get("halfDomain") == "true" raw_halfs = xml.get("rawHalfs") == "true" @@ -259,7 +297,7 @@ class LUT3D(ProcessNode): References ---------- - https://docs.acescentral.com/specifications/clf/#lut3d + - https://docs.acescentral.com/specifications/clf/#lut3d """ array: Array @@ -269,30 +307,42 @@ class LUT3D(ProcessNode): @staticmethod @register_process_node_xml_constructor("LUT3D") - def from_xml(xml, config: ParserConfig): + def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT3D | None: """ - Parse and return the LUT3D from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.LUT3D` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. Expects the xml element to be a valid element according to the CLF specification. + Returns + ------- + class:`colour_clf_io.LUT3D` or :py:data:`None` + Parsed XML node. + Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + super_args = ProcessNode.parse_attributes(xml, config) - array = Array.from_xml(child_element(xml, "Array", config), config) + array = Array.from_xml(child_element(xml, "Array", config), config) # pyright: ignore + if array is None: - raise ParsingError("LUT3D processing node does not have an Array element.") + exception = "LUT3D processing node does not have an Array element." + + raise ParsingError(exception) + half_domain = xml.get("halfDomain") == "true" raw_halfs = xml.get("rawHalfs") == "true" interpolation = Interpolation3D(xml.get("interpolation")) + return LUT3D( array=array, half_domain=half_domain, @@ -309,34 +359,47 @@ class Matrix(ProcessNode): References ---------- - https://docs.acescentral.com/specifications/clf/#matrix + - https://docs.acescentral.com/specifications/clf/#matrix """ array: Array @staticmethod @register_process_node_xml_constructor("Matrix") - def from_xml(xml, config: ParserConfig): + def from_xml( + xml: lxml.etree._Element | None, config: ParserConfig + ) -> Matrix | None: """ - Parse and return the Matrix from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.Matrix` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. - Expects the xml element to be a valid element according to the CLF - specification. + Expects the xml element to be a valid element according to the CLF + specification. + + Returns + ------- + class:`colour_clf_io.Matrix` or :py:data:`None` + Parsed XML node. Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + super_args = ProcessNode.parse_attributes(xml, config) - array = Array.from_xml(child_element(xml, "Array", config), config) + array = Array.from_xml(child_element(xml, "Array", config), config) # pyright: ignore + if array is None: - raise ParsingError("Matrix processing node does not have an Array element.") + exception = "Matrix processing node does not have an Array element." + + raise ParsingError(exception) + return Matrix(array=array, **super_args) @@ -347,7 +410,7 @@ class Range(ProcessNode): References ---------- - https://docs.acescentral.com/specifications/clf/#range + - https://docs.acescentral.com/specifications/clf/#range """ min_in_value: float | None @@ -359,27 +422,35 @@ class Range(ProcessNode): @staticmethod @register_process_node_xml_constructor("Range") - def from_xml(xml, config: ParserConfig): + def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Range | None: """ - Parse and return the Range from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.Range` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. - Expects the xml element to be a valid element according to the CLF - specification. + Expects the xml element to be a valid element according to the CLF + specification. + + Returns + ------- + class:`colour_clf_io.Range` or :py:data:`None` + Parsed XML node. Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None super_args = ProcessNode.parse_attributes(xml, config) - def optional_float(name): + def optional_float(name: str) -> float | None: + """Convert given name to float.""" + return element_as_float(xml, name, config) min_in_value = optional_float("minInValue") @@ -406,7 +477,7 @@ class Log(ProcessNode): References ---------- - https://docs.acescentral.com/specifications/clf/#log + - https://docs.acescentral.com/specifications/clf/#log """ style: LogStyle @@ -414,34 +485,42 @@ class Log(ProcessNode): @staticmethod @register_process_node_xml_constructor("Log") - def from_xml(xml, config: ParserConfig): + def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Log | None: """ - Parse and return the Log from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.Log` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. - Expects the xml element to be a valid element according to the CLF - specification. + Expects the xml element to be a valid element according to the CLF + specification. + + Returns + ------- + class:`colour_clf_io.Log` or :py:data:`None` + Parsed XML node. Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + super_args = ProcessNode.parse_attributes(xml, config) style = LogStyle(xml.get("style")) param_elements = child_elements(xml, "LogParams", config) params = [ param for param in [ - LogParams.from_xml(param_element, config) + LogParams.from_xml(param_element, config) # pyright: ignore for param_element in param_elements ] if param is not None ] + return Log(style=style, log_params=params, **super_args) @@ -452,7 +531,7 @@ class Exponent(ProcessNode): References ---------- - https://docs.acescentral.com/specifications/clf/#exponent + - https://docs.acescentral.com/specifications/clf/#exponent """ style: ExponentStyle @@ -460,38 +539,56 @@ class Exponent(ProcessNode): @staticmethod @register_process_node_xml_constructor("Exponent") - def from_xml(xml, config: ParserConfig): + def from_xml( + xml: lxml.etree._Element | None, config: ParserConfig + ) -> Exponent | None: """ - Parse and return the Exponent from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.Exponent` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. + + Expects the xml element to be a valid element according to the CLF + specification. + + Returns + ------- + class:`colour_clf_io.Exponent` or :py:data:`None` + Parsed XML node. - Expects the xml element to be a valid element according to the CLF - specification. Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + super_args = ProcessNode.parse_attributes(xml, config) style = map_optional(ExponentStyle, xml.get("style")) + if style is None: - raise ParsingError("Exponent process node has no `style' value.") + exception = "Exponent process node has no `style' value." + + raise ParsingError(exception) + param_elements = child_elements(xml, "ExponentParams", config) params = [ param for param in [ - ExponentParams.from_xml(param_element, config) + ExponentParams.from_xml(param_element, config) # pyright: ignore for param_element in param_elements ] if param is not None ] + if not params: - raise ParsingError("Exponent process node has no `ExponentParams' element.") + exception = "Exponent process node has no `ExponentParams' element." + + raise ParsingError(exception) + return Exponent(style=style, exponent_params=params, **super_args) @@ -502,7 +599,7 @@ class ASC_CDL(ProcessNode): References ---------- - https://docs.acescentral.com/specifications/clf/#asc_cdl + - https://docs.acescentral.com/specifications/clf/#asc_cdl """ style: ASC_CDL_Style @@ -511,25 +608,35 @@ class ASC_CDL(ProcessNode): @staticmethod @register_process_node_xml_constructor("ASC_CDL") - def from_xml(xml, config: ParserConfig): + def from_xml( + xml: lxml.etree._Element | None, config: ParserConfig + ) -> ASC_CDL | None: """ - Parse and return the ASC_CDL from the given XML node. Returns None if the given - element is None. + Parse and return a :class:`colour_clf_io.ASC_CDL` class instance + from the given XML node. Returns `None`` if the given XML node is ``None``. - Expects the xml element to be a valid element according to the CLF - specification. + Expects the xml element to be a valid element according to the CLF + specification. + + Returns + ------- + class:`colour_clf_io.ASC_CDL` or :py:data:`None` + Parsed XML node. Raises ------ - :class: ParsingError - If the node does not conform to the specification, a `ParsingError` - will be raised. The error message will indicate the details of the issue - that was encountered. + :class:`ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ + if xml is None: return None + super_args = ProcessNode.parse_attributes(xml, config) style = ASC_CDL_Style(xml.get("style")) - sopnode = SOPNode.from_xml(child_element(xml, "SOPNode", config), config) - sat_node = SatNode.from_xml(child_element(xml, "SatNode", config), config) - return ASC_CDL(style=style, sopnode=sopnode, sat_node=sat_node, **super_args) + sop_node = SOPNode.from_xml(child_element(xml, "SOPNode", config), config) # pyright: ignore + sat_node = SatNode.from_xml(child_element(xml, "SatNode", config), config) # pyright: ignore + + return ASC_CDL(style=style, sopnode=sop_node, sat_node=sat_node, **super_args) diff --git a/colour_clf_io/tests/conftest.py b/colour_clf_io/tests/conftest.py index 0689eea..1bc4c55 100644 --- a/colour_clf_io/tests/conftest.py +++ b/colour_clf_io/tests/conftest.py @@ -1,7 +1,18 @@ -import pytest # noqa: D100 +""" +Pytest Configuration +==================== +Configure *pytest* to use with *OpenColorIO* if available. +""" + +from __future__ import annotations + +import pytest + + +def pytest_addoption(parser) -> None: # noqa: ANN001 + """Add a *pytest* option for test requiring *OpenColorIO*.""" -def pytest_addoption(parser): # noqa: D103 parser.addoption( "--with_ocio", action="store_true", @@ -10,15 +21,20 @@ def pytest_addoption(parser): # noqa: D103 ) -def pytest_configure(config): # noqa: D103 +def pytest_configure(config) -> None: # noqa: ANN001 + """Configure *pytest* for *OpenColorIO*.""" + config.addinivalue_line( "markers", "with_ocio: mark test that require the OpenColorIO library" ) -def pytest_collection_modifyitems(config, items): # noqa: D103 +def pytest_collection_modifyitems(config, items) -> None: # noqa: ANN001 + """Modify *pytest* collection for *OpenColorIO*.""" + if config.getoption("--with_ocio"): return + skip_slow = pytest.mark.skip(reason="need --with_ocio option to run") for item in items: if "with_ocio" in item.keywords: diff --git a/colour_clf_io/tests/test_clf_common.py b/colour_clf_io/tests/test_clf_common.py index 85335b4..8d16392 100644 --- a/colour_clf_io/tests/test_clf_common.py +++ b/colour_clf_io/tests/test_clf_common.py @@ -2,64 +2,133 @@ Defines helper functionality for CLF tests. """ +from __future__ import annotations + import os import tempfile import numpy as np +import numpy.typing as npt import colour_clf_io.parsing import colour_clf_io.process_list __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" __status__ = "Production" -__all__ = ["snippet_to_process_list", "wrap_snippet"] +__all__ = [ + "EXAMPLE_WRAPPER", + "wrap_snippet", + "snippet_to_process_list", + "snippet_as_tmp_file", + "result_as_array", +] -EXAMPLE_WRAPPER = """ +EXAMPLE_WRAPPER: str = """ + {0} -""" +""".strip() def wrap_snippet(snippet: str) -> str: - """# noqa: D401 - Takes a string that should contain the text representation of a CLF node, and + """ + Take a string that should contain the text representation of a CLF node, and returns valid CLF document. Essentially the given string is pasted into the `ProcessList` if a CLF document. This is useful to quickly convert example snippets of Process Nodes into valid CLF documents for parsing. + + Parameters + ---------- + snippet + Snippet to wrap as a CLF document. + + Returns + ------- + :class:`str` + CLF document. """ + return EXAMPLE_WRAPPER.format(snippet) -def snippet_to_process_list(snippet: str) -> colour_clf_io.process_list.ProcessList: - """# noqa: D401 - Takes a string that should contain a valid body for a XML Process List and - returns the parsed `ProcessList`. +def snippet_to_process_list( + snippet: str, +) -> colour_clf_io.process_list.ProcessList | None: """ + Take a string that should contain a valid body for an XML Process List and + returns the parsed :class:`colour_clf_io.process_list.ProcessList` class + instance. + + Parameters + ---------- + snippet + Snippet to parse. + + Returns + ------- + :class:`colour_clf_io.process_list.ProcessList` + """ + doc = wrap_snippet(snippet) + return colour_clf_io.parse_clf(doc) -def snippet_as_tmp_file(snippet): +def snippet_as_tmp_file(snippet: str) -> str: + """ + Write given snippet to a temporary file. + + Parameters + ---------- + snippet + Snippet to write + + Returns + ------- + :class:`str` + Temporary filename. + """ + doc = wrap_snippet(snippet) tmp_folder = tempfile.gettempdir() file_name = os.path.join(tmp_folder, "colour_snippet.clf") - with open(file_name, "w") as f: - f.write(doc) + + with open(file_name, "w") as clf_file: + clf_file.write(doc) + return file_name -def result_as_array(result_text): - result_parts = result_text.decode("utf-8").strip().split() +def result_as_array(result: bytes) -> npt.NDArray: + """ + Decode given result and convert them to an array. + + Parameters + ---------- + result + Result to convert to an array. + + Returns + ------- + :class:`np.ndarray` + Converted result array. + """ + + result_parts = result.decode("utf-8").strip().split() if len(result_parts) != 3: - raise RuntimeError(f"Invalid OCIO result: {result_text}") + exception = f"Invalid OCIO result: {result}" + + raise RuntimeError(exception) + result_values = list(map(float, result_parts)) + return np.array(result_values) diff --git a/colour_clf_io/tests/test_clf_parsing.py b/colour_clf_io/tests/test_clf_parsing.py index cacb554..c438855 100644 --- a/colour_clf_io/tests/test_clf_parsing.py +++ b/colour_clf_io/tests/test_clf_parsing.py @@ -1,8 +1,9 @@ # !/usr/bin/env python """Define the unit tests for the :mod:`colour.io.clf` module.""" +from __future__ import annotations + import os -import unittest import numpy as np import pytest @@ -16,45 +17,49 @@ from .test_clf_common import wrap_snippet __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" __status__ = "Production" __all__ = [ + "ROOT_CLF", + "EXAMPLE_WRAPPER", "TestParseCLF", ] ROOT_CLF: str = os.path.join(os.path.dirname(__file__), "resources") -EXAMPLE_WRAPPER = """ +EXAMPLE_WRAPPER: str = """ {0} """ -class TestParseCLF(unittest.TestCase): +class TestParseCLF: """ Define tests methods for parsing CLF files using the functionality provided in the :mod: `colour.io.clf`module. """ - def test_read_sample_document_1(self): + def test_read_sample_document_1(self) -> None: """ Test parsing of the sample document `ACES2065_1_to_ACEScct.xml`. """ + clf_data = read_clf(os.path.join(ROOT_CLF, "ACES2065_1_to_ACEScct.xml")) - self.assertEqual( - clf_data.description, ["Conversion from linear ACES2065-1 to ACEScct"] - ) - self.assertEqual(clf_data.input_descriptor, "ACES (SMPTE ST 2065-1)") - self.assertEqual(clf_data.output_descriptor, "ACEScct") - self.assertEqual(len(clf_data.process_nodes), 3) + assert clf_data is not None + + assert clf_data.description == ["Conversion from linear ACES2065-1 to ACEScct"] + assert clf_data.input_descriptor == "ACES (SMPTE ST 2065-1)" + assert clf_data.output_descriptor == "ACEScct" + assert len(clf_data.process_nodes) == 3 first_process_node = clf_data.process_nodes[0] - self.assertIsInstance(first_process_node, colour_clf_io.process_nodes.Matrix) + assert isinstance(first_process_node, colour_clf_io.process_nodes.Matrix) + np.testing.assert_array_almost_equal( first_process_node.array.as_array(), np.array( @@ -66,33 +71,40 @@ def test_read_sample_document_1(self): ), ) - def test_read_sample_document_2(self): + def test_read_sample_document_2(self) -> None: """ Test parsing of the sample document `LMT Kodak 2383 Print Emulation.xml`. """ + clf_data = read_clf( os.path.join(ROOT_CLF, "LMT Kodak 2383 Print Emulation.xml") ) - self.assertEqual(clf_data.description, ["Print film emulation (Kodak 2383)"]) - self.assertEqual(clf_data.input_descriptor, "ACES (SMPTE ST 2065-1)") - self.assertEqual(clf_data.output_descriptor, "ACES (SMPTE ST 2065-1)") - self.assertEqual(len(clf_data.process_nodes), 10) - def test_read_sample_document_3(self): + assert clf_data is not None + assert clf_data.description == ["Print film emulation (Kodak 2383)"] + assert clf_data.input_descriptor == "ACES (SMPTE ST 2065-1)" + assert clf_data.output_descriptor == "ACES (SMPTE ST 2065-1)" + assert len(clf_data.process_nodes) == 10 + + def test_read_sample_document_3(self) -> None: """ Test parsing of the sample document `LMT_ARRI_K1S1_709_EI800_v3.xml`. """ + clf_data = read_clf(os.path.join(ROOT_CLF, "LMT_ARRI_K1S1_709_EI800_v3.xml")) - self.assertEqual(clf_data.description, ["An ARRI based look"]) - self.assertEqual(clf_data.input_descriptor, "ACES (SMPTE ST 2065-1)") - self.assertEqual(clf_data.output_descriptor, "ACES (SMPTE ST 2065-1)") - self.assertEqual(len(clf_data.process_nodes), 7) - def test_LUT1D_example(self): + assert clf_data is not None + assert clf_data.description == ["An ARRI based look"] + assert clf_data.input_descriptor == "ACES (SMPTE ST 2065-1)" + assert clf_data.output_descriptor == "ACES (SMPTE ST 2065-1)" + assert len(clf_data.process_nodes) == 7 + + def test_LUT1D_example(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 1. """ + example = """ 1D LUT - Turn 4 grey levels into 4 inverted codes @@ -104,25 +116,29 @@ def test_LUT1D_example(self): """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.LUT1D) - self.assertEqual(node.id, "lut-23") - self.assertEqual(node.name, "4 Value Lut") - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.i12) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.i12) - self.assertEqual( - node.description, "1D LUT - Turn 4 grey levels into 4 inverted codes" - ) + + assert isinstance(node, colour_clf_io.process_nodes.LUT1D) + assert node.id == "lut-23" + assert node.name == "4 Value Lut" + assert node.in_bit_depth == colour_clf_io.values.BitDepth.i12 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.i12 + assert node.description == "1D LUT - Turn 4 grey levels into 4 inverted codes" np.testing.assert_array_almost_equal( node.array.as_array(), np.array([3, 2, 1, 0]) ) - def test_LUT3D_example(self): + def test_LUT3D_example(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 2. """ + example = """ 3D LUT @@ -138,18 +154,20 @@ def test_LUT3D_example(self): """ # noqa: E501 + doc = parse_clf(wrap_snippet(example)) + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.LUT3D) - self.assertEqual(node.id, "lut-24") - self.assertEqual(node.name, "green look") - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.i12) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f16) - self.assertEqual( - node.interpolation, colour_clf_io.values.Interpolation3D.TRILINEAR - ) - self.assertEqual(node.description, "3D LUT") + + assert isinstance(node, colour_clf_io.process_nodes.LUT3D) + assert node.id == "lut-24" + assert node.name == "green look" + assert node.in_bit_depth == colour_clf_io.values.BitDepth.i12 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f16 + assert node.interpolation == colour_clf_io.values.Interpolation3D.TRILINEAR + assert node.description == "3D LUT" np.testing.assert_array_almost_equal( node.array.as_array(), np.array( @@ -166,11 +184,12 @@ def test_LUT3D_example(self): ), ) - def test_matrix_example_1(self): + def test_matrix_example_1(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 3. """ + example = """ 3x3 color space conversion from AP0 to AP1 @@ -181,14 +200,19 @@ def test_matrix_example_1(self): """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Matrix) - self.assertEqual(node.id, "lut-28") - self.assertEqual(node.name, "AP0 to AP1") - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f16) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f16) - self.assertEqual(node.description, "3x3 color space conversion from AP0 to AP1") + + assert isinstance(node, colour_clf_io.process_nodes.Matrix) + assert node.id == "lut-28" + assert node.name == "AP0 to AP1" + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f16 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f16 + assert node.description == "3x3 color space conversion from AP0 to AP1" np.testing.assert_array_almost_equal( node.array.as_array(), np.array( @@ -200,11 +224,12 @@ def test_matrix_example_1(self): ), ) - def test_matrix_example_2(self): + def test_matrix_example_2(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 4. """ + example = """ 3x4 Matrix , 4th column is offset @@ -215,14 +240,19 @@ def test_matrix_example_2(self): """ # noqa: E501 + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Matrix) - self.assertEqual(node.id, "lut-25") - self.assertEqual(node.name, "colorspace conversion") - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.i10) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.i10) - self.assertEqual(node.description, " 3x4 Matrix , 4th column is offset ") + + assert isinstance(node, colour_clf_io.process_nodes.Matrix) + assert node.id == "lut-25" + assert node.name == "colorspace conversion" + assert node.in_bit_depth == colour_clf_io.values.BitDepth.i10 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.i10 + assert node.description == " 3x4 Matrix , 4th column is offset " np.testing.assert_array_almost_equal( node.array.as_array(), np.array( @@ -249,11 +279,12 @@ def test_matrix_example_2(self): ), ) - def test_range_example(self): + def test_range_example(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 5. """ + example = """ 10-bit full range to SMPTE range @@ -263,44 +294,56 @@ def test_range_example(self): 940 """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Range) - self.assertEqual(node.id, None) - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.i10) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.i10) - self.assertEqual(node.description, "10-bit full range to SMPTE range") - self.assertEqual(node.min_in_value, 0.0) - self.assertEqual(node.min_out_value, 64.0) - self.assertEqual(node.max_out_value, 940.0) - - def test_log_example_1(self): + + assert isinstance(node, colour_clf_io.process_nodes.Range) + assert node.id is None + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.i10 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.i10 + assert node.description == "10-bit full range to SMPTE range" + assert node.min_in_value == 0.0 + assert node.min_out_value == 64.0 + assert node.max_out_value == 940.0 + + def test_log_example_1(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 6. """ + example = """ Base 10 Logarithm """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Log) - self.assertEqual(node.id, None) - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f16) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f16) - self.assertEqual(node.description, "Base 10 Logarithm") - self.assertEqual(node.style, colour_clf_io.elements.LogStyle.LOG_10) - self.assertEqual(node.log_params, []) - def test_log_example_2(self): + assert isinstance(node, colour_clf_io.process_nodes.Log) + assert node.id is None + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f16 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f16 + assert node.description == "Base 10 Logarithm" + assert node.style == colour_clf_io.elements.LogStyle.LOG_10 + assert node.log_params == [] + + def test_log_example_2(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 7. """ + example = """ Linear to DJI D-Log @@ -309,119 +352,162 @@ def test_log_example_2(self): linearSlope="6.025"/> """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Log) - self.assertEqual(node.id, None) - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.description, "Linear to DJI D-Log") - self.assertEqual(node.style, colour_clf_io.elements.LogStyle.CAMERA_LIN_TO_LOG) - self.assertAlmostEqual(node.log_params[0].base, 10.0) - self.assertAlmostEqual(node.log_params[0].log_side_slope, 0.256663) - self.assertAlmostEqual(node.log_params[0].log_side_offset, 0.584555) - self.assertAlmostEqual(node.log_params[0].lin_side_slope, 0.9892) - self.assertAlmostEqual(node.log_params[0].lin_side_offset, 0.0108) - self.assertAlmostEqual(node.log_params[0].lin_side_break, 0.0078) - self.assertAlmostEqual(node.log_params[0].linear_slope, 6.025) - - def test_exponent_example_1(self): + + assert isinstance(node, colour_clf_io.process_nodes.Log) + assert node.id is None + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.description == "Linear to DJI D-Log" + assert node.style == colour_clf_io.elements.LogStyle.CAMERA_LIN_TO_LOG + assert node.log_params[0].base is not None + np.testing.assert_allclose(node.log_params[0].base, 10.0) + assert node.log_params[0].log_side_slope is not None + np.testing.assert_allclose(node.log_params[0].log_side_slope, 0.256663) + assert node.log_params[0].log_side_offset is not None + np.testing.assert_allclose(node.log_params[0].log_side_offset, 0.584555) + assert node.log_params[0].lin_side_slope is not None + np.testing.assert_allclose(node.log_params[0].lin_side_slope, 0.9892) + assert node.log_params[0].lin_side_offset is not None + np.testing.assert_allclose(node.log_params[0].lin_side_offset, 0.0108) + assert node.log_params[0].lin_side_break is not None + np.testing.assert_allclose(node.log_params[0].lin_side_break, 0.0078) + assert node.log_params[0].linear_slope is not None + np.testing.assert_allclose(node.log_params[0].linear_slope, 6.025) + + def test_exponent_example_1(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 8. """ + example = """ Basic 2.2 Gamma """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Exponent) - self.assertEqual(node.id, None) - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.description, "Basic 2.2 Gamma") - self.assertEqual(node.style, colour_clf_io.elements.ExponentStyle.BASIC_FWD) - self.assertAlmostEqual(node.exponent_params[0].exponent, 2.2) - def test_exponent_example_2(self): + assert isinstance(node, colour_clf_io.process_nodes.Exponent) + assert node.id is None + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.description == "Basic 2.2 Gamma" + assert node.style == colour_clf_io.elements.ExponentStyle.BASIC_FWD + np.testing.assert_allclose(node.exponent_params[0].exponent, 2.2) + + def test_exponent_example_2(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 9. """ + example = """ EOTF (sRGB) """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Exponent) - self.assertEqual(node.id, None) - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.description, "EOTF (sRGB)") - self.assertEqual(node.style, colour_clf_io.elements.ExponentStyle.MON_CURVE_FWD) - self.assertAlmostEqual(node.exponent_params[0].exponent, 2.4) - self.assertAlmostEqual(node.exponent_params[0].offset, 0.055) - - def test_exponent_example_3(self): + + assert isinstance(node, colour_clf_io.process_nodes.Exponent) + assert node.id is None + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.description == "EOTF (sRGB)" + assert node.style, colour_clf_io.elements.ExponentStyle.MON_CURVE_FWD + assert node.exponent_params[0].exponent is not None + np.testing.assert_allclose(node.exponent_params[0].exponent, 2.4) + assert node.exponent_params[0].offset is not None + np.testing.assert_allclose(node.exponent_params[0].offset, 0.055) + + def test_exponent_example_3(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 10. """ + example = """ CIE L* """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Exponent) - self.assertEqual(node.id, None) - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.description, "CIE L*") - self.assertEqual(node.style, colour_clf_io.elements.ExponentStyle.MON_CURVE_REV) - self.assertAlmostEqual(node.exponent_params[0].exponent, 3.0) - self.assertAlmostEqual(node.exponent_params[0].offset, 0.16) - - def test_exponent_example_4(self): + + assert isinstance(node, colour_clf_io.process_nodes.Exponent) + assert node.id is None + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.description == "CIE L*" + assert node.style == colour_clf_io.elements.ExponentStyle.MON_CURVE_REV + assert node.exponent_params[0].exponent is not None + np.testing.assert_allclose(node.exponent_params[0].exponent, 3.0) + assert node.exponent_params[0].offset is not None + np.testing.assert_allclose(node.exponent_params[0].offset, 0.16) + + def test_exponent_example_4(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 11. """ + example = """ Rec. 709 OETF """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.Exponent) - self.assertEqual(node.id, None) - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f32) - self.assertEqual(node.description, "Rec. 709 OETF") - self.assertEqual(node.style, colour_clf_io.elements.ExponentStyle.MON_CURVE_REV) - self.assertAlmostEqual(node.exponent_params[0].exponent, 2.2222222222222222) - self.assertAlmostEqual(node.exponent_params[0].offset, 0.099) - - def test_ASC_CDL_example(self): + + assert isinstance(node, colour_clf_io.process_nodes.Exponent) + assert node.id is None + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 + assert node.description == "Rec. 709 OETF" + assert node.style == colour_clf_io.elements.ExponentStyle.MON_CURVE_REV + assert node.exponent_params[0].exponent is not None + np.testing.assert_allclose(node.exponent_params[0].exponent, 2.2222222222222222) + assert node.exponent_params[0].offset is not None + np.testing.assert_allclose(node.exponent_params[0].offset, 0.099) + + def test_ASC_CDL_example(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 12. """ + example = """ scene 1 exterior look @@ -435,25 +521,34 @@ def test_ASC_CDL_example(self): """ + doc = parse_clf(wrap_snippet(example)) + + assert doc is not None + node = doc.process_nodes[0] - self.assertIsInstance(node, colour_clf_io.process_nodes.ASC_CDL) - self.assertEqual(node.id, "cc01234") - self.assertEqual(node.name, None) - self.assertEqual(node.in_bit_depth, colour_clf_io.values.BitDepth.f16) - self.assertEqual(node.out_bit_depth, colour_clf_io.values.BitDepth.f16) - self.assertEqual(node.description, "scene 1 exterior look") - self.assertEqual(node.style, colour_clf_io.values.ASC_CDL_Style.FWD) - self.assertEqual(node.sopnode.slope, (1.000000, 1.000000, 0.900000)) - self.assertEqual(node.sopnode.offset, (-0.030000, -0.020000, 0.000000)) - self.assertEqual(node.sopnode.power, (1.2500000, 1.000000, 1.000000)) - self.assertAlmostEqual(node.sat_node.saturation, 1.700000) - - def test_ACES2065_1_to_ACEScg_example(self): + + assert isinstance(node, colour_clf_io.process_nodes.ASC_CDL) + assert node.id == "cc01234" + assert node.name is None + assert node.in_bit_depth == colour_clf_io.values.BitDepth.f16 + assert node.out_bit_depth == colour_clf_io.values.BitDepth.f16 + assert node.description == "scene 1 exterior look" + assert node.style == colour_clf_io.values.ASC_CDL_Style.FWD + assert node.sopnode is not None + assert node.sopnode.slope == (1.000000, 1.000000, 0.900000) + assert node.sopnode.offset == (-0.030000, -0.020000, 0.000000) + assert node.sopnode.power == (1.2500000, 1.000000, 1.000000) + assert node.sat_node is not None + assert node.sat_node.saturation is not None + np.testing.assert_allclose(node.sat_node.saturation, 1.700000) + + def test_ACES2065_1_to_ACEScg_example(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 13. """ + # Note that this string uses binary encoding, as the XML document specifies its # own encoding. example = b""" @@ -475,15 +570,20 @@ def test_ACES2065_1_to_ACEScg_example(self): """ + doc = parse_clf(example) - self.assertEqual(len(doc.process_nodes), 1) - self.assertIsInstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix) - def test_ACES2065_1_to_ACEScct_example(self): + assert doc is not None + + assert len(doc.process_nodes) == 1 + assert isinstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix) + + def test_ACES2065_1_to_ACEScct_example(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 14. """ + # Note that this string uses binary encoding, as the XML document specifies its # own encoding. example = b""" @@ -509,16 +609,21 @@ def test_ACES2065_1_to_ACEScct_example(self): """ # noqa: E501 + doc = parse_clf(example) - self.assertEqual(len(doc.process_nodes), 2) - self.assertIsInstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix) - self.assertIsInstance(doc.process_nodes[1], colour_clf_io.process_nodes.Log) - def test_CIE_XYZ_to_CIELAB_example(self): + assert doc is not None + + assert len(doc.process_nodes) == 2 + assert isinstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix) + assert isinstance(doc.process_nodes[1], colour_clf_io.process_nodes.Log) + + def test_CIE_XYZ_to_CIELAB_example(self) -> None: """ Test parsing of the example process node from the official CLF specification Example 14. """ + # Note that this string uses binary encoding, as the XML document specifies its # own encoding. example = b""" @@ -547,18 +652,21 @@ def test_CIE_XYZ_to_CIELAB_example(self): """ + doc = parse_clf(example) - self.assertEqual(len(doc.process_nodes), 3) - self.assertIsInstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix) - self.assertIsInstance( - doc.process_nodes[1], colour_clf_io.process_nodes.Exponent - ) - self.assertIsInstance(doc.process_nodes[2], colour_clf_io.process_nodes.Matrix) - def test_fail_on_invalid_namespace(self): + assert doc is not None + + assert len(doc.process_nodes) == 3 + assert isinstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix) + assert isinstance(doc.process_nodes[1], colour_clf_io.process_nodes.Exponent) + assert isinstance(doc.process_nodes[2], colour_clf_io.process_nodes.Matrix) + + def test_fail_on_invalid_namespace(self) -> None: """ Test parsing oa a process list with an invalid xmlns attribute. """ + example = b""" @@ -567,20 +675,15 @@ def test_fail_on_invalid_namespace(self): """ - try: - parse_clf(example) - except ParsingError: - return - self.fail( - "Parsing should have thrown a validation error due to invalid xmlns " - "attribute." - ) + + pytest.raises(ParsingError, parse_clf, example) @pytest.mark.with_ocio - def test_CLF_from_OCIO(self): + def test_CLF_from_OCIO(self) -> None: """ Test parsing of a CLF file written by OpenColorIO. """ + import PyOpenColorIO as ocio ocio_transform = ( @@ -589,20 +692,22 @@ def test_CLF_from_OCIO(self): .createGroupTransform() ) clf_text = ocio_transform.write("Academy/ASC Common LUT Format").encode() + doc = parse_clf(clf_text) - self.assertEqual(len(doc.process_nodes), 2) - self.assertIsInstance(doc.process_nodes[0], colour_clf_io.process_nodes.Log) - self.assertIsInstance(doc.process_nodes[1], colour_clf_io.process_nodes.Matrix) + + assert doc is not None + + assert len(doc.process_nodes) == 2 + assert isinstance(doc.process_nodes[0], colour_clf_io.process_nodes.Log) + assert isinstance(doc.process_nodes[1], colour_clf_io.process_nodes.Matrix) + node = doc.process_nodes[0] - self.assertIsNotNone( - node.log_params, "Log Params were not parsed successfully." - ) - self.assertAlmostEqual(node.log_params[0].base, ocio_transform[0].getBase()) - self.assertAlmostEqual( + + assert node.log_params is not None + assert node.log_params[0].base is not None + np.testing.assert_allclose(node.log_params[0].base, ocio_transform[0].getBase()) + assert node.log_params[0].log_side_slope is not None + np.testing.assert_allclose( node.log_params[0].log_side_slope, ocio_transform[0].getLogSideSlopeValue()[0], ) - - -if __name__ == "__main__": - unittest.main() diff --git a/colour_clf_io/values.py b/colour_clf_io/values.py index 3bd6c3b..62230f5 100644 --- a/colour_clf_io/values.py +++ b/colour_clf_io/values.py @@ -2,9 +2,8 @@ Values ======= -Defines enums that represent allowed values in some of the fields contained in a CLF -document. - +Defines enums that represent allowed values in some of the fields contained in a +CLF document. """ from __future__ import annotations @@ -13,13 +12,13 @@ from enum import Enum __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" __status__ = "Production" -__ALL__ = [ +__all__ = [ "BitDepth", "Channel", "Interpolation1D", @@ -34,7 +33,7 @@ class BitDepth(Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#processNode + - https://docs.acescentral.com/specifications/clf/#processNode """ i8 = "8i" @@ -44,13 +43,13 @@ class BitDepth(Enum): f16 = "16f" f32 = "32f" - def scale_factor(self): - """Return the scale factor that is needed to normalise a value of the given - BitDepth to the range 0..1. + def scale_factor(self) -> float: + """ + Return the scale factor that is needed to normalise a value of the given + bit depth to the range 0..1. Examples -------- - ``` >>> from colour_clf_io.values import BitDepth >>> 255 / BitDepth.i8.scale_factor() == 1.0 True @@ -60,34 +59,37 @@ def scale_factor(self): True >>> 1.0 / BitDepth.f16.scale_factor() == 1.0 True - - ``` """ + if self == BitDepth.i8: return 2**8 - 1 - elif self == BitDepth.i10: + + if self == BitDepth.i10: return 2**10 - 1 - elif self == BitDepth.i12: + + if self == BitDepth.i12: return 2**12 - 1 - elif self == BitDepth.i16: + + if self == BitDepth.i16: return 2**16 - 1 - elif self in [BitDepth.f16, BitDepth.f32]: + + if self in [BitDepth.f16, BitDepth.f32]: return 1.0 - raise NotImplementedError() + + raise NotImplementedError @classmethod - def all(cls): - """Return a list of all valid BitDepth values. + def all(cls: type[BitDepth]) -> list: + """ + Return a list of all valid bit depth values. Examples -------- - ``` >>> from colour_clf_io.values import BitDepth >>> BitDepth.all() ['8i', '10i', '12i', '16i', '16f', '32f'] - - ``` """ + return [e.value for e in cls] @@ -97,7 +99,7 @@ class Channel(enum.Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#ranges + - https://docs.acescentral.com/specifications/clf/#ranges """ R = "R" @@ -111,7 +113,7 @@ class Interpolation1D(Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#lut1d + - https://docs.acescentral.com/specifications/clf/#lut1d """ LINEAR = "linear" @@ -123,7 +125,7 @@ class Interpolation3D(Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#lut3d + - https://docs.acescentral.com/specifications/clf/#lut3d """ TRILINEAR = "trilinear" @@ -136,7 +138,7 @@ class ASC_CDL_Style(enum.Enum): References ---------- - https://docs.acescentral.com/specifications/clf/#asc_cdl + - https://docs.acescentral.com/specifications/clf/#asc_cdl """ FWD = "Fwd" diff --git a/docs/conf.py b/docs/conf.py index d823cd3..81fbc30 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ sys.path.append(str(Path(__file__).parent.parent)) -import colour_clf_io as package # noqa: E402 +import colour_clf_io as package basename = re.sub("_(\\w)", lambda x: x.group(1).upper(), package.__name__.title()) diff --git a/docs/index.rst b/docs/index.rst index 46e77e6..53d6180 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -71,6 +71,6 @@ About ----- | **Colour - CLF IO** by Colour Developers -| Copyright 2015 Colour Developers – `colour-developers@colour-science.org `__ +| Copyright 2024 Colour Developers – `colour-developers@colour-science.org `__ | This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause | `https://github.com/colour-science/colour-clf-io `__ diff --git a/docs/requirements.txt b/docs/requirements.txt index 04b885e..cef2a23 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,28 +5,27 @@ alabaster==1.0.0 babel==2.16.0 beautifulsoup4==4.12.3 biblib-simple==0.1.2 -certifi==2024.8.30 -charset-normalizer==3.4.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 colorama==0.4.6 ; sys_platform == 'win32' docutils==0.21.2 idna==3.10 imagesize==1.4.1 -jinja2==3.1.4 +jinja2==3.1.5 latexcodec==3.0.0 lxml==5.3.0 -lxml-stubs==0.5.1 markupsafe==3.0.2 -numpy==2.1.2 +numpy==2.2.1 packaging==24.2 pybtex==0.24.0 pybtex-docutils==1.0.3 -pydata-sphinx-theme==0.16.0 -pygments==2.18.0 +pydata-sphinx-theme==0.16.1 +pygments==2.19.1 pyyaml==6.0.2 requests==2.32.3 restructuredtext-lint==1.4.0 -setuptools==75.3.0 ; python_full_version >= '3.12' -six==1.16.0 +setuptools==75.8.0 ; python_full_version >= '3.12' +six==1.17.0 snowballstemmer==2.2.0 soupsieve==2.6 sphinx==8.1.3 @@ -37,6 +36,6 @@ sphinxcontrib-htmlhelp==2.1.0 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 -tomli==2.0.2 ; python_full_version < '3.11' +tomli==2.2.1 ; python_full_version < '3.11' typing-extensions==4.12.2 -urllib3==2.2.3 +urllib3==2.3.0 diff --git a/pyproject.toml b/pyproject.toml index 89c9fb1..8421d68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,10 +45,8 @@ dependencies = [ "numpy>=1.24,<3", "typing-extensions>=4,<5", "lxml>=5.2.1,<6", - "lxml-stubs>=0.5.1,<0.6" ] - [project.optional-dependencies] docs = [ "biblib-simple", @@ -58,7 +56,6 @@ docs = [ "sphinxcontrib-bibtex", ] - [tool.uv] package = true dev-dependencies = [ @@ -74,6 +71,8 @@ dev-dependencies = [ "pytest-xdist", "toml", "twine", + # Package Specific + "lxml-stubs>=0.5.1,<0.6" ] [build-system] @@ -84,11 +83,8 @@ build-backend = "hatchling.build" packages = [ "colour-clf-io" ] [tool.codespell] -ignore-words-list = 'co-ordinates,exitance,hart,ist' -skip = 'BIBLIOGRAPHY.bib,CONTRIBUTORS.rst' - -[tool.flynt] -line_length = 999 +ignore-words-list = "socio-economic" +skip = "BIBLIOGRAPHY.bib,CONTRIBUTORS.rst,*.ipynb" [tool.isort] ensure_newline_before_comments = true @@ -96,7 +92,6 @@ force_grid_wrap = 0 include_trailing_comma = true line_length = 88 multi_line_output = 3 -skip_glob = ["colour/**/__init__.py"] split_on_trailing_comma = true use_parentheses = true @@ -105,105 +100,67 @@ reportMissingImports = false reportMissingModuleSource = false reportUnboundVariable = false reportUnnecessaryCast = true -reportUnnecessaryTypeIgnorComment = true +reportUnnecessaryTypeIgnoreComment = true reportUnsupportedDunderAll = false reportUnusedExpression = false - +[tool.pytest.ini_options] +addopts = "--durations=5" [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 88 -lint.select = [ - "A", # flake8-builtins - "ARG", # flake8-unused-arguments - # "ANN", # flake8-annotations - "B", # flake8-bugbear - # "BLE", # flake8-blind-except - "C4", # flake8-comprehensions - # "C90", # mccabe - # "COM", # flake8-commas - "DTZ", # flake8-datetimez - "D", # pydocstyle - "E", # pydocstyle - # "ERA", # eradicate - # "EM", # flake8-errmsg - "EXE", # flake8-executable - "F", # flake8 - # "FBT", # flake8-boolean-trap - "G", # flake8-logging-format - "I", # isort - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "ISC", # flake8-implicit-str-concat - "N", # pep8-naming - # "PD", # pandas-vet - "PIE", # flake8-pie - "PGH", # pygrep-hooks - "PL", # pylint - # "PT", # flake8-pytest-style - # "PTH", # flake8-use-pathlib [Enable] - "Q", # flake8-quotes - "RET", # flake8-return - "RUF", # Ruff - "S", # flake8-bandit - "SIM", # flake8-simplify - "T10", # flake8-debugger - "T20", # flake8-print - # "TCH", # flake8-type-checking - "TID", # flake8-tidy-imports - "TRY", # tryceratops - "UP", # pyupgrade - "W", # pydocstyle - "YTT", # flake8-2020 -] -lint.ignore = [ - "B008", - "B905", - "D104", - "D200", - "D202", - "D205", - "D301", - "D400", - "I001", - "N801", - "N802", - "N803", - "N806", - "N813", - "N815", - "N816", - "PGH003", - "PIE804", - "PLE0605", - "PLR0911", - "PLR0912", - "PLR0913", - "PLR0915", - "PLR2004", - "RET504", - "RET505", - "RET506", - "RET507", - "RET508", - "TRY003", - "TRY300", +select = ["ALL"] +ignore = [ + "C", # Pylint - Convention + "C90", # mccabe + "COM", # flake8-commas + "ERA", # eradicate + "FBT", # flake8-boolean-trap + "FIX", # flake8-fixme + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib [Enable] + "TD", # flake8-todos + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `**kwargs` + "D200", # One-line docstring should fit on one line + "D202", # No blank lines allowed after function docstring + "D205", # 1 blank line required between summary line and description + "D301", # Use `r"""` if any backslashes in a docstring + "D400", # First line should end with a period + "I001", # Import block is un-sorted or un-formatted + "N801", # Class name `.*` should use CapWords convention + "N802", # Function name `.*` should be lowercase + "N803", # Argument name `.*` should be lowercase + "N806", # Variable `.*` in function should be lowercase + "N813", # Camelcase `.*` imported as lowercase `.*` + "N815", # Variable `.*` in class scope should not be mixedCase + "N816", # Variable `.*` in global scope should not be mixedCase + "NPY002", # Replace legacy `np.random.random` call with `np.random.Generator` + "PGH003", # Use specific rule codes when ignoring type issues + "PLR0912", # Too many branches + "PLR0913", # Too many arguments in function definition + "PLR0915", # Too many statements + "PLR2004", # Magic value used in comparison, consider replacing `.*` with a constant variable + "PYI036", # Star-args in `.*` should be annotated with `object` + "PYI051", # `Literal[".*"]` is redundant in a union with `str` + "PYI056", # Calling `.append()` on `__all__` may not be supported by all type checkers (use `+=` instead) + "RUF022", # [*] `__all__` is not sorted + "TRY003", # Avoid specifying long messages outside the exception class + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` ] -lint.fixable = ["B", "C", "E", "F", "PIE", "RUF", "SIM", "UP", "W"] +typing-modules = [] -[tool.ruff.lint.pydocstyle] +[tool.ruff.pydocstyle] convention = "numpy" -[tool.ruff.lint.per-file-ignores] +[tool.ruff.per-file-ignores] +"__init__.py" = ["D104"] +"colour_hdri/examples/*" = ["INP", "T201", "T203"] "docs/*" = ["INP"] "tasks.py" = ["INP"] +"test_*" = ["S101"] "utilities/*" = ["EXE001", "INP"] "utilities/unicode_to_ascii.py" = ["RUF001"] [tool.ruff.format] docstring-code-format = true - - -[tool.setuptools] -packages = ["colour_clf_io"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7a07226 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,178 @@ +# This file was autogenerated by uv via the following command: +# uv export --no-hashes --all-extras +accessible-pygments==0.0.5 +alabaster==1.0.0 +anyio==4.8.0 +appnope==0.1.4 ; sys_platform == 'darwin' +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +arrow==1.3.0 +asttokens==3.0.0 +async-lru==2.0.4 +attrs==24.3.0 +babel==2.16.0 +backports-tarfile==1.2.0 ; python_full_version < '3.12' +beautifulsoup4==4.12.3 +biblib-simple==0.1.2 +bleach==6.2.0 +certifi==2024.12.14 +cffi==1.17.1 +cfgv==3.4.0 +charset-normalizer==3.4.1 +click==8.1.8 +colorama==0.4.6 ; sys_platform == 'win32' +comm==0.2.2 +coverage==7.6.10 +coveralls==4.0.1 +cryptography==44.0.0 ; sys_platform == 'linux' +debugpy==1.8.11 +decorator==5.1.1 +defusedxml==0.7.1 +distlib==0.3.9 +docopt==0.6.2 +docutils==0.21.2 +exceptiongroup==1.2.2 ; python_full_version < '3.11' +execnet==2.1.1 +executing==2.1.0 +fastjsonschema==2.21.1 +filelock==3.16.1 +fqdn==1.5.1 +h11==0.14.0 +hatch==1.14.0 +hatchling==1.27.0 +httpcore==1.0.7 +httpx==0.28.1 +hyperlink==21.0.0 +identify==2.6.5 +idna==3.10 +imagesize==1.4.1 +importlib-metadata==8.5.0 ; python_full_version < '3.12' +iniconfig==2.0.0 +invoke==2.2.0 +ipykernel==6.29.5 +ipython==8.31.0 +ipywidgets==8.1.5 +isoduration==20.11.0 +jaraco-classes==3.4.0 +jaraco-context==6.0.1 +jaraco-functools==4.1.0 +jedi==0.19.2 +jeepney==0.8.0 ; sys_platform == 'linux' +jinja2==3.1.5 +json5==0.10.0 +jsonpointer==3.0.0 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +jupyter==1.1.1 +jupyter-client==8.6.3 +jupyter-console==6.6.3 +jupyter-core==5.7.2 +jupyter-events==0.11.0 +jupyter-lsp==2.2.5 +jupyter-server==2.15.0 +jupyter-server-terminals==0.5.3 +jupyterlab==4.3.4 +jupyterlab-pygments==0.3.0 +jupyterlab-server==2.27.3 +jupyterlab-widgets==3.0.13 +keyring==25.6.0 +latexcodec==3.0.0 +lxml==5.3.0 +lxml-stubs==0.5.1 +markdown-it-py==3.0.0 +markupsafe==3.0.2 +matplotlib-inline==0.1.7 +mdurl==0.1.2 +mistune==3.1.0 +more-itertools==10.5.0 +nbclient==0.10.2 +nbconvert==7.16.5 +nbformat==5.10.4 +nest-asyncio==1.6.0 +nh3==0.2.20 +nodeenv==1.9.1 +notebook==7.3.2 +notebook-shim==0.2.4 +numpy==2.2.1 +overrides==7.7.0 +packaging==24.2 +pandocfilters==1.5.1 +parso==0.8.4 +pathspec==0.12.1 +pexpect==4.9.0 +pkginfo==1.12.0 +platformdirs==4.3.6 +pluggy==1.5.0 +pre-commit==4.0.1 +prometheus-client==0.21.1 +prompt-toolkit==3.0.48 +psutil==6.1.1 +ptyprocess==0.7.0 +pure-eval==0.2.3 +pybtex==0.24.0 +pybtex-docutils==1.0.3 +pycparser==2.22 +pydata-sphinx-theme==0.16.1 +pygments==2.19.1 +pyright==1.1.391 +pytest==8.3.4 +pytest-cov==6.0.0 +pytest-xdist==3.6.1 +python-dateutil==2.9.0.post0 +python-json-logger==3.2.1 +pywin32==308 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32' +pywin32-ctypes==0.2.3 ; sys_platform == 'win32' +pywinpty==2.0.14 ; os_name == 'nt' +pyyaml==6.0.2 +pyzmq==26.2.0 +readme-renderer==44.0 +referencing==0.35.1 +requests==2.32.3 +requests-toolbelt==1.0.0 +restructuredtext-lint==1.4.0 +rfc3339-validator==0.1.4 +rfc3986==2.0.0 +rfc3986-validator==0.1.1 +rich==13.9.4 +rpds-py==0.22.3 +secretstorage==3.3.3 ; sys_platform == 'linux' +send2trash==1.8.3 +setuptools==75.8.0 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +snowballstemmer==2.2.0 +soupsieve==2.6 +sphinx==8.1.3 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-bibtex==2.6.3 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 +stack-data==0.6.3 +terminado==0.18.1 +tinycss2==1.4.0 +toml==0.10.2 +tomli==2.2.1 ; python_full_version <= '3.11' +tomli-w==1.1.0 +tomlkit==0.13.2 +tornado==6.4.2 +traitlets==5.14.3 +trove-classifiers==2025.1.10.15 +twine==6.0.1 +types-python-dateutil==2.9.0.20241206 +typing-extensions==4.12.2 +uri-template==1.3.0 +urllib3==2.3.0 +userpath==1.9.2 +uv==0.5.16 +virtualenv==20.28.1 +wcwidth==0.2.13 +webcolors==24.11.1 +webencodings==0.5.1 +websocket-client==1.8.0 +widgetsnbextension==4.0.13 +zipp==3.21.0 ; python_full_version < '3.12' +zstandard==0.23.0 diff --git a/tasks.py b/tasks.py index 1978d72..f71677d 100644 --- a/tasks.py +++ b/tasks.py @@ -13,19 +13,23 @@ import uuid from itertools import chain from textwrap import TextWrapper -from typing import Callable +from typing import TYPE_CHECKING import biblib.bib -from invoke.context import Context from invoke.tasks import task import colour_clf_io +if TYPE_CHECKING: + from collections.abc import Callable + + from invoke.context import Context + if not hasattr(inspect, "getargspec"): inspect.getargspec = inspect.getfullargspec # pyright: ignore __author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" @@ -73,7 +77,7 @@ def message_box( width: int = 79, padding: int = 3, print_callable: Callable = print, -): +) -> None: """ Print a message inside a box. @@ -123,7 +127,7 @@ def message_box( ideal_width = width - padding * 2 - 2 - def inner(text): + def inner(text: str) -> str: """Format and pads inner text for the message box.""" return ( @@ -148,7 +152,7 @@ def inner(text): @task -def literalise(ctx: Context): +def literalise(ctx: Context) -> None: """ Write various literals in the `colour.hints` module. @@ -171,7 +175,7 @@ def clean( docs: bool = True, bytecode: bool = False, pytest: bool = True, -): +) -> None: """ Clean the project. @@ -211,7 +215,7 @@ def formatting( ctx: Context, asciify: bool = True, bibtex: bool = True, -): +) -> None: """ Convert unicode characters to ASCII and cleanup the *BibTeX* file. @@ -254,7 +258,7 @@ def quality( ctx: Context, pyright: bool = True, rstlint: bool = True, -): +) -> None: """ Check the codebase with *Pyright* and lints various *restructuredText* files with *rst-lint*. @@ -279,7 +283,7 @@ def quality( @task -def precommit(ctx: Context): +def precommit(ctx: Context) -> None: """ Run the "pre-commit" hooks on the codebase. @@ -294,7 +298,7 @@ def precommit(ctx: Context): @task -def tests(ctx: Context): +def tests(ctx: Context) -> None: """ Run the unit tests with *Pytest*. @@ -315,7 +319,7 @@ def tests(ctx: Context): @task -def examples(ctx: Context, plots: bool = False): +def examples(ctx: Context, plots: bool = False) -> None: """ Run the examples. @@ -346,7 +350,7 @@ def examples(ctx: Context, plots: bool = False): @task(formatting, quality, precommit, tests, examples) -def preflight(ctx: Context): # noqa: ARG001 +def preflight(ctx: Context) -> None: # noqa: ARG001 """ Perform the preflight tasks, i.e., *formatting*, *tests*, *quality*, and *examples*. @@ -365,7 +369,7 @@ def docs( ctx: Context, html: bool = True, pdf: bool = True, -): +) -> None: """ Build the documentation. @@ -391,7 +395,7 @@ def docs( @task -def todo(ctx: Context): +def todo(ctx: Context) -> None: """ Export the TODO items. @@ -408,7 +412,7 @@ def todo(ctx: Context): @task -def requirements(ctx: Context): +def requirements(ctx: Context) -> None: """ Export the *requirements.txt* file. @@ -429,7 +433,7 @@ def requirements(ctx: Context): @task(literalise, clean, preflight, docs, todo, requirements) -def build(ctx: Context): +def build(ctx: Context) -> None: """ Build the project and runs dependency tasks, i.e., *docs*, *todo*, and *preflight*. @@ -442,7 +446,8 @@ def build(ctx: Context): message_box("Building...") if "modified: README.rst" in ctx.run("git status").stdout: # pyright: ignore - raise RuntimeError('Please commit your changes to the "README.rst" file!') + msg = 'Please commit your changes to the "README.rst" file!' + raise RuntimeError(msg) with open("README.rst") as readme_file: readme_content = readme_file.read() @@ -472,7 +477,7 @@ def build(ctx: Context): @task -def virtualise(ctx: Context, tests: bool = True): +def virtualise(ctx: Context, tests: bool = True) -> None: """ Create a virtual environment for the project build. @@ -505,7 +510,7 @@ def virtualise(ctx: Context, tests: bool = True): @task -def tag(ctx: Context): +def tag(ctx: Context) -> None: """ Tag the repository according to defined version using *git-flow*. @@ -519,7 +524,8 @@ def tag(ctx: Context): result = ctx.run("git rev-parse --abbrev-ref HEAD", hide="both") if result.stdout.strip() != "develop": # pyright: ignore - raise RuntimeError("Are you still on a feature or master branch?") + msg = "Are you still on a feature or master branch?" + raise RuntimeError(msg) with open(os.path.join(PYTHON_PACKAGE_NAME, "__init__.py")) as file_handle: file_content = file_handle.read() @@ -539,7 +545,7 @@ def tag(ctx: Context): 1 ) - version = ".".join((major_version, minor_version, change_version)) + version = f"{major_version}.{minor_version}.{change_version}" result = ctx.run("git ls-remote --tags upstream", hide="both") remote_tags = result.stdout.strip().split("\n") # pyright: ignore @@ -548,17 +554,18 @@ def tag(ctx: Context): tags.add(remote_tag.split("refs/tags/")[1].replace("refs/tags/", "^{}")) version_tags = sorted(tags) if f"v{version}" in version_tags: - raise RuntimeError( + msg = ( f'A "{PYTHON_PACKAGE_NAME}" "v{version}" tag already exists in ' f"remote repository!" ) + raise RuntimeError(msg) ctx.run(f"git flow release start v{version}") ctx.run(f"git flow release finish v{version}") @task(build) -def release(ctx: Context): +def release(ctx: Context) -> None: """ Release the project to *Pypi* with *Twine*. @@ -575,7 +582,7 @@ def release(ctx: Context): @task -def sha256(ctx: Context): +def sha256(ctx: Context) -> None: """ Compute the project *Pypi* package *sha256* with *OpenSSL*. diff --git a/utilities/export_todo.py b/utilities/export_todo.py index 8d18eb9..31bfced 100755 --- a/utilities/export_todo.py +++ b/utilities/export_todo.py @@ -9,7 +9,7 @@ import codecs import os -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" @@ -34,7 +34,7 @@ ----- | **Colour** by Colour Developers -| Copyright 2013 Colour Developers - \ +| Copyright 2024 Colour Developers - \ `colour-developers@colour-science.org `__ | This software is released under terms of BSD-3-Clause: \ https://opensource.org/licenses/BSD-3-Clause @@ -93,7 +93,7 @@ def extract_todo_items(root_directory: str) -> dict: return todo_items -def export_todo_items(todo_items: dict, file_path: str): +def export_todo_items(todo_items: dict, file_path: str) -> None: """ Export TODO items to given file. diff --git a/utilities/unicode_to_ascii.py b/utilities/unicode_to_ascii.py index 7bfbf7c..61faa92 100755 --- a/utilities/unicode_to_ascii.py +++ b/utilities/unicode_to_ascii.py @@ -10,7 +10,7 @@ import os import unicodedata -__copyright__ = "Copyright 2013 Colour Developers" +__copyright__ = "Copyright 2024 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" __maintainer__ = "Colour Developers" __email__ = "colour-developers@colour-science.org" @@ -31,7 +31,7 @@ } -def unicode_to_ascii(root_directory: str): +def unicode_to_ascii(root_directory: str) -> None: """ Recursively convert from unicode to ASCII *.py*, *.bib* and *.rst* files in given directory. From 29e0b5c38b52d8258f22e9cb2df6cfdc36c4d1bb Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 12 Jan 2025 16:49:10 +1300 Subject: [PATCH 2/5] Update documentation and various docstrings. --- .readthedocs.yaml | 3 - README.rst | 15 +- colour_clf_io/__init__.py | 26 +- colour_clf_io/elements.py | 443 ++++++++++++++++++++++-- colour_clf_io/errors.py | 6 +- colour_clf_io/parsing.py | 39 ++- colour_clf_io/process_list.py | 85 ++++- colour_clf_io/process_nodes.py | 210 +++++++---- colour_clf_io/tests/test_clf_common.py | 18 +- colour_clf_io/tests/test_clf_parsing.py | 46 +-- colour_clf_io/values.py | 61 +++- docs/_templates/class.rst | 8 + docs/colour_clf_io.rst | 75 ++++ docs/conf.py | 2 +- docs/index.rst | 13 +- docs/installation.rst | 4 +- 16 files changed, 853 insertions(+), 201 deletions(-) create mode 100644 docs/_templates/class.rst create mode 100644 docs/colour_clf_io.rst diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 46e7436..47935c5 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,9 +4,6 @@ build: os: ubuntu-20.04 tools: python: "3.11" - apt_packages: - - graphviz - - graphviz-dev sphinx: configuration: docs/conf.py diff --git a/README.rst b/README.rst index 7205525..dc1ab60 100644 --- a/README.rst +++ b/README.rst @@ -37,16 +37,16 @@ Features The following features are available: -- Reading CLF files to a Python representation. +- Reading *CLF* files to a Python representation. The following features are planned and in development: -- Writing CLF files from the Python representation. -- Validating CLF files according to the specification. +- Writing *CLF* files from the Python representation. +- Validating *CLF* files according to the specification. Features that will not be part of this library: -- Executing CLF workflows and applying them to colours or images. This feature will be implemented as part of `Colour +- Executing *CLF* workflows and applying them to colours or images. This feature will be implemented as part of `Colour `__. Examples @@ -94,9 +94,9 @@ Primary Dependencies **Colour - CLF IO** requires various dependencies in order to run: -- `python >= 3.9, < 4 `__ +- `python >= 3.10, < 4 `__ - `lxml >= 5.2.1 < 6 `__ -- `numpy >= 1.22, < 2 `__ +- `numpy >= 1.24, < 2 `__ Pypi ~~~~ @@ -161,7 +161,8 @@ The *Colour Developers* can be reached via different means: - `Facebook `__ - `Github Discussions `__ - `Gitter `__ -- `Twitter `__ +- `X `__ +- `Bluesky `__ About ----- diff --git a/colour_clf_io/__init__.py b/colour_clf_io/__init__.py index 5e68010..5501467 100644 --- a/colour_clf_io/__init__.py +++ b/colour_clf_io/__init__.py @@ -2,12 +2,12 @@ CLF Parsing =========== -Defines the functionality and data structures to parse CLF documents. +Defines the functionality and data structures to parse *CLF* files. The main functionality is exposed through the following two methods: -- :func:`colour.io.clf.read_clf`: Read a file in the CLF format and return the +- :func:`colour.io.clf.read_clf`: Read a file in the *CLF* format and return the corresponding :class: ProcessList. -- :func:`colour.io.clf.parse_clf`: Read a string that contains a CLF document and +- :func:`colour.io.clf.parse_clf`: Read a string that contains a *CLF* file and return the corresponding :class: ProcessList. References @@ -106,7 +106,7 @@ def read_clf(path: str | Path) -> ProcessList | None: """ - Read given *CLF* file and return the resulting `ProcessList`. + Read given *CLF* file and return a *ProcessList*. Parameters ---------- @@ -115,12 +115,13 @@ def read_clf(path: str | Path) -> ProcessList | None: Returns ------- - :class: colour.clf.ProcessList + :class:`colour_clf_io.ProcessList` + *ProcessList*. Raises ------ - :class: ParsingError - If the given file does not contain a valid CLF document. + :class:`colour_clf_io.errors.ParsingError` + If the given file does not contain a valid *CLF* file. """ xml = lxml.etree.parse(str(path)) # noqa: S320 @@ -131,21 +132,22 @@ def read_clf(path: str | Path) -> ProcessList | None: def parse_clf(text: str | bytes) -> ProcessList | None: """ - Read given string as a *CLF* document and return the resulting `ProcessList`. + Read given string as a *CLF* file and return a *ProcessList*. Parameters ---------- text - String that contains the *CLF* document. + String that contains the *CLF* file. Returns ------- - :class: colour.clf.ProcessList. + :class:`colour_clf_io.ProcessList` + *ProcessList*. Raises ------ - :class: ParsingError - If the given string does not contain a valid CLF document. + :class:`colour_clf_io.errors.ParsingError` + If the given string does not contain a valid *CLF* file. """ xml = lxml.etree.fromstring(text) # noqa: S320 diff --git a/colour_clf_io/elements.py b/colour_clf_io/elements.py index 4423921..31a5826 100644 --- a/colour_clf_io/elements.py +++ b/colour_clf_io/elements.py @@ -2,8 +2,8 @@ Elements ======== -Defines objects that hold data from elements contained in a CLF document. These -typically are child elements of Process Nodes. +Defines objects that hold data from elements contained in a *CLF* file. These +typically are child elements of *Process* Nodes. """ from __future__ import annotations @@ -22,10 +22,10 @@ from colour_clf_io.parsing import ( ParserConfig, XMLParsable, + check_none, child_element, child_element_or_exception, map_optional, - must_have, retrieve_attributes, retrieve_attributes_as_float, three_floats, @@ -56,7 +56,17 @@ @dataclass class Array(XMLParsable): """ - Represents an Array element. + Represent an *Array* element. + + Attributes + ---------- + - :attr:`~colour_clf_io.Array.values` + - :attr:`~colour_clf_io.Array.dim` + + Methods + ------- + - :meth:`~colour_clf_io.Array.from_xml` + - :meth:`~colour_clf_io.Array.as_array` References ---------- @@ -64,7 +74,13 @@ class Array(XMLParsable): """ values: list[float] + """Values contained by the element.""" + dim: tuple[int, ...] + """ + Specifies the dimension of the LUT or the matrix and the number of + colour components. + """ @staticmethod def from_xml( @@ -73,11 +89,18 @@ def from_xml( ) -> Array | None: """ Parse and return a :class:`colour_clf_io.Array` class instance from the - given XML node. Returns `None`` if the given XML node is ``None``. + given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.Array` or :py:data:`None` @@ -85,7 +108,7 @@ def from_xml( Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -95,7 +118,7 @@ def from_xml( return None dim = xml.get("dim") - must_have( + check_none( xml, 'Array must have a "dim" attribute', ) @@ -107,7 +130,7 @@ def from_xml( def as_array(self) -> npt.NDArray: """ - Convert the CLF element into a numpy array. + Convert the *CLF* element into a numpy array. Returns ------- @@ -127,7 +150,22 @@ def as_array(self) -> npt.NDArray: @dataclass class CalibrationInfo(XMLParsable): """ - Represents a Calibration Info element. + Represent a *CalibrationInfo* container element for a + :class:`colour_clf_io.ProcessList` class instance. + + Attributes + ---------- + - :attr:`~colour_clf_io.CalibrationInfo.display_device_serial_num` + - :attr:`~colour_clf_io.CalibrationInfo.display_device_host_name` + - :attr:`~colour_clf_io.CalibrationInfo.operator_name` + - :attr:`~colour_clf_io.CalibrationInfo.calibration_date_time` + - :attr:`~colour_clf_io.CalibrationInfo.measurement_probe` + - :attr:`~colour_clf_io.CalibrationInfo.calibration_software_name` + - :attr:`~colour_clf_io.CalibrationInfo.calibration_software_version` + + Methods + ------- + - :meth:`~colour_clf_io.CalibrationInfo.from_xml` References ---------- @@ -149,11 +187,19 @@ def from_xml( ) -> CalibrationInfo | None: """ Parse and return a :class:`colour_clf_io.CalibrationInfo` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + from the given XML element. Returns `None`` if the given XML element is + ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.CalibrationInfo` or :py:data:`None` @@ -161,7 +207,7 @@ def from_xml( Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -188,7 +234,13 @@ def from_xml( class RangeStyle(enum.Enum): """ - Represents the valid values of the style attribute within a Range element. + Represent the valid values of the *style* attribute of a + :class:`colour_clf_io.Range` *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.RangeStyle.CLAMP` + - :attr:`~colour_clf_io.RangeStyle.NO_CLAMP` References ---------- @@ -196,12 +248,32 @@ class RangeStyle(enum.Enum): """ CLAMP = "Clamp" + """ + Clamping is applied upon the result of the scale and offset expressed by + the result of the non-clamping Range equation.""" + NO_CLAMP = "noClamp" + """ + Scale and offset is applied without clamping (i.e., values below + minOutValue or above maxOutValue are preserved). + """ class LogStyle(enum.Enum): """ - Represents the valid values of the style attribute in a Log element. + Represent the valid values of the *style* attribute of a + :class:`colour_clf_io.Log` *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.LogStyle.LOG_10` + - :attr:`~colour_clf_io.LogStyle.ANTI_LOG_10` + - :attr:`~colour_clf_io.LogStyle.LOG_2` + - :attr:`~colour_clf_io.LogStyle.ANTI_LOG_2` + - :attr:`~colour_clf_io.LogStyle.LIN_TO_LOG` + - :attr:`~colour_clf_io.LogStyle.LOG_TO_LIN` + - :attr:`~colour_clf_io.LogStyle.CAMERA_LIN_TO_LOG` + - :attr:`~colour_clf_io.LogStyle.CAMERA_LOG_TO_LIN` References ---------- @@ -209,18 +281,53 @@ class LogStyle(enum.Enum): """ LOG_10 = "log10" + """Apply a base 10 logarithm.""" + ANTI_LOG_10 = "antiLog10" + """Apply a base 10 anti-logarithm.""" + LOG_2 = "log2" + """Apply a base 2 logarithm.""" + ANTI_LOG_2 = "antiLog2" + """Apply a base 2 anti-logarithm.""" + LIN_TO_LOG = "linToLog" + """Apply a logarithm.""" + LOG_TO_LIN = "logToLin" + """Apply an anti-logarithm.""" + CAMERA_LIN_TO_LOG = "cameraLinToLog" + """ + Apply a piecewise function with logarithmic and linear segments on linear + values, converting them to non-linear values. + """ + CAMERA_LOG_TO_LIN = "cameraLogToLin" + """ + Applies a piecewise function with logarithmic and linear segments on + non-linear values, converting them to linear values. + """ class ExponentStyle(enum.Enum): """ - Represents the valid values of the style attribute of an Exponent element. + Represent the valid values of the *style* attribute of a + :class:`colour_clf_io.Exponent` *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.ExponentStyle.BASIC_FWD` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_REV` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_MIRROR_FWD` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_MIRROR_REV` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_PASS_THRU_FWD` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_PASS_THRU_REV` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_FWD` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_REV` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_MIRROR_FWD` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_MIRROR_REV` References ---------- @@ -228,21 +335,87 @@ class ExponentStyle(enum.Enum): """ BASIC_FWD = "basicFwd" + """ + Apply a power law using the exponent value specified in the ExponentParams + element. + """ + BASIC_REV = "basicRev" + """ + Apply a power law using the exponent value specified in the ExponentParams + element. + """ + BASIC_MIRROR_FWD = "basicMirrorFwd" + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and mirror + the function for values less than zero (i.e., rotationally symmetric around + the origin). + """ + BASIC_MIRROR_REV = "basicMirrorRev" + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and mirror + the function for values less than zero (i.e., rotationally symmetric around + the origin). + """ + BASIC_PASS_THRU_FWD = "basicPassThruFwd" # noqa: S105 + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and passes + values less than zero unchanged. + """ + BASIC_PASS_THRU_REV = "basicPassThruRev" # noqa: S105 + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and passes + values less than zero unchanged. + """ + MON_CURVE_FWD = "monCurveFwd" + """ + Apply a power law function with a linear segment near the origin. + """ + MON_CURVE_REV = "monCurveRev" + """ + Apply a power law function with a linear segment near the origin. + """ + MON_CURVE_MIRROR_FWD = "monCurveMirrorFwd" + """ + Apply a power law function with a linear segment near the origin and + mirror the function for values less than zero (i.e., rotationally symmetric + around the origin). + """ + MON_CURVE_MIRROR_REV = "monCurveMirrorRev" + """ + Apply a power law function with a linear segment near the origin and mirror + the function for values less than zero (i.e., rotationally symmetric around + the origin). + """ @dataclass class SOPNode(XMLParsable): """ - Represents a SOPNode element. + Represent a *SOPNode* element for a :class:`colour_clf_io.ASC_CDL` + *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.SOPNode.slope` + - :attr:`~colour_clf_io.SOPNode.offset` + - :attr:`~colour_clf_io.SOPNode.power` + + Methods + ------- + - :meth:`~colour_clf_io.SOPNode.from_xml` References ---------- @@ -250,8 +423,27 @@ class SOPNode(XMLParsable): """ slope: tuple[float, float, float] + """ + Three decimal values representing the R, G, and B slope values, which is + similar to gain, but changes the slope of the transfer function without + shifting the black level established by offset. Valid values for slope must + be greater than or equal to zero. The nominal value is 1.0 for all channels. + """ + offset: tuple[float, float, float] + """ + Three decimal values representing the R, G, and B offset values, which + raise or lower overall brightness of a color component by shifting the + transfer function up or down while holding the slope constant. The nominal + value is 0.0 for all channels. + """ + power: tuple[float, float, float] + """ + Three decimal values representing the R, G, and B power values, which + change the intermediate shape of the transfer function. Valid values for + power must be greater than zero. The nominal value is 1.0 for all channels. + """ @staticmethod def from_xml( @@ -259,11 +451,18 @@ def from_xml( ) -> SOPNode | None: """ Parse and return a :class:`colour_clf_io.SOPNode` class instance from the - given XML node. Returns `None`` if the given XML node is ``None``. + given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.SOPNode` or :py:data:`None` @@ -271,7 +470,7 @@ def from_xml( Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -290,7 +489,16 @@ def from_xml( @dataclass class SatNode(XMLParsable): """ - Represents a SatNode element. + Represent a *SatNode* element for a :class:`colour_clf_io.ASC_CDL` + *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.SatNode.saturation` + + Methods + ------- + - :meth:`~colour_clf_io.SatNode.from_xml` References ---------- @@ -298,6 +506,10 @@ class SatNode(XMLParsable): """ saturation: float + """ + A single decimal value applied to all color channels. Valid values for + saturation must be greater than or equal to zero. The nominal value is 1.0. + """ @staticmethod def from_xml( @@ -305,11 +517,18 @@ def from_xml( ) -> SatNode | None: """ Parse and return a :class:`colour_clf_io.SatNode` class instance from the - given XML node. Returns `None`` if the given XML node is ``None``. + given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.SatNode` or :py:data:`None` @@ -317,7 +536,7 @@ def from_xml( Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -340,7 +559,21 @@ def from_xml( @dataclass class Info(XMLParsable): """ - Represents a Info element. + Represent an *Info* element. + + Attributes + ---------- + - :attr:`~colour_clf_io.Info.app_release` + - :attr:`~colour_clf_io.Info.copyright` + - :attr:`~colour_clf_io.Info.revision` + - :attr:`~colour_clf_io.Info.aces_transform_id` + - :attr:`~colour_clf_io.Info.aces_user_name` + - :attr:`~colour_clf_io.Info.calibration_info` + - :attr:`~colour_clf_io.Info.saturation` + + Methods + ------- + - :meth:`~colour_clf_io.Info.from_xml` References ---------- @@ -348,21 +581,55 @@ class Info(XMLParsable): """ app_release: str | None + """A string used for indicating application software release level.""" + copyright: str | None + """A string containing a copyright notice for authorship of the *CLF* file.""" + revision: str | None + """ + A string used to track the version of the LUT itself (e.g., an increased + resolution from a previous version of the LUT). + """ + aces_transform_id: str | None + """ + A string containing an ACES transform identifier as described in + Academy S-2014-002. If the transform described by the ProcessList is the + concatenation of several ACES transforms, this element may contain several + ACES Transform IDs, separated by white space or line separators. This + element is mandatory for ACES transforms and may be referenced from ACES + Metadata Files. + """ + aces_user_name: str | None + """ + A string containing the user-friendly name recommended for use in product + user interfaces as described in Academy TB-2014-002. + """ + calibration_info: CalibrationInfo | None + """ + Container element for calibration metadata used when making a LUT for a + specific device. + """ @staticmethod def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Info | None: """ Parse and return a :class:`colour_clf_io.Info` class instance from the - given XML node. Returns `None`` if the given XML node is ``None``. + given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.Info` or :py:data:`None` @@ -370,7 +637,7 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Info | No Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -400,7 +667,23 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Info | No @dataclass class LogParams(XMLParsable): """ - Represents a Log Param List element. + Represent a *LogParams* element for a :class:`colour_clf_io.Log` + *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.LogParams.base` + - :attr:`~colour_clf_io.LogParams.log_side_slope` + - :attr:`~colour_clf_io.LogParams.log_side_offset` + - :attr:`~colour_clf_io.LogParams.lin_side_slope` + - :attr:`~colour_clf_io.LogParams.lin_side_offset` + - :attr:`~colour_clf_io.LogParams.lin_side_break` + - :attr:`~colour_clf_io.LogParams.linear_slope` + - :attr:`~colour_clf_io.LogParams.channel` + + Methods + ------- + - :meth:`~colour_clf_io.LogParams.from_xml` References ---------- @@ -408,13 +691,54 @@ class LogParams(XMLParsable): """ base: float | None + """The base of the logarithmic function. Default is 2.""" + log_side_slope: float | None + """ + Slope" (or gain) applied to the log side of the logarithmic segment. Default is 1. + """ + log_side_offset: float | None + """ + Offset applied to the log side of the logarithmic segment. Default is 0. + """ + lin_side_slope: float | None + """ + Slope of the linear side of the logarithmic segment. Default is 1. + """ + lin_side_offset: float | None + """ + Offset applied to the linear side of the logarithmic segment. Default is 0. + """ + lin_side_break: float | None + """ + The break-point, defined in linear space, at which the piece-wise function + transitions between the logarithmic and linear segments. This is required + if style="cameraLinToLog" or "cameraLogToLin". + """ + linear_slope: float | None + """ + The slope of the linear segment of the piecewise function. This attribute + does not need to be provided unless the formula being implemented requires + it. The default is to calculate using linSideBreak such that the linear + portion is continuous in value with the logarithmic portion of the curve, + by using the value of the logarithmic portion of the curve at the break-point. + """ + channel: Channel | None + """ + The colour channel to which the exponential function is applied. Possible + values are "R", "G", "B". If this attribute is utilized to target different + adjustments per channel, then up to three *LogParams* elements may be used, + provided that "channel" is set differently in each. However, the same value + of base must be used for all channels. If this attribute is not otherwise + specified, the logarithmic function is applied identically to all three + colour channels. + """ @staticmethod def from_xml( @@ -423,11 +747,18 @@ def from_xml( ) -> LogParams | None: """ Parse and return a :class:`colour_clf_io.LogParams` class instance from - the given XML node. Returns `None`` if the given XML node is ``None``. + the given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.LogParams` or :py:data:`None` @@ -435,7 +766,7 @@ def from_xml( Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -465,7 +796,18 @@ def from_xml( @dataclass class ExponentParams(XMLParsable): """ - Represents a Exponent Params element. + Represent a *ExponentParams* element for a :class:`colour_clf_io.Exponent` + *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.ExponentParams.exponent` + - :attr:`~colour_clf_io.ExponentParams.offset` + - :attr:`~colour_clf_io.ExponentParams.channel` + + Methods + ------- + - :meth:`~colour_clf_io.ExponentParams.from_xml` References ---------- @@ -473,8 +815,29 @@ class ExponentParams(XMLParsable): """ exponent: float + """ + The power to which the value is to be raised. If style is any of the + "monCurve" types, the valid range is [1.0, 10.0]. The nominal value is 1.0. + """ + offset: float | None + """ + The offset value to use. If offset is used, the enclosing Exponent + element's style attribute must be set to one of the "monCurve" types. + Offset is not allowed when style is any of the "basic" types. The valid + range is [0.0, 0.9]. The nominal value is 0.0. + """ + channel: Channel | None + """ + The colour channel to which the exponential function is applied. Possible + values are "R", "G", "B". If this attribute is utilized to target different + adjustments per channel, then up to three *ExponentParams* elements may be used, + provided that "channel" is set differently in each. However, the same value + of base must be used for all channels. If this attribute is not otherwise + specified, the logarithmic function is applied identically to all three + colour channels. + """ @staticmethod def from_xml( @@ -483,11 +846,19 @@ def from_xml( ) -> ExponentParams | None: """ Parse and return a :class:`colour_clf_io.ExponentParams` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + from the given XML element. Returns `None`` if the given XML element is + ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.ExponentParams` or :py:data:`None` @@ -495,7 +866,7 @@ def from_xml( Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. diff --git a/colour_clf_io/errors.py b/colour_clf_io/errors.py index 62e586c..8e812f8 100644 --- a/colour_clf_io/errors.py +++ b/colour_clf_io/errors.py @@ -2,7 +2,7 @@ Errors ====== -Defines errors that are used as part of the parsing and validation of CLF documents. +Defines errors that are used as part of the parsing and validation of *CLF* files. """ from __future__ import annotations @@ -22,11 +22,11 @@ class ParsingError(Exception): """ - Indicates an error with parsing a CLF document. + Indicate an error with parsing a *CLF* file. """ class ValidationError(Exception): """ - Indicates a semantic error with the data in a CLF document. + Indicate a semantic error with the data in a *CLF* file. """ diff --git a/colour_clf_io/parsing.py b/colour_clf_io/parsing.py index 0cd856e..a0e8110 100644 --- a/colour_clf_io/parsing.py +++ b/colour_clf_io/parsing.py @@ -2,7 +2,7 @@ Parsing ======= -Defines utilities that are used to parse CLF documents. +Defines utilities that are used to parse *CLF* files. """ from __future__ import annotations @@ -54,20 +54,25 @@ @dataclass class ParserConfig: """ - Additional settings for parsing the CLF document. + Additional settings for parsing the *CLF* file. - Parameters + Attributes ---------- - namespace_name - The namespace name used for parsing the CLF document. Usually this should be - the `CLF_NAMESPACE`, but it can be omitted. + - :attr:`~colour_clf_io.ParserConfig.namespace_name` + + Methods + ------- + - :meth:`~colour_clf_io.ParserConfig.clf_namespace_prefix_mapping` """ namespace_name: str | None = NAMESPACE_NAME + """ + The namespace name used for parsing the *CLF* file. Usually this should + be the `CLF_NAMESPACE`, but it can be omitted.""" def clf_namespace_prefix_mapping(self) -> dict[str, str] | None: """ - Return the namespaces prefix mapping used for CLF documents. + Return the namespaces prefix mapping used for *CLF* files. Returns ------- @@ -83,7 +88,7 @@ def clf_namespace_prefix_mapping(self) -> dict[str, str] | None: class XMLParsable(ABC): """ - Define the base class for objects that can be generated from XML documents. + Define the base class for objects that can be generated from XML files. This is an :class:`ABCMeta` abstract class that must be inherited by sub-classes. @@ -104,9 +109,9 @@ def from_xml( Parameters ---------- xml - XML document to read. + XML file to read. config - Additional settings for parsing the document. + Additional settings for parsing the file. Returns ------- @@ -117,14 +122,14 @@ def from_xml( def map_optional(function: Callable, value: Any | None) -> Any: """ - Apply ``function`` to value, if ``value`` is not ``None``. + Apply the given function to given ``value`` if ``value`` is not ``None``. Parameters ---------- function The function to apply. value - The value to apply the function onto + The value to apply the function onto. Returns ------- @@ -149,7 +154,7 @@ def retrieve_attributes( Parameters ---------- xml - The XML element to retrieve attributes from. + the XML element to retrieve attributes from. attribute_mapping The dictionary containing keys and attribute names. @@ -177,7 +182,7 @@ def retrieve_attributes_as_float( Parameters ---------- xml - The XML element to retrieve attributes from. + the XML element to retrieve attributes from. attribute_mapping The dictionary containing keys and attribute names. @@ -216,7 +221,7 @@ def check_none(value: T | None, message: str) -> TypeGuard[T]: Raises ------ - :class:`ParsingError` if `value` is :py:data:`None`. + :class:`colour_clf_io.errors.ParsingError` if `value` is :py:data:`None`. Returns ------- @@ -325,7 +330,7 @@ def child_element_or_exception( Raises ------ - :class:`ParsingError` if the child element is not found. + :class:`colour_clf_io.errors.ParsingError` if the child element is not found. Returns ------- @@ -482,7 +487,7 @@ def three_floats(text: str | None) -> tuple[float, float, float]: Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If `text` is :py:data:`None`, or cannot be parsed as three floats. Returns diff --git a/colour_clf_io/process_list.py b/colour_clf_io/process_list.py index ae7ccb9..8e05170 100644 --- a/colour_clf_io/process_list.py +++ b/colour_clf_io/process_list.py @@ -1,8 +1,8 @@ """ -Process List +*ProcessList* ============ -Defines the top level Process List object that represents a CLF process. +Defines the top level *ProcessList* object that represents a *CLF* process. """ from __future__ import annotations @@ -16,9 +16,9 @@ from colour_clf_io.errors import ParsingError from colour_clf_io.parsing import ( ParserConfig, + check_none, element_as_text, elements_as_text_list, - must_have, ) from colour_clf_io.process_nodes import ( ProcessNode, @@ -39,7 +39,24 @@ @dataclass class ProcessList: """ - Represents a Process List. + Represent a *ProcessList*, the root element for any *CLF* file. It is + composed of one or more :class:`colour_clf_io.ProcessNodes` class instances. + + Attributes + ---------- + - :attr:`~colour_clf_io.ProcessList.id` + - :attr:`~colour_clf_io.ProcessList.compatible_CLF_version` + - :attr:`~colour_clf_io.ProcessList.name` + - :attr:`~colour_clf_io.ProcessList.inverse_of` + - :attr:`~colour_clf_io.ProcessList.description` + - :attr:`~colour_clf_io.ProcessList.input_descriptor` + - :attr:`~colour_clf_io.ProcessList.output_descriptor` + - :attr:`~colour_clf_io.ProcessList.info` + - :attr:`~colour_clf_io.ProcessList.process_nodes` + + Methods + ------- + - :meth:`~colour_clf_io.ProcessList.from_xml` References ---------- @@ -47,27 +64,73 @@ class ProcessList: """ id: str + """A string to serve as a unique identifier of the *ProcessList*.""" + compatible_CLF_version: str - process_nodes: list[ProcessNode] + """ + A string indicating the minimum compatible CLF specification version + required to read this file. The compCLFversion corresponding to this + version of the specification is be "3.0". + """ name: str | None + """ + A concise string used as a text name of the *ProcessList* for display or + selection from an application's user interface. + """ + inverse_of: str | None + """ + A string for linking to another *ProcessList* id (unique) which is the + inverse of this one. + """ description: list[str] + """ + A list for comments describing the function, usage, or any notes about + the *ProcessList*. + """ + input_descriptor: str | None + """ + An arbitrary string used to describe the intended source code values of the + *ProcessList*. + """ + output_descriptor: str | None + """ + An arbitrary string used to describe the intended output target of the + *ProcessList* (e.g., target display). + """ + + process_nodes: list[ProcessNode] + """ + A list of colour operators. The *ProcessList* must contain at least one + *ProcessNode*. + """ info: Info | None + """ + Optional element for including additional custom metadata not needed to + interpret the transforms. + """ + """""" @staticmethod def from_xml(xml: lxml.etree._Element | None) -> ProcessList | None: """ Parse and return a :class:`colour_clf_io.ProcessList` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + from the given XML element. Returns `None`` if the given XML element is + ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + Returns ------- class:`colour_clf_io.ProcessList` or :py:data:`None` @@ -75,7 +138,7 @@ def from_xml(xml: lxml.etree._Element | None) -> ProcessList | None: Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -85,10 +148,10 @@ def from_xml(xml: lxml.etree._Element | None) -> ProcessList | None: return None id_ = xml.get("id") - must_have(id_, "ProcessList must contain an `id` attribute") + check_none(id_, "ProcessList must contain an `id` attribute") compatible_clf_version = xml.get("compCLFversion") - must_have( + check_none( compatible_clf_version, 'ProcessList must contain a "compCLFversion" attribute', ) @@ -101,7 +164,7 @@ def from_xml(xml: lxml.etree._Element | None) -> ProcessList | None: if not namespace: config.namespace_name = None elif namespace != config.namespace_name: - exception = f"Found invalid xmlns attribute in process list: {namespace}" + exception = f"Found invalid xmlns attribute in *ProcessList*: {namespace}" raise ParsingError(exception) diff --git a/colour_clf_io/process_nodes.py b/colour_clf_io/process_nodes.py index 5eea963..cfc792d 100644 --- a/colour_clf_io/process_nodes.py +++ b/colour_clf_io/process_nodes.py @@ -2,7 +2,7 @@ Process Nodes ============ -Defines the available process nodes in a CLF document. +Defines the available process nodes in a *CLF* file. """ from __future__ import annotations @@ -97,7 +97,12 @@ def register(constructor: Callable) -> Callable: @dataclass class ProcessNode(XMLParsable, ABC): """ - Represents the common data of all Process Node elements. + Represent a *ProcessNode*, an operation to be applied to the image data. + + At least one *ProcessNode* sub-class must be included in a + :class:`colour_clf_io.ProcessList` class instance. The base *ProcessNode* + class contains attributes and elements that are common to and inherited + by the specific sub-types of the *ProcessNode* class. References ---------- @@ -105,23 +110,44 @@ class ProcessNode(XMLParsable, ABC): """ id: str | None + """A unique identifier for the *ProcessNode*.""" + name: str | None + """ + A concise string defining a name for the *ProcessNode* that can be used + by an application for display in a user interface. + """ + in_bit_depth: BitDepth + """ + A string that is used by some *ProcessNodes* to indicate how array or + parameter values have been scaled. + """ + out_bit_depth: BitDepth + """ + A string that is used by some ProcessNodes to indicate how array or + parameter values have been scaled. + """ + description: str | None + """ + An arbitrary string for describing the function, usage, or notes about the + *ProcessNode*. + """ @staticmethod def parse_attributes(xml: lxml.etree._Element, config: ParserConfig) -> dict: """ - Parse the default attributes of a *ProcessNode* and return them as a dictionary - of names and their values. + Parse the default attributes of a *ProcessNode* and return them as a + dictionary of names and their values. Parameters ---------- xml - Source XML element. + XML element to parse. config - Additional parser configuration. + XML parser config. Returns ------- @@ -149,7 +175,8 @@ def parse_attributes(xml: lxml.etree._Element, config: ParserConfig) -> dict: def assert_bit_depth_compatibility(process_nodes: list[ProcessNode]) -> bool: - """Check that the input and output values of adjacent process nodes are + """ + Check that the input and output values of adjacent process nodes are compatible. Return true if all nodes are compatible, false otherwise. Examples @@ -204,7 +231,14 @@ def assert_bit_depth_compatibility(process_nodes: list[ProcessNode]) -> bool: def parse_process_node(xml: lxml.etree._Element, config: ParserConfig) -> ProcessNode: """ - Return the correct process node that corresponds to this XML element. + Return the *ProcessNode* that corresponds to given XML element. + + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. Returns ------- @@ -232,7 +266,7 @@ def parse_process_node(xml: lxml.etree._Element, config: ParserConfig) -> Proces @dataclass class LUT1D(ProcessNode): """ - Represents a LUT1D element. + Represent a *LUT1D* element. References ---------- @@ -248,12 +282,19 @@ class LUT1D(ProcessNode): @register_process_node_xml_constructor("LUT1D") def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT1D | None: """ - Parse and return a :class:`colour_clf_io.LUT1D` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + Parse and return a :class:`colour_clf_io.LUT1D` class instance from the + given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.LUT1D` or :py:data:`None` @@ -261,7 +302,7 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT1D | N Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -293,7 +334,7 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT1D | N @dataclass class LUT3D(ProcessNode): """ - Represents a LUT3D element. + Represent a *LUT3D* element. References ---------- @@ -309,12 +350,19 @@ class LUT3D(ProcessNode): @register_process_node_xml_constructor("LUT3D") def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT3D | None: """ - Parse and return a :class:`colour_clf_io.LUT3D` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + Parse and return a :class:`colour_clf_io.LUT3D` class instance from the + given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF + Expects the XML element to be a valid element according to the *CLF* specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. + Returns ------- class:`colour_clf_io.LUT3D` or :py:data:`None` @@ -322,7 +370,7 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT3D | N Raises ------ - :class:`ParsingError` + :class:`colour_clf_io.errors.ParsingError` If the node does not conform to the specification, a ``ParsingError`` exception will be raised. The error message will indicate the details of the issue that was encountered. @@ -355,7 +403,7 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT3D | N @dataclass class Matrix(ProcessNode): """ - Represents a Matrix element. + Represent a *Matrix* element. References ---------- @@ -370,11 +418,18 @@ def from_xml( xml: lxml.etree._Element | None, config: ParserConfig ) -> Matrix | None: """ - Parse and return a :class:`colour_clf_io.Matrix` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + Parse and return a :class:`colour_clf_io.Matrix` class instance from + the given XML element. Returns `None`` if the given XML element is ``None``. + + Expects the XML element to be a valid element according to the *CLF* + specification. - Expects the xml element to be a valid element according to the CLF - specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. Returns ------- @@ -383,10 +438,10 @@ def from_xml( Raises ------ - :class:`ParsingError` - If the node does not conform to the specification, a ``ParsingError`` - exception will be raised. The error message will indicate the - details of the issue that was encountered. + :class:`colour_clf_io.errors.ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ if xml is None: @@ -406,7 +461,7 @@ def from_xml( @dataclass class Range(ProcessNode): """ - Represents a Range element. + Represent a *Range* element. References ---------- @@ -424,11 +479,18 @@ class Range(ProcessNode): @register_process_node_xml_constructor("Range") def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Range | None: """ - Parse and return a :class:`colour_clf_io.Range` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + Parse and return a :class:`colour_clf_io.Range` class instance from the + given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF - specification. + Expects the XML element to be a valid element according to the *CLF* + specification. + + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. Returns ------- @@ -437,10 +499,10 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Range | N Raises ------ - :class:`ParsingError` - If the node does not conform to the specification, a ``ParsingError`` - exception will be raised. The error message will indicate the - details of the issue that was encountered. + :class:`colour_clf_io.errors.ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ if xml is None: @@ -473,7 +535,7 @@ def optional_float(name: str) -> float | None: @dataclass class Log(ProcessNode): """ - Represents a Log element. + Represent a *Log* element. References ---------- @@ -487,11 +549,18 @@ class Log(ProcessNode): @register_process_node_xml_constructor("Log") def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Log | None: """ - Parse and return a :class:`colour_clf_io.Log` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + Parse and return a :class:`colour_clf_io.Log` class instance from the + given XML element. Returns `None`` if the given XML element is ``None``. + + Expects the XML element to be a valid element according to the *CLF* + specification. - Expects the xml element to be a valid element according to the CLF - specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. Returns ------- @@ -500,10 +569,10 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Log | Non Raises ------ - :class:`ParsingError` - If the node does not conform to the specification, a ``ParsingError`` - exception will be raised. The error message will indicate the - details of the issue that was encountered. + :class:`colour_clf_io.errors.ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ if xml is None: @@ -527,7 +596,7 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Log | Non @dataclass class Exponent(ProcessNode): """ - Represents a Exponent element. + Represent an *Exponent* element. References ---------- @@ -543,24 +612,30 @@ def from_xml( xml: lxml.etree._Element | None, config: ParserConfig ) -> Exponent | None: """ - Parse and return a :class:`colour_clf_io.Exponent` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + Parse and return a :class:`colour_clf_io.Exponent` class instance from + the given XML element. Returns `None`` if the given XML element is ``None``. + + Expects the XML element to be a valid element according to the *CLF* + specification. - Expects the xml element to be a valid element according to the CLF - specification. + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. Returns ------- class:`colour_clf_io.Exponent` or :py:data:`None` Parsed XML node. - Raises ------ - :class:`ParsingError` - If the node does not conform to the specification, a ``ParsingError`` - exception will be raised. The error message will indicate the - details of the issue that was encountered. + :class:`colour_clf_io.errors.ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ if xml is None: @@ -595,7 +670,7 @@ def from_xml( @dataclass class ASC_CDL(ProcessNode): """ - Represents a ASC_CDL element. + Represent an *ASC_CDL* element. References ---------- @@ -612,11 +687,18 @@ def from_xml( xml: lxml.etree._Element | None, config: ParserConfig ) -> ASC_CDL | None: """ - Parse and return a :class:`colour_clf_io.ASC_CDL` class instance - from the given XML node. Returns `None`` if the given XML node is ``None``. + Parse and return a :class:`colour_clf_io.ASC_CDL` class instance from + the given XML element. Returns `None`` if the given XML element is ``None``. - Expects the xml element to be a valid element according to the CLF - specification. + Expects the XML element to be a valid element according to the *CLF* + specification. + + Parameters + ---------- + xml + XML element to parse. + config + XML parser config. Returns ------- @@ -625,10 +707,10 @@ def from_xml( Raises ------ - :class:`ParsingError` - If the node does not conform to the specification, a ``ParsingError`` - exception will be raised. The error message will indicate the - details of the issue that was encountered. + :class:`colour_clf_io.errors.ParsingError` + If the node does not conform to the specification, a ``ParsingError`` + exception will be raised. The error message will indicate the + details of the issue that was encountered. """ if xml is None: diff --git a/colour_clf_io/tests/test_clf_common.py b/colour_clf_io/tests/test_clf_common.py index 8d16392..0de0100 100644 --- a/colour_clf_io/tests/test_clf_common.py +++ b/colour_clf_io/tests/test_clf_common.py @@ -1,5 +1,5 @@ """ -Defines helper functionality for CLF tests. +Defines helper functionality for *CLF* tests. """ from __future__ import annotations @@ -39,22 +39,22 @@ def wrap_snippet(snippet: str) -> str: """ - Take a string that should contain the text representation of a CLF node, and - returns valid CLF document. Essentially the given string is pasted into the - `ProcessList` if a CLF document. + Take a string that should contain the text representation of a *CLF* node, and + returns valid *CLF* file. Essentially the given string is pasted into the + `ProcessList` if a *CLF* file. - This is useful to quickly convert example snippets of Process Nodes into valid CLF - documents for parsing. + This is useful to quickly convert example snippets of Process Nodes into valid *CLF* + files for parsing. Parameters ---------- snippet - Snippet to wrap as a CLF document. + Snippet to wrap as a *CLF* file. Returns ------- :class:`str` - CLF document. + *CLF* file. """ return EXAMPLE_WRAPPER.format(snippet) @@ -64,7 +64,7 @@ def snippet_to_process_list( snippet: str, ) -> colour_clf_io.process_list.ProcessList | None: """ - Take a string that should contain a valid body for an XML Process List and + Take a string that should contain a valid body for an XML *ProcessList* and returns the parsed :class:`colour_clf_io.process_list.ProcessList` class instance. diff --git a/colour_clf_io/tests/test_clf_parsing.py b/colour_clf_io/tests/test_clf_parsing.py index c438855..025c4be 100644 --- a/colour_clf_io/tests/test_clf_parsing.py +++ b/colour_clf_io/tests/test_clf_parsing.py @@ -40,18 +40,18 @@ class TestParseCLF: """ - Define tests methods for parsing CLF files using the functionality provided in - the :mod: `colour.io.clf`module. + Define tests methods for parsing *CLF* files using the functionality + provided in the :mod: `colour.io.clf`module. """ def test_read_sample_document_1(self) -> None: """ - Test parsing of the sample document `ACES2065_1_to_ACEScct.xml`. + Test parsing of the sample file `ACES2065_1_to_ACEScct.xml`. """ clf_data = read_clf(os.path.join(ROOT_CLF, "ACES2065_1_to_ACEScct.xml")) - assert clf_data is not None + assert clf_data is not None assert clf_data.description == ["Conversion from linear ACES2065-1 to ACEScct"] assert clf_data.input_descriptor == "ACES (SMPTE ST 2065-1)" assert clf_data.output_descriptor == "ACEScct" @@ -73,7 +73,7 @@ def test_read_sample_document_1(self) -> None: def test_read_sample_document_2(self) -> None: """ - Test parsing of the sample document `LMT Kodak 2383 Print Emulation.xml`. + Test parsing of the sample file `LMT Kodak 2383 Print Emulation.xml`. """ clf_data = read_clf( @@ -88,7 +88,7 @@ def test_read_sample_document_2(self) -> None: def test_read_sample_document_3(self) -> None: """ - Test parsing of the sample document `LMT_ARRI_K1S1_709_EI800_v3.xml`. + Test parsing of the sample file `LMT_ARRI_K1S1_709_EI800_v3.xml`. """ clf_data = read_clf(os.path.join(ROOT_CLF, "LMT_ARRI_K1S1_709_EI800_v3.xml")) @@ -101,7 +101,7 @@ def test_read_sample_document_3(self) -> None: def test_LUT1D_example(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 1. """ @@ -135,7 +135,7 @@ def test_LUT1D_example(self) -> None: def test_LUT3D_example(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 2. """ @@ -186,7 +186,7 @@ def test_LUT3D_example(self) -> None: def test_matrix_example_1(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 3. """ @@ -226,7 +226,7 @@ def test_matrix_example_1(self) -> None: def test_matrix_example_2(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 4. """ @@ -281,7 +281,7 @@ def test_matrix_example_2(self) -> None: def test_range_example(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 5. """ @@ -313,7 +313,7 @@ def test_range_example(self) -> None: def test_log_example_1(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 6. """ @@ -340,7 +340,7 @@ def test_log_example_1(self) -> None: def test_log_example_2(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 7. """ @@ -383,7 +383,7 @@ def test_log_example_2(self) -> None: def test_exponent_example_1(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 8. """ @@ -411,7 +411,7 @@ def test_exponent_example_1(self) -> None: def test_exponent_example_2(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 9. """ @@ -442,7 +442,7 @@ def test_exponent_example_2(self) -> None: def test_exponent_example_3(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 10. """ @@ -473,7 +473,7 @@ def test_exponent_example_3(self) -> None: def test_exponent_example_4(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 11. """ @@ -504,7 +504,7 @@ def test_exponent_example_4(self) -> None: def test_ASC_CDL_example(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 12. """ @@ -545,7 +545,7 @@ def test_ASC_CDL_example(self) -> None: def test_ACES2065_1_to_ACEScg_example(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 13. """ @@ -580,7 +580,7 @@ def test_ACES2065_1_to_ACEScg_example(self) -> None: def test_ACES2065_1_to_ACEScct_example(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 14. """ @@ -620,7 +620,7 @@ def test_ACES2065_1_to_ACEScct_example(self) -> None: def test_CIE_XYZ_to_CIELAB_example(self) -> None: """ - Test parsing of the example process node from the official CLF specification + Test parsing of the example process node from the official *CLF* specification Example 14. """ @@ -664,7 +664,7 @@ def test_CIE_XYZ_to_CIELAB_example(self) -> None: def test_fail_on_invalid_namespace(self) -> None: """ - Test parsing oa a process list with an invalid xmlns attribute. + Test parsing oa a *ProcessList* with an invalid xmlns attribute. """ example = b""" @@ -681,7 +681,7 @@ def test_fail_on_invalid_namespace(self) -> None: @pytest.mark.with_ocio def test_CLF_from_OCIO(self) -> None: """ - Test parsing of a CLF file written by OpenColorIO. + Test parsing of a *CLF* file written by OpenColorIO. """ import PyOpenColorIO as ocio diff --git a/colour_clf_io/values.py b/colour_clf_io/values.py index 62230f5..4a5c713 100644 --- a/colour_clf_io/values.py +++ b/colour_clf_io/values.py @@ -2,8 +2,8 @@ Values ======= -Defines enums that represent allowed values in some of the fields contained in a -CLF document. +Defines the enumerations that represent allowed values in some of the +fields contained in a *CLF* file. """ from __future__ import annotations @@ -29,7 +29,16 @@ class BitDepth(Enum): """ - Represents the valid bit depth values of the CLF specification. + Represents the valid bit depth values of the *CLF* specification. + + Attributes + ---------- + - :attr:`~colour_clf_io.BitDepth.i8` + - :attr:`~colour_clf_io.BitDepth.i10` + - :attr:`~colour_clf_io.BitDepth.i12` + - :attr:`~colour_clf_io.BitDepth.i16` + - :attr:`~colour_clf_io.BitDepth.f16` + - :attr:`~colour_clf_io.BitDepth.f32` References ---------- @@ -37,11 +46,22 @@ class BitDepth(Enum): """ i8 = "8i" + """8-bit unsigned integer.""" + i10 = "10i" + """10-bit unsigned integer.""" + i12 = "12i" + """12-bit unsigned integer.""" + i16 = "16i" + """16-bit unsigned integer.""" + f16 = "16f" + """16-bit floating point (half-float).""" + f32 = "32f" + """32-bit floating point (single precision).""" def scale_factor(self) -> float: """ @@ -95,7 +115,13 @@ def all(cls: type[BitDepth]) -> list: class Channel(enum.Enum): """ - Represents the valid values of the channel attribute in the Range element. + Represents the valid values of the channel attribute in the *Range* element. + + Attributes + ---------- + - :attr:`~colour_clf_io.Channel.R` + - :attr:`~colour_clf_io.Channel.G` + - :attr:`~colour_clf_io.Channel.B` References ---------- @@ -109,7 +135,11 @@ class Channel(enum.Enum): class Interpolation1D(Enum): """ - Represents the valid interpolation values of a LUT1D element. + Represents the valid interpolation values of a *LUT1D* element. + + Attributes + ---------- + - :attr:`~colour_clf_io.Interpolation1D.LINEAR` References ---------- @@ -121,7 +151,12 @@ class Interpolation1D(Enum): class Interpolation3D(Enum): """ - Represents the valid interpolation values of a LUT3D element. + Represents the valid interpolation values of a *LUT3D* element. + + Attributes + ---------- + - :attr:`~colour_clf_io.Interpolation3D.TRILINEAR` + - :attr:`~colour_clf_io.Interpolation3D.TETRAHEDRAL` References ---------- @@ -136,12 +171,26 @@ class ASC_CDL_Style(enum.Enum): """ Represents the valid values of the style attribute of an ASC_CDL element. + Attributes + ---------- + - :attr:`~colour_clf_io.ASC_CDL_Style.FWD` + - :attr:`~colour_clf_io.ASC_CDL_Style.REV` + - :attr:`~colour_clf_io.ASC_CDL_Style.FWD_NO_CLAMP` + - :attr:`~colour_clf_io.ASC_CDL_Style.REV_NO_CLAMP` + References ---------- - https://docs.acescentral.com/specifications/clf/#asc_cdl """ FWD = "Fwd" + """Implementation of v1.2 ASC CDL equation (default).""" + REV = "Rev" + """Inverse equation.""" + FWD_NO_CLAMP = "FwdNoClamp" + """Similar to the Fwd equation, but without clamping.""" + REV_NO_CLAMP = "RevNoClamp" + """Inverse equation, without clamping.""" diff --git a/docs/_templates/class.rst b/docs/_templates/class.rst new file mode 100644 index 0000000..07e2fd4 --- /dev/null +++ b/docs/_templates/class.rst @@ -0,0 +1,8 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :special-members: + :show-inheritance: diff --git a/docs/colour_clf_io.rst b/docs/colour_clf_io.rst new file mode 100644 index 0000000..f839ef7 --- /dev/null +++ b/docs/colour_clf_io.rst @@ -0,0 +1,75 @@ +Colour - CLF IO +=============== + +Reading and Writing CLF Files +----------------------------- + +``colour.colour_clf_io`` + +.. currentmodule:: colour_clf_io + +.. autosummary:: + :toctree: generated/ + + read_clf + parse_clf + +Process List & Process Nodes +---------------------------- + +``colour_clf_io`` + +.. currentmodule:: colour_clf_io + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + ProcessList + ProcessNode + LUT1D + LUT3D + Matrix + Range + Log + Exponent + ASC_CDL + +Elements +-------- + +``colour_clf_io`` + +.. currentmodule:: colour_clf_io + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + Array + CalibrationInfo + ExponentParams + ExponentStyle + Info + LogParams + LogStyle + RangeStyle + SatNode + SOPNode + +Values (Enumerations) +--------------------- + +``colour_clf_io`` + +.. currentmodule:: colour_clf_io + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + BitDepth + Channel + Interpolation1D + Interpolation3D + ASC_CDL_Style diff --git a/docs/conf.py b/docs/conf.py index 81fbc30..a257c9c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,5 @@ """ -Colour - CLF IO - Documentation Configuration +Colour - *CLF* IO - Documentation Configuration ============================================= """ diff --git a/docs/index.rst b/docs/index.rst index 53d6180..dcf0661 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,6 @@ Colour - CLF IO =============== - A `Python `__ package implementing functionality to read and write files in the `Common LUT Format (CLF) `__. @@ -15,19 +14,18 @@ Features The following features are available: -- Reading CLF files to a Python representation. +- Reading *CLF* files to a Python representation. The following features are planned and in development: -- Writing CLF files from the Python representation. -- Validating CLF files according to the specification. +- Writing *CLF* files from the Python representation. +- Validating *CLF* files according to the specification. Features that will not be part of this library: -- Executing CLF workflows and applying them to colours or images. This feature will be implemented as part of `Colour +- Executing *CLF* workflows and applying them to colours or images. This feature will be implemented as part of `Colour `__. - Examples ^^^^^^^^ @@ -65,7 +63,8 @@ The *Colour Developers* can be reached via different means: - `Facebook `__ - `Github Discussions `__ - `Gitter `__ -- `Twitter `__ +- `X `__ +- `Bluesky `__ About ----- diff --git a/docs/installation.rst b/docs/installation.rst index 1d2c32a..ad3adf4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -6,9 +6,9 @@ Primary Dependencies **Colour - CLF IO** requires various dependencies in order to run: -- `python >= 3.9, < 4 `__ +- `python >= 3.10, < 4 `__ - `lxml >= 5.2.1 < 6 `__ -- `numpy >= 1.22, < 2 `__ +- `numpy >= 1.24, < 2 `__ Pypi ---- From 5fc0c6d99fc6739c7b8bacb2b8d6f19c886f8115 Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 12 Jan 2025 17:02:49 +1300 Subject: [PATCH 3/5] Move `colour_clf_io.ExponentStyle`, `colour_clf_io.LogStyle` and `colour_clf_io.RangeStyle` enumerations to `colour_clf_io.values` module. --- colour_clf_io/__init__.py | 22 +-- colour_clf_io/elements.py | 173 ---------------------- colour_clf_io/process_nodes.py | 12 +- colour_clf_io/tests/test_clf_parsing.py | 14 +- colour_clf_io/values.py | 184 +++++++++++++++++++++++- docs/colour_clf_io.rst | 8 +- 6 files changed, 206 insertions(+), 207 deletions(-) diff --git a/colour_clf_io/__init__.py b/colour_clf_io/__init__.py index 5501467..e1d89d4 100644 --- a/colour_clf_io/__init__.py +++ b/colour_clf_io/__init__.py @@ -31,11 +31,8 @@ Array, CalibrationInfo, ExponentParams, - ExponentStyle, Info, LogParams, - LogStyle, - RangeStyle, SatNode, SOPNode, ) @@ -51,11 +48,14 @@ Range, ) from .values import ( - ASC_CDL_Style, + ASC_CDLStyle, BitDepth, Channel, + ExponentStyle, Interpolation1D, Interpolation3D, + LogStyle, + RangeStyle, ) __author__ = "Colour Developers" @@ -68,14 +68,11 @@ __all__ = [ "Array", "CalibrationInfo", - "ExponentParams", - "ExponentStyle", + "SOPNode", + "SatNode", "Info", "LogParams", - "LogStyle", - "RangeStyle", - "SatNode", - "SOPNode", + "ExponentParams", ] __all__ += ["ProcessList"] __all__ += [ @@ -89,11 +86,14 @@ "Range", ] __all__ += [ - "ASC_CDL_Style", "BitDepth", "Channel", "Interpolation1D", "Interpolation3D", + "RangeStyle", + "LogStyle", + "ExponentStyle", + "ASC_CDLStyle", ] __application_name__ = "Colour - CLF IO" diff --git a/colour_clf_io/elements.py b/colour_clf_io/elements.py index 31a5826..d345f33 100644 --- a/colour_clf_io/elements.py +++ b/colour_clf_io/elements.py @@ -8,7 +8,6 @@ from __future__ import annotations -import enum import typing from dataclasses import dataclass @@ -42,9 +41,6 @@ __all__ = [ "Array", "CalibrationInfo", - "RangeStyle", - "LogStyle", - "ExponentStyle", "SOPNode", "SatNode", "Info", @@ -232,175 +228,6 @@ def from_xml( return CalibrationInfo(**attributes) -class RangeStyle(enum.Enum): - """ - Represent the valid values of the *style* attribute of a - :class:`colour_clf_io.Range` *Process Node*. - - Attributes - ---------- - - :attr:`~colour_clf_io.RangeStyle.CLAMP` - - :attr:`~colour_clf_io.RangeStyle.NO_CLAMP` - - References - ---------- - - https://docs.acescentral.com/specifications/clf/#range - """ - - CLAMP = "Clamp" - """ - Clamping is applied upon the result of the scale and offset expressed by - the result of the non-clamping Range equation.""" - - NO_CLAMP = "noClamp" - """ - Scale and offset is applied without clamping (i.e., values below - minOutValue or above maxOutValue are preserved). - """ - - -class LogStyle(enum.Enum): - """ - Represent the valid values of the *style* attribute of a - :class:`colour_clf_io.Log` *Process Node*. - - Attributes - ---------- - - :attr:`~colour_clf_io.LogStyle.LOG_10` - - :attr:`~colour_clf_io.LogStyle.ANTI_LOG_10` - - :attr:`~colour_clf_io.LogStyle.LOG_2` - - :attr:`~colour_clf_io.LogStyle.ANTI_LOG_2` - - :attr:`~colour_clf_io.LogStyle.LIN_TO_LOG` - - :attr:`~colour_clf_io.LogStyle.LOG_TO_LIN` - - :attr:`~colour_clf_io.LogStyle.CAMERA_LIN_TO_LOG` - - :attr:`~colour_clf_io.LogStyle.CAMERA_LOG_TO_LIN` - - References - ---------- - - https://docs.acescentral.com/specifications/clf/#processList - """ - - LOG_10 = "log10" - """Apply a base 10 logarithm.""" - - ANTI_LOG_10 = "antiLog10" - """Apply a base 10 anti-logarithm.""" - - LOG_2 = "log2" - """Apply a base 2 logarithm.""" - - ANTI_LOG_2 = "antiLog2" - """Apply a base 2 anti-logarithm.""" - - LIN_TO_LOG = "linToLog" - """Apply a logarithm.""" - - LOG_TO_LIN = "logToLin" - """Apply an anti-logarithm.""" - - CAMERA_LIN_TO_LOG = "cameraLinToLog" - """ - Apply a piecewise function with logarithmic and linear segments on linear - values, converting them to non-linear values. - """ - - CAMERA_LOG_TO_LIN = "cameraLogToLin" - """ - Applies a piecewise function with logarithmic and linear segments on - non-linear values, converting them to linear values. - """ - - -class ExponentStyle(enum.Enum): - """ - Represent the valid values of the *style* attribute of a - :class:`colour_clf_io.Exponent` *Process Node*. - - Attributes - ---------- - - :attr:`~colour_clf_io.ExponentStyle.BASIC_FWD` - - :attr:`~colour_clf_io.ExponentStyle.BASIC_REV` - - :attr:`~colour_clf_io.ExponentStyle.BASIC_MIRROR_FWD` - - :attr:`~colour_clf_io.ExponentStyle.BASIC_MIRROR_REV` - - :attr:`~colour_clf_io.ExponentStyle.BASIC_PASS_THRU_FWD` - - :attr:`~colour_clf_io.ExponentStyle.BASIC_PASS_THRU_REV` - - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_FWD` - - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_REV` - - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_MIRROR_FWD` - - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_MIRROR_REV` - - References - ---------- - - https://docs.acescentral.com/specifications/clf/#exponent - """ - - BASIC_FWD = "basicFwd" - """ - Apply a power law using the exponent value specified in the ExponentParams - element. - """ - - BASIC_REV = "basicRev" - """ - Apply a power law using the exponent value specified in the ExponentParams - element. - """ - - BASIC_MIRROR_FWD = "basicMirrorFwd" - """ - Apply a basic power law using the exponent value specified in the - ExponentParams element for values greater than or equal to zero and mirror - the function for values less than zero (i.e., rotationally symmetric around - the origin). - """ - - BASIC_MIRROR_REV = "basicMirrorRev" - """ - Apply a basic power law using the exponent value specified in the - ExponentParams element for values greater than or equal to zero and mirror - the function for values less than zero (i.e., rotationally symmetric around - the origin). - """ - - BASIC_PASS_THRU_FWD = "basicPassThruFwd" # noqa: S105 - """ - Apply a basic power law using the exponent value specified in the - ExponentParams element for values greater than or equal to zero and passes - values less than zero unchanged. - """ - - BASIC_PASS_THRU_REV = "basicPassThruRev" # noqa: S105 - """ - Apply a basic power law using the exponent value specified in the - ExponentParams element for values greater than or equal to zero and passes - values less than zero unchanged. - """ - - MON_CURVE_FWD = "monCurveFwd" - """ - Apply a power law function with a linear segment near the origin. - """ - - MON_CURVE_REV = "monCurveRev" - """ - Apply a power law function with a linear segment near the origin. - """ - - MON_CURVE_MIRROR_FWD = "monCurveMirrorFwd" - """ - Apply a power law function with a linear segment near the origin and - mirror the function for values less than zero (i.e., rotationally symmetric - around the origin). - """ - - MON_CURVE_MIRROR_REV = "monCurveMirrorRev" - """ - Apply a power law function with a linear segment near the origin and mirror - the function for values less than zero (i.e., rotationally symmetric around - the origin). - """ - - @dataclass class SOPNode(XMLParsable): """ diff --git a/colour_clf_io/process_nodes.py b/colour_clf_io/process_nodes.py index cfc792d..8371c82 100644 --- a/colour_clf_io/process_nodes.py +++ b/colour_clf_io/process_nodes.py @@ -19,10 +19,7 @@ from colour_clf_io.elements import ( Array, ExponentParams, - ExponentStyle, LogParams, - LogStyle, - RangeStyle, SatNode, SOPNode, ) @@ -39,10 +36,13 @@ sliding_window, ) from colour_clf_io.values import ( - ASC_CDL_Style, + ASC_CDLStyle, BitDepth, + ExponentStyle, Interpolation1D, Interpolation3D, + LogStyle, + RangeStyle, ) __author__ = "Colour Developers" @@ -677,7 +677,7 @@ class ASC_CDL(ProcessNode): - https://docs.acescentral.com/specifications/clf/#asc_cdl """ - style: ASC_CDL_Style + style: ASC_CDLStyle sopnode: SOPNode | None sat_node: SatNode | None @@ -717,7 +717,7 @@ def from_xml( return None super_args = ProcessNode.parse_attributes(xml, config) - style = ASC_CDL_Style(xml.get("style")) + style = ASC_CDLStyle(xml.get("style")) sop_node = SOPNode.from_xml(child_element(xml, "SOPNode", config), config) # pyright: ignore sat_node = SatNode.from_xml(child_element(xml, "SatNode", config), config) # pyright: ignore diff --git a/colour_clf_io/tests/test_clf_parsing.py b/colour_clf_io/tests/test_clf_parsing.py index 025c4be..43a98be 100644 --- a/colour_clf_io/tests/test_clf_parsing.py +++ b/colour_clf_io/tests/test_clf_parsing.py @@ -335,7 +335,7 @@ def test_log_example_1(self) -> None: assert node.in_bit_depth == colour_clf_io.values.BitDepth.f16 assert node.out_bit_depth == colour_clf_io.values.BitDepth.f16 assert node.description == "Base 10 Logarithm" - assert node.style == colour_clf_io.elements.LogStyle.LOG_10 + assert node.style == colour_clf_io.values.LogStyle.LOG_10 assert node.log_params == [] def test_log_example_2(self) -> None: @@ -365,7 +365,7 @@ def test_log_example_2(self) -> None: assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.description == "Linear to DJI D-Log" - assert node.style == colour_clf_io.elements.LogStyle.CAMERA_LIN_TO_LOG + assert node.style == colour_clf_io.values.LogStyle.CAMERA_LIN_TO_LOG assert node.log_params[0].base is not None np.testing.assert_allclose(node.log_params[0].base, 10.0) assert node.log_params[0].log_side_slope is not None @@ -406,7 +406,7 @@ def test_exponent_example_1(self) -> None: assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.description == "Basic 2.2 Gamma" - assert node.style == colour_clf_io.elements.ExponentStyle.BASIC_FWD + assert node.style == colour_clf_io.values.ExponentStyle.BASIC_FWD np.testing.assert_allclose(node.exponent_params[0].exponent, 2.2) def test_exponent_example_2(self) -> None: @@ -434,7 +434,7 @@ def test_exponent_example_2(self) -> None: assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.description == "EOTF (sRGB)" - assert node.style, colour_clf_io.elements.ExponentStyle.MON_CURVE_FWD + assert node.style, colour_clf_io.values.ExponentStyle.MON_CURVE_FWD assert node.exponent_params[0].exponent is not None np.testing.assert_allclose(node.exponent_params[0].exponent, 2.4) assert node.exponent_params[0].offset is not None @@ -465,7 +465,7 @@ def test_exponent_example_3(self) -> None: assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.description == "CIE L*" - assert node.style == colour_clf_io.elements.ExponentStyle.MON_CURVE_REV + assert node.style == colour_clf_io.values.ExponentStyle.MON_CURVE_REV assert node.exponent_params[0].exponent is not None np.testing.assert_allclose(node.exponent_params[0].exponent, 3.0) assert node.exponent_params[0].offset is not None @@ -496,7 +496,7 @@ def test_exponent_example_4(self) -> None: assert node.in_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.out_bit_depth == colour_clf_io.values.BitDepth.f32 assert node.description == "Rec. 709 OETF" - assert node.style == colour_clf_io.elements.ExponentStyle.MON_CURVE_REV + assert node.style == colour_clf_io.values.ExponentStyle.MON_CURVE_REV assert node.exponent_params[0].exponent is not None np.testing.assert_allclose(node.exponent_params[0].exponent, 2.2222222222222222) assert node.exponent_params[0].offset is not None @@ -534,7 +534,7 @@ def test_ASC_CDL_example(self) -> None: assert node.in_bit_depth == colour_clf_io.values.BitDepth.f16 assert node.out_bit_depth == colour_clf_io.values.BitDepth.f16 assert node.description == "scene 1 exterior look" - assert node.style == colour_clf_io.values.ASC_CDL_Style.FWD + assert node.style == colour_clf_io.values.ASC_CDLStyle.FWD assert node.sopnode is not None assert node.sopnode.slope == (1.000000, 1.000000, 0.900000) assert node.sopnode.offset == (-0.030000, -0.020000, 0.000000) diff --git a/colour_clf_io/values.py b/colour_clf_io/values.py index 4a5c713..b559da1 100644 --- a/colour_clf_io/values.py +++ b/colour_clf_io/values.py @@ -23,7 +23,10 @@ "Channel", "Interpolation1D", "Interpolation3D", - "ASC_CDL_Style", + "RangeStyle", + "LogStyle", + "ExponentStyle", + "ASC_CDLStyle", ] @@ -167,16 +170,185 @@ class Interpolation3D(Enum): TETRAHEDRAL = "tetrahedral" -class ASC_CDL_Style(enum.Enum): +class RangeStyle(enum.Enum): + """ + Represent the valid values of the *style* attribute of a + :class:`colour_clf_io.Range` *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.RangeStyle.CLAMP` + - :attr:`~colour_clf_io.RangeStyle.NO_CLAMP` + + References + ---------- + - https://docs.acescentral.com/specifications/clf/#range + """ + + CLAMP = "Clamp" + """ + Clamping is applied upon the result of the scale and offset expressed by + the result of the non-clamping Range equation.""" + + NO_CLAMP = "noClamp" + """ + Scale and offset is applied without clamping (i.e., values below + minOutValue or above maxOutValue are preserved). + """ + + +class LogStyle(enum.Enum): + """ + Represent the valid values of the *style* attribute of a + :class:`colour_clf_io.Log` *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.LogStyle.LOG_10` + - :attr:`~colour_clf_io.LogStyle.ANTI_LOG_10` + - :attr:`~colour_clf_io.LogStyle.LOG_2` + - :attr:`~colour_clf_io.LogStyle.ANTI_LOG_2` + - :attr:`~colour_clf_io.LogStyle.LIN_TO_LOG` + - :attr:`~colour_clf_io.LogStyle.LOG_TO_LIN` + - :attr:`~colour_clf_io.LogStyle.CAMERA_LIN_TO_LOG` + - :attr:`~colour_clf_io.LogStyle.CAMERA_LOG_TO_LIN` + + References + ---------- + - https://docs.acescentral.com/specifications/clf/#processList + """ + + LOG_10 = "log10" + """Apply a base 10 logarithm.""" + + ANTI_LOG_10 = "antiLog10" + """Apply a base 10 anti-logarithm.""" + + LOG_2 = "log2" + """Apply a base 2 logarithm.""" + + ANTI_LOG_2 = "antiLog2" + """Apply a base 2 anti-logarithm.""" + + LIN_TO_LOG = "linToLog" + """Apply a logarithm.""" + + LOG_TO_LIN = "logToLin" + """Apply an anti-logarithm.""" + + CAMERA_LIN_TO_LOG = "cameraLinToLog" + """ + Apply a piecewise function with logarithmic and linear segments on linear + values, converting them to non-linear values. + """ + + CAMERA_LOG_TO_LIN = "cameraLogToLin" + """ + Applies a piecewise function with logarithmic and linear segments on + non-linear values, converting them to linear values. + """ + + +class ExponentStyle(enum.Enum): + """ + Represent the valid values of the *style* attribute of a + :class:`colour_clf_io.Exponent` *Process Node*. + + Attributes + ---------- + - :attr:`~colour_clf_io.ExponentStyle.BASIC_FWD` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_REV` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_MIRROR_FWD` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_MIRROR_REV` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_PASS_THRU_FWD` + - :attr:`~colour_clf_io.ExponentStyle.BASIC_PASS_THRU_REV` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_FWD` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_REV` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_MIRROR_FWD` + - :attr:`~colour_clf_io.ExponentStyle.MON_CURVE_MIRROR_REV` + + References + ---------- + - https://docs.acescentral.com/specifications/clf/#exponent + """ + + BASIC_FWD = "basicFwd" + """ + Apply a power law using the exponent value specified in the ExponentParams + element. + """ + + BASIC_REV = "basicRev" + """ + Apply a power law using the exponent value specified in the ExponentParams + element. + """ + + BASIC_MIRROR_FWD = "basicMirrorFwd" + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and mirror + the function for values less than zero (i.e., rotationally symmetric around + the origin). + """ + + BASIC_MIRROR_REV = "basicMirrorRev" + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and mirror + the function for values less than zero (i.e., rotationally symmetric around + the origin). + """ + + BASIC_PASS_THRU_FWD = "basicPassThruFwd" # noqa: S105 + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and passes + values less than zero unchanged. + """ + + BASIC_PASS_THRU_REV = "basicPassThruRev" # noqa: S105 + """ + Apply a basic power law using the exponent value specified in the + ExponentParams element for values greater than or equal to zero and passes + values less than zero unchanged. + """ + + MON_CURVE_FWD = "monCurveFwd" + """ + Apply a power law function with a linear segment near the origin. + """ + + MON_CURVE_REV = "monCurveRev" + """ + Apply a power law function with a linear segment near the origin. + """ + + MON_CURVE_MIRROR_FWD = "monCurveMirrorFwd" + """ + Apply a power law function with a linear segment near the origin and + mirror the function for values less than zero (i.e., rotationally symmetric + around the origin). + """ + + MON_CURVE_MIRROR_REV = "monCurveMirrorRev" + """ + Apply a power law function with a linear segment near the origin and mirror + the function for values less than zero (i.e., rotationally symmetric around + the origin). + """ + + +class ASC_CDLStyle(enum.Enum): """ Represents the valid values of the style attribute of an ASC_CDL element. Attributes ---------- - - :attr:`~colour_clf_io.ASC_CDL_Style.FWD` - - :attr:`~colour_clf_io.ASC_CDL_Style.REV` - - :attr:`~colour_clf_io.ASC_CDL_Style.FWD_NO_CLAMP` - - :attr:`~colour_clf_io.ASC_CDL_Style.REV_NO_CLAMP` + - :attr:`~colour_clf_io.ASC_CDLStyle.FWD` + - :attr:`~colour_clf_io.ASC_CDLStyle.REV` + - :attr:`~colour_clf_io.ASC_CDLStyle.FWD_NO_CLAMP` + - :attr:`~colour_clf_io.ASC_CDLStyle.REV_NO_CLAMP` References ---------- diff --git a/docs/colour_clf_io.rst b/docs/colour_clf_io.rst index f839ef7..daed147 100644 --- a/docs/colour_clf_io.rst +++ b/docs/colour_clf_io.rst @@ -49,11 +49,8 @@ Elements Array CalibrationInfo ExponentParams - ExponentStyle Info LogParams - LogStyle - RangeStyle SatNode SOPNode @@ -72,4 +69,7 @@ Values (Enumerations) Channel Interpolation1D Interpolation3D - ASC_CDL_Style + ExponentStyle + LogStyle + RangeStyle + ASC_CDLStyle From 01cbb5ae05ee8296c2bdc1144cf69e86ec4644f2 Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 12 Jan 2025 21:15:56 +1300 Subject: [PATCH 4/5] Ensure that project can be built. --- TODO.rst | 15 +++++++++++++++ tasks.py | 53 ++--------------------------------------------------- 2 files changed, 17 insertions(+), 51 deletions(-) create mode 100644 TODO.rst diff --git a/TODO.rst b/TODO.rst new file mode 100644 index 0000000..a8d4ae7 --- /dev/null +++ b/TODO.rst @@ -0,0 +1,15 @@ +Colour - TODO +============= + +TODO +---- + + + +About +----- + +| **Colour** by Colour Developers +| Copyright 2024 Colour Developers - `colour-developers@colour-science.org `__ +| This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause +| `https://github.com/colour-science/colour `__ diff --git a/tasks.py b/tasks.py index f71677d..2fade13 100644 --- a/tasks.py +++ b/tasks.py @@ -42,7 +42,6 @@ "PYPI_PACKAGE_NAME", "PYPI_ARCHIVE_NAME", "BIBLIOGRAPHY_NAME", - "literalise", "clean", "formatting", "quality", @@ -66,7 +65,7 @@ PYTHON_PACKAGE_NAME: str = colour_clf_io.__name__ -PYPI_PACKAGE_NAME: str = "colour-science-clf-io" +PYPI_PACKAGE_NAME: str = "colour-clf-io" PYPI_ARCHIVE_NAME: str = PYPI_PACKAGE_NAME.replace("-", "_") BIBLIOGRAPHY_NAME: str = "BIBLIOGRAPHY.bib" @@ -151,24 +150,6 @@ def inner(text: str) -> str: print_callable("=" * width) -@task -def literalise(ctx: Context) -> None: - """ - Write various literals in the `colour.hints` module. - - Parameters - ---------- - ctx - Context. - """ - - message_box("Literalising...") - with ctx.cd("utilities"): - ctx.run("./literalise.py") - - ctx.run("pre-commit run --files colour/hints/__init__.py", warn=True) - - @task def clean( ctx: Context, @@ -432,7 +413,7 @@ def requirements(ctx: Context) -> None: ) -@task(literalise, clean, preflight, docs, todo, requirements) +@task(clean, preflight, docs, todo, requirements) def build(ctx: Context) -> None: """ Build the project and runs dependency tasks, i.e., *docs*, *todo*, and @@ -445,34 +426,7 @@ def build(ctx: Context) -> None: """ message_box("Building...") - if "modified: README.rst" in ctx.run("git status").stdout: # pyright: ignore - msg = 'Please commit your changes to the "README.rst" file!' - raise RuntimeError(msg) - - with open("README.rst") as readme_file: - readme_content = readme_file.read() - - with open("README.rst", "w") as readme_file: - # Adding the *Colour* logo as the first content line because the *raw* - # directive to support light and dark theme is later trimmed. - readme_content = ( - ".. image:: https://raw.githubusercontent.com/colour-science/" - "colour-branding/master/images/Colour_Logo_001.png\n" + readme_content - ) - readme_file.write( - re.sub( - ( - "(\\.\\. begin-trim-long-description.*?" - "\\.\\. end-trim-long-description)" - ), - "", - readme_content, - flags=re.DOTALL, - ) - ) - ctx.run("uv build") - ctx.run("git checkout -- README.rst") ctx.run("twine check dist/*") @@ -495,9 +449,6 @@ def virtualise(ctx: Context, tests: bool = True) -> None: ctx.run(f"mv {PYPI_ARCHIVE_NAME}-{APPLICATION_VERSION} {unique_name}") with ctx.cd(unique_name): ctx.run("uv sync --all-extras --no-dev") - ctx.run( - 'uv run python -c "import imageio;imageio.plugins.freeimage.download()"' - ) if tests: ctx.run( "source .venv/bin/activate && " From a93b8f16391ebb64ae87c5b0867637994c373b0e Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 12 Jan 2025 21:22:36 +1300 Subject: [PATCH 5/5] Update *Github* templates. --- .github/ISSUE_TEMPLATE/bug-report.yml | 4 ++-- .github/ISSUE_TEMPLATE/documentation-improvement.yml | 6 +++--- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- .github/ISSUE_TEMPLATE/question.yml | 6 +++--- .github/PULL_REQUEST_TEMPLATE.md | 6 ++---- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 4010192..1e9a4e3 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -7,8 +7,8 @@ body: - type: markdown attributes: value: > - Thank you for taking the time to file a bug report. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour/issues) and also the [draft release notes](https://gist.github.com/KelSolaar/4a6ebe9ec3d389f0934b154fec8df51d). - The issue could already be fixed in the [develop](https://github.com/colour-science/colour) branch. If you have an installation problem, the [installation guide](https://www.colour-science.org/installation-guide/) describes the recommended process. + Thank you for taking the time to file a bug report. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour-clf-io/issues). + The issue could already be fixed in the [develop](https://github.com/colour-science/colour-clf-io) branch. If you have an installation problem, the [installation guide](https://www.colour-science.org/installation-guide/) describes the recommended process. - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/documentation-improvement.yml b/.github/ISSUE_TEMPLATE/documentation-improvement.yml index 27581a1..21d00c0 100644 --- a/.github/ISSUE_TEMPLATE/documentation-improvement.yml +++ b/.github/ISSUE_TEMPLATE/documentation-improvement.yml @@ -7,15 +7,15 @@ body: - type: markdown attributes: value: > - Thank you for taking the time to file a documentation improvement report. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour/issues). + Thank you for taking the time to file a documentation improvement report. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour-clf-io/issues). - type: input attributes: label: Documentation Link description: > - Please link to any documentation or examples that you are referencing. Suggested improvements should be based on the [development version of the documentation](https://colour.readthedocs.io/en/develop/). + Please link to any documentation or examples that you are referencing. Suggested improvements should be based on the [development version of the documentation](https://colour-clf-io.readthedocs.io/en/develop/). placeholder: > - << https://colour.readthedocs.io/en/develop/... >> + << https://colour-clf-io.readthedocs.io/en/develop/... >> validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 05ca55d..6bea646 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -7,7 +7,7 @@ body: - type: markdown attributes: value: > - Thank you for taking the time to file a feature request. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour/issues) and also the [draft release notes](https://gist.github.com/KelSolaar/4a6ebe9ec3d389f0934b154fec8df51d). + Thank you for taking the time to file a feature request. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour-clf-io/issues) and also the [draft release notes](https://gist.github.com/KelSolaar/4a6ebe9ec3d389f0934b154fec8df51d). - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 9bf9427..79aef81 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -6,12 +6,12 @@ labels: [Discussion] body: - type: markdown attributes: - value: Thank you for taking the time to ask a question or discuss. Before continuing, we would be glad if you were to start this discussion in the dedicated [discussions](https://github.com/colour-science/colour/discussions) area. + value: Thank you for taking the time to ask a question or discuss. Before continuing, we would be glad if you were to start this discussion in the dedicated [discussions](https://github.com/colour-science/colour-clf-io/discussions) area. - type: textarea attributes: label: "Question" description: > - If you are still here, please consider using the dedicated [discussions](https://github.com/colour-science/colour/discussions) area. + If you are still here, please consider using the dedicated [discussions](https://github.com/colour-science/colour-clf-io/discussions) area. placeholder: > - << The discussions area is this way: https://github.com/colour-science/colour/discussions... >> + << The discussions area is this way: https://github.com/colour-science/colour-clf-io/discussions... >> diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index adc2413..6562351 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,11 +17,9 @@ is available to guide the process: https://www.colour-science.org/contributing/. - [ ] Unit tests have been implemented and passed. - [ ] Pyright static checking has been run and passed. - [ ] Pre-commit hooks have been run and passed. -- [ ] New transformations have been added to the _Automatic Colour Conversion Graph_. -- [ ] New transformations have been exported to the relevant namespaces, e.g. `colour`, `colour.models`. - - + + **Documentation**