├── .gitignore
├── README.md
├── setup.py
└── typecast
├── __init__.py
├── lib
├── __init__.py
├── time.py
└── web.py
└── typecast.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Typecast: Cast-Oriented programming
2 |
3 | Typecast is an experimental python library for defining casts (transformations) between different classes.
4 |
5 | Casts:
6 | * Defined as Type1 -> Type2
7 | * Are applied to instances (in this example, instances of Type1)
8 | * Connect into cast-chains (shortest path is chosen)
9 |
10 | In order to demonstrate the power and usefulness of this mechanism, let's look at a few case studies:
11 |
12 | ## Case study 1: Time units
13 |
14 | Typecast's library comes with time units which allow you to convert between them:
15 |
16 | ```python
17 | >>> from typecast.lib.time import Seconds, Minutes, Hours, Days, Weeks
18 | >>> Minutes(60) >> Seconds
19 | Seconds(3600)
20 | >>> Hours(60) >> Days
21 | Days(2.5)
22 | ```
23 |
24 | You can cast from any unit to any unit. However, the library doesn't implement O(n!) of casts. Every unit can cast to and from Seconds, and the chaining mechanism lets us get away with only O(n) cast implementations.
25 |
26 | Let's have a better look at chaining, when we try to add a new Fortnights class.
27 |
28 | ```python
29 | >>> from typecast import Typecast
30 | >>>
31 | >>> class Fortnights(metaclass=Typecast):
32 | >>> def __init__(self, fortnights):
33 | >>> self.fortnights = fortnights
34 | >>> def to__Weeks(self, cls):
35 | >>> return cls(self.fortnights * 2)
36 | >>>
37 | >>> Fortnights(2) >> Days
38 | Days(28)
39 | >>> Fortnights(2) >> Hours
40 | Hours(672)
41 | ```
42 |
43 | The *to\_\_* prefix is special to the Typecast metaclass.
44 |
45 | Notice how the chaining mechanism automatically lets us cast to Days, even though we only defined a cast to Weeks. We could have chosen any unit (Hours, Seconds, etc.) and it would have just worked.
46 |
47 | (Of course, to cast into Fortnights we'll also have to define a from\_\_ cast)
48 |
49 | Another little benefit of these units is using operations on them:
50 |
51 | ```python
52 | >>> Hours(1) + Minutes(30)
53 | Hours(1.5)
54 | >>> Days(1) < Minutes(800)
55 | False
56 | >>> Days(1) <= Minutes(2000)
57 | True
58 | ```
59 |
60 | ## Case study 2: HTML & type-safety
61 |
62 | The typecast library defines a HTML type, with a few basic casts:
63 |
64 | ```python
65 | >>> from typecast.lib.web import HTML
66 | >>>
67 | >>> HTML << 'a < b'
68 | HTML('a < b')
69 | >>> HTML << ['a', 'b']
70 | '
\n- a
\n- b
\n
'
71 | ```
72 |
73 | Notice that we can be confident that a HTML type is safe to insert into a html document.
74 |
75 | Another feature of typecast, called "autocast", allows us to write safe operations on html without much effort:
76 |
77 | ```python
78 | >>> from typecast import autocast
79 | >>>
80 | >>> @autocast
81 | >>> def div(html: HTML):
82 | >>> return HTML(f'{html.html}
')
83 | >>>
84 | >>> div(div('use to emphasize'))
85 | HTML('')
86 | ```
87 |
88 | Typecast lets us have html correctness, without having to worry about stacking operations.
89 |
90 | Of course, it's possible to define new casts (from database objects?) and chain them if necessary.
91 |
92 | ## Case studies: Summary
93 |
94 | These examples show possible uses for this library. However there might be many others: Improving APIs, interoperability of external libraries, etc.
95 |
96 | I encourage the curious reader to think of ways this library might apply to solve some of the design challenges in their current projects.
97 |
98 | # Caveats
99 |
100 | I will be the first to admit that this library is experimental, not only implementation but also in concept. Powerful tools are easy to abuse, so I advise potential users to think critically about whether this library is the right solution for their problems, or will it only add a new problem in the long run.
101 |
102 | Also, this library obviously adds some overhead, so it's not recommended for use in speed-critical code.
103 |
104 | # Support
105 |
106 | Typecast works on all versions of Python 3. (autocast relies on annotations)
107 |
108 | If there's enough demand, I will make it work for Python 2 too.
109 |
110 | # Installation
111 |
112 | ```bash
113 | $ git clone https://github.com/erezsh/typecast
114 | $ cd typecast
115 | $ python3 setup.py install
116 | ```
117 |
118 | # How to contribute
119 |
120 | If you want to contribute the typecast's lib of classes, or have ideas on how I can improve typecast, please let me know!
121 |
122 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 | from distutils.core import setup
3 |
4 | __version__ = "0.1a"
5 |
6 | setup(
7 | name = "Typecast",
8 | version = __version__,
9 | packages = ['typecast', 'typecast.lib'],
10 |
11 | requires = [],
12 | install_requires = [],
13 |
14 | package_data = {
15 | '': ['*.md'],
16 | },
17 |
18 | # metadata for upload to PyPI
19 | author = "Erez Shinan",
20 | author_email = "erezshin@gmail.com",
21 | description = "Cast-Oriented Programming",
22 | license = "MIT/GPL",
23 | keywords = "cast typecast",
24 | url = "https://github.com/erezsh/typecast", # project home page, if any
25 | download_url = "https://github.com/erezsh/typecast/tarball/master",
26 | long_description='''
27 | Typecast is an experimental python library for defining casts (transformations) between different classes.
28 |
29 | Casts:
30 | * Defined as Type1 -> Type2
31 | * Are applied to instances (in this example, instances of Type1)
32 | * Connect into cast-chains (shortest path is chosen)
33 | ''',
34 |
35 | classifiers=[
36 | "Development Status :: 3 - Alpha",
37 | "Intended Audience :: Developers",
38 | "Programming Language :: Python :: 3",
39 | "Topic :: Software Development :: Libraries :: Python Modules",
40 | "License :: OSI Approved :: MIT License",
41 | ],
42 |
43 | )
44 |
45 |
--------------------------------------------------------------------------------
/typecast/__init__.py:
--------------------------------------------------------------------------------
1 | from .typecast import Typecast, typecast_decor, autocast, CastError, cast_instance
2 |
--------------------------------------------------------------------------------
/typecast/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erezsh/typecast/b8ee7ee43763032deae5fd64c6f51a4181bd8d24/typecast/lib/__init__.py
--------------------------------------------------------------------------------
/typecast/lib/time.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | import time
3 |
4 | from ..typecast import typecast_decor, CastError, Typecast, autocast
5 |
6 | class Unit(metaclass=Typecast):
7 |
8 | def __init__(self, value):
9 | setattr(self, self._attr, value)
10 |
11 | def __repr__(self):
12 | return "%s(%g)" % (self.__class__.__name__, getattr(self, self._attr))
13 |
14 | class TimeUnit(Unit):
15 | def __add__(self, other):
16 | return Seconds((self >> Seconds).seconds + (other >> Seconds).seconds) >> type(self)
17 |
18 | def __sub__(self, other):
19 | return Seconds((self >> Seconds).seconds - (other >> Seconds).seconds) >> type(self)
20 |
21 | def __mul__(self, scalar):
22 | assert isinstance(scalar, (int, float))
23 | return type(self)(getattr(self, self._attr) * scalar)
24 |
25 | def __div__(self, scalar):
26 | assert isinstance(scalar, (int, float))
27 | return type(self)(getattr(self, self._attr) / scalar)
28 |
29 | def __eq__(self, other):
30 | other = other >> type(self)
31 | return getattr(self, self._attr) == getattr(other, self._attr)
32 |
33 | def __lt__(self, other):
34 | other = other >> type(self)
35 | return getattr(self, self._attr) < getattr(other, self._attr)
36 |
37 | def __ne__(self, other):
38 | return not (self == other)
39 | def __gt__(self, other):
40 | return other < self
41 | def __ge__(self, other):
42 | return not (self < other)
43 | def __le__(self, other):
44 | return not (other < self)
45 |
46 | def __hash__(self):
47 | return hash((self >> Seconds).seconds)
48 |
49 |
50 | class Seconds(TimeUnit):
51 | _attr = 'seconds'
52 |
53 |
54 | class Millisecs(TimeUnit):
55 | _attr = 'millisecs'
56 |
57 | def to__Seconds(self, cls):
58 | return cls(self.millisecs / 1000.0)
59 |
60 | def from__Seconds(cls, seconds):
61 | return cls(seconds.seconds * 1000.0)
62 |
63 |
64 | class Minutes(TimeUnit):
65 | _attr = 'minutes'
66 |
67 | def to__Seconds(self, cls):
68 | return cls(self.minutes * 60.0)
69 |
70 | def from__Seconds(cls, seconds):
71 | return cls(seconds.seconds / 60.0)
72 |
73 | class Hours(TimeUnit):
74 | _attr = 'hours'
75 |
76 | def to__Seconds(self, cls):
77 | return cls(self.hours * (60.0 * 60.0))
78 |
79 | def from__Seconds(cls, seconds):
80 | return cls(seconds.seconds / (60.0 * 60.0))
81 |
82 | class Days(TimeUnit):
83 | _attr = 'days'
84 |
85 | def to__Seconds(self, cls):
86 | return cls(self.days * (60.0 * 24.0 * 60.0))
87 |
88 | def from__Seconds(cls, seconds):
89 | return cls(seconds.seconds / (24.0 * 60.0 * 60.0))
90 |
91 |
92 | class Weeks(TimeUnit):
93 | _attr = 'weeks'
94 |
95 | def to__Days(self, cls):
96 | return cls(self.weeks * 7)
97 |
98 | def from__Days(cls, days):
99 | return cls(days.days / 7.0)
100 |
101 |
102 | @autocast
103 | def sleep(secs: Seconds):
104 | time.sleep(secs.seconds)
105 |
106 |
--------------------------------------------------------------------------------
/typecast/lib/web.py:
--------------------------------------------------------------------------------
1 | from ..typecast import typecast_decor, CastError, Typecast, autocast
2 |
3 | class HTML(metaclass=Typecast):
4 |
5 | def __init__(self, html):
6 | self.html = html
7 |
8 | def __repr__(self):
9 | return "HTML(%r)" % self.html
10 |
11 | def from__str(cls, s):
12 | assert isinstance(s, str), s
13 | return cls(s.replace('<', '<').replace('>', '>'))
14 |
15 | def to__str(self, str):
16 | return str(self.html.replace('>', '>').replace('<', '<'))
17 |
18 | def from__list(cls, l):
19 | return '\n%s\n
' % '\n'.join('%s' % (n >> cls).html for n in l)
20 |
21 | def from__set(cls, s):
22 | return '' % '\n'.join('%s' % (n >> cls).html for n in s)
23 |
24 | def from__dict(cls, d):
25 | return '\n%s\n
' % '\n'.join('%s%s' % ((k>>cls).html, (v>>cls).html) for k,v in d.items())
26 |
27 |
--------------------------------------------------------------------------------
/typecast/typecast.py:
--------------------------------------------------------------------------------
1 | # TODO: from/to should follow inheritance even when defined outside of the class
2 |
3 | from collections import defaultdict, deque
4 | import inspect
5 |
6 | _g_classmap = defaultdict(dict)
7 |
8 | class CastError(Exception):
9 | def __str__(self):
10 | return "%s: %s >> %s" % (self[0], self[1], self[2])
11 |
12 |
13 | def bfs(initial, expand):
14 | open_q = deque(list(initial))
15 | visited = set(open_q)
16 | while open_q:
17 | node = open_q.popleft()
18 | yield node
19 | for next_node in expand(node):
20 | if next_node not in visited:
21 | visited.add(next_node)
22 | open_q.append(next_node)
23 |
24 |
25 |
26 | def _add_cast_function(orig, target, f):
27 | if target in _g_classmap[orig]:
28 | raise ValueError("Duplicate function", orig, target)
29 | _g_classmap[orig][target] = f
30 |
31 |
32 | def _get_cast_elements(cls_module, cls, attr):
33 | direction, other_cls_name = attr.split('__', 1)
34 | assert direction in ('from', 'to')
35 | try:
36 | other_cls = getattr(cls_module, other_cls_name)
37 | except AttributeError:
38 | other_cls = __builtins__[other_cls_name]
39 |
40 | if direction == 'to':
41 | orig, target = cls, other_cls
42 | f = getattr(cls, attr)
43 | else:
44 | orig, target = other_cls, cls
45 | # g = getattr(cls, attr).__func__ # Python 2.7
46 | g = getattr(cls, attr)
47 | def f(self, cls):
48 | return g(cls, self)
49 | f.__name__ = g.__name__
50 |
51 | return orig, target, f
52 |
53 | def _cast(instance, orig_cls, target_cls):
54 | try:
55 | return _g_classmap[orig_cls][target_cls](instance, target_cls)
56 | except KeyError:
57 | breadcrumbs = {}
58 | def expand(n):
59 | for k in _g_classmap[n]:
60 | if k not in breadcrumbs:
61 | breadcrumbs[k] = n
62 | yield k
63 | for x in bfs([orig_cls], expand):
64 | if x == target_cls:
65 | break
66 | else:
67 | raise CastError("Couldn't find a cast path", orig_cls, target_cls)
68 |
69 | x = target_cls
70 | path = []
71 | while x != orig_cls:
72 | path.append(x)
73 | x = breadcrumbs[x]
74 | path.reverse()
75 |
76 | prev = orig_cls
77 | inst = instance
78 | for n in path:
79 | inst = _g_classmap[prev][n](inst, n)
80 | prev = n
81 | return inst
82 |
83 | def cast_instance(instance, target_cls):
84 | "Attempt to cast an instance to the target class"
85 | # Not using type() because it requires inheritance from object
86 | return _cast(instance, instance.__class__, target_cls)
87 |
88 |
89 | def typecast_decor(cls):
90 | "Class decorator to activate typecast magic. For full functionality, use Typecast metaclass"
91 | cls_module = inspect.getmodule(cls)
92 | for attr in dir(cls):
93 | if attr.startswith(('from__', 'to__')):
94 | orig, target, f = _get_cast_elements(cls_module, cls, attr)
95 |
96 | _add_cast_function(orig, target, f)
97 | # TODO remove original functions?
98 |
99 | cls.__rshift__ = cast_instance
100 | cls.__rlshift__ = cast_instance
101 | return cls
102 |
103 | class Typecast(type):
104 | "Metaclass to activate typecast magic on class"
105 | def __init__(cls, name, bases, nmspc):
106 | super(Typecast, cls).__init__(name, bases, nmspc)
107 |
108 | typecast_decor(cls)
109 |
110 | # classmethods
111 | def __lshift__(cls, inst):
112 | return cast_instance(inst, cls)
113 | __rrshift__ = __lshift__
114 |
115 |
116 | def _match_annotations_decor(f, match):
117 | names = list(f.__code__.co_varnames)
118 | annotations = f.__annotations__
119 | indices = [(names.index(n), a) for n,a in annotations.items()]
120 | def _inner(*args, **kwargs):
121 | args = list(args)
122 | for i, ann in indices:
123 | if i >= len(args):
124 | break
125 | args[i] = _autocast_match(args[i], ann)
126 |
127 | for name, val in kwargs.items():
128 | if name in annotations:
129 | kwargs[name] = _autocast_match(val, annotations[name])
130 |
131 | return f(*args, **kwargs)
132 |
133 | return _inner
134 |
135 | # def _verify_match(inst, type_):
136 | # assert isinstance(inst, type_), "Expected type: '%s'. Instead got '%s'" % (type_, inst.__class__)
137 |
138 | def _autocast_match(inst, type_):
139 | if isinstance(inst, type_):
140 | return inst
141 | else:
142 | return cast_instance(inst, type_)
143 |
144 |
145 | def autocast(f):
146 | return _match_annotations_decor(f, _autocast_match)
147 |
148 | # def verify_types(f):
149 | # return _match_annotations_decor(f, _verify_match)
150 |
151 |
152 |
--------------------------------------------------------------------------------