diff --git a/jutl/__init__.py b/jutl/__init__.py index 0104410..5f87658 100644 --- a/jutl/__init__.py +++ b/jutl/__init__.py @@ -1,5 +1,5 @@ # Dunder attributes -__version__ = "0.5.2" +__version__ = "0.5.3" __author__ = "Jordan Welsman" __license__ = "MIT" __copyright__ = "Copyright 2023 Jordan Welsman" diff --git a/jutl/cryptography/__init__.py b/jutl/cryptography/__init__.py index e69de29..d875201 100644 --- a/jutl/cryptography/__init__.py +++ b/jutl/cryptography/__init__.py @@ -0,0 +1,10 @@ +# Import utils.py so module +# functions are usable at +# 'from jutl import cryptography' level. +from .caesarcipher import * +from .rot13cipher import * +from .substitutioncipher import * + +# Only show functions specified in +# submodule files to the outside world. +__all__ = caesarcipher.__all__, rot13cipher.__all__, substitutioncipher.__all__ \ No newline at end of file diff --git a/jutl/cryptography/caesarcipher.py b/jutl/cryptography/caesarcipher.py new file mode 100644 index 0000000..5c01c18 --- /dev/null +++ b/jutl/cryptography/caesarcipher.py @@ -0,0 +1,4 @@ +# Module imports + +# External class visibility +__all__ = [] \ No newline at end of file diff --git a/jutl/cryptography/rot13cipher.py b/jutl/cryptography/rot13cipher.py new file mode 100644 index 0000000..5c01c18 --- /dev/null +++ b/jutl/cryptography/rot13cipher.py @@ -0,0 +1,4 @@ +# Module imports + +# External class visibility +__all__ = [] \ No newline at end of file diff --git a/jutl/cryptography/substitutioncipher.py b/jutl/cryptography/substitutioncipher.py new file mode 100644 index 0000000..1d3a03d --- /dev/null +++ b/jutl/cryptography/substitutioncipher.py @@ -0,0 +1,63 @@ +# Module imports + +# External class visibility +__all__ = ['SubstitutionCipher'] + + +class SubstitutionCipher(): + """ + Class which acts as the + base Cipher implementation. + """ + def __init__(self, rotation: int = 0): + "Initialization method." + self.rotation = self._check_rotation(rotation) + + def __repr__(self) -> str: + """ + Tells the interpreter how + to represent this class. + """ + if self.rotation != 0: + print("s") + + + def _check_rotation(self, rotation: int) -> int: + """ + Checks if rotation is valid. + """ + allowed_range = range(0, 27) + + if rotation in allowed_range: + return rotation + else: + return 0 + + + def set_rotation(self, rotation: int = 0): + """ + Sets the rotation + after initialization. + """ + self.rotation = self._check_rotation(rotation) + + + def _encrypt(self, message: str): + """ + Encrypts the passed message + using the defined rotation. + """ + __alpha_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + __alpha_lower = "abcdefghijklmnopqrstuvwxyz" + encrypted_message: str = "" + + for letter in message: + if letter in __alpha_upper: + encrypted_message += __alpha_upper[(__alpha_upper.find(letter) + self.rotation) % 26] + elif letter in __alpha_lower: + encrypted_message += __alpha_lower[(__alpha_lower.find(letter) + self.rotation) % 26] + else: + encrypted_message += letter + + return encrypted_message + diff --git a/jutl/timers/__init__.py b/jutl/timers/__init__.py index 86b18f2..74f85f3 100644 --- a/jutl/timers/__init__.py +++ b/jutl/timers/__init__.py @@ -1,9 +1,10 @@ # Import submodule files so # classes and functions are usable at # 'from jutl.timers import _' level. +from .reporter import * from .stopwatch import * from .timer import * # Only show functions specified in # submodule files to the outside world. -__all__ = stopwatch.__all__, timer.__all__ \ No newline at end of file +__all__ = reporter.__all__, stopwatch.__all__, timer.__all__ \ No newline at end of file diff --git a/jutl/timers/reporter.py b/jutl/timers/reporter.py new file mode 100644 index 0000000..6efac4d --- /dev/null +++ b/jutl/timers/reporter.py @@ -0,0 +1,196 @@ +# Module imports +from jutl.formatting import apply +from time import time + +# External class visibility +__all__ = ['Reporter'] + + +class Reporter(): + """ + Class which acts as a timer to + record loop times for comparison. + """ + def __init__(self, name: str = None, declare_times: bool = False): + "Initialization method." + self.name: str = name + self._start_time: float + self._stop_time: float + self.total_time: float = None + self._loops: list[float] = [] + self.loop_times: list[float] = [] + self._declare_times = declare_times + + def __repr__(self) -> str: + """ + Tells the interpreter how + to represent this class. + """ + if self.name is None: + if self.total_time is None: + return "Reporter()" + else: + return f"Reporter({round(self.total_time, 2)}s)" + else: + if self.total_time is None: + return f"Reporter({self.name})" + else: + return f"Reporter({self.name}, {round(self.total_time, 2)}s)" + + def __call__(self, color: str = None): + """ + Tells the interpreter what to + do when an object of this + class is called directly. + """ + if self.loop_times: + for n, time in enumerate(self.loop_times): + if color: + print(apply(text=f"Loop {n+1}: {round(time, 2)}s", text_color=color)) + else: + print(f"Loop {n+1}: {round(time, 2)}s") + else: + print("There are no loop times.") + + def __len__(self) -> int: + """ + Tells the interpreter what to + consider this class' length. + """ + return len(self._loops) + + def __iter__(self) -> iter: + """ + Tells the interpreter what to + iterate over thwn iterator methods + are called on this class. + """ + return iter(self.loop_times) + + def __eq__(self, other) -> bool: + """ + Tells the interpreter how this class + handles equal operators. + """ + return self.total_time == other.total_time + + def __ne__(self, other) -> bool: + """ + Tells the interpreter how this class + handles not equal operators. + """ + return self.total_time != other.total_time + + def __gt__(self, other) -> bool: + """ + Tells the interpreter how this class + handles greater than operators. + """ + return self.total_time > other.total_time + + def __ge__(self, other) -> bool: + """ + Tells the interpreter how this class + handles greater or equal operators. + """ + return self.total_time >= other.total_time + + def __lt__(self, other) -> bool: + """ + Tells the interpreter how this class + handles less than operators. + """ + return self.total_time < other.total_time + + def __le__(self, other) -> bool: + """ + Tells the interpreter how this class + handles less than or equal operators. + """ + return self.total_time <= other.total_time + + def __add__(self, other) -> float: + """ + Tells the interpreter how to sum these objects. + """ + return self.total_time + other.total_time + + def __sub__(self, other) -> float: + """ + Tells the interpreter how to subtract these objects. + """ + return self.total_time - other.total_time + + def __mul__(self, multiplier) -> float: + """ + Tells the interpreter how to subtract these objects. + """ + return self.total_time * multiplier + + def __truediv__(self, other) -> float: + """ + Tells the interpreter how to subtract these objects. + """ + return self.total_time / other.total_time + + + def start(self, message: str = None, color : str = None): + """ + Starts the reporter and + optionall prints a message. + """ + self._start_time = time() + if message: + print(apply(text=message, text_color=color) if color else print(message)) + + + def loop(self, loop_time: float = None, message: str = None, color: str = None): + """ + Adds the current time to the loop + time list, records the time since + the start or time of the last recorded + loop, and optionally prints a message. + """ + if loop_time: + self._loops.append(loop_time) + else: + self._loops.append(time()) + if not self.loop_times: + self.loop_times.append(self._calculate_time(self._start_time, self._loops[-1])) + else: + self.loop_times.append(self._calculate_time(self._loops[-2], self._loops[-1])) + if message: + print(apply(text=message, text_color=color) if color else print(message)) + + def stop(self, message: str = None, color: str = None): + """ + Stops the reporter, calculates + the total time passed, and + optionally prints a message. + """ + self._stop_time = time() + self.total_time = self._calculate_time(self._start_time, self._stop_time) + self.loop(self._stop_time) # add final loop time and calculate loop difference + if message: + print(apply(text=message, text_color=color) if color else print(text=message)) + + + def _calculate_time(self, time1: float, time2: float) -> float: + """ + Returns the difference in time where t2>t1. + """ + return time2 - time1 + + + def reset(self, message: str = None, color: str = None): + """ + Resets all reporter attributes + and optionally prints a message. + """ + self._start_time = None + self._stop_time = None + self.total_time = None + self._loops.clear() + self.loop_times.clear() + if message: + print(apply(text=message, text_color=color) if color else print(text=message)) diff --git a/jutl/timers/timer.py b/jutl/timers/timer.py index 963c930..6b21312 100644 --- a/jutl/timers/timer.py +++ b/jutl/timers/timer.py @@ -36,7 +36,7 @@ def __repr__(self) -> str: else: return f"Timer({self.name}, {round(self.total_time, 2)}s)" - def __call__(self, color: str = None): + def __call__(self, precision: int = 2, color: str = None): """ Tells the interpreter what to do when an object of this @@ -44,9 +44,9 @@ class is called directly. """ if self.total_time: if color: - print(apply(text=f"{self.name} total time: {round(self.total_time, 2)}s", text_color=color)) + print(apply(text=f"{self.name} total time: {round(self.total_time, precision)}s", text_color=color)) else: - print(f"{self.name} total time: {round(self.total_time, 2)}s") + print(f"{self.name} total time: {round(self.total_time, precision)}s") else: print("There is no recorded time.") diff --git a/setup.py b/setup.py index 94281d7..38166f5 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ # Arguments git_name = "jutils" pypi_name = "jutl" -version = "0.5.2" # update __init__.py +version = "0.5.3" # update __init__.py python_version = ">=3.10" # Long description from README.md diff --git a/test/timers/test_reporter.py b/test/timers/test_reporter.py new file mode 100644 index 0000000..147018d --- /dev/null +++ b/test/timers/test_reporter.py @@ -0,0 +1,271 @@ +# jutils/test/timers/test_reporter.py +from jutl.timers import Reporter +from time import sleep + +test_name: str = "Test name" + +class TestInit(): + def test_def(self): + "Tests if an object can be created from the Reporter class." + reporter = Reporter() + assert isinstance(reporter, Reporter) + del(reporter) + + def test_name(self): + "Tests if naming a reporter works." + reporter = Reporter() + assert reporter.name is None + del(reporter) + + reporter = Reporter(test_name) + assert reporter.name == test_name + del(reporter) + + +class TestDunder(): + def test_repr(self): + "Tests what is output for representation." + reporter = Reporter() + assert repr(reporter) == f"Reporter()" + del(reporter) + + reporter = Reporter() + reporter.start() + reporter.stop() + assert repr(reporter) == f"Reporter({round(reporter.total_time, 2)}s)" + del(reporter) + + reporter = Reporter(test_name) + assert repr(reporter) == f"Reporter({test_name})" + del(reporter) + + reporter = Reporter(test_name) + reporter.start() + reporter.stop() + assert repr(reporter) == f"Reporter({test_name}, {round(reporter.total_time, 2)}s)" + del(reporter) + + def test_len(self): + "Tests what is output for object length." + reporter = Reporter() + reporter.start() + reporter.stop() + assert len(reporter) == 1 + del(reporter) + + reporter = Reporter() + reporter.start() + reporter.loop() + reporter.stop() + assert len(reporter) == 2 + del(reporter) + + reporter = Reporter() + reporter.start() + reporter.loop() + reporter.loop() + reporter.stop() + assert len(reporter) == 3 + del(reporter) + + def test_iter(self): + "Tests object's iteration reporting." + reporter = Reporter() + reporter.start() + reporter.stop() + reporter_iter = iter(reporter) + assert next(reporter_iter) == reporter.total_time + del(reporter) + del(reporter_iter) + + reporter = Reporter() + reporter.start() + reporter.loop() + reporter.loop() + reporter.loop() + reporter.stop() + reporter_iter = iter(reporter) + assert next(reporter_iter) == reporter.loop_times[0] + assert next(reporter_iter) == reporter.loop_times[1] + assert next(reporter_iter) == reporter.loop_times[2] + assert next(reporter_iter) == reporter.loop_times[3] + del(reporter) + del(reporter_iter) + + def test_equal(self): + "Tests the overridden equal function." + reporter1 = Reporter() + reporter1.start() + reporter1.stop() + reporter2 = reporter1 + assert reporter1 == reporter2 + del(reporter1) + del(reporter2) + + def test_not_equal(self): + "Tests the overridden not equal function." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.01) + reporter1.stop() + reporter2.start() + reporter2.stop() + assert reporter1 != reporter2 + del(reporter1) + del(reporter2) + + def test_greater_than(self): + "Tests the overridden greater than function." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.01) + reporter1.stop() + reporter2.start() + reporter2.stop() + assert reporter1 > reporter2 + del(reporter1) + del(reporter2) + + def test_greater_equal(self): + "Tests the overridden greater or equal function." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.01) + reporter1.stop() + reporter2.start() + reporter2.stop() + assert reporter1 > reporter2 + del(reporter1) + del(reporter2) + + reporter1 = Reporter() + reporter1.start() + reporter1.stop() + reporter2 = reporter1 + assert reporter1 == reporter2 + del(reporter1) + del(reporter2) + + def test_less_than(self): + "Tests the overridden less than function." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.01) + reporter1.stop() + reporter2.start() + reporter2.stop() + assert reporter2 < reporter1 + del(reporter1) + del(reporter2) + + def test_less_equal(self): + "Tests the overridden less or equal function." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.01) + reporter1.stop() + reporter2.start() + reporter2.stop() + assert reporter2 < reporter1 + del(reporter1) + del(reporter2) + + reporter1 = Reporter() + reporter1.start() + reporter1.stop() + reporter2 = reporter1 + assert reporter1 == reporter2 + del(reporter1) + del(reporter2) + + def test_add(self): + "Tests the sum operator." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.01) + reporter1.stop() + reporter2.start() + reporter2.stop() + assert reporter1 + reporter2 == reporter1.total_time + reporter2.total_time + del(reporter1) + del(reporter2) + + def test_sub(self): + "Tests the subtract operator." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.01) + reporter1.stop() + reporter2.start() + reporter2.stop() + assert reporter1 - reporter2 == reporter1.total_time - reporter2.total_time + del(reporter1) + del(reporter2) + + def test_mul(self): + "Tests the multiply operator." + reporter = Reporter() + reporter.start() + sleep(0.01) + reporter.stop() + assert reporter * 2 == reporter.total_time * 2 + del(reporter) + + def test_truediv(self): + "Tests the divide operator." + reporter1 = Reporter() + reporter2 = Reporter() + reporter1.start() + sleep(0.02) + reporter1.stop() + reporter2.start() + sleep(0.01) + reporter2.stop() + assert reporter1 / reporter2 == reporter1.total_time / reporter2.total_time + del(reporter1) + del(reporter2) + + +class TestRobustness(): + def test_equal_list_sizes(self): + """ + Checks recorded loop time and loop + difference lists are equally sized. + """ + reporter = Reporter() + assert len(reporter._loops) == len(reporter.loop_times) + del(reporter) + + reporter = Reporter() + reporter.start() + reporter.stop() + assert len(reporter._loops) == len(reporter.loop_times) + del(reporter) + + reporter = Reporter() + reporter.start() + reporter.loop() + reporter.loop() + reporter.loop() + reporter.stop() + assert len(reporter._loops) == len(reporter.loop_times) + del(reporter) + + def test_stop_accuracy(self): + """ + Checks accuracy of + calculate_time() method. + """ + reporter = Reporter() + reporter.start() + sleep(0.1) + reporter.stop() + assert 0.100 <= reporter.total_time <= 0.110 # within 10ms tolerance + del(reporter)