Skip to content

OwenPendrighElliott/contemplation

Repository files navigation

🤔 Contemplation

test

Novel and powerful introspection utilities for Python.

Contemplation is a library for introspection of Python code. It provides a number of introspection utilities that are not available in the standard library. This library is designed to be used for debugging and analysing code.

pip install contemplation

Table of Contents

Documentation

Find the documentation here.

Introspections

There are three categories of introspection provided in this package:

  • Execution Introspections
  • Instance Introspections
  • Type Introspections

Execution introspections are introspections that are performed on the execution of code.

Instance introspections are introspections that are performed on instances of objects.

Type introspections are introspections that are performed on types.

These are detailed below.

Execution Introspections

The following execution introspections are provided:

  • CallCounter - counts the number of times functions registered to it are called
  • ExecutionTimer - times the execution of functions registered to it
  • FunctionLogger - logs the times, arguments, and return values of functions registered to it

These classes all act in very much the same way, they use function metadata to track the execution of functions. For example, you can time the execution of a function by wrapping it in an ExecutionTimer instance.

import time
from contemplation import ExecutionTimer

execution_timer = ExecutionTimer()

@execution_timer.time_execution
def my_func(a: int, b: int):
    time.sleep(0.01)
    return a + b

for i in range(100):
    _ = my_func(i, i + 1)

# get the total execution time by function
print(execution_timer.get_execution_time(my_func))
# or by name
print(execution_timer.get_execution_time("my_func"))

# log another function
@execution_timer.time_execution
def my_other_func(a: int, b: int):
    time.sleep(0.005)
    return a - b

for i in range(100):
    _ = my_other_func(i, i + 1)

# print the times in a table
execution_timer.pretty_print_times()

The same API applies to the CallCounter:

import time
from contemplation import CallCounter

call_counter = CallCounter()

@call_counter.count_calls
def my_func(a: int, b: int):
    return a + b

for i in range(100):
    _ = my_func(i, i + 1)

# get the total execution time by function
print(call_counter.get_counts(my_func))
# or by name
print(call_counter.get_counts("my_func"))

# log another function
@call_counter.count_calls
def my_other_func(a: int, b: int):
    return a - b

for i in range(50):
   _ = my_other_func(i, i + 1)

# print the times in a table
call_counter.pretty_print_counts()

The FunctionLogger is a little different, it logs the arguments and return values of functions. It can be used to log the execution of functions to a file. The decorator for function logger has arguments as well.

from contemplation import FunctionLogger

function_logger = FunctionLogger()

@function_logger.log_function(log_args=True, log_returns=True)
def my_func(a: int, b: int):
    return a + b

_ = my_func(1, 2)

function_logger.pretty_print_logs()

# log another function
@function_logger.log_function(log_args=True, log_returns=True)
def my_other_func(a: int, b: int):
    return a - b

_ = my_other_func(3, 4)
_ = my_func(5, 6)

for event in function_logger:
    print(event)

Instance Introspections

Instance introspections are introspections that are performed on instances of objects. The following instance introspections are provided, they are poised as questions:

  • how_many_of_my_type_exist - how many instances of a type exist given an instance
  • how_many_of_type_exist - how many instances of a type exist given a type
  • get_name_in_caller_scope - get the name an instance was assigned to in the scope of the caller
  • get_name_in_all_scope - get the name an instance was assigned to in the scope of the caller and all parent scopes

These introspections are useful for debugging and analysing code. For example, you can use how_many_of_my_type_exist and how_many_of_type_exist to find out how many instances of a type exist given an instance.

from contemplation import how_many_of_my_type_exist, how_many_of_type_exist

class MyClass:
    pass

my_instance = MyClass()
my_instance_2 = MyClass()

print(how_many_of_my_type_exist(my_instance))

print(how_many_of_type_exist(MyClass))

You can use get_name_in_caller_scope an instance was assigned to in the scope of the caller.

from collections import defaultdict
from contemplation import get_name_in_caller_scope

class MyClass:
    pass

my_class = MyClass()

def my_func1(obj: object):
    print(get_name_in_caller_scope(obj))
    assert get_name_in_caller_scope(obj) == "my_class"

my_func1(my_class)

dd = defaultdict(int)

def my_func2(obj: object):
    print(get_name_in_caller_scope(obj))
    assert get_name_in_caller_scope(obj) == "dd"

my_func2(dd)

You can use get_name_in_all_scope an instance was assigned to in the scope of the caller and all parent scopes. This is very powerful for tracing how a variable made its way somewhere but can also be a bit confusing.

from contemplation import get_name_in_all_scope

def f1(a):
    print(get_name_in_all_scope(a))
    return get_name_in_all_scope(a)

def f2(b):
    return f1(b)

def f3(c):
    return f2(c)

def f4(d):
    return f3(d)

names = f4(1)
assert names[3] == "d"
assert names[2] == "c"
assert names[1] == "b"
assert names[0] == "a"

Experimental Type Introspections

These introspections are experimental and may change in the future, additional there are caveats to their use and a number of edge cases that may not be covered at the moment.

Type introspections are introspections that are performed on types. Currently there the @type_enforced() decorator and the introspect_type function.

The @type_enforced() decorator can be used to enforce the type of a function's arguments and return value.

This uses the annotations on the functions to check the types of the arguments against what was received.

You can toggle between shallow and deep type checking. Shallow will not work with annotations from the typing module however deep will. Deep is the default.

Don't use this outside of debugging, deep type checking will check every element of every iterable at every depth every time the function is called. This is slow.

from typing import Dict, List
from contemplation.experimental import type_enforced

@type_enforced(deep=True)
def test_func(d: Dict[str, List[int]]) -> int:
    s = 0
    for k in d:
        s += sum(d[k])
    return s

# this works no problems
d = {"a": [1, 2, 3], "b": [4, 5, 6]}
print(test_func(d))

# this fails
d = {"a": [1, 2, 3], "b": [4, 5, "6"]}
print(test_func(d))

A failed type check will print a detailed specification of the type that was received.

TypeError: Argument 'd' for function 'test_func' must be of type typing.Dict[str, typing.List[int]], instead type typing.Dict[str, typing.Union[typing.List[typing.Union[int, str]], typing.List[int]]] was passed

You can also get this type information directly for any object with the introspect_type function:

from contemplation.experimental import introspect_type

my_dict = {"a": [1, 2, 3], "b": [4, 5, 6]}

print(introspect_type(my_dict))

This will print typing.Dict[str, typing.List[int]].

The type introspection occurs recursively for arbitrarily complex objects.

e.g.

d = {"a": [1, 2, 3], "b": [4, {5}, "6"]}
print(introspect_type(d))

Generating Documentation

Documentation is located at docs/index.html.

You can generate documentation with pdoc3.

pip3 install pdoc3
pdoc3 --html --output-dir docs contemplation --force

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages