├── tests ├── __init__.py ├── env1 │ ├── arb_file │ ├── the_thing.txt │ ├── raw_file │ ├── json_config2 │ ├── yaml_config │ ├── json_file │ ├── plugins │ │ ├── thing.py │ │ └── widgets │ │ │ └── nublet.py │ ├── default.cfg │ └── json_config ├── state_tests.py ├── env_tests.py ├── file_tests.py └── config_tests.py ├── example ├── README └── duckman │ ├── duckman │ ├── ducks.py │ ├── plugins │ │ └── core_plugin.py │ ├── default.cfg │ └── __init__.py │ ├── user_plugins │ └── example.py │ └── setup.py ├── setup.cfg ├── requirements.txt ├── scripts ├── projectname.sh ├── tag.sh ├── release.sh ├── functions.sh └── deletebranch.sh ├── .travis.yml ├── doc ├── code.rst ├── index.rst ├── Makefile └── conf.py ├── .gitignore ├── setup.py ├── scruffy ├── __init__.py ├── plugin.py ├── state.py ├── env.py ├── config.py └── file.py ├── LICENSE └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/env1/arb_file: -------------------------------------------------------------------------------- 1 | arb_file value -------------------------------------------------------------------------------- /tests/env1/the_thing.txt: -------------------------------------------------------------------------------- 1 | thing -------------------------------------------------------------------------------- /tests/env1/raw_file: -------------------------------------------------------------------------------- 1 | raw_file value 2 | -------------------------------------------------------------------------------- /example/README: -------------------------------------------------------------------------------- 1 | See the main Scruffy README -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML>=3.11 2 | six>=1.10.0 3 | -------------------------------------------------------------------------------- /scripts/projectname.sh: -------------------------------------------------------------------------------- 1 | DISTRIBUTION_NAME="$(python3 ./setup.py --name)" 2 | -------------------------------------------------------------------------------- /tests/env1/json_config2: -------------------------------------------------------------------------------- 1 | { 2 | "setting1": 888, 3 | "setting2": true 4 | } -------------------------------------------------------------------------------- /tests/env1/yaml_config: -------------------------------------------------------------------------------- 1 | setting1: 666 2 | setting2: true 3 | setting3: 4 | key1: value 5 | key2: value 6 | -------------------------------------------------------------------------------- /tests/env1/json_file: -------------------------------------------------------------------------------- 1 | { 2 | "setting1": 667, 3 | "setting3": { 4 | "key1": "not value" 5 | } 6 | } -------------------------------------------------------------------------------- /example/duckman/duckman/ducks.py: -------------------------------------------------------------------------------- 1 | class Duck(object): pass 2 | 3 | class FlatDuck(Duck): pass 4 | 5 | class UprightDuck(Duck): pass -------------------------------------------------------------------------------- /tests/env1/plugins/thing.py: -------------------------------------------------------------------------------- 1 | from scruffy.plugin import Plugin 2 | 3 | class ThingPlugin(Plugin): 4 | def do_a_thing(self): 5 | return 666 -------------------------------------------------------------------------------- /tests/env1/plugins/widgets/nublet.py: -------------------------------------------------------------------------------- 1 | from scruffy.plugin import Plugin 2 | 3 | class ThangPlugin(Plugin): 4 | def do_a_thing(self): 5 | return 777 -------------------------------------------------------------------------------- /tests/env1/default.cfg: -------------------------------------------------------------------------------- 1 | { 2 | "setting1": 666, 3 | "setting2": true, 4 | "setting3": { 5 | "key1": "value", 6 | "key2": "value" 7 | } 8 | } -------------------------------------------------------------------------------- /tests/env1/json_config: -------------------------------------------------------------------------------- 1 | { 2 | "setting1": 667, 3 | "setting3": { 4 | "key1": "not value" 5 | }, 6 | "somedir": "/tmp/scruffy_test_dir", 7 | "somefile": "tests/env1/the_thing.txt" 8 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | - "3.6" 6 | - "3.7" 7 | - "3.8" 8 | arch: 9 | - amd64 10 | - ppc64le 11 | install: 12 | - pip install . 13 | - pip install -r requirements.txt 14 | script: nosetests 15 | -------------------------------------------------------------------------------- /example/duckman/duckman/plugins/core_plugin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from scruffy import Plugin 3 | 4 | log = logging.getLogger('main') 5 | 6 | class SomePlugin(Plugin): 7 | def do_a_thing(self): 8 | log.info("{}.{} is doing a thing!".format(__name__, self.__class__.__name__)) -------------------------------------------------------------------------------- /example/duckman/user_plugins/example.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from scruffy import Plugin 3 | 4 | log = logging.getLogger('main') 5 | 6 | class SomeOtherPlugin(Plugin): 7 | def do_a_thing(self): 8 | log.info("{}.{} is doing a thing!".format(__name__, self.__class__.__name__)) -------------------------------------------------------------------------------- /doc/code.rst: -------------------------------------------------------------------------------- 1 | Scruffy API 2 | *********** 3 | 4 | .. automodule:: scruffy.config 5 | :members: 6 | .. automodule:: scruffy.env 7 | :members: 8 | .. automodule:: scruffy.file 9 | :members: 10 | .. automodule:: scruffy.plugin 11 | :members: 12 | .. automodule:: scruffy.state 13 | :members: 14 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. scruffy documentation master file, created by 2 | sphinx-quickstart on Sun May 15 13:30:45 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Scruffy documentation 7 | ===================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | code 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /example/duckman/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name = "duckman", 7 | version = "0.1", 8 | author = "snare", 9 | author_email = "snare@ho.ax", 10 | description = ("Ya thrust yer pelvis HUAGHH"), 11 | license = "Buy snare a beer", 12 | keywords = "duckman", 13 | url = "https://github.com/snare/scruffy", 14 | packages=['duckman'], 15 | entry_points = { 16 | 'console_scripts': ['duckman = duckman:main'] 17 | }, 18 | install_requires = ['scruffington'] 19 | ) 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | *.py[cod] 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Rope 40 | .ropeproject 41 | 42 | .DS_Store 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.rst", "r") as fp: 4 | long_description = fp.read() 5 | 6 | setup( 7 | name="scruffington", 8 | version="0.3.9", 9 | author="snare", 10 | author_email="snare@ho.ax", 11 | long_description=long_description, 12 | long_description_content_type="text/x-rst", 13 | description=("The janitor"), 14 | license="MIT", 15 | keywords="scruffy", 16 | url="https://github.com/snare/scruffy", 17 | packages=['scruffy'], 18 | install_requires=['pyyaml', 'six'], 19 | ) 20 | -------------------------------------------------------------------------------- /scruffy/__init__.py: -------------------------------------------------------------------------------- 1 | from .env import Environment 2 | from .file import File, LogFile, LockFile, Directory, PluginDirectory, PackageDirectory, PackageFile 3 | from .plugin import PluginRegistry, Plugin, PluginManager 4 | from .config import ConfigNode, Config, ConfigEnv, ConfigFile, ConfigApplicator 5 | from .state import State 6 | 7 | __all__ = [ 8 | "Environment", 9 | "Directory", "PluginDirectory", "PackageDirectory", "PackageFile", 10 | "File", "LogFile", "LockFile", 11 | "PluginRegistry", "Plugin", "PluginManager", 12 | "ConfigNode", "Config", "ConfigEnv", "ConfigFile", "ConfigApplicator", 13 | "State" 14 | ] 15 | -------------------------------------------------------------------------------- /scripts/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | source "$(dirname $0)"/functions.sh 4 | 5 | if ! branch_is_master; 6 | then 7 | quit "Attempting to tag from branch $branch. Check out 'master' first." 1 8 | fi 9 | 10 | 11 | 12 | if ! branch_is_clean; 13 | then 14 | echo "Tree contains uncommitted modifications:" 15 | git ls-files -m 16 | quit 1 17 | fi 18 | 19 | version=$(current_version); 20 | 21 | if version_is_tagged "$version"; 22 | then 23 | echo "Version $version already tagged." 24 | git tag -l 25 | quit 1 26 | fi 27 | 28 | echo "Tagging version: $version" 29 | 30 | git tag -a "v$version" -m "version $version" || quit "Failed to tag $version" $? 31 | 32 | echo "Tags:" 33 | git tag -l 34 | 35 | quit "Done." 0 36 | -------------------------------------------------------------------------------- /example/duckman/duckman/default.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # default.cfg for duckman 3 | # 4 | 5 | # some heathens prefer upright ducks I guess 6 | duck_pref: upright 7 | 8 | # temporary sqlite database 9 | db_url: sqlite:////tmp/duckman/duckman.db 10 | 11 | # logging stuff 12 | logging: 13 | # dict_config is passed to logging.config.dictConfig() to configure logging 14 | dict_config: 15 | version: 1 16 | formatters: 17 | standard: {'fmt': '%(asctime)s [%(levelname)s] %(message)s'} 18 | loggers: 19 | main: 20 | handlers: [] 21 | level: INFO 22 | propagate: false 23 | 24 | # these two vars are substituted into the Directory and LogFile entries 25 | log_dir: /tmp/duckman 26 | log_file: duckman.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Loukas K 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | DIRNAME="$(dirname $0)" 3 | 4 | # set DISTRIBUTION_NAME variable 5 | source "$DIRNAME"/projectname.sh 6 | 7 | # utility functions 8 | source "$DIRNAME"/functions.sh 9 | 10 | if ! branch_is_master; 11 | then 12 | quit "Checkout branch 'master' before generating release." 1 13 | fi 14 | 15 | if ! branch_is_clean; 16 | then 17 | echo "Tree contains uncommitted modifications:" 18 | git ls-files -m 19 | quit 1 20 | fi 21 | version=$(current_version); 22 | 23 | if ! version_is_tagged "$version"; 24 | then 25 | echo "Current version $version isn't tagged." 26 | echo "Attempting to tag..." 27 | "$DIRNAME"/tag.sh || quit "Failed to tag a release." 1 28 | fi 29 | 30 | generate_dist(){ 31 | python3 setup.py sdist bdist_wheel || quit "Failed to generate source & binary distributions." 1 32 | } 33 | 34 | version=$(current_version); 35 | 36 | generate_dist; 37 | echo "About to post the following distribution files to pypi.org." 38 | ls -1 dist/"$DISTRIBUTION_NAME"-$version.* 39 | 40 | if prompt_yes_no; 41 | then 42 | python3 -m twine upload dist/"$DISTRIBUTION_NAME"-$version* 43 | fi 44 | 45 | -------------------------------------------------------------------------------- /tests/state_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | try: 3 | import sqlalchemy 4 | HAVE_SQLALCHEMY = True 5 | except: 6 | HAVE_SQLALCHEMY = False 7 | 8 | from nose.tools import * 9 | from scruffy.state import * 10 | 11 | STATE_FILE = 'test.state' 12 | 13 | def setup(): 14 | State(STATE_FILE).cleanup() 15 | 16 | def test_state(): 17 | s = State(STATE_FILE) 18 | s['xxx'] = 1 19 | assert s.d == {'xxx': 1} 20 | s.save() 21 | assert os.path.exists(STATE_FILE) 22 | s2 = State(STATE_FILE) 23 | assert s2['xxx'] == 1 24 | s.d = {} 25 | s.load() 26 | assert s['xxx'] == 1 27 | s.cleanup() 28 | assert not os.path.exists(STATE_FILE) 29 | 30 | def test_with(): 31 | with State(STATE_FILE) as s: 32 | s['yyy'] = 123 33 | s2 = State(STATE_FILE) 34 | assert s2['yyy'] == 123 35 | s2.cleanup() 36 | 37 | if HAVE_SQLALCHEMY: 38 | def test_db_state(): 39 | url='sqlite:///' 40 | s = DBState.state(url) 41 | s['xxx'] = 1 42 | s.save() 43 | assert s.d == {'xxx': 1} 44 | s2 = DBState.state(url) 45 | assert s2['xxx'] == 1 46 | s.cleanup() 47 | assert s['xxx'] == None 48 | 49 | def test_db_state_with(): 50 | url='sqlite:///' 51 | with DBState.state(url) as s: 52 | s['yyy'] = 123 53 | s2 = DBState.state(url) 54 | assert s2['yyy'] == 123 55 | s2.cleanup() 56 | -------------------------------------------------------------------------------- /scripts/functions.sh: -------------------------------------------------------------------------------- 1 | # In shell, success is exit(0), error is anything else, e.g., exit(1) 2 | SUCCESS=0 3 | FAILURE=1 4 | 5 | quit(){ 6 | if [ $# -gt 1 ]; 7 | then 8 | echo $1 9 | shift 10 | fi 11 | exit $1 12 | } 13 | 14 | branch_is_master(){ 15 | local branch=$(git rev-parse --abbrev-ref HEAD) 16 | if [ $branch == "master" ]; 17 | then 18 | return $SUCCESS; 19 | else 20 | return $FAILURE; 21 | fi 22 | } 23 | 24 | branch_is_clean(){ 25 | local modified=$(git ls-files -m) || quit "Unable to check for modified files." $? 26 | if [ -z "$modified" ]; 27 | then 28 | return $SUCCESS; 29 | else 30 | return $FAILURE; 31 | fi 32 | } 33 | 34 | current_version() { 35 | local version="$(python ./setup.py --version)" || quit "Unable to detect package version" $? 36 | printf "%s" "$version" 37 | } 38 | 39 | version_is_tagged(){ 40 | local version="$1" 41 | # e.g., verion = 0.1.0 42 | # check if git tag -l v0.1.0 exists 43 | tag_description=$(git tag -l v"$version") 44 | if [ ! -z "$tag_description" ]; 45 | then 46 | return $SUCCESS; 47 | else 48 | return $FAILURE; 49 | fi 50 | } 51 | 52 | prompt_yes_no(){ 53 | prompt_string="$1" 54 | read -p "$prompt_string [Y/n] " response 55 | 56 | case $response in 57 | [yY][eE][sS]|[yY]) 58 | return $SUCCESS 59 | ;; 60 | *) 61 | return $FAILURE 62 | ;; 63 | esac 64 | } 65 | -------------------------------------------------------------------------------- /example/duckman/duckman/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from scruffy import * 4 | 5 | from .ducks import * 6 | 7 | log = logging.getLogger('main') 8 | 9 | 10 | def main(): 11 | # Set up the Environment and load the config 12 | e = Environment( 13 | # Main user directory in ~/.duckman, create it if it doesn't exist 14 | dir=Directory('~/.duckman', create=True, 15 | # Config file inside ~/.duckman, defaults loaded from default.cfg 16 | # in the duckman package 17 | config=ConfigFile('config', defaults=File('default.cfg', parent=PackageDirectory())), 18 | 19 | # User plugins directory at ~/.duckman/plugins 20 | plugins=PluginDirectory('plugins', create=True) 21 | ), 22 | 23 | # Plugins directory included with the duckman package. 24 | plugins_dir=PluginDirectory('plugins', parent=PackageDirectory()), 25 | 26 | # Log directory in a location specified in the config file, create it 27 | # if it doesn't exist 28 | log_dir=Directory('{config:logging.log_dir}', create=True, 29 | # Main log file named as per the configuration. A handler will 30 | # be created for this log file and added to the logger 'main' 31 | main_log=LogFile('{config:logging.log_file}', logger='main', formatter='standard') 32 | ) 33 | ) 34 | 35 | print("Set up environment in {}".format(e.dir.path)) 36 | print("Logging to {}".format(e.log_dir.main_log.path)) 37 | 38 | log.info("=== Start of duckman log ===") 39 | 40 | # Instantiate any plugins that were registered and tell them to do a thing 41 | for p in e.plugins: 42 | log.info("Initialising plugin {}".format(p)) 43 | p().do_a_thing() 44 | 45 | # Check our duck preference 46 | if e.config.duck_pref == 'upright': 47 | log.warn("User is an upright heathen.") 48 | elif e.config.duck_pref == 'flat': 49 | log.info("User is a flat friend *brofist*.") 50 | elif e.config.duck_pref == 'rubber': 51 | log.info("User must be dominicgs") 52 | else: 53 | log.error("Umm, {} is not a type of duck".format(e.config.duck_pref)) 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /scruffy/plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin 3 | ------ 4 | 5 | Classes for representing and loading plugins. 6 | """ 7 | import os 8 | import importlib 9 | import six 10 | 11 | 12 | class PluginRegistry(type): 13 | """ 14 | Metaclass that registers any classes using it in the `plugins` array 15 | """ 16 | plugins = [] 17 | def __init__(cls, name, bases, attrs): 18 | if name != 'Plugin' and cls.__name__ not in map(lambda x: x.__name__, PluginRegistry.plugins): 19 | PluginRegistry.plugins.append(cls) 20 | 21 | 22 | @six.add_metaclass(PluginRegistry) 23 | class Plugin(object): 24 | """ 25 | Top-level plugin class, using the PluginRegistry metaclass. 26 | 27 | All plugin modules must implement a single subclass of this class. This 28 | subclass will be the class collected in the PluginRegistry, and should 29 | contain references to any other resources required within the module. 30 | """ 31 | 32 | 33 | class PluginManager(object): 34 | """ 35 | Loads plugins which are automatically registered with the PluginRegistry 36 | class, and provides an interface to the plugin collection. 37 | """ 38 | def load_plugins(self, directory): 39 | """ 40 | Loads plugins from the specified directory. 41 | 42 | `directory` is the full path to a directory containing python modules 43 | which each contain a subclass of the Plugin class. 44 | 45 | There is no criteria for a valid plugin at this level - any python 46 | module found in the directory will be loaded. Only modules that 47 | implement a subclass of the Plugin class above will be collected. 48 | 49 | The directory will be traversed recursively. 50 | """ 51 | # walk directory 52 | for filename in os.listdir(directory): 53 | # path to file 54 | filepath = os.path.join(directory, filename) 55 | 56 | # if it's a file, load it 57 | modname, ext = os.path.splitext(filename) 58 | if os.path.isfile(filepath) and ext == '.py': 59 | spec = importlib.util.spec_from_file_location(modname, filepath) 60 | if spec: 61 | mod = importlib.util.module_from_spec(spec) 62 | spec.loader.exec_module(mod) 63 | 64 | # if it's a directory, recurse into it 65 | if os.path.isdir(filepath): 66 | self.load_plugins(filepath) 67 | 68 | @property 69 | def plugins(self): 70 | return PluginRegistry.plugins -------------------------------------------------------------------------------- /scripts/deletebranch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # File: deletebranch.sh 4 | # Author: Zachary Cutlip 5 | # Purpose: (Relatively) safely delete specified branch from local and origin in one pass 6 | 7 | quit(){ 8 | if [ $# -gt 1 ]; 9 | then 10 | echo $1 11 | shift 12 | fi 13 | exit $1 14 | } 15 | 16 | local_branch_exists() { 17 | local branch="$1" 18 | git branch | grep "[:space:]*$branch$" 19 | return $? 20 | } 21 | 22 | remote_branch_exists() { 23 | local remote="$1" 24 | local branch="$2" 25 | local remote_string="remotes\/$remote\/$branch" 26 | git branch -a | grep "[:space:]*$remote_string$" 27 | return $? 28 | } 29 | 30 | is_current_branch() { 31 | local to_delete="$1" 32 | local branch=$(git rev-parse --abbrev-ref HEAD) 33 | [ "$to_delete" = "$branch" ] 34 | return $? 35 | } 36 | 37 | merged() { 38 | local to_delete="$1" 39 | local commit=$(git log $to_delete | head -1) 40 | git log | grep "$commit"; 41 | return $? 42 | } 43 | 44 | remote_merged() { 45 | local remote="$1" 46 | local to_delete="$2" 47 | local remote_string="remotes/$remote/$to_delete" 48 | local commit=$(git log $remote_string | head -1) 49 | git log | grep "$commit"; 50 | return $? 51 | } 52 | 53 | to_delete=$1 54 | remote="origin" 55 | if [ $# -gt 1 ]; 56 | then 57 | remote="$2" 58 | fi 59 | 60 | if [ -z "$to_delete" ]; 61 | then 62 | quit "Specify a branch to delete" 1 63 | fi 64 | 65 | if is_current_branch "$to_delete"; 66 | then 67 | quit "Can't delete current branch: $branch" 1 68 | fi 69 | 70 | if [ "$to_delete" = "master" ]; 71 | then 72 | quit "Refusing to delete master branch." 1 73 | fi 74 | 75 | local_exists=0 76 | remote_exists=0 77 | if local_branch_exists "$to_delete"; 78 | then 79 | local_exists=1 80 | fi 81 | if remote_branch_exists "$remote" "$to_delete"; 82 | then 83 | remote_exists=1 84 | fi 85 | 86 | if [ $local_exists -eq 0 ] && [ $remote_exists -eq 0 ]; 87 | then 88 | quit "Neither local nor remote branch exists" 1 89 | fi 90 | 91 | if [ $local_exists -gt 0 ]; 92 | then 93 | if ! merged "$to_delete"; 94 | then 95 | quit "Branch $to_delete appears not to be merged." 1 96 | fi 97 | elif [ $remote_exists -gt 0 ]; 98 | then 99 | if ! remote_merged "$remote" "$to_delete"; 100 | then 101 | quit "Remote branch $to_delete appears not to be merged." 1 102 | fi 103 | fi 104 | 105 | echo "Deleting branch $to_delete from local and $remote." 106 | 107 | git push -d $remote "$to_delete" 108 | ret1=$? 109 | git branch -d "$to_delete" 110 | ret2=$? 111 | 112 | [ $ret1 -eq 0 ] || [ $ret2 -eq 0 ] 113 | quit $? 114 | -------------------------------------------------------------------------------- /tests/env_tests.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import subprocess 3 | import shutil 4 | import yaml 5 | import os 6 | 7 | from nose.tools import * 8 | 9 | import scruffy 10 | from scruffy import Environment, ConfigFile, Directory 11 | from scruffy.plugin import PluginManager 12 | 13 | 14 | def test_environment_config(): 15 | e = Environment(config=ConfigFile('tests/env1/json_config')) 16 | assert e.config.setting1 == 667 17 | assert e.config.setting3.key1 == 'not value' 18 | 19 | def test_environment_config_default(): 20 | e = Environment(config=ConfigFile('tests/env1/json_config', defaults='tests/env1/default.cfg')) 21 | assert e.config.setting1 == 667 22 | assert e.config.setting2 == True 23 | assert e.config.setting3.key1 == 'not value' 24 | assert e.config.setting3.key2 == 'value' 25 | 26 | def test_environment_directory_config(): 27 | e = Environment( 28 | dir=Directory('tests/env1', 29 | config=ConfigFile('json_config', defaults='default.cfg'), 30 | otherconfig=ConfigFile('json_config2') 31 | ) 32 | ) 33 | assert e.config.setting1 == 667 34 | assert e.config.setting2 == True 35 | assert e.config.setting3.key1 == 'not value' 36 | assert e.config.setting3.key2 == 'value' 37 | assert e.dir.config.setting1 == 667 38 | assert e.dir.config.setting2 == True 39 | assert e.dir.config.setting3.key1 == 'not value' 40 | assert e.dir.config.setting3.key2 == 'value' 41 | assert e.dir.otherconfig.setting1 == 888 42 | assert e.dir.otherconfig.setting2 == True 43 | 44 | def test_environment_full(): 45 | e = Environment( 46 | dir=Directory('tests/env1', 47 | config=ConfigFile('json_config', defaults='default.cfg'), 48 | otherconfig=ConfigFile('json_config2') 49 | ), 50 | config_var_dir=Directory('{config:somedir}', create=True), 51 | somefile=scruffy.File('{config:somefile}'), 52 | string_dir='/tmp/scruffy_string_dir' 53 | ) 54 | assert e.config_var_dir.path == '/tmp/scruffy_test_dir' 55 | assert os.path.exists('/tmp/scruffy_test_dir') 56 | assert e.somefile.content.strip() == 'thing' 57 | e.string_dir.create() 58 | assert os.path.exists('/tmp/scruffy_string_dir') 59 | e.string_dir.remove() 60 | 61 | def test_environment_add(): 62 | e = Environment( 63 | dir=Directory('tests/env1', 64 | config=ConfigFile('json_config', defaults='default.cfg'), 65 | otherconfig=ConfigFile('json_config2') 66 | ) 67 | ) 68 | e.add( 69 | config_var_dir=Directory(e.config.somedir, create=True), 70 | somefile=scruffy.File(e.config.somefile), 71 | string_dir='/tmp/scruffy_string_dir' 72 | ) 73 | assert os.path.exists('/tmp/scruffy_test_dir') 74 | assert e.config_var_dir.exists 75 | assert e.config_var_dir.path == '/tmp/scruffy_test_dir' 76 | e.config_var_dir.remove() 77 | assert not os.path.exists('/tmp/scruffy_test_dir') 78 | assert e.somefile.content.strip() == 'thing' 79 | e.config_var_dir.create() 80 | assert os.path.exists('/tmp/scruffy_string_dir') 81 | e.config_var_dir.remove() 82 | -------------------------------------------------------------------------------- /scruffy/state.py: -------------------------------------------------------------------------------- 1 | """ 2 | State 3 | ----- 4 | 5 | Classes for storing a program's state. 6 | """ 7 | import os 8 | import atexit 9 | import yaml 10 | 11 | 12 | try: 13 | from sqlalchemy import create_engine, Column, Integer, String 14 | from sqlalchemy.ext.declarative import declarative_base 15 | from sqlalchemy.orm import sessionmaker, reconstructor 16 | Base = declarative_base() 17 | HAVE_SQL_ALCHEMY = True 18 | except: 19 | HAVE_SQL_ALCHEMY = False 20 | 21 | 22 | class State(object): 23 | """ 24 | A program's state. 25 | 26 | Contains a dictionary that can be periodically saved and restored at startup. 27 | 28 | Maybe later this will be subclassed with database connectors and whatnot, 29 | but for now it'll just save to a yaml file. 30 | """ 31 | @classmethod 32 | def state(cls, *args, **kwargs): 33 | return cls(*args, **kwargs) 34 | 35 | def __init__(self, path=None): 36 | self.path = path 37 | self.d = {} 38 | self.load() 39 | atexit.register(self._exit_handler) 40 | 41 | def __enter__(self): 42 | self.load() 43 | return self 44 | 45 | def __exit__(self, type, value, traceback): 46 | self.save() 47 | 48 | def __getitem__(self, key): 49 | try: 50 | return self.d[key] 51 | except KeyError: 52 | return None 53 | 54 | def __setitem__(self, key, value): 55 | self.d[key] = value 56 | 57 | def _exit_handler(self): 58 | self.save() 59 | 60 | def save(self): 61 | """ 62 | Save the state to a file. 63 | """ 64 | with open(self.path, 'w') as f: 65 | f.write(yaml.dump(dict(self.d))) 66 | 67 | def load(self): 68 | """ 69 | Load a saved state file. 70 | """ 71 | if os.path.exists(self.path): 72 | with open(self.path, 'r') as f: 73 | d = yaml.safe_load(f.read().replace('\t', ' '*4)) 74 | # don't clobber self.d if we successfully opened the state file 75 | # but it was empty 76 | if d: 77 | self.d = d 78 | 79 | def cleanup(self): 80 | """ 81 | Clean up the saved state. 82 | """ 83 | if os.path.exists(self.path): 84 | os.remove(self.path) 85 | 86 | 87 | if HAVE_SQL_ALCHEMY: 88 | class DBState(State, Base): 89 | """ 90 | State stored in a database, using SQLAlchemy. 91 | """ 92 | __tablename__ = 'state' 93 | 94 | id = Column(Integer, primary_key=True) 95 | data = Column(String) 96 | 97 | session = None 98 | 99 | @classmethod 100 | def state(cls, url=None, *args, **kwargs): 101 | if not cls.session: 102 | engine = create_engine(url, echo=False) 103 | Base.metadata.create_all(engine) 104 | Session = sessionmaker(bind=engine) 105 | cls.session = Session() 106 | 107 | inst = cls.session.query(DBState).first() 108 | if inst: 109 | return inst 110 | else: 111 | return cls(*args, **kwargs) 112 | 113 | def __init__(self, *args, **kwargs): 114 | super(DBState, self).__init__(*args, **kwargs) 115 | self.d = {} 116 | self.data = '{}' 117 | 118 | def save(self): 119 | self.data = yaml.dump(self.d) 120 | self.session.add(self) 121 | self.session.commit() 122 | 123 | @reconstructor 124 | def load(self): 125 | if self.data: 126 | self.d = yaml.safe_load(self.data) 127 | 128 | def cleanup(self): 129 | self.d = {} 130 | self.save() 131 | -------------------------------------------------------------------------------- /scruffy/env.py: -------------------------------------------------------------------------------- 1 | """ 2 | Environment 3 | ----------- 4 | 5 | Classes for representing the encompassing environment in which your application 6 | runs. 7 | """ 8 | import os 9 | import yaml 10 | import itertools 11 | import errno 12 | import logging 13 | import logging.config 14 | 15 | from six import string_types 16 | 17 | from .file import Directory 18 | from .plugin import PluginManager 19 | from .config import ConfigNode, Config, ConfigEnv, ConfigApplicator, ConfigFile 20 | 21 | 22 | class Environment(object): 23 | """ 24 | An environment in which to run a program 25 | """ 26 | def __init__(self, setup_logging=True, *args, **kwargs): 27 | self._pm = PluginManager() 28 | self._children = {} 29 | self.config = None 30 | 31 | # find a config if we have one and load it 32 | self.config = self.find_config(kwargs) 33 | if self.config: 34 | self.config.load() 35 | 36 | # setup logging 37 | if setup_logging: 38 | if self.config != None and self.config.logging.dict_config != None: 39 | # configure logging from the configuration 40 | logging.config.dictConfig(self.config.logging.dict_config.to_dict()) 41 | else: 42 | # no dict config, set up a basic config so we at least get messages logged to stdout 43 | log = logging.getLogger() 44 | log.setLevel(logging.INFO) 45 | if len(list(filter(lambda h: isinstance(h, logging.StreamHandler), log.handlers))) == 0: 46 | log.addHandler(logging.StreamHandler()) 47 | 48 | # add children 49 | self.add(**kwargs) 50 | 51 | def __enter__(self): 52 | return self 53 | 54 | def __exit__(self, type, value, traceback): 55 | self.cleanup() 56 | 57 | def __getitem__(self, key): 58 | return self._children[key] 59 | 60 | def __getattr__(self, key): 61 | return self._children[key] 62 | 63 | def find_config(self, children): 64 | """ 65 | Find a config in our children so we can fill in variables in our other 66 | children with its data. 67 | """ 68 | named_config = None 69 | found_config = None 70 | 71 | # first see if we got a kwarg named 'config', as this guy is special 72 | if 'config' in children: 73 | if isinstance(children['config'], string_types): 74 | children['config'] = ConfigFile(children['config']) 75 | elif isinstance(children['config'], Config): 76 | children['config'] = children['config'] 77 | elif type(children['config']) == dict: 78 | children['config'] = Config(data=children['config']) 79 | else: 80 | raise TypeError("Don't know how to turn {} into a Config".format(type(children['config']))) 81 | 82 | named_config = children['config'] 83 | 84 | # next check the other kwargs 85 | for k in children: 86 | if isinstance(children[k], Config): 87 | found_config = children[k] 88 | 89 | # if we still don't have a config, see if there's a directory with one 90 | for k in children: 91 | if isinstance(children[k], Directory): 92 | for j in children[k]._children: 93 | if j == 'config' and not named_config: 94 | named_config = children[k]._children[j] 95 | if isinstance(children[k]._children[j], Config): 96 | found_config = children[k]._children[j] 97 | 98 | if named_config: 99 | return named_config 100 | else: 101 | return found_config 102 | 103 | def add(self, **kwargs): 104 | """ 105 | Add objects to the environment. 106 | """ 107 | for key in kwargs: 108 | if isinstance(kwargs[key], string_types): 109 | self._children[key] = Directory(kwargs[key]) 110 | else: 111 | self._children[key] = kwargs[key] 112 | self._children[key]._env = self 113 | self._children[key].apply_config(ConfigApplicator(self.config)) 114 | self._children[key].prepare() 115 | 116 | def cleanup(self): 117 | """ 118 | Clean up the environment 119 | """ 120 | for key in self._children: 121 | self._children[key].cleanup() 122 | 123 | @property 124 | def plugins(self): 125 | return self._pm.plugins 126 | -------------------------------------------------------------------------------- /tests/file_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import nose 4 | 5 | import scruffy 6 | from scruffy import * 7 | 8 | 9 | def safe_unlink(path): 10 | try: 11 | os.unlink(path) 12 | except: 13 | pass 14 | 15 | 16 | def test_file(): 17 | p = '/tmp/scruffy_test_file.txt' 18 | f = File(p) 19 | safe_unlink(p) 20 | assert not os.path.exists(p) 21 | f.create() 22 | assert os.path.exists(p) 23 | assert f.name == 'scruffy_test_file.txt' 24 | assert f.ext == '.txt' 25 | assert f.path == '/tmp/scruffy_test_file.txt' 26 | f.write('xyz') 27 | assert f.exists 28 | assert f.content == 'xyz' 29 | f.remove() 30 | assert not f.exists 31 | assert str(f) == '/tmp/scruffy_test_file.txt' 32 | 33 | 34 | def test_prepare_cleanup(): 35 | p = '/tmp/scruffy_test_file' 36 | f = File(p, create=True, cleanup=True) 37 | safe_unlink(p) 38 | assert not os.path.exists(p) 39 | f.prepare() 40 | assert os.path.exists(p) 41 | f.cleanup() 42 | assert not os.path.exists(p) 43 | 44 | 45 | def test_file_with(): 46 | p = '/tmp/scruffy_test_file' 47 | safe_unlink(p) 48 | assert not os.path.exists(p) 49 | with File(p, create=True, cleanup=True): 50 | assert os.path.exists(p) 51 | assert not os.path.exists(p) 52 | 53 | 54 | def test_lock_file(): 55 | p = '/tmp/scruffy_test_file' 56 | safe_unlink(p) 57 | assert not os.path.exists(p) 58 | with LockFile(p): 59 | assert os.path.exists(p) 60 | assert not os.path.exists(p) 61 | f = File(p) 62 | f.create() 63 | try: 64 | with LockFile(p): 65 | assert False 66 | except: 67 | assert True 68 | f.remove() 69 | assert not os.path.exists(p) 70 | 71 | 72 | def test_log_file(): 73 | log = logging.getLogger() 74 | log.handlers = [] 75 | f = LogFile('/tmp/test.log') 76 | try: 77 | f.remove() 78 | except: 79 | pass 80 | f.prepare() 81 | assert f.path == '/tmp/test.log' 82 | assert len(log.handlers) == 1 83 | assert isinstance(log.handlers[0], logging.FileHandler) 84 | log.info('test') 85 | with open(f.path) as fi: 86 | assert fi.read().strip() == "test" 87 | f.remove() 88 | 89 | 90 | def test_package_file(): 91 | f = PackageFile('xxx', package='scruffy') 92 | assert f.path == os.path.join(os.getcwd(), 'scruffy/xxx') 93 | 94 | 95 | def test_directory(): 96 | d = Directory('tests/env1') 97 | p = '/tmp/scruffy_test' 98 | assert os.path.exists(d.path) 99 | try: 100 | os.removedirs(p) 101 | except: 102 | pass 103 | with Directory(p, cleanup=True) as d: 104 | assert os.path.exists(p) 105 | assert d.exists 106 | assert d.path_to('x') == os.path.join(p, 'x') 107 | assert not os.path.exists(p) 108 | d.create() 109 | d.add(File('xxx')) 110 | assert d.xxx.path == '/tmp/scruffy_test/xxx' 111 | d.xxx.create() 112 | assert type(d.list()[0]) == File 113 | assert str(d.list()[0]) == '/tmp/scruffy_test/xxx' 114 | 115 | 116 | def test_plugin_directory(): 117 | scruffy.plugin.PluginRegistry.plugins = [] 118 | assert len(PluginManager().plugins) == 0 119 | d = PluginDirectory('tests/env1/plugins') 120 | d.load() 121 | assert len(PluginManager().plugins) == 2 122 | 123 | 124 | def test_package_directory(): 125 | d = PackageDirectory() 126 | assert d._base == os.path.join(os.getcwd(), 'tests') 127 | d = PackageDirectory(package='scruffy') 128 | assert d._base == os.path.join(os.getcwd(), 'scruffy') 129 | d = PackageDirectory('xxx', package='scruffy') 130 | assert d._base == os.path.join(os.getcwd(), 'scruffy') 131 | assert d.path == os.path.join(os.getcwd(), 'scruffy/xxx') 132 | 133 | 134 | def test_nested_package_plugin(): 135 | d = PluginDirectory('env1/plugins', parent=PackageDirectory()) 136 | assert d.path == os.path.join(os.getcwd(), 'tests/env1/plugins') 137 | scruffy.plugin.PluginRegistry.plugins = [] 138 | assert len(PluginManager().plugins) == 0 139 | d.load() 140 | assert len(PluginManager().plugins) == 2 141 | 142 | 143 | def test_directory_config(): 144 | d = Directory('tests/env1', config=ConfigFile('json_config')) 145 | d.prepare() 146 | assert type(d.config) == ConfigFile 147 | assert d.config.setting1 == 667 148 | 149 | 150 | def test_directory_file(): 151 | d = Directory('tests/env1', thing=File('raw_file')) 152 | d.prepare() 153 | assert type(d.thing) == File 154 | assert d.thing.content.strip() == 'raw_file value' 155 | d.write('temp_file', 'xxx') 156 | assert d.read('temp_file') == 'xxx' 157 | 158 | 159 | def test_directory_file_with(): 160 | with Directory('tests/env1', thing=File('raw_file')) as d: 161 | assert type(d.thing) == File 162 | assert d.thing.content.strip() == 'raw_file value' 163 | 164 | 165 | def test_directory_add_file(): 166 | d = Directory('tests/env1') 167 | x = d.add(File('xxx')) 168 | assert d.xxx.name == 'xxx' 169 | assert x == d.xxx 170 | x = d.add('yyy') 171 | assert d.yyy.name == 'yyy' 172 | assert x == d.yyy 173 | 174 | 175 | @nose.tools.raises(TypeError) 176 | def test_directory_add_file_fail(): 177 | d = Directory('tests/env1') 178 | d.add(1) 179 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Scruffy 2 | ======= 3 | 4 | .. image:: https://img.shields.io/travis/snare/scruffy.svg 5 | :target: https://travis-ci.org/snare/scruffy 6 | 7 | .. image:: https://img.shields.io/pypi/format/scruffington.svg 8 | :target: https://pypi.python.org/pypi/scruffington 9 | 10 | .. image:: https://readthedocs.org/projects/scruffy/badge/?version=latest 11 | :target: http://scruffy.readthedocs.org/en/latest/ 12 | 13 | 14 | *Scruffy. The Janitor.* 15 | 16 | Scruffy is a framework for taking care of a bunch of boilerplate in Python apps. It handles the loading of configuration files, the loading and management of plugins, and the management of other filesystem resources such as temporary files and directories, log files, etc. 17 | 18 | A typical use case for Scruffy is a command-line Python tool with some or all of the following requirements: 19 | 20 | * Read a set of configuration defaults 21 | * Read a local configuration file and apply it on top of the defaults 22 | * Allow overriding some configuration options with command line flags or at runtime 23 | * Load a core set of Python-based plugins 24 | * Load a set of user-defined Python-based plugins 25 | * Generate log files whose name, location and other logging settings are based on configuration 26 | * Store application state between runs in a file or database 27 | 28 | Scruffy is used by Voltron_ and Calculon_ 29 | 30 | .. _Voltron: https://github.com/snare/voltron 31 | .. _Calculon: https://github.com/snare/calculon 32 | 33 | Installation 34 | ------------ 35 | 36 | A standard python setup script is included. 37 | 38 | $ python setup.py install 39 | 40 | This will install the Scruffy package wherever that happens on your system. 41 | 42 | Alternately, Scruffy can be installed with `pip` from PyPi (where it's called `scruffington`, because I didn't check for a conflict before I named it). 43 | 44 | $ pip install scruffington 45 | 46 | Documentation 47 | ------------- 48 | 49 | Full documentation is hosted at readthedocs_ 50 | 51 | .. _readthedocs: http://scruffy.readthedocs.io/ 52 | 53 | Quick start 54 | ----------- 55 | 56 | Config 57 | ~~~~~~ 58 | 59 | Load a user config file, and apply it on top of a set of defaults loaded from inside the Python package we're currently running from. 60 | 61 | *thingy.yaml*: 62 | 63 | .. code:: yaml 64 | 65 | some_property: 1 66 | other_property: a thing 67 | 68 | *thingy.py*: 69 | 70 | .. code:: python 71 | 72 | from scruffy import ConfigFile 73 | 74 | c = ConfigFile('thingy.yaml', load=True, 75 | defaults=File('defaults.yaml', parent=PackageDirectory()) 76 | ) 77 | 78 | print("c.some_property == {c.some_property}".format(c=c)) 79 | print("c.other_property == {c.other_property}".format(c=c)) 80 | 81 | Run it: 82 | 83 | :: 84 | 85 | $ python thingy.py 86 | c.some_property == 1 87 | c.other_property == a thing 88 | 89 | Plugins 90 | ~~~~~~~ 91 | 92 | Load some plugins. 93 | 94 | *~/.thingy/plugins/example.py*: 95 | 96 | .. code:: python 97 | 98 | from scruffy import Plugin 99 | 100 | class ExamplePlugin(Plugin): 101 | def do_a_thing(self): 102 | print('{}.{} is doing a thing'.format(__name__, self.__class__.__name__)) 103 | 104 | *thingy.py*: 105 | 106 | .. code:: python 107 | 108 | from scruffy import PluginDirectory, PluginRegistry 109 | 110 | pd = PluginDirectory('~/.thingy/plugins') 111 | pd.load() 112 | 113 | for p in PluginRegistry.plugins: 114 | print("Initialising plugin {}".format(p)) 115 | p().do_a_thing() 116 | 117 | Run it: 118 | 119 | :: 120 | 121 | $ python thingy.py 122 | Initialising plugin 123 | example.ExamplePlugin is doing a thing 124 | 125 | Logging 126 | ~~~~~~~ 127 | 128 | Scruffy's `LogFile` class will do some configuration of Python's `logging` module. 129 | 130 | *log.py*: 131 | 132 | .. code:: python 133 | 134 | import logging 135 | from scruffy import LogFile 136 | 137 | log = logging.getLogger('main') 138 | log.setLevel(logging.INFO) 139 | LogFile('/tmp/thingy.log', logger='main').configure() 140 | 141 | log.info('Hello from log.py') 142 | 143 | */tmp/thingy.log*: 144 | 145 | :: 146 | 147 | Hello from log.py 148 | 149 | Environment 150 | ~~~~~~~~~~~ 151 | 152 | Scruffy's `Environment` class ties all the other stuff together. The other classes can be instantiated as named children of an `Environment`, which will load any `Config` objects, apply the configs to the other objects, and then prepare the other objects. 153 | 154 | *~/.thingy/config*: 155 | 156 | .. code:: yaml 157 | 158 | log_dir: /tmp/logs 159 | log_file: thingy.log 160 | 161 | *env.py*: 162 | 163 | .. code:: python 164 | 165 | from scruffy import * 166 | 167 | e = Environment( 168 | main_dir=Directory('~/.thingy', create=True, 169 | config=ConfigFile('config', defaults=File('defaults.yaml', parent=PackageDirectory())), 170 | lock=LockFile('lock') 171 | user_plugins=PluginDirectory('plugins') 172 | ), 173 | log_dir=Directory('{config:log_dir}', create=True 174 | LogFile('{config:log_file}', logger='main') 175 | ), 176 | pkg_plugins=PluginDirectory('plugins', parent=PackageDirectory()) 177 | ) 178 | 179 | License 180 | ------- 181 | 182 | See LICENSE file. If you use this and don't hate it, buy me a beer at a conference some time. 183 | 184 | Credits 185 | ------- 186 | 187 | Props to richo_. Flat duck pride. 188 | 189 | .. _richo: http://github.com/richo -------------------------------------------------------------------------------- /tests/config_tests.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | from six import string_types 4 | 5 | from nose.tools import * 6 | from scruffy import * 7 | 8 | YAML = """ 9 | thing: 123 10 | another: 11 | - 666 12 | - 777 13 | - 888 14 | thang: 15 | a: 1 16 | b: 2 17 | c: 3 18 | d: {a: 1, b: 2, c: 3} 19 | derp: 20 | - {a: 1} 21 | - {a: 2} 22 | - {a: 3} 23 | - {a: 4} 24 | """ 25 | 26 | # config object 27 | def test_config_object(): 28 | d = yaml.load(YAML) 29 | c = Config(data=d) 30 | assert type(c) == Config 31 | assert c.thang.d.b == 2 32 | assert c['thang']['d']['b'] == 2 33 | assert c['thang.d.b'] == 2 34 | assert c.derp[0] == {'a': 1} 35 | assert c.derp[0].a == 1 36 | assert c.xxx == None 37 | if c.xxx: 38 | assert False 39 | 40 | def test_config_object_set(): 41 | c = Config() 42 | c.derp1 = {'a': [1,2,3], 'b': 'xxx'} 43 | assert c.derp1.a[0] == 1 44 | assert c.derp1.b == 'xxx' 45 | assert isinstance(c.derp1.b, string_types) 46 | c['derp2.a.b.c'] = 123 47 | assert c.derp2.a.b.c == 123 48 | c.derp3.a.b.c = 666 49 | assert c.derp3.a.b.c == 666 50 | try: 51 | c.derp3.a.b.c.d = 666 52 | raise Exception() 53 | except Exception as e: 54 | if type(e) != AttributeError: 55 | raise 56 | c.derp3.b = 'x' 57 | c['derp4']['a']['b'] = 777 58 | assert c.derp4.a.b == 777 59 | assert type(c.derp4) == ConfigNode 60 | assert type(c.derp4.a.b) == int 61 | 62 | def test_config_object_update(): 63 | d = yaml.load(YAML) 64 | c = Config(defaults=d) 65 | c.update(options={'derp.0.a': 666, 'derp.0.b': 777}) 66 | assert c.derp[0].a == 666 67 | assert c.derp[0].b == 777 68 | assert type(c.derp) == ConfigNode 69 | c.derp[1].a = 123 70 | c.derp[1].b = 234 71 | assert c.derp[1] == {'a': 123, 'b': 234} 72 | c.update(data={'thang': {'d': {'b': 666}}}) 73 | assert c.thang.d == {'a': 1, 'b': 666, 'c': 3} 74 | c2 = Config({'thang': {'d': {'b': 777}}}) 75 | c.update(c2) 76 | assert c.thang.d == {'a': 1, 'b': 777, 'c': 3} 77 | 78 | def test_config_object_reset(): 79 | d = yaml.load(YAML) 80 | c = Config(defaults=d) 81 | c.update(options={'derp.0.a': 666, 'derp.0.b': 777}) 82 | c.reset() 83 | assert c == d 84 | 85 | def test_config_file(): 86 | c = ConfigFile(defaults='tests/env1/json_config') 87 | c.load() 88 | c.update(options={'derp.0.a': 666, 'derp.0.b': 777}) 89 | assert c.derp[0].a == 666 90 | assert c.derp[0].b == 777 91 | assert c.setting1 == 667 92 | assert c.setting3.key1 == "not value" 93 | c = ConfigFile('tests/env1/json_config', load=True) 94 | assert c.setting1 == 667 95 | assert c.setting3.key1 == "not value" 96 | c.update(options={'derp.0.a': 666, 'derp.0.b': 777}) 97 | assert c.derp[0].a == 666 98 | assert c.derp[0].b == 777 99 | 100 | def test_config_env(): 101 | os.environ['__SC_TEST.A.A.A'] = 'AAAA' 102 | os.environ['__SC_TEST.A.A.B'] = '1234' 103 | os.environ['__SC_TEST.A.A.C'] = '1234.5678' 104 | os.environ['__SC_TEST.A.A.D'] = '0x1234' 105 | c = ConfigEnv() 106 | assert c == {'test': {'a': {'a': {'a': 'AAAA', 'c': 1234.5678, 'b': 1234, 'd': 4660}}}} 107 | c = ConfigFile('tests/env1/config1', load=True, apply_env=True) 108 | assert c.test == {'a': {'a': {'a': 'AAAA', 'c': 1234.5678, 'b': 1234, 'd': 4660}}} 109 | del os.environ['__SC_TEST.A.A.A'] 110 | del os.environ['__SC_TEST.A.A.B'] 111 | del os.environ['__SC_TEST.A.A.C'] 112 | del os.environ['__SC_TEST.A.A.D'] 113 | 114 | os.environ['SCRUFFY_TEST.A.A.A'] = 'AAAA' 115 | os.environ['SCRUFFY_TEST.A.A.B'] = '1234' 116 | os.environ['SCRUFFY_TEST.A.A.C'] = '1234.5678' 117 | os.environ['SCRUFFY_TEST.A.A.D'] = '0x1234' 118 | c = ConfigEnv() 119 | assert c == {'test': {'a': {'a': {'a': 'AAAA', 'c': 1234.5678, 'b': 1234, 'd': 4660}}}} 120 | c = ConfigFile('tests/env1/config1', load=True, apply_env=True) 121 | assert c.test == {'a': {'a': {'a': 'AAAA', 'c': 1234.5678, 'b': 1234, 'd': 4660}}} 122 | del os.environ['SCRUFFY_TEST.A.A.A'] 123 | del os.environ['SCRUFFY_TEST.A.A.B'] 124 | del os.environ['SCRUFFY_TEST.A.A.C'] 125 | del os.environ['SCRUFFY_TEST.A.A.D'] 126 | 127 | os.environ['THING_TEST.A.A.A'] = 'AAAA' 128 | os.environ['THING_TEST.A.A.B'] = '1234' 129 | os.environ['THING_TEST.A.A.C'] = '1234.5678' 130 | os.environ['THING_TEST.A.A.D'] = '0x1234' 131 | c = ConfigEnv(prefix='THING') 132 | assert c == {'test': {'a': {'a': {'a': 'AAAA', 'c': 1234.5678, 'b': 1234, 'd': 4660}}}} 133 | c = ConfigFile('tests/env1/config1', load=True, apply_env=True, env_prefix='THING') 134 | assert c.test == {'a': {'a': {'a': 'AAAA', 'c': 1234.5678, 'b': 1234, 'd': 4660}}} 135 | del os.environ['THING_TEST.A.A.A'] 136 | del os.environ['THING_TEST.A.A.B'] 137 | del os.environ['THING_TEST.A.A.C'] 138 | del os.environ['THING_TEST.A.A.D'] 139 | 140 | def test_config_yaml(): 141 | c = ConfigFile('tests/env1/yaml_config', defaults='tests/env1/default.cfg', load=True) 142 | assert c.setting1 == 666 143 | assert c.setting2 == True 144 | assert c.setting3.key1 == "value" 145 | 146 | def test_config_file_with(): 147 | with ConfigFile('tests/env1/yaml_config', defaults='tests/env1/default.cfg') as c: 148 | assert c.setting1 == 666 149 | assert c.setting2 == True 150 | assert c.setting3.key1 == "value" 151 | 152 | def test_config_applicator(): 153 | ap = ConfigApplicator(ConfigFile('tests/env1/yaml_config', load=True)) 154 | assert ap.apply('/some/path/{config:setting1}/hurr/{config:setting3.key1}.txt') == '/some/path/666/hurr/value.txt' 155 | try: 156 | ap.apply('{config:xxx') 157 | assert False 158 | except KeyError: 159 | assert True 160 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/scruffy.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/scruffy.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/scruffy" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/scruffy" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # scruffy documentation build configuration file, created by 4 | # sphinx-quickstart on Sun May 15 13:30:45 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.doctest', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'scruffy' 52 | copyright = u'2016, snare' 53 | author = u'snare' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = u'0.3.3' 61 | # The full version, including alpha/beta/rc tags. 62 | release = u'0.3.3' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | # html_theme = 'alabaster' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | #html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | #html_sidebars = {} 159 | 160 | # Additional templates that should be rendered to pages, maps page names to 161 | # template names. 162 | #html_additional_pages = {} 163 | 164 | # If false, no module index is generated. 165 | #html_domain_indices = True 166 | 167 | # If false, no index is generated. 168 | #html_use_index = True 169 | 170 | # If true, the index is split into individual pages for each letter. 171 | #html_split_index = False 172 | 173 | # If true, links to the reST sources are added to the pages. 174 | #html_show_sourcelink = True 175 | 176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 177 | #html_show_sphinx = True 178 | 179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages will 183 | # contain a tag referring to it. The value of this option must be the 184 | # base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Language to be used for generating the HTML full-text search index. 191 | # Sphinx supports the following languages: 192 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 193 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 194 | #html_search_language = 'en' 195 | 196 | # A dictionary with options for the search language support, empty by default. 197 | # Now only 'ja' uses this config value 198 | #html_search_options = {'type': 'default'} 199 | 200 | # The name of a javascript file (relative to the configuration directory) that 201 | # implements a search results scorer. If empty, the default will be used. 202 | #html_search_scorer = 'scorer.js' 203 | 204 | # Output file base name for HTML help builder. 205 | htmlhelp_basename = 'scruffydoc' 206 | 207 | # -- Options for LaTeX output --------------------------------------------- 208 | 209 | latex_elements = { 210 | # The paper size ('letterpaper' or 'a4paper'). 211 | #'papersize': 'letterpaper', 212 | 213 | # The font size ('10pt', '11pt' or '12pt'). 214 | #'pointsize': '10pt', 215 | 216 | # Additional stuff for the LaTeX preamble. 217 | #'preamble': '', 218 | 219 | # Latex figure (float) alignment 220 | #'figure_align': 'htbp', 221 | } 222 | 223 | # Grouping the document tree into LaTeX files. List of tuples 224 | # (source start file, target name, title, 225 | # author, documentclass [howto, manual, or own class]). 226 | latex_documents = [ 227 | (master_doc, 'scruffy.tex', u'scruffy Documentation', 228 | u'snare', 'manual'), 229 | ] 230 | 231 | # The name of an image file (relative to this directory) to place at the top of 232 | # the title page. 233 | #latex_logo = None 234 | 235 | # For "manual" documents, if this is true, then toplevel headings are parts, 236 | # not chapters. 237 | #latex_use_parts = False 238 | 239 | # If true, show page references after internal links. 240 | #latex_show_pagerefs = False 241 | 242 | # If true, show URL addresses after external links. 243 | #latex_show_urls = False 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #latex_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #latex_domain_indices = True 250 | 251 | 252 | # -- Options for manual page output --------------------------------------- 253 | 254 | # One entry per manual page. List of tuples 255 | # (source start file, name, description, authors, manual section). 256 | man_pages = [ 257 | (master_doc, 'scruffy', u'scruffy Documentation', 258 | [author], 1) 259 | ] 260 | 261 | # If true, show URL addresses after external links. 262 | #man_show_urls = False 263 | 264 | 265 | # -- Options for Texinfo output ------------------------------------------- 266 | 267 | # Grouping the document tree into Texinfo files. List of tuples 268 | # (source start file, target name, title, author, 269 | # dir menu entry, description, category) 270 | texinfo_documents = [ 271 | (master_doc, 'scruffy', u'scruffy Documentation', 272 | author, 'scruffy', 'One line description of project.', 273 | 'Miscellaneous'), 274 | ] 275 | 276 | # Documents to append as an appendix to all manuals. 277 | #texinfo_appendices = [] 278 | 279 | # If false, no module index is generated. 280 | #texinfo_domain_indices = True 281 | 282 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 283 | #texinfo_show_urls = 'footnote' 284 | 285 | # If true, do not generate a @detailmenu in the "Top" node's menu. 286 | #texinfo_no_detailmenu = False 287 | -------------------------------------------------------------------------------- /scruffy/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Config 3 | ------ 4 | 5 | Classes for loading and accessing configuration data. 6 | """ 7 | from six import string_types 8 | 9 | import copy 10 | import os 11 | import ast 12 | import yaml 13 | import re 14 | 15 | from six import string_types 16 | from .file import File 17 | 18 | 19 | class ConfigNode(object): 20 | """ 21 | Represents a Scruffy config object. 22 | 23 | Can be accessed as a dictionary, like this: 24 | 25 | >>> config['top-level-section']['second-level-property'] 26 | 27 | Or as a dictionary with a key path, like this: 28 | 29 | >>> config['top_level_section.second_level_property'] 30 | 31 | Or as an object, like this: 32 | 33 | >>> config.top_level_section.second_level_property 34 | """ 35 | def __init__(self, data={}, defaults={}, root=None, path=None): 36 | super(ConfigNode, self).__init__() 37 | self._root = root 38 | if not self._root: 39 | self._root = self 40 | self._path = path 41 | self._defaults = defaults 42 | self._data = copy.deepcopy(self._defaults) 43 | self.update(data) 44 | 45 | def __getitem__(self, key): 46 | c = self._child(key) 47 | v = c._get_value() 48 | if type(v) in [dict, list, type(None)]: 49 | return c 50 | else: 51 | return v 52 | 53 | def __setitem__(self, key, value): 54 | container, last = self._child(key)._resolve_path(create=True) 55 | container[last] = value 56 | 57 | def __getattr__(self, key): 58 | return self[key] 59 | 60 | def __setattr__(self, key, value): 61 | if key.startswith("_"): 62 | super(ConfigNode, self).__setattr__(key, value) 63 | else: 64 | self[key] = value 65 | 66 | def __str__(self): 67 | return str(self._get_value()) 68 | 69 | def __repr__(self): 70 | return str(self._get_value()) 71 | 72 | def __int__(self): 73 | return int(self._get_value()) 74 | 75 | def __float__(self): 76 | return float(self._get_value()) 77 | 78 | def __lt__(self, other): 79 | return self._get_value() < other 80 | 81 | def __le__(self, other): 82 | return self._get_value() <= other 83 | 84 | def __eq__(self, other): 85 | return self._get_value() == other 86 | 87 | def __ne__(self, other): 88 | return self._get_value() != other 89 | 90 | def __gt__(self, other): 91 | return self._get_value() > other 92 | 93 | def __ge__(self, other): 94 | return self._get_value() >= other 95 | 96 | def __contains__(self, key): 97 | return key in self._get_value() 98 | 99 | def __nonzero__(self): 100 | return self._get_value() != None 101 | 102 | def __bool__(self): 103 | return self._get_value() != None 104 | 105 | def items(self): 106 | return self._get_value().items() 107 | 108 | def keys(self): 109 | return self._get_value().keys() 110 | 111 | def __iter__(self): 112 | return self._get_value().__iter__() 113 | 114 | def _child(self, path): 115 | """ 116 | Return a ConfigNode object representing a child node with the specified 117 | relative path. 118 | """ 119 | if self._path: 120 | path = '{}.{}'.format(self._path, path) 121 | return ConfigNode(root=self._root, path=path) 122 | 123 | def _resolve_path(self, create=False): 124 | """ 125 | Returns a tuple of a reference to the last container in the path, and 126 | the last component in the key path. 127 | 128 | For example, with a self._value like this: 129 | 130 | { 131 | 'thing': { 132 | 'another': { 133 | 'some_leaf': 5, 134 | 'one_more': { 135 | 'other_leaf': 'x' 136 | } 137 | } 138 | } 139 | } 140 | 141 | And a self._path of: 'thing.another.some_leaf' 142 | 143 | This will return a tuple of a reference to the 'another' dict, and 144 | 'some_leaf', allowing the setter and casting methods to directly access 145 | the item referred to by the key path. 146 | """ 147 | # Split up the key path 148 | if isinstance(self._path, string_types): 149 | key_path = self._path.split('.') 150 | else: 151 | key_path = [self._path] 152 | 153 | # Start at the root node 154 | node = self._root._data 155 | nodes = [self._root._data] 156 | 157 | # Traverse along key path 158 | while len(key_path): 159 | # Get the next key in the key path 160 | key = key_path.pop(0) 161 | 162 | # See if the test could be an int for array access, if so assume it is 163 | try: 164 | key = int(key) 165 | except: 166 | pass 167 | 168 | # If the next level doesn't exist, create it 169 | if create: 170 | if type(node) == dict and key not in node: 171 | node[key] = {} 172 | elif type(node) == list and type(key) == int and len(node) < key: 173 | node.append([None for i in range(key-len(node))]) 174 | 175 | # Store the last node and traverse down the hierarchy 176 | nodes.append(node) 177 | try: 178 | node = node[key] 179 | except TypeError: 180 | if type(key) == int: 181 | raise IndexError(key) 182 | else: 183 | raise KeyError(key) 184 | 185 | return (nodes[-1], key) 186 | 187 | def _get_value(self): 188 | """ 189 | Get the value represented by this node. 190 | """ 191 | if self._path: 192 | try: 193 | container, last = self._resolve_path() 194 | return container[last] 195 | except KeyError: 196 | return None 197 | except IndexError: 198 | return None 199 | else: 200 | return self._data 201 | 202 | def update(self, data={}, options={}): 203 | """ 204 | Update the configuration with new data. 205 | 206 | This can be passed either or both `data` and `options`. 207 | 208 | `options` is a dict of keypath/value pairs like this (similar to 209 | CherryPy's config mechanism: 210 | 211 | >>> c.update(options={ 212 | ... 'server.port': 8080, 213 | ... 'server.host': 'localhost', 214 | ... 'admin.email': 'admin@lol' 215 | ... }) 216 | 217 | `data` is a dict of actual config data, like this: 218 | 219 | >>> c.update(data={ 220 | ... 'server': { 221 | ... 'port': 8080, 222 | ... 'host': 'localhost' 223 | ... }, 224 | ... 'admin': { 225 | ... 'email': 'admin@lol' 226 | ... } 227 | ... }) 228 | """ 229 | # Handle an update with a set of options like CherryPy does 230 | for key in options: 231 | self[key] = options[key] 232 | 233 | # Merge in any data in `data` 234 | if isinstance(data, ConfigNode): 235 | data = data._get_value() 236 | update_dict(self._get_value(), data) 237 | 238 | def reset(self): 239 | """ 240 | Reset the config to defaults. 241 | """ 242 | self._data = copy.deepcopy(self._defaults) 243 | 244 | def to_dict(self): 245 | """ 246 | Generate a plain dictionary. 247 | """ 248 | return self._get_value() 249 | 250 | 251 | class Config(ConfigNode): 252 | """ 253 | Config root node class. Just for convenience. 254 | """ 255 | 256 | 257 | class ConfigEnv(ConfigNode): 258 | """ 259 | Config based on based on environment variables. 260 | """ 261 | def __init__(self, prefix='SCRUFFY', *args, **kwargs): 262 | super(ConfigEnv, self).__init__(*args, **kwargs) 263 | 264 | # build options dictionary from environment variables starting with the prefix 265 | options = {} 266 | for key in [v for v in os.environ if v.startswith('__SC_') or v.startswith(prefix + '_')]: 267 | try: 268 | val = ast.literal_eval(os.environ[key]) 269 | except: 270 | val = os.environ[key] 271 | options[key.replace('__SC_', '').replace(prefix + '_', '').lower()] = val 272 | 273 | # update config with the values we've found 274 | self.update(options=options) 275 | 276 | 277 | class ConfigFile(Config, File): 278 | """ 279 | Config based on a loaded YAML or JSON file. 280 | """ 281 | def __init__(self, path=None, defaults=None, load=False, apply_env=False, env_prefix='SCRUFFY', *args, **kwargs): 282 | self._loaded = False 283 | self._defaults_file = defaults 284 | self._apply_env = apply_env 285 | self._env_prefix = env_prefix 286 | Config.__init__(self) 287 | File.__init__(self, path=path, *args, **kwargs) 288 | 289 | if load: 290 | self.load() 291 | 292 | def load(self, reload=False): 293 | """ 294 | Load the config and defaults from files. 295 | """ 296 | if reload or not self._loaded: 297 | # load defaults 298 | if self._defaults_file and isinstance(self._defaults_file, string_types): 299 | self._defaults_file = File(self._defaults_file, parent=self._parent) 300 | defaults = {} 301 | if self._defaults_file: 302 | defaults = yaml.safe_load(self._defaults_file.read().replace('\t', ' ')) 303 | 304 | # load data 305 | data = {} 306 | if self.exists: 307 | data = yaml.safe_load(self.read().replace('\t', ' ')) 308 | 309 | # initialise with the loaded data 310 | self._defaults = defaults 311 | self._data = copy.deepcopy(self._defaults) 312 | self.update(data=data) 313 | 314 | # if specified, apply environment variables 315 | if self._apply_env: 316 | self.update(ConfigEnv(self._env_prefix)) 317 | 318 | self._loaded = True 319 | 320 | return self 321 | 322 | def save(self): 323 | """ 324 | Save the config back to the config file. 325 | """ 326 | self.write(yaml.safe_dump(self._data, default_flow_style=False)) 327 | 328 | def prepare(self): 329 | """ 330 | Load the file when the Directory/Environment prepares us. 331 | """ 332 | self.load() 333 | 334 | 335 | class ConfigApplicator(object): 336 | """ 337 | Applies configs to other objects. 338 | """ 339 | def __init__(self, config): 340 | self.config = config 341 | 342 | def apply(self, obj): 343 | """ 344 | Apply the config to an object. 345 | """ 346 | if isinstance(obj, string_types): 347 | return self.apply_to_str(obj) 348 | 349 | def apply_to_str(self, obj): 350 | """ 351 | Apply the config to a string. 352 | """ 353 | toks = re.split('({config:|})', obj) 354 | newtoks = [] 355 | try: 356 | while len(toks): 357 | tok = toks.pop(0) 358 | if tok == '{config:': 359 | # pop the config variable, look it up 360 | var = toks.pop(0) 361 | val = self.config[var] 362 | 363 | # if we got an empty node, then it didn't exist 364 | if type(val) == ConfigNode and val == None: 365 | raise KeyError("No such config variable '{}'".format(var)) 366 | 367 | # add the value to the list 368 | newtoks.append(str(val)) 369 | 370 | # pop the '}' 371 | toks.pop(0) 372 | else: 373 | # not the start of a config block, just append it to the list 374 | newtoks.append(tok) 375 | return ''.join(newtoks) 376 | except IndexError: 377 | pass 378 | 379 | return obj 380 | 381 | 382 | def update_dict(target, source): 383 | """ 384 | Recursively merge values from a nested dictionary into another nested 385 | dictionary. 386 | 387 | For example: 388 | 389 | >>> target = { 390 | ... 'thing': 123, 391 | ... 'thang': { 392 | ... 'a': 1, 393 | ... 'b': 2 394 | ... } 395 | ... } 396 | >>> source = { 397 | ... 'thang': { 398 | ... 'a': 666, 399 | ... 'c': 777 400 | ... } 401 | ... } 402 | >>> update_dict(target, source) 403 | >>> target 404 | { 405 | 'thing': 123, 406 | 'thang': { 407 | 'a': 666, 408 | 'b': 2, 409 | 'c': 777 410 | } 411 | } 412 | """ 413 | for k,v in source.items(): 414 | if isinstance(v, dict) and k in target and isinstance(source[k], dict): 415 | update_dict(target[k], v) 416 | else: 417 | target[k] = v 418 | 419 | -------------------------------------------------------------------------------- /scruffy/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | File 3 | ---- 4 | 5 | Classes for representing and performing operations on files and directories. 6 | """ 7 | from __future__ import unicode_literals 8 | import os 9 | from six import string_types 10 | import yaml 11 | import copy 12 | import logging 13 | import logging.config 14 | import inspect 15 | import pkg_resources 16 | import shutil 17 | 18 | from .plugin import PluginManager 19 | 20 | 21 | class File(object): 22 | """ 23 | Represents a file that may or may not exist on the filesystem. 24 | 25 | Usually encapsulated by a Directory or an Environment. 26 | """ 27 | def __init__(self, path=None, create=False, cleanup=False, parent=None): 28 | super(File, self).__init__() 29 | self._parent = parent 30 | self._fpath = path 31 | self._create = create 32 | self._cleanup = cleanup 33 | 34 | if self._fpath: 35 | self._fpath = os.path.expanduser(self._fpath) 36 | 37 | def __enter__(self): 38 | self.prepare() 39 | return self 40 | 41 | def __exit__(self, type, value, traceback): 42 | self.cleanup() 43 | 44 | def __str__(self): 45 | return self.path 46 | 47 | def apply_config(self, applicator): 48 | """ 49 | Replace any config tokens in the file's path with values from the config. 50 | """ 51 | if isinstance(self._fpath, string_types): 52 | self._fpath = applicator.apply(self._fpath) 53 | 54 | def create(self): 55 | """ 56 | Create the file if it doesn't already exist. 57 | """ 58 | open(self.path, 'a').close() 59 | 60 | def remove(self): 61 | """ 62 | Remove the file if it exists. 63 | """ 64 | if self.exists: 65 | os.unlink(self.path) 66 | 67 | def prepare(self): 68 | """ 69 | Prepare the file for use in an Environment or Directory. 70 | 71 | This will create the file if the create flag is set. 72 | """ 73 | if self._create: 74 | self.create() 75 | 76 | def cleanup(self): 77 | """ 78 | Clean up the file after use in an Environment or Directory. 79 | 80 | This will remove the file if the cleanup flag is set. 81 | """ 82 | if self._cleanup: 83 | self.remove() 84 | 85 | @property 86 | def path(self): 87 | """ 88 | Get the path to the file relative to its parent. 89 | """ 90 | if self._parent: 91 | return os.path.join(self._parent.path, self._fpath) 92 | else: 93 | return self._fpath 94 | 95 | @property 96 | def name(self): 97 | """ 98 | Get the file name. 99 | """ 100 | return os.path.basename(self.path) 101 | 102 | @property 103 | def ext(self): 104 | """ 105 | Get the file's extension. 106 | """ 107 | return os.path.splitext(self.path)[1] 108 | 109 | @property 110 | def content(self): 111 | """ 112 | Property for the content of the file. 113 | """ 114 | return self.read() 115 | 116 | @property 117 | def exists(self): 118 | """ 119 | Whether or not the file exists. 120 | """ 121 | return self.path and os.path.exists(self.path) 122 | 123 | def read(self): 124 | """ 125 | Read and return the contents of the file. 126 | """ 127 | with open(self.path) as f: 128 | d = f.read() 129 | return d 130 | 131 | def write(self, data, mode='w'): 132 | """ 133 | Write data to the file. 134 | 135 | `data` is the data to write 136 | `mode` is the mode argument to pass to `open()` 137 | """ 138 | with open(self.path, mode) as f: 139 | f.write(data) 140 | 141 | 142 | class LogFile(File): 143 | """ 144 | A log file to configure with Python's logging module. 145 | """ 146 | def __init__(self, path=None, logger=None, loggers=[], formatter={}, format=None, *args, **kwargs): 147 | super(LogFile, self).__init__(path=path, *args, **kwargs) 148 | self._create = True 149 | self._cleanup = True 150 | self._formatter = formatter 151 | self._format = format 152 | 153 | if logger: 154 | self._loggers = [logger] 155 | else: 156 | self._loggers = loggers 157 | 158 | def prepare(self): 159 | """ 160 | Configure the log file. 161 | """ 162 | self.configure() 163 | 164 | def configure(self): 165 | """ 166 | Configure the Python logging module for this file. 167 | """ 168 | # build a file handler for this file 169 | handler = logging.FileHandler(self.path, delay=True) 170 | 171 | # if we got a format string, create a formatter with it 172 | if self._format: 173 | handler.setFormatter(logging.Formatter(self._format)) 174 | 175 | # if we got a string for the formatter, assume it's the name of a 176 | # formatter in the environment's config 177 | if isinstance(self._format, string_types): 178 | if self._env and self._env.config.logging.dict_config.formatters[self._formatter]: 179 | d = self._env.config.logging.dict_config.formatters[self._formatter].to_dict() 180 | handler.setFormatter(logging.Formatter(**d)) 181 | elif type(self._formatter) == dict: 182 | # if it's a dict it must be the actual formatter params 183 | handler.setFormatter(logging.Formatter(**self._formatter)) 184 | 185 | # add the file handler to whatever loggers were specified 186 | if len(self._loggers): 187 | for name in self._loggers: 188 | logging.getLogger(name).addHandler(handler) 189 | else: 190 | # none specified, just add it to the root logger 191 | logging.getLogger().addHandler(handler) 192 | 193 | 194 | class LockFile(File): 195 | """ 196 | A file that is automatically created and cleaned up. 197 | """ 198 | def __init__(self, *args, **kwargs): 199 | super(LockFile, self).__init__(*args, **kwargs) 200 | self._create = True 201 | self._cleanup = True 202 | 203 | def create(self): 204 | """ 205 | Create the file. 206 | 207 | If the file already exists an exception will be raised 208 | """ 209 | if not os.path.exists(self.path): 210 | open(self.path, 'a').close() 211 | else: 212 | raise Exception("File exists: {}".format(self.path)) 213 | 214 | 215 | class YamlFile(File): 216 | """ 217 | A yaml file that is parsed into a dictionary. 218 | """ 219 | @property 220 | def content(self): 221 | """ 222 | Parse the file contents into a dictionary. 223 | """ 224 | return yaml.safe_load(self.read()) 225 | 226 | 227 | class JsonFile(YamlFile): 228 | """ 229 | A json file that is parsed into a dictionary. 230 | """ 231 | 232 | 233 | class PackageFile(File): 234 | """ 235 | A file whose path is relative to a Python package. 236 | """ 237 | def __init__(self, path=None, create=False, cleanup=False, parent=None, package=None): 238 | super(PackageFile, self).__init__(path=path, create=create, cleanup=cleanup, parent=PackageDirectory(package=package)) 239 | 240 | 241 | class Directory(object): 242 | """ 243 | A filesystem directory. 244 | 245 | A Scruffy Environment usually encompasses a number of these. For example, 246 | the main Directory object may represent `~/.myproject`. 247 | 248 | >>> d = Directory({ 249 | ... path='~/.myproject', 250 | ... create=True, 251 | ... cleanup=False, 252 | ... children=[ 253 | ... ... 254 | ... ] 255 | ... }) 256 | 257 | `path` can be either a string representing the path to the directory, or 258 | a nested Directory object. If a Directory object is passed as the `path` 259 | its path will be requested instead. This is so Directory objects can be 260 | wrapped in others to inherit their properties. 261 | """ 262 | def __init__(self, path=None, base=None, create=True, cleanup=False, parent=None, **kwargs): 263 | self._path = path 264 | self._base = base 265 | self._create = create 266 | self._cleanup = cleanup 267 | self._pm = PluginManager() 268 | self._children = {} 269 | self._env = None 270 | self._parent = parent 271 | 272 | if self._path and isinstance(self._path, string_types): 273 | self._path = os.path.expanduser(self._path) 274 | 275 | self.add(**kwargs) 276 | 277 | def __enter__(self): 278 | self.create() 279 | return self 280 | 281 | def __exit__(self, type, value, traceback): 282 | self.cleanup() 283 | 284 | def __getitem__(self, key): 285 | return self._children[key] 286 | 287 | def __getattr__(self, key): 288 | return self._children[key] 289 | 290 | def apply_config(self, applicator): 291 | """ 292 | Replace any config tokens with values from the config. 293 | """ 294 | if isinstance(self._path, string_types): 295 | self._path = applicator.apply(self._path) 296 | 297 | for key in self._children: 298 | self._children[key].apply_config(applicator) 299 | 300 | @property 301 | def path(self): 302 | """ 303 | Return the path to this directory. 304 | """ 305 | p = '' 306 | 307 | if self._parent and self._parent.path: 308 | p = os.path.join(p, self._parent.path) 309 | if self._base: 310 | p = os.path.join(p, self._base) 311 | if self._path: 312 | p = os.path.join(p, self._path) 313 | 314 | return p 315 | 316 | def create(self): 317 | """ 318 | Create the directory. 319 | 320 | Directory will only be created if the create flag is set. 321 | """ 322 | if not self.exists: 323 | os.mkdir(self.path) 324 | 325 | def remove(self, recursive=True, ignore_error=True): 326 | """ 327 | Remove the directory. 328 | """ 329 | try: 330 | if recursive or self._cleanup == 'recursive': 331 | shutil.rmtree(self.path) 332 | else: 333 | os.rmdir(self.path) 334 | except Exception as e: 335 | if not ignore_error: 336 | raise e 337 | 338 | def prepare(self): 339 | """ 340 | Prepare the Directory for use in an Environment. 341 | 342 | This will create the directory if the create flag is set. 343 | """ 344 | if self._create: 345 | self.create() 346 | for k in self._children: 347 | self._children[k]._env = self._env 348 | self._children[k].prepare() 349 | 350 | def cleanup(self): 351 | """ 352 | Clean up children and remove the directory. 353 | 354 | Directory will only be removed if the cleanup flag is set. 355 | """ 356 | for k in self._children: 357 | self._children[k].cleanup() 358 | 359 | if self._cleanup: 360 | self.remove(True) 361 | 362 | def path_to(self, path): 363 | """ 364 | Find the path to something inside this directory. 365 | """ 366 | return os.path.join(self.path, str(path)) 367 | 368 | @property 369 | def exists(self): 370 | """ 371 | Check if the directory exists. 372 | """ 373 | return os.path.exists(self.path) 374 | 375 | def list(self): 376 | """ 377 | List the contents of the directory. 378 | """ 379 | return [File(f, parent=self) for f in os.listdir(self.path)] 380 | 381 | def write(self, filename, data, mode='w'): 382 | """ 383 | Write to a file in the directory. 384 | """ 385 | with open(self.path_to(str(filename)), mode) as f: 386 | f.write(data) 387 | 388 | def read(self, filename): 389 | """ 390 | Read a file from the directory. 391 | """ 392 | with open(self.path_to(str(filename))) as f: 393 | d = f.read() 394 | return d 395 | 396 | def add(self, *args, **kwargs): 397 | """ 398 | Add objects to the directory. 399 | """ 400 | for key in kwargs: 401 | if isinstance(kwargs[key], string_types): 402 | self._children[key] = File(kwargs[key]) 403 | else: 404 | self._children[key] = kwargs[key] 405 | self._children[key]._parent = self 406 | self._children[key]._env = self._env 407 | 408 | added = [] 409 | for arg in args: 410 | if isinstance(arg, File): 411 | self._children[arg.name] = arg 412 | self._children[arg.name]._parent = self 413 | self._children[arg.name]._env = self._env 414 | elif isinstance(arg, string_types): 415 | f = File(arg) 416 | added.append(f) 417 | self._children[arg] = f 418 | self._children[arg]._parent = self 419 | self._children[arg]._env = self._env 420 | else: 421 | raise TypeError(type(arg)) 422 | 423 | # if we were passed a single file/filename, return the File object for convenience 424 | if len(added) == 1: 425 | return added[0] 426 | if len(args) == 1: 427 | return args[0] 428 | 429 | 430 | class PluginDirectory(Directory): 431 | """ 432 | A filesystem directory containing plugins. 433 | """ 434 | def prepare(self): 435 | """ 436 | Preparing a plugin directory just loads the plugins. 437 | """ 438 | super(PluginDirectory, self).prepare() 439 | self.load() 440 | 441 | def load(self): 442 | """ 443 | Load the plugins in this directory. 444 | """ 445 | self._pm.load_plugins(self.path) 446 | 447 | 448 | class PackageDirectory(Directory): 449 | """ 450 | A filesystem directory relative to a Python package. 451 | """ 452 | def __init__(self, path=None, package=None, *args, **kwargs): 453 | super(PackageDirectory, self).__init__(path=path, *args, **kwargs) 454 | 455 | # if we weren't passed a package name, walk up the stack and find the first non-scruffy package 456 | if not package: 457 | frame = inspect.currentframe() 458 | while frame: 459 | if frame.f_globals['__package__'] != 'scruffy': 460 | package = frame.f_globals['__package__'] 461 | break 462 | frame = frame.f_back 463 | 464 | # if we found a package, set the path directory to the base dir for the package 465 | if package: 466 | self._base = pkg_resources.resource_filename(package, '') 467 | else: 468 | raise Exception('No package found') 469 | --------------------------------------------------------------------------------