Skip to content

Commit

Permalink
Behold innerscope!
Browse files Browse the repository at this point in the history
  • Loading branch information
eriknw committed Aug 10, 2020
0 parents commit bdc0ea8
Show file tree
Hide file tree
Showing 11 changed files with 1,039 additions and 0 deletions.
117 changes: 117 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Vi
*.swp
*.swo

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

# PyCharm
.idea

# Mac
.DS_Store

# VSCode
.vscode
25 changes: 25 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
sudo: false
language: python
os: linux
dist: xenial

python:
- "3.8"
- "3.9-dev"

install:
- pip install toolz coverage pytest flake8 black
- pip install -e .

script:
- coverage run --branch -m pytest --doctest-modules
- flake8
- black innerscope --check --diff

after_success:
- coverage report --show-missing
- pip install coveralls
- coveralls

notifications:
email: false
28 changes: 28 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Copyright (c) 2020 Erik Welch

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

a. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
b. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
c. Neither the name of innerscope nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.


THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
115 changes: 115 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Innerscope

`innerscope` exposes the inner scope of functions and offers primitives suitable for creating pipelines. It explores a design space around functions, dictionaries, and classes.

A function can be made like a dictionary:
```python
@innerscope.call
def info():
first_name = 'Erik'
last_name = 'Welch'
full_name = f'{first_name} {last_name}'

>>> info['first_name']
'Erik'
>>> info['full_name']
'Erik Welch'
```
Sometimes we want functions to be more *functional* and accept arguments:
```python
if is_a_good_idea:
suffix = 'the amazing'
else:
suffix = 'the bewildering'

@innerscope.callwith(suffix)
def info_with_suffix(suffix=None):
first_name = 'Erik'
last_name = 'Welch'
full_name = f'{first_name} {last_name}'
if suffix:
full_name = f'{full_name} {suffix}'

>>> info_with_suffix['full_name']
'Erik Welch the bewildering'
```
Cool!

But, what if we want to reuse the data computed in `info`? We can control *exactly* what values are within scope inside of a function (including from closures and globals; more on these later). Let's bind the variables in `info` to a new function:
```python
@info.bindto
def add_suffix(suffix):
full_name = f'{first_name} {last_name} {suffix}'

>>> scope = add_suffix('the astonishing')
>>> scope['full_name']
'Erik Welch the astonishing'
```
`add_suffix` here is a `ScopedFunction`. It returns a `Scope`, which is the dict-like object we've already seen.

## `scoped_function` ftw!

Except for the simplest tasks (as with `call` and `callwith` above), using `scoped_function` should usually be preferred.

```python
# step1 becomes a ScopedFunction that we can call
@scoped_function
def step1(a):
b = a + 1

>>> scope1 = step1(1)
>>> scope1 == {'a': 1, 'b': 2}
True

# Bind any number of mappings to variables (later mappings have precedence)
@scoped_function(scope1, {'c': 3})
def step2(d):
e = max(a + d, b + c)

>>> step2.outer_scope == {'a': 1, 'b': 2, 'c': 3, '__builtins__': __builtins__}
True
>>> scope2 = step2(4)
>>> scope2 == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
True
>>> scope2.inner_scope == {'d': 4, 'e': 5}
True
```
Suppose you're paranoid (like me!) and want to control whether a function uses values from closures or globals. You're in luck!
```python
global_x = 1

def f():
closure_y = 2
def g():
local_z = global_x + closure_y
return g

# If you're the trusting type...
>>> g = f()
>>> innerscope.call(g) == {'global_x': 1, 'closure_y': 2, 'local_z': 3}
True

# And for the intelligent...
>>> paranoid_g = scoped_function(g, use_closures=False, use_globals=False)
>>> paranoid_g.missing
{'closure_y', 'global_x'}
>>> paranoid_g()
```
<pre style='color:red'>NameError: Undefined variables: 'global_x', 'closure_y'.
Use `bind` method to assign values for these names before calling.</pre>
```python
>>> new_g = paranoid_g.bind({'global_x': 100, 'closure_y': 200})
>>> new_g.missing
set()
>>> new_g() == {'global_x': 100, 'closure_y': 200, 'local_z': 300}
True
```
## How?
This library requires surprisingly little magic. Perhaps I'll explain it some day. Here's a hint: the wrapped function shouldn't have any return statements.

## Why?
It's all [@mrocklin's](https://github.com/mrocklin) fault for [asking a question.](https://github.com/dask/distributed/issues/4003)
`innerscope` is exploring a data model that could be convenient for running code remotely with [dask.](https://dask.org)
I bet it would even be useful for building pipelines with dask.

#### *This library is totally awesome and you should use it and tell all your friends* 😉 *!*
1 change: 1 addition & 0 deletions innerscope/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .core import bindwith, call, callwith, scoped_function # noqa

0 comments on commit bdc0ea8

Please sign in to comment.