├── .gitignore ├── .travis.yml ├── COPYING ├── README.rst ├── jsonsempai ├── __init__.py ├── magic.py ├── sempai.py └── tests │ ├── __init__.py │ └── test_sempai.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Distribution / packaging 6 | .Python 7 | env/ 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.2" 6 | - "3.3" 7 | - "3.4" 8 | - "pypy" 9 | sudo: false 10 | install: pip install -e . 11 | script: python setup.py test 12 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Louis Taylor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | json-sempai 3 | =========== 4 | 5 | .. image:: https://travis-ci.org/kragniz/json-sempai.svg?branch=master 6 | :target: https://travis-ci.org/kragniz/json-sempai 7 | 8 | .. image:: https://img.shields.io/pypi/v/json-sempai.svg 9 | :target: https://pypi.python.org/pypi/json-sempai 10 | 11 | Have you ever been kept awake at night, desperately feeling a burning desire to 12 | do nothing else but directly import JSON files as if they were python modules 13 | [#]_? Now you can! 14 | 15 | This abomination allows you to write 16 | 17 | .. code:: python 18 | 19 | import some_json_file 20 | 21 | and if ``some_json_file.json`` can be found, it will be available as if it is a 22 | python module. 23 | 24 | Usage 25 | ----- 26 | 27 | Slap a json file somewhere on your python path. ``tester.json``: 28 | 29 | .. code:: json 30 | 31 | { 32 | "hello": "world", 33 | "this": { 34 | "can": { 35 | "be": "nested" 36 | } 37 | } 38 | } 39 | 40 | Now import jsonsempai and your json file! 41 | 42 | .. code:: python 43 | 44 | >>> from jsonsempai import magic 45 | >>> import tester 46 | >>> tester 47 | 48 | >>> tester.hello 49 | u'world' 50 | >>> tester.this.can.be 51 | u'nested' 52 | >>> 53 | 54 | Alternatively, a context manager may be used (100% less magic): 55 | 56 | .. code:: python 57 | 58 | >>> import jsonsempai 59 | >>> with jsonsempai.imports(): 60 | ... import tester 61 | >>> tester 62 | 63 | 64 | 65 | Python packages are also supported: 66 | 67 | .. code:: bash 68 | 69 | $ tree 70 | . 71 | └── python_package 72 | ├── file.json 73 | ├── __init__.py 74 | └── nested_package 75 | ├── __init__.py 76 | └── second.json 77 | 78 | .. code:: python 79 | 80 | >>> from jsonsempai import magic 81 | >>> from python_package import file 82 | >>> file 83 | 84 | >>> import python_package.nested_package.second 85 | >>> python_package.nested_package.second 86 | 87 | 88 | 89 | Installing 90 | ---------- 91 | 92 | Install from pip: 93 | 94 | .. code:: bash 95 | 96 | $ pip install json-sempai 97 | 98 | or clone this repo and install from source: 99 | 100 | .. code:: bash 101 | 102 | $ python setup.py install 103 | 104 | To purge this horror from your machine: 105 | 106 | .. code:: bash 107 | 108 | $ pip uninstall json-sempai 109 | 110 | .. [#] Disclaimer: Only do this if you hate yourself and the rest of the world. 111 | -------------------------------------------------------------------------------- /jsonsempai/__init__.py: -------------------------------------------------------------------------------- 1 | from .sempai import imports 2 | from . import tests 3 | -------------------------------------------------------------------------------- /jsonsempai/magic.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .sempai import SempaiLoader 4 | 5 | sys.meta_path.append(SempaiLoader) 6 | -------------------------------------------------------------------------------- /jsonsempai/sempai.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import imp 3 | import json 4 | import os 5 | import sys 6 | 7 | 8 | class DottedDict(dict): 9 | 10 | def __getattr__(self, attr): 11 | try: 12 | return self[attr] 13 | except KeyError: 14 | raise AttributeError("'{}'".format(attr)) 15 | 16 | __setattr__ = dict.__setitem__ 17 | 18 | __delattr__ = dict.__delitem__ 19 | 20 | 21 | def get_json_path(directory, name): 22 | json_path = os.path.join(directory, '{name}.json'.format(name=name)) 23 | if os.path.isfile(json_path): 24 | return json_path 25 | 26 | 27 | class SempaiLoader(object): 28 | def __init__(self, json_path): 29 | self.json_path = json_path 30 | 31 | @classmethod 32 | def find_module(cls, name, path=None): 33 | for d in sys.path: 34 | json_path = get_json_path(d, name) 35 | if json_path is not None: 36 | return cls(json_path) 37 | 38 | if path is not None: 39 | name = name.split('.')[-1] 40 | for d in path: 41 | json_path = get_json_path(d, name) 42 | if json_path is not None: 43 | return cls(json_path) 44 | 45 | 46 | def load_module(self, name): 47 | if name in sys.modules: 48 | return sys.modules[name] 49 | 50 | mod = imp.new_module(name) 51 | mod.__file__ = self.json_path 52 | mod.__loader__ = self 53 | 54 | decoder = json.JSONDecoder(object_hook=DottedDict) 55 | 56 | try: 57 | with open(self.json_path) as f: 58 | d = decoder.decode(f.read()) 59 | except ValueError: 60 | raise ImportError( 61 | '"{name}" does not contain valid json.'.format(name=self.json_path)) 62 | except: 63 | raise ImportError( 64 | 'Could not open "{name}".'.format(name=self.json_path)) 65 | 66 | mod.__dict__.update(d) 67 | 68 | sys.modules[name] = mod 69 | return mod 70 | 71 | 72 | @contextlib.contextmanager 73 | def imports(): 74 | try: 75 | sys.meta_path.append(SempaiLoader) 76 | yield 77 | finally: 78 | sys.meta_path.remove(SempaiLoader) 79 | -------------------------------------------------------------------------------- /jsonsempai/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kragniz/json-sempai/1e0fb4f28f94dd29cee320cdf32e843dd42c891b/jsonsempai/tests/__init__.py -------------------------------------------------------------------------------- /jsonsempai/tests/test_sempai.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | import tempfile 5 | import unittest 6 | 7 | import jsonsempai 8 | 9 | TEST_FILE = '''{ 10 | "three": 3, 11 | "one": { 12 | "two": { 13 | "three": 3 14 | } 15 | }, 16 | "array": [ 17 | {"nested": "but dotted"}, 18 | {"array": [ 19 | {"and_nested_again": "dotted"} 20 | ]} 21 | ], 22 | "lots_of_lists": [[{"in_da_list": true}]] 23 | }''' 24 | 25 | 26 | class TestSempai(unittest.TestCase): 27 | 28 | def setUp(self): 29 | self.direc = tempfile.mkdtemp(prefix='jsonsempai') 30 | sys.path.append(self.direc) 31 | 32 | with open(os.path.join(self.direc, 'sempai.json'), 'w') as f: 33 | f.write(TEST_FILE) 34 | 35 | def tearDown(self): 36 | sys.path.remove(self.direc) 37 | shutil.rmtree(self.direc) 38 | 39 | def test_import(self): 40 | with jsonsempai.imports(): 41 | import sempai 42 | self.assertTrue(sempai is not None) 43 | 44 | def test_access(self): 45 | with jsonsempai.imports(): 46 | import sempai 47 | self.assertEqual(3, sempai.three) 48 | 49 | def test_access_nested(self): 50 | with jsonsempai.imports(): 51 | import sempai 52 | self.assertEqual(3, sempai.one.two.three) 53 | 54 | def test_acts_like_dict(self): 55 | with jsonsempai.imports(): 56 | import sempai 57 | self.assertEqual({"three": 3}, sempai.one.two) 58 | 59 | def test_set(self): 60 | with jsonsempai.imports(): 61 | import sempai 62 | sempai.one.two.three = 4 63 | self.assertEqual(4, sempai.one.two.three) 64 | 65 | def test_del(self): 66 | with jsonsempai.imports(): 67 | import sempai 68 | del sempai.one.two.three 69 | self.assertEqual('not at home', 70 | sempai.one.two.get('three', 'not at home')) 71 | 72 | def test_location(self): 73 | del sys.modules['sempai'] # force the module to be reloaded 74 | with jsonsempai.imports(): 75 | import sempai 76 | self.assertEqual(os.path.join(self.direc, 'sempai.json'), 77 | sempai.__file__) 78 | 79 | def test_array(self): 80 | with jsonsempai.imports(): 81 | import sempai 82 | self.assertEqual({"nested": "but dotted"}, 83 | sempai.array[0]) 84 | def test_array_dotting(self): 85 | with jsonsempai.imports(): 86 | import sempai 87 | self.assertEqual('but dotted', sempai.array[0].nested) 88 | 89 | def test_array_nested_dotting(self): 90 | with jsonsempai.imports(): 91 | import sempai 92 | self.assertEqual('dotted', sempai.array[1].array[0].and_nested_again) 93 | 94 | def test_obj_in_list_in_list(self): 95 | with jsonsempai.imports(): 96 | import sempai 97 | self.assertTrue(sempai.lots_of_lists[0][0].in_da_list) 98 | 99 | def test_import_invalid_file(self): 100 | with open(os.path.join(self.direc, 'invalid.json'), 'w') as f: 101 | f.write('not a valid json file') 102 | 103 | with jsonsempai.imports(): 104 | self.assertRaises(ImportError, __import__, 'invalid') 105 | 106 | 107 | class TestSempaiPackages(unittest.TestCase): 108 | 109 | def setUp(self): 110 | self.direc = tempfile.mkdtemp(prefix='jsonsempai') 111 | sys.path.append(self.direc) 112 | 113 | python_package = os.path.join(self.direc, 'python_package') 114 | os.makedirs(python_package) 115 | 116 | open(os.path.join(python_package, '__init__.py'), 'w').close() 117 | 118 | with open(os.path.join(python_package, 'nested.json'), 'w') as f: 119 | f.write(TEST_FILE) 120 | 121 | def tearDown(self): 122 | sys.path.remove(self.direc) 123 | shutil.rmtree(self.direc) 124 | 125 | def test_import_from_package(self): 126 | with jsonsempai.imports(): 127 | from python_package import nested 128 | 129 | self.assertEqual(3, nested.three) 130 | 131 | def test_import_package(self): 132 | with jsonsempai.imports(): 133 | import python_package.nested 134 | 135 | self.assertEqual(3, python_package.nested.three) 136 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | with open('README.rst') as file_readme: 9 | readme = file_readme.read() 10 | 11 | setup(name='json-sempai', 12 | version='0.4.0', 13 | description='Use JSON files as if they\'re python modules', 14 | long_description=readme, 15 | author='Louis Taylor', 16 | author_email='kragniz@gmail.com', 17 | license='MIT', 18 | url='https://github.com/kragniz/json-sempai', 19 | classifiers=[ 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Programming Language :: Python', 23 | 'Programming Language :: Python :: 2.6', 24 | 'Programming Language :: Python :: 2.7', 25 | 'Programming Language :: Python :: 3.2', 26 | 'Programming Language :: Python :: 3.3', 27 | 'Programming Language :: Python :: 3.4', 28 | ], 29 | keywords='please don\'t use this library for anything', 30 | packages=['jsonsempai', 'jsonsempai.tests'], 31 | test_suite='jsonsempai.tests' 32 | ) 33 | --------------------------------------------------------------------------------