Repository: Vipul-Cariappa/py-utility

Issue:
Multiprocessing Errors
Python throws EOFError and RuntimeError, when decorator is used. Even when `__name__ == __main__` if statement is present it throws PicklingError. This is because multiprocessing is using spawn to create new processes. Some details can be found here: [StackOverFlow Question](https://stackoverflow.com/questions/41385708/multiprocessing-example-giving-attributeerror)
Either using other method to create new process or changing it to function may resolve the problem.

Discussion:

Patch:
diff --git a/README.md b/README.md
index e6336ee..47feb7f 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,8 @@ Utility functions for managing and monitoring python resources.
 ## installation
 <pre><code>pip install py-utility</code></pre>
 
-## Decorator Function
+
+## Documentation
 
 ### memoryit
 memoryit returns the total memory used by the function at runtime in bytes.
@@ -12,25 +13,32 @@ memoryit returns the total memory used by the function at runtime in bytes.
 Example Code:
 <br>
 <pre><code>from pyutility import memoryit
-@memoryit
+
 def prime_check(x):
   for i in range(2, x):
     if x % i == 0:
       return False
-  return True</code></pre>
+  return True
+  
+if __name__ == "__main__":
+  ...
+  memoryit(prime_check, args=(89,))
+  ...
+  
+  </code></pre>
 
 ### limit_memory
-limit_memory limits the memory consumption of function at runtime. It takes the limit in MB. You will find unexpected behaviour if too low value is set. The default value is 25 MB. It is throw MemoryError if it exceeds the limit.
+limit_memory limits the memory consumption of function at runtime. It takes the limit in MB. You will find unexpected behaviour if too low value is set. The default value is 25 MB. It throws MemoryError if it exceeds the limit.
 <br>
 Example Code:
 <br>
 <pre><code>from pyutility import limit_memory
-@limit_memory(30)
-def prime_check(x):
-  for i in range(2, x):
-    if x % i == 0:
-      return False
-  return True</code></pre>
+
+if __name__ == "__main__":
+  ...
+  limit_memory(prime_check, memory=30, args=(89,))
+  ...
+  </code></pre>
   
 ### timeit
 timeit returns the total time take to execute the function in seconds.
@@ -38,12 +46,11 @@ timeit returns the total time take to execute the function in seconds.
 Example Code:
 <br>
 <pre><code>from pyutility import timeit
-@timeit
-def prime_check(x):
-  for i in range(2, x):
-    if x % i == 0:
-      return False
-  return True</code></pre>
+
+if __name__ == "__main__":
+   ...
+   timeit(prime_check, args=(89,))
+   ...</code></pre>
 
 ### limit_time
 limit_time limits the time used to execute the function. It takes limit as seconds. The default value is 10 seconds. It throws TimeoutError if it exceeds the limit.
@@ -51,12 +58,12 @@ limit_time limits the time used to execute the function. It takes limit as secon
 Example Code:
 <br>
 <pre><code>from pyutility import limit_time
-@limit_time(30)
-def prime_check(x):
-  for i in range(2, x):
-    if x % i == 0:
-      return False
-  return True</code></pre>
+
+if __name__ == "__main__":
+  ...
+  limit_time(prime_check, time=2, args=(89,))
+  ...</code></pre>
   
   ## Contribution
   All contributions are welcomed. If it is a bug fix or new feature, creating new issue is highly recommend before any pull request.
+  If you want any new feature to be included create new issue asking for the feature with a use case and example code if possible.
diff --git a/pyutility/memoryutil.py b/pyutility/memoryutil.py
index 29c5f3f..ca8d3ed 100644
--- a/pyutility/memoryutil.py
+++ b/pyutility/memoryutil.py
@@ -4,7 +4,7 @@ import resource
 
 
 def me_worker(func, storage, *args, **kwargs):
-    """measures the peak memory consumption of given function; should be run by memoryit decorator as a new process
+    """measures the peak memory consumption of given function
 
     Args:
         func (`function`): function to execute
@@ -30,11 +30,11 @@ def me_worker(func, storage, *args, **kwargs):
 
 
 def li_worker(func, limit, storage, *args, **kwargs):
-    """limits the memory consumption of given function; should be run by limit_memory decorator as a new process
+    """limits the memory consumption of given function
 
     Args:
         func (`function`): function to execute
-        limit (`int`): maximum allowed memory consuption
+        limit (`int`): maximum allowed memory consumption
         storage (`list`): multiprocessing.Manager().List() to store the peak memory
         args (`tuple`): arguments for the function
         kwargs(`dict`): keyword arguments for the function
@@ -57,53 +57,51 @@ def li_worker(func, limit, storage, *args, **kwargs):
     return 0
 
 
-def memoryit(func):
-    """decorator function to measures the peak memory consumption of given function
+def memoryit(func, args=(), kwargs={}):
+    """measures the peak memory consumption of given function
 
     Args:
         func (`function`): function to execute
+        args (`tuple`): arguments for the function
+        kwargs(`dict`): keyword arguments for the function
 
     Return:
         peak memory used during the execution of given function in bytes (`int`)
     """
-    def wrapper(*args, **kwargs):
-        ctx = mp.get_context('spawn')
-        manager = ctx.Manager()
-        com_obj = manager.list()
-        p = ctx.Process(target=me_worker, args=(
-            func, com_obj, *args), kwargs=kwargs)
-        p.start()
-        p.join()
-
-        return com_obj[-1]
+    ctx = mp.get_context('spawn')
+    manager = ctx.Manager()
+    com_obj = manager.list()
+    p = ctx.Process(target=me_worker, args=(
+        func, com_obj, *args), kwargs=kwargs)
+    p.start()
+    p.join()
 
-    return wrapper
+    return com_obj[-1]
 
 
-def limit_memory(value=15):
-    """decorator function to limits the memory consumption of given function
+def limit_memory(func, memory=25, args=(), kwargs={}):
+    """limits the memory consumption of given function. 
+    If limit set is very low it will not behave in expected way.
 
     Args:
-        value (`int`): maximum allowed memory consumption in MB
         func (`function`): function to execute
+        limit (`int`): maximum allowed memory consumption in MB default is 25 MB
+        args (`tuple`): arguments for the function
+        kwargs(`dict`): keyword arguments for the function
 
     Return:
         return value of function or MemoryError
     """
-    def decorator(func):
-        def wrapper(*args, **kwargs):
-            ctx = mp.get_context('spawn')
-            manager = ctx.Manager()
-            com_obj = manager.list()
-            p = ctx.Process(target=li_worker, args=(
-                func, value, com_obj, *args), kwargs=kwargs)
-            p.start()
-            p.join()
-
-            if isinstance(com_obj[-1], Exception):
-                raise com_obj[-1]
-            else:
-                return com_obj[-1]
-
-        return wrapper
-    return decorator
+
+    ctx = mp.get_context('spawn')
+    manager = ctx.Manager()
+    com_obj = manager.list()
+    p = ctx.Process(target=li_worker, args=(
+        func, memory, com_obj, *args), kwargs=kwargs)
+    p.start()
+    p.join()
+
+    if isinstance(com_obj[-1], Exception):
+        raise com_obj[-1]
+    else:
+        return com_obj[-1]
diff --git a/pyutility/timeutil.py b/pyutility/timeutil.py
index 325830a..dc2c0bd 100644
--- a/pyutility/timeutil.py
+++ b/pyutility/timeutil.py
@@ -4,8 +4,7 @@ import signal
 
 
 def me_worker(func, storage, *args, **kwargs):
-    """measures the time taken to execute given function should be run by timeit decorator as a new process
-
+    """measures the time taken to execute given function
     Args:
         func (`function`): function to execute
         storage (`list`): multiprocessing.Manager().List() to store the time taken to excute
@@ -13,7 +12,7 @@ def me_worker(func, storage, *args, **kwargs):
         kwargs(`dict`): keyword arguments for the function
 
     Return:
-        time taken to execute the given function in seconds (`int`)
+        time taken to execute the given function in seconds (`float`)
     """
     t1 = time()
     func(*args, **kwargs)
@@ -23,7 +22,7 @@ def me_worker(func, storage, *args, **kwargs):
 
 
 def li_worker(func, time, storage, *args, **kwargs):
-    """limits the time taken for exection of given function; should be run by limit_time decorator as a new process
+    """limits the time taken for exection of given function
 
     Args:
         func (`function`): function to execute
@@ -52,53 +51,51 @@ def li_worker(func, time, storage, *args, **kwargs):
     return 0
 
 
-def timeit(func):
-    """decorator function to measure time taken to execute a given function in new process
+def timeit(func, args=(), kwargs={}):
+    """measures the time taken to execute given function
 
     Args:
         func (`function`): function to execute
+        args (`tuple`): arguments for the function
+        kwargs(`dict`): keyword arguments for the function
 
     Return:
-        time taken to execute the given function in seconds (`int`)
+        time taken to execute the given function in seconds (`float`)
     """
-    def wrapper(*args, **kwargs):
-        ctx = mp.get_context('spawn')
-        manager = ctx.Manager()
-        com_obj = manager.list()
-        p = ctx.Process(target=me_worker, args=(
-            func, com_obj, *args), kwargs=kwargs)
-        p.start()
-        p.join()
 
-        return com_obj[-1]
-    return wrapper
+    ctx = mp.get_context('spawn')
+    manager = ctx.Manager()
+    com_obj = manager.list()
+    p = ctx.Process(target=me_worker, args=(
+        func, com_obj, *args), kwargs=kwargs)
+    p.start()
+    p.join()
+
+    return com_obj[-1]
 
 
-def limit_time(time=10):
-    """decorator function to limits the time taken to execute given function
+def limit_time(func, time=10, args=(), kwargs={}):
+    """limits the time taken for exection of given function
 
     Args:
-        value (`int`): maximum allowed time in seconds
         func (`function`): function to execute
+        limit (`int`): maximum allowed time in seconds, default is 10
+        args (`tuple`): arguments for the function
+        kwargs(`dict`): keyword arguments for the function
 
     Return:
         return value of function or TimeoutError
     """
-    def inner(func):
-        def wrapper(*args, **kwargs):
-
-            ctx = mp.get_context('spawn')
-            manager = ctx.Manager()
-            com_obj = manager.list()
-            p = ctx.Process(target=li_worker, args=(
-                func, time, com_obj, *args), kwargs=kwargs)
-            p.start()
-            p.join()
-
-            if isinstance(com_obj[-1], Exception):
-                raise com_obj[-1]
-            else:
-                return com_obj[-1]
-
-        return wrapper
-    return inner
+
+    ctx = mp.get_context('spawn')
+    manager = ctx.Manager()
+    com_obj = manager.list()
+    p = ctx.Process(target=li_worker, args=(
+        func, time, com_obj, *args), kwargs=kwargs)
+    p.start()
+    p.join()
+
+    if isinstance(com_obj[-1], Exception):
+        raise com_obj[-1]
+    else:
+        return com_obj[-1]
diff --git a/pyutility/utility.py b/pyutility/utility.py
index d0eefbc..841fd7e 100644
--- a/pyutility/utility.py
+++ b/pyutility/utility.py
@@ -15,7 +15,7 @@ def me_worker(func, storage, *args, **kwargs):
         kwargs(`dict`): keyword arguments for the function
 
     Return:
-        peak memory used, time taken during the execution of given function in bytes (`list` of 'int', 'float')
+        peak memory used, time taken for the execution of given function (`list` of 'int', 'float')
     """
 
     tm.start()
@@ -74,58 +74,54 @@ def li_worker(func, storage, time, memory, *args, **kwargs):
     return 0
 
 
-def measureit(func):
-    """decorator function to measures the peak memory consumption and time taken to execute given function
+def measureit(func, args=(), kwargs={}):
+    """measures the peak memory consumption and time taken for execution of given function
 
     Args:
         func (`function`): function to execute
+        args (`tuple`): arguments for the function
+        kwargs(`dict`): keyword arguments for the function
 
     Return:
-        peak memory used, time taken during the execution of given function in bytes (`tuple` of 'int', 'float')
+        peak memory used (MB), time taken (seconds) during the execution of given function (`list` of 'int', 'float')
     """
-    def wrapper(*args, **kwargs):
-        ctx = mp.get_context('spawn')
-        manager = ctx.Manager()
-        com_obj = manager.list()
-        p = ctx.Process(target=me_worker, args=(
-            func, com_obj, *args), kwargs=kwargs)
-        p.start()
-        p.join()
-
-        if len(com_obj) == 2:
-            return tuple(com_obj)
-
-        # else
-        raise com_obj[-1]
+    ctx = mp.get_context('spawn')
+    manager = ctx.Manager()
+    com_obj = manager.list()
+    p = ctx.Process(target=me_worker, args=(
+        func, com_obj, *args), kwargs=kwargs)
+    p.start()
+    p.join()
 
-    return wrapper
+    if len(com_obj) == 2:
+        return tuple(com_obj)
 
+    # else
+    raise com_obj[-1]
 
-def limit_resource(time=10, memory=25):
-    """decorator function to limits the memory consumption and time taken to execute given function
+
+def limit_resource(func, time=10, memory=25, args=(), kwargs={}):
+    """limits the memory consumption and time taken to execute given function
 
     Args:
-        time (`int`): maximum allowed time consuption in seconds
-        memory (`int`): maximum allowed memory consuption in MB
         func (`function`): function to execute
+        time (`int`): maximum allowed time in seconds, default is 10
+        memory (`int`): maximum allowed memory consumption in MB, default is 25
+        args (`tuple`): arguments for the function
+        kwargs(`dict`): keyword arguments for the function
 
     Return:
         return value of function or MemoryError or TimeoutError
     """
-    def decorator(func):
-        def wrapper(*args, **kwargs):
-            ctx = mp.get_context('spawn')
-            manager = ctx.Manager()
-            com_obj = manager.list()
-            p = ctx.Process(target=li_worker, args=(
-                func, com_obj, time, memory, *args), kwargs=kwargs)
-            p.start()
-            p.join()
-
-            if isinstance(com_obj[-1], Exception):
-                raise com_obj[-1]
-            else:
-                return com_obj[-1]
-
-        return wrapper
-    return decorator
+    ctx = mp.get_context('spawn')
+    manager = ctx.Manager()
+    com_obj = manager.list()
+    p = ctx.Process(target=li_worker, args=(
+        func, com_obj, time, memory, *args), kwargs=kwargs)
+    p.start()
+    p.join()
+
+    if isinstance(com_obj[-1], Exception):
+        raise com_obj[-1]
+    # else
+    return com_obj[-1]
diff --git a/setup.py b/setup.py
index a1aebf6..744b7df 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
 
 setup(
     name="py-utility",
-    version="0.1.1",
+    version="0.1.0",
     description="Utility functions for managing and monitoring python resources",
     long_description="Utility functions for managing and monitoring python resources",
     url="https://github.com/Vipul-Cariappa/py-utility",
@@ -11,7 +11,7 @@ setup(
     author_email="vipulcariappa@gmail.com",
     license="MIT",
     classifiers=[
-        "Development Status :: 4 - Beta",
+        "Development Status :: 3 - Alpha",
         "License :: OSI Approved :: MIT License",
         "Operating System :: Unix",
         "Programming Language :: Python :: 3.9",


Test patch:
diff --git a/tests/func.py b/tests/func.py
new file mode 100644
index 0000000..cbe21ea
--- /dev/null
+++ b/tests/func.py
@@ -0,0 +1,20 @@
+def memory(x):
+    x = [i for i in range(x)]
+    return -1
+
+
+def time(x):
+    # recursive function to find xth fibonacci number
+    if x < 3:
+        return 1
+    return time(x-1) + time(x-2)
+
+
+def error(x=None):
+    # error function
+    return "a" / 2
+
+
+def return_check(*args, **kwagrs):
+    # args and kwargs function
+    return list(args) + list(kwagrs.values())
diff --git a/tests/test_memory.py b/tests/test_memory.py
index c5b3197..437a8ac 100644
--- a/tests/test_memory.py
+++ b/tests/test_memory.py
@@ -1,50 +1,46 @@
 from unittest import TestCase
 from pyutility import limit_memory, memoryit
 
-
-def func1(x):
-    x = [i for i in range(x)]
-    return -1
-
-
-def func2():
-    # error function
-    return "a" / 2
-
-
-def func3(*args, **kwagrs):
-    # args and kwargs function
-    return list(args) + list(kwagrs.values())
+from .func import memory, error, return_check
 
 
 class MemoryitTest(TestCase):
-    def setUp(self):
-        self.er_func = memoryit(func2)
-        self.func = memoryit(func1)
-        self.ka_func = memoryit(func3)
 
-    def test_memoryit_1(self):
-        self.assertIsInstance(self.func(5), int)
+    def test_memoryit1(self):
+        v = memoryit(memory, args=(5,))
+        self.assertIsInstance(v, int)
 
-    def test_memoryit_2(self):
-        self.assertRaises(Exception, self.er_func)
+    def test_memoryit2(self):
+        self.assertRaises(Exception, memoryit, error, 5)
 
 
 class LimitMemoryTest(TestCase):
-    def setUp(self):
-        self.er_func = limit_memory()(func2)
-        self.func = limit_memory()(func1)
-        self.ka_func = limit_memory()(func3)
 
     def test_limit_memory_1(self):
-        self.assertEqual(self.func(3), -1)
+        v = limit_memory(memory, args=(10,))
+        self.assertEqual(v, -1)
 
     def test_limit_memory_2(self):
-        self.assertRaises(Exception, self.er_func)
+        self.assertRaises(
+            Exception,
+            limit_memory,
+            error,
+            args=(100_000_000,)
+        )
 
     def test_limit_memory_3(self):
-        self.assertRaises(MemoryError, self.func, 100_000_000)
+        self.assertRaises(
+            MemoryError,
+            limit_memory,
+            memory,
+            args=(500_000_000,)
+        )
 
     def test_limit_memory_4(self):
-        self.assertEqual(self.ka_func(
-            1, 2, 3, four=4, five=5), [1, 2, 3, 4, 5])
+        v = limit_memory(
+            return_check,
+            args=(1, 2, 3),
+            kwargs={"four": 4, "five": 5}
+        )
+
+        self.assertEqual(v, [1, 2, 3, 4, 5])
diff --git a/tests/test_time.py b/tests/test_time.py
index ce5ec03..78bbd2c 100644
--- a/tests/test_time.py
+++ b/tests/test_time.py
@@ -1,52 +1,49 @@
 from unittest import TestCase
 from pyutility import limit_time, timeit
 
-
-def func1(x):
-    # recursive function to find xth fibonacci number
-    if x < 3:
-        return 1
-    return func1(x-1) + func1(x-2)
-
-
-def func2():
-    # error function
-    return "a" / 2
-
-
-def func3(*args, **kwagrs):
-    # args and kwargs function
-    return list(args) + list(kwagrs.values())
+from .func import time, error, return_check
 
 
 class TimeitTest(TestCase):
-    def setUp(self):
-        self.er_func = timeit(func2)
-        self.func = timeit(func1)
-        self.ka_func = timeit(func3)
 
     def test_timeit1(self):
-        self.assertIsInstance(self.func(5), float)
+        v = timeit(time, args=(5,))
+        self.assertIsInstance(v, float)
 
     def test_timeit2(self):
-        self.assertRaises(Exception, self.er_func)
+        self.assertRaises(Exception, timeit, error, 5)
 
 
 class LimitTimeTest(TestCase):
-    def setUp(self):
-        self.er_func = limit_time(2)(func2)
-        self.func = limit_time(2)(func1)
-        self.ka_func = limit_time(2)(func3)
 
     def test_limit_time_1(self):
-        self.assertEqual(self.func(10), 55)
+        v = limit_time(time, time=2, args=(10,))
+        self.assertEqual(v, 55)
 
     def test_limit_time_2(self):
-        self.assertRaises(Exception, self.er_func)
+        self.assertRaises(
+            Exception,
+            limit_time,
+            error,
+            time=2,
+            args=(2,)
+        )
 
     def test_limit_time_3(self):
-        self.assertRaises(TimeoutError, self.func, 50)
+        self.assertRaises(
+            TimeoutError,
+            limit_time,
+            time,
+            time=2,
+            args=(50,)
+        )
 
     def test_limit_time_4(self):
-        self.assertEqual(self.ka_func(
-            1, 2, 3, four=4, five=5), [1, 2, 3, 4, 5])
+        v = limit_time(
+            return_check,
+            time=2,
+            args=(1, 2, 3),
+            kwargs={"four": 4, "five": 5}
+        )
+
+        self.assertEqual(v, [1, 2, 3, 4, 5])
diff --git a/tests/test_utility.py b/tests/test_utility.py
index 603316a..266cdfa 100644
--- a/tests/test_utility.py
+++ b/tests/test_utility.py
@@ -1,73 +1,65 @@
 from unittest import TestCase
 from pyutility import limit_resource, measureit
 
-
-def func1a(x):
-    x = [i for i in range(x)]
-    return -1
-
-
-def func1b(x):
-    # recursive function to find xth fibonacci number
-    if x < 3:
-        return 1
-    return func1b(x-1) + func1b(x-2)
-
-
-def func2():
-    # error function
-    return "a" / 2
-
-
-def func3(*args, **kwagrs):
-    # args and kwargs function
-    return list(args) + list(kwagrs.values())
+from .func import memory, time, return_check, error
 
 
 class MeasureitTest(TestCase):
-    def setUp(self):
-        self.er_func = measureit(func2)
-        self.func_m = measureit(func1a)
-        self.func_t = measureit(func1b)
-        self.ka_func = measureit(func3)
 
     def test_measureit_1(self):
-        self.assertIsInstance(self.func_m(100), tuple)
+        v = measureit(memory, args=(100,))
+        self.assertIsInstance(v, tuple)
 
     def test_measureit_2(self):
-        x = self.func_t(10)
-        self.assertIsInstance(x[0], int)
-        self.assertIsInstance(x[1], float)
+        v = measureit(time, args=(15,))
+        self.assertIsInstance(v[0], int)
+        self.assertIsInstance(v[1], float)
 
     def test_measureit_3(self):
-        self.assertIsInstance(self.func_t(15), tuple)
+        v = measureit(time, args=(15,))
+        self.assertIsInstance(v, tuple)
 
     def test_measureit_4(self):
-        self.assertRaises(Exception, self.er_func)
+        self.assertRaises(Exception, measureit, error, 100)
 
 
 class LimitResourceTest(TestCase):
-    def setUp(self):
-        self.er_func = limit_resource(time=2)(func2)
-        self.func_m = limit_resource(time=2)(func1a)
-        self.func_t = limit_resource(time=2)(func1b)
-        self.ka_func = limit_resource(time=2)(func3)
 
     def test_limit_resource_1(self):
-        self.assertEqual(self.func_m(300), -1)
+        v = limit_resource(memory, time=2, args=(300,))
+        self.assertEqual(v, -1)
 
     def test_limit_resource_2(self):
-        self.assertEqual(self.func_t(3), 2)
+        v = limit_resource(time, time=2, args=(3,))
+        self.assertEqual(v, 2)
 
     def test_limit_resource_3(self):
-        self.assertRaises(Exception, self.er_func)
+        self.assertRaises(Exception, error)
 
     def test_limit_resource_4(self):
-        self.assertRaises(MemoryError, self.func_m, 100_000_000)
+        self.assertRaises(
+            MemoryError,
+            limit_resource,
+            memory,
+            args=(100_000_000,),
+            time=2
+        )
 
     def test_limit_resource_5(self):
-        self.assertRaises(TimeoutError, self.func_t, 50)
+        self.assertRaises(
+            TimeoutError,
+            limit_resource,
+            time,
+            args=(50,),
+            time=2
+        )
 
     def test_limit_resource_6(self):
-        self.assertEqual(self.ka_func(
-            1, 2, 3, four=4, five=5), [1, 2, 3, 4, 5])
+        v = limit_resource(
+            return_check,
+            time=2,
+            args=(1, 2, 3),
+            kwargs={"four": 4, "five": 5}
+        )
+
+        self.assertEqual(v, [1, 2, 3, 4, 5])