Code Monkey home page Code Monkey logo

sorcery's Introduction

sorcery

Build Status Coverage Status Supports Python 3.5+, including PyPy

This package lets you use and write callables called 'spells' that know where they're being called from and can use that information to do otherwise impossible things.

Note: previously spells had a complicated implementation that placed limitations on how they could be called. Now spells are just a thin wrapper around executing which is much better. You may be better off using executing directly depending on your use case. This repo is now mostly just a fun collection of things to do with it.

Installation

pip install sorcery

Quick examples

See the docstrings for more detail.

from sorcery import (assigned_names, unpack_keys, unpack_attrs,
                     dict_of, print_args, call_with_name,
                     delegate_to_attr, maybe, select_from)

assigned_names

Instead of:

foo = func('foo')
bar = func('bar')

write:

foo, bar = [func(name) for name in assigned_names()]

Instead of:

class Thing(Enum):
    foo = 'foo'
    bar = 'bar'

write:

class Thing(Enum):
    foo, bar = assigned_names()

unpack_keys and unpack_attrs

Instead of:

foo = d['foo']
bar = d['bar']

write:

foo, bar = unpack_keys(d)

Similarly, instead of:

foo = x.foo
bar = x.bar

write:

foo, bar = unpack_attrs(x)

dict_of

Instead of:

dict(foo=foo, bar=bar, spam=thing())

write:

dict_of(foo, bar, spam=thing())

(see also: magic_kwargs)

print_args

For easy debugging, instead of:

print("foo =", foo)
print("bar() =", bar())

write:

print_args(foo, bar())

To write your own version of this (e.g. if you want to add colour), use args_with_source.

If you like this, I recommend the pp function in the snoop library.

call_with_name and delegate_to_attr

Sometimes you want to create many similar methods which differ only in a string argument which is equal to the name of the method. Given this class:

class C:
    def generic(self, method_name, *args, **kwargs):
        ...

Inside the class definition, instead of:

    def foo(self, x, y):
        return self.generic('foo', x, y)

    def bar(self, z):
        return self.generic('bar', z)

write:

    foo, bar = call_with_name(generic)

For a specific common use case:

class Wrapper:
    def __init__(self, thing):
        self.thing = thing

    def foo(self, x, y):
        return self.thing.foo(x, y)

    def bar(self, z):
        return self.thing.bar(z)

you can instead write:

    foo, bar = delegate_to_attr('thing')

For a more concrete example, here is a class that wraps a list and has all the usual list methods while ensuring that any methods which usually create a new list actually create a new wrapper:

class MyListWrapper(object):
    def __init__(self, lst):
        self.list = lst

    def _make_new_wrapper(self, method_name, *args, **kwargs):
        method = getattr(self.list, method_name)
        new_list = method(*args, **kwargs)
        return type(self)(new_list)

    append, extend, clear, __repr__, __str__, __eq__, __hash__, \
        __contains__, __len__, remove, insert, pop, index, count, \
        sort, __iter__, reverse, __iadd__ = spells.delegate_to_attr('list')

    copy, __add__, __radd__, __mul__, __rmul__ = spells.call_with_name(_make_new_wrapper)

Of course, there are less magical DRY ways to accomplish this (e.g. looping over some strings and using setattr), but they will not tell your IDE/linter what methods MyListWrapper has or doesn't have.

maybe

While we wait for the ?. operator from PEP 505, here's an alternative. Instead of:

None if foo is None else foo.bar()

write:

maybe(foo).bar()

If you want a slightly less magical version, consider pymaybe.

timeit

Instead of

import timeit

nums = [3, 1, 2]
setup = 'from __main__ import nums'

print(timeit.repeat('min(nums)', setup))
print(timeit.repeat('sorted(nums)[0]', setup))

write:

import sorcery

nums = [3, 1, 2]

if sorcery.timeit():
    result = min(nums)
else:
    result = sorted(nums)[0]

switch

Instead of:

if val == 1:
    x = 1
elif val == 2 or val == bar():
    x = spam()
elif val == dangerous_function():
    x = spam() * 2
else:
    x = -1

write:

x = switch(val, lambda: {
    1: 1,
    {{ 2, bar() }}: spam(),
    dangerous_function(): spam() * 2
}, default=-1)

This really will behave like the if/elif chain above. The dictionary is just some nice syntax, but no dictionary is ever actually created. The keys are evaluated only as needed, in order, and only the matching value is evaluated.

select_from

Instead of:

cursor.execute('''
    SELECT foo, bar
    FROM my_table
    WHERE spam = ?
      AND thing = ?
    ''', [spam, thing])

for foo, bar in cursor:
    ...

write:

for foo, bar in select_from('my_table', where=[spam, thing]):
    ...

How to write your own spells

Decorate a function with @spell. An instance of the class FrameInfo will be passed to the first argument of the function, while the other arguments will come from the call. For example:

from sorcery import spell

@spell
def my_spell(frame_info, foo):
    ...

will be called as just my_spell(foo).

The most important piece of information you are likely to use is frame_info.call. This is the ast.Call node where the spell is being called. Here is some helpful documentation for navigating the AST. Every node also has a parent attribute added to it.

frame_info.frame is the execution frame in which the spell is being called - see the inspect docs for what you can do with this.

Those are the essentials. See the source of various spells for some examples, it's not that complicated.

Using other spells within spells

Sometimes you want to reuse the magic of one spell in another spell. Simply calling the other spell won't do what you want - you want to tell the other spell to act as if it's being called from the place your own spell is called. For this, add insert .at(frame_info) between the spell you're using and its arguments.

Let's look at a concrete example. Here's the definition of the spell args_with_source:

@spell
def args_with_source(frame_info, *args):
    """
    Returns a list of pairs of:
        - the source code of the argument
        - the value of the argument
    for each argument.

    For example:

        args_with_source(foo(), 1+2)

    is the same as:

        [
            ("foo()", foo()),
            ("1+2", 3)
        ]
    """
    ...

The magic of args_with_source is that it looks at its arguments wherever it's called and extracts their source code. Here is a simplified implementation of the print_args spell which uses that magic:

@spell
def simple_print_args(frame_info, *args):
    for source, arg in args_with_source.at(frame_info)(*args):
        print(source, '=', arg)

Then when you call simple_print_args(foo(), 1+2), the Call node of that expression will be passed down to args_with_source.at(frame_info) so that the source is extracted from the correct arguments. Simply writing args_with_source(*args) would be wrong, as that would give the source "*args".

Other helpers

That's all you really need to get started writing a spell, but here are pointers to some other stuff that might help. See the docstrings for details.

The module sorcery.core has these helper functions:

  • node_names(node: ast.AST) -> Tuple[str]
  • node_name(node: ast.AST) -> str
  • statement_containing_node(node: ast.AST) -> ast.stmt:

FrameInfo has these methods:

  • assigned_names(...)
  • get_source(self, node: ast.AST) -> str

Should I actually use this library?

If you're still getting the hang of Python, no. This will lead to confusion about what is normal and expected in Python and will hamper your learning.

In a serious business or production context, I wouldn't recommend most of the spells unless you're quite careful. Their unusual nature may confuse other readers of the code, and tying the behaviour of your code to things like the names of variables may not be good for readability and refactoring. There are some exceptions though:

  • call_with_name and delegate_to_attr
  • assigned_names for making Enums.
  • print_args when debugging

If you're writing code where performance and stability aren't critical, e.g. if it's for fun or you just want to get some code down as fast as possible and you can polish it later, then go for it.

The point of this library is not just to be used in actual code. It's a way to explore and think about API and language design, readability, and the limits of Python itself. It was fun to create and I hope others can have fun playing around with it. Come have a chat about what spells you think would be cool, what features you wish Python had, or what crazy projects you want to create.

If you're interested in this stuff, particularly creative uses of the Python AST, you may also be interested in:

  • executing the backbone of this library
  • snoop: a feature-rich and convenient debugging library which also uses executing as well as various other magic and tricks
  • birdseye: a debugger which records the value of every expression
  • MacroPy: syntactic macros in Python by transforming the AST at import time

sorcery's People

Contributors

alexmojaki avatar cnheider avatar gitter-badger avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sorcery's Issues

Jupyter notebook error

Hi @alexmojaki love sorcery - thank you!

Just been trying to invoke dict_of inside Jupyter but get the below - any ideas? Thanks again!

from sorcery import dict_of
my_dict=dict_of(x,y,z)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)

[...]

.../lib/python3.9/site-packages/sorcery/core.py in __call__(self, *args, **kwargs)
    183 
    184         executing = Source.executing(frame)
--> 185         assert executing.node, "Failed to find call node"
    186         return self.at(FrameInfo(executing))(*args, **kwargs)
    187 

AssertionError: Failed to find call node

dict_of strange behavior

% ipython
Python 3.8.2 | packaged by conda-forge | (default, Apr 16 2020, 18:04:51)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.13.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: %doctest_mode
Exception reporting mode: Plain
Doctest mode is: ON
>>> import sorcery
>>> sorcery.__version__
'0.2.0'
>>> x, y, z = range(3)
>>> sorcery.dict_of(x, y, z)
{'code_obj': 0, 'user_global_ns': 1, 'user_ns': 2}
>>> sorcery.dict_of(x, y, z, w='WTF?')
{'code_obj': 0, 'user_global_ns': 1, 'user_ns': 2, 'w': 'WTF?'}
>>> dict(x=x, y=y, z=z)
{'x': 0, 'y': 1, 'z': 2}

Support walrus operator assignments

...such as this:

if (some_key := unpack_keys(dict(some_key=42))):
    print(some_key)

currently this raises

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/local/lib/python3.11/site-packages/sorcery/core.py", line 185, in __call__
    assert executing.node, "Failed to find call node"
AssertionError: Failed to find call node

while it would be magical if sorcery could handle this walrus assignment expression, too, wouldn't it?

Consider the walrus operator for assigned_names?

Hi Alex and sorcerers! Should the walrus operator be considered an assigned name for assigned_names?
I think so and this is an example with current and expected output:

from sorcery import spell, print_args

@spell
def lhs(frame_info) -> str:
    return frame_info.assigned_names(allow_one=True)[0]

test = [
    x := lhs(),
    y := lhs(),
]

print_args(x, y, test)

Current:

x =
('test',)

y =
('test',)

test =
[('test',), ('test',)]

Expected:

x =
('x',)

y =
('y',)

test =
[('x',), ('y',)]

What do you think?

Jupyter Notebook support

Does sorcery have a chance of working inside Jupyter Notebook? I was really happy to find this package since I very often use the unpack_keys operation and dict_of when working with training flows in machine learning but ran into an error very quickly.

FileNotFoundError                         Traceback (most recent call last)
<ipython-input-39-1de5f0c5f0a0> in <module>
      1 a = 'a'
      2 b = 'b'
----> 3 test = dict_of(a, b)
      4 a = unpack_keys(test)

~/.local/lib/python3.6/site-packages/sorcery/core.py in __call__(self, *args, **kwargs)
    329     def __call__(self, *args, **kwargs):
    330         frame = sys._getframe(1)
--> 331         call = FileInfo.for_frame(frame)._plain_call_at(frame, self)
    332         return self.at(FrameInfo(frame, call))(*args, **kwargs)
    333 

~/.local/lib/python3.6/site-packages/sorcery/core.py in for_frame(frame)
     42     @staticmethod
     43     def for_frame(frame) -> 'FileInfo':
---> 44         return file_info(frame.f_code.co_filename)
     45 
     46     @lru_cache()

~/.local/lib/python3.6/site-packages/sorcery/core.py in __init__(self, path)
     29 
     30     def __init__(self, path):
---> 31         with tokenize.open(path) as f:
     32             self.source = f.read()
     33         self.tree = ast.parse(self.source, filename=path)

/usr/lib/python3.6/tokenize.py in open(filename)
    450     detect_encoding().
    451     """
--> 452     buffer = _builtin_open(filename, 'rb')
    453     try:
    454         encoding, lines = detect_encoding(buffer.readline)

FileNotFoundError: [Errno 2] No such file or directory: '<ipython-input-39-1de5f0c5f0a0>'```

FileNotFoundError when running interactively

from sorcery import dict_of
a=1
b=2
c = dict_of(a,b)
print(c)

If I save the script as mypytest.py and run it it prints {'a': 1, 'b': 2} as expected

But if I run it from python console, it complains

Type "help", "copyright", "credits" or "license" for more information.
>>> from sorcery import dict_of
>>> a=1
>>> b=2
>>> c = dict_of(a,b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.5/dist-packages/sorcery/core.py", line 331, in __call__
    call = FileInfo.for_frame(frame)._plain_call_at(frame, self)
  File "/usr/local/lib/python3.5/dist-packages/sorcery/core.py", line 44, in for_frame
    return file_info(frame.f_code.co_filename)
  File "/usr/local/lib/python3.5/dist-packages/sorcery/core.py", line 31, in __init__
    with tokenize.open(path) as f:
  File "/usr/lib/python3.5/tokenize.py", line 454, in open
    buffer = _builtin_open(filename, 'rb')
FileNotFoundError: [Errno 2] No such file or directory: '<stdin>'

can we use expressions

I want

a = 10
b = 20
dict_of(a,b,a < b)
if a < b:
    ....

to return

{
"a": 10,
"b": 20,
"a<b":True
}

Or can this be possible

a = 10
b = 20
dict_of(a,b,"a<10"=a < b)
or
dict_of(a,b,{"a<10":a < b})

if a < b:
   .........

currently i am doing

print({**dict_of(a,b),**{"a<b": a < b}})

dict_of doesn't work in the python shell

$ python
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 1
>>> y = 2
>>> from sorcery import dict_of
>>> dict_of(x, y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/david/PycharmProjects/sandbox/venv/lib/python3.6/site-packages/sorcery/core.py", line 177, in __call__
    while frame.f_code in self._excluded_codes or frame.f_code.co_filename.startswith('<'):
AttributeError: 'NoneType' object has no attribute 'f_code'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.