Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Classicist Library Change Log

## [1.0.4] - 2026-02-03
### Added
- Added support for the new `@runtimer` decorator which can be used to gather run times
for functions, class methods and instance methods.

### Updated
- Improved top-level import availability for the `AliasError` and `AnnotationError` classes.

## [1.0.3] - 2026-01-30
### Updated
- Improved logging for aliasing functionality.
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The Classicist library provides several useful decorators and helper methods inc
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
* `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
* `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
* `@runtimer` – a decorator that can be used to time function and method calls;
* `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases.

Expand Down Expand Up @@ -449,6 +450,48 @@ class Test(object):
pass
```

#### Runtimer: Function & Method Call Timing

The `@runtimer` decorator can be used to obtain run times for function and method calls,
including the start and stop `datetime`, the `timedelta` and the duration in seconds.

To collect timing information simply import the `runtimer` decorator from the library,
and apply it to the function, class method or instance method that you wish to time, and
after the call has been made, you can obtain the run time information from the function
or method via the `classicist` library's `runtime` helper method, which provides access
to an instance of the library's `Runtimer` class which is used to track the run time:

```python
from classicist import runtimer, runtime, Runtimer
from datetime import datetime
from time import sleep

@runtimer
def function_to_time(value: int) -> int:
sleep(0.01)
return value * 100

# Obtain a reference to the function's Runtimer (created by the @runtimer decorator)
# This reference can be obtained before or after a call to the decorated function
runtimer: Runtimer = runtime(function_to_time)
assert isinstance(runtimer, Runtimer)

# Obtain the time before the function call for illustrative purposes (not needed in use)
started: datetime = datetime.now()

# Call the method to perform its work, and its runtime will be gathered
assert function_to_time(2) == 200

# Obtain the time after the function call for illustrative purposes (not needed in use)
stopped: datetime = datetime.now()

# Use the gathered runtime information as needed
assert runtimer.started > started
assert runtimer.duration >= 0.01
assert runtimer.timedelta.total_seconds() >= 0.01
assert runtimer.stopped < stopped
```

#### ShadowProof: Attribute Shadowing Protection Metaclass

The `shadowproof` metaclass can be used to protect classes and subclasses from attribute
Expand Down
34 changes: 31 additions & 3 deletions source/classicist/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
# Decorator Classes
# Decorators
from classicist.decorators import (
# @alias decorator
alias,
annotate,
# @annotation decorator
annotation,
annotations,
# @classproperty decorator
classproperty,
# @deprecated decorator
deprecated,
# @hybridmethod decorator
hybridmethod,
# @nocache decorator
nocache,
# @runtimer decorator
runtimer,
)

# Decorator Helper Methods
from classicist.decorators import (
# @alias decorator helper methods
is_aliased,
aliases,
# @annotation decorator helper methods
annotate,
annotations,
# @deprecated decorator helper methods
is_deprecated,
# @runtimer decorator helper methods
runtime,
has_runtimer,
)

# Decorator Related Classes
from classicist.decorators import (
Runtimer,
)

# Meta Classes
Expand All @@ -25,6 +44,8 @@

# Exception Classes
from classicist.exceptions import (
AliasError,
AnnotationError,
AttributeShadowingError,
)

Expand All @@ -38,13 +59,20 @@
"deprecated",
"hybridmethod",
"nocache",
"runtimer",
# Decorator Helper Methods
"is_aliased",
"aliases",
"is_deprecated",
"runtime",
"has_runtimer",
# Decorator Related Classes
"Runtimer",
# Meta Classes
"aliased",
"shadowproof",
# Exception Classes
"AliasError",
"AnnotationError",
"AttributeShadowingError",
]
5 changes: 5 additions & 0 deletions source/classicist/decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from classicist.decorators.deprecated import deprecated, is_deprecated
from classicist.decorators.hybridmethod import hybridmethod
from classicist.decorators.nocache import nocache
from classicist.decorators.runtimer import Runtimer, runtimer, runtime, has_runtimer

__all__ = [
"alias",
Expand All @@ -17,4 +18,8 @@
"is_deprecated",
"hybridmethod",
"nocache",
"Runtimer",
"runtimer",
"runtime",
"has_runtimer",
]
165 changes: 165 additions & 0 deletions source/classicist/decorators/runtimer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from __future__ import annotations

from datetime import datetime, timedelta
from functools import wraps
from inspect import unwrap

from classicist.logging import logger

logger = logger.getChild(__name__)


class Runtimer(object):
"""The Runtimer class times and tracks the runtime of function calls."""

_funcobj: callable = None
_started: datetime = None
_stopped: datetime = None

def __init__(self, function: callable):
"""Supports instantiating an instance of the Runtimer class."""

if not callable(function):
raise TypeError("The 'function' argument must reference a callable!")

self._funcobj = function

def __str__(self) -> str:
"""Returns a string representation of the current Runtimer instance."""

return f"<{self.__class__.__name__}(started: {self.started}, stopped: {self.stopped}, duration: {self.duration})>"

def __repr__(self) -> str:
"""Returns a debug string representation of the current Runtimer instance."""

return f"<{self.__class__.__name__}(started: {self.started}, stopped: {self.stopped}, duration: {self.duration}) @ {hex(id(self))}>"

def reset(self) -> Runtimer:
"""Supports resetting the Runtimer timing information."""

self._started = None
self._stopped = None

return self

def start(self) -> Runtimer:
"""Supports starting the Runtimer timer by recording the current datetime."""

self._started = datetime.now()
self._stopped = None

return self

def stop(self) -> Runtimer:
"""Supports stopping the Runtimer timer by recording the current datetime."""

if self._started is None:
self._started = datetime.now()
self._stopped = datetime.now()

return self

@property
def function(self) -> callable:
"""Supports returning the Runtimer instance's associated function/method."""

return self._funcobj

@property
def started(self) -> datetime:
"""Supports returning the started datetime or the current time as a fallback."""

return self._started or datetime.now()

@property
def stopped(self) -> datetime:
"""Supports returning the stopped datetime or the current time as a fallback."""

return self._stopped or datetime.now()

@property
def timedelta(self) -> timedelta:
"""Supports returning the timedelta for the decorated function's call time."""

if isinstance(self._started, datetime) and isinstance(self._stopped, datetime):
return self._stopped - self._started
else:
return timedelta(0)

@property
def duration(self) -> float:
"""Supports returning the duration of the decorated function's call time."""

return self.timedelta.total_seconds()


def runtimer(function: callable) -> callable:
"""The runtimer decorator method creates an instance of the Runtimer class for the
specified function, allowing calls to the function to be timed."""

if not callable(function):
raise TypeError("The 'function' argument must reference a callable!")

logger.debug("runtimer(function: %s)", function)

# If the function already has an associated Runtimer instance, reset it
if isinstance(
_runtimer := getattr(function, "_classicist_runtimer", None), Runtimer
):
_runtimer.reset()
else:
# Otherwise, create a new instance and associate it with the function
_runtimer = function._classicist_runtimer = Runtimer(function)

@wraps(function)
def wrapper(*args, **kwargs):
logger.debug(
"runtimer(function: %s).wrapper(args: %s, kwargs: %s)",
function,
args,
kwargs,
)

_runtimer.start()
result = function(*args, **kwargs)
_runtimer.stop()

return result

return wrapper


def runtime(function: callable) -> Runtimer | None:
"""The runtime helper method can be used to obtain the Runtimer instance for the
specified function, if one is present, allowing access to the most recent function
call start and stop time stamps and call duration."""

if not callable(function):
raise TypeError("The 'function' argument must reference a callable!")

function = unwrap(function)

logger.debug("runtime(function: %s)" % (function))

if isinstance(
_runtimer := getattr(function, "_classicist_runtimer", None), Runtimer
):
return _runtimer


def has_runtimer(function: callable) -> bool:
"""The has_runtimer helper method can be used to determine if the specified function
has an associated Runtimer instance or not, returning a boolean to indicate this."""

if isinstance(getattr(function, "_classicist_runtimer", None), Runtimer):
return True
else:
return False


__all__ = [
"Runtimer",
"runtimer",
"runtime",
"hasruntimer",
]
2 changes: 2 additions & 0 deletions source/classicist/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from classicist.exceptions.decorators import (
AliasError,
AnnotationError,
)

Expand All @@ -7,6 +8,7 @@
)

__all__ = [
"AliasError",
"AnnotationError",
"AttributeShadowingError",
]
2 changes: 1 addition & 1 deletion source/classicist/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.3
1.0.4
Loading