diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3fb5b4c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Dummy dependencies + run: | + python3 _pip_install_dummy.py PyGObject 3.99 + python3 _pip_install_dummy.py pySerial 3.5 + python3 _pip_install_dummy.py Pillow 7.1 + python3 _pip_install_dummy.py scour 0.37 + python3 _pip_install_dummy.py tinycss2 1.0 + - run: pip install -r requirements.txt pytest + - name: Test + run: | + python3 -m pytest diff --git a/_pip_install_dummy.py b/_pip_install_dummy.py new file mode 100644 index 0000000..b651697 --- /dev/null +++ b/_pip_install_dummy.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +""" +Create a dummy Python package and install it with pip. +""" + +import sys +import subprocess +import tempfile +from pathlib import Path + +name, version = sys.argv[1:] + +pyprojectbody = f"""[project] +name = "{name}" +version = "{version}" +""" + +with tempfile.TemporaryDirectory() as tmpdirname: + assert isinstance(tmpdirname, str) + pyprojectpath = Path(tmpdirname) / "pyproject.toml" + pyprojectpath.write_text(pyprojectbody) + subprocess.check_call([sys.executable, "-m", "pip", "install", tmpdirname]) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e7a58fc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[project] +name = "inkscape-speleo" +version = "1.9" +description = "Inkscape extensions for cave surveying" +authors = [ + {name = "Thomas Holder"}, +] +requires-python = ">=3.7" +dynamic = ["dependencies"] + +[project.urls] +repository = "https://github.com/speleo3/inkscape-speleo" + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} + +[tool.setuptools.packages.find] +where = ["extensions"] + +[tool.yapf] +column_limit = 88 +based_on_style = "pep8" +allow_split_before_dict_value = false +each_dict_entry_on_separate_line = false + + +[tool.ruff] +extend-select = ["W", "B", "Q003"] +ignore = [ + "E401", # Multiple imports on one line + "E501", # Line too long + "E731", # lambda-assignment + "W191", # Indentation contains tabs + "W293", # Blank line contains whitespace +] + +[tool.pytest.ini_options] +addopts = "--strict-markers" +pythonpath = ["extensions"] +testpaths = ["tests"] + +[tool.coverage.run] +source = ["extensions"] + +[tool.mypy] +files = [ + "extensions", + "tests", +] +ignore_missing_imports = true +explicit_package_bases = true + +# vi:sw=4 diff --git a/tests/test_th2ex.py b/tests/test_th2ex.py new file mode 100644 index 0000000..d785f90 --- /dev/null +++ b/tests/test_th2ex.py @@ -0,0 +1,114 @@ +import th2ex +import pytest + + +def _skipunexpected(s): + raise UserWarning(s) + + +th2ex._skipunexpected = _skipunexpected + +multival = lambda *a: tuple(a) + + +def test_quote(): + assert "foo" == th2ex.quote("foo") + assert '"foo bar"' == th2ex.quote('foo bar') + assert '"foo\\" bar"' == th2ex.quote('foo" bar') + assert '[foo bar]' == th2ex.quote('[foo bar]') + + +def test_splitquoted(): + assert ["foo"] == th2ex.splitquoted("foo") + assert ["foo", "bar"] == th2ex.splitquoted("foo bar") + assert ["foo bar"] == th2ex.splitquoted('"foo bar"') + assert ["[foo bar]"] == th2ex.splitquoted('[foo bar]') + + +def test_parse_options(): + parse_options = th2ex.parse_options + + # spaced argument + expected = {'foo': 'bar', 'bar': "Thomas Höhle"} + assert expected == parse_options('-foo bar -bar "Thomas Höhle"') + + # spaced argument with quote + expected = {'foo': 'bar', 'bar': 'Thomas" Höhle'} + assert expected == parse_options('-foo bar -bar "Thomas\\" Höhle"') + + # spaced argument with escaped quote + expected = {'foo': 'bar', 'bar': 'Thomas\\" Höhle'} + assert expected == parse_options(r'-foo bar -bar "Thomas\\\" Höhle"') + + # spaced argument with backslash + expected = {'foo': 'bar', 'bar': 'Thomas Höhle\\'} + assert expected == parse_options('-foo bar -bar "Thomas Höhle\\\\"') + + # spaced argument with quote-backslash + expected = {'foo': 'bar', 'bar': 'Thomas Höhle"\\'} + assert expected == parse_options('-foo bar -bar "Thomas Höhle\\"\\\\"') + + # parse_options with list (needed?) + expected = {'foo': 'bar', 'com': 'bla'} + assert expected == parse_options(['-foo', 'bar', '-com', 'bla']) + + # simple multi-value + expected = {'foo': multival('bar', 'com'), 'bla': True} + assert expected == parse_options('-foo bar com -bla') + + # spaced multi-value + expected = {'author': [multival('2001', 'Thomas Höhle')]} + assert expected == parse_options('-author 2001 "Thomas Höhle"') + + # brackated value + expected = {'foo': '[1 2 3]', 'bla': True} + assert expected == parse_options('-foo [1 2 3] -bla') + + # brackated multi-arg value + expected = {'foo': multival('xxx', '[1 2 3]', 'yyy')} + assert expected == parse_options('-foo xxx [1 2 3] yyy') + + # known multi-arg + expected = {'attr': [multival('foo', '-bar'), multival('x', '123')]} + assert expected == parse_options('-attr foo -bar -attr x 123') + + +def test_format_options(): + assert th2ex.format_options({'foo': 'bar', 'bla': '1 2 3'}) == '-bla "1 2 3" -foo bar' + assert th2ex.format_options({'foo': True, 'bla': '[1 2 3]'}) == '-bla [1 2 3] -foo' + assert th2ex.format_options({'author': "2000 Max"}) == '-author 2000 Max' + assert th2ex.format_options({'author': ("2000 Max")}) == '-author 2000 Max' + assert th2ex.format_options({'author': ["2000 Max"]}) == '-author 2000 Max' + assert th2ex.format_options({'author': [("2000 Max")]}) == '-author 2000 Max' + assert th2ex.format_options({'author': [("2000", "Max Foo"), ("2000", "Jane Bar")]}) == '-author 2000 "Max Foo" -author 2000 "Jane Bar"' + + +def test_name_survex2therion(): + assert th2ex.name_survex2therion('ab.cd.3') == '3@cd.ab' + + +def test_name_therion2survex(): + assert th2ex.name_therion2survex('3@cd.ab') == 'ab.cd.3' + + +def test_is_numeric(): + assert th2ex.is_numeric('abc') is False + assert th2ex.is_numeric('123') is True + assert th2ex.is_numeric('-1e10') is True + + +def test_maybe_key(): + assert th2ex.maybe_key('-foo') is True + assert th2ex.maybe_key('foo') is False + assert th2ex.maybe_key('-a b') is False + assert th2ex.maybe_key('-e10') is True + assert th2ex.maybe_key('-1e10') is False + + +def test_zero_division(): + # unexpected "foo", expect key + with pytest.raises(UserWarning): + th2ex.parse_options('foo') + # unexpected "com" because -attr takes two values + with pytest.raises(UserWarning): + th2ex.parse_options('-attr foo -bar com')