├── ipythonblocks ├── test │ ├── __init__.py │ ├── test_misc.py │ ├── test_colors.py │ ├── test_pixel.py │ ├── test_images.py │ ├── test_imagegrid.py │ ├── test_block.py │ ├── test_ipborg.py │ └── test_blockgrid.py ├── __init__.py └── ipythonblocks.py ├── setup.cfg ├── MANIFEST.in ├── .coveragerc ├── test_requirements.txt ├── .travis.yml ├── LICENSE.txt ├── setup.py ├── CODE_OF_CONDUCT.md ├── demos ├── learning_colors.ipynb ├── ipythonblocks_animation.ipynb └── ipythonblocks_imagegrid.ipynb ├── .gitignore ├── CHANGES.rst ├── README.rst └── ez_setup.py /ipythonblocks/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include ez_setup.py 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = ipythonblocks/ipythonblocks.py 3 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | mock>=1.0 2 | responses>=0.1 3 | pillow>=2.4 4 | pytest>=2.3 5 | -------------------------------------------------------------------------------- /ipythonblocks/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ipythonblocks provides a BlockGrid class that displays a colored grid in the 3 | IPython Notebook. The colors can be manipulated, making it useful for 4 | practicing control flow stuctures and quickly seeing the results. 5 | 6 | """ 7 | 8 | from .ipythonblocks import * 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | install: 9 | - easy_install -U setuptools 10 | - pip install "ipython<6.0" 11 | - pip install . 12 | - pip install -r test_requirements.txt 13 | - pip install pytest-cov 14 | - pip install coveralls 15 | script: 16 | - py.test --cov ipythonblocks --cov-report term-missing 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /ipythonblocks/test/test_misc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test miscellaneous utility functions in the ipythonblocks module. 3 | 4 | """ 5 | 6 | from .. import ipythonblocks 7 | 8 | 9 | def test_flatten(): 10 | # single thing 11 | for x in ipythonblocks._flatten(1): 12 | assert x == 1 13 | 14 | # simple list 15 | thing = range(5) 16 | for i, x in enumerate(ipythonblocks._flatten(thing)): 17 | assert x == i 18 | 19 | # nested lists 20 | thing = [[0], [1, 2], [3], [4, 5, [6]]] 21 | for i, x in enumerate(ipythonblocks._flatten(thing)): 22 | assert x == i 23 | 24 | -------------------------------------------------------------------------------- /ipythonblocks/test/test_colors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for ipythonblocks' color conveniences. 3 | 4 | """ 5 | 6 | import pytest 7 | 8 | from .. import ipythonblocks 9 | 10 | 11 | def test_color(): 12 | color = ipythonblocks.Color(1, 2, 3) 13 | 14 | assert color == (1, 2, 3) 15 | assert color.red == 1 16 | assert color.green == 2 17 | assert color.blue == 3 18 | 19 | 20 | @pytest.mark.parametrize( 21 | 'colors, color_name', 22 | [ 23 | (ipythonblocks.colors, 'Crimson'), 24 | (ipythonblocks.fui_colors, 'Carrot'), 25 | ] 26 | ) 27 | def test_colors(colors, color_name): 28 | assert isinstance(colors, ipythonblocks._ColorBunch) 29 | assert color_name in colors 30 | assert hasattr(colors, color_name) 31 | assert isinstance(colors[color_name], ipythonblocks.Color) 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ipythonblocks 2 | Copyright (c) 2012 Matt Davis 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 5 | and associated documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 14 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from ez_setup import use_setuptools 3 | use_setuptools() 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open('README.rst') as f: 8 | long_description = f.read() 9 | 10 | setup(name='ipythonblocks', 11 | version='1.10.dev', 12 | description='Practice Python with colored grids in the IPython Notebook', 13 | long_description=long_description, 14 | author='Matt Davis', 15 | author_email='jiffyclub@gmail.com', 16 | url='https://github.com/jiffyclub/ipythonblocks/', 17 | packages=find_packages(exclude=['*.test']), 18 | classifiers=['License :: OSI Approved :: MIT License', 19 | 'Programming Language :: Python', 20 | 'Programming Language :: Python :: 2', 21 | 'Programming Language :: Python :: 2.7', 22 | 'Programming Language :: Python :: 3', 23 | 'Intended Audience :: Education', 24 | 'Topic :: Education'], 25 | install_requires=[ 26 | 'ipython>=4.0', 27 | 'notebook>=4.0', 28 | 'requests>=1.0', 29 | ]) 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | ## Version 0.4 3 | 4 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 5 | 6 | If any participant in this project has issues or takes exception with a contribution, they are obligated to provide constructive feedback and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 7 | 8 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 9 | 10 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 11 | 12 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, ability or disability, ethnicity, religion, or level of experience. 13 | -------------------------------------------------------------------------------- /ipythonblocks/test/test_pixel.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from .. import ipythonblocks 5 | 6 | 7 | @pytest.fixture 8 | def basic_pixel(): 9 | return ipythonblocks.Pixel(5, 6, 7, size=20) 10 | 11 | 12 | def test_xy(basic_pixel): 13 | """ 14 | Test the .x and .y attributes. 15 | 16 | """ 17 | bp = basic_pixel 18 | 19 | assert bp.x is None 20 | assert bp.y is None 21 | 22 | bp._row = 1 23 | bp._col = 2 24 | 25 | assert bp.x == 2 26 | assert bp.y == 1 27 | 28 | 29 | def test_td(basic_pixel): 30 | """ 31 | Test the Pixel._td proerty that returns an HTML table cell. 32 | 33 | """ 34 | bp = basic_pixel 35 | 36 | bp._row = 1 37 | bp._col = 2 38 | 39 | title = ipythonblocks._TITLE.format(bp._col, bp._row, 40 | bp.red, bp.green, bp.blue) 41 | rgb = ipythonblocks._RGB.format(bp.red, bp.green, bp.blue) 42 | td = ipythonblocks._TD.format(title, bp.size, rgb) 43 | 44 | assert bp._td == td 45 | 46 | 47 | def test_str1(basic_pixel): 48 | """ 49 | Test the Block.__str__ method used with print. 50 | 51 | """ 52 | bp = basic_pixel 53 | 54 | s = os.linesep.join(['Pixel', 'Color: (5, 6, 7)']) 55 | 56 | assert bp.__str__() == s 57 | 58 | 59 | def test_str2(basic_pixel): 60 | """ 61 | Test the Block.__str__ method used with print. 62 | 63 | """ 64 | bp = basic_pixel 65 | bp._row = 8 66 | bp._col = 9 67 | 68 | s = os.linesep.join(['Pixel [9, 8]', 'Color: (5, 6, 7)']) 69 | 70 | assert bp.__str__() == s 71 | -------------------------------------------------------------------------------- /ipythonblocks/test/test_images.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the ability to save images of grids. 3 | 4 | """ 5 | import os 6 | import tempfile 7 | 8 | import mock 9 | import pytest 10 | from PIL import Image 11 | 12 | from ..ipythonblocks import BlockGrid, ImageGrid, colors 13 | 14 | 15 | @pytest.fixture 16 | def bg(): 17 | grid = BlockGrid(2, 2, block_size=1, lines_on=False) 18 | grid[0, 0] = colors.Yellow 19 | grid[0, 1] = colors.Blue 20 | grid[1, 0] = colors.Red 21 | grid[1, 1] = colors.Green 22 | return grid 23 | 24 | 25 | @pytest.fixture 26 | def ig(): 27 | grid = ImageGrid(2, 2, block_size=1, lines_on=False, origin='lower-left') 28 | grid[0, 0] = colors.Red 29 | grid[0, 1] = colors.Yellow 30 | grid[1, 0] = colors.Green 31 | grid[1, 1] = colors.Blue 32 | return grid 33 | 34 | 35 | @pytest.fixture 36 | def bg_png(request): 37 | name = tempfile.NamedTemporaryFile(suffix='.png').name 38 | 39 | def fin(): 40 | os.remove(name) 41 | request.addfinalizer(fin) 42 | 43 | return name 44 | 45 | 46 | @pytest.fixture 47 | def ig_png(request): 48 | name = tempfile.NamedTemporaryFile(suffix='.png').name 49 | 50 | def fin(): 51 | os.remove(name) 52 | request.addfinalizer(fin) 53 | 54 | return name 55 | 56 | 57 | def test_fixtures_equal(bg, ig): 58 | assert bg == ig 59 | 60 | 61 | def test_save_image(bg, ig, bg_png, ig_png): 62 | bg.save_image(bg_png) 63 | ig.save_image(ig_png) 64 | 65 | bg_im = Image.open(bg_png) 66 | ig_im = Image.open(ig_png) 67 | 68 | for im in (bg_im, ig_im): 69 | assert im.format == 'PNG' 70 | assert im.size == (bg.width, bg.height) 71 | 72 | for bg_block, bg_pix, ig_pix in zip(bg, bg_im.getdata(), ig_im.getdata()): 73 | assert bg_block.rgb == bg_pix 74 | assert bg_block.rgb == ig_pix 75 | 76 | 77 | def test_show_image(bg, ig): 78 | from .. import ipythonblocks 79 | 80 | with mock.patch.object(ipythonblocks, 'display') as display: 81 | bg.show_image() 82 | display.assert_called_once() 83 | 84 | with mock.patch.object(ipythonblocks, 'display') as display: 85 | ig.show_image() 86 | display.assert_called_once() 87 | -------------------------------------------------------------------------------- /demos/learning_colors.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "learning_colors" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "heading", 12 | "level": 1, 13 | "metadata": {}, 14 | "source": [ 15 | "Learning Colors with show_color and embed_colorpicker" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "A prerequisite to playing with [`ipythonblocks`](https://github.com/jiffyclub/ipythonblocks) is understanding how RGB colors work. To make it easy for students to experiment with different combinations of RGB `ipythonblocks` includes a `show_color` function that just shows a big stripe of a given color." 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "collapsed": false, 28 | "input": [ 29 | "from ipythonblocks import show_color, embed_colorpicker" 30 | ], 31 | "language": "python", 32 | "metadata": {}, 33 | "outputs": [], 34 | "prompt_number": 1 35 | }, 36 | { 37 | "cell_type": "code", 38 | "collapsed": false, 39 | "input": [ 40 | "show_color(12, 123, 234)" 41 | ], 42 | "language": "python", 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "html": [ 47 | "
" 48 | ], 49 | "output_type": "display_data", 50 | "text": [ 51 | "" 52 | ] 53 | } 54 | ], 55 | "prompt_number": 2 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "If students are expected to have an internet connection the `embed_colorpicker` function can be used to embed the website [ColorPicker.com](http://www.colorpicker.com/) in the Notebook." 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "collapsed": false, 67 | "input": [ 68 | "embed_colorpicker()" 69 | ], 70 | "language": "python", 71 | "metadata": {}, 72 | "outputs": [ 73 | { 74 | "html": [ 75 | "" 76 | ], 77 | "output_type": "display_data", 78 | "text": [ 79 | "" 80 | ] 81 | } 82 | ], 83 | "prompt_number": 3 84 | }, 85 | { 86 | "cell_type": "code", 87 | "collapsed": false, 88 | "input": [], 89 | "language": "python", 90 | "metadata": {}, 91 | "outputs": [] 92 | } 93 | ], 94 | "metadata": {} 95 | } 96 | ] 97 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,visualstudiocode,jupyternotebooks 3 | # Edit at https://www.gitignore.io/?templates=python,visualstudiocode,jupyternotebooks 4 | 5 | ### JupyterNotebooks ### 6 | # gitignore template for Jupyter Notebooks 7 | # website: http://jupyter.org/ 8 | 9 | .ipynb_checkpoints 10 | */.ipynb_checkpoints/* 11 | 12 | # Remove previous ipynb_checkpoints 13 | # git rm -r .ipynb_checkpoints/ 14 | # 15 | 16 | ### Python ### 17 | # Byte-compiled / optimized / DLL files 18 | __pycache__/ 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | pip-wheel-metadata/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | 69 | # Translations 70 | *.mo 71 | *.pot 72 | 73 | # Django stuff: 74 | *.log 75 | local_settings.py 76 | db.sqlite3 77 | db.sqlite3-journal 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | target/ 91 | 92 | # Jupyter Notebook 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | .python-version 100 | 101 | # pipenv 102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 105 | # install all needed dependencies. 106 | #Pipfile.lock 107 | 108 | # celery beat schedule file 109 | celerybeat-schedule 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | ### VisualStudioCode ### 142 | .vscode/* 143 | !.vscode/settings.json 144 | !.vscode/tasks.json 145 | !.vscode/launch.json 146 | !.vscode/extensions.json 147 | 148 | ### VisualStudioCode Patch ### 149 | # Ignore all local history of files 150 | .history 151 | 152 | # End of https://www.gitignore.io/api/python,visualstudiocode,jupyternotebooks 153 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | v 1.9 2 | ===== 3 | 4 | * Added ``show_color_triple`` function as a convenience for displaying 5 | (red, green, blue) triplets without breaking them out into components 6 | * Added ``Color`` wrapper for colors in the ``colors`` and ``fui_colors`` 7 | dictionaries to support ``.red``, ``.green``, and ``.blue`` attribute 8 | access for the component colors 9 | 10 | v 1.8 11 | ===== 12 | 13 | * Add ``clear`` function 14 | * Use ``clear_output(wait=True)`` to avoid flickering in animations 15 | 16 | v 1.7 17 | ===== 18 | 19 | * Added ``show_image`` and ``save_image`` for embedding an image in the 20 | Notebook and saving an image to a file, respectively. 21 | 22 | v 1.6 23 | ===== 24 | 25 | * Added ``post_to_web`` and ``from_web`` methods for for communication 26 | with ipythonblocks.org. 27 | * Added a ``display_time`` keyword to the ``flash()`` method. 28 | * Changed the ``animate`` property to a method so that it can take 29 | a ``stop_time`` keyword to control the amount of time between loop steps. 30 | * Fixed an error with improperly set attributes on views of grids. 31 | * Fixed negative indexing in lower-left origin ImageGrids. 32 | * Fixed an error that allowed too-large indices in lower-left origin ImageGrids. 33 | * Allow color assignment from Blocks/Pixels, not just tuples. (Thanks @wolever.) 34 | 35 | v 1.5 36 | ===== 37 | 38 | * Fixed an issue with an index of -1 in a 2D slice. 39 | * Fixed a display issue with show_color on Firefox. 40 | * Added a dictionary of HTML colors called ``colors`` 41 | 42 | v 1.4 43 | ===== 44 | 45 | * The lines between cells can be toggled on and off. Turning the lines 46 | off can sometimes improve the aesthetics of grids, especially at small 47 | block sizes. 48 | * Added a ``BlockGrid.to_text`` method for easily sending grid data to a file. 49 | * Added a ``show_color`` function that just shows a stripe of a given color. 50 | * Added an ``embed_colorpicker`` function that embeds the website 51 | http://www.colorpicker.com/ in the Notebook. 52 | 53 | v 1.3 54 | ===== 55 | 56 | * Added ``BlockGrid.animate`` attribute and ``BlockGrid.flash`` method 57 | to facilitate animation of grid changes in the Notebook. 58 | * Added `Pixel` and `ImageGrid` subclasses that behave more like image 59 | manipulation libraries. 60 | * Added cell titles so hover the mouse over a block displays the block 61 | index and color. 62 | 63 | v 1.2 64 | ===== 65 | 66 | * ``Block.row`` and ``Block.col`` are now set by their containing 67 | ``BlockGrid`` at the time the ``Block`` is returned to the user. 68 | This means that while iterating over a subgrid the first ``Block`` 69 | will have coordinates (0, 0). 70 | * Getting a single row from a ``BlockGrid`` will return a new ``BlockGrid`` 71 | * Raise a TypeError when indexing a ``BlockGrid`` with a length one tuple 72 | * Added a ``Block.colors`` property 73 | 74 | v 1.1.1 75 | ======= 76 | 77 | * Convert assigned floating point numbers to integers for HTML compatibility 78 | * Added the ability to change the size of individual blocks 79 | 80 | v 1.1 81 | ===== 82 | 83 | * Python 3 support (thanks to Thomas Kluyver) 84 | * ``for block in grid`` style iteration 85 | * ``Block`` now has an HTML representation for displaying individual blocks 86 | * ``__str__`` methods on ``Block`` and ``BlockGrid`` 87 | * Ability to control block size via ``block_size`` keyword and attribute 88 | on ``BlockGrid`` 89 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``ipythonblocks`` 2 | ================= 3 | 4 | .. image:: https://travis-ci.org/jiffyclub/ipythonblocks.png?branch=master 5 | :alt: Travis-CI 6 | :target: https://travis-ci.org/jiffyclub/ipythonblocks 7 | 8 | .. image:: https://coveralls.io/repos/jiffyclub/ipythonblocks/badge.png 9 | :alt: Coveralls 10 | :target: https://coveralls.io/r/jiffyclub/ipythonblocks 11 | 12 | .. image:: https://img.shields.io/pypi/v/ipythonblocks.svg 13 | :alt: PyPI 14 | :target: https://pypi.python.org/pypi/ipythonblocks 15 | 16 | ``ipythonblocks`` is a teaching tool for use with the IPython_ Notebook. 17 | It provides a ``BlockGrid`` object whose representation is an HTML table. 18 | Individual table cells are represented by ``Block`` objects that have ``.red``, 19 | ``.green``, and ``.blue`` attributes by which the color of that cell can be 20 | specified. 21 | 22 | ``ipythonblocks`` allows students to experiment with Python flow control concepts 23 | and immediately see the effects of their code represented in a colorful, 24 | attractive way. ``BlockGrid`` objects can be indexed and sliced like 2D NumPy 25 | arrays making them good practice for learning how to access arrays. 26 | 27 | See these demo notebooks for more on using ``ipythonblocks``: 28 | 29 | * `Practicing with RGB`_ 30 | * `Basic usage`_ with explanations 31 | * `Fun examples`_ 32 | * `Animation`_ 33 | * `ImageGrid`_ 34 | * Going from a `JPEG to BlockGrid`_ and text 35 | * A programatic `firework animation`_ 36 | * `Using ipythonblocks.org`_ 37 | 38 | Install 39 | ------- 40 | 41 | ``ipythonblocks`` can be installed with ``pip``:: 42 | 43 | pip install ipythonblocks 44 | 45 | However, the package is contained in a single ``.py`` file and if you prefer 46 | you can just grab `ipythonblocks.py`_ and copy it to wherever you 47 | want to use it (useful for packaging with other teaching materials). 48 | 49 | Dependencies 50 | ------------ 51 | 52 | Required dependencies: 53 | 54 | * Python_ >= 2.7 55 | * IPython_ 56 | 57 | Optional dependencies: 58 | 59 | * requests_ >= 1.0 (for posting and getting to/from `ipythonblocks.org`_) 60 | * Pillow_ (for creating images) 61 | 62 | Testing dependencies: 63 | 64 | * pytest_ >= 2.3, (for the test suite, see below) 65 | * responses_ >= 0.1 66 | * mock_ (dependency of responses) 67 | 68 | Demo dependencies: 69 | 70 | * PIL_ (for ``starry_night_to_text.ipynb``) 71 | 72 | 73 | Testing 74 | ------- 75 | 76 | The test suite is written using pytest_, so you can run the test suite 77 | with:: 78 | 79 | py.test 80 | 81 | .. _IPython: http://ipython.org 82 | .. _Practicing with RGB: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/learning_colors.ipynb 83 | .. _Basic usage: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/ipythonblocks_demo.ipynb 84 | .. _Fun examples: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/ipythonblocks_fun.ipynb 85 | .. _Animation: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/ipythonblocks_animation.ipynb 86 | .. _ImageGrid: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/ipythonblocks_imagegrid.ipynb 87 | .. _JPEG to BlockGrid: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/starry_night_to_text.ipynb 88 | .. _firework animation: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/Firework.ipynb 89 | .. _Using ipythonblocks.org: http://nbviewer.ipython.org/urls/raw.github.com/jiffyclub/ipythonblocks/master/demos/ipythonblocks_org_demo.ipynb 90 | .. _ipythonblocks.py: https://github.com/jiffyclub/ipythonblocks/blob/master/ipythonblocks/ipythonblocks.py 91 | .. _Python: http://python.org/ 92 | .. _pytest: http://pytest.org/ 93 | .. _requests: http://docs.python-requests.org/en/latest/ 94 | .. _PIL: http://www.pythonware.com/products/pil/ 95 | .. _Pillow: https://python-pillow.org/ 96 | .. _responses: https://github.com/dropbox/responses 97 | .. _mock: http://www.voidspace.org.uk/python/mock/ 98 | .. _ipythonblocks.org: http://www.ipythonblocks.org 99 | -------------------------------------------------------------------------------- /ipythonblocks/test/test_imagegrid.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .. import ipythonblocks 4 | 5 | 6 | @pytest.fixture 7 | def upper_left(): 8 | return ipythonblocks.ImageGrid(2, 3, (7, 8, 9), 20, True, 'upper-left') 9 | 10 | 11 | @pytest.fixture 12 | def lower_left(): 13 | return ipythonblocks.ImageGrid(2, 3, (7, 8, 9), 20, True, 'lower-left') 14 | 15 | 16 | def test_init_bad_origin(): 17 | """ 18 | Test for an error with a bad origin keyword. 19 | 20 | """ 21 | with pytest.raises(ValueError): 22 | ipythonblocks.ImageGrid(5, 6, origin='nowhere') 23 | 24 | 25 | def test_basic_api(upper_left, lower_left): 26 | """ 27 | Test basic interfaces different from BlockGrid. 28 | 29 | """ 30 | ul = upper_left 31 | ll = lower_left 32 | 33 | assert ul.origin == 'upper-left' 34 | assert ll.origin == 'lower-left' 35 | 36 | with pytest.raises(AttributeError): 37 | ul.block_size = 50 38 | 39 | 40 | def test_getitem_bad_index_ul(upper_left): 41 | ul = upper_left 42 | 43 | with pytest.raises(IndexError): 44 | ul[1] 45 | 46 | with pytest.raises(IndexError): 47 | ul[1:] 48 | 49 | with pytest.raises(IndexError): 50 | ul[0, 3] 51 | 52 | with pytest.raises(IndexError): 53 | ul[2, 0] 54 | 55 | 56 | def test_getitem_bad_index_ll(lower_left): 57 | ll = lower_left 58 | 59 | with pytest.raises(IndexError): 60 | ll[1] 61 | 62 | with pytest.raises(IndexError): 63 | ll[1:] 64 | 65 | with pytest.raises(IndexError): 66 | ll[0, 3] 67 | 68 | with pytest.raises(IndexError): 69 | ll[2, 0] 70 | 71 | 72 | def test_setitem_bad_index(upper_left): 73 | ul = upper_left 74 | 75 | with pytest.raises(IndexError): 76 | ul[1] = (4, 5, 6) 77 | 78 | with pytest.raises(IndexError): 79 | ul[1:] = (4, 5, 6) 80 | 81 | 82 | def test_getitem_upper_left_single(upper_left): 83 | ul = upper_left 84 | 85 | for row in range(ul.height): 86 | for col in range(ul.width): 87 | assert ul[col, row] is ul._grid[row][col] 88 | 89 | 90 | def test_getitem_upper_left_slice(upper_left): 91 | ul = upper_left 92 | 93 | ng = ul[:1, :2] 94 | 95 | assert ng.width == 1 96 | assert ng.height == 2 97 | assert ng._grid == [[ul._grid[0][0]], [ul._grid[1][0]]] 98 | 99 | 100 | def test_getitem_lower_left_single(lower_left): 101 | ll = lower_left 102 | 103 | for row in range(ll.height): 104 | for col in range(ll.width): 105 | trow = ll.height - row - 1 106 | assert ll[col, row] is ll._grid[trow][col] 107 | 108 | 109 | def test_getitem_lower_left_single_neg(lower_left): 110 | ll = lower_left 111 | 112 | ll[1, 2] = (1, 2, 3) 113 | 114 | pix = ll[-1, -1] 115 | 116 | assert pix.red == 1 117 | assert pix.green == 2 118 | assert pix.blue == 3 119 | 120 | 121 | def test_getitem_lower_left_slice(lower_left): 122 | ll = lower_left 123 | 124 | ng = ll[:1, :2] 125 | 126 | assert ng.width == 1 127 | assert ng.height == 2 128 | assert ng._grid == [[ll._grid[-2][0]], [ll._grid[-1][0]]] 129 | 130 | 131 | def test_setitem_lower_left_single(lower_left): 132 | ll = lower_left 133 | 134 | ll[0, 1].set_colors(201, 202, 203) 135 | 136 | assert ll._grid[-2][0].red == 201 137 | assert ll._grid[-2][0].green == 202 138 | assert ll._grid[-2][0].blue == 203 139 | 140 | 141 | def test_setitem_lower_left_slice(lower_left): 142 | ll = lower_left 143 | 144 | ll[:, ::2] = (201, 202, 203) 145 | 146 | for pix in ll._grid[0]: 147 | assert pix.red == 201 148 | assert pix.green == 202 149 | assert pix.blue == 203 150 | 151 | for pix in ll._grid[2]: 152 | assert pix.red == 201 153 | assert pix.green == 202 154 | assert pix.blue == 203 155 | 156 | 157 | def test_slice_assignment(lower_left): 158 | ll = lower_left 159 | 160 | ll[1, 1] = (42, 42, 42) 161 | ll[0, 0] = ll[1, 1] 162 | assert ll[0, 0].rgb == (42, 42, 42) 163 | 164 | 165 | def test_setitem_with_grid_ul(upper_left): 166 | og = upper_left.copy() 167 | og[:, :] = (4, 5, 6) 168 | 169 | upper_left[:1, :2] = og[-1:, -2:] 170 | 171 | for b in upper_left: 172 | if b.col < 1 and b.row < 2: 173 | assert b.rgb == (4, 5, 6) 174 | else: 175 | assert b.rgb == (7, 8, 9) 176 | 177 | 178 | def test_setitem_with_grid_ll(lower_left): 179 | og = lower_left.copy() 180 | og[:, :] = (4, 5, 6) 181 | 182 | lower_left[:1, :2] = og[-1:, -2:] 183 | 184 | for b in lower_left: 185 | if b.col < 1 and b.row < 2: 186 | assert b.rgb == (4, 5, 6) 187 | else: 188 | assert b.rgb == (7, 8, 9) 189 | -------------------------------------------------------------------------------- /ipythonblocks/test/test_block.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from .. import ipythonblocks 5 | 6 | 7 | @pytest.fixture 8 | def basic_block(): 9 | return ipythonblocks.Block(5, 6, 7, size=20) 10 | 11 | 12 | def test_basic_api(basic_block): 13 | """ 14 | Check that inputs are going to the right attributes and that assignment 15 | works when it should and not when it shouldn't. 16 | 17 | """ 18 | bb = basic_block 19 | 20 | assert bb.rgb == (5, 6, 7) 21 | 22 | assert bb.red == 5 23 | bb.red = 1 24 | assert bb.red == 1 25 | 26 | assert bb.green == 6 27 | bb.green = 2 28 | assert bb.green == 2 29 | 30 | assert bb.blue == 7 31 | bb.blue = 3 32 | assert bb.blue == 3 33 | 34 | assert bb.rgb == (1, 2, 3) 35 | 36 | assert bb.size == 20 37 | bb.size = 10 38 | assert bb.size == 10 39 | 40 | assert bb.row is None 41 | with pytest.raises(AttributeError): 42 | bb.row = 5 43 | 44 | assert bb.col is None 45 | with pytest.raises(AttributeError): 46 | bb.col = 5 47 | 48 | 49 | def test_attribute_limits(basic_block): 50 | """ 51 | Color and size attributes have some builtin limits, test that they 52 | are respected. 53 | 54 | """ 55 | bb = basic_block 56 | 57 | bb.red = -50 58 | assert bb.red == 0 59 | 60 | bb.green = 1000 61 | assert bb.green == 255 62 | 63 | bb.size = -200 64 | assert bb.size == ipythonblocks._SMALLEST_BLOCK 65 | 66 | 67 | def test_check_value(basic_block): 68 | """ 69 | Test the Block._check_value method that enforces color limits, 70 | converts to int, and checks values are numbers. 71 | 72 | """ 73 | bb = basic_block 74 | 75 | bb.red = 4.56 76 | assert isinstance(bb.red, int) 77 | assert bb.red == 5 78 | 79 | bb.blue = 200.1 80 | assert isinstance(bb.blue, int) 81 | assert bb.blue == 200 82 | 83 | with pytest.raises(ipythonblocks.InvalidColorSpec): 84 | bb.green = 'green' 85 | 86 | 87 | def test_set_colors(basic_block): 88 | """ 89 | Test the Block.set_colors method. 90 | 91 | """ 92 | bb = basic_block 93 | 94 | bb.set_colors(200, 201, 202) 95 | 96 | assert bb.red == 200 97 | assert bb.green == 201 98 | assert bb.blue == 202 99 | 100 | 101 | def test_rgb_attr(basic_block): 102 | """ 103 | Test out the .rgb attribute. 104 | 105 | """ 106 | bb = basic_block 107 | 108 | assert bb.rgb == (5, 6, 7) 109 | 110 | bb.rgb = (1, 2, 3) 111 | assert bb.rgb == (1, 2, 3) 112 | assert bb._red == 1 113 | assert bb._green == 2 114 | assert bb._blue == 3 115 | 116 | with pytest.raises(ValueError): 117 | bb.rgb = (1, 2) 118 | 119 | with pytest.raises(ValueError): 120 | bb.rgb = (4, 5, 6, 7, 8) 121 | 122 | 123 | def test_td(basic_block): 124 | """ 125 | Test the Block._td proerty that returns an HTML table cell. 126 | 127 | """ 128 | bb = basic_block 129 | 130 | bb._row = 1 131 | bb._col = 2 132 | 133 | title = ipythonblocks._TITLE.format(bb._row, bb._col, 134 | bb.red, bb.green, bb.blue) 135 | rgb = ipythonblocks._RGB.format(bb.red, bb.green, bb.blue) 136 | td = ipythonblocks._TD.format(title, bb.size, rgb) 137 | 138 | assert bb._td == td 139 | 140 | 141 | def test_repr_html(basic_block, monkeypatch): 142 | """ 143 | Test the Block._repr_html_ method that returns a single cell HTML table. 144 | 145 | """ 146 | from .test_blockgrid import uuid, fake_uuid 147 | 148 | bb = basic_block 149 | 150 | monkeypatch.setattr(uuid, 'uuid4', fake_uuid) 151 | 152 | table = ipythonblocks._TABLE.format(fake_uuid(), 0, 153 | ipythonblocks._TR.format(bb._td)) 154 | 155 | assert bb._repr_html_() == table 156 | 157 | 158 | def test_str1(basic_block): 159 | """ 160 | Test the Block.__str__ method used with print. 161 | 162 | """ 163 | bb = basic_block 164 | 165 | s = os.linesep.join(['Block', 'Color: (5, 6, 7)']) 166 | 167 | assert bb.__str__() == s 168 | 169 | 170 | def test_str2(basic_block): 171 | """ 172 | Test the Block.__str__ method used with print. 173 | 174 | """ 175 | bb = basic_block 176 | bb._row = 8 177 | bb._col = 9 178 | 179 | s = os.linesep.join(['Block [8, 9]', 'Color: (5, 6, 7)']) 180 | 181 | assert bb.__str__() == s 182 | 183 | def test_repr(basic_block): 184 | assert repr(basic_block) == "Block(5, 6, 7, size=20)" 185 | 186 | def test_eq(): 187 | b1 = ipythonblocks.Block(0, 0, 0) 188 | b2 = ipythonblocks.Block(0, 0, 0) 189 | b3 = ipythonblocks.Block(1, 1, 1) 190 | b4 = ipythonblocks.Block(0, 0, 0, size=30) 191 | 192 | assert b1 == b1 193 | assert b1 == b2 194 | assert b1 != b3 195 | assert b1 != b4 196 | assert b1 != 42 197 | 198 | def test_hash(basic_block): 199 | with pytest.raises(TypeError): 200 | set([basic_block]) 201 | 202 | def test_update(): 203 | b1 = ipythonblocks.Block(0, 0, 0) 204 | b2 = ipythonblocks.Block(1, 1, 1, size=30) 205 | 206 | b1._update((42, 42, 42)) 207 | assert b1.rgb == (42, 42, 42) 208 | 209 | b1._update(b2) 210 | assert b1.rgb == b2.rgb 211 | assert b1.size == b2.size 212 | 213 | with pytest.raises(ValueError): 214 | b1._update((1, 2, 3, 4)) 215 | -------------------------------------------------------------------------------- /demos/ipythonblocks_animation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "ipythonblocks_animation" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "heading", 12 | "level": 1, 13 | "metadata": {}, 14 | "source": [ 15 | "Animation with ipythonblocks" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "For more on `ipythonblocks` see the home page at [https://github.com/jiffyclub/ipythonblocks](https://github.com/jiffyclub/ipythonblocks)." 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "collapsed": false, 28 | "input": [ 29 | "from ipythonblocks import BlockGrid" 30 | ], 31 | "language": "python", 32 | "metadata": {}, 33 | "outputs": [], 34 | "prompt_number": 1 35 | }, 36 | { 37 | "cell_type": "code", 38 | "collapsed": false, 39 | "input": [ 40 | "import time\n", 41 | "from IPython.display import clear_output" 42 | ], 43 | "language": "python", 44 | "metadata": {}, 45 | "outputs": [], 46 | "prompt_number": 2 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "It's possible to do animation of sorts using IPython's `clear_output` function." 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "collapsed": false, 58 | "input": [ 59 | "grid = BlockGrid(3, 3)\n", 60 | "\n", 61 | "previous_block = None\n", 62 | "\n", 63 | "for block in grid:\n", 64 | " clear_output()\n", 65 | " block.green = 255\n", 66 | " \n", 67 | " if previous_block:\n", 68 | " previous_block.green = 0\n", 69 | " \n", 70 | " grid.show()\n", 71 | " \n", 72 | " previous_block = block\n", 73 | " \n", 74 | " time.sleep(0.2)" 75 | ], 76 | "language": "python", 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "html": [ 81 | "
" 82 | ], 83 | "output_type": "display_data", 84 | "text": [ 85 | "" 86 | ] 87 | } 88 | ], 89 | "prompt_number": 3 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "And this is so much fun that it's been added to `ipythonblocks`: one way is to use the `BlockGrid.animate()` method. The `.animate()` method takes\n", 96 | "an optional `stop_time` keyword to control the amount of time between loop steps." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "collapsed": false, 102 | "input": [ 103 | "grid = BlockGrid(3, 3)\n", 104 | "\n", 105 | "previous_block = None\n", 106 | "\n", 107 | "for block in grid.animate():\n", 108 | " block.green = 255\n", 109 | " \n", 110 | " if previous_block:\n", 111 | " previous_block.green = 0\n", 112 | " \n", 113 | " previous_block = block" 114 | ], 115 | "language": "python", 116 | "metadata": {}, 117 | "outputs": [ 118 | { 119 | "html": [ 120 | "
" 121 | ], 122 | "output_type": "display_data", 123 | "text": [ 124 | "" 125 | ] 126 | } 127 | ], 128 | "prompt_number": 4 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "And another way, if not iterating over the grid, is to use the `BlockGrid.flash()` method. `.flash()` takes an optional `display_time` keyword that\n", 135 | "controls for how long each frame is displayed. The default is 0.2 seconds." 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "collapsed": false, 141 | "input": [ 142 | "grid = BlockGrid(3, 3)\n", 143 | "\n", 144 | "previous_block = None\n", 145 | "\n", 146 | "indices = [[(0, 0), (0, 2), (2, 0), (2, 2)], [(0, 1), (1, 0), (1, 2), (2, 1)]] * 10\n", 147 | "\n", 148 | "for ind in indices:\n", 149 | " for i in ind:\n", 150 | " grid[i[0], i[1]].green = 255\n", 151 | " \n", 152 | " grid.flash(display_time=0.1)\n", 153 | " \n", 154 | " for i in ind:\n", 155 | " grid[i[0], i[1]].green = 0\n", 156 | "\n", 157 | "grid.show()" 158 | ], 159 | "language": "python", 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "html": [ 164 | "
" 165 | ], 166 | "output_type": "display_data", 167 | "text": [ 168 | "" 169 | ] 170 | } 171 | ], 172 | "prompt_number": 5 173 | }, 174 | { 175 | "cell_type": "code", 176 | "collapsed": false, 177 | "input": [], 178 | "language": "python", 179 | "metadata": {}, 180 | "outputs": [] 181 | } 182 | ], 183 | "metadata": {} 184 | } 185 | ] 186 | } -------------------------------------------------------------------------------- /ipythonblocks/test/test_ipborg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for ipythonblocks communication with ipythonblocks.org. 3 | 4 | """ 5 | 6 | import json 7 | import string 8 | import sys 9 | 10 | import mock 11 | import pytest 12 | import responses 13 | 14 | from .. import ipythonblocks as ipb 15 | 16 | A10 = [a for a in string.ascii_lowercase[:10]] 17 | 18 | 19 | def setup_module(module): 20 | """ 21 | mock out the get_ipython() function for the tests. 22 | 23 | """ 24 | def get_ipython(): 25 | class ip(object): 26 | user_ns = {'In': A10} 27 | return ip() 28 | ipb.get_ipython = get_ipython 29 | 30 | 31 | def teardown_module(module): 32 | del ipb.get_ipython 33 | 34 | 35 | @pytest.fixture 36 | def data_2x2(): 37 | return [[(1, 2, 3, 4), (5, 6, 7, 8)], 38 | [(9, 10, 11, 12), (13, 14, 15, 16)]] 39 | 40 | 41 | def block_grid(data_2x2): 42 | bg = ipb.BlockGrid(2, 2) 43 | bg[0, 0].rgb = data_2x2[0][0][:3] 44 | bg[0, 0].size = data_2x2[0][0][3] 45 | bg[0, 1].rgb = data_2x2[0][1][:3] 46 | bg[0, 1].size = data_2x2[0][1][3] 47 | bg[1, 0].rgb = data_2x2[1][0][:3] 48 | bg[1, 0].size = data_2x2[1][0][3] 49 | bg[1, 1].rgb = data_2x2[1][1][:3] 50 | bg[1, 1].size = data_2x2[1][1][3] 51 | return bg 52 | 53 | 54 | @pytest.fixture(name='block_grid') 55 | def block_grid_fixture(data_2x2): 56 | return block_grid(data_2x2) 57 | 58 | 59 | def image_grid_ll(data_2x2): 60 | ig = ipb.ImageGrid(2, 2, origin='lower-left') 61 | ig[0, 0].rgb = data_2x2[1][0][:3] 62 | ig[0, 0].size = data_2x2[1][0][3] 63 | ig[0, 1].rgb = data_2x2[0][0][:3] 64 | ig[0, 1].size = data_2x2[0][0][3] 65 | ig[1, 0].rgb = data_2x2[1][1][:3] 66 | ig[1, 0].size = data_2x2[1][1][3] 67 | ig[1, 1].rgb = data_2x2[0][1][:3] 68 | ig[1, 1].size = data_2x2[0][1][3] 69 | return ig 70 | 71 | 72 | @pytest.fixture(name='image_grid_ll') 73 | def image_grid_ll_fixture(data_2x2): 74 | return image_grid_ll(data_2x2) 75 | 76 | 77 | def image_grid_ul(data_2x2): 78 | ig = ipb.ImageGrid(2, 2, origin='upper-left') 79 | ig[0, 0].rgb = data_2x2[0][0][:3] 80 | ig[0, 0].size = data_2x2[0][0][3] 81 | ig[0, 1].rgb = data_2x2[1][0][:3] 82 | ig[0, 1].size = data_2x2[1][0][3] 83 | ig[1, 0].rgb = data_2x2[0][1][:3] 84 | ig[1, 0].size = data_2x2[0][1][3] 85 | ig[1, 1].rgb = data_2x2[1][1][:3] 86 | ig[1, 1].size = data_2x2[1][1][3] 87 | return ig 88 | 89 | 90 | @pytest.fixture(name='image_grid_ul') 91 | def image_grid_ul_fixture(data_2x2): 92 | return image_grid_ul(data_2x2) 93 | 94 | 95 | class Test_parse_cells_spec(object): 96 | def test_single_int(self): 97 | assert ipb._parse_cells_spec(5, 100) == [5] 98 | 99 | def test_single_int_str(self): 100 | assert ipb._parse_cells_spec('5', 100) == [5] 101 | 102 | def test_multi_int_str(self): 103 | assert ipb._parse_cells_spec('2,9,4', 100) == [2, 4, 9] 104 | 105 | def test_slice(self): 106 | assert ipb._parse_cells_spec(slice(2, 5), 100) == [2, 3, 4] 107 | 108 | def test_slice_str(self): 109 | assert ipb._parse_cells_spec('2:5', 100) == [2, 3, 4] 110 | 111 | def test_slice_and_int(self): 112 | assert ipb._parse_cells_spec('4,9:12', 100) == [4, 9, 10, 11] 113 | assert ipb._parse_cells_spec('9:12,4', 100) == [4, 9, 10, 11] 114 | assert ipb._parse_cells_spec('4,9:12,16', 100) == [4, 9, 10, 11, 16] 115 | assert ipb._parse_cells_spec('10,9:12', 100) == [9, 10, 11] 116 | 117 | 118 | class Test_get_code_cells(object): 119 | def test_single_int(self): 120 | assert ipb._get_code_cells(5) == [A10[5]] 121 | 122 | def test_single_int_str(self): 123 | assert ipb._get_code_cells('5') == [A10[5]] 124 | 125 | def test_multi_int_str(self): 126 | assert ipb._get_code_cells('2,9,4') == [A10[x] for x in [2, 4, 9]] 127 | 128 | def test_slice(self): 129 | assert ipb._get_code_cells(slice(2, 5)) == [A10[x] for x in [2, 3, 4]] 130 | 131 | def test_slice_str(self): 132 | assert ipb._get_code_cells('2:5') == [A10[x] for x in [2, 3, 4]] 133 | 134 | def test_slice_and_int(self): 135 | assert ipb._get_code_cells('1,3:6') == [A10[x] for x in [1, 3, 4, 5]] 136 | assert ipb._get_code_cells('3:6,1') == [A10[x] for x in [1, 3, 4, 5]] 137 | assert ipb._get_code_cells('1,3:6,8') == [A10[x] for x in [1, 3, 4, 5, 8]] 138 | assert ipb._get_code_cells('4,3:6') == [A10[x] for x in [3, 4, 5]] 139 | 140 | 141 | @pytest.mark.parametrize( 142 | 'fixture', 143 | [block_grid, image_grid_ll, image_grid_ul] 144 | ) 145 | def test_to_simple_grid(fixture, data_2x2): 146 | grid = fixture(data_2x2) 147 | assert grid._to_simple_grid() == data_2x2 148 | 149 | 150 | @pytest.mark.parametrize( 151 | 'test_grid, ref_grid', 152 | [(ipb.BlockGrid(2, 2), block_grid), 153 | (ipb.ImageGrid(2, 2, origin='upper-left'), image_grid_ul), 154 | (ipb.ImageGrid(2, 2, origin='lower-left'), image_grid_ll)] 155 | ) 156 | def test_load_simple_grid(test_grid, ref_grid, data_2x2): 157 | ref_grid = ref_grid(data_2x2) 158 | test_grid._load_simple_grid(data_2x2) 159 | assert test_grid == ref_grid 160 | 161 | 162 | @responses.activate 163 | @mock.patch.object(ipb, '__version__', 'ipb_version') 164 | @mock.patch.object(ipb, '_POST_URL', 'http://www.ipythonblocks.org/post_url') 165 | def test_BlockGrid_post_to_web(data_2x2, block_grid): 166 | expected = { 167 | 'python_version': tuple(sys.version_info), 168 | 'ipb_version': ipb.__version__, 169 | 'ipb_class': 'BlockGrid', 170 | 'code_cells': None, 171 | 'secret': False, 172 | 'grid_data': { 173 | 'lines_on': block_grid.lines_on, 174 | 'width': block_grid.width, 175 | 'height': block_grid.height, 176 | 'blocks': data_2x2 177 | } 178 | } 179 | expected = json.dumps(expected) 180 | 181 | responses.add(responses.POST, ipb._POST_URL, 182 | body=json.dumps({'url': 'url'}).encode('utf-8'), 183 | status=200, content_type='application/json') 184 | 185 | url = block_grid.post_to_web() 186 | 187 | assert url == 'url' 188 | assert len(responses.calls) == 1 189 | 190 | req = responses.calls[0].request 191 | assert req.url == ipb._POST_URL 192 | assert req.body == expected 193 | 194 | 195 | @responses.activate 196 | @mock.patch.object(ipb, '__version__', 'ipb_version') 197 | @mock.patch.object(ipb, '_POST_URL', 'http://www.ipythonblocks.org/post_url') 198 | def test_ImageGrid_ul_post_to_web(data_2x2, image_grid_ul): 199 | expected = { 200 | 'python_version': tuple(sys.version_info), 201 | 'ipb_version': ipb.__version__, 202 | 'ipb_class': 'ImageGrid', 203 | 'code_cells': None, 204 | 'secret': False, 205 | 'grid_data': { 206 | 'lines_on': image_grid_ul.lines_on, 207 | 'width': image_grid_ul.width, 208 | 'height': image_grid_ul.height, 209 | 'blocks': data_2x2 210 | } 211 | } 212 | expected = json.dumps(expected) 213 | 214 | responses.add(responses.POST, ipb._POST_URL, 215 | body=json.dumps({'url': 'url'}).encode('utf-8'), 216 | status=200, content_type='application/json') 217 | 218 | url = image_grid_ul.post_to_web() 219 | 220 | assert url == 'url' 221 | assert len(responses.calls) == 1 222 | 223 | req = responses.calls[0].request 224 | assert req.url == ipb._POST_URL 225 | assert req.body == expected 226 | 227 | 228 | @responses.activate 229 | @mock.patch.object(ipb, '_GET_URL_PUBLIC', 'http://www.ipythonblocks.org/get_url/{0}') 230 | def test_BlockGrid_from_web(data_2x2): 231 | grid_id = 'abc' 232 | get_url = ipb._GET_URL_PUBLIC.format(grid_id) 233 | resp = { 234 | 'lines_on': True, 235 | 'width': 2, 236 | 'height': 2, 237 | 'blocks': data_2x2 238 | } 239 | 240 | responses.add(responses.GET, get_url, 241 | body=json.dumps(resp).encode('utf-8'), status=200, 242 | content_type='application/json') 243 | 244 | grid = ipb.BlockGrid.from_web(grid_id) 245 | 246 | assert grid.height == resp['height'] 247 | assert grid.width == resp['width'] 248 | assert grid.lines_on == resp['lines_on'] 249 | assert grid._to_simple_grid() == data_2x2 250 | 251 | assert len(responses.calls) == 1 252 | assert responses.calls[0].request.url == get_url 253 | 254 | 255 | @responses.activate 256 | @mock.patch.object(ipb, '_GET_URL_SECRET', 'http://www.ipythonblocks.org/get_url/{0}') 257 | def test_ImageGrid_ul_from_web(data_2x2): 258 | grid_id = 'abc' 259 | get_url = ipb._GET_URL_SECRET.format(grid_id) 260 | resp = { 261 | 'lines_on': True, 262 | 'width': 2, 263 | 'height': 2, 264 | 'blocks': data_2x2 265 | } 266 | 267 | responses.add(responses.GET, get_url, 268 | body=json.dumps(resp).encode('utf-8'), status=200, 269 | content_type='application/json') 270 | 271 | origin = 'upper-left' 272 | grid = ipb.ImageGrid.from_web(grid_id, secret=True, origin=origin) 273 | 274 | assert grid.height == resp['height'] 275 | assert grid.width == resp['width'] 276 | assert grid.lines_on == resp['lines_on'] 277 | assert grid._to_simple_grid() == data_2x2 278 | assert grid.origin == origin 279 | 280 | assert len(responses.calls) == 1 281 | assert responses.calls[0].request.url == get_url 282 | -------------------------------------------------------------------------------- /ipythonblocks/test/test_blockgrid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import pytest 4 | 5 | from .. import ipythonblocks 6 | 7 | 8 | def fake_uuid(): 9 | return 'abc' 10 | 11 | 12 | @pytest.fixture 13 | def basic_grid(): 14 | return ipythonblocks.BlockGrid(5, 6, (1, 2, 3), 20, True) 15 | 16 | 17 | def test_basic_api(basic_grid): 18 | """ 19 | Check that inputs are going to the right attributes and that assignment 20 | works when it should and not when it shouldn't. 21 | 22 | """ 23 | bg = basic_grid 24 | 25 | assert bg.width == 5 26 | with pytest.raises(AttributeError): 27 | bg.width = 20 28 | 29 | assert bg.height == 6 30 | with pytest.raises(AttributeError): 31 | bg.height = 20 32 | 33 | assert bg.shape == (5, 6) 34 | assert bg.block_size == 20 35 | assert bg.lines_on is True 36 | 37 | 38 | def test_grid_init(basic_grid): 39 | """ 40 | Test that the grid is properly initialized. 41 | 42 | """ 43 | bg = basic_grid 44 | 45 | for r in range(bg.height): 46 | for c in range(bg.width): 47 | assert bg[r, c].size == 20 48 | assert bg[r, c].red == 1 49 | assert bg[r, c].green == 2 50 | assert bg[r, c].blue == 3 51 | assert bg[r, c].row == r 52 | assert bg[r, c].col == c 53 | 54 | 55 | def test_change_block_size(basic_grid): 56 | """ 57 | Test that all blocks are properly resized when changing the 58 | BlockGrid.block_size attribute. 59 | 60 | """ 61 | bg = basic_grid 62 | 63 | bg.block_size = 10 64 | assert bg.block_size == 10 65 | 66 | for block in bg: 67 | assert block.size == 10 68 | 69 | 70 | def test_change_lines_on(basic_grid): 71 | """ 72 | Test changing the BlockGrid.lines_on attribute. 73 | 74 | """ 75 | bg = basic_grid 76 | 77 | assert bg.lines_on is True 78 | 79 | bg.lines_on = False 80 | assert bg.lines_on is False 81 | 82 | with pytest.raises(ValueError): 83 | bg.lines_on = 5 84 | 85 | with pytest.raises(ValueError): 86 | bg.lines_on = 'asdf' 87 | 88 | 89 | def test_view(basic_grid): 90 | """ 91 | Check that getting a new BlockGrid object via slicing returns a view 92 | and not a copy. 93 | 94 | """ 95 | bg = basic_grid 96 | ng = bg[:2, :2] 97 | 98 | ng[1, 1].set_colors(200, 201, 202) 99 | 100 | for block in (ng[1, 1], bg[1, 1]): 101 | assert block.red == 200 102 | assert block.green == 201 103 | assert block.blue == 202 104 | 105 | 106 | def test_view_coords(basic_grid): 107 | """ 108 | Make sure that when we get a view that it has its own appropriate 109 | coordinates. 110 | 111 | """ 112 | ng = basic_grid[-2:, -2:] 113 | 114 | coords = ((0, 0), (0, 1), (1, 0), (1, 1)) 115 | 116 | for b, c in zip(ng, coords): 117 | assert b.row == c[0] 118 | assert b.col == c[1] 119 | 120 | 121 | def test_copy(basic_grid): 122 | """ 123 | Check that getting a new BlockGrid via BlockGrid.copy returns a totally 124 | independent copy and not a view. 125 | 126 | """ 127 | bg = basic_grid 128 | ng = bg[:2, :2].copy() 129 | 130 | ng[1, 1].set_colors(200, 201, 202) 131 | 132 | assert ng[1, 1].red == 200 133 | assert ng[1, 1].green == 201 134 | assert ng[1, 1].blue == 202 135 | assert bg[1, 1].red == 1 136 | assert bg[1, 1].green == 2 137 | assert bg[1, 1].blue == 3 138 | 139 | 140 | def test_str(basic_grid): 141 | """ 142 | Test the BlockGrid.__str__ method used with print. 143 | 144 | """ 145 | bg = basic_grid 146 | 147 | s = os.linesep.join(['BlockGrid', 'Shape: (5, 6)']) 148 | 149 | assert bg.__str__() == s 150 | 151 | 152 | def test_repr_html(monkeypatch): 153 | """ 154 | HTML repr should be the same for a 1, 1 BlockGrid as for a single Block. 155 | (As long as the BlockGrid border is off.) 156 | 157 | """ 158 | bg = ipythonblocks.BlockGrid(1, 1, lines_on=False) 159 | 160 | monkeypatch.setattr(uuid, 'uuid4', fake_uuid) 161 | 162 | assert bg._repr_html_() == bg[0, 0]._repr_html_() 163 | 164 | 165 | def test_iter(): 166 | """ 167 | Test that we do complete, row first iteration. 168 | 169 | """ 170 | bg = ipythonblocks.BlockGrid(2, 2) 171 | 172 | coords = ((0, 0), (0, 1), (1, 0), (1, 1)) 173 | 174 | for b, c in zip(bg, coords): 175 | assert b.row == c[0] 176 | assert b.col == c[1] 177 | 178 | 179 | def test_bad_index(basic_grid): 180 | """ 181 | Test for the correct errors with bad indices. 182 | 183 | """ 184 | bg = basic_grid 185 | 186 | with pytest.raises(IndexError): 187 | bg[1, 2, 3, 4] 188 | 189 | with pytest.raises(IndexError): 190 | bg[{4: 5}] 191 | 192 | with pytest.raises(TypeError): 193 | bg[1, ] 194 | 195 | with pytest.raises(IndexError): 196 | bg[0, 5] 197 | 198 | with pytest.raises(IndexError): 199 | bg[6, 0] 200 | 201 | 202 | def test_bad_colors(basic_grid): 203 | """ 204 | Make sure this gets the right error when trying to assign something 205 | other than three integers. 206 | 207 | """ 208 | with pytest.raises(ValueError): 209 | basic_grid[0, 0] = (1, 2, 3, 4) 210 | 211 | 212 | def test_getitem(basic_grid): 213 | """ 214 | Exercise a bunch of different indexing. 215 | 216 | """ 217 | bg = basic_grid 218 | 219 | # single block 220 | block = bg[1, 2] 221 | 222 | assert isinstance(block, ipythonblocks.Block) 223 | assert block.row == 1 224 | assert block.col == 2 225 | 226 | # single row 227 | ng = bg[2] 228 | 229 | assert isinstance(ng, ipythonblocks.BlockGrid) 230 | assert ng.shape == (bg.width, 1) 231 | 232 | # two rows 233 | ng = bg[1:3] 234 | 235 | assert isinstance(ng, ipythonblocks.BlockGrid) 236 | assert ng.shape == (bg.width, 2) 237 | 238 | # one row via a slice 239 | ng = bg[2, :] 240 | 241 | assert isinstance(ng, ipythonblocks.BlockGrid) 242 | assert ng.shape == (bg.width, 1) 243 | 244 | # one column 245 | ng = bg[:, 2] 246 | 247 | assert isinstance(ng, ipythonblocks.BlockGrid) 248 | assert ng.shape == (1, bg.height) 249 | 250 | # 2 x 2 subgrid 251 | ng = bg[:2, :2] 252 | 253 | assert isinstance(ng, ipythonblocks.BlockGrid) 254 | assert ng.shape == (2, 2) 255 | 256 | # strided slicing 257 | ng = bg[::3, ::3] 258 | 259 | assert isinstance(ng, ipythonblocks.BlockGrid) 260 | assert ng.shape == (2, 2) 261 | 262 | # one column / one row with a -1 index 263 | # testing fix for #7 264 | ng = bg[-1, :] 265 | 266 | assert isinstance(ng, ipythonblocks.BlockGrid) 267 | assert ng.shape == (bg.width, 1) 268 | 269 | ng = bg[1:4, -1] 270 | 271 | assert isinstance(ng, ipythonblocks.BlockGrid) 272 | assert ng.shape == (1, 3) 273 | 274 | 275 | def test_setitem(basic_grid): 276 | """ 277 | Test assigning colors to blocks. 278 | 279 | """ 280 | bg = basic_grid 281 | colors = (21, 22, 23) 282 | 283 | # single block 284 | bg[0, 0] = colors 285 | assert bg[0, 0].rgb == colors 286 | 287 | # single row 288 | bg[2] = colors 289 | for block in bg[2]: 290 | assert block.rgb == colors 291 | 292 | # two rows 293 | bg[3:5] = colors 294 | for block in bg[3:5]: 295 | assert block.rgb == colors 296 | 297 | # one row via a slice 298 | bg[1, :] = colors 299 | for block in bg[1, :]: 300 | assert block.rgb == colors 301 | 302 | # one column 303 | bg[:, 5] = colors 304 | for block in bg[:, 5]: 305 | assert block.rgb == colors 306 | 307 | # 2 x 2 subgrid 308 | bg[:2, :2] = colors 309 | for block in bg[:2, :2]: 310 | assert block.rgb == colors 311 | 312 | # strided slicing 313 | bg[::3, ::3] = colors 314 | for block in bg[::3, ::3]: 315 | assert block.rgb == colors 316 | 317 | 318 | def test_setitem_to_block(basic_grid): 319 | """ 320 | Test assigning a Block to a BlockGrid. 321 | """ 322 | bg = basic_grid 323 | bg[0, 0] = (0, 0, 0) 324 | bg[1, 1] = bg[0, 0] 325 | assert bg[0, 0] == bg[1, 1] 326 | assert bg[1, 1].rgb == (0, 0, 0) 327 | 328 | 329 | def test_setitem_with_grid(basic_grid): 330 | og = basic_grid.copy() 331 | og[:] = (4, 5, 6) 332 | 333 | basic_grid[:1, :2] = og[-1:, -2:] 334 | 335 | for b in basic_grid: 336 | if b.row < 1 and b.col < 2: 337 | assert b.rgb == (4, 5, 6) 338 | else: 339 | assert b.rgb == (1, 2, 3) 340 | 341 | 342 | def test_setitem_raises(basic_grid): 343 | og = basic_grid.copy() 344 | 345 | with pytest.raises(ipythonblocks.ShapeMismatch): 346 | basic_grid[:, :] = og[:2, :2] 347 | 348 | with pytest.raises(TypeError): 349 | basic_grid[0, 0] = og[:2, :2] 350 | 351 | 352 | def test_to_text(capsys): 353 | """ 354 | Test using the BlockGrid.to_text method. 355 | 356 | """ 357 | bg = ipythonblocks.BlockGrid(2, 1, block_size=20) 358 | 359 | bg[0, 0].rgb = (1, 2, 3) 360 | bg[0, 1].rgb = (4, 5, 6) 361 | 362 | ref = ['# width height', 363 | '2 1', 364 | '# block size', 365 | '20', 366 | '# initial color', 367 | '0 0 0', 368 | '# row column red green blue', 369 | '0 0 1 2 3', 370 | '0 1 4 5 6'] 371 | ref = os.linesep.join(ref) + os.linesep 372 | 373 | bg.to_text() 374 | out, err = capsys.readouterr() 375 | 376 | assert out == ref 377 | -------------------------------------------------------------------------------- /ez_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Bootstrap setuptools installation 3 | 4 | To use setuptools in your package's setup.py, include this 5 | file in the same directory and add this to the top of your setup.py:: 6 | 7 | from ez_setup import use_setuptools 8 | use_setuptools() 9 | 10 | To require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, simply supply 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import shutil 18 | import sys 19 | import tempfile 20 | import tarfile 21 | import optparse 22 | import subprocess 23 | import platform 24 | import textwrap 25 | 26 | from distutils import log 27 | 28 | try: 29 | from site import USER_SITE 30 | except ImportError: 31 | USER_SITE = None 32 | 33 | DEFAULT_VERSION = "2.2" 34 | DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" 35 | 36 | def _python_cmd(*args): 37 | """ 38 | Return True if the command succeeded. 39 | """ 40 | args = (sys.executable,) + args 41 | return subprocess.call(args) == 0 42 | 43 | def _install(tarball, install_args=()): 44 | # extracting the tarball 45 | tmpdir = tempfile.mkdtemp() 46 | log.warn('Extracting in %s', tmpdir) 47 | old_wd = os.getcwd() 48 | try: 49 | os.chdir(tmpdir) 50 | tar = tarfile.open(tarball) 51 | _extractall(tar) 52 | tar.close() 53 | 54 | # going in the directory 55 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 56 | os.chdir(subdir) 57 | log.warn('Now working in %s', subdir) 58 | 59 | # installing 60 | log.warn('Installing Setuptools') 61 | if not _python_cmd('setup.py', 'install', *install_args): 62 | log.warn('Something went wrong during the installation.') 63 | log.warn('See the error message above.') 64 | # exitcode will be 2 65 | return 2 66 | finally: 67 | os.chdir(old_wd) 68 | shutil.rmtree(tmpdir) 69 | 70 | 71 | def _build_egg(egg, tarball, to_dir): 72 | # extracting the tarball 73 | tmpdir = tempfile.mkdtemp() 74 | log.warn('Extracting in %s', tmpdir) 75 | old_wd = os.getcwd() 76 | try: 77 | os.chdir(tmpdir) 78 | tar = tarfile.open(tarball) 79 | _extractall(tar) 80 | tar.close() 81 | 82 | # going in the directory 83 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 84 | os.chdir(subdir) 85 | log.warn('Now working in %s', subdir) 86 | 87 | # building an egg 88 | log.warn('Building a Setuptools egg in %s', to_dir) 89 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 90 | 91 | finally: 92 | os.chdir(old_wd) 93 | shutil.rmtree(tmpdir) 94 | # returning the result 95 | log.warn(egg) 96 | if not os.path.exists(egg): 97 | raise IOError('Could not build the egg.') 98 | 99 | 100 | def _do_download(version, download_base, to_dir, download_delay): 101 | egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' 102 | % (version, sys.version_info[0], sys.version_info[1])) 103 | if not os.path.exists(egg): 104 | tarball = download_setuptools(version, download_base, 105 | to_dir, download_delay) 106 | _build_egg(egg, tarball, to_dir) 107 | sys.path.insert(0, egg) 108 | 109 | # Remove previously-imported pkg_resources if present (see 110 | # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). 111 | if 'pkg_resources' in sys.modules: 112 | del sys.modules['pkg_resources'] 113 | 114 | import setuptools 115 | setuptools.bootstrap_install_from = egg 116 | 117 | 118 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 119 | to_dir=os.curdir, download_delay=15): 120 | to_dir = os.path.abspath(to_dir) 121 | rep_modules = 'pkg_resources', 'setuptools' 122 | imported = set(sys.modules).intersection(rep_modules) 123 | try: 124 | import pkg_resources 125 | except ImportError: 126 | return _do_download(version, download_base, to_dir, download_delay) 127 | try: 128 | pkg_resources.require("setuptools>=" + version) 129 | return 130 | except pkg_resources.DistributionNotFound: 131 | return _do_download(version, download_base, to_dir, download_delay) 132 | except pkg_resources.VersionConflict as VC_err: 133 | if imported: 134 | msg = textwrap.dedent(""" 135 | The required version of setuptools (>={version}) is not available, 136 | and can't be installed while this script is running. Please 137 | install a more recent version first, using 138 | 'easy_install -U setuptools'. 139 | 140 | (Currently using {VC_err.args[0]!r}) 141 | """).format(VC_err=VC_err, version=version) 142 | sys.stderr.write(msg) 143 | sys.exit(2) 144 | 145 | # otherwise, reload ok 146 | del pkg_resources, sys.modules['pkg_resources'] 147 | return _do_download(version, download_base, to_dir, download_delay) 148 | 149 | def _clean_check(cmd, target): 150 | """ 151 | Run the command to download target. If the command fails, clean up before 152 | re-raising the error. 153 | """ 154 | try: 155 | subprocess.check_call(cmd) 156 | except subprocess.CalledProcessError: 157 | if os.access(target, os.F_OK): 158 | os.unlink(target) 159 | raise 160 | 161 | def download_file_powershell(url, target): 162 | """ 163 | Download the file at url to target using Powershell (which will validate 164 | trust). Raise an exception if the command cannot complete. 165 | """ 166 | target = os.path.abspath(target) 167 | cmd = [ 168 | 'powershell', 169 | '-Command', 170 | "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), 171 | ] 172 | _clean_check(cmd, target) 173 | 174 | def has_powershell(): 175 | if platform.system() != 'Windows': 176 | return False 177 | cmd = ['powershell', '-Command', 'echo test'] 178 | devnull = open(os.path.devnull, 'wb') 179 | try: 180 | try: 181 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 182 | except: 183 | return False 184 | finally: 185 | devnull.close() 186 | return True 187 | 188 | download_file_powershell.viable = has_powershell 189 | 190 | def download_file_curl(url, target): 191 | cmd = ['curl', url, '--silent', '--output', target] 192 | _clean_check(cmd, target) 193 | 194 | def has_curl(): 195 | cmd = ['curl', '--version'] 196 | devnull = open(os.path.devnull, 'wb') 197 | try: 198 | try: 199 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 200 | except: 201 | return False 202 | finally: 203 | devnull.close() 204 | return True 205 | 206 | download_file_curl.viable = has_curl 207 | 208 | def download_file_wget(url, target): 209 | cmd = ['wget', url, '--quiet', '--output-document', target] 210 | _clean_check(cmd, target) 211 | 212 | def has_wget(): 213 | cmd = ['wget', '--version'] 214 | devnull = open(os.path.devnull, 'wb') 215 | try: 216 | try: 217 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 218 | except: 219 | return False 220 | finally: 221 | devnull.close() 222 | return True 223 | 224 | download_file_wget.viable = has_wget 225 | 226 | def download_file_insecure(url, target): 227 | """ 228 | Use Python to download the file, even though it cannot authenticate the 229 | connection. 230 | """ 231 | try: 232 | from urllib.request import urlopen 233 | except ImportError: 234 | from urllib2 import urlopen 235 | src = dst = None 236 | try: 237 | src = urlopen(url) 238 | # Read/write all in one block, so we don't create a corrupt file 239 | # if the download is interrupted. 240 | data = src.read() 241 | dst = open(target, "wb") 242 | dst.write(data) 243 | finally: 244 | if src: 245 | src.close() 246 | if dst: 247 | dst.close() 248 | 249 | download_file_insecure.viable = lambda: True 250 | 251 | def get_best_downloader(): 252 | downloaders = [ 253 | download_file_powershell, 254 | download_file_curl, 255 | download_file_wget, 256 | download_file_insecure, 257 | ] 258 | 259 | for dl in downloaders: 260 | if dl.viable(): 261 | return dl 262 | 263 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 264 | to_dir=os.curdir, delay=15, 265 | downloader_factory=get_best_downloader): 266 | """Download setuptools from a specified location and return its filename 267 | 268 | `version` should be a valid setuptools version number that is available 269 | as an egg for download under the `download_base` URL (which should end 270 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 271 | `delay` is the number of seconds to pause before an actual download 272 | attempt. 273 | 274 | ``downloader_factory`` should be a function taking no arguments and 275 | returning a function for downloading a URL to a target. 276 | """ 277 | # making sure we use the absolute path 278 | to_dir = os.path.abspath(to_dir) 279 | tgz_name = "setuptools-%s.tar.gz" % version 280 | url = download_base + tgz_name 281 | saveto = os.path.join(to_dir, tgz_name) 282 | if not os.path.exists(saveto): # Avoid repeated downloads 283 | log.warn("Downloading %s", url) 284 | downloader = downloader_factory() 285 | downloader(url, saveto) 286 | return os.path.realpath(saveto) 287 | 288 | 289 | def _extractall(self, path=".", members=None): 290 | """Extract all members from the archive to the current working 291 | directory and set owner, modification time and permissions on 292 | directories afterwards. `path' specifies a different directory 293 | to extract to. `members' is optional and must be a subset of the 294 | list returned by getmembers(). 295 | """ 296 | import copy 297 | import operator 298 | from tarfile import ExtractError 299 | directories = [] 300 | 301 | if members is None: 302 | members = self 303 | 304 | for tarinfo in members: 305 | if tarinfo.isdir(): 306 | # Extract directories with a safe mode. 307 | directories.append(tarinfo) 308 | tarinfo = copy.copy(tarinfo) 309 | tarinfo.mode = 448 # decimal for oct 0700 310 | self.extract(tarinfo, path) 311 | 312 | # Reverse sort directories. 313 | directories.sort(key=operator.attrgetter('name'), reverse=True) 314 | 315 | # Set correct owner, mtime and filemode on directories. 316 | for tarinfo in directories: 317 | dirpath = os.path.join(path, tarinfo.name) 318 | try: 319 | self.chown(tarinfo, dirpath) 320 | self.utime(tarinfo, dirpath) 321 | self.chmod(tarinfo, dirpath) 322 | except ExtractError as e: 323 | if self.errorlevel > 1: 324 | raise 325 | else: 326 | self._dbg(1, "tarfile: %s" % e) 327 | 328 | 329 | def _build_install_args(options): 330 | """ 331 | Build the arguments to 'python setup.py install' on the setuptools package 332 | """ 333 | return ['--user'] if options.user_install else [] 334 | 335 | def _parse_args(): 336 | """ 337 | Parse the command line for options 338 | """ 339 | parser = optparse.OptionParser() 340 | parser.add_option( 341 | '--user', dest='user_install', action='store_true', default=False, 342 | help='install in user site package (requires Python 2.6 or later)') 343 | parser.add_option( 344 | '--download-base', dest='download_base', metavar="URL", 345 | default=DEFAULT_URL, 346 | help='alternative URL from where to download the setuptools package') 347 | parser.add_option( 348 | '--insecure', dest='downloader_factory', action='store_const', 349 | const=lambda: download_file_insecure, default=get_best_downloader, 350 | help='Use internal, non-validating downloader' 351 | ) 352 | options, args = parser.parse_args() 353 | # positional arguments are ignored 354 | return options 355 | 356 | def main(version=DEFAULT_VERSION): 357 | """Install or upgrade setuptools and EasyInstall""" 358 | options = _parse_args() 359 | tarball = download_setuptools(download_base=options.download_base, 360 | downloader_factory=options.downloader_factory) 361 | return _install(tarball, _build_install_args(options)) 362 | 363 | if __name__ == '__main__': 364 | sys.exit(main()) 365 | -------------------------------------------------------------------------------- /ipythonblocks/ipythonblocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | ipythonblocks provides a BlockGrid class that displays a colored grid in the 3 | IPython Notebook. The colors can be manipulated, making it useful for 4 | practicing control flow stuctures and quickly seeing the results. 5 | 6 | """ 7 | 8 | # This file is copyright 2013 by Matt Davis and covered by the license at 9 | # https://github.com/jiffyclub/ipythonblocks/blob/master/LICENSE.txt 10 | 11 | import copy 12 | import collections 13 | import json 14 | import numbers 15 | import os 16 | import sys 17 | import time 18 | import uuid 19 | 20 | from collections import namedtuple 21 | from operator import iadd 22 | from functools import reduce 23 | 24 | from IPython.display import HTML, IFrame, display, clear_output 25 | from IPython.display import Image as ipyImage 26 | 27 | # This statement is required for compatibility issues with Python 3.10. 28 | # Since that version, Iterable and Sequence must be imported from 29 | # collections.abc, instead of collections 30 | try: 31 | from collections.abc import Iterable 32 | from collections.abc import Sequence 33 | except ImportError: 34 | from collections import Iterable 35 | from collections import Sequence 36 | 37 | __all__ = ( 38 | 'Block', 39 | 'BlockGrid', 40 | 'Pixel', 41 | 'ImageGrid', 42 | 'InvalidColorSpec', 43 | 'ShapeMismatch', 44 | 'show_color', 45 | 'show_color_triple', 46 | 'embed_colorpicker', 47 | 'clear', 48 | 'colors', 49 | 'fui_colors', 50 | '__version__', 51 | ) 52 | __version__ = '1.10.dev' 53 | 54 | _TABLE = ('' 60 | '{2}
') 61 | _TR = '{0}' 62 | _TD = ('') 64 | _RGB = 'rgb({0}, {1}, {2})' 65 | _TITLE = 'Index: [{0}, {1}] Color: ({2}, {3}, {4})' 66 | 67 | _SINGLE_ITEM = 'single item' 68 | _SINGLE_ROW = 'single row' 69 | _ROW_SLICE = 'row slice' 70 | _DOUBLE_SLICE = 'double slice' 71 | 72 | _SMALLEST_BLOCK = 1 73 | 74 | _POST_URL = 'http://www.ipythonblocks.org/post' 75 | _GET_URL_PUBLIC = 'http://www.ipythonblocks.org/get/{0}' 76 | _GET_URL_SECRET = 'http://www.ipythonblocks.org/get/secret/{0}' 77 | 78 | 79 | class InvalidColorSpec(Exception): 80 | """ 81 | Error for a color value that is not a number. 82 | 83 | """ 84 | pass 85 | 86 | 87 | class ShapeMismatch(Exception): 88 | """ 89 | Error for when a grid assigned to another doesn't have the same shape. 90 | 91 | """ 92 | pass 93 | 94 | 95 | def clear(): 96 | """ 97 | Clear the output of the current cell. 98 | 99 | This is a thin wrapper around IPython.display.clear_output. 100 | 101 | """ 102 | clear_output() 103 | 104 | 105 | def show_color(red, green, blue): 106 | """ 107 | Show a given color in the IPython Notebook. 108 | 109 | Parameters 110 | ---------- 111 | red, green, blue : int 112 | Integers on the range [0 - 255]. 113 | 114 | """ 115 | div = ('
') 117 | display(HTML(div.format(_RGB.format(red, green, blue)))) 118 | 119 | 120 | def show_color_triple(rgb_triple): 121 | """ 122 | Show a given color in the IPython Notebook. 123 | 124 | Parameters 125 | ---------- 126 | rgb_triple : iterable 127 | A Python iterable containing three integers in the range [0 - 255] 128 | representing red, green, and blue colors. 129 | 130 | """ 131 | return show_color(*rgb_triple) 132 | 133 | 134 | def embed_colorpicker(): 135 | """ 136 | Embed the web page www.colorpicker.com inside the IPython Notebook. 137 | 138 | """ 139 | display( 140 | IFrame(src='http://www.colorpicker.com/', height='550px', width='100%') 141 | ) 142 | 143 | 144 | def _color_property(name): 145 | real_name = "_" + name 146 | 147 | @property 148 | def prop(self): 149 | return getattr(self, real_name) 150 | 151 | @prop.setter 152 | def prop(self, value): 153 | value = Block._check_value(value) 154 | setattr(self, real_name, value) 155 | 156 | return prop 157 | 158 | 159 | def _flatten(thing, ignore_types=(str,)): 160 | """ 161 | Yield a single item or str/unicode or recursively yield from iterables. 162 | 163 | Adapted from Beazley's Python Cookbook. 164 | 165 | """ 166 | if isinstance(thing, Iterable) and \ 167 | not isinstance(thing, ignore_types): 168 | for i in thing: 169 | for x in _flatten(i): 170 | yield x 171 | else: 172 | yield thing 173 | 174 | 175 | def _parse_str_cell_spec(cells, length): 176 | """ 177 | Parse a single string cell specification representing either a single 178 | integer or a slice. 179 | 180 | Parameters 181 | ---------- 182 | cells : str 183 | E.g. '5' for an int or '5:9' for a slice. 184 | length : int 185 | The number of items in the user's In history list. Used for 186 | normalizing slices. 187 | 188 | Returns 189 | ------- 190 | cell_nos : list of int 191 | 192 | """ 193 | if ':' not in cells: 194 | return _parse_cells_spec(int(cells), length) 195 | 196 | else: 197 | return _parse_cells_spec(slice(*[int(x) if x else None 198 | for x in cells.split(':')]), 199 | length) 200 | 201 | 202 | def _parse_cells_spec(cells, length): 203 | """ 204 | Used by _get_code_cells to parse a cell specification string into an 205 | ordered list of cell numbers. 206 | 207 | Parameters 208 | ---------- 209 | cells : str, int, or slice 210 | Specification of which cells to retrieve. Can be a single number, 211 | a slice, or a combination of either separated by commas. 212 | length : int 213 | The number of items in the user's In history list. Used for 214 | normalizing slices. 215 | 216 | Returns 217 | ------- 218 | cell_nos : list of int 219 | Ordered list of cell numbers derived from spec. 220 | 221 | """ 222 | if isinstance(cells, int): 223 | return [cells] 224 | 225 | elif isinstance(cells, slice): 226 | return list(range(*cells.indices(length))) 227 | 228 | else: 229 | # string parsing 230 | return sorted(set(_flatten(_parse_str_cell_spec(s, length) 231 | for s in cells.split(',')))) 232 | 233 | 234 | def _get_code_cells(cells): 235 | """ 236 | Get the inputs of the specified cells from the notebook. 237 | 238 | Parameters 239 | ---------- 240 | cells : str, int, or slice 241 | Specification of which cells to retrieve. Can be a single number, 242 | a slice, or a combination of either separated by commas. 243 | 244 | Returns 245 | ------- 246 | code : list of str 247 | Contents of cells as strings in chronological order. 248 | 249 | """ 250 | In = get_ipython().user_ns['In'] 251 | cells = _parse_cells_spec(cells, len(In)) 252 | return [In[x] for x in cells] 253 | 254 | 255 | class Block(object): 256 | """ 257 | A colored square. 258 | 259 | Parameters 260 | ---------- 261 | red, green, blue : int 262 | Integers on the range [0 - 255]. 263 | size : int, optional 264 | Length of the sides of this block in pixels. One is the lower limit. 265 | 266 | Attributes 267 | ---------- 268 | red, green, blue : int 269 | The color values for this `Block`. The color of the `Block` can be 270 | updated by assigning new values to these attributes. 271 | rgb : tuple of int 272 | Tuple of (red, green, blue) values. Can be used to set all the colors 273 | at once. 274 | row, col : int 275 | The zero-based grid position of this `Block`. 276 | size : int 277 | Length of the sides of this block in pixels. The block size can be 278 | changed by modifying this attribute. Note that one is the lower limit. 279 | 280 | """ 281 | 282 | red = _color_property('red') 283 | green = _color_property('green') 284 | blue = _color_property('blue') 285 | 286 | def __init__(self, red, green, blue, size=20): 287 | self.red = red 288 | self.green = green 289 | self.blue = blue 290 | self.size = size 291 | 292 | self._row = None 293 | self._col = None 294 | 295 | @staticmethod 296 | def _check_value(value): 297 | """ 298 | Check that a value is a number and constrain it to [0 - 255]. 299 | 300 | """ 301 | if not isinstance(value, numbers.Number): 302 | s = 'value must be a number. got {0}.'.format(value) 303 | raise InvalidColorSpec(s) 304 | 305 | return int(round(min(255, max(0, value)))) 306 | 307 | @property 308 | def rgb(self): 309 | return (self._red, self._green, self._blue) 310 | 311 | @rgb.setter 312 | def rgb(self, colors): 313 | if len(colors) != 3: 314 | s = 'Setting colors requires three values: (red, green, blue).' 315 | raise ValueError(s) 316 | 317 | self.red, self.green, self.blue = colors 318 | 319 | @property 320 | def row(self): 321 | return self._row 322 | 323 | @property 324 | def col(self): 325 | return self._col 326 | 327 | @property 328 | def size(self): 329 | return self._size 330 | 331 | @size.setter 332 | def size(self, size): 333 | self._size = max(_SMALLEST_BLOCK, size) 334 | 335 | def set_colors(self, red, green, blue): 336 | """ 337 | Updated block colors. 338 | 339 | Parameters 340 | ---------- 341 | red, green, blue : int 342 | Integers on the range [0 - 255]. 343 | 344 | """ 345 | self.red = red 346 | self.green = green 347 | self.blue = blue 348 | 349 | def _update(self, other): 350 | if isinstance(other, Block): 351 | self.rgb = other.rgb 352 | self.size = other.size 353 | elif isinstance(other, Sequence) and len(other) == 3: 354 | self.rgb = other 355 | else: 356 | errmsg = ( 357 | 'Value must be a Block or a sequence of 3 integers. ' 358 | 'Got {0!r}.' 359 | ) 360 | raise ValueError(errmsg.format(other)) 361 | 362 | @property 363 | def _td(self): 364 | """ 365 | The HTML for a table cell with the background color of this Block. 366 | 367 | """ 368 | title = _TITLE.format(self._row, self._col, 369 | self._red, self._green, self._blue) 370 | rgb = _RGB.format(self._red, self._green, self._blue) 371 | return _TD.format(title, self._size, rgb) 372 | 373 | def _repr_html_(self): 374 | return _TABLE.format(uuid.uuid4(), 0, _TR.format(self._td)) 375 | 376 | def show(self): 377 | display(HTML(self._repr_html_())) 378 | 379 | __hash__ = None 380 | 381 | def __eq__(self, other): 382 | if not isinstance(other, Block): 383 | return False 384 | return self.rgb == other.rgb and self.size == other.size 385 | 386 | def __str__(self): 387 | s = ['{0}'.format(self.__class__.__name__), 388 | 'Color: ({0}, {1}, {2})'.format(self._red, 389 | self._green, 390 | self._blue)] 391 | 392 | # add position information if we have it 393 | if self._row is not None: 394 | s[0] += ' [{0}, {1}]'.format(self._row, self._col) 395 | 396 | return os.linesep.join(s) 397 | 398 | def __repr__(self): 399 | type_name = type(self).__name__ 400 | return '{0}({1}, {2}, {3}, size={4})'.format(type_name, 401 | self.red, 402 | self.green, 403 | self.blue, 404 | self.size) 405 | 406 | 407 | class BlockGrid(object): 408 | """ 409 | A grid of blocks whose colors can be individually controlled. 410 | 411 | Parameters 412 | ---------- 413 | width : int 414 | Number of blocks wide to make the grid. 415 | height : int 416 | Number of blocks high to make the grid. 417 | fill : tuple of int, optional 418 | An optional initial color for the grid, defaults to black. 419 | Specified as a tuple of (red, green, blue). E.g.: (10, 234, 198) 420 | block_size : int, optional 421 | Length of the sides of grid blocks in pixels. One is the lower limit. 422 | lines_on : bool, optional 423 | Whether or not to display lines between blocks. 424 | 425 | Attributes 426 | ---------- 427 | width : int 428 | Number of blocks along the width of the grid. 429 | height : int 430 | Number of blocks along the height of the grid. 431 | shape : tuple of int 432 | A tuple of (width, height). 433 | block_size : int 434 | Length of the sides of grid blocks in pixels. The block size can be 435 | changed by modifying this attribute. Note that one is the lower limit. 436 | lines_on : bool 437 | Whether lines are shown between blocks when the grid is displayed. 438 | This attribute can used to toggle the whether the lines appear. 439 | 440 | """ 441 | 442 | def __init__(self, width, height, fill=(0, 0, 0), 443 | block_size=20, lines_on=True): 444 | self._width = width 445 | self._height = height 446 | self._block_size = block_size 447 | self.lines_on = lines_on 448 | self._initialize_grid(fill) 449 | 450 | def _initialize_grid(self, fill): 451 | grid = [[Block(*fill, size=self._block_size) 452 | for col in range(self.width)] 453 | for row in range(self.height)] 454 | 455 | self._grid = grid 456 | 457 | @property 458 | def width(self): 459 | return self._width 460 | 461 | @property 462 | def height(self): 463 | return self._height 464 | 465 | @property 466 | def shape(self): 467 | return (self._width, self._height) 468 | 469 | @property 470 | def block_size(self): 471 | return self._block_size 472 | 473 | @block_size.setter 474 | def block_size(self, size): 475 | self._block_size = size 476 | 477 | for block in self: 478 | block.size = size 479 | 480 | @property 481 | def lines_on(self): 482 | return self._lines_on 483 | 484 | @lines_on.setter 485 | def lines_on(self, value): 486 | if value not in (0, 1): 487 | s = 'lines_on may only be True or False.' 488 | raise ValueError(s) 489 | 490 | self._lines_on = value 491 | 492 | def __eq__(self, other): 493 | if not isinstance(other, BlockGrid): 494 | return False 495 | else: 496 | # compare the underlying grids 497 | return self._grid == other._grid 498 | 499 | def _view_from_grid(self, grid): 500 | """ 501 | Make a new grid from a list of lists of Block objects. 502 | 503 | """ 504 | new_width = len(grid[0]) 505 | new_height = len(grid) 506 | 507 | new_BG = self.__class__(new_width, new_height, 508 | block_size=self._block_size, 509 | lines_on=self._lines_on) 510 | new_BG._grid = grid 511 | 512 | return new_BG 513 | 514 | @staticmethod 515 | def _categorize_index(index): 516 | """ 517 | Used by __getitem__ and __setitem__ to determine whether the user 518 | is asking for a single item, single row, or some kind of slice. 519 | 520 | """ 521 | if isinstance(index, int): 522 | return _SINGLE_ROW 523 | 524 | elif isinstance(index, slice): 525 | return _ROW_SLICE 526 | 527 | elif isinstance(index, tuple): 528 | if len(index) > 2: 529 | s = 'Invalid index, too many dimensions.' 530 | raise IndexError(s) 531 | 532 | elif len(index) == 1: 533 | s = 'Single indices must be integers, not tuple.' 534 | raise TypeError(s) 535 | 536 | if isinstance(index[0], slice): 537 | if isinstance(index[1], (int, slice)): 538 | return _DOUBLE_SLICE 539 | 540 | if isinstance(index[1], slice): 541 | if isinstance(index[0], (int, slice)): 542 | return _DOUBLE_SLICE 543 | 544 | elif isinstance(index[0], int) and isinstance(index[0], int): 545 | return _SINGLE_ITEM 546 | 547 | raise IndexError('Invalid index.') 548 | 549 | def __getitem__(self, index): 550 | ind_cat = self._categorize_index(index) 551 | 552 | if ind_cat == _SINGLE_ROW: 553 | return self._view_from_grid([self._grid[index]]) 554 | 555 | elif ind_cat == _SINGLE_ITEM: 556 | block = self._grid[index[0]][index[1]] 557 | block._row, block._col = index 558 | return block 559 | 560 | elif ind_cat == _ROW_SLICE: 561 | return self._view_from_grid(self._grid[index]) 562 | 563 | elif ind_cat == _DOUBLE_SLICE: 564 | new_grid = self._get_double_slice(index) 565 | return self._view_from_grid(new_grid) 566 | 567 | def __setitem__(self, index, value): 568 | thing = self[index] 569 | 570 | if isinstance(value, BlockGrid): 571 | if isinstance(thing, BlockGrid): 572 | if thing.shape != value.shape: 573 | raise ShapeMismatch('Both sides of grid assignment must ' 574 | 'have the same shape.') 575 | 576 | for a, b in zip(thing, value): 577 | a._update(b) 578 | 579 | else: 580 | raise TypeError('Cannot assign grid to single block.') 581 | 582 | elif isinstance(value, (Iterable, Block)): 583 | for b in _flatten(thing): 584 | b._update(value) 585 | 586 | def _get_double_slice(self, index): 587 | sl_height, sl_width = index 588 | 589 | if isinstance(sl_width, int): 590 | if sl_width == -1: 591 | sl_width = slice(sl_width, None) 592 | else: 593 | sl_width = slice(sl_width, sl_width + 1) 594 | 595 | if isinstance(sl_height, int): 596 | if sl_height == -1: 597 | sl_height = slice(sl_height, None) 598 | else: 599 | sl_height = slice(sl_height, sl_height + 1) 600 | 601 | rows = self._grid[sl_height] 602 | grid = [r[sl_width] for r in rows] 603 | 604 | return grid 605 | 606 | def __iter__(self): 607 | for r in range(self.height): 608 | for c in range(self.width): 609 | yield self[r, c] 610 | 611 | def animate(self, stop_time=0.2): 612 | """ 613 | Call this method in a loop definition to have your changes to the grid 614 | animated in the IPython Notebook. 615 | 616 | Parameters 617 | ---------- 618 | stop_time : float 619 | Amount of time to pause between loop steps. 620 | 621 | """ 622 | for block in self: 623 | self.show() 624 | time.sleep(stop_time) 625 | yield block 626 | clear_output(wait=True) 627 | self.show() 628 | 629 | def _repr_html_(self): 630 | rows = range(self._height) 631 | cols = range(self._width) 632 | 633 | html = reduce(iadd, 634 | (_TR.format(reduce(iadd, 635 | (self[r, c]._td 636 | for c in cols))) 637 | for r in rows)) 638 | 639 | return _TABLE.format(uuid.uuid4(), int(self._lines_on), html) 640 | 641 | def __str__(self): 642 | s = ['{0}'.format(self.__class__.__name__), 643 | 'Shape: {0}'.format(self.shape)] 644 | 645 | return os.linesep.join(s) 646 | 647 | def copy(self): 648 | """ 649 | Returns an independent copy of this BlockGrid. 650 | 651 | """ 652 | return copy.deepcopy(self) 653 | 654 | def show(self): 655 | """ 656 | Display colored grid as an HTML table. 657 | 658 | """ 659 | display(HTML(self._repr_html_())) 660 | 661 | def flash(self, display_time=0.2): 662 | """ 663 | Display the grid for a time. 664 | 665 | Useful for making an animation or iteratively displaying changes. 666 | 667 | Note that this will leave the grid in place until something replaces 668 | it in the same cell. You can use the ``clear`` function to 669 | manually clear output. 670 | 671 | Parameters 672 | ---------- 673 | display_time : float 674 | Amount of time, in seconds, to display the grid. 675 | 676 | """ 677 | self.show() 678 | time.sleep(display_time) 679 | clear_output(wait=True) 680 | 681 | def _calc_image_size(self): 682 | """ 683 | Calculate the size, in pixels, of the grid as an image. 684 | 685 | Returns 686 | ------- 687 | px_width : int 688 | px_height : int 689 | 690 | """ 691 | px_width = self._block_size * self._width 692 | px_height = self._block_size * self._height 693 | 694 | if self._lines_on: 695 | px_width += self._width + 1 696 | px_height += self._height + 1 697 | 698 | return px_width, px_height 699 | 700 | def _write_image(self, fp, format='png'): 701 | """ 702 | Write an image of the current grid to a file-object. 703 | 704 | Parameters 705 | ---------- 706 | fp : file-like 707 | A file-like object such as an open file pointer or 708 | a StringIO/BytesIO instance. 709 | format : str, optional 710 | An image format that will be understood by PIL, 711 | e.g. 'png', 'jpg', 'gif', etc. 712 | 713 | """ 714 | try: 715 | # PIL 716 | import Image 717 | import ImageDraw 718 | except ImportError: 719 | # pillow 720 | from PIL import Image, ImageDraw 721 | 722 | im = Image.new( 723 | mode='RGB', size=self._calc_image_size(), color=(255, 255, 255)) 724 | draw = ImageDraw.Draw(im) 725 | 726 | _bs = self._block_size 727 | 728 | for r in range(self._height): 729 | for c in range(self._width): 730 | px_r = r * _bs 731 | px_c = c * _bs 732 | if self._lines_on: 733 | px_r += r + 1 734 | px_c += c + 1 735 | 736 | rect = ((px_c, px_r), (px_c + _bs - 1, px_r + _bs - 1)) 737 | draw.rectangle(rect, fill=self._grid[r][c].rgb) 738 | 739 | im.save(fp, format=format) 740 | 741 | def show_image(self): 742 | """ 743 | Embed grid in the notebook as a PNG image. 744 | 745 | """ 746 | if sys.version_info[0] == 2: 747 | from StringIO import StringIO as BytesIO 748 | elif sys.version_info[0] == 3: 749 | from io import BytesIO 750 | 751 | im = BytesIO() 752 | self._write_image(im) 753 | display(ipyImage(data=im.getvalue(), format='png')) 754 | 755 | def save_image(self, filename): 756 | """ 757 | Save an image representation of the grid to a file. 758 | Image format will be inferred from file extension. 759 | 760 | Parameters 761 | ---------- 762 | filename : str 763 | Name of file to save to. 764 | 765 | """ 766 | with open(filename, 'wb') as f: 767 | self._write_image(f, format=filename.split('.')[-1]) 768 | 769 | def to_text(self, filename=None): 770 | """ 771 | Write a text file containing the size and block color information 772 | for this grid. 773 | 774 | If no file name is given the text is sent to stdout. 775 | 776 | Parameters 777 | ---------- 778 | filename : str, optional 779 | File into which data will be written. Will be overwritten if 780 | it already exists. 781 | 782 | """ 783 | if filename: 784 | f = open(filename, 'w') 785 | else: 786 | f = sys.stdout 787 | 788 | s = ['# width height', '{0} {1}'.format(self.width, self.height), 789 | '# block size', '{0}'.format(self.block_size), 790 | '# initial color', '0 0 0', 791 | '# row column red green blue'] 792 | f.write(os.linesep.join(s) + os.linesep) 793 | 794 | for block in self: 795 | things = [str(x) for x in (block.row, block.col) + block.rgb] 796 | f.write(' '.join(things) + os.linesep) 797 | 798 | if filename: 799 | f.close() 800 | 801 | def _to_simple_grid(self): 802 | """ 803 | Make a simple representation of the table: nested lists of 804 | of the rows containing tuples of (red, green, blue, size) 805 | for each of the blocks. 806 | 807 | Returns 808 | ------- 809 | grid : list of lists 810 | No matter the class this method is called on the returned 811 | grid will be Python-style: row oriented with the top-left 812 | block in the [0][0] position. 813 | 814 | """ 815 | return [[(x.red, x.green, x.blue, x.size) for x in row] 816 | for row in self._grid] 817 | 818 | def _construct_post_request(self, code_cells, secret): 819 | """ 820 | Construct the request dictionary that will be posted 821 | to ipythonblocks.org. 822 | 823 | Parameters 824 | ---------- 825 | code_cells : int, str, slice, or None 826 | Specify any code cells to be sent and displayed with the grid. 827 | You can specify a single cell, a Python, slice, or a combination 828 | as a string separated by commas. 829 | 830 | For example, '3,5,8:10' would send cells 3, 5, 8, and 9. 831 | secret : bool 832 | If True, this grid will not be shown randomly on ipythonblocks.org. 833 | 834 | Returns 835 | ------- 836 | request : dict 837 | 838 | """ 839 | if code_cells is not None: 840 | code_cells = _get_code_cells(code_cells) 841 | 842 | req = { 843 | 'python_version': tuple(sys.version_info), 844 | 'ipb_version': __version__, 845 | 'ipb_class': self.__class__.__name__, 846 | 'code_cells': code_cells, 847 | 'secret': secret, 848 | 'grid_data': { 849 | 'lines_on': self.lines_on, 850 | 'width': self.width, 851 | 'height': self.height, 852 | 'blocks': self._to_simple_grid() 853 | } 854 | } 855 | 856 | return req 857 | 858 | def post_to_web(self, code_cells=None, secret=False): 859 | """ 860 | Post this grid to ipythonblocks.org and return a URL to 861 | view the grid on the web. 862 | 863 | Parameters 864 | ---------- 865 | code_cells : int, str, or slice, optional 866 | Specify any code cells to be sent and displayed with the grid. 867 | You can specify a single cell, a Python, slice, or a combination 868 | as a string separated by commas. 869 | 870 | For example, '3,5,8:10' would send cells 3, 5, 8, and 9. 871 | secret : bool, optional 872 | If True, this grid will not be shown randomly on ipythonblocks.org. 873 | 874 | Returns 875 | ------- 876 | url : str 877 | URL to view your grid on ipythonblocks.org. 878 | 879 | """ 880 | import requests 881 | 882 | req = self._construct_post_request(code_cells, secret) 883 | response = requests.post(_POST_URL, data=json.dumps(req)) 884 | response.raise_for_status() 885 | 886 | return response.json()['url'] 887 | 888 | def _load_simple_grid(self, block_data): 889 | """ 890 | Modify the grid to reflect the data in `block_data`, which 891 | should be a nested list of tuples as produced by `_to_simple_grid`. 892 | 893 | Parameters 894 | ---------- 895 | block_data : list of lists 896 | Nested list of tuples as produced by `_to_simple_grid`. 897 | 898 | """ 899 | if len(block_data) != self.height or \ 900 | len(block_data[0]) != self.width: 901 | raise ShapeMismatch('block_data must have same shape as grid.') 902 | 903 | for row in range(self.height): 904 | for col in range(self.width): 905 | self._grid[row][col].rgb = block_data[row][col][:3] 906 | self._grid[row][col].size = block_data[row][col][3] 907 | 908 | @classmethod 909 | def from_web(cls, grid_id, secret=False): 910 | """ 911 | Make a new BlockGrid from a grid on ipythonblocks.org. 912 | 913 | Parameters 914 | ---------- 915 | grid_id : str 916 | ID of a grid on ipythonblocks.org. This will be the part of the 917 | URL after 'ipythonblocks.org/'. 918 | secret : bool, optional 919 | Whether or not the grid on ipythonblocks.org is secret. 920 | 921 | Returns 922 | ------- 923 | grid : BlockGrid 924 | 925 | """ 926 | import requests 927 | 928 | get_url = _GET_URL_PUBLIC if not secret else _GET_URL_SECRET 929 | resp = requests.get(get_url.format(grid_id)) 930 | resp.raise_for_status() 931 | grid_spec = resp.json() 932 | 933 | grid = cls(grid_spec['width'], grid_spec['height'], 934 | lines_on=grid_spec['lines_on']) 935 | grid._load_simple_grid(grid_spec['blocks']) 936 | 937 | return grid 938 | 939 | 940 | class Pixel(Block): 941 | @property 942 | def x(self): 943 | """ 944 | Horizontal coordinate of Pixel. 945 | 946 | """ 947 | return self._col 948 | 949 | @property 950 | def y(self): 951 | """ 952 | Vertical coordinate of Pixel. 953 | 954 | """ 955 | return self._row 956 | 957 | @property 958 | def _td(self): 959 | """ 960 | The HTML for a table cell with the background color of this Pixel. 961 | 962 | """ 963 | title = _TITLE.format(self._col, self._row, 964 | self._red, self._green, self._blue) 965 | rgb = _RGB.format(self._red, self._green, self._blue) 966 | return _TD.format(title, self._size, rgb) 967 | 968 | def __str__(self): 969 | s = ['{0}'.format(self.__class__.__name__), 970 | 'Color: ({0}, {1}, {2})'.format(self._red, 971 | self._green, 972 | self._blue)] 973 | 974 | # add position information if we have it 975 | if self._row is not None: 976 | s[0] += ' [{0}, {1}]'.format(self._col, self._row) 977 | 978 | return os.linesep.join(s) 979 | 980 | 981 | class ImageGrid(BlockGrid): 982 | """ 983 | A grid of blocks whose colors can be individually controlled. 984 | 985 | Parameters 986 | ---------- 987 | width : int 988 | Number of blocks wide to make the grid. 989 | height : int 990 | Number of blocks high to make the grid. 991 | fill : tuple of int, optional 992 | An optional initial color for the grid, defaults to black. 993 | Specified as a tuple of (red, green, blue). E.g.: (10, 234, 198) 994 | block_size : int, optional 995 | Length of the sides of grid blocks in pixels. One is the lower limit. 996 | lines_on : bool, optional 997 | Whether or not to display lines between blocks. 998 | origin : {'lower-left', 'upper-left'}, optional 999 | Set the location of the grid origin. 1000 | 1001 | Attributes 1002 | ---------- 1003 | width : int 1004 | Number of blocks along the width of the grid. 1005 | height : int 1006 | Number of blocks along the height of the grid. 1007 | shape : tuple of int 1008 | A tuple of (width, height). 1009 | block_size : int 1010 | Length of the sides of grid blocks in pixels. 1011 | lines_on : bool 1012 | Whether lines are shown between blocks when the grid is displayed. 1013 | This attribute can used to toggle the whether the lines appear. 1014 | origin : str 1015 | The location of the grid origin. 1016 | 1017 | """ 1018 | 1019 | def __init__(self, width, height, fill=(0, 0, 0), 1020 | block_size=20, lines_on=True, origin='lower-left'): 1021 | super(ImageGrid, self).__init__(width, height, fill, 1022 | block_size, lines_on) 1023 | 1024 | if origin not in ('lower-left', 'upper-left'): 1025 | s = "origin keyword must be one of {'lower-left', 'upper-left'}." 1026 | raise ValueError(s) 1027 | 1028 | self._origin = origin 1029 | 1030 | def _initialize_grid(self, fill): 1031 | grid = [[Pixel(*fill, size=self._block_size) 1032 | for col in range(self.width)] 1033 | for row in range(self.height)] 1034 | 1035 | self._grid = grid 1036 | 1037 | @property 1038 | def block_size(self): 1039 | return self._block_size 1040 | 1041 | @property 1042 | def origin(self): 1043 | return self._origin 1044 | 1045 | def _transform_index(self, index): 1046 | """ 1047 | Transform a single-item index from Python style coordinates to 1048 | image style coordinates in which the first item refers to column and 1049 | the second item refers to row. Also takes into account the 1050 | location of the origin. 1051 | 1052 | """ 1053 | # in ImageGrid index is guaranteed to be a tuple. 1054 | 1055 | # first thing, switch the coordinates since ImageGrid is column 1056 | # major and ._grid is row major. 1057 | new_ind = [index[1], index[0]] 1058 | 1059 | # now take into account that the ImageGrid origin may be lower-left, 1060 | # while the ._grid origin is upper-left. 1061 | if self._origin == 'lower-left': 1062 | if new_ind[0] >= 0: 1063 | new_ind[0] = self._height - new_ind[0] - 1 1064 | else: 1065 | new_ind[0] = abs(new_ind[0]) - 1 1066 | 1067 | return tuple(new_ind) 1068 | 1069 | def __getitem__(self, index): 1070 | ind_cat = self._categorize_index(index) 1071 | 1072 | # ImageGrid will only support single item indexing and 2D slices 1073 | if ind_cat not in (_DOUBLE_SLICE, _SINGLE_ITEM): 1074 | s = 'ImageGrid only supports 2D indexing.' 1075 | raise IndexError(s) 1076 | 1077 | if ind_cat == _SINGLE_ITEM: 1078 | # should be able to index ._grid with new_ind regardless of any 1079 | # following coordinate transforms. let's just make sure. 1080 | self._grid[index[1]][index[0]] 1081 | 1082 | real_index = self._transform_index(index) 1083 | pixel = self._grid[real_index[0]][real_index[1]] 1084 | pixel._col, pixel._row = index 1085 | return pixel 1086 | 1087 | elif ind_cat == _DOUBLE_SLICE: 1088 | new_grid = self._get_double_slice(index) 1089 | return self._view_from_grid(new_grid) 1090 | 1091 | def _get_double_slice(self, index): 1092 | cslice, rslice = index 1093 | 1094 | if isinstance(rslice, int): 1095 | if rslice == -1: 1096 | rslice = slice(rslice, None) 1097 | else: 1098 | rslice = slice(rslice, rslice + 1) 1099 | 1100 | if isinstance(cslice, int): 1101 | if cslice == -1: 1102 | cslice = slice(cslice, None) 1103 | else: 1104 | cslice = slice(cslice, cslice + 1) 1105 | 1106 | rows = range(self._height)[rslice] 1107 | if self._origin == 'lower-left': 1108 | rows = rows[::-1] 1109 | 1110 | cols = range(self._width)[cslice] 1111 | 1112 | new_grid = [[self[c, r] for c in cols] for r in rows] 1113 | 1114 | return new_grid 1115 | 1116 | def __iter__(self): 1117 | for col in range(self.width): 1118 | for row in range(self.height): 1119 | yield self[col, row] 1120 | 1121 | def _repr_html_(self): 1122 | rows = range(self._height) 1123 | cols = range(self._width) 1124 | 1125 | if self._origin == 'lower-left': 1126 | rows = rows[::-1] 1127 | 1128 | html = reduce(iadd, 1129 | (_TR.format(reduce(iadd, 1130 | (self[c, r]._td 1131 | for c in cols))) 1132 | for r in rows)) 1133 | 1134 | return _TABLE.format(uuid.uuid4(), int(self._lines_on), html) 1135 | 1136 | @classmethod 1137 | def from_web(cls, grid_id, secret=False, origin='lower-left'): 1138 | """ 1139 | Make a new ImageGrid from a grid on ipythonblocks.org. 1140 | 1141 | Parameters 1142 | ---------- 1143 | grid_id : str 1144 | ID of a grid on ipythonblocks.org. This will be the part of the 1145 | URL after 'ipythonblocks.org/'. 1146 | secret : bool, optional 1147 | Whether or not the grid on ipythonblocks.org is secret. 1148 | origin : {'lower-left', 'upper-left'}, optional 1149 | Set the location of the grid origin. 1150 | 1151 | Returns 1152 | ------- 1153 | grid : ImageGrid 1154 | 1155 | """ 1156 | import requests 1157 | 1158 | get_url = _GET_URL_PUBLIC if not secret else _GET_URL_SECRET 1159 | resp = requests.get(get_url.format(grid_id)) 1160 | resp.raise_for_status() 1161 | grid_spec = resp.json() 1162 | 1163 | grid = cls(grid_spec['width'], grid_spec['height'], 1164 | lines_on=grid_spec['lines_on'], origin=origin) 1165 | grid._load_simple_grid(grid_spec['blocks']) 1166 | 1167 | return grid 1168 | 1169 | 1170 | # Convenience wrapper for color tuples with attribute access for the 1171 | # component colors 1172 | Color = namedtuple('Color', ['red', 'green', 'blue']) 1173 | 1174 | # This doesn't work on Python 2 1175 | # Color.__doc__ += """\ 1176 | # Wrapper for a color tuple that provides .red, .green, and .blue 1177 | # attributes for access to the individual components. 1178 | # """ 1179 | 1180 | 1181 | # As a convenience, provide some colors as a custom hybrid 1182 | # dictionary and object with the color names as attributes 1183 | class _ColorBunch(dict): 1184 | """ 1185 | Customized dictionary that exposes its keys as attributes. 1186 | 1187 | """ 1188 | def __init__(self, colors): 1189 | colors = {name: Color(*rgb) for name, rgb in colors.items()} 1190 | super(_ColorBunch, self).__init__(colors) 1191 | self.__dict__.update(colors) 1192 | 1193 | 1194 | # HTML colors 1195 | colors = _ColorBunch({ 1196 | 'AliceBlue': (240, 248, 255), 1197 | 'AntiqueWhite': (250, 235, 215), 1198 | 'Aqua': (0, 255, 255), 1199 | 'Aquamarine': (127, 255, 212), 1200 | 'Azure': (240, 255, 255), 1201 | 'Beige': (245, 245, 220), 1202 | 'Bisque': (255, 228, 196), 1203 | 'Black': (0, 0, 0), 1204 | 'BlanchedAlmond': (255, 235, 205), 1205 | 'Blue': (0, 0, 255), 1206 | 'BlueViolet': (138, 43, 226), 1207 | 'Brown': (165, 42, 42), 1208 | 'BurlyWood': (222, 184, 135), 1209 | 'CadetBlue': (95, 158, 160), 1210 | 'Chartreuse': (127, 255, 0), 1211 | 'Chocolate': (210, 105, 30), 1212 | 'Coral': (255, 127, 80), 1213 | 'CornflowerBlue': (100, 149, 237), 1214 | 'Cornsilk': (255, 248, 220), 1215 | 'Crimson': (220, 20, 60), 1216 | 'Cyan': (0, 255, 255), 1217 | 'DarkBlue': (0, 0, 139), 1218 | 'DarkCyan': (0, 139, 139), 1219 | 'DarkGoldenrod': (184, 134, 11), 1220 | 'DarkGray': (169, 169, 169), 1221 | 'DarkGreen': (0, 100, 0), 1222 | 'DarkKhaki': (189, 183, 107), 1223 | 'DarkMagenta': (139, 0, 139), 1224 | 'DarkOliveGreen': (85, 107, 47), 1225 | 'DarkOrange': (255, 140, 0), 1226 | 'DarkOrchid': (153, 50, 204), 1227 | 'DarkRed': (139, 0, 0), 1228 | 'DarkSalmon': (233, 150, 122), 1229 | 'DarkSeaGreen': (143, 188, 143), 1230 | 'DarkSlateBlue': (72, 61, 139), 1231 | 'DarkSlateGray': (47, 79, 79), 1232 | 'DarkTurquoise': (0, 206, 209), 1233 | 'DarkViolet': (148, 0, 211), 1234 | 'DeepPink': (255, 20, 147), 1235 | 'DeepSkyBlue': (0, 191, 255), 1236 | 'DimGray': (105, 105, 105), 1237 | 'DodgerBlue': (30, 144, 255), 1238 | 'FireBrick': (178, 34, 34), 1239 | 'FloralWhite': (255, 250, 240), 1240 | 'ForestGreen': (34, 139, 34), 1241 | 'Fuchsia': (255, 0, 255), 1242 | 'Gainsboro': (220, 220, 220), 1243 | 'GhostWhite': (248, 248, 255), 1244 | 'Gold': (255, 215, 0), 1245 | 'Goldenrod': (218, 165, 32), 1246 | 'Gray': (128, 128, 128), 1247 | 'Green': (0, 128, 0), 1248 | 'GreenYellow': (173, 255, 47), 1249 | 'Honeydew': (240, 255, 240), 1250 | 'HotPink': (255, 105, 180), 1251 | 'IndianRed': (205, 92, 92), 1252 | 'Indigo': (75, 0, 130), 1253 | 'Ivory': (255, 255, 240), 1254 | 'Khaki': (240, 230, 140), 1255 | 'Lavender': (230, 230, 250), 1256 | 'LavenderBlush': (255, 240, 245), 1257 | 'LawnGreen': (124, 252, 0), 1258 | 'LemonChiffon': (255, 250, 205), 1259 | 'LightBlue': (173, 216, 230), 1260 | 'LightCoral': (240, 128, 128), 1261 | 'LightCyan': (224, 255, 255), 1262 | 'LightGoldenrodYellow': (250, 250, 210), 1263 | 'LightGray': (211, 211, 211), 1264 | 'LightGreen': (144, 238, 144), 1265 | 'LightPink': (255, 182, 193), 1266 | 'LightSalmon': (255, 160, 122), 1267 | 'LightSeaGreen': (32, 178, 170), 1268 | 'LightSkyBlue': (135, 206, 250), 1269 | 'LightSlateGray': (119, 136, 153), 1270 | 'LightSteelBlue': (176, 196, 222), 1271 | 'LightYellow': (255, 255, 224), 1272 | 'Lime': (0, 255, 0), 1273 | 'LimeGreen': (50, 205, 50), 1274 | 'Linen': (250, 240, 230), 1275 | 'Magenta': (255, 0, 255), 1276 | 'Maroon': (128, 0, 0), 1277 | 'MediumAquamarine': (102, 205, 170), 1278 | 'MediumBlue': (0, 0, 205), 1279 | 'MediumOrchid': (186, 85, 211), 1280 | 'MediumPurple': (147, 112, 219), 1281 | 'MediumSeaGreen': (60, 179, 113), 1282 | 'MediumSlateBlue': (123, 104, 238), 1283 | 'MediumSpringGreen': (0, 250, 154), 1284 | 'MediumTurquoise': (72, 209, 204), 1285 | 'MediumVioletRed': (199, 21, 133), 1286 | 'MidnightBlue': (25, 25, 112), 1287 | 'MintCream': (245, 255, 250), 1288 | 'MistyRose': (255, 228, 225), 1289 | 'Moccasin': (255, 228, 181), 1290 | 'NavajoWhite': (255, 222, 173), 1291 | 'Navy': (0, 0, 128), 1292 | 'OldLace': (253, 245, 230), 1293 | 'Olive': (128, 128, 0), 1294 | 'OliveDrab': (107, 142, 35), 1295 | 'Orange': (255, 165, 0), 1296 | 'OrangeRed': (255, 69, 0), 1297 | 'Orchid': (218, 112, 214), 1298 | 'PaleGoldenrod': (238, 232, 170), 1299 | 'PaleGreen': (152, 251, 152), 1300 | 'PaleTurquoise': (175, 238, 238), 1301 | 'PaleVioletRed': (219, 112, 147), 1302 | 'PapayaWhip': (255, 239, 213), 1303 | 'PeachPuff': (255, 218, 185), 1304 | 'Peru': (205, 133, 63), 1305 | 'Pink': (255, 192, 203), 1306 | 'Plum': (221, 160, 221), 1307 | 'PowderBlue': (176, 224, 230), 1308 | 'Purple': (128, 0, 128), 1309 | 'Red': (255, 0, 0), 1310 | 'RosyBrown': (188, 143, 143), 1311 | 'RoyalBlue': (65, 105, 225), 1312 | 'SaddleBrown': (139, 69, 19), 1313 | 'Salmon': (250, 128, 114), 1314 | 'SandyBrown': (244, 164, 96), 1315 | 'SeaGreen': (46, 139, 87), 1316 | 'Seashell': (255, 245, 238), 1317 | 'Sienna': (160, 82, 45), 1318 | 'Silver': (192, 192, 192), 1319 | 'SkyBlue': (135, 206, 235), 1320 | 'SlateBlue': (106, 90, 205), 1321 | 'SlateGray': (112, 128, 144), 1322 | 'Snow': (255, 250, 250), 1323 | 'SpringGreen': (0, 255, 127), 1324 | 'SteelBlue': (70, 130, 180), 1325 | 'Tan': (210, 180, 140), 1326 | 'Teal': (0, 128, 128), 1327 | 'Thistle': (216, 191, 216), 1328 | 'Tomato': (255, 99, 71), 1329 | 'Turquoise': (64, 224, 208), 1330 | 'Violet': (238, 130, 238), 1331 | 'Wheat': (245, 222, 179), 1332 | 'White': (255, 255, 255), 1333 | 'WhiteSmoke': (245, 245, 245), 1334 | 'Yellow': (255, 255, 0), 1335 | 'YellowGreen': (154, 205, 50) 1336 | }) 1337 | 1338 | 1339 | # Flat UI colors: http://flatuicolors.com/ 1340 | fui_colors = _ColorBunch({ 1341 | 'Alizarin': (231, 76, 60), 1342 | 'Pomegranate': (192, 57, 43), 1343 | 'Carrot': (230, 126, 34), 1344 | 'Pumpkin': (211, 84, 0), 1345 | 'SunFlower': (241, 196, 15), 1346 | 'Orange': (243, 156, 18), 1347 | 'Emerald': (46, 204, 113), 1348 | 'Nephritis': (39, 174, 96), 1349 | 'Turquoise': (26, 188, 156), 1350 | 'GreenSea': (22, 160, 133), 1351 | 'PeterRiver': (52, 152, 219), 1352 | 'BelizeHole': (41, 128, 185), 1353 | 'Amethyst': (155, 89, 182), 1354 | 'Wisteria': (142, 68, 173), 1355 | 'WetAsphalt': (52, 73, 94), 1356 | 'MidnightBlue': (44, 62, 80), 1357 | 'Concrete': (149, 165, 166), 1358 | 'Asbestos': (127, 140, 141), 1359 | 'Clouds': (236, 240, 241), 1360 | 'Silver': (189, 195, 199) 1361 | }) 1362 | -------------------------------------------------------------------------------- /demos/ipythonblocks_imagegrid.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "ipythonblocks_imagegrid" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "heading", 12 | "level": 1, 13 | "metadata": {}, 14 | "source": [ 15 | "ipythonblocks.ImageGrid" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "To learn more about `ipythonblocks` visit the homepage at [https://github.com/jiffyclub/ipythonblocks](https://github.com/jiffyclub/ipythonblocks).\n", 23 | "\n", 24 | "`ImageGrid` is a class that imitates image manipulation libraries like [PIL](http://www.pythonware.com/products/pil/).\n", 25 | "Key differences from `BlockGrid` are:\n", 26 | "\n", 27 | "* Only 2D indexing is supported\n", 28 | "* Indices are [column, row]\n", 29 | "* Grid units are `Pixel` objects, which have `.x` (column) and `.y` (row) attributes\n", 30 | "* Grid origin defaults to the lower-left corner, but can be set to the upper-left" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "collapsed": false, 36 | "input": [ 37 | "from ipythonblocks import ImageGrid" 38 | ], 39 | "language": "python", 40 | "metadata": {}, 41 | "outputs": [], 42 | "prompt_number": 1 43 | }, 44 | { 45 | "cell_type": "code", 46 | "collapsed": false, 47 | "input": [ 48 | "grid = ImageGrid(10, 10, fill=(124, 124, 124), block_size=15)\n", 49 | "grid" 50 | ], 51 | "language": "python", 52 | "metadata": {}, 53 | "outputs": [ 54 | { 55 | "html": [ 56 | "
" 57 | ], 58 | "output_type": "pyout", 59 | "prompt_number": 2, 60 | "text": [ 61 | "" 62 | ] 63 | } 64 | ], 65 | "prompt_number": 2 66 | }, 67 | { 68 | "cell_type": "code", 69 | "collapsed": false, 70 | "input": [ 71 | "for pixel in grid:\n", 72 | " if pixel.x < 5:\n", 73 | " pixel.green = 255\n", 74 | " elif pixel.x > 5 and pixel.y >= 5:\n", 75 | " pixel.blue = 255\n", 76 | "grid" 77 | ], 78 | "language": "python", 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "html": [ 83 | "
" 84 | ], 85 | "output_type": "pyout", 86 | "prompt_number": 3, 87 | "text": [ 88 | "" 89 | ] 90 | } 91 | ], 92 | "prompt_number": 3 93 | }, 94 | { 95 | "cell_type": "code", 96 | "collapsed": false, 97 | "input": [ 98 | "grid[:3, :3] = (255, 124, 124)\n", 99 | "grid" 100 | ], 101 | "language": "python", 102 | "metadata": {}, 103 | "outputs": [ 104 | { 105 | "html": [ 106 | "
" 107 | ], 108 | "output_type": "pyout", 109 | "prompt_number": 4, 110 | "text": [ 111 | "" 112 | ] 113 | } 114 | ], 115 | "prompt_number": 4 116 | }, 117 | { 118 | "cell_type": "code", 119 | "collapsed": false, 120 | "input": [ 121 | "grid[::4, ::4] = (0, 0, 0)\n", 122 | "grid" 123 | ], 124 | "language": "python", 125 | "metadata": {}, 126 | "outputs": [ 127 | { 128 | "html": [ 129 | "
" 130 | ], 131 | "output_type": "pyout", 132 | "prompt_number": 5, 133 | "text": [ 134 | "" 135 | ] 136 | } 137 | ], 138 | "prompt_number": 5 139 | }, 140 | { 141 | "cell_type": "code", 142 | "collapsed": false, 143 | "input": [ 144 | "grid[6, :] = (255, 0, 255)\n", 145 | "grid" 146 | ], 147 | "language": "python", 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "html": [ 152 | "
" 153 | ], 154 | "output_type": "pyout", 155 | "prompt_number": 6, 156 | "text": [ 157 | "" 158 | ] 159 | } 160 | ], 161 | "prompt_number": 6 162 | }, 163 | { 164 | "cell_type": "code", 165 | "collapsed": false, 166 | "input": [ 167 | "grid[:, 6] = (255, 255, 0)\n", 168 | "grid" 169 | ], 170 | "language": "python", 171 | "metadata": {}, 172 | "outputs": [ 173 | { 174 | "html": [ 175 | "
" 176 | ], 177 | "output_type": "pyout", 178 | "prompt_number": 7, 179 | "text": [ 180 | "" 181 | ] 182 | } 183 | ], 184 | "prompt_number": 7 185 | } 186 | ], 187 | "metadata": {} 188 | } 189 | ] 190 | } --------------------------------------------------------------------------------