easypy – an unknown library for dealing with boiler-plate in python
If you are doing autotests in python, often work with multithreading and want to reduce the amount of boiler-plate code, it makes sense to look at the library easypy.
In the next test framework on pytest, I came across a convenient way to work with multithreading, at first I thought that this was my bike, but it turned out that it was an external dependency. There are few stars on the github, there is almost no documentation either, but I liked the approach to working with multithreading and a couple of other interesting things.
The library itself is a set of loosely coupled modules that provide convenient wrappers for a variety of boiler-plate code. Well suited for tasks that arise in the process of writing autotests. No obvious bugs could be found in the library – everything works stably. However, I’m not sure if it’s a good idea to use product code in production – monkey-patching is used everywhere, there are not many unit tests, there is little documentation, there is a chance to shoot something for yourself.
Let’s go through the parts of the library, starting with those that are used most often in autotests.
easypy.concurrency
The most useful module, because of it, I, in fact, decided to share this library, at least, a similar approach can be implemented on my own. Class MultiObject
allows you to do this:
from time import sleep
from easypy.concurrency import MultiObject
def load_test_via_ip(ip: str):
print(f'start load test via {ip}')
sleep(2)
print(f'end load test via {ip}')
ips = ['172.17.10.1', '172.17.10.2', '172.17.10.3']
MultiObject(ips).call(lambda ip: load_test_via_ip(ip))
The output will be like this:
start load test via 172.17.10.1
start load test via 172.17.10.2
start load test via 172.17.10.3
end load test via 172.17.10.1
end load test via 172.17.10.2
end load test via 172.17.10.3
A collection of elements is passed as an argument to the constructor. Using the method .call()
specifies which method to call for each of these elements. Each call will be made in a separate thread. Using the parameter workers
you can specify the number of threads. By default, the number of threads is equal to the number of elements.
In addition, you can directly call the methods of the passed objects (objects must have the same interface):
from time import sleep
from easypy.concurrency import concurrent
class Server:
def __init__(self, server_name: str):
self.server_name = server_name
def send(self, msg: str):
print(f'Sending "{msg}" to "{self.server_name}"')
sleep(2)
print(f'Sent "{msg}" to "{self.server_name}"')
servers = [Server('1'), Server('2'), Server('3')]
responses = MultiObject(servers, workers=1).send('Hello')
Conclusion:
Sending "Hello" to "1"
Sent "Hello" to "1"
Sending "Hello" to "2"
Sent "Hello" to "2"
Sending "Hello" to "3"
Sent "Hello" to "3"
Method send()
is an instance method of the class Server
. The library itself calls this method for each instance. Each call occurs in a separate thread, you can adjust the number using the parameter workers
.
For comparison, pure threading code for the same situation would look something like this:
from threading import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [
executor.submit(server.send, args=['Hello'])
for server in servers
]
responses = [future.result() for future in futures]
Method concurrent()
The following useful method can be used as a context manager:
def _log():
print(f'Logging smth {time.time()}')
with concurrent(_log, loop=True, sleep=1 * SECOND):
for _ in range(3):
sleep(2)
print('Some work is being done')
Conclusion:
Logging smth 1688165534.384237
Logging smth 1688165535.389322
Some work is being done
Logging smth 1688165536.39416
Logging smth 1688165537.399373
Some work is being done
Logging smth 1688165538.403112
Logging smth 1688165539.4064589
Some work is being done
It can also be used directly – to run a task in the background. If you do not specify the loop and sleep parameters, the method inside will be called once:
concurrent(send_heartbeat, loop=True, sleep=1).start()
Both options – and the method concurrent
and class MultiObject
it is very convenient to use when the system under test consists of several nodes and you need to do something with these nodes at the same time. It can also be used for failover tests, when something goes wrong with the system during some test scenario – for example, some random node in the system is constantly rebooting, and we are working with this system in parallel.
easypy.collections
The ListCollection class is an add-on to the standard list, adding several convenient methods and support for the MultiObject class:
from easypy.collections import ListCollection
class Server:
def __init__(self, server_name: str, server_number: int):
self.server_name = server_name
self.server_number = server_number
def __str__(self):
return f'Sever: "{self.server_name}"; number: {self.server_number}'
lc = ListCollection(
[
Server('nice_server_name_1', 1),
Server('nice_server_name_2', 2),
Server('nice_server_name_3', 3)
]
)
print(lc.choose())
print(lc.choose(server_number=2))
print(lc.select(lambda s: s.server_number > 1))
choose()
allows you to select one element from the collection, specifying either a predicate or a filter (the value of some field of the object). select()
selects a subset of elements from the collection. There are also a number of methods like shuffled()
, sorted()
, filtered()
, without()
which are responsible for simple, but convenient, operations on the collection.
Through the M parameter, you can create an object MultiObject
and immediately call the method on the entire collection send()
, which will be executed in different threads. The result is the following construction:
lc.select(lambda s: s.server_number > 1).M.send('Hello')
Again – very handy when working with a multi-node system – nodes can be stored in a ListCollection and perform operations on different threads.
easypy.units
A small module with constants for measuring time and information. Eliminates the need to write your own, for example –
>> from easypy.units import MiB, MB, GiB, GB, MINUTE, DAY
>> print(MiB/1000)
1048.576
>> print(MiB * 1000)
1000MiB
>> print(MiB * 1024)
GiB
Mathematical operations are supported – it is convenient to operate.
easypy.resilience
Provides several decorators that are useful when dealing with unstable systems:
from easypy.resilience import resilient
@resilient(default=0, acceptable=AssertionError)
def get_number():
print('Going to raise AssertionError')
raise AssertionError
print(get_number())
Allows you to suppress the error and return a value from the default method, even in the event of an error. Through arguments acceptable
And unacceptable
You can control which errors to pay attention to and which ones not.
The next decorator is @retrying
repeats a method call, you can specify either the number of repetitions, or the time during which these repetitions will be made and the time between repetitions. Plus, you can also specify which errors to skip and which not.
from easypy.resilience import retrying
@retrying(10 * SECOND, acceptable=AssertionError, sleep=1)
def false_assertion():
print('Going to raise AssertionError')
raise AssertionError
false_assertion()
AND retrying
And resilient
they help well with the already mentioned failover tests, when you need to interrogate the system while it is not feeling well.
wait
Where without the implementation of wait:
from time import sleep
from easypy.sync import wait
def something_went_wrong():
sleep(1)
wait(5 * SECOND, something_went_wrong, sleep=1, message=f"Something went wrong")
wait
waits for the specified predicate to return something other than False
or None
. If no other value is returned during the timeout, it falls by timeout with the text specified in the message parameter.
easypy.random
Two handy methods that are used to generate arbitrary names and text:
>>> from easypy.random import random_nice_name
>>> random_nice_name()
'fastidious-vulture'
>>> random_nice_name()
'balanced-skate'
>>> from easypy.random import random_string
>>> import string
>>> random_string(length=10, charset=string.ascii_lowercase)
'ejwzbrgvtd'
>>> random_string(length=10, charset=string.ascii_lowercase)
'zecalshoji'
Nuances
Installation – pip install real-easypy
To use it, you need to import the logging library module – import easypy.logging
otherwise it will swear by errors when using any method from the library.
Another link to the library – https://github.com/real-easypy/easypy