Skip to content

Commit 80aa405

Browse files
committed
Added auto-skip mixin metacls, some serious brainfuck, if the required module was not found. Its actually a nice mix between decorators which are class types, and a mixin as a metaclass, which applies said decorator. The InstanceDecorator wouldn't actually be needed, but it adds flexibility. Maybe it should be removed ...
1 parent 690828c commit 80aa405

File tree

3 files changed

+123
-14
lines changed

3 files changed

+123
-14
lines changed

git/test/db/dulwich/lib.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""dulwich specific utilities, as well as all the default ones"""
2+
3+
from git.test.lib import *
4+
5+
#{ Decoorators
6+
7+
def needs_dulwich_or_skip(func):
8+
"""Skip this test if we have no dulwich - print warning"""
9+
return needs_module_or_skip('dulwich')(func)
10+
11+
#}END decorators
12+
13+
#{ MetaClasses
14+
15+
class DulwichRequiredMetaMixin(InheritedTestMethodsOverrideWrapperMetaClsAutoMixin):
16+
decorator = [needs_dulwich_or_skip]
17+
18+
#} END metaclasses

git/test/db/dulwich/test_base.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22
#
33
# This module is part of GitDB and is released under
44
# the New BSD License: http://www.opensource.org/licenses/bsd-license.php
5+
from lib import *
56
from git.test.db.base import RepoBase
67
from git.db.complex import PureCompatibilityGitDB
78

89
try:
9-
import git.db.dulwich # import test
10+
import dulwich
11+
except ImportError:
12+
# om this case, all other dulwich tests will be skipped
13+
pass
1014

11-
class TestPyDBBase(RepoBase):
12-
13-
RepoCls = PureCompatibilityGitDB
15+
class TestPyDBBase(RepoBase):
16+
__metaclass__ = DulwichRequiredMetaMixin
17+
RepoCls = PureCompatibilityGitDB
18+
19+
@needs_dulwich_or_skip
20+
def test_basics(self):
21+
import dulwich
22+
pass
1423

15-
# def test_basics(self):
16-
# pass
17-
18-
except ImportError:
19-
del(RepoBase)
20-
import warnings
21-
warnings.warn("Skipped all dulwich tests as they are not in the path")
22-
#END handle import

git/test/lib/helper.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@
1212
import shutil
1313
import cStringIO
1414

15+
import warnings
16+
from nose import SkipTest
17+
1518
from base import (
1619
maketemp,
1720
rorepo_dir
1821
)
1922

2023

2124
__all__ = (
22-
'StringProcessAdapter', 'GlobalsItemDeletorMetaCls',
23-
'with_rw_repo', 'with_rw_and_rw_remote_repo', 'TestBase', 'TestCase',
25+
'StringProcessAdapter', 'GlobalsItemDeletorMetaCls', 'InheritedTestMethodsOverrideWrapperInstanceDecorator',
26+
'InheritedTestMethodsOverrideWrapperMetaClsAutoMixin',
27+
'with_rw_repo', 'with_rw_and_rw_remote_repo', 'TestBase', 'TestCase', 'needs_module_or_skip'
2428
)
2529

2630

@@ -191,6 +195,27 @@ def remote_repo_creator(self):
191195

192196
return argument_passer
193197

198+
def needs_module_or_skip(module):
199+
"""Decorator to be used for test cases only.
200+
Print a warning if the given module could not be imported, and skip the test.
201+
Otherwise run the test as usual
202+
:param module: the name of the module to skip"""
203+
def argpasser(func):
204+
def wrapper(self, *args, **kwargs):
205+
try:
206+
__import__(module)
207+
except ImportError:
208+
msg = "Module %r is required to run this test - skipping" % module
209+
warnings.warn(msg)
210+
raise SkipTest(msg)
211+
#END check import
212+
return func(self, *args, **kwargs)
213+
#END wrapper
214+
wrapper.__name__ = func.__name__
215+
return wrapper
216+
#END argpasser
217+
return argpasser
218+
194219
#} END decorators
195220

196221
#{ Meta Classes
@@ -214,6 +239,71 @@ def __new__(metacls, name, bases, clsdict):
214239
#END skip case that people import our base without actually using it
215240
#END handle deletion
216241
return new_type
242+
243+
244+
class InheritedTestMethodsOverrideWrapperInstanceDecorator(object):
245+
"""Utility to wrap all inherited methods into a given decorator and set up new
246+
overridden methods on our actual type. This allows to adjust tests which are inherited
247+
by our parent type, automatically. The decorator set in a derived type should
248+
do what it has to do, possibly skipping the test if some prerequesites are not met.
249+
250+
To use it, instatiate it and use it as a wrapper for the __new__ function of your metacls, as in
251+
252+
__new__ = @InheritedTestMethodsOverrideWrapperInstanceDecorator(mydecorator)(MyMetaclsBase.__new__)"""
253+
254+
255+
def __init__(self, decorator):
256+
self.decorator = decorator
257+
258+
def _patch_methods_recursive(self, bases, clsdict):
259+
"""depth-first patching of methods"""
260+
for base in bases:
261+
self._patch_methods_recursive(base.__bases__, clsdict)
262+
for name, item in base.__dict__.iteritems():
263+
if not name.startswith('test_'):
264+
continue
265+
#END skip non-tests
266+
clsdict[name] = self.decorator(item)
267+
#END for each item
268+
#END for each base
269+
270+
def __call__(self, func):
271+
def wrapper(metacls, name, bases, clsdict):
272+
self._patch_methods_recursive(bases, clsdict)
273+
return func(metacls, name, bases, clsdict)
274+
#END wrapper
275+
assert func.__name__ == '__new__', "Can only wrap __new__ function of metaclasses"
276+
wrapper.__name__ = func.__name__
277+
return wrapper
278+
279+
280+
281+
class InheritedTestMethodsOverrideWrapperMetaClsAutoMixin(object):
282+
"""Automatically picks up the actual metaclass of the the type to be created,
283+
that is the one inherited by one of the bases, and patch up its __new__ to use
284+
the InheritedTestMethodsOverrideWrapperInstanceDecorator with our configured decorator"""
285+
286+
#{ Configuration
287+
# decorator function to use when wrapping the inherited methods. Put it into a list as first member
288+
# to hide it from being created as class method
289+
decorator = []
290+
#}END configuration
291+
292+
@classmethod
293+
def _find_metacls(metacls, bases):
294+
"""emulate pythons lookup"""
295+
mcls_attr = '__metaclass__'
296+
for base in bases:
297+
if hasattr(base, mcls_attr):
298+
return getattr(base, mcls_attr)
299+
return metacls._find_metacls(base.__bases__)
300+
#END for each base
301+
raise AssertionError("base class had not metaclass attached")
302+
303+
def __new__(metacls, name, bases, clsdict):
304+
assert metacls.decorator, "'decorator' member needs to be set in subclass"
305+
base_metacls = metacls._find_metacls(bases)
306+
return InheritedTestMethodsOverrideWrapperInstanceDecorator(metacls.decorator[0])(base_metacls.__new__)(base_metacls, name, bases, clsdict)
217307

218308
#} END meta classes
219309

0 commit comments

Comments
 (0)