Skip to content

Commit 7e52b60

Browse files
bourkezzzeek
authored andcommitted
Refactor test.util into mako.testing
Fixes: #349 Change-Id: I202c252a913fb72cc328a6e7f0f33174802487d3
1 parent 3c31967 commit 7e52b60

38 files changed

Lines changed: 712 additions & 348 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.. change::
2+
:tags: changed
3+
:tickets: 349
4+
5+
Refactored test utilities into ``mako.testing`` module. Removed
6+
``unittest.TestCase`` dependency in favor of ``pytest``.

mako/testing/__init__.py

Whitespace-only changes.

mako/testing/_config.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import configparser
2+
import dataclasses
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
from typing import Callable
6+
from typing import ClassVar
7+
from typing import Optional
8+
from typing import Union
9+
10+
from .helpers import make_path
11+
12+
13+
class ConfigError(BaseException):
14+
pass
15+
16+
17+
class MissingConfig(ConfigError):
18+
pass
19+
20+
21+
class MissingConfigSection(ConfigError):
22+
pass
23+
24+
25+
class MissingConfigItem(ConfigError):
26+
pass
27+
28+
29+
class ConfigValueTypeError(ConfigError):
30+
pass
31+
32+
33+
class _GetterDispatch:
34+
def __init__(self, initialdata, default_getter: Callable):
35+
self.default_getter = default_getter
36+
self.data = initialdata
37+
38+
def get_fn_for_type(self, type_):
39+
return self.data.get(type_, self.default_getter)
40+
41+
def get_typed_value(self, type_, name):
42+
get_fn = self.get_fn_for_type(type_)
43+
return get_fn(name)
44+
45+
46+
def _parse_cfg_file(filespec: Union[Path, str]):
47+
cfg = configparser.ConfigParser()
48+
try:
49+
filepath = make_path(filespec, check_exists=True)
50+
except FileNotFoundError as e:
51+
raise MissingConfig(f"No config file found at {filespec}") from e
52+
else:
53+
with open(filepath) as f:
54+
cfg.read_file(f)
55+
return cfg
56+
57+
58+
def _build_getter(cfg_obj, cfg_section, method, converter=None):
59+
def caller(option, **kwargs):
60+
try:
61+
rv = getattr(cfg_obj, method)(cfg_section, option, **kwargs)
62+
except configparser.NoSectionError as nse:
63+
raise MissingConfigSection(
64+
f"No config section named {cfg_section}"
65+
) from nse
66+
except configparser.NoOptionError as noe:
67+
raise MissingConfigItem(f"No config item for {option}") from noe
68+
except ValueError as ve:
69+
# ConfigParser.getboolean, .getint, .getfloat raise ValueError
70+
# on bad types
71+
raise ConfigValueTypeError(
72+
f"Wrong value type for {option}"
73+
) from ve
74+
else:
75+
if converter:
76+
try:
77+
rv = converter(rv)
78+
except Exception as e:
79+
raise ConfigValueTypeError(
80+
f"Wrong value type for {option}"
81+
) from e
82+
return rv
83+
84+
return caller
85+
86+
87+
def _build_getter_dispatch(cfg_obj, cfg_section, converters=None):
88+
converters = converters or {}
89+
90+
default_getter = _build_getter(cfg_obj, cfg_section, "get")
91+
92+
# support ConfigParser builtins
93+
getters = {
94+
int: _build_getter(cfg_obj, cfg_section, "getint"),
95+
bool: _build_getter(cfg_obj, cfg_section, "getboolean"),
96+
float: _build_getter(cfg_obj, cfg_section, "getfloat"),
97+
str: default_getter,
98+
}
99+
100+
# use ConfigParser.get and convert value
101+
getters.update(
102+
{
103+
type_: _build_getter(
104+
cfg_obj, cfg_section, "get", converter=converter_fn
105+
)
106+
for type_, converter_fn in converters.items()
107+
}
108+
)
109+
110+
return _GetterDispatch(getters, default_getter)
111+
112+
113+
@dataclass
114+
class ReadsCfg:
115+
section_header: ClassVar[str]
116+
converters: ClassVar[Optional[dict]] = None
117+
118+
@classmethod
119+
def from_cfg_file(cls, filespec: Union[Path, str]):
120+
cfg = _parse_cfg_file(filespec)
121+
dispatch = _build_getter_dispatch(
122+
cfg, cls.section_header, converters=cls.converters
123+
)
124+
kwargs = {
125+
field.name: dispatch.get_typed_value(field.type, field.name)
126+
for field in dataclasses.fields(cls)
127+
}
128+
return cls(**kwargs)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ def eq_(a, b, msg=None):
88
assert a == b, msg or "%r != %r" % (a, b)
99

1010

11+
def ne_(a, b, msg=None):
12+
"""Assert a != b, with repr messaging on failure."""
13+
assert a != b, msg or "%r == %r" % (a, b)
14+
15+
16+
def in_(a, b, msg=None):
17+
"""Assert a in b, with repr messaging on failure."""
18+
assert a in b, msg or "%r not in %r" % (a, b)
19+
20+
21+
def not_in(a, b, msg=None):
22+
"""Assert a in not b, with repr messaging on failure."""
23+
assert a not in b, msg or "%r is in %r" % (a, b)
24+
25+
1126
def _assert_proper_exception_context(exception):
1227
"""assert that any exception we're catching does not have a __context__
1328
without a __cause__, and that __suppress_context__ is never set.

mako/testing/config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
4+
from ._config import ReadsCfg
5+
from .helpers import make_path
6+
7+
8+
@dataclass
9+
class Config(ReadsCfg):
10+
module_base: Path
11+
template_base: Path
12+
13+
section_header = "mako_testing"
14+
converters = {Path: make_path}
15+
16+
17+
config = Config.from_cfg_file("./setup.cfg")

mako/testing/exclusions.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import pytest
2+
3+
from mako.ext.beaker_cache import has_beaker
4+
from mako.util import update_wrapper
5+
6+
7+
try:
8+
import babel.messages.extract as babel
9+
except ImportError:
10+
babel = None
11+
12+
13+
try:
14+
import lingua
15+
except ImportError:
16+
lingua = None
17+
18+
19+
try:
20+
import dogpile.cache # noqa
21+
except ImportError:
22+
has_dogpile_cache = False
23+
else:
24+
has_dogpile_cache = True
25+
26+
27+
requires_beaker = pytest.mark.skipif(
28+
not has_beaker, reason="Beaker is required for these tests."
29+
)
30+
31+
32+
requires_babel = pytest.mark.skipif(
33+
babel is None, reason="babel not installed: skipping babelplugin test"
34+
)
35+
36+
37+
requires_lingua = pytest.mark.skipif(
38+
lingua is None, reason="lingua not installed: skipping linguaplugin test"
39+
)
40+
41+
42+
requires_dogpile_cache = pytest.mark.skipif(
43+
not has_dogpile_cache,
44+
reason="dogpile.cache is required to run these tests",
45+
)
46+
47+
48+
def _pygments_version():
49+
try:
50+
import pygments
51+
52+
version = pygments.__version__
53+
except:
54+
version = "0"
55+
return version
56+
57+
58+
requires_pygments_14 = pytest.mark.skipif(
59+
_pygments_version() < "1.4", reason="Requires pygments 1.4 or greater"
60+
)
61+
62+
63+
# def requires_pygments_14(fn):
64+
65+
# return skip_if(
66+
# lambda: version < "1.4", "Requires pygments 1.4 or greater"
67+
# )(fn)
68+
69+
70+
def requires_no_pygments_exceptions(fn):
71+
def go(*arg, **kw):
72+
from mako import exceptions
73+
74+
exceptions._install_fallback()
75+
try:
76+
return fn(*arg, **kw)
77+
finally:
78+
exceptions._install_highlighting()
79+
80+
return update_wrapper(go, fn)
Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,29 @@
11
import os
2-
import unittest
32

43
from mako.cache import CacheImpl
54
from mako.cache import register_plugin
65
from mako.template import Template
76
from .assertions import eq_
7+
from .config import config
88

99

10-
def _ensure_environment_variable(key, fallback):
11-
env_var = os.getenv(key)
12-
if env_var is None:
13-
return fallback
14-
return env_var
15-
16-
17-
def _get_module_base():
18-
return _ensure_environment_variable(
19-
"TEST_MODULE_BASE", os.path.abspath("./test/templates/modules")
20-
)
21-
22-
23-
def _get_template_base():
24-
return _ensure_environment_variable(
25-
"TEST_TEMPLATE_BASE", os.path.abspath("./test/templates/")
26-
)
27-
28-
29-
module_base = _get_module_base()
30-
template_base = _get_template_base()
31-
32-
33-
class TemplateTest(unittest.TestCase):
10+
class TemplateTest:
3411
def _file_template(self, filename, **kw):
3512
filepath = self._file_path(filename)
3613
return Template(
37-
uri=filename, filename=filepath, module_directory=module_base, **kw
14+
uri=filename,
15+
filename=filepath,
16+
module_directory=config.module_base,
17+
**kw,
3818
)
3919

4020
def _file_path(self, filename):
4121
name, ext = os.path.splitext(filename)
42-
py3k_path = os.path.join(template_base, name + "_py3k" + ext)
22+
py3k_path = os.path.join(config.template_base, name + "_py3k" + ext)
4323
if os.path.exists(py3k_path):
4424
return py3k_path
4525

46-
return os.path.join(template_base, filename)
26+
return os.path.join(config.template_base, filename)
4727

4828
def _do_file_test(
4929
self,
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import contextlib
22
import pathlib
3+
from pathlib import Path
34
import re
45
import time
6+
from typing import Union
57
from unittest import mock
68

7-
from test.util.fixtures import module_base
8-
99

1010
def flatten_result(result):
1111
return re.sub(r"[\s\r\n]+", " ", result).strip()
@@ -19,6 +19,19 @@ def result_lines(result):
1919
]
2020

2121

22+
def make_path(
23+
filespec: Union[Path, str],
24+
make_absolute: bool = True,
25+
check_exists: bool = False,
26+
) -> Path:
27+
path = Path(filespec)
28+
if make_absolute:
29+
path = path.resolve(strict=check_exists)
30+
if check_exists and (not path.exists()):
31+
raise FileNotFoundError(f"No file or directory at {filespec}")
32+
return path
33+
34+
2235
def _unlink_path(path, missing_ok=False):
2336
# Replicate 3.8+ functionality in 3.7
2437
cm = contextlib.nullcontext()
@@ -52,9 +65,3 @@ def rewind_compile_time(hours=1):
5265
with mock.patch("mako.codegen.time") as codegen_time:
5366
codegen_time.time.return_value = rewound
5467
yield
55-
56-
57-
def teardown():
58-
import shutil
59-
60-
shutil.rmtree(module_base, True)

setup.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ exclude =
4242
examples*
4343

4444
[options.extras_require]
45+
testing =
46+
pytest
4547
babel =
4648
Babel
4749
lingua =
@@ -73,6 +75,7 @@ tag_build = dev
7375
[tool:pytest]
7476
addopts= --tb native -v -r fxX -W error
7577
python_files=test/*test_*.py
78+
python_classes=*Test
7679

7780
[upload]
7881
sign = 1
@@ -92,3 +95,7 @@ ignore =
9295
exclude = .venv,.git,.tox,dist,docs/*,*egg,build
9396
import-order-style = google
9497
application-import-names = mako,test
98+
99+
[mako_testing]
100+
module_base = ./test/templates/modules
101+
template_base = ./test/templates/

0 commit comments

Comments
 (0)