pymagic9.py

This module provides functions for analyzing call stacks such as nameof, auto-implemented properties, etc.

class pymagic9.pymagic9.PropertyMeta(name, bases, attrs)

This metaclass allows you to create auto-implemented properties (like in C#, where you can declare properties without explicitly defining a getter and setter), for which you can use an ellipsis or empty functions to indicate that the Python itself would create the special accessor.

Detailed working principle:

  1. Ordinary auto-implemented properties:

    property1 = property(..., ...)
    property1 = property(..., ..., ...)
    

    In this case, the following are created:

    • dictionary (in the closure of the metaclass initializer) for recording and retrieving the value;

    • getter that returns a value from the dictionary (if the value is not in the dictionary, then the AttributeError exception is thrown);

    • setter that writes a value to the dictionary if there is no value yet, or the value differs from that written in the dictionary;

    • deleter that deletes a value from the dictionary if it exists (if the value is not in the dictionary, then the AttributeError exception is thrown).

  2. Readonly auto-implemented properties:

    property1 = property(...)
    

    In this case, the following are created:

    • dictionary (in the closure of the metaclass initializer) for recording and retrieving the value;

    • getter that returns a value from the dictionary (if the value is not in the dictionary, then the AttributeError exception is thrown);

    • setter that can be called only once from the class initializer when creating an instance (when called again, the AttributeError exception is thrown);

    • deleter that deletes a value from the dictionary if it exists (if the value is not in the dictionary, then the AttributeError exception is thrown).

  3. Auto-implemented properties with custom getter:

    # getter is not empty function
    property1 = property(getter, ...)
    

    In this case, a custom getter remains. Otherwise, everything is the same as in the previous paragraphs.

  4. Auto-implemented properties with custom setter, deleter:

    # setter, deleter is not empty functions
    property1 = property(..., setter)
    property1 = property(..., ..., deleter)
    property1 = property(..., setter, deleter)
    

    In this case, the following are created:

    • dictionary (in the closure of the metaclass initializer) for recording and retrieving the value;

    • getter that returns a value from the dictionary (if the value is not in the dictionary, then the AttributeError exception is thrown);

    • setter that preserves the functionality of the initially defined user setter (that is, after the new value is written to the dictionary, the initially defined user setter is called).

    • deleter that preserves the functionality of the initially defined user deleter (that is, after deleting the value from the dictionary, the initially defined custom deleter is called).

  5. Properties that will not be processed by the PropertyMeta metaclass:

    # getter, setter, deleter is not empty functions
    property1 = property(getter)
    property1 = property(getter, setter)
    property1 = property(getter, setter, deleter)
    property1 = property(getter, setter, ...)
    

Examples

  1. Import the PropertyMeta metaclass and assign it as a metaclass for the desired class:

from pymagic9 import PropertyMeta


class Person(metaclass=PropertyMeta):
    pass

2. Create properties in this class with empty accessors (using empty function or ellipsis) to indicate that this property will be auto-implemented:

from pymagic9 import PropertyMeta


class Person(metaclass=PropertyMeta):
    """class Person"""
    def __init__(self, name):
        self.name = name

    name = property(fget=...,)           # readonly property
    age = property(fget=..., fset=...,)  # ordinary property

3. Now for an ordinary property we can get and put values into it at any time. But for a readonly property, you can put a value into it only once, at the time of creating an instance of the class:

from pymagic9 import PropertyMeta


class Person(metaclass=PropertyMeta):
    """class Person"""
    def __init__(self, name):
        self.name = name
        # self.name = "Sam"  # raise AttributeError: 'property' is readonly (reassigning value)

    name = property(fget=...,)           # readonly property
    age = property(fget=..., fset=...,)  # ordinary property


if __name__ == "__main__":
    person = Person("Tom")
    person.age = 24
    print(person.name + ',', person.age)  # Tom, 24
    # person.name = "Sam"  # raise AttributeError: 'property' is readonly
  1. To delete a property value, use the del operator:

from pymagic9 import PropertyMeta


class Person(metaclass=PropertyMeta):
    """class Person"""
    def __init__(self, name):
        self.name = name

    name = property(fget=...,)           # readonly property
    age = property(fget=..., fset=...,)  # ordinary property


if __name__ == "__main__":
    person = Person("Tom")
    person.age = 24
    print(person.name + ',', person.age)  # Tom, 24
    del person.name
    # print(person.name)  # raise AttributeError: auto-implemented field does not exist or has already been
                          # erased

5. If the getter is specified by an empty accessor (using empty function or ellipsis), and the setter is not an empty function, then setter will also be called. This can be used as a callback when assigning a value to a property:

from pymagic9 import nameof, PropertyMeta


def NotifyPropertyChanged(propertyname, value):
    """Notify property changed"""
    # Do something
    print(propertyname + ',', value)


class Person(metaclass=PropertyMeta):
    """class Person"""
    def __init__(self, name):
        self.name = name

    name = property(fget=...,)           # readonly property
    age = property(fget=..., fset=...,)  # ordinary property

    @property
    def height(self):
        """Person height in cm"""
        return

    @height.setter
    def height(self, value):
        NotifyPropertyChanged(nameof(self.height), value)


if __name__ == "__main__":
    person = Person("Tom")
    person.age = 24
    print(person.name + ',', person.age)  # Tom, 24
    person.height = 180  # height, 180
  1. Similar code for Python 2.7 looks like this:

from pymagic9 import nameof, PropertyMeta

__metaclass__ = PropertyMeta


def NotifyPropertyChanged(propertyname, value):
    """Notify property changed"""
    # Do something
    print(propertyname + ', ' + str(value))


class Person:
    """class Person"""
    def __init__(self, name):
        self.name = name

    name = property(fget=Ellipsis,)                # readonly property
    age = property(fget=Ellipsis, fset=Ellipsis,)  # ordinary property

    @property
    def height(self):
        """Person height in cm"""
        return

    @height.setter
    def height(self, value):
        NotifyPropertyChanged(nameof(self.height), value)


if __name__ == "__main__":
    person = Person("Tom")
    person.age = 24
    print(person.name + ', ' + str(person.age))  # Tom, 24
    person.height = 180  # height, 180
pymagic9.pymagic9.getframe(__depth=0)

The sys._getframe function is used here if it exists in the version of python being used. Otherwise, the _getframe polyfill is used.

pymagic9.pymagic9.isemptyfunction(func)

Checks if a function is empty or not.

Parameters:

func (function) – The function to check.

Returns:

True if the function is empty, False otherwise.

Return type:

bool

Raises:

TypeError – If the input object is not a function.

This function determines whether the given function is empty or not. An empty function is defined here as a function that: - may contain operators pass, return; - may only return None; - may contain a documentation string (classic documentation string in triple quotes, in double/single quotes as str type, and also in bytes type) and comments; - may contain any unreachable code (see odd_function in Examples); - may contain statements to have no effect (see odd_function in Examples). If something else is present in the function, then it is considered not empty. It can be useful in scenarios where you need to check if a function has any implementation or if it is just a placeholder.

The definition above may seem strange, but an empty function is defined this way for the sake of compatibility with the versions of Python supported by this project (since the interpreter behaves differently on different versions of Python, a simpler implementation of this function would return different values).

Examples

>>> def empty_function():
...     pass
...
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():
...     return
...
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():
...     return None
...
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():  
...     # only in Python3
...     ...
...
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():
...     """ docstring """
...
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():
...     "doc"
...
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():
...     b'Hello World!'
...     return
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():
...     # comments
...     return
...
>>> print(isemptyfunction(empty_function))
True
>>> def empty_function():
...     """ docstring """
...     return
...
>>> print(isemptyfunction(empty_function))
True
>>> empty_lambda = lambda x: None
>>> print(isemptyfunction(empty_lambda))
True
>>> def non_empty_function():
...     print("Hello, world!")
...
>>> print(isemptyfunction(non_empty_function))
False
>>> def non_empty_function():
...     return 0
...
>>> print(isemptyfunction(non_empty_function))
False
>>> not_empty_lambda = lambda: 0
>>> print(isemptyfunction(not_empty_lambda))
False
>>> # All odd_functions is empty!
>>> def odd_function():  # statement (immutable) to have no effect
...     None
...     100
...     True
...     ()
...     "string"
...     b'd'
...     return
...
>>> print(isemptyfunction(odd_function))
True
>>> def odd_function():  # statement (mutable) to have no effect
...     []
...     return
...
>>> print(isemptyfunction(odd_function))
True
>>> def odd_function(): """ docstring """; return; None;  # oneline function and unreachable code
>>> print(isemptyfunction(odd_function))
True
>>> def odd_function():  # unreachable code
...     return
...     a = 2
...
>>> print(isemptyfunction(odd_function))
True
pymagic9.pymagic9.isfunctionincallchain(o, __depth=-1)

Determines whether the given function object or code object is present in the call chain.

Parameters:
  • o (FunctionType or CodeType) – The function object or code object to check.

  • __depth (int, optional) – The depth of the call chain to search. Default is -1, which means search the entire call chain.

Returns:

True if the function or code object is found in the call chain, False otherwise.

Return type:

bool

Raises:

TypeError – If the input object is not a function or code object.

This function checks if the given function object or code object is present in the call chain of the current execution. The call chain is the sequence of function calls that led to the current point of execution.

Warning

Be careful when debugging in PyCharm - there may be incorrect behavior when a function that is being debugged (a function that has breakpoints) is passed as an argument.

Examples

>>> def foo():
...     return isfunctionincallchain(foo)
...
>>> def bar():
...     return isfunctionincallchain(foo)
...
>>> def baz():
...     return foo()
...
>>> print(foo())
True
>>> print(bar())
False
>>> print(baz())
True
pymagic9.pymagic9.nameof(o)

Returns the name of an object.

Parameters:

o (object) – The object for which to retrieve the name.

Returns:

The name of the object or empty string.

Return type:

str

This function correctly determines the ‘name’ of an object, without being tied to the object itself. It can be used to retrieve the name of variables, functions, classes, modules, and more. An empty string will be returned if an explicit value or call is passed to the nameof function as an argument.

Examples

>>> var1 = [1, 2]
>>> var2 = var1
>>> print(nameof(var1))
var1
>>> print(nameof(var2))
var2
>>> print(nameof(123))  
empty string ("")
pymagic9.pymagic9._getframe(__depth=0)

Polyfill for the built-in sys._getframe function.

Parameters:

__depth (int, optional) – The depth of the call stack to traverse. A value of 0 represents the current frame, 1 represents the caller’s frame, and so on.

Returns:

The frame object at the specified depth in the call stack, or None if the depth is out of range.

Return type:

FrameType or None

Raises:
  • TypeError – If the depth argument is not an integer.

  • ValueError – If call stack is not deep enough.

This function provides a polyfill for the built-in sys._getframe function, which is used to access the call stack frames. It allows you to retrieve the frame object at a specific depth in the call stack.

The depth argument specifies the number of frames to traverse. A value of 0 or less 0 represents the current frame, 1 represents the caller’s frame, and so on. If the depth is negative, it will traverse the frames in the opposite direction.

If the specified depth is out of range (i.e., greater than the number of frames in the call stack), the function raises ValueEror.

Examples

>>> def foo():
...     frame = _getframe(1)
...     print(frame.f_code.co_name)  # Output: bar
>>> def bar():
...     foo()
>>> bar()
bar
pymagic9.pymagic9._unpack_opargs(code)

Unpacks the opcodes and their arguments from the given bytecode. Works in Python 2.7 and Python 3.

Parameters:

code (bytes) – The bytecode to unpack.

Yields:

tuple – A tuple containing the offset, opcode, and argument of each opcode.

It takes a bytecode object as input and yields a sequence of tuples containing the offset, opcode, and argument of each opcode in the bytecode.