Repository: ManiacalLabs/BiblioPixel
Pull request #555 (2017-12-28 14:46:35)

Allow yaml project files.
A bit of a New Year's Eve present for you.

It doesn't change any writing at all, and lets you read .yml files if you like.

I use the original .json parser for .json files, because it gives better error messages.


rec: Well, I actually did think about that - rather a bit too much really.  :-D

Trouble is, I hate names like data and config a lot more.  They say even less.

What it is is "read and write JSON-family data".  It continues to read and write JSON data, it just also recognizes a derivative form too, YAML.

How about a comment in the file.
adammhaile: Yeah... IDK. Couldn't really think of anything better.
Yeah, for now throw a comment in there if you will and then +1

On Thu, Dec 28, 2017 at 10:05 AM, Tom Swirly <notifications@github.com>
wrote:

> Well, I actually did think about that - rather a bit too much really. :-D
>
> Trouble is, I hate names like data and config a lot more. They say even
> less.
>
> What it is is "read and write JSON-family data". It continues to read and
> write JSON data, it just also recognizes a derivative form too, YAML.
>
> How about a comment in the file.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/ManiacalLabs/BiblioPixel/pull/555#issuecomment-354302087>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AA6a6mMyOYWRuRMxVWhRtXGrczhx6ketks5tE64tgaJpZM4ROYgc>
> .
>

rec: All right, I redid this to make life simpler and added a --dumpy flag that dumps in yaml...
adammhaile: Cool. +1 but please explain what the point of dump is? I mean, isn't it already loading the project file? Or is this basically for conversion purposes?
rec: It's for debugging purposes.

At this point, a pretty huge amount of manipulation goes on to make a
project.  Potentially, you could be reading multiple custom defaults, your
project file AND the default-defaults and merging them all in - plus there
are other manipulations going on with driver vs drivers and perhaps soon
animation/animations.

More, a next step for me is going to be allowing you to include a .json
file inside another.  Again, this is for convenience of the developer -
often you want to debug one animation and then use it in a bigger sequence,
but then later maybe tweak it again - and trying to run it inside a really
large sequence takes a long time.

So dumping out the whole file will be pretty useful in this big picture.



On Thu, Jan 4, 2018 at 12:17 AM, Adam Haile <notifications@github.com>
wrote:

> Cool. +1 but please explain what the point of dump is? I mean, isn't it
> already loading the project file? Or is this basically for conversion
> purposes?
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/ManiacalLabs/BiblioPixel/pull/555#issuecomment-355155176>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAPdssSuSNqDTrIoSPjXefoit1lMpoLaks5tHAp6gaJpZM4ROYgc>
> .
>



-- 
     /t

*https://tom.ritchford.com <https://tom.ritchford.com>*
*https://tom.swirly.com <https://tom.swirly.com>*


Patch:
diff --git a/bibliopixel/animation/collection.py b/bibliopixel/animation/collection.py
index 73cc524..dab1994 100644
--- a/bibliopixel/animation/collection.py
+++ b/bibliopixel/animation/collection.py
@@ -9,7 +9,7 @@ class Collection(animation.BaseAnimation):
 
     @staticmethod
     def pre_recursion(desc):
-        def make_animation(a):
+        def cleanup_animation(a):
             if callable(a) or isinstance(a, str) or 'animation' not in a:
                 animation = a
                 a = {}
@@ -32,19 +32,14 @@ class Collection(animation.BaseAnimation):
                 raise ValueError('Extra fields in animation: ' + ', '.join(a))
             return animation
 
-        desc['animations'] = [make_animation(a) for a in desc['animations']]
+        desc['animations'] = [cleanup_animation(a) for a in desc['animations']]
         return desc
 
     CHILDREN = 'animations',
 
     def __init__(self, layout, animations=None, **kwds):
         super().__init__(layout, **kwds)
-        if animations:
-            if isinstance(animations[0], (str, dict)):
-                self.animations = [self._make_animation(i) for i in animations]
-            else:
-                self.animations = animations
-
+        self.animations = animations or []
         self.index = 0
         self.internal_delay = 0  # never wait
 
@@ -69,28 +64,6 @@ class Collection(animation.BaseAnimation):
         if 0 <= self.index < len(self.animations):
             return self.animations[self.index]
 
-    def _make_animation(self, a):
-        if isinstance(a, str):
-            animation = a
-            run = None
-
-        else:
-            animation = a.get('animation')
-            if animation:
-                # Looks like {'animation': ..., 'run': }
-                run = a.get('run')
-            else:
-                # It's an animation itself.
-                animation, run = a, None
-
-        try:
-            return project.make_animation(self.layout, animation, run)
-        except:
-            if self.FAIL_ON_EXCEPTION:
-                raise
-
-            log.error(traceback.format_exc())
-
 
 class Parallel(Collection):
     def __init__(self, *args, levels, **kwds):
diff --git a/bibliopixel/main/color.py b/bibliopixel/main/color.py
index 7abb32f..5ecfe35 100644
--- a/bibliopixel/main/color.py
+++ b/bibliopixel/main/color.py
@@ -2,7 +2,6 @@
 Converts its arguments between color names and color tuples.
 """
 
-import json
 from .. colors import names
 from .. util import log
 
diff --git a/bibliopixel/main/common_flags.py b/bibliopixel/main/common_flags.py
index f3ff9ea..73ac830 100644
--- a/bibliopixel/main/common_flags.py
+++ b/bibliopixel/main/common_flags.py
@@ -1,5 +1,6 @@
-import json, os
+import os
 from .. project import data_maker, defaults, project
+from .. util import json
 
 """Common command line arguments for run and demo."""
 
@@ -47,6 +48,18 @@ def add_project_flags(parser):
         '--dimensions', '--dim', default=None,
         help='x, (x, y) or (x, y, z) dimensions for project')
 
+    parser.add_argument(
+        '--dump', action='store_true',
+        help='Dump the full project as JSON after loading but before running')
+
+    parser.add_argument(
+        '--dumpy', action='store_true',
+        help='Dump the full project as YAML after loading but before running')
+
+    parser.add_argument(
+        '--dry_run', action='store_true',
+        help='Load projects but do not run them')
+
     parser.add_argument(
         '-f', '--fail_on_exception', action='store_true',
         help='If true, bp fail if any subanimation fails to construct')
@@ -127,13 +140,9 @@ def _make_project_flags(args):
     return project_flags
 
 
-def make_project(args, desc):
+def make_project(args, desc, **kwds):
     project_flags = _make_project_flags(args)
-    return project.project(project_flags, desc)
-
-
-def make_animation(args, desc):
-    return make_project(args, desc).make_animation()
+    return project.project(project_flags, desc, **kwds)
 
 
 # Help messages.
diff --git a/bibliopixel/main/demo.py b/bibliopixel/main/demo.py
index 91b2dab..c3b3a71 100644
--- a/bibliopixel/main/demo.py
+++ b/bibliopixel/main/demo.py
@@ -38,7 +38,8 @@ def make_runnable_animation(demo, args):
         if 'z' in layout:
             layout['z'] = layout['z'] or args.depth
 
-    return common_flags.make_animation(args, demo)
+    project = common_flags.make_project(args, demo)
+    return project.animation
 
 
 def usage():
diff --git a/bibliopixel/main/main.py b/bibliopixel/main/main.py
index 4526ff7..119906e 100644
--- a/bibliopixel/main/main.py
+++ b/bibliopixel/main/main.py
@@ -2,7 +2,7 @@ import argparse, os, sys
 from . import common_flags
 from .. util import log
 from .. project.importer import import_module
-from .. project import aliases
+from .. project import aliases, project
 
 
 __all__ = ['main']
diff --git a/bibliopixel/main/run.py b/bibliopixel/main/run.py
index 7a8fe45..34ff257 100644
--- a/bibliopixel/main/run.py
+++ b/bibliopixel/main/run.py
@@ -4,7 +4,7 @@ Run a project description file.
 
 import os, sys, time, traceback
 from . import common_flags, simpixel
-from .. util import log
+from .. util import json, log
 from .. animation.collection import Collection
 from .. project import load
 
@@ -51,17 +51,24 @@ def _get_animations(args):
             if filename.endswith('.py'):
                 desc = _load_py(filename)
             else:
-                desc = load.data(filename)
-            animation = common_flags.make_animation(args, desc)
-            animations.append(animation)
+                desc = load.data(filename, False)
+                desc = json.loads(desc, filename)
+
+            project = common_flags.make_project(args, desc)
+            animations.append(project.animation)
+            if args.dump:
+                json.dump(project.desc)
+            if args.dumpy:
+                print(json.yaml.dump(project.desc))
 
         except Exception as exception:
             if filename.endswith('.py'):
                 raise
+            eargs = exception.args
             if args.verbose:
                 exception = traceback.format_exc()
             msg = RUN_ERROR.format(**locals())
-            failed.append((msg, exception.args))
+            failed.append((msg, eargs))
 
         finally:
             sys.path[:] = saved_path
@@ -104,6 +111,9 @@ def run(args):
     args.name = args.name or ['']
     animations = _get_animations(args)
 
+    if args.dry_run:
+        print('(dry run - nothing executed)')
+        return
     if args.simpixel:
         simpixel.open_simpixel(args.simpixel)
     elif args.s:
diff --git a/bibliopixel/main/set.py b/bibliopixel/main/set.py
index 57ec5a6..3eb471f 100644
--- a/bibliopixel/main/set.py
+++ b/bibliopixel/main/set.py
@@ -1,4 +1,4 @@
-import json, sys
+import sys
 from .. project import merge, defaults
 
 
diff --git a/bibliopixel/project/defaults.py b/bibliopixel/project/defaults.py
index 36b1abe..e35be3c 100644
--- a/bibliopixel/project/defaults.py
+++ b/bibliopixel/project/defaults.py
@@ -1,6 +1,6 @@
-import json, os, sys
+import os, sys
 from . import merge as _merge
-from .. util import datafile
+from .. util import datafile, json
 
 # This is set to true during testing.
 BYPASS_PROJECT_DEFAULTS = False
@@ -27,7 +27,7 @@ def merge(*projects):
 def show_defaults():
     """List current user defaults in JSON format"""
     _warn_if_empty()
-    _json_dump(USER_DEFAULTS.data, sys.stdout)
+    json.dump(USER_DEFAULTS.data, sys.stdout)
     print()
 
 
@@ -77,7 +77,7 @@ def save_defaults(name):
             return
 
     with open(path, 'w') as fp:
-        _json_dump(USER_DEFAULTS.data, fp)
+        json.dump(USER_DEFAULTS.data, fp)
 
     print('Written project defaults to', name)
 
@@ -171,10 +171,6 @@ def _default_file(name):
     return os.path.join(SAVE_DIRECTORY, name)
 
 
-def _json_dump(data, fp):
-    json.dump(data, fp, indent=4, sort_keys=True)
-
-
 def _load_defaults(name):
     n = name
     if not os.path.exists(n):
@@ -183,8 +179,8 @@ def _load_defaults(name):
             list_saved_defaults()
             raise ValueError('No such default: ' + name)
     try:
-        return json.load(open(n))
+        return json.load(n)
     except Exception as e:
-        e.args = ('There was a JSON error in file ' + name,
+        e.args = ('There was an error in data file ' + name,
                   'Did you edit this file by hand?') + e.args
         raise
diff --git a/bibliopixel/project/importer.py b/bibliopixel/project/importer.py
index 6dceeac..981258e 100644
--- a/bibliopixel/project/importer.py
+++ b/bibliopixel/project/importer.py
@@ -1,4 +1,3 @@
-import json
 from . import load
 from . import fields
 from distutils.version import LooseVersion
diff --git a/bibliopixel/project/load.py b/bibliopixel/project/load.py
index 21b9110..17a2556 100644
--- a/bibliopixel/project/load.py
+++ b/bibliopixel/project/load.py
@@ -1,5 +1,6 @@
 from . import aliases
-import json, loady, os
+import loady, os
+from .. util import json
 
 ISNT_GIT_PATH_ERROR = """\
 Because the --isolate flag is set, all paths must start with //git.
@@ -8,14 +9,14 @@ Your path was %s."""
 guess_name = loady.importer.guess_name
 
 
-def data(name):
+def data(name, use_json=True):
     if not name:
         return {}
 
     try:
         return json.loads(name)
     except:
-        return loady.data.load(name, True)
+        return loady.data.load(name, use_json)
 
 
 def code(name, python_path=None):
diff --git a/bibliopixel/project/project.py b/bibliopixel/project/project.py
index d0cf25d..58b0769 100644
--- a/bibliopixel/project/project.py
+++ b/bibliopixel/project/project.py
@@ -1,6 +1,5 @@
-import copy
 from . import attributes, construct, cleanup, defaults, load, recurse
-from .. util import exception
+from .. util import exception, json
 
 
 class Project:
@@ -39,20 +38,18 @@ class Project:
             self.layout = self.construct_child(**layout)
         self.animation = create(animation, 'animation')
 
-    def make_animation(self):
-        return self.animation
-
 
 def project(*descs, **kwds):
+    def default(o):
+        if isinstance(o, type):
+            return str(o)
+        return str(o.__class__).replace('<class ', '<').replace('>', ' object>')
+
     desc = defaults.merge(*descs, **kwds)
+
     with load.extender(desc.get('path', '')):
         desc = recurse.recurse(desc)
-        return construct.construct(**desc)
-
-
-def read_project(location, threaded=None, default=None, **kwds):
-    project_data = load.data(location)
-    if threaded is not None:
-        project_data.setdefault('run', {})['threaded'] = threaded
 
-    return project(default, copy.deepcopy(project_data), **kwds)
+    project = construct.construct(**desc)
+    project.desc = desc
+    return project
diff --git a/bibliopixel/util/datafile.py b/bibliopixel/util/datafile.py
index b50ee1d..06fe587 100644
--- a/bibliopixel/util/datafile.py
+++ b/bibliopixel/util/datafile.py
@@ -1,22 +1,20 @@
-import copy, json
+import copy
+from . import json
 
 
 class DataFile(object):
-    def __init__(self, filename, open=open):
+    def __init__(self, filename):
         self.filename = filename
-        self.open = open
         self.read()
 
     def read(self):
         try:
-            fp = self.open(self.filename)
+            self.data = json.load(self.filename)
         except FileNotFoundError:
             self.data = {}
-        else:
-            self.data = json.load(fp)
 
     def write(self):
-        json.dump(self.data, self.open(self.filename, 'w'))
+        json.dump(self.data, self.filename)
 
     def get(self, key):
         return self.data.get(key)
diff --git a/bibliopixel/util/json.py b/bibliopixel/util/json.py
new file mode 100644
index 0000000..c7b9914
--- /dev/null
+++ b/bibliopixel/util/json.py
@@ -0,0 +1,41 @@
+import json, sys, yaml
+
+
+# Allow open to be patched for tests.
+open = __builtins__['open']
+
+
+def loads(s, filename=''):
+    if filename.endswith('.yml'):
+        return yaml.load(s)
+    return json.loads(s)
+
+
+def dump(data, file=sys.stdout, **kwds):
+    """Dump JSON data to a file or file handle"""
+    def dump(fp):
+        return json.dump(data, fp, indent=4, sort_keys=True, **kwds)
+
+    if not isinstance(file, str):
+        return dump(file)
+
+    with open(file, 'w') as fp:
+        return dump(fp)
+
+
+def load(filename):
+    """
+    Loads not only JSON files but also YAML files ending in .yml.
+    """
+    if isinstance(filename, str):
+        fp = open(filename)
+    else:
+        fp = filename
+        filename = getattr(fp, 'name', '')
+
+    try:
+        return loads(fp.read(), filename)
+
+    except Exception as e:
+        e.args = ('There was a error in the data file', filename) + e.args
+        raise
diff --git a/projects/01-matrix.json b/projects/01-matrix.json
deleted file mode 100644
index 2d96a51..0000000
--- a/projects/01-matrix.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
-    "driver": {
-        "num": 1024,
-        "typename": "simpixel"
-    },
-
-    "layout": {
-        "typename": "matrix",
-        "width": 32,
-        "height": 32
-    },
-
-    "run": {
-        "fps": 30,
-        "seconds": 14
-    },
-
-    "aliases": {
-        "bpa": "BiblioPixelAnimations.matrix"
-    },
-
-    "animation": {
-        "typename": "sequence",
-        "slideshow": 2,
-        "animations": [
-            "@bpa.ImageAnim",
-            "@bpa.ImageShow",
-            "@bpa.ImageDissolve",
-            "@bpa.ScreenGrab",
-            "@bpa.AnalogClock",
-            "@bpa.AnalogClock",
-            "@bpa.BinaryClock.MatrixBinaryClock",
-            "@bpa.CPUUsage",
-            "@bpa.GameOfLife",
-            "@bpa.GameOfLife.GameOfLifeRGB",
-            "@bpa.GradientClock",
-            "@bpa.LangtonsAnt",
-            "@bpa.LangtonsAnt.LangtonsAntRainbow",
-            "@bpa.Mainframe",
-            "@bpa.MatrixRain",
-            "@bpa.MatrixRain.MatrixRainBow",
-            "@bpa.SpinningTriangle",
-            "@bpa.TallClock",
-            "@bpa.Text.BounceText",
-            "@bpa.Text.ScrollText",
-            "@bpa.TicTacToe",
-            "@bpa.bloom",
-            "@bpa.circlepop",
-            "@bpa.perlin_simplex",
-            "@bpa.pinwheel"
-        ]
-    }
-}
diff --git a/projects/01-matrix.yml b/projects/01-matrix.yml
new file mode 100644
index 0000000..9505215
--- /dev/null
+++ b/projects/01-matrix.yml
@@ -0,0 +1,45 @@
+driver:
+    num: 1024
+    typename: simpixel
+
+layout:
+    typename: matrix
+    width: 32
+    height: 32
+
+run:
+    fps: 30
+    seconds: 14
+
+aliases:
+    bpa: BiblioPixelAnimations.matrix
+
+animation:
+    typename: sequence
+    slideshow: 2
+    animations:
+        - "@bpa.ImageAnim"
+        - "@bpa.ImageShow"
+        - "@bpa.ImageDissolve"
+        - "@bpa.ScreenGrab"
+        - "@bpa.AnalogClock"
+        - "@bpa.AnalogClock"
+        - "@bpa.BinaryClock.MatrixBinaryClock"
+        - "@bpa.CPUUsage"
+        - "@bpa.GameOfLife"
+        - "@bpa.GameOfLife.GameOfLifeRGB"
+        - "@bpa.GradientClock"
+        - "@bpa.LangtonsAnt"
+        - "@bpa.LangtonsAnt.LangtonsAntRainbow"
+        - "@bpa.Mainframe"
+        - "@bpa.MatrixRain"
+        - "@bpa.MatrixRain.MatrixRainBow"
+        - "@bpa.SpinningTriangle"
+        - "@bpa.TallClock"
+        - "@bpa.Text.BounceText"
+        - "@bpa.Text.ScrollText"
+        - "@bpa.TicTacToe"
+        - "@bpa.bloom"
+        - "@bpa.circlepop"
+        - "@bpa.perlin_simplex"
+        - "@bpa.pinwheel"
diff --git a/requirements.txt b/requirements.txt
index 3d4b446..42fb9f4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
-loady
-flask
 BiblioPixelAnimations
 BiblioPixelTriggers
+flask
+loady
+pyyaml


Test patch:
diff --git a/test/bibliopixel/mock.py b/test/bibliopixel/mock.py
index cd6a7c0..a1ccc9e 100644
--- a/test/bibliopixel/mock.py
+++ b/test/bibliopixel/mock.py
@@ -1,28 +1,39 @@
 from argparse import Namespace
+from . import patch
 
 
 class MockFP(object):
-    def __init__(self, filename, context):
+    def __init__(self, filename, filesystem):
         self.filename = filename
-        self.context = context
+        self.filesystem = filesystem
         self.write_started = False
 
     def read(self):
-        return self.context[self.filename]
+        return self.filesystem[self.filename]
 
     def write(self, x):
         if self.write_started:
-            self.context[self.filename] += x
+            self.filesystem[self.filename] += x
         else:
             self.write_started = True
-            self.context[self.filename] = x
+            self.filesystem[self.filename] = x
 
     def __iter__(self):
         return iter(self.read().splitlines())
 
+    def __enter__(self):
+        return self
 
-def mock_open(context):
+    def __exit__(self, type, value, traceback):
+        pass
+
+
+def mock_open(filesystem):
     def open(filename, access='r'):
-        return MockFP(filename, context)
+        return MockFP(filename, filesystem)
 
     return open
+
+
+def patch_open(filesystem, object):
+    return patch.patch(object, 'open', mock_open(filesystem))
diff --git a/test/bibliopixel/project/bad_project_test.py b/test/bibliopixel/project/bad_project_test.py
index bbc2420..59390bb 100644
--- a/test/bibliopixel/project/bad_project_test.py
+++ b/test/bibliopixel/project/bad_project_test.py
@@ -65,19 +65,14 @@ class BadProjectTest(unittest.TestCase):
 
         self.assertEquals(
             e.exception.args[0::2],
-            ('There was a JSON error in the file',
+            ('There was a error in the data file',
              'Expecting value: line 1 column 1 (char 0)'))
 
     def test_cant_open(self):
         with self.assertRaises(FileNotFoundError) as e:
             make('this-file-does-not-exist.json')
 
-        intro = e.exception.args[:2]
-        tail = e.exception.args[-2:]
-        self.assertEquals(
-            intro, ('There was an error reading the file',
-                    'this-file-does-not-exist.json'))
-        self.assertEquals(tail, (2, 'No such file or directory'))
+        self.assertEquals(e.exception.args, (2, 'No such file or directory'))
 
     def test_bad_section(self):
         with self.assertRaises(ValueError) as e:
diff --git a/test/bibliopixel/project/make.py b/test/bibliopixel/project/make.py
index 1c696d8..ba87373 100644
--- a/test/bibliopixel/project/make.py
+++ b/test/bibliopixel/project/make.py
@@ -1,5 +1,6 @@
-import json, tempfile
+import tempfile
 from bibliopixel.project import project
+from bibliopixel.util import json
 from .. mark_tests import SKIP_LONG_TESTS
 
 
@@ -10,8 +11,9 @@ def make(data, run_start=not SKIP_LONG_TESTS):
         fp.seek(0)
         data = fp.name
 
-    pr = project.read_project(data)
-    animation = pr.make_animation()
+    desc = json.load(data)
+    pr = project.project(desc)
+    animation = pr.animation
     if run_start:
         animation.start()
 
diff --git a/test/bibliopixel/project/project.yml b/test/bibliopixel/project/project.yml
new file mode 100644
index 0000000..a7617d0
--- /dev/null
+++ b/test/bibliopixel/project/project.yml
@@ -0,0 +1,13 @@
+driver:
+    typename: bibliopixel.drivers.dummy_driver.Dummy
+    num: 12
+
+layout:
+    typename: bibliopixel.layout.strip.Strip
+
+
+animation:
+    typename: bibliopixel.animation.tests.StripChannelTest
+
+run:
+    max_steps: 5
diff --git a/test/bibliopixel/project/project_test.py b/test/bibliopixel/project/project_test.py
index 96c2c21..c8e6867 100644
--- a/test/bibliopixel/project/project_test.py
+++ b/test/bibliopixel/project/project_test.py
@@ -27,6 +27,9 @@ class ProjectTest(unittest.TestCase):
     def test_file(self):
         make('test/bibliopixel/project/project.json', False)
 
+    def test_yaml_file(self):
+        make('test/bibliopixel/project/project.yml', False)
+
     def test_multi(self):
         animation = make(PROJECT_MULTI)
         k = [d._kwds for d in animation.layout.drivers]
diff --git a/test/bibliopixel/util/datafile_test.py b/test/bibliopixel/util/datafile_test.py
index 52c76d3..88378d2 100644
--- a/test/bibliopixel/util/datafile_test.py
+++ b/test/bibliopixel/util/datafile_test.py
@@ -1,6 +1,6 @@
-import io, json, unittest
-from .. mock import mock_open
-
+import io, unittest
+from .. import mock
+from bibliopixel.util import json
 from bibliopixel.util.datafile import DataFile
 
 DEFAULTS = {
@@ -16,11 +16,11 @@ JSON_TEST = """
 
 class DatafileTest(unittest.TestCase):
     def test_reader_writer_json(self):
-        context = {'test.json': JSON_TEST}
-        mopen = mock_open(context)
-        datafile = DataFile('test.json', open=mopen)
-        datafile.read()
-        self.assertEqual(datafile.data, {'a': {'foo': 'bar', 'bang': 1}})
-        datafile.data = {'bang': {'hi': 'there'}}
-        datafile.write()
-        self.assertEqual(json.loads(context['test.json']), datafile.data)
+        filesystem = {'test.json': JSON_TEST}
+        with mock.patch_open(filesystem, json):
+            datafile = DataFile('test.json')
+            datafile.read()
+            self.assertEqual(datafile.data, {'a': {'foo': 'bar', 'bang': 1}})
+            datafile.data = {'bang': {'hi': 'there'}}
+            datafile.write()
+            self.assertEqual(json.loads(filesystem['test.json']), datafile.data)