Skip to content

Commit

Permalink
Add tests for cli, ucum-ureg and clean-up
Browse files Browse the repository at this point in the history
  • Loading branch information
dalito committed Jan 16, 2024
1 parent a9444d2 commit 5daa308
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 55 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ jobs:
with:
name: coverage-data

- name: Combine coverage and fail if less than 100%
- name: Combine coverage and fail if less than threshold.
run: |
python -m coverage combine
python -m coverage html --skip-empty --skip-covered
# python -m coverage report --fail-under=100 # enable later
python -m coverage html --skip-empty # --skip-covered
python -m coverage report --fail-under=94
- name: Upload HTML report
if: ${{ failure() }}
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ lint = [
"black",
]
dev = [
# Recursively including the project's own optional dependencies requires pip>=21.2
"ucumvert[tests,lint]",
"ruff",
"openpyxl"
"openpyxl",
"pydot",
]

[project.scripts]
Expand Down
13 changes: 9 additions & 4 deletions src/ucumvert/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@
PintUcumRegistry,
UcumToPintStrTransformer,
UcumToPintTransformer,
get_pint_registry,
ucum_preprocessor,
)

try:
from ucumvert._version import __version__, __version_tuple__
except ImportError:
except ImportError: # pragma: no cover
__version__ = "0.0.0"
__version_tuple__ = (0, 0, 0)

try:
import pydot # noqa: F401

HAS_PYDOT = True
except ImportError: # pragma: no cover
HAS_PYDOT = False

__all__ = [
"get_ucum_parser",
"get_pint_registry",
"make_parse_tree_png",
"ucum_preprocessor",
"update_lark_ucum_grammar_file",
Expand Down Expand Up @@ -54,7 +59,7 @@ def setup_logging(loglevel: int = logging.INFO, logfile: Path | None = None) ->
# Setup handler for logging to console
logging.basicConfig(level=loglevel, format="%(levelname)-8s|%(message)s")

if logfile is not None:
if logfile is not None: # pragma: no cover
# Setup handler for logging to file
fh = logging.handlers.RotatingFileHandler(
logfile, maxBytes=100000, backupCount=5
Expand Down
45 changes: 21 additions & 24 deletions src/ucumvert/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,34 @@

from lark.exceptions import LarkError, UnexpectedInput, VisitError

from ucumvert import __version__, setup_logging
import ucumvert
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

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:
if not ucumvert.HAS_PYDOT:
print("Package pydot not installed, skipping parse-tree image generation.")

ucum_parser = get_ucum_parser()
transformer = UcumToPintTransformer().transform

while True:
ucum_code = input("> ")
if ucum_code in "qQ":
print(f"input: {ucum_code!r}")
if ucum_code and (ucum_code in "qQ"):
break
try:
if has_pydot:
if ucumvert.HAS_PYDOT:
parsed_data = make_parse_tree_png(
ucum_code, filename="parse_tree.png", parser=ucum_parser
ucum_parser, ucum_code, filename="parse_tree.png"
)
print("Created visualization of parse tree (parse_tree.png).")
else:
Expand All @@ -49,9 +44,9 @@ def interactive():
continue
try:
# parsed_data = ucum_parser.parse(data) # parse data without visualization
pint_quantity = UcumToPintTransformer().transform(parsed_data)
pint_quantity = transformer(parsed_data)
print(f"--> Pint {pint_quantity!r}")
except (VisitError, ValueError) as e:
except (VisitError, ValueError) as e: # pragma: no cover
print(e)
continue

Expand Down Expand Up @@ -91,15 +86,14 @@ def _split_lines(self, text, width):


def root_cmds(args):
if args.version: # pragma: no cover
print(f"ucumvert {__version__}")
if args.version:
print(f"ucumvert {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)
update_lark_ucum_grammar_file(grammar_file=args.grammar_update)


def create_root_parser():
Expand All @@ -125,10 +119,13 @@ def create_root_parser():
"-g",
"--grammar_update",
help=(
"Recreate grammar file 'ucum_grammar.lark' with UCUM atoms "
"extracted from ucum-essence.xml."
"Create grammar file with UCUM atoms extracted from ucum-essence.xml. "
"Default is to write to 'ucum_grammar.lark' in the current directory."
),
action="store_true",
type=Path,
metavar=("FILE"),
nargs="?", # make file an optional argument
const=Path("ucum_grammar.lark"), # default value
)
parser.add_argument(
"-m",
Expand Down Expand Up @@ -159,7 +156,7 @@ def main_cli(raw_args=None):
# 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)
ucumvert.setup_logging(loglevel=logging.INFO)
args.func(args)


Expand All @@ -169,10 +166,10 @@ def run_cli_app(raw_args=None):
raw_args = sys.argv[1:]
try:
main_cli(raw_args)
except LarkError:
except LarkError: # pragma: no cover
logger.exception("Terminating with ucumvert error.")
sys.exit(1)
except Exception:
except Exception: # pragma: no cover
logger.exception("Unexpected error.")
sys.exit(3) # value 2 is used by argparse for invalid args.

Expand Down
9 changes: 2 additions & 7 deletions src/ucumvert/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,7 @@ def get_ucum_parser(grammar_file=None):
return Lark(ucum_grammar, start="main_term", strict=True)


def make_parse_tree_png(data, filename="parse_tree_unit.png", parser=None):
if parser is None:
parser = get_ucum_parser()
def make_parse_tree_png(parser, data, filename="parse_tree_unit.png"):
parsed_data = parser.parse(data)
try:
tree.pydot__tree_to_png(parsed_data, filename)
except ImportError:
logger.warning("pydot not installed, skipping png generation")
tree.pydot__tree_to_png(parsed_data, filename)
return parsed_data
17 changes: 3 additions & 14 deletions src/ucumvert/ucum_pint.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ def ucum_preprocessor(unit_input):
Note: This will make most standard pint unit expressions invalid.
Usage:
>>> import pint
>>> ureg = pint.get_application_registry()
>>> from ucumvert import PintUcumRegistry, ucum_preprocessor
>>> ureg = PintUcumRegistry()
>>> ureg.preprocessors.append(ucum_preprocessor)
"""
ucum_parser = get_ucum_parser()
Expand Down Expand Up @@ -337,17 +337,6 @@ def find_matching_pint_definitions(report_file: Path | None = None) -> None:
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 = get_application_registry()
if "peripheral_vascular_resistance_unit" not in ureg: # UCUM already loaded?
ureg.load_definitions(Path(__file__).resolve().parent / "pint_ucum_defs.txt")
if ucum_preprocessor not in ureg.preprocessors:
ureg.preprocessors.append(ucum_preprocessor)
return ureg


class PintUcumRegistry(UnitRegistry):
def _after_init(self) -> None:
"""This is called after all __init__"""
Expand Down Expand Up @@ -375,7 +364,7 @@ def from_ucum(self, ucum_code):
return self._from_ucum_transformer(parsed_data)


def run_examples():
def run_examples(): # pragma: no cover
test_ucum_units = [
# "Cel",
# "/s2",
Expand Down
69 changes: 69 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import logging
import os
from unittest import mock

import pytest
from ucumvert.cli import main_cli, run_cli_app


def test_run_cli_app_no_args_entrypoint(monkeypatch, capsys):
monkeypatch.setattr("sys.argv", ["ucumvert"])
run_cli_app()
captured = capsys.readouterr()
assert "usage: ucumvert" in captured.out


def test_run_cli_app_no_args(capsys):
run_cli_app([])
captured = capsys.readouterr()
assert "usage: ucumvert" in captured.out


def test_main_unknown_arg(capsys):
with pytest.raises(SystemExit) as exc_info:
main_cli(["--unknown-arg"])
assert exc_info.value.code == 2 # noqa: PLR2004
captured = capsys.readouterr()
assert "ucumvert: error: unrecognized arguments: --unknown-arg" in captured.err


def test_main_version(capsys):
main_cli(["--version"])
captured = capsys.readouterr()
assert captured.out.startswith("ucumvert")


def test_run_mapping_report_generation(tmp_path):
dst = tmp_path / "mapping.txt"
main_cli(["--mapping_report", str(dst)])
expected = dst
assert expected.exists()


def test_run_grammar_update(tmp_path):
dst = tmp_path / "ucum_grammar.lark"
main_cli(["--grammar_update", str(dst)])
expected = dst
assert expected.exists()


@pytest.mark.parametrize(("pydot_installed"), [(True), (False)])
def test_run_interactive(monkeypatch, capsys, pydot_installed):
if not pydot_installed:
monkeypatch.setattr("ucumvert.HAS_PYDOT", False)
inputs = ["kg", "missing", "q"]
input_generator = (i for i in inputs)
monkeypatch.setattr("builtins.input", lambda _: next(input_generator))
main_cli(["--interactive"])
captured = capsys.readouterr()
assert "Pint <Quantity(1, 'kilogram')>" in captured.out


@mock.patch.dict(os.environ, {"LOGLEVEL": "ERROR"})
def test_valid_config(caplog, tmp_path):
# Don't remove "temp_config". The fixture avoid global config change.
dst = tmp_path / "grammar_log.txt"
main_cli(["-g", str(dst)])
with caplog.at_level(logging.ERROR):
main_cli(["-g", str(dst)])
assert not caplog.text
22 changes: 21 additions & 1 deletion tests/test_ucum_pint.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import pytest
from lark import LarkError
from pint import UnitRegistry
from test_parser import ucum_examples_valid
from ucumvert import UcumToPintStrTransformer, UcumToPintTransformer
from ucumvert import (
PintUcumRegistry,
UcumToPintStrTransformer,
UcumToPintTransformer,
ucum_preprocessor,
)
from ucumvert.ucum_pint import find_ucum_codes_that_need_mapping
from ucumvert.xml_util import get_metric_units, get_non_metric_units

Expand Down Expand Up @@ -90,3 +96,17 @@ def test_ucum_all_unit_atoms_pint_vs_str(
expected_quantity = transform_ucum_pint(parsed_atom)
result_str = transform_ucum_str(parsed_atom)
assert ureg_ucumvert(result_str) == expected_quantity


def test_ucum_preprocessor(ureg_ucumvert):
expected = ureg_ucumvert("m*kg")
ureg_ucumvert.preprocessors.append(ucum_preprocessor)
assert ureg_ucumvert("m.kg") == expected
with pytest.raises(LarkError):
ureg_ucumvert("degC")


def test_ucum_unitregistry():
ureg = PintUcumRegistry()
assert ureg.from_ucum("m.kg") == ureg("m*kg")
assert ureg.from_ucum("Cel") == ureg("degC")

0 comments on commit 5daa308

Please sign in to comment.