From d6925806bdce87345b34b37e9a0591d5f483248c Mon Sep 17 00:00:00 2001 From: David Linke Date: Mon, 15 Jan 2024 01:24:29 +0100 Subject: [PATCH] Add cli & logging; fix prefix-handling in str-transformer --- README.md | 42 ++++- pyproject.toml | 4 +- src/ucumvert/__init__.py | 41 +++++ src/ucumvert/cli.py | 165 ++++++++++++++++-- src/ucumvert/parser.py | 7 +- .../pint_ucum_defs_mapping_report.txt | 2 +- src/ucumvert/ucum_pint.py | 61 +++---- tests/conftest.py | 9 +- tests/test_ucum_pint.py | 59 +++++-- 9 files changed, 323 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 54b6467..ab62c4f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Easier access to UCUM from Python -> **This is almost done. Feedback welcome!** -> The lark grammar to parse UCUM codes and the transformer that converts UCUM units to pint are implemented. -> For some UCUM units we still have to define pint units or aliases and for some also name mappings. +> **Feedback welcome!** +> Currently only the conversion direction from UCM to pint is supported. Supporting pint to UCUM is not of high priority. +> Please carefully review definitions before you trust them. +> While we a lot of tests in place and reviewed the mappings carefully, bugs may still be present. [UCUM](https://ucum.org/) (Unified Code for Units of Measure) is a code system intended to cover all units of measures. It provides a formalism to express units in an unambiguous way suitable for electronic communication. @@ -10,9 +11,9 @@ Note that UCUM does non provide a canonical representation, e.g. `m/s` and `m.s- **ucumvert** is a pip-installable Python package. Features: -- Parser for UCUM unit strings that implements the full grammar -- Converter for creating [pint](https://pypi.org/project/pint/) units from UCUM unit strings -- A pint unit definition file [pint_ucum_defs.txt](https://github.com/dalito/ucumvert/blob/main/src/ucumvert/pint_ucum_defs.txt) that extends pint´s default units with UCUM units +- Parser for UCUM unit strings that implements the full grammar. +- Converter for creating [pint](https://pypi.org/project/pint/) units from UCUM unit strings. +- A pint unit definition file [pint_ucum_defs.txt](https://github.com/dalito/ucumvert/blob/main/src/ucumvert/pint_ucum_defs.txt) that extends pint´s default units with UCUM units. All UCUM units from Version 2.1 of the specification are included. **ucumvert** generates the UCUM grammar by filling a template with unit codes, prefixes etc. from the official [ucum-essence.xml](https://github.com/ucum-org/ucum/blob/main/ucum-essence.xml) file (a copy is included in this repo). So updating the parser for new UCUM releases is straight forward. @@ -50,10 +51,16 @@ Optionally you can visualize the parse trees with [Graphviz](https://www.graphvi ## Demo -This is just a demo command line interface to show that the code does something... +We provide a basic command line interface. ```cmd (.venv) $ ucumvert +``` + +It has an interactive mode to test parsing UCUM codes: + +```cmd +(.venv) $ ucumvert -i Enter UCUM unit code to parse, or 'q' to quit. > m/s2.kg Created visualization of parse tree (parse_tree.png). @@ -73,7 +80,7 @@ main_term > q ``` -So the intermediate result is a tree which is then traversed to convert the elements to pint: +So the intermediate result is a tree which is then traversed to convert the elements to pint quantities (or pint-compatible strings with another transformer): ![parse tree](parse_tree.png) @@ -90,9 +97,26 @@ You may use the package in your code for converting UCUM codes to pint like this >>> ``` +We also experimented with creating a UCUM-aware pint UnitRegistry. +This has been tried by registering a preprocessor that intercepts the entered unit string and converts it from UCUM to pint. +Due to the way preprocessors work, pint will then no longer accept standard pint unit expressions but only UCUM (see below). +This is inconvenient! So we suggest to convert UCUM units as shown above, until a less disruptive way is found/possible. + +```python +>>> from ucumvert import get_pint_registry +>>> ureg = get_pint_registry() +>>> ureg("m/s2.kg") + +>>> ureg("Cel") + +>>> ureg("degC") # a standard pint unit code +... (traceback cut out) +lark.exceptions.UnexpectedCharacters: No terminal matches 'C' in the current parser context +``` + ## Tests -The unit tests include a test to parse all common UCUM unit codes from the official repo. To see this run +The unit tests include parsing and converting all common UCUM unit codes from the official repo. Run the test suite by: ```bash pytest diff --git a/pyproject.toml b/pyproject.toml index 0d33951..2bcbd2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,8 +245,6 @@ max-complexity = 10 # Allow Pydantic's `@validator` decorator to trigger class method treatment. classmethod-decorators = [ "classmethod", - # for pydantic 1.x - "pydantic.validator", "pydantic.class_validators.root_validator" ] [tool.ruff.format] @@ -255,7 +253,7 @@ docstring-code-format = true [tool.codespell] -skip = "pyproject.toml,src/ucumvert/vendor/ucum-essence.xml,src/ucumvert/vendor/ucum_examples.tsv,src/ucumvert/ucum_grammar.lark" +skip = "pyproject.toml,src/ucumvert/vendor/ucum-essence.xml,src/ucumvert/vendor/ucum_examples.tsv,src/ucumvert/ucum_grammar.lark,src/ucumvert/pint_ucum_defs_mapping_report.txt" # Note: words have to be lowercased for the ignore-words-list ignore-words-list = "linke,tne,sie,smoot" diff --git a/src/ucumvert/__init__.py b/src/ucumvert/__init__.py index 6d98212..f14cfd7 100644 --- a/src/ucumvert/__init__.py +++ b/src/ucumvert/__init__.py @@ -1,9 +1,16 @@ +from __future__ import annotations + +import logging +import os +from pathlib import Path + from ucumvert.parser import ( get_ucum_parser, make_parse_tree_png, update_lark_ucum_grammar_file, ) from ucumvert.ucum_pint import ( + UcumToPintStrTransformer, UcumToPintTransformer, get_pint_registry, ucum_preprocessor, @@ -22,4 +29,38 @@ "ucum_preprocessor", "update_lark_ucum_grammar_file", "UcumToPintTransformer", + "UcumToPintStrTransformer", ] + +# Note that nothing is passed to getLogger to set the "root" logger +logger = logging.getLogger() + + +def setup_logging(loglevel: int = logging.INFO, logfile: Path | None = None) -> None: + """ + Setup logging to console and optionally a file. + + The default loglevel is INFO. + """ + loglevel_name = os.getenv("LOGLEVEL", "").strip().upper() + if loglevel_name in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: + loglevel = getattr(logging, loglevel_name, logging.INFO) + + # Apply constraints. CRITICAL=FATAL=50 is the maximum, NOTSET=0 the minimum. + loglevel = min(logging.FATAL, max(loglevel, logging.NOTSET)) + + # Setup handler for logging to console + logging.basicConfig(level=loglevel, format="%(levelname)-8s|%(message)s") + + if logfile is not None: + # Setup handler for logging to file + fh = logging.handlers.RotatingFileHandler( + logfile, maxBytes=100000, backupCount=5 + ) + fh.setLevel(loglevel) + fh_formatter = logging.Formatter( + fmt="%(asctime)s|%(name)-20s|%(levelname)-8s|%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + fh.setFormatter(fh_formatter) + logger.addHandler(fh) diff --git a/src/ucumvert/cli.py b/src/ucumvert/cli.py index 0826b9a..e3dc7f1 100644 --- a/src/ucumvert/cli.py +++ b/src/ucumvert/cli.py @@ -1,11 +1,34 @@ -from lark.exceptions import UnexpectedInput, VisitError +import argparse +import logging +import sys +import textwrap +from pathlib import Path -from ucumvert.parser import get_ucum_parser, make_parse_tree_png -from ucumvert.ucum_pint import UcumToPintTransformer +from lark.exceptions import LarkError, UnexpectedInput, VisitError +from ucumvert import __version__, setup_logging +from ucumvert.parser import ( + get_ucum_parser, + make_parse_tree_png, + update_lark_ucum_grammar_file, +) +from ucumvert.ucum_pint import UcumToPintTransformer, find_matching_pint_definitions -def main(): +try: + import pydot # noqa: F401 + + has_pydot = True +except ImportError: + has_pydot = False + +logger = logging.getLogger(__name__) + + +def interactive(): print("Enter UCUM unit code to parse, or 'q' to quit.") + if not has_pydot: + print("Package pydot not installed, skipping parse-tree image generation.") + ucum_parser = get_ucum_parser() while True: @@ -13,10 +36,13 @@ def main(): if ucum_code in "qQ": break try: - parsed_data = make_parse_tree_png( - ucum_code, filename="parse_tree.png", parser=ucum_parser - ) - print("Created visualization of parse tree (parse_tree.png).") + if has_pydot: + parsed_data = make_parse_tree_png( + ucum_code, filename="parse_tree.png", parser=ucum_parser + ) + print("Created visualization of parse tree (parse_tree.png).") + else: + parsed_data = ucum_parser.parse(ucum_code) print(parsed_data.pretty()) except UnexpectedInput as e: print(e) @@ -30,9 +56,126 @@ def main(): continue -def run_cli_app(): - main() +# === argparse-cli-related code === + + +class DecentFormatter(argparse.HelpFormatter): + """ + An argparse formatter that preserves newlines & keeps indentation. + """ + + def _fill_text(self, text, width, indent): + """ + Reformat text while keeping newlines for lines shorter than width. + """ + lines = [] + for line in textwrap.indent(textwrap.dedent(text), indent).splitlines(): + lines.append( # noqa: PERF401 + textwrap.fill(line, width, subsequent_indent=indent) + ) + return "\n".join(lines) + + def _split_lines(self, text, width): + """ + Conserve indentation in help/description lines when splitting long lines. + """ + lines = [] + for line in textwrap.dedent(text).splitlines(): + if not line.strip(): + continue + indent = " " * (len(line) - len(line.lstrip())) + lines.extend( + textwrap.fill(line, width, subsequent_indent=indent).splitlines() + ) + return lines + + +def root_cmds(args): + if args.version: # pragma: no cover + print(f"ucumvert {__version__}") + if args.interactive: + interactive() + if args.mapping_report: + find_matching_pint_definitions(report_file=args.mapping_report) + if args.grammar_update: + grammar_file = Path(__file__).resolve().parent / "ucum_grammar.lark" + update_lark_ucum_grammar_file(grammar_file=grammar_file) + + +def create_root_parser(): + parser = argparse.ArgumentParser( + prog="ucumvert", + description=("Simple CLI for ucumvert."), + allow_abbrev=False, + formatter_class=DecentFormatter, + ) + parser.add_argument( + "-V", + "--version", + help="The version of ucumvert.", + action="store_true", + ) + parser.add_argument( + "-i", + "--interactive", + help="Interactive mode to explore parsing of UCUM unit codes.", + action="store_true", + ) + parser.add_argument( + "-g", + "--grammar_update", + help=( + "Recreate grammar file 'ucum_grammar.lark' with UCUM atoms " + "extracted from ucum-essence.xml." + ), + action="store_true", + ) + parser.add_argument( + "-m", + "--mapping_report", + help=( + "Write a report of mappings between UCUM unit atoms and pint " + "definitions to the given file. Default is to write to " + "'pint_ucum_defs_mapping_report.txt' in the current directory." + ), + type=Path, + metavar=("FILE"), + nargs="?", # make file an optional argument + const=Path("pint_ucum_defs_mapping_report.txt"), # default value + ) + parser.set_defaults(func=root_cmds) + return parser + + +def main_cli(raw_args=None): + """Setup CLI app and run commands based on arguments.""" + # Create root parser for cli app + parser = create_root_parser() + + if not raw_args: + parser.print_help() + return + + # Parse the command-line arguments + # parse_args will call sys.exit(2) if invalid commands are given. + args = parser.parse_args(raw_args) + setup_logging(loglevel=logging.INFO) + args.func(args) + + +def run_cli_app(raw_args=None): + """Entry point for running the cli app.""" + if raw_args is None: + raw_args = sys.argv[1:] + try: + main_cli(raw_args) + except LarkError: + logger.exception("Terminating with ucumvert error.") + sys.exit(1) + except Exception: + logger.exception("Unexpected error.") + sys.exit(3) # value 2 is used by argparse for invalid args. if __name__ == "__main__": - main() + run_cli_app(sys.argv[1:]) diff --git a/src/ucumvert/parser.py b/src/ucumvert/parser.py index 75a7ef7..d32b7a4 100644 --- a/src/ucumvert/parser.py +++ b/src/ucumvert/parser.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import textwrap from pathlib import Path @@ -13,6 +14,9 @@ get_prefixes, ) +logger = logging.getLogger(__name__) + + # UCUM syntax in the Backus-Naur Form, copied from https://ucum.org/ucum#section-Syntax-Rules # : "+" | "-" # : "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" @@ -162,6 +166,7 @@ def update_lark_ucum_grammar_file( with grammar_file.open("w") as f: f.write("\n".join(wrapped)) f.write("\n") # newline at end of file + logger.info("Updated grammar written to '%s'.", grammar_file) def get_ucum_parser(grammar_file=None): @@ -179,5 +184,5 @@ def make_parse_tree_png(data, filename="parse_tree_unit.png", parser=None): try: tree.pydot__tree_to_png(parsed_data, filename) except ImportError: - print("pydot not installed, skipping png generation") + logger.warning("pydot not installed, skipping png generation") return parsed_data diff --git a/src/ucumvert/pint_ucum_defs_mapping_report.txt b/src/ucumvert/pint_ucum_defs_mapping_report.txt index fea1f6a..c66d264 100644 --- a/src/ucumvert/pint_ucum_defs_mapping_report.txt +++ b/src/ucumvert/pint_ucum_defs_mapping_report.txt @@ -331,4 +331,4 @@ # [car_Au] --> carat_of_gold_alloys (ucumvert registry) # [car_Au] = 1/24 # NON_METRIC, carat of gold alloys, mass fraction (misc) # [smoot] --> smoot (ucumvert registry) # [smoot] = 67 * [in_i] # NON_METRIC, Smoot, length (misc) # [m/s2/Hz^(1/2)] --> meter_per_square_second_per_square_root_of_hertz (ucumvert registry) # [m/s2/Hz^(1/2)] = 1 * sqrt(1 m2/s4/Hz) # NON_METRIC, meter per square seconds per square root of hertz, amplitude spectral density (misc) -# bit_s --> bit (ucumvert registry) # bit_s = 1 * ld(1 1) # NON_METRIC, bit, amount of information (infotech) \ No newline at end of file +# bit_s --> bit (ucumvert registry) # bit_s = 1 * ld(1 1) # NON_METRIC, bit, amount of information (infotech) diff --git a/src/ucumvert/ucum_pint.py b/src/ucumvert/ucum_pint.py index 1491d42..bd261eb 100644 --- a/src/ucumvert/ucum_pint.py +++ b/src/ucumvert/ucum_pint.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from pathlib import Path import pint @@ -9,7 +10,6 @@ from ucumvert.parser import ( get_ucum_parser, make_parse_tree_png, - update_lark_ucum_grammar_file, ) from ucumvert.xml_util import ( get_metric_units, @@ -18,7 +18,10 @@ get_units_with_full_definition, ) -# Some UCUM unit atoms are syntactically incompatiple with pint. For these we +logger = logging.getLogger(__name__) + + +# Some UCUM unit atoms are syntactically incompatible with pint. For these we # map to a pint-compatible unit name which we define in pint_ucum_defs.txt # as alias or new unit. To determine what needs a mapping, use the function # "find_ucum_codes_that_need_mapping()" below. @@ -192,7 +195,7 @@ def component(self, args): def simple_unit(self, args): # print("DBGsu>", repr(args), len(args)) if len(args) == 2: # prefix is present # noqa: PLR2004 - return f"({args[0]} + {args[1]})" + return f"({args[0]}{args[1]})" # Substitute UCUM atoms that cannot be defined in pint as units or aliases. ret = MAPPINGS_UCUM_TO_PINT.get(args[0], args[0]) @@ -206,11 +209,15 @@ def annotatable(self, args): def ucum_preprocessor(unit_input): - """Preprocess UCUM code before parsing as pint unit. + """ + Preprocessor for pint to convert all input from UCUM to pint units. + + Note: This will make most standard pint unit expressions invalid. Usage: - ureg = pint.UnitRegistry() - ureg.preprocessors.append(ucum_preprocessor) + >>> import pint + >>> ureg = pint.UnitRegistry() + >>> ureg.preprocessors.append(ucum_preprocessor) """ ucum_parser = get_ucum_parser() transformer = UcumToPintStrTransformer() @@ -219,8 +226,8 @@ def ucum_preprocessor(unit_input): def find_ucum_codes_that_need_mapping(existing_mappings=MAPPINGS_UCUM_TO_PINT): - """Find UCUM atoms that are syntactically incompatiple with pint.""" - print("The following UCUM atoms must be mapped to valid pint unit names.") + """Find UCUM atoms that are syntactically incompatible with pint.""" + logger.info("The following UCUM atoms must be mapped to valid pint unit names.") ureg = pint.UnitRegistry() sections = { "prefixes": get_prefixes, @@ -229,7 +236,7 @@ def find_ucum_codes_that_need_mapping(existing_mappings=MAPPINGS_UCUM_TO_PINT): } need_mappings = {k: [] for k in sections} for section, get_fcn in sections.items(): - print(f"\n=== {section} ===") + logger.info(f"\n=== {section} ===") # noqa: G004 for ucum_code in get_fcn(): if ucum_code in existing_mappings: continue @@ -240,10 +247,10 @@ def find_ucum_codes_that_need_mapping(existing_mappings=MAPPINGS_UCUM_TO_PINT): ureg.define(def_str) except pint.DefinitionSyntaxError: need_mappings[section].append(ucum_code) - print(f"{ucum_code}") + logger.info(f"{ucum_code}") # noqa: G004 continue if not need_mappings[section]: - print("all good!") + logger.info("all good!") return need_mappings @@ -295,7 +302,7 @@ def find_matching_pint_definitions(report_file: Path | None = None) -> None: try: parsed_data = ucum_parser.parse(lookup_str) except VisitError as exc: - print(f"PARSER ERROR: {exc.args[0]}") + logger.exception("PARSER ERROR: %s", {exc.args[0]}) raise lookup_str = MAPPINGS_UCUM_TO_PINT.get(ucum_code, ucum_code) if is_in_registry(transformer_default, parsed_data): @@ -320,7 +327,17 @@ def find_matching_pint_definitions(report_file: Path | None = None) -> None: report.append(f"# {ucum_code:>10} --> {'NOT DEFINED':<42} # {info}") with Path(report_file).open("w", encoding="utf8") as fp: - fp.write("\n".join(report)) + fp.write("\n".join(report) + "\n") + logger.info("Created mapping report: %s", report_file) + + +def get_pint_registry(ureg=None): + """Return a pint registry with the UCUM definitions loaded.""" + if ureg is None: + ureg = pint.UnitRegistry(on_redefinition="raise") + ureg.preprocessors.append(ucum_preprocessor) + ureg.load_definitions(Path(__file__).resolve().parent / "pint_ucum_defs.txt") + return ureg def run_examples(): @@ -339,22 +356,6 @@ def run_examples(): print(f"Pint {q!r}") -def get_pint_registry(ureg=None): - """Return a pint registry with the UCUM definitions loaded.""" - if ureg is None: - ureg = pint.UnitRegistry(on_redefinition="raise") - ureg.preprocessors.append(ucum_preprocessor) - ureg.load_definitions(Path(__file__).resolve().parent / "pint_ucum_defs.txt") - return ureg - - if __name__ == "__main__": - update_lark_ucum_grammar_file() # run_examples() - - # find_ucum_codes_that_need_mapping() - find_matching_pint_definitions() - - # ureg = get_pint_registry() - # print(ureg("Cel")) - # print(ureg("'")) + find_ucum_codes_that_need_mapping() diff --git a/tests/conftest.py b/tests/conftest.py index 5cafe29..4367e4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,14 @@ def ureg_ucumvert(): @pytest.fixture(scope="session") -def transform(ureg_ucumvert): +def transform_ucum_pint(ureg_ucumvert): from ucumvert import UcumToPintTransformer return UcumToPintTransformer(ureg_ucumvert).transform + + +@pytest.fixture(scope="session") +def transform_ucum_str(ureg_ucumvert): + from ucumvert import UcumToPintStrTransformer + + return UcumToPintStrTransformer(ureg_ucumvert).transform diff --git a/tests/test_ucum_pint.py b/tests/test_ucum_pint.py index b4bbe1b..f9951bb 100644 --- a/tests/test_ucum_pint.py +++ b/tests/test_ucum_pint.py @@ -1,7 +1,7 @@ import pytest from pint import UnitRegistry from test_parser import ucum_examples_valid -from ucumvert import UcumToPintTransformer +from ucumvert import UcumToPintStrTransformer, UcumToPintTransformer from ucumvert.ucum_pint import find_ucum_codes_that_need_mapping from ucumvert.xml_util import get_metric_units, get_non_metric_units @@ -21,8 +21,8 @@ def test_find_ucum_codes_that_need_mapping(): def test_ucum_to_pint(ucum_parser, ureg_std): - expected_quantity = ureg("millimeter") - parsed_data = ucum_parser.parse("mm") + expected_quantity = ureg("kilogram") + parsed_data = ucum_parser.parse("kg") result = UcumToPintTransformer(ureg=ureg_std).transform(parsed_data) assert result == expected_quantity @@ -32,24 +32,61 @@ def test_ucum_to_pint(ucum_parser, ureg_std): ucum_examples_valid.values(), ids=[" ".join(kv) for kv in ucum_examples_valid.items()], ) -def test_ucum_to_pint_official_examples(ucum_parser, transform, ucum_code): +def test_ucum_to_pint_official_examples(ucum_parser, transform_ucum_pint, ucum_code): if ucum_code == "Torr": # Torr is missing in ucum-essence.xml but included in the official examples. # see https://github.com/ucum-org/ucum/issues/289 pytest.skip("Torr is not defined in official ucum-essence.xml") if ucum_code == "[pH]": # TODO create pint issue - pytest.skip("[ph] = pH_value is not defined in pint due to an issue.") + pytest.skip("[pH] = pH_value is not defined in pint due to an issue.") parsed_data = ucum_parser.parse(ucum_code) - transform(parsed_data) + transform_ucum_pint(parsed_data) + + +@pytest.mark.parametrize( + "ucum_code", + ucum_examples_valid.values(), + ids=[" ".join(kv) for kv in ucum_examples_valid.items()], +) +def test_ucum_to_str_official_examples(ucum_parser, transform_ucum_str, ucum_code): + if ucum_code == "Torr": + # Torr is missing in ucum-essence.xml but included in the official examples. + # see https://github.com/ucum-org/ucum/issues/289 + pytest.skip("Torr is not defined in official ucum-essence.xml") + if ucum_code == "[pH]": + # TODO create pint issue + pytest.skip("[pH] = pH_value is not defined in pint due to an issue.") + parsed_data = ucum_parser.parse(ucum_code) + transform_ucum_str(parsed_data) -# comment out next line to see what is missing -# @pytest.mark.skip("TODO: Add missing UCUM units to pint_ucum_defs.txt") @pytest.mark.parametrize("unit_atom", get_unit_atoms()) -def test_ucum_all_unit_atoms(ucum_parser, transform, unit_atom): +def test_ucum_all_unit_atoms_pint(ucum_parser, transform_ucum_pint, unit_atom): + if unit_atom == "[pH]": + # TODO create pint issue + pytest.skip("[pH] = pH_value is not defined in pint due to an issue.") parsed_atom = ucum_parser.parse(unit_atom) + transform_ucum_pint(parsed_atom) + + +def test_ucum_to_pint_vs_str(ucum_parser, ureg_ucumvert): + parsed_data = ucum_parser.parse("m/s2.kg") + expected_quantity = UcumToPintTransformer().transform(parsed_data) + result_str = UcumToPintStrTransformer().transform(parsed_data) + assert result_str == "((((m) / (s)**2) * (kg)))" + assert expected_quantity == ureg_ucumvert("m/s**2 * kg") + assert ureg_ucumvert(result_str) == expected_quantity + + +@pytest.mark.parametrize("unit_atom", get_unit_atoms()) +def test_ucum_all_unit_atoms_pint_vs_str( + ucum_parser, transform_ucum_pint, transform_ucum_str, ureg_ucumvert, unit_atom +): if unit_atom == "[pH]": # TODO create pint issue - pytest.skip("[ph] = pH_value is not defined in pint due to an issue.") - transform(parsed_atom) + pytest.skip("[pH] = pH_value is not defined in pint due to an issue.") + parsed_atom = ucum_parser.parse(unit_atom) + expected_quantity = transform_ucum_pint(parsed_atom) + result_str = transform_ucum_str(parsed_atom) + assert ureg_ucumvert(result_str) == expected_quantity