Python Forum
Metaprogramming: automating conversions between types
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Metaprogramming: automating conversions between types
#1
I tend to use more and more unary conversions between types in my programming, so here is a small utility module that I wrote today for composing automatically converter functions. Comments are welcomed!

Edit: This new version allows circular conversion between types and has a richer API.
# converter.py

__version__ = '2022.05.23'

from functools import partial, singledispatch
import itertools as itt

def _default_index(cls):
    raise TypeError('Cannot convert to/from type', cls)

def _first_arg(i, *args):
    return i

class LinearConverter:
    """Implements automatic composition of conversion functions
    
    This class implements composition of conversion functions
    between a sequence of n types
    
        T[0] -> T[1] -> T[2] -> ... -> T[n-1]
    
    assuming that we have n-1 or n unary conversion functions
    
        func[i]:  T[i] -> T[i+1]

    then the converter offers a method to convert an object of type
    T[i] to type T[j] when i <= j by composing the successive conversion
    functions
    
        conv.convert(object, type)
    
    The converter uses generic functions to allow conversion of
    objects which type is a subtype of T[i].
    
    The converter can also return a callable to convert one type into
    another
    
        fun = conv.converter(type_A, type_B)
        fun(obj) # converts an instance of type_A
    
    The constructor takes two arguments:
    
        LinearConverter(<seq of n types>, <seq of n-1 or n functions>)

    If the constructor is passed n unary conversion functions, the
    last one is supposed to convert type T[n-1] into T[0]. In this
    case the linear converter is said to be cyclic and it can
    also convert type T[i] to T[j] when j < i.
    
    The converter can indicate which ancestor type is actually used
    when passing an object of the given type
    
        tp = conv.used_type(type)
    
    This used type is one of the types passed to the converter's ctor.
    """

    def __init__(self, iclass, ifunc):
        self.iclass = tuple(iclass)
        self.ifunc = tuple(ifunc)
        d = len(self.iclass) - len(self.ifunc)
        if d not in (0, 1):
            raise TypeError(
                'Constructor expected n classes and n-1 or n functions')
        self._is_cyclic = not d
        self._index_gen = singledispatch(_default_index)
        for i, cls in enumerate(self.iclass):
            self._index_gen.register(cls)(partial(_first_arg, i))
    
    def _index(self, cls):
        return self._index_gen.dispatch(cls)(cls)

    def convert(self, obj, cls):
        """Convert an object to another type by composing converters"""
        return self.converter(type(obj), cls)(obj)

    def converter(self, type_A, type_B):
        """Return a unary conversion function from one type to another"""
        i, j = self._index(type_A), self._index(type_B)
        if j < i:
            if self._is_cyclic:
                return partial(self._conversion_2, i, j)
            raise TypeError(
                'Converter cannot convert', obj, 'to type', cls)
        return partial(self._conversion_1, i, j)
    
    def _conversion_1(self, i, j, obj):
        for f in self.ifunc[i:j]:
            obj = f(obj)
        return obj

    def _conversion_2(self, i, j, obj):
        for f in itt.chain(self.ifunc[i:], self.ifunc[:j]):
            obj = f(obj)
        return obj
    
    def is_cyclic(self):
        """Return a boolean indicating if the linear converter is cyclic"""
        return self._is_cyclic
    
    def used_type(self, type_A):
        """Return the actual ancestor type used when converting an instance of a given type"""
        return self.iclass[self._index(type_A)]

if __name__ == '__main__':
    class ModuleName(str):
        pass    
    from types import ModuleType    
    from pathlib import Path
    import importlib
    
    def name_as_module(name):
        return importlib.import_module(name)
    
    def module_as_path(mod):
        return Path(mod.__file__)
    
    def path_as_tuple(path):
        return path.parts
    
    def tuple_as_module_name(t):
        if t[-1] in ('__init__.py', '__init__.pyc'):
            return ModuleName(t[-2])
        elif t[-1].endswith(('.py', '.pyc')):
            return ModuleName(t[-1].rsplit('.', 1)[0])
        else:
            raise ValueError(t)
    
    cv = LinearConverter(
        [ModuleName, ModuleType, Path, tuple],
        [
            name_as_module, module_as_path,
            path_as_tuple, tuple_as_module_name])
    
    t = cv.convert(ModuleName('subprocess'), tuple)
    print(t)
    p = cv.convert(importlib, Path)
    print(p)
    m = cv.convert(p, ModuleType)
    print(m)
Output:
('/', 'usr', 'lib', 'python3.8', 'subprocess.py') /usr/lib/python3.8/importlib/__init__.py <module 'importlib' from '/usr/lib/python3.8/importlib/__init__.py'>
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Enhanced properties with some Metaprogramming mnesarco 0 1,297 Jul-24-2020, 05:42 PM
Last Post: mnesarco

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020