From 88396740f746c76d20ee9258bfbed8fae0c5ab2b Mon Sep 17 00:00:00 2001 From: Jay Marcyes Date: Tue, 4 Feb 2020 19:12:09 -0700 Subject: [PATCH] v1.2.0. Adds a mock interface that I needed for another project, this interface is only partly related to issue #35 and issue #45. It might eventually work for all of those ideas but right now the interface is rough. Moves data.py into the data/ util to remove a warning, I'm not sure why python was complaining about the data directory not having an __init__.py file when there was a data.py file, but it was. Updates the setup.py file --- setup.py | 60 +++++++++------- testdata/__init__.py | 45 +++++++++++- testdata/{data.py => data/__init__.py} | 0 testdata/test.py | 8 +-- testdata/utils.py | 96 ++++++++++++++++++++++++++ testdata_test.py | 39 +++++++++++ 6 files changed, 217 insertions(+), 31 deletions(-) rename testdata/{data.py => data/__init__.py} (100%) diff --git a/setup.py b/setup.py index 812aab0..5c99f92 100644 --- a/setup.py +++ b/setup.py @@ -7,29 +7,16 @@ from codecs import open -name = 'testdata' -with open(os.path.join(name, "__init__.py")) as f: - version = re.search("^__version__\s*=\s*[\'\"]([^\'\"]+)", f.read(), flags=re.I | re.M).group(1) - -long_description = "" -if os.path.isfile('README.rst'): - with open('README.rst', encoding='utf-8') as f: - long_description = f.read() - -setup( - name=name, - version=version, - description='Easily generate random unicode test data among other things', - long_description=long_description, - author='Jay Marcyes', - author_email='jay@marcyes.com', - url='http://github.com/Jaymon/{}'.format(name), - #py_modules=[name], - packages=find_packages(), - package_data={name: ['data/*']}, - license="MIT", - #tests_require=[], - classifiers=[ # https://pypi.python.org/pypi?:action=list_classifiers +name = "testdata" +kwargs = { + "name": name + "description": 'Easily generate random unicode test data among other things', + "author": 'Jay Marcyes', + "author_email": 'jay@marcyes.com', + "url": 'http://github.com/Jaymon/{}'.format(name), + "package_data": {name: ['data/*']}, + "license": "MIT", + "classifiers": [ # https://pypi.python.org/pypi?:action=list_classifiers 'Development Status :: 4 - Beta', 'Environment :: Plugins', 'Intended Audience :: Developers', @@ -41,6 +28,29 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - test_suite = "{}_test".format(name), -) + "test_suite": "{}_test".format(name), +} + +def read(path): + if os.path.isfile(path): + with open(path, encoding='utf-8') as f: + return f.read() + return "" + + +vpath = os.path.join(name, "__init__.py") +if os.path.isfile(vpath): + kwargs["packages"] = find_packages() +else: + vpath = "{}.py".format(name) + kwargs["py_modules"] = [name] +kwargs["version"] = re.search(r"^__version__\s*=\s*[\'\"]([^\'\"]+)", read(vpath), flags=re.I | re.M).group(1) + + +# https://pypi.org/help/#description-content-type +kwargs["long_description"] = read('README.md') +kwargs["long_description_content_type"] = "text/markdown" + + +setup(**kwargs) diff --git a/testdata/__init__.py b/testdata/__init__.py index e0ca7ce..438aae6 100644 --- a/testdata/__init__.py +++ b/testdata/__init__.py @@ -32,7 +32,7 @@ import pkgutil from .compat import * -from .utils import String, ByteString +from .utils import String, ByteString, Mock from .data import ( _names, _unicode_names, @@ -75,7 +75,7 @@ from .image import make_png -__version__ = '1.1.6' +__version__ = '1.2.0' # get rid of "No handler found" warnings (cribbed from requests) @@ -669,6 +669,8 @@ def get_filename(ext="", prefix="", name=""): return get_module_name(prefix=prefix, postfix=ext, name=name) get_file_name = get_filename +filename = get_filename +file_name = get_filename def get_module_name(bits=1, prefix="", postfix="", name=""): @@ -704,6 +706,16 @@ def get_module_name(bits=1, prefix="", postfix="", name=""): get_modpath = get_module_name get_modulepath = get_module_name get_module_path = get_module_name +modulename = get_module_name +module_name = get_module_name + + +def get_classname(name=""): + n = get_filename(name=name) + return n.title() +get_class_name = get_classname +classname = get_classname +class_name = get_classname def get_source_filepath(v): @@ -1541,6 +1553,35 @@ def patch(mod, patches=None, **kwargs_patches): return m +def mock_class(name="", **props_and_methods): + """create a class with the given method and properties + + :param name: string, the name of the class, default is just a random classname + :param **props_and_methods: dict, keys will be attributes on the object + :returns: type, the object + """ + classname = get_classname(name=name) + return type(ByteString(classname), (object,), props_and_methods) + + +def mock_instance(name="", **props_and_methods): + """This is the same as mock_class but returns an instance of that class + + see mock_class() docs""" + return mock_class(name, **props_and_methods)() + + +def mock(**props_and_methods): + """Create a mocked object that tries to be really magical + + This is different than mock_instance because it creates an object that uses a lot + of magic to try and be a jack of all trades + + :returns: Mock instance + """ + return Mock(**props_and_methods) + + def get_birthday(as_str=False, start_age=18, stop_age=100): """ return a random YYYY-MM-DD diff --git a/testdata/data.py b/testdata/data/__init__.py similarity index 100% rename from testdata/data.py rename to testdata/data/__init__.py diff --git a/testdata/test.py b/testdata/test.py index e6a5611..1ffeadf 100644 --- a/testdata/test.py +++ b/testdata/test.py @@ -193,21 +193,21 @@ def assertRegex(self, s, r): https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRegex """ - self.assertRegexpMatches(s, r) + return self.assertRegexpMatches(s, r) def assertNotRegex(self, s, r): """brings py3 assert to py2 https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertNotRegex """ - self.assertNotRegexpMatches(s, r) + return self.assertNotRegexpMatches(s, r) - def assertRaisesRegex(self, exception, regex, callable, *args, **kwds): + def assertRaisesRegex(self, exception, regex, *args, **kwds): """brings py3 assert to py2 https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaisesRegex """ - self.assertRaisesRegexp(exception, regex, callable, *args, **kwds) + return self.assertRaisesRegexp(exception, regex, *args, **kwds) def assertLogs(self, logger=None, level=None): """This just makes sure something gets logged, and raises an exception diff --git a/testdata/utils.py b/testdata/utils.py index e50fef4..cb4d60b 100644 --- a/testdata/utils.py +++ b/testdata/utils.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals, division, print_function, absolute_import +import types +import inspect import sys + from .compat import * @@ -37,3 +40,96 @@ def __new__(cls, val, encoding="UTF-8"): instance.encoding = encoding return instance + +class Mock(object): + """Do our very best to mock functionality + + This class tries to make it easy to mock an object, and can be used to quickly + get an object that can match a certain state, it isn't exhaustive and will fail + things like isinstance checks, but it does it's very best to get you a mocked + up object that you can use for testing. + + If the given attribute doesn't exist then this will return an instance of self + so you can chain together objects of any depth with just the first object + + :Example: + m = Mock(foo=1) + m.foo() # 1 + m.foo # 1 + m.bar.che.foo # 1 + + m = Mock(foo=ValueError("the error you want to raise")) + m.foo(1, 2) # raises ValueError + """ + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + def __getitem__(self, key): + try: + v = super(Mock, self).__getattribute__(key) + + except AttributeError: + return self + + else: + self._raise_if_error(v) + #self.__dict__["_raise_if_error"](v) + return v + + def __getattribute__(self, key): + if hasattr(type(self), key): + return super(Mock, self).__getattribute__(key) + + try: + v = super(Mock, self).__getattribute__(key) + + except AttributeError: + return self + + else: + if v is not None: + + self._raise_if_error(v) + #self.__dict__["_raise_if_error"](v) + + if not hasattr(v, "__call__"): + + frames = inspect.stack() + frame = frames[1] + loc = "\n".join(frame[4]) + if ".{}(".format(key) in loc or ".{}".format(key) not in loc: + + class MockAttr(type(v)): + def __new__(cls, *args, **kwargs): + return super(MockAttr, cls).__new__(cls, *args, **kwargs) + + def __call__(self, *args, **kwargs): + return v + + return MockAttr(v) + + return v + + def _raise_if_error(self, v): + do_raise = False + try: + do_raise = isinstance(v, Exception) + + except TypeError: + pass + + else: + if do_raise: + raise v + + try: + do_raise = issubclass(v, Exception) + + except TypeError: + pass + + else: + if do_raise: + raise v() + diff --git a/testdata_test.py b/testdata_test.py index cfba5f9..5c6b976 100644 --- a/testdata_test.py +++ b/testdata_test.py @@ -427,6 +427,45 @@ def test_modules_prefix(self): self.assertTrue(modpath in mps) +class MockTest(TestCase): +# def test_mock_instance(self): +# instance = testdata.mock_instance(foo=1, bar=lambda *a, **kw: 5) +# pout.v(instance.foo) +# pout.v(instance.bar()) + + def test_mock_instance(self): + instance = testdata.mock(foo="1", bar=2, che=lambda *a, **k: 3) + self.assertEqual("1", instance.foo) + self.assertEqual("1", instance.foo()) + self.assertTrue(isinstance(instance.bar, int)) + self.assertTrue(isinstance(instance.bar(), int)) + + def test_mock_dict(self): + d = testdata.mock(foo="1", bar=2) + self.assertEqual("1", d["foo"]) + self.assertEqual("1", d["che"]["baz"]["foo"]) + + def test_mock_depth(self): + """make sure we can mock one object and have it look like many objects""" + instance = testdata.mock(foo=1) + self.assertEqual(1, instance.bar.che.foo) + + def test_mock_error(self): + instance = testdata.mock(foo=AttributeError, bar=RuntimeError("bar is bad")) + + with self.assertRaises(AttributeError): + instance.foo() + + with self.assertRaises(AttributeError): + instance.foo + + with self.assertRaisesRegex(RuntimeError, "bar\s+is\s+bad"): + instance.bar + + with self.assertRaisesRegex(RuntimeError, "bar\s+is\s+bad"): + instance.bar() + + class TestdataTest(TestCase): def test_get_filename(self): n = testdata.get_filename(ext="py", name="foo")